diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-20 16:13:13 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-20 16:13:13 +0200 |
| commit | be220f2b3e902f76cfded32c0abc7492f21d149f (patch) | |
| tree | 84b0ee6ea22f80e787c6b98548cb9fea21a89dd8 /clients | |
| parent | 079f780e0a4d014dbb89d5976a3084a957b9e50d (diff) | |
| download | bokf-be220f2b3e902f76cfded32c0abc7492f21d149f.tar.gz bokf-be220f2b3e902f76cfded32c0abc7492f21d149f.zip | |
tui: fakturering with invoice form, preview and send
Diffstat (limited to 'clients')
| -rw-r--r-- | clients/bokftui.c | 1431 |
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"); } |
