aboutsummaryrefslogtreecommitdiff
path: root/clients
diff options
context:
space:
mode:
Diffstat (limited to 'clients')
-rw-r--r--clients/bokftui.c1431
1 files changed, 1390 insertions, 41 deletions
diff --git a/clients/bokftui.c b/clients/bokftui.c
index 5b68460..06ccfe8 100644
--- a/clients/bokftui.c
+++ b/clients/bokftui.c
@@ -9,6 +9,7 @@
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
+#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
@@ -70,6 +71,8 @@ struct app {
char role[32];
int64_t fy;
int64_t voucher_sel; /* last selected voucher id in the list view */
+ int64_t invoice_sel; /* last selected invoice id in the list view */
+ int64_t customer_sel; /* last selected customer id in the list view */
char default_series[16];
char attachment_dir[256];
long max_attachment_bytes;
@@ -162,6 +165,7 @@ static void bokslut_screen(struct app *a);
static void arsredovisning_screen(struct app *a);
static char *rpc_dry(struct app *a, const char *cmd, const char *args);
static int64_t vouchers_new(struct app *a);
+static void config_mkdirs(const char *path);
/* Whole kronor with space thousands, öre truncated like the blankett. */
static void kr_whole(int64_t ore, char *buf, size_t n)
@@ -4600,10 +4604,37 @@ static void company_screen(struct app *a)
}
}
-static const char *const SETTING_KEYS[] = { "default_series",
- "attachment_dir" };
-static const char *const SETTING_LABELS[] = { "Standardserie",
- "Bilagornas mapp" };
+static const char *const SETTING_KEYS[] = {
+ "default_series",
+ "attachment_dir",
+ "invoice_bankgiro",
+ "invoice_our_ref",
+ "invoice_receivable_account",
+ "invoice_revenue_account",
+ "smtp_host",
+ "smtp_port",
+ "smtp_user",
+ "smtp_from",
+ "smtp_reply_to",
+ "smtp_security",
+ "smtp_password",
+};
+static const char *const SETTING_LABELS[] = {
+ "Standardserie",
+ "Bilagornas mapp",
+ "Bankgiro",
+ "Vår referens",
+ "Fordringskonto",
+ "Intäktskonto",
+ "SMTP-server",
+ "SMTP-port",
+ "SMTP-användare",
+ "SMTP-avsändare",
+ "SMTP-svarsadress",
+ "SMTP-säkerhet",
+ "SMTP-lösenord",
+};
+static const char *const SECURITY_CHOICES[] = { "starttls", "tls", "plain" };
static void settings_screen(struct app *a)
{
@@ -4619,9 +4650,15 @@ static void settings_screen(struct app *a)
free(resp);
return;
}
- char *vals[8];
- struct tui_form_field ff[8];
+ int pw_set = jbool_val(resp, "result.smtp_password_set", 0);
+ char pwstatus[192];
+ snprintf(pwstatus, sizeof pwstatus,
+ "SMTP-lösenord (lämna tomt för oförändrat): %s",
+ pw_set ? "satt" : "inte satt");
+ char *vals[16];
+ struct tui_form_field ff[16];
memset(ff, 0, sizeof ff);
+ int sec_choice = 0;
for (int i = 0; i < nset; i++) {
char path[64], *v;
vals[i] = xcalloc(512, 1);
@@ -4629,39 +4666,64 @@ static void settings_screen(struct app *a)
v = jstr_dup(resp, path);
snprintf(vals[i], 512, "%s", v ? v : "");
free(v);
+ const char *key = SETTING_KEYS[i];
ff[i].label = SETTING_LABELS[i];
ff[i].value = vals[i];
ff[i].cap = 512;
ff[i].kind = TUI_F_TEXT;
+ if (strcmp(key, "smtp_security") == 0) {
+ ff[i].kind = TUI_F_CHOICE;
+ ff[i].choices = SECURITY_CHOICES;
+ ff[i].nchoices = 3;
+ ff[i].choice = &sec_choice;
+ for (int k = 0; k < 3; k++)
+ if (strcmp(vals[i], SECURITY_CHOICES[k]) == 0)
+ sec_choice = k;
+ } else if (strcmp(key, "smtp_password") == 0) {
+ ff[i].mask = 1;
+ }
}
free(resp);
- int r = tui_form_run("Inställningar", ff, nset, 1, &sel);
- if (r >= 0) {
- yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
- yyjson_mut_val *o = yyjson_mut_obj(d);
- yyjson_mut_doc_set_root(d, o);
- yyjson_mut_obj_add_strcpy(d, o, "key", SETTING_KEYS[r]);
- yyjson_mut_obj_add_strcpy(d, o, "value", vals[r]);
- char *args = yyjson_mut_write(d, 0, NULL);
- yyjson_mut_doc_free(d);
- char *rr = client_rpc(&a->conn, "settings.set", a->session,
- a->org, args);
- free(args);
- if (rr && client_ok(rr)) {
- char *nv = jstr_dup(rr, "result.value");
- if (nv) {
- if (strcmp(SETTING_KEYS[r], "default_series") == 0)
- snprintf(a->default_series, sizeof a->default_series,
- "%s", nv);
- else if (strcmp(SETTING_KEYS[r], "attachment_dir") == 0)
+ struct tui_form_action acts[1];
+ memset(acts, 0, sizeof acts);
+ acts[0].label = pwstatus;
+ acts[0].enabled = -1; /* dim status row, not selectable */
+ int r = tui_form_run_actions(
+ "Inställningar", ff, nset, 1, acts, 1,
+ "upp/ned/Home/End Enter = ändra F5 = uppdatera"
+ " Esc/q = tillbaka ^C = avsluta",
+ &sel);
+ if (r >= 0 && r < nset) {
+ const char *key = SETTING_KEYS[r];
+ const char *val = vals[r];
+ if (strcmp(key, "smtp_security") == 0)
+ val = SECURITY_CHOICES[sec_choice];
+ if (strcmp(key, "smtp_password") == 0 && !*vals[r]) {
+ /* empty means "keep the stored secret" */
+ } else {
+ yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val *o = yyjson_mut_obj(d);
+ yyjson_mut_doc_set_root(d, o);
+ yyjson_mut_obj_add_strcpy(d, o, "key", key);
+ yyjson_mut_obj_add_strcpy(d, o, "value", val);
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ char *rr = client_rpc(&a->conn, "settings.set", a->session,
+ a->org, args);
+ free(args);
+ if (rr && client_ok(rr)) {
+ snprintf(vals[r], 512, "%s", val);
+ if (strcmp(key, "default_series") == 0)
+ snprintf(a->default_series,
+ sizeof a->default_series, "%s", val);
+ else if (strcmp(key, "attachment_dir") == 0)
snprintf(a->attachment_dir,
- sizeof a->attachment_dir, "%s", nv);
- free(nv);
+ sizeof a->attachment_dir, "%s", val);
+ } else {
+ show_error("Kunde inte spara inställningen", rr);
}
- } else {
- show_error("Kunde inte spara inställningen", rr);
+ free(rr);
}
- free(rr);
}
for (int i = 0; i < nset; i++)
free(vals[i]);
@@ -4670,6 +4732,1281 @@ static void settings_screen(struct app *a)
}
}
+/* ------------------------------------------------------------------ */
+/* invoicing: Fakturor, Kunder */
+/* ------------------------------------------------------------------ */
+
+static void today_iso(char *buf, size_t n)
+{
+ time_t now = time(NULL);
+ struct tm tm;
+ localtime_r(&now, &tm);
+ char tmp[64];
+ snprintf(tmp, sizeof tmp, "%04d-%02d-%02d", tm.tm_year + 1900,
+ tm.tm_mon + 1, tm.tm_mday);
+ size_t len = strlen(tmp);
+ if (len >= n)
+ len = n ? n - 1 : 0;
+ memcpy(buf, tmp, len);
+ if (n)
+ buf[len] = '\0';
+}
+
+/* Directory for generated invoice PDFs. */
+static void invoice_cache_dir(char *buf, size_t n)
+{
+ const char *cache = getenv("XDG_CACHE_HOME");
+ if (cache && *cache) {
+ snprintf(buf, n, "%s/bokf", cache);
+ return;
+ }
+ snprintf(buf, n, "/tmp");
+}
+
+/* Keep a customer-supplied name from escaping the directory. */
+static void safe_fname(const char *in, char *out, size_t n)
+{
+ size_t o = 0;
+ for (const char *p = in ? in : ""; *p && o + 1 < n; p++) {
+ unsigned char c = (unsigned char)*p;
+ if (c < 32 || c == '/')
+ c = '-';
+ out[o++] = (char)c;
+ }
+ out[o] = '\0';
+}
+
+static int write_pdf_b64(const char *b64, const char *path)
+{
+ unsigned char *data = NULL;
+ size_t len = 0;
+ if (!b64 || util_b64_decode(b64, strlen(b64), &data, &len) != 0)
+ return -1;
+ FILE *fp = fopen(path, "wb");
+ if (!fp) {
+ free(data);
+ return -1;
+ }
+ size_t wrote = fwrite(data, 1, len, fp);
+ fclose(fp);
+ free(data);
+ return wrote == len ? 0 : -1;
+}
+
+/* Open a saved PDF with xdg-open when a desktop session is present;
+ otherwise (or on fork failure) show where it was saved. */
+static void open_pdf(const char *path)
+{
+ const char *display = getenv("DISPLAY");
+ const char *wayland = getenv("WAYLAND_DISPLAY");
+ if ((!display || !*display) && (!wayland || !*wayland)) {
+ tui_message("Faktura PDF", "Sparad: %s", path);
+ return;
+ }
+ pid_t pid = fork();
+ if (pid == 0) {
+ pid_t gc = fork();
+ if (gc == 0) {
+ setsid();
+ execlp("xdg-open", "xdg-open", path, (char *)NULL);
+ _exit(127);
+ }
+ _exit(gc < 0 ? 1 : 0);
+ }
+ if (pid > 0)
+ waitpid(pid, NULL, 0);
+ else
+ tui_message("Faktura PDF", "Sparad: %s", path);
+}
+
+static void save_and_open_pdf(const char *b64, const char *filename)
+{
+ char dir[512], path[700], safe[600];
+ invoice_cache_dir(dir, sizeof dir);
+ safe_fname(filename, safe, sizeof safe);
+ if (snprintf(path, sizeof path, "%s/%s", dir, safe) >= (int)sizeof path) {
+ tui_message("Faktura PDF", "Sökvägen blir för lång.");
+ return;
+ }
+ config_mkdirs(path);
+ if (write_pdf_b64(b64, path) != 0) {
+ tui_message("Faktura PDF", "Kunde inte spara %s", path);
+ return;
+ }
+ open_pdf(path);
+}
+
+static const char *invoice_status_sv(const char *s)
+{
+ if (s && strcmp(s, "credited") == 0)
+ return "krediterad";
+ return "utfärdad";
+}
+
+static void qty_sv(int64_t milli, char *buf, size_t n)
+{
+ int64_t whole = milli / 1000;
+ int64_t frac = milli % 1000;
+ char tmp[64];
+ if (frac == 0) {
+ snprintf(tmp, sizeof tmp, "%lld", (long long)whole);
+ } else {
+ if (frac < 0)
+ frac = -frac;
+ char fr[16];
+ snprintf(fr, sizeof fr, "%03lld", (long long)frac);
+ while (fr[0] && fr[strlen(fr) - 1] == '0')
+ fr[strlen(fr) - 1] = '\0';
+ snprintf(tmp, sizeof tmp, "%lld,%s", (long long)whole, fr);
+ }
+ size_t len = strlen(tmp);
+ if (len >= n)
+ len = n ? n - 1 : 0;
+ memcpy(buf, tmp, len);
+ if (n)
+ buf[len] = '\0';
+}
+
+static const char *vat_label(const char *code)
+{
+ if (!code)
+ return "";
+ if (strcmp(code, "25") == 0)
+ return "25%";
+ if (strcmp(code, "12") == 0)
+ return "12%";
+ if (strcmp(code, "6") == 0)
+ return "6%";
+ if (strcmp(code, "0") == 0)
+ return "0%";
+ if (strcmp(code, "rc") == 0)
+ return "Omvänd moms";
+ if (strcmp(code, "eu") == 0)
+ return "EU";
+ return code;
+}
+
+static const char *vat_code_of(const char *s)
+{
+ if (!s || !*s)
+ return "25";
+ if (strcmp(s, "25") == 0 || strcmp(s, "25%") == 0)
+ return "25";
+ if (strcmp(s, "12") == 0 || strcmp(s, "12%") == 0)
+ return "12";
+ if (strcmp(s, "6") == 0 || strcmp(s, "6%") == 0)
+ return "6";
+ if (strcmp(s, "0") == 0 || strcmp(s, "0%") == 0)
+ return "0";
+ if (strcasecmp(s, "rc") == 0 || strcasecmp(s, "Omvänd moms") == 0)
+ return "rc";
+ if (strcasecmp(s, "eu") == 0 || strcasecmp(s, "EU") == 0)
+ return "eu";
+ return NULL;
+}
+
+static const char *unit_code_of(const char *s)
+{
+ if (!s || !*s)
+ return "st";
+ if (strcasecmp(s, "st") == 0)
+ return "st";
+ if (strcasecmp(s, "tim") == 0)
+ return "tim";
+ if (strcasecmp(s, "dag") == 0)
+ return "dag";
+ if (strcasecmp(s, "km") == 0)
+ return "km";
+ return NULL;
+}
+
+static int unit_index(const char *s)
+{
+ static const char *const opts[] = { "st", "tim", "dag", "km" };
+ if (s && *s) {
+ for (int i = 0; i < 4; i++)
+ if (strcmp(s, opts[i]) == 0)
+ return i;
+ }
+ return 0;
+}
+
+static int qty_is_valid(const char *s)
+{
+ if (!s || !*s)
+ return 0;
+ int sep = 0, frac = 0, intd = 0, nonzero = 0;
+ for (const char *p = s; *p; p++) {
+ if (*p == ',' || *p == '.') {
+ if (sep)
+ return 0;
+ sep = 1;
+ continue;
+ }
+ if (*p < '0' || *p > '9')
+ return 0;
+ if (*p != '0')
+ nonzero = 1;
+ if (sep) {
+ frac++;
+ if (frac > 3)
+ return 0;
+ } else if (++intd > 15) {
+ return 0;
+ }
+ }
+ if (!intd || (sep && frac == 0))
+ return 0;
+ return nonzero;
+}
+
+/* ---------------------------- Kunder ------------------------------ */
+
+static int customer_save(struct app *a, int64_t id, const char *name,
+ const char *address, const char *postal,
+ const char *city, const char *vat, const char *email,
+ const char *your_ref, const char *payment,
+ const char *notes, int dry)
+{
+ if (!name || !*name) {
+ tui_message("Kund", "Namn krävs.");
+ return 0;
+ }
+ if (!payment || !*payment) {
+ tui_message("Kund", "Betalningsdagar krävs.");
+ return 0;
+ }
+ for (const char *p = payment; *p; p++) {
+ if (*p < '0' || *p > '9') {
+ tui_message("Kund", "Betalningsdagar ska vara ett heltal.");
+ return 0;
+ }
+ }
+ yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val *o = yyjson_mut_obj(d);
+ yyjson_mut_doc_set_root(d, o);
+ if (id > 0)
+ yyjson_mut_obj_add_int(d, o, "id", id);
+ yyjson_mut_obj_add_strcpy(d, o, "name", name);
+ yyjson_mut_obj_add_strcpy(d, o, "address", address);
+ yyjson_mut_obj_add_strcpy(d, o, "postal_code", postal);
+ yyjson_mut_obj_add_strcpy(d, o, "city", city);
+ yyjson_mut_obj_add_strcpy(d, o, "vat_nr", vat);
+ yyjson_mut_obj_add_strcpy(d, o, "email", email);
+ yyjson_mut_obj_add_strcpy(d, o, "your_ref", your_ref);
+ yyjson_mut_obj_add_int(d, o, "payment_days", strtoll(payment, NULL, 10));
+ yyjson_mut_obj_add_strcpy(d, o, "notes", notes);
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ const char *cmd = id > 0 ? "customer.update" : "customer.create";
+ char *r = dry ? rpc_dry(a, cmd, args)
+ : client_rpc(&a->conn, cmd, a->session, a->org, args);
+ free(args);
+ int ok = r && client_ok(r);
+ if (ok)
+ tui_message("Kund", dry ? "Uppgifterna är giltiga." : "Sparat.");
+ else
+ show_error(dry ? "Kunden kunde inte valideras" : "Kunde inte spara",
+ r);
+ free(r);
+ return ok;
+}
+
+static void customer_form(struct app *a, int64_t id)
+{
+ char name[256] = "", address[512] = "", postal[32] = "", city[128] = "";
+ char vat[64] = "", email[256] = "", your_ref[128] = "";
+ char payment[16] = "30", notes[2048] = "";
+ if (id > 0) {
+ char args[64];
+ snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id);
+ char *resp =
+ client_rpc(&a->conn, "customer.get", a->session, a->org, args);
+ if (!resp || !client_ok(resp)) {
+ show_error("Kund", resp);
+ free(resp);
+ return;
+ }
+ struct {
+ const char *key;
+ char *dst;
+ size_t cap;
+ } fields[] = {
+ { "name", name, sizeof name },
+ { "address", address, sizeof address },
+ { "postal_code", postal, sizeof postal },
+ { "city", city, sizeof city },
+ { "vat_nr", vat, sizeof vat },
+ { "email", email, sizeof email },
+ { "your_ref", your_ref, sizeof your_ref },
+ { "notes", notes, sizeof notes },
+ };
+ for (size_t i = 0; i < sizeof fields / sizeof fields[0]; i++) {
+ char path[64], *v;
+ snprintf(path, sizeof path, "result.%s", fields[i].key);
+ v = jstr_dup(resp, path);
+ if (v) {
+ snprintf(fields[i].dst, fields[i].cap, "%s", v);
+ free(v);
+ }
+ }
+ snprintf(payment, sizeof payment, "%lld",
+ (long long)jint_val(resp, "result.payment_days", 30));
+ free(resp);
+ }
+ const char *labels[] = { "Namn", "Adress",
+ "Postnummer", "Ort",
+ "Momsreg.nr", "E-post",
+ "Er referens", "Betalningsdagar",
+ "Anteckningar" };
+ char *vals[] = { name, address, postal, city, vat,
+ email, your_ref, payment, notes };
+ size_t caps[] = { sizeof name, sizeof address, sizeof postal,
+ sizeof city, sizeof vat, sizeof email,
+ sizeof your_ref, sizeof payment, sizeof notes };
+ struct tui_form_field ff[10];
+ memset(ff, 0, sizeof ff);
+ for (int i = 0; i < 9; i++) {
+ ff[i].label = labels[i];
+ ff[i].value = vals[i];
+ ff[i].cap = caps[i];
+ ff[i].kind = TUI_F_TEXT;
+ }
+ ff[9].label = "";
+ ff[9].value = "Spara";
+ ff[9].kind = TUI_F_ACTION;
+ static int sel = 0;
+ for (;;) {
+ int r = tui_form_run(id > 0 ? "Kund" : "Ny kund", ff, 10, 1, &sel);
+ if (r == TUI_FORM_BACK)
+ return;
+ if (r == TUI_FORM_REFRESH) {
+ customer_save(a, id, name, address, postal, city, vat, email,
+ your_ref, payment, notes, 1);
+ continue;
+ }
+ if (r == TUI_FORM_SUBMIT || r == 9) {
+ if (customer_save(a, id, name, address, postal, city, vat, email,
+ your_ref, payment, notes, 0))
+ return;
+ }
+ }
+}
+
+static void customer_archive_toggle(struct app *a, int64_t id, const char *name,
+ int active)
+{
+ if (!tui_confirm("Kund", active ? "Arkivera kund %s?" :
+ "Återaktivera kund %s?",
+ name ? name : ""))
+ return;
+ char args[128];
+ snprintf(args, sizeof args, "{\"id\":%lld,\"active\":%s}", (long long)id,
+ active ? "false" : "true");
+ char *r =
+ client_rpc(&a->conn, "customer.archive", a->session, a->org, args);
+ if (r && client_ok(r))
+ tui_message("Kund", active ? "Arkiverad." : "Återaktiverad.");
+ else
+ show_error("Kund", r);
+ free(r);
+}
+
+static void customers_screen(struct app *a)
+{
+ char **items = NULL;
+ char **names = NULL;
+ int64_t *ids = NULL;
+ unsigned char *active = NULL;
+ size_t n = 0;
+ int fetch = 1;
+ for (;;) {
+ if (g_quit) {
+ for (size_t i = 0; i < n; i++) {
+ free(items[i]);
+ free(names[i]);
+ }
+ free(items);
+ free(names);
+ free(ids);
+ free(active);
+ return;
+ }
+ if (fetch) {
+ for (size_t i = 0; i < n; i++) {
+ free(items[i]);
+ free(names[i]);
+ }
+ free(items);
+ free(names);
+ free(ids);
+ free(active);
+ items = NULL;
+ names = NULL;
+ ids = NULL;
+ active = NULL;
+ n = 0;
+ fetch = 0;
+ char *resp = client_rpc(&a->conn, "customer.list", a->session,
+ a->org, "{}");
+ if (!resp || !client_ok(resp)) {
+ show_error("Kunder", resp);
+ free(resp);
+ return;
+ }
+ n = jarr_size(resp, "result.items");
+ items = xcalloc(n + 1, sizeof(char *));
+ names = xcalloc(n + 1, sizeof(char *));
+ ids = xcalloc(n ? n : 1, sizeof(int64_t));
+ active = xcalloc(n + 1, 1);
+ for (size_t i = 0; i < n; i++) {
+ char path[64], line[768];
+ snprintf(path, sizeof path, "result.items.%zu.id", i);
+ ids[i] = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.items.%zu.active", i);
+ active[i] = (unsigned char)jbool_val(resp, path, 1);
+ snprintf(path, sizeof path, "result.items.%zu.name", i);
+ char *name = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.postal_code", i);
+ char *postal = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.city", i);
+ char *city = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.vat_nr", i);
+ char *vat = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.payment_days", i);
+ int64_t days = jint_val(resp, path, 0);
+ char nbuf[256], pbuf[256], vbuf[80];
+ snprintf(nbuf, sizeof nbuf, "%s", name ? name : "");
+ tui_pad_field(nbuf, sizeof nbuf, 30);
+ snprintf(pbuf, sizeof pbuf, "%s %s", postal ? postal : "",
+ city ? city : "");
+ tui_pad_field(pbuf, sizeof pbuf, 30);
+ snprintf(vbuf, sizeof vbuf, "%s", vat ? vat : "");
+ tui_pad_field(vbuf, sizeof vbuf, 18);
+ snprintf(line, sizeof line, "%s %s %s %3lld dagar %s", nbuf,
+ pbuf, vbuf, (long long)days,
+ active[i] ? "" : "(arkiverad)");
+ items[i] = xstrdup(line);
+ names[i] = xstrdup(name ? name : "");
+ free(name);
+ free(postal);
+ free(city);
+ free(vat);
+ }
+ free(resp);
+ items[n] = xstrdup("+ Ny kund (^N)");
+ }
+ int start = 0;
+ if (a->customer_sel) {
+ for (size_t i = 0; i < n; i++)
+ if (ids[i] == a->customer_sel) {
+ start = (int)i;
+ break;
+ }
+ }
+ int cur = start;
+ int sel = tui_select_list("Kunder", items, (int)n + 1, start, 1, &cur,
+ 1, "arkivera/återaktivera", 0);
+ if (cur >= 0 && (size_t)cur < n)
+ a->customer_sel = ids[cur];
+ if (sel == -1) {
+ for (size_t i = 0; i < n; i++) {
+ free(items[i]);
+ free(names[i]);
+ }
+ free(items);
+ free(names);
+ free(ids);
+ free(active);
+ return;
+ }
+ if (sel == -2) {
+ fetch = 1;
+ continue;
+ }
+ if (sel == -6 && cur >= 0 && (size_t)cur < n) {
+ customer_archive_toggle(a, ids[cur], names[cur],
+ active[cur] != 0);
+ fetch = 1;
+ continue;
+ }
+ if (sel == -4 || (sel >= 0 && (size_t)sel == n)) {
+ customer_form(a, 0);
+ fetch = 1;
+ continue;
+ }
+ if (sel >= 0 && (size_t)sel < n) {
+ customer_form(a, ids[sel]);
+ fetch = 1;
+ continue;
+ }
+ }
+}
+
+/* --------------------------- Fakturor ----------------------------- */
+
+struct invctx {
+ struct app *a;
+ int64_t id;
+ int64_t number;
+ int64_t customer_id;
+ char customer[256];
+};
+
+static void inv_detail_pdf(struct invctx *ctx)
+{
+ char args[64];
+ snprintf(args, sizeof args, "{\"id\":%lld}", (long long)ctx->id);
+ char *resp = client_rpc(&ctx->a->conn, "invoice.pdf", ctx->a->session,
+ ctx->a->org, args);
+ if (!resp || !client_ok(resp)) {
+ show_error("Kunde inte hämta PDF", resp);
+ free(resp);
+ return;
+ }
+ char *b64 = jstr_dup(resp, "result.content_base64");
+ if (b64) {
+ char fname[800];
+ snprintf(fname, sizeof fname, "Faktura %lld %s.pdf",
+ (long long)ctx->number, ctx->customer);
+ save_and_open_pdf(b64, fname);
+ free(b64);
+ }
+ free(resp);
+}
+
+static void inv_detail_send(struct invctx *ctx)
+{
+ struct app *a = ctx->a;
+ char email[256] = "";
+ if (ctx->customer_id > 0) {
+ char cargs[64];
+ snprintf(cargs, sizeof cargs, "{\"id\":%lld}",
+ (long long)ctx->customer_id);
+ char *c =
+ client_rpc(&a->conn, "customer.get", a->session, a->org, cargs);
+ if (c && client_ok(c)) {
+ char *e = jstr_dup(c, "result.email");
+ if (e) {
+ snprintf(email, sizeof email, "%s", e);
+ free(e);
+ }
+ }
+ free(c);
+ }
+ if (!email[0]) {
+ tui_message("Skicka faktura", "Kunden har ingen e-postadress.");
+ return;
+ }
+ if (!tui_confirm("Skicka faktura", "Skicka faktura %lld till %s?",
+ (long long)ctx->number, email))
+ return;
+ char args[64];
+ snprintf(args, sizeof args, "{\"id\":%lld}", (long long)ctx->id);
+ char *r =
+ client_rpc(&a->conn, "invoice.send", a->session, a->org, args);
+ if (r && client_ok(r)) {
+ char *to = jstr_dup(r, "result.sent_to");
+ tui_message("Skickat", "Faktura %lld skickad till %s.",
+ (long long)ctx->number, to && *to ? to : email);
+ free(to);
+ } else {
+ show_error("Skicka faktura", r);
+ }
+ free(r);
+}
+
+static int inv_key(void *ud, int ch)
+{
+ struct invctx *ctx = ud;
+ if (ch == 'p') {
+ inv_detail_pdf(ctx);
+ return TUI_HOOK_STAY;
+ }
+ if (ch == 's') {
+ inv_detail_send(ctx);
+ return TUI_HOOK_STAY;
+ }
+ return TUI_HOOK_NONE;
+}
+
+static void invoices_detail(struct app *a, int64_t id)
+{
+ char args[64];
+ snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id);
+ for (;;) {
+ char *resp =
+ client_rpc(&a->conn, "invoice.get", a->session, a->org, args);
+ if (!resp || !client_ok(resp)) {
+ show_error("Faktura", resp);
+ free(resp);
+ return;
+ }
+ int64_t number = jint_val(resp, "result.number", 0);
+ int64_t customer_id = jint_val(resp, "result.customer_id", 0);
+ int64_t net = jint_val(resp, "result.net_ore", 0);
+ int64_t vat = jint_val(resp, "result.vat_ore", 0);
+ int64_t total = jint_val(resp, "result.total_ore", 0);
+ char *cust = jstr_dup(resp, "result.customer_name");
+ char *status = jstr_dup(resp, "result.status");
+ char *date = jstr_dup(resp, "result.invoice_date");
+ char *due = jstr_dup(resp, "result.due_date");
+ char *delivery = jstr_dup(resp, "result.delivery_date");
+ char *your_ref = jstr_dup(resp, "result.your_ref");
+ char *our_ref = jstr_dup(resp, "result.our_ref");
+ char *notes = jstr_dup(resp, "result.notes");
+ char *ocr = jstr_dup(resp, "result.ocr");
+ char *sent_at = jstr_dup(resp, "result.last_sent_at");
+ char *sent_to = jstr_dup(resp, "result.last_sent_to");
+ struct buf t;
+ buf_init(&t);
+ buf_line(&t, MK_HEAD "Faktura %lld %s\n", (long long)number,
+ invoice_status_sv(status));
+ buf_line(&t, MK_DIM "%s\n\n", cust ? cust : "");
+ buf_line(&t, "Fakturadatum: %s\n", date ? date : "");
+ buf_line(&t, "Förfallodatum: %s\n", due ? due : "");
+ if (delivery && *delivery)
+ buf_line(&t, "Leveransdatum: %s\n", delivery);
+ if (your_ref && *your_ref)
+ buf_line(&t, "Er referens: %s\n", your_ref);
+ if (our_ref && *our_ref)
+ buf_line(&t, "Vår referens: %s\n", our_ref);
+ if (ocr && *ocr)
+ buf_line(&t, "OCR: %s\n", ocr);
+ if (sent_at && *sent_at)
+ buf_line(&t, "Skickad: %s%s%s\n", sent_at,
+ sent_to && *sent_to ? " till " : "",
+ sent_to && *sent_to ? sent_to : "");
+ if (notes && *notes)
+ buf_line(&t, "\nFritext:\n%s\n", notes);
+ buf_line(&t, "\n");
+ buf_line(&t,
+ MK_STICKY MK_HEAD
+ "%-34s %8s %-6s %12s %-13s %-18s %12s\n",
+ "Beskrivning", "Antal", "Enhet", "À-pris", "Moms", "Anm",
+ "Belopp");
+ size_t nrows = jarr_size(resp, "result.rows");
+ for (size_t i = 0; i < nrows; i++) {
+ char path[64];
+ snprintf(path, sizeof path, "result.rows.%zu.description", i);
+ char *desc = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.rows.%zu.quantity_milli", i);
+ int64_t qm = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.rows.%zu.unit", i);
+ char *unit = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.rows.%zu.unit_price_ore", i);
+ int64_t price = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.rows.%zu.vat_code", i);
+ char *vat_code = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.rows.%zu.note", i);
+ char *note = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.rows.%zu.amount_ore", i);
+ int64_t amount = jint_val(resp, path, 0);
+ char dbuf[256], qbuf[24], qcol[24], ubuf[24], pcol[40],
+ vbuf[24], nbuf[128], acol[40];
+ snprintf(dbuf, sizeof dbuf, "%s", desc ? desc : "");
+ tui_pad_field(dbuf, sizeof dbuf, 34);
+ qty_sv(qm, qbuf, sizeof qbuf);
+ tui_pad_hdr(qcol, sizeof qcol, 8, qbuf);
+ snprintf(ubuf, sizeof ubuf, "%s", unit ? unit : "");
+ tui_pad_field(ubuf, sizeof ubuf, 6);
+ tui_amt_col(pcol, sizeof pcol, 12, price);
+ snprintf(vbuf, sizeof vbuf, "%s", vat_label(vat_code));
+ tui_pad_field(vbuf, sizeof vbuf, 13);
+ snprintf(nbuf, sizeof nbuf, "%s", note ? note : "");
+ tui_pad_field(nbuf, sizeof nbuf, 18);
+ tui_amt_col(acol, sizeof acol, 12, amount);
+ buf_line(&t, "%s %s %s %s %s %s %s\n", dbuf, qcol, ubuf, pcol,
+ vbuf, nbuf, acol);
+ free(desc);
+ free(unit);
+ free(vat_code);
+ free(note);
+ }
+ char netc[40], vatc[40], totc[40];
+ tui_amt_col(netc, sizeof netc, 14, net);
+ tui_amt_col(vatc, sizeof vatc, 14, vat);
+ tui_amt_col(totc, sizeof totc, 14, total);
+ buf_line(&t, "\n%46s %14s\n", "Netto", netc);
+ buf_line(&t, "%46s %14s\n", "Moms", vatc);
+ buf_line(&t, MK_HEAD "%46s %14s\n", "Att betala", totc);
+ buf_append(&t, "\0", 1);
+ struct invctx ctx;
+ memset(&ctx, 0, sizeof ctx);
+ ctx.a = a;
+ ctx.id = id;
+ ctx.number = number;
+ ctx.customer_id = customer_id;
+ snprintf(ctx.customer, sizeof ctx.customer, "%s", cust ? cust : "");
+ int ret = tui_pager_hook("Faktura", (const char *)t.p,
+ "p = visa PDF s = skicka", 0, inv_key,
+ &ctx);
+ free(cust);
+ free(status);
+ free(date);
+ free(due);
+ free(delivery);
+ free(your_ref);
+ free(our_ref);
+ free(notes);
+ free(ocr);
+ free(sent_at);
+ free(sent_to);
+ buf_free(&t);
+ free(resp);
+ if (ret != 1)
+ return;
+ }
+}
+
+#define IFORM_ROWS 64
+
+struct irow {
+ char description[128];
+ char quantity[16];
+ char unit[8];
+ char price[24];
+ char vat[16];
+ char note[64];
+};
+
+struct iform {
+ struct app *a;
+ struct tui_rt rt;
+ struct irow rows[IFORM_ROWS];
+ char date[16];
+ char due[16];
+ char delivery[16];
+ char your_ref[64];
+ char our_ref[64];
+ char notes[256];
+ char auto_due[16];
+ char auto_ref[64];
+ size_t ncust;
+ int64_t *cust_ids;
+ int *cust_days;
+ char **cust_names;
+ char **cust_emails;
+ char **cust_refs;
+ int cust_sel;
+ int issued;
+};
+
+static void iform_customers_free(struct iform *f)
+{
+ for (size_t i = 0; i < f->ncust; i++) {
+ free(f->cust_names[i]);
+ free(f->cust_emails[i]);
+ free(f->cust_refs[i]);
+ }
+ free(f->cust_names);
+ free(f->cust_emails);
+ free(f->cust_refs);
+ free(f->cust_ids);
+ free(f->cust_days);
+ f->cust_names = NULL;
+ f->cust_emails = NULL;
+ f->cust_refs = NULL;
+ f->cust_ids = NULL;
+ f->cust_days = NULL;
+ f->ncust = 0;
+}
+
+static int iform_load_customers(struct iform *f)
+{
+ char *resp = client_rpc(&f->a->conn, "customer.list", f->a->session,
+ f->a->org, "{\"active_only\":true}");
+ if (!resp || !client_ok(resp)) {
+ show_error("Kunder", resp);
+ free(resp);
+ return -1;
+ }
+ size_t n = jarr_size(resp, "result.items");
+ f->cust_ids = xcalloc(n ? n : 1, sizeof(int64_t));
+ f->cust_days = xcalloc(n ? n : 1, sizeof(int));
+ f->cust_names = xcalloc(n ? n : 1, sizeof(char *));
+ f->cust_emails = xcalloc(n ? n : 1, sizeof(char *));
+ f->cust_refs = xcalloc(n ? n : 1, sizeof(char *));
+ for (size_t i = 0; i < n; i++) {
+ char path[64];
+ snprintf(path, sizeof path, "result.items.%zu.id", i);
+ f->cust_ids[i] = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.items.%zu.payment_days", i);
+ f->cust_days[i] = (int)jint_val(resp, path, 30);
+ snprintf(path, sizeof path, "result.items.%zu.name", i);
+ char *nm = jstr_dup(resp, path);
+ f->cust_names[i] = nm ? nm : xstrdup("");
+ snprintf(path, sizeof path, "result.items.%zu.email", i);
+ char *em = jstr_dup(resp, path);
+ f->cust_emails[i] = em ? em : xstrdup("");
+ snprintf(path, sizeof path, "result.items.%zu.your_ref", i);
+ char *rf = jstr_dup(resp, path);
+ f->cust_refs[i] = rf ? rf : xstrdup("");
+ }
+ f->ncust = n;
+ free(resp);
+ return 0;
+}
+
+/* Re-apply the customer-derived defaults only while the user has not
+ changed them (they still equal what the previous customer produced). */
+static void iform_customer_changed(struct iform *f)
+{
+ if (f->ncust == 0 || f->cust_sel < 0 || (size_t)f->cust_sel >= f->ncust)
+ return;
+ char today[16], newdue[16];
+ today_iso(today, sizeof today);
+ util_date_add_days(today, f->cust_days[f->cust_sel], newdue,
+ sizeof newdue);
+ if (!f->due[0] || strcmp(f->due, f->auto_due) == 0)
+ snprintf(f->due, sizeof f->due, "%s", newdue);
+ snprintf(f->auto_due, sizeof f->auto_due, "%s", newdue);
+ const char *ref = f->cust_refs[f->cust_sel];
+ if (!f->your_ref[0] || strcmp(f->your_ref, f->auto_ref) == 0)
+ snprintf(f->your_ref, sizeof f->your_ref, "%s", ref ? ref : "");
+ snprintf(f->auto_ref, sizeof f->auto_ref, "%s", ref ? ref : "");
+}
+
+static void iform_cell(void *ud, int row, int col, char **buf, size_t *cap)
+{
+ struct iform *f = ud;
+ struct irow *r = &f->rows[row];
+ if (col == 0) {
+ *buf = r->description;
+ *cap = sizeof r->description;
+ } else if (col == 1) {
+ *buf = r->quantity;
+ *cap = sizeof r->quantity;
+ } else if (col == 2) {
+ *buf = r->unit;
+ *cap = sizeof r->unit;
+ } else if (col == 3) {
+ *buf = r->price;
+ *cap = sizeof r->price;
+ } else if (col == 4) {
+ *buf = r->vat;
+ *cap = sizeof r->vat;
+ } else {
+ *buf = r->note;
+ *cap = sizeof r->note;
+ }
+}
+
+static const char *const UNIT_CHOICES[] = { "st", "tim", "dag", "km" };
+static const char *const VAT_LABELS[] = { "25%", "12%", "6%",
+ "0%", "Omvänd moms", "EU" };
+static const char *const VAT_CODES[] = { "25", "12", "6", "0", "rc", "eu" };
+
+static int vat_index(const char *s)
+{
+ const char *code = vat_code_of(s);
+ if (!code)
+ return 0;
+ for (int i = 0; i < 6; i++)
+ if (strcmp(code, VAT_CODES[i]) == 0)
+ return i;
+ return 0;
+}
+
+static int iform_key(void *ud, int ch)
+{
+ struct iform *f = ud;
+ struct tui_rt *rt = &f->rt;
+ if (ch != '\n' && ch != '\r' && ch != KEY_ENTER)
+ return TUI_HOOK_NONE;
+ if (rt->focus == 0) {
+ if (f->ncust == 0) {
+ tui_message("Ny faktura", "Inga kunder. Skapa en under Register "
+ "→ Kunder först.");
+ return TUI_HOOK_STAY;
+ }
+ int c = tui_choice_prompt("Kund: ",
+ (const char *const *)f->cust_names,
+ (int)f->ncust, f->cust_sel);
+ if (c >= 0) {
+ int changed = c != f->cust_sel;
+ f->cust_sel = c;
+ if (changed)
+ iform_customer_changed(f);
+ tui_rt_normalize(rt);
+ rt->focus = tui_rt_focus_step(0, rt->nfields, &rt->row, &rt->col,
+ rt->nrows, rt->neditable, 1);
+ }
+ return TUI_HOOK_STAY;
+ }
+ if (rt->focus < 0 && rt->row >= 0 && rt->row < IFORM_ROWS) {
+ struct irow *r = &f->rows[rt->row];
+ if (rt->col == 2) {
+ int c = tui_choice_prompt("Enhet: ", UNIT_CHOICES, 4,
+ unit_index(r->unit));
+ if (c >= 0)
+ snprintf(r->unit, sizeof r->unit, "%s", UNIT_CHOICES[c]);
+ return TUI_HOOK_STAY;
+ }
+ if (rt->col == 4) {
+ int c = tui_choice_prompt("Moms: ", VAT_LABELS, 6,
+ vat_index(r->vat));
+ if (c >= 0)
+ snprintf(r->vat, sizeof r->vat, "%s", VAT_LABELS[c]);
+ return TUI_HOOK_STAY;
+ }
+ }
+ return TUI_HOOK_NONE;
+}
+
+static char *iform_draft_args(struct iform *f, char *err, size_t errn)
+{
+ if (f->ncust == 0 || f->cust_sel < 0 || (size_t)f->cust_sel >= f->ncust) {
+ snprintf(err, errn, "Välj en kund.");
+ return NULL;
+ }
+ if (!util_parse_iso_date(f->date)) {
+ snprintf(err, errn, "Ogiltigt fakturadatum (ÅÅÅÅ-MM-DD).");
+ return NULL;
+ }
+ if (!util_parse_iso_date(f->due)) {
+ snprintf(err, errn, "Ogiltigt förfallodatum (ÅÅÅÅ-MM-DD).");
+ return NULL;
+ }
+ if (f->delivery[0] && !util_parse_iso_date(f->delivery)) {
+ snprintf(err, errn, "Ogiltigt leveransdatum (ÅÅÅÅ-MM-DD).");
+ return NULL;
+ }
+ yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val *o = yyjson_mut_obj(d);
+ yyjson_mut_doc_set_root(d, o);
+ yyjson_mut_obj_add_int(d, o, "customer_id", f->cust_ids[f->cust_sel]);
+ yyjson_mut_obj_add_strcpy(d, o, "invoice_date", f->date);
+ yyjson_mut_obj_add_strcpy(d, o, "due_date", f->due);
+ yyjson_mut_obj_add_strcpy(d, o, "delivery_date", f->delivery);
+ yyjson_mut_obj_add_strcpy(d, o, "your_ref", f->your_ref);
+ yyjson_mut_obj_add_strcpy(d, o, "our_ref", f->our_ref);
+ yyjson_mut_obj_add_strcpy(d, o, "notes", f->notes);
+ yyjson_mut_val *arr = yyjson_mut_arr(d);
+ int used = 0;
+ for (int i = 0; i < f->rt.nrows; i++) {
+ struct irow *r = &f->rows[i];
+ if (!r->description[0] && !r->quantity[0] && !r->unit[0] &&
+ !r->price[0] && !r->vat[0] && !r->note[0])
+ continue;
+ if (!r->description[0]) {
+ snprintf(err, errn, "Rad %d: beskrivning saknas.", i + 1);
+ yyjson_mut_doc_free(d);
+ return NULL;
+ }
+ const char *qty = r->quantity[0] ? r->quantity : "1";
+ if (!qty_is_valid(qty)) {
+ snprintf(err, errn,
+ "Rad %d: antal måste vara ett positivt tal med högst "
+ "3 decimaler.",
+ i + 1);
+ yyjson_mut_doc_free(d);
+ return NULL;
+ }
+ int64_t price = 0;
+ if (tui_parse_kr(r->price, &price) != 0) {
+ snprintf(err, errn, "Rad %d: ogiltigt à-pris.", i + 1);
+ yyjson_mut_doc_free(d);
+ return NULL;
+ }
+ const char *unit = unit_code_of(r->unit);
+ if (!unit) {
+ snprintf(err, errn, "Rad %d: ogiltig enhet (st, tim, dag, km).",
+ i + 1);
+ yyjson_mut_doc_free(d);
+ return NULL;
+ }
+ const char *vat = vat_code_of(r->vat);
+ if (!vat) {
+ snprintf(err, errn, "Rad %d: ogiltig momskod.", i + 1);
+ yyjson_mut_doc_free(d);
+ return NULL;
+ }
+ yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr);
+ yyjson_mut_obj_add_strcpy(d, ro, "description", r->description);
+ yyjson_mut_obj_add_strcpy(d, ro, "quantity", qty);
+ yyjson_mut_obj_add_strcpy(d, ro, "unit", unit);
+ yyjson_mut_obj_add_int(d, ro, "unit_price_ore", price);
+ yyjson_mut_obj_add_strcpy(d, ro, "note", r->note);
+ yyjson_mut_obj_add_strcpy(d, ro, "vat_code", vat);
+ used++;
+ }
+ if (used == 0) {
+ snprintf(err, errn, "Minst en rad krävs.");
+ yyjson_mut_doc_free(d);
+ return NULL;
+ }
+ yyjson_mut_obj_add_val(d, o, "rows", arr);
+ char *out = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ return out;
+}
+
+static void iform_preview(struct iform *f)
+{
+ char err[256];
+ char *args = iform_draft_args(f, err, sizeof err);
+ if (!args) {
+ tui_message("Faktura", "%s", err);
+ return;
+ }
+ char *resp = client_rpc(&f->a->conn, "invoice.preview", f->a->session,
+ f->a->org, args);
+ free(args);
+ if (!resp || !client_ok(resp)) {
+ show_error("Faktura", resp);
+ free(resp);
+ return;
+ }
+ char *b64 = jstr_dup(resp, "result.content_base64");
+ if (b64) {
+ save_and_open_pdf(b64, "Faktura förhandsvisning.pdf");
+ free(b64);
+ }
+ free(resp);
+}
+
+static int64_t iform_issue(struct iform *f)
+{
+ if (f->issued)
+ return 0;
+ char err[256];
+ char *args = iform_draft_args(f, err, sizeof err);
+ if (!args) {
+ tui_message("Faktura", "%s", err);
+ return 0;
+ }
+ char *resp = client_rpc(&f->a->conn, "invoice.issue", f->a->session,
+ f->a->org, args);
+ free(args);
+ if (!resp || !client_ok(resp)) {
+ show_error("Faktura", resp);
+ free(resp);
+ return 0;
+ }
+ int64_t id = jint_val(resp, "result.id", 0);
+ int64_t number = jint_val(resp, "result.number", 0);
+ free(resp);
+ if (id <= 0)
+ return 0;
+ f->issued = 1;
+ const char *email = f->cust_emails[f->cust_sel];
+ if (email && *email) {
+ if (tui_confirm("Skicka faktura", "Skicka faktura %lld till %s?",
+ (long long)number, email)) {
+ char args2[64];
+ snprintf(args2, sizeof args2, "{\"id\":%lld}", (long long)id);
+ char *r = client_rpc(&f->a->conn, "invoice.send", f->a->session,
+ f->a->org, args2);
+ if (r && client_ok(r)) {
+ char *to = jstr_dup(r, "result.sent_to");
+ tui_message("Skickat", "Faktura %lld skickad till %s.",
+ (long long)number, to && *to ? to : email);
+ free(to);
+ } else {
+ show_error("Skicka faktura", r);
+ }
+ free(r);
+ }
+ } else {
+ tui_message("Faktura", "Faktura %lld skapad.", (long long)number);
+ }
+ return id;
+}
+
+static int64_t invoices_new(struct app *a)
+{
+ struct iform f;
+ memset(&f, 0, sizeof f);
+ f.a = a;
+ today_iso(f.date, sizeof f.date);
+ snprintf(f.delivery, sizeof f.delivery, "%s", f.date);
+ snprintf(f.due, sizeof f.due, "%s", f.date);
+ snprintf(f.auto_due, sizeof f.auto_due, "%s", f.due);
+ if (iform_load_customers(&f) != 0) {
+ iform_customers_free(&f);
+ return 0;
+ }
+ char *resp =
+ client_rpc(&a->conn, "settings.get", a->session, a->org, "{}");
+ if (resp && client_ok(resp)) {
+ char *v = jstr_dup(resp, "result.invoice_our_ref");
+ if (v) {
+ snprintf(f.our_ref, sizeof f.our_ref, "%s", v);
+ free(v);
+ }
+ }
+ free(resp);
+ if (f.ncust > 0) {
+ f.cust_sel = 0;
+ iform_customer_changed(&f);
+ }
+ struct tui_rt_col cols[6] = {
+ { "Beskrivning", 2, 34, TUI_RT_EDIT },
+ { "Antal", 37, 8, TUI_RT_EDIT },
+ { "Enhet", 46, 6, TUI_RT_EDIT },
+ { "À-pris", 53, 14, TUI_RT_EDIT },
+ { "Moms", 68, 14, TUI_RT_EDIT },
+ { "Anm", 83, 32, TUI_RT_EDIT },
+ };
+ tui_rt_init(&f.rt, 1, IFORM_ROWS, 6, cols, 0, iform_cell, NULL, &f);
+ tui_rt_set_key(&f.rt, iform_key);
+ struct tui_form_field ff[7];
+ memset(ff, 0, sizeof ff);
+ ff[0].label = "Kund";
+ ff[0].kind = TUI_F_CHOICE;
+ ff[0].choices = (const char *const *)f.cust_names;
+ ff[0].nchoices = (int)f.ncust;
+ ff[0].choice = &f.cust_sel;
+ ff[1].label = "Fakturadatum";
+ ff[1].value = f.date;
+ ff[1].cap = sizeof f.date;
+ ff[1].kind = TUI_F_DATE;
+ ff[2].label = "Förfallodatum";
+ ff[2].value = f.due;
+ ff[2].cap = sizeof f.due;
+ ff[2].kind = TUI_F_DATE;
+ ff[3].label = "Leveransdatum";
+ ff[3].value = f.delivery;
+ ff[3].cap = sizeof f.delivery;
+ ff[3].kind = TUI_F_DATE;
+ ff[4].label = "Er referens";
+ ff[4].value = f.your_ref;
+ ff[4].cap = sizeof f.your_ref;
+ ff[4].kind = TUI_F_TEXT;
+ ff[5].label = "Vår referens";
+ ff[5].value = f.our_ref;
+ ff[5].cap = sizeof f.our_ref;
+ ff[5].kind = TUI_F_TEXT;
+ ff[6].label = "Fritext";
+ ff[6].value = f.notes;
+ ff[6].cap = sizeof f.notes;
+ ff[6].kind = TUI_F_TEXT;
+ tui_rt_set_fields(&f.rt, ff, 7);
+ const char *hint =
+ "Enter = välj/ändra Tab = byta fält F5 = förhandsvisa"
+ " ^Enter = utfärda Esc = avbryt";
+ int64_t out = 0;
+ for (;;) {
+ int rr = tui_rt_run("Ny faktura", &f.rt, hint);
+ if (rr == -1)
+ break;
+ if (rr == -2) {
+ iform_preview(&f);
+ continue;
+ }
+ if (rr == -3) {
+ out = iform_issue(&f);
+ if (out > 0)
+ break;
+ }
+ }
+ iform_customers_free(&f);
+ return out;
+}
+
+static void invoices_screen(struct app *a)
+{
+ char **items = NULL;
+ int64_t *ids = NULL;
+ size_t n = 0;
+ int fetch = 1;
+ for (;;) {
+ if (g_quit) {
+ for (size_t i = 0; i < n; i++)
+ free(items[i]);
+ free(items);
+ free(ids);
+ return;
+ }
+ if (fetch) {
+ for (size_t i = 0; i < n; i++)
+ free(items[i]);
+ free(items);
+ free(ids);
+ items = NULL;
+ ids = NULL;
+ n = 0;
+ fetch = 0;
+ char *resp = client_rpc(&a->conn, "invoice.list", a->session,
+ a->org, "{\"limit\":200}");
+ if (!resp || !client_ok(resp)) {
+ show_error("Fakturor", resp);
+ free(resp);
+ return;
+ }
+ n = jarr_size(resp, "result.items");
+ items = xcalloc(n + 1, sizeof(char *));
+ ids = xcalloc(n ? n : 1, sizeof(int64_t));
+ for (size_t i = 0; i < n; i++) {
+ char path[64], line[768];
+ snprintf(path, sizeof path, "result.items.%zu.id", i);
+ ids[i] = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.items.%zu.number", i);
+ int64_t number = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.items.%zu.invoice_date", i);
+ char *date = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.customer_name",
+ i);
+ char *cust = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.total_ore", i);
+ int64_t total = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.items.%zu.status", i);
+ char *status = jstr_dup(resp, path);
+ char nbuf[32], cbuf[256], abuf[40];
+ snprintf(nbuf, sizeof nbuf, "Faktura %lld", (long long)number);
+ snprintf(cbuf, sizeof cbuf, "%s", cust ? cust : "");
+ tui_pad_field(cbuf, sizeof cbuf, 28);
+ tui_amt_col(abuf, sizeof abuf, 14, total);
+ snprintf(line, sizeof line, "%s %s %s %s %s", nbuf,
+ date ? date : "", cbuf, abuf,
+ invoice_status_sv(status));
+ items[i] = xstrdup(line);
+ free(date);
+ free(cust);
+ free(status);
+ }
+ free(resp);
+ items[n] = xstrdup("+ Ny faktura (^N)");
+ }
+ int start = 0;
+ if (a->invoice_sel) {
+ for (size_t i = 0; i < n; i++)
+ if (ids[i] == a->invoice_sel) {
+ start = (int)i;
+ break;
+ }
+ }
+ int cur = start;
+ int sel = tui_select_list("Fakturor", items, (int)n + 1, start, 1,
+ &cur, 1, NULL, 0);
+ if (cur >= 0 && (size_t)cur < n)
+ a->invoice_sel = ids[cur];
+ if (sel == -1) {
+ for (size_t i = 0; i < n; i++)
+ free(items[i]);
+ free(items);
+ free(ids);
+ return;
+ }
+ if (sel == -2) {
+ fetch = 1;
+ continue;
+ }
+ if (sel == -4 || (sel >= 0 && (size_t)sel == n)) {
+ int64_t nid = invoices_new(a);
+ if (nid > 0)
+ a->invoice_sel = nid;
+ fetch = 1;
+ continue;
+ }
+ if (sel >= 0 && (size_t)sel < n) {
+ invoices_detail(a, ids[sel]);
+ continue;
+ }
+ }
+}
+
static void templates_screen(struct app *a)
{
int cursor = 0;
@@ -5946,6 +7283,10 @@ static void open_scene(struct app *a, const char *scene)
inbox_screen(a);
else if (strcmp(scene, "bank") == 0)
bank_screen(a);
+ else if (strcmp(scene, "invoices") == 0)
+ invoices_screen(a);
+ else if (strcmp(scene, "customers") == 0)
+ customers_screen(a);
else if (strcmp(scene, "ib") == 0)
ib_screen(a);
else if (strcmp(scene, "templates") == 0)
@@ -5999,10 +7340,12 @@ static void do_reload(struct app *a, const char *self)
static const char *const MAIN_ITEMS[] = {
MK_SECTION "Bokföring",
"Verifikat", "Underlag (inkorg)", "Bankavstämning",
+ MK_SECTION "Fakturering",
+ "Fakturor",
MK_SECTION "Rapporter & bokslut",
"Rapporter", "Bokslut",
MK_SECTION "Register",
- "Mallar", "Momsregler", "Företagsuppgifter", "Inställningar",
+ "Mallar", "Kunder", "Momsregler", "Företagsuppgifter", "Inställningar",
MK_SECTION "Räkenskapsår",
"Ingående balans", "Räkenskapsår",
MK_SECTION "Övrigt",
@@ -6040,33 +7383,39 @@ static void dashboard(struct app *a)
open_scene(a, "bank");
break;
case 5:
+ open_scene(a, "invoices");
+ break;
+ case 7:
open_scene(a, "reports");
break;
- case 6:
+ case 8:
open_scene(a, "bokslut");
break;
- case 8:
+ case 10:
open_scene(a, "templates");
break;
- case 9:
+ case 11:
+ open_scene(a, "customers");
+ break;
+ case 12:
open_scene(a, "rules");
break;
- case 10:
+ case 13:
open_scene(a, "company");
break;
- case 11:
+ case 14:
open_scene(a, "settings");
break;
- case 13:
+ case 16:
open_scene(a, "ib");
break;
- case 14:
+ case 17:
select_fiscal_year(a);
break;
- case 16:
+ case 19:
open_scene(a, "audit");
break;
- case 17:
+ case 20:
return;
default:
break; /* section header */
@@ -6351,7 +7700,7 @@ static void usage(FILE *f)
" --screen NAME start in a view: dashboard, vouchers, inbox,\n"
" bank, ib, templates, rules, reports, bokslut\n"
" (yearinfo is an alias), audit, settings,\n"
- " company\n"
+ " company, invoices, customers\n"
" --version\n");
}