diff options
Diffstat (limited to 'clients/screens_invoices.c')
| -rw-r--r-- | clients/screens_invoices.c | 814 |
1 files changed, 659 insertions, 155 deletions
diff --git a/clients/screens_invoices.c b/clients/screens_invoices.c index ca59eef..46d2406 100644 --- a/clients/screens_invoices.c +++ b/clients/screens_invoices.c @@ -14,6 +14,7 @@ #include <unistd.h> #include "client.h" +#include "drafts.h" #include "tui.h" #include "formula.h" #include "util.h" @@ -219,12 +220,124 @@ static int customer_save(struct app *a, int64_t id, const char *name, return ok; } -static void customer_form(struct app *a, int64_t id) +/* Drafts for the customer register: new entities exist only as drafts + until Spara writes them (docs/TUI-GUIDELINES.md "Interaction model"). */ + +static struct drafts g_drafts; +static int g_drafts_ready; + +static struct drafts *kdrafts(void) +{ + if (!g_drafts_ready) { + drafts_init(&g_drafts); + g_drafts_ready = 1; + } + return &g_drafts; +} + +struct cform_ctx { + struct app *a; + int64_t id; + const char *draft_id; + int deleted; + struct tui_action acts[3]; + char name[256]; + char address[512]; + char postal[32]; + char city[128]; + char vat[64]; + char email[256]; + char your_ref[128]; + char payment[24]; + char notes[2048]; +}; + +static char *customer_fields_json(const struct cform_ctx *c) +{ + 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, "name", c->name); + yyjson_mut_obj_add_strcpy(d, o, "address", c->address); + yyjson_mut_obj_add_strcpy(d, o, "postal_code", c->postal); + yyjson_mut_obj_add_strcpy(d, o, "city", c->city); + yyjson_mut_obj_add_strcpy(d, o, "vat_nr", c->vat); + yyjson_mut_obj_add_strcpy(d, o, "email", c->email); + yyjson_mut_obj_add_strcpy(d, o, "your_ref", c->your_ref); + yyjson_mut_obj_add_int(d, o, "payment_days", + strtoll(c->payment, NULL, 10)); + yyjson_mut_obj_add_strcpy(d, o, "notes", c->notes); + char *s = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + return s; +} + +static void customer_load_json(const char *json, struct cform_ctx *c) +{ + yyjson_doc *doc = yyjson_read(json, strlen(json), 0); + + if (!doc) + return; + yyjson_val *o = yyjson_doc_get_root(doc); + const struct { + const char *key; + char *dst; + size_t cap; + } fields[] = { + { "name", c->name, sizeof c->name }, + { "address", c->address, sizeof c->address }, + { "postal_code", c->postal, sizeof c->postal }, + { "city", c->city, sizeof c->city }, + { "vat_nr", c->vat, sizeof c->vat }, + { "email", c->email, sizeof c->email }, + { "your_ref", c->your_ref, sizeof c->your_ref }, + { "notes", c->notes, sizeof c->notes }, + }; + for (size_t i = 0; i < sizeof fields / sizeof fields[0]; i++) + snprintf(fields[i].dst, fields[i].cap, "%s", + vstr(o, fields[i].key)); + int64_t days = vint(o, "payment_days"); + if (days > 0) + snprintf(c->payment, sizeof c->payment, "%lld", (long long)days); + yyjson_doc_free(doc); +} + +#define CFORM_DRAFT_DELETE TUI_KEY_ACTION(0) + +static int cform_key(void *ud, int ch) +{ + struct cform_ctx *c = ud; + + if (ch == TUI_KEY_MENU) { + c->acts[2].enabled = + drafts_have(kdrafts(), c->a->org, "customer", c->draft_id); + return TUI_HOOK_NONE; + } + if (ch != CFORM_DRAFT_DELETE) + return TUI_HOOK_NONE; + if (tui_confirm("Kund", "Radera utkastet?")) { + drafts_del(kdrafts(), c->a->org, "customer", c->draft_id); + c->deleted = 1; + return TUI_HOOK_BACK; + } + return TUI_HOOK_STAY; +} + +static void customer_form(struct app *a, int64_t id, const char *draft_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) { + struct cform_ctx c; + + memset(&c, 0, sizeof c); + c.a = a; + c.id = id; + c.draft_id = draft_id; + snprintf(c.payment, sizeof c.payment, "30"); + + const char *fields = drafts_get(kdrafts(), a->org, "customer", draft_id); + if (fields) { + customer_load_json(fields, &c); + } else if (id > 0) { char args[64]; snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id); char *resp = @@ -234,43 +347,46 @@ static void customer_form(struct app *a, int64_t id) free(resp); return; } - struct { + const 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 }, + } f[] = { + { "name", c.name, sizeof c.name }, + { "address", c.address, sizeof c.address }, + { "postal_code", c.postal, sizeof c.postal }, + { "city", c.city, sizeof c.city }, + { "vat_nr", c.vat, sizeof c.vat }, + { "email", c.email, sizeof c.email }, + { "your_ref", c.your_ref, sizeof c.your_ref }, + { "notes", c.notes, sizeof c.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); + for (size_t i = 0; i < sizeof f / sizeof f[0]; i++) { + char path[64]; + char *v; + snprintf(path, sizeof path, "result.%s", f[i].key); v = jstr_dup(resp, path); if (v) { - snprintf(fields[i].dst, fields[i].cap, "%s", v); + snprintf(f[i].dst, f[i].cap, "%s", v); free(v); } } - snprintf(payment, sizeof payment, "%lld", + snprintf(c.payment, sizeof c.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 }; + char *vals[] = { c.name, c.address, c.postal, c.city, + c.vat, c.email, c.your_ref, c.payment, + c.notes }; + size_t caps[] = { sizeof c.name, sizeof c.address, sizeof c.postal, + sizeof c.city, sizeof c.vat, sizeof c.email, + sizeof c.your_ref, sizeof c.payment, sizeof c.notes }; struct tui_form_field ff[10]; memset(ff, 0, sizeof ff); for (int i = 0; i < 9; i++) { @@ -282,22 +398,53 @@ static void customer_form(struct app *a, int64_t id) ff[9].label = ""; ff[9].value = "Spara"; ff[9].kind = TUI_F_ACTION; + static int sel = 0; + c.acts[0] = (struct tui_action){ "customer.save", "Spara", + TUI_KEY_SUBMIT, 1, NULL }; + c.acts[1] = (struct tui_action){ "customer.validate", "Validera", + TUI_KEY_REFRESH, 1, NULL }; + c.acts[2] = (struct tui_action){ "customer.draft.delete", "Radera utkast", + CFORM_DRAFT_DELETE, 0, "inget utkast" }; for (;;) { - int r = tui_form_run(id > 0 ? "Kund" : "Ny kund", ff, 10, 1, &sel); + char title[64]; + snprintf(title, sizeof title, "%s%s", id > 0 ? "Kund" : "Ny kund", + drafts_have(kdrafts(), a->org, "customer", draft_id) + ? " <UTKAST>" + : ""); + tui_set_actions(c.acts, 3); + int r = tui_form_run_hook(title, ff, 10, 1, cform_key, &c, &sel); + if (c.deleted) + break; + if (r >= 0) { + char *json = customer_fields_json(&c); + if (json) { + drafts_put(kdrafts(), a->org, "customer", draft_id, json); + free(json); + } + } if (r == TUI_FORM_BACK) - return; + break; if (r == TUI_FORM_REFRESH) { - customer_save(a, id, name, address, postal, city, vat, email, - your_ref, payment, notes, 1); + customer_save(a, id, c.name, c.address, c.postal, c.city, c.vat, + c.email, c.your_ref, c.payment, c.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; + if (customer_save(a, id, c.name, c.address, c.postal, c.city, + c.vat, c.email, c.your_ref, c.payment, c.notes, + 0)) { + drafts_del(kdrafts(), a->org, "customer", draft_id); + break; + } + char *json = customer_fields_json(&c); + if (json) { + drafts_put(kdrafts(), a->org, "customer", draft_id, json); + free(json); + } } } + tui_form_hint_extra(NULL); } static void customer_archive_toggle(struct app *a, int64_t id, const char *name, @@ -319,59 +466,153 @@ static void customer_archive_toggle(struct app *a, int64_t id, const char *name, free(r); } +struct clist { + struct app *a; + int *cur; + char **items; + char **names; + char **dkeys; + int64_t *ids; + unsigned char *active; + size_t nserver; + size_t nrows; /* server rows + temp drafts; the add row is at nrows */ + struct tui_action act; /* 'd' for the current row (see clist_key) */ +}; + +static void clist_clear(struct clist *l) +{ + if (l->items) { + for (size_t i = 0; i <= l->nrows; i++) { + free(l->items[i]); + free(l->names[i]); + free(l->dkeys[i]); + } + } + free(l->items); + free(l->names); + free(l->dkeys); + free(l->ids); + free(l->active); + l->items = NULL; + l->names = NULL; + l->dkeys = NULL; + l->ids = NULL; + l->active = NULL; + l->nserver = l->nrows = 0; +} + +static int clist_is_server_id(const struct clist *l, const char *id) +{ + long long v = strtoll(id, NULL, 10); + + if (v <= 0) + return 0; + for (size_t i = 0; i < l->nserver; i++) + if (l->ids[i] == v) + return 1; + return 0; +} + +static void clist_collect_temp(void *ud, const char *id, const char *fields) +{ + struct clist *l = ud; + char nbuf[256], line[768]; + const char *name = "(namnlös)"; + yyjson_doc *doc; + + if (clist_is_server_id(l, id)) + return; + doc = yyjson_read(fields, strlen(fields), 0); + if (doc) { + yyjson_val *o = yyjson_doc_get_root(doc); + const char *n = vstr(o, "name"); + if (*n) + name = n; + size_t i = l->nrows; + l->names[i] = xstrdup(name); + snprintf(nbuf, sizeof nbuf, "<UTKAST> %s", name); + snprintf(line, sizeof line, "%s", nbuf); + l->items[i] = xstrdup(line); + l->dkeys[i] = xstrdup(id); + l->ids[i] = 0; + l->active[i] = 1; + l->nrows++; + yyjson_doc_free(doc); + return; + } + size_t i = l->nrows; + l->names[i] = xstrdup(name); + snprintf(line, sizeof line, "<UTKAST> %s", name); + l->items[i] = xstrdup(line); + l->dkeys[i] = xstrdup(id); + l->ids[i] = 0; + l->active[i] = 1; + l->nrows++; +} + +/* The row action behind 'd' depends on the row: a draft is deleted, a + saved customer archived or reactivated. The menu shows the right one. */ +static int clist_key(void *ud, int ch) +{ + struct clist *l = ud; + + if (ch != TUI_KEY_MENU || !l->cur) + return TUI_HOOK_NONE; + int i = *l->cur; + struct tui_action *a = &l->act; + *a = (struct tui_action){ "customer.remove", "Arkivera", 'd', 1, NULL }; + if (i < 0 || (size_t)i >= l->nrows) { + a->enabled = 0; + a->reason = "välj en kund"; + } else if (l->dkeys[i] && l->ids[i] == 0) { + a->label = "Radera utkast"; + a->id = "customer.draft.delete"; + } else if (!l->active[i]) { + a->label = "Återaktivera"; + } + return TUI_HOOK_NONE; +} + void customers_screen(struct app *a) { - char **items = NULL; - char **names = NULL; - int64_t *ids = NULL; - unsigned char *active = NULL; - size_t n = 0; + struct clist l; + memset(&l, 0, sizeof l); + l.a = a; + int cur = 0; + l.cur = &cur; 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); + clist_clear(&l); 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; + clist_clear(&l); 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); + clist_clear(&l); 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); + size_t n = jarr_size(resp, "result.items"); + size_t ndraft = drafts_count(kdrafts(), a->org, "customer"); + size_t cap = n + ndraft + 2; + l.items = xcalloc(cap, sizeof(char *)); + l.names = xcalloc(cap, sizeof(char *)); + l.dkeys = xcalloc(cap, sizeof(char *)); + l.ids = xcalloc(cap, sizeof(int64_t)); + l.active = xcalloc(cap, 1); + l.nserver = n; + l.nrows = n; for (size_t i = 0; i < n; i++) { - char path[64], line[768]; + char path[64], line[768], idbuf[32]; snprintf(path, sizeof path, "result.items.%zu.id", i); - ids[i] = jint_val(resp, path, 0); + l.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); + l.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); @@ -390,60 +631,83 @@ void customers_screen(struct app *a) 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 : ""); + snprintf(idbuf, sizeof idbuf, "%lld", (long long)l.ids[i]); + int isdraft = + drafts_have(kdrafts(), a->org, "customer", idbuf); + if (isdraft) + l.dkeys[i] = xstrdup(idbuf); + snprintf(line, sizeof line, "%s%s %s %s %3lld dagar %s", + isdraft ? "<UTKAST> " : "", nbuf, pbuf, vbuf, + (long long)days, + l.active[i] ? "" : "(arkiverad)"); + l.items[i] = xstrdup(line); + l.names[i] = xstrdup(name ? name : ""); free(name); free(postal); free(city); free(vat); } free(resp); - items[n] = xstrdup("+ Ny kund (^N)"); + drafts_foreach(kdrafts(), a->org, "customer", clist_collect_temp, + &l); + l.items[l.nrows] = xstrdup("+ Ny kund (n)"); + l.names[l.nrows] = xstrdup(""); + l.ids[l.nrows] = 0; + l.active[l.nrows] = 1; } int start = 0; if (a->customer_sel) { - for (size_t i = 0; i < n; i++) - if (ids[i] == a->customer_sel) { + for (size_t i = 0; i < l.nserver; i++) + if (l.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]; + cur = start; + l.act = (struct tui_action){ "customer.remove", "Arkivera", 'd', 1, + NULL }; + tui_set_actions(&l.act, 1); + int sel = tui_select_list_hook("Kunder", l.items, + (int)l.nrows + 1, start, 1, &cur, 1, + "arkivera/återaktivera", 0, clist_key, + &l); + if (cur >= 0 && (size_t)cur < l.nserver) + a->customer_sel = l.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); + clist_clear(&l); 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); + if (sel == -6 && cur >= 0 && (size_t)cur < l.nrows) { + if (l.ids[cur] == 0) { + if (l.dkeys[cur] && tui_confirm("Kund", "Radera utkastet?")) + drafts_del(kdrafts(), a->org, "customer", l.dkeys[cur]); + } else { + customer_archive_toggle(a, l.ids[cur], l.names[cur], + l.active[cur] != 0); + } fetch = 1; continue; } - if (sel == -4 || (sel >= 0 && (size_t)sel == n)) { - customer_form(a, 0); + if (sel == -4 || (cur >= 0 && (size_t)cur == l.nrows)) { + char *tid = drafts_new_id(); + drafts_put(kdrafts(), a->org, "customer", tid, "{}"); + customer_form(a, 0, tid); + free(tid); fetch = 1; continue; } - if (sel >= 0 && (size_t)sel < n) { - customer_form(a, ids[sel]); + if (sel >= 0 && (size_t)sel < l.nrows) { + char idbuf[32]; + const char *dk = l.dkeys[sel]; + if (!dk) { + snprintf(idbuf, sizeof idbuf, "%lld", (long long)l.ids[sel]); + dk = idbuf; + } + customer_form(a, l.ids[sel], dk); fetch = 1; continue; } @@ -457,9 +721,13 @@ struct invctx { int64_t id; int64_t number; int64_t customer_id; + int64_t total_ore; char customer[256]; + char paid_date[16]; }; +static int64_t invoices_new_from(struct app *a, int64_t id); + static void inv_detail_pdf(struct invctx *ctx) { char args[64]; @@ -476,7 +744,7 @@ static void inv_detail_pdf(struct invctx *ctx) char fname[800]; snprintf(fname, sizeof fname, "Faktura %lld %s.pdf", (long long)ctx->number, ctx->customer); - pdf_save_and_open(b64, fname, "Faktura PDF"); + file_save_and_open(b64, fname, "Faktura PDF"); free(b64); } free(resp); @@ -523,6 +791,61 @@ static void inv_detail_send(struct invctx *ctx) free(r); } +/* Prefills the new-invoice form with the payment voucher: D the org's + bank account, K the invoice receivable account, both editable. */ +static void inv_detail_pay(struct invctx *ctx) +{ + struct app *a = ctx->a; + if (ctx->paid_date[0]) { + tui_message("Kvittera betalning", "Fakturan är redan betald."); + return; + } + char pay[16] = "1930", rec[16] = "1510"; + char *resp = client_rpc(&a->conn, "settings.get", a->session, a->org, + "{}"); + if (resp && client_ok(resp)) { + char *v = jstr_dup(resp, "result.bank_account"); + if (v && *v) + snprintf(pay, sizeof pay, "%s", v); + free(v); + v = jstr_dup(resp, "result.invoice_receivable_account"); + if (v && *v) + snprintf(rec, sizeof rec, "%s", v); + free(v); + } + free(resp); + char desc[160]; + snprintf(desc, sizeof desc, "Betalning faktura %lld %.120s", + (long long)ctx->number, ctx->customer); + struct voucher_prefill p; + memset(&p, 0, sizeof p); + char date[16]; + today_iso(date, sizeof date); + p.date = date; + p.description = desc; + p.bank_account = pay; + p.counter_account = rec; + p.amount_ore = ctx->total_ore; + int64_t vid = vouchers_new_prefill(a, &p); + if (vid <= 0) + return; + char args[96]; + snprintf(args, sizeof args, "{\"id\":%lld,\"voucher_id\":%lld}", + (long long)ctx->id, (long long)vid); + resp = client_rpc(&a->conn, "invoice.pay", a->session, a->org, args); + if (resp && client_ok(resp)) { + char *ser = jstr_dup(resp, "result.voucher_series"); + int64_t num = jint_val(resp, "result.voucher_number", 0); + tui_message("Kvittera betalning", "Faktura %lld kvitterad (%s%lld).", + (long long)ctx->number, ser ? ser : "", + (long long)num); + free(ser); + } else { + show_error("Kvittera betalning", resp); + } + free(resp); +} + static int inv_key(void *ud, int ch) { struct invctx *ctx = ud; @@ -534,6 +857,14 @@ static int inv_key(void *ud, int ch) inv_detail_send(ctx); return TUI_HOOK_STAY; } + if (ch == 'u') { + invoices_new_from(ctx->a, ctx->id); + return TUI_HOOK_REFRESH; + } + if (ch == 'b') { + inv_detail_pay(ctx); + return TUI_HOOK_REFRESH; + } return TUI_HOOK_NONE; } @@ -565,10 +896,19 @@ static void invoices_detail(struct app *a, int64_t id) 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"); + char *paid_date = jstr_dup(resp, "result.paid_date"); + char *paid_ser = jstr_dup(resp, "result.paid_voucher_series"); + int64_t paid_num = jint_val(resp, "result.paid_voucher_number", 0); + char statbuf[96]; + if (paid_date && *paid_date) + snprintf(statbuf, sizeof statbuf, "betald %s", paid_date); + else + snprintf(statbuf, sizeof statbuf, "%s", + invoice_status_sv(status)); struct buf t; buf_init(&t); buf_line(&t, MK_HEAD "Faktura %lld %s\n", (long long)number, - invoice_status_sv(status)); + statbuf); 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 : ""); @@ -584,6 +924,9 @@ static void invoices_detail(struct app *a, int64_t id) buf_line(&t, "Skickad: %s%s%s\n", sent_at, sent_to && *sent_to ? " till " : "", sent_to && *sent_to ? sent_to : ""); + if (paid_ser && *paid_ser && paid_num > 0) + buf_line(&t, "Betalningsverifikat: %s%lld\n", paid_ser, + (long long)paid_num); if (notes && *notes) buf_line(&t, "\nFritext:\n%s\n", notes); buf_line(&t, "\n"); @@ -609,6 +952,18 @@ static void invoices_detail(struct app *a, int64_t id) char *note = jstr_dup(resp, path); snprintf(path, sizeof path, "result.rows.%zu.amount_ore", i); int64_t amount = jint_val(resp, path, 0); + snprintf(path, sizeof path, "result.rows.%zu.is_text", i); + if (jbool_val(resp, path, 0)) { + char dbuf[256]; + snprintf(dbuf, sizeof dbuf, "%s", desc ? desc : ""); + tui_pad_field(dbuf, sizeof dbuf, 34); + buf_line(&t, "%s\n", dbuf); + free(desc); + free(unit); + free(vat_code); + free(note); + continue; + } char dbuf[256], qbuf[24], qcol[24], ubuf[24], pcol[40], vbuf[24], nbuf[128], acol[40]; snprintf(dbuf, sizeof dbuf, "%s", desc ? desc : ""); @@ -644,10 +999,25 @@ static void invoices_detail(struct app *a, int64_t id) ctx.id = id; ctx.number = number; ctx.customer_id = customer_id; + ctx.total_ore = total; 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); + if (paid_date && *paid_date) + snprintf(ctx.paid_date, sizeof ctx.paid_date, "%s", paid_date); + char ahint[192]; + snprintf(ahint, sizeof ahint, + "p = visa PDF s = skicka u = duplicera%s", + ctx.paid_date[0] ? "" : " b = kvittera betalning"); + struct tui_action acts[] = { + { "invoice.pdf", "Visa PDF", 'p', 1, NULL }, + { "invoice.send", "Skicka…", 's', 1, NULL }, + { "invoice.duplicate", "Duplicera till nytt utkast", 'u', 1, + NULL }, + { "invoice.pay", "Kvittera betalning…", 'b', + ctx.paid_date[0] ? 0 : 1, "redan betald" }, + }; + tui_set_actions(acts, 4); + int ret = tui_pager_hook("Faktura", (const char *)t.p, ahint, 0, + inv_key, &ctx); free(cust); free(status); free(date); @@ -659,6 +1029,8 @@ static void invoices_detail(struct app *a, int64_t id) free(ocr); free(sent_at); free(sent_to); + free(paid_date); + free(paid_ser); buf_free(&t); free(resp); if (ret != 1) @@ -890,7 +1262,7 @@ static char *iform_draft_args(struct iform *f, char *err, size_t errn) 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; + int used = 0, priced = 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] && @@ -901,6 +1273,13 @@ static char *iform_draft_args(struct iform *f, char *err, size_t errn) yyjson_mut_doc_free(d); return NULL; } + if (!r->quantity[0] && !r->unit[0] && !r->price[0] && !r->vat[0]) { + 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_bool(d, ro, "text", true); + used++; + continue; + } const char *qty = r->quantity[0] ? r->quantity : "1"; if (!qty_is_valid(qty)) { snprintf(err, errn, @@ -937,12 +1316,19 @@ static char *iform_draft_args(struct iform *f, char *err, size_t errn) yyjson_mut_obj_add_strcpy(d, ro, "note", r->note); yyjson_mut_obj_add_strcpy(d, ro, "vat_code", vat); used++; + priced++; } if (used == 0) { snprintf(err, errn, "Minst en rad krävs."); yyjson_mut_doc_free(d); return NULL; } + if (priced == 0) { + snprintf(err, errn, + "Minst en prissatt rad krävs (fritextrader saknar belopp)."); + 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); @@ -967,7 +1353,7 @@ static void iform_preview(struct iform *f) } char *b64 = jstr_dup(resp, "result.content_base64"); if (b64) { - pdf_save_and_open(b64, "Faktura förhandsvisning.pdf", "Faktura PDF"); + file_save_and_open(b64, "Faktura förhandsvisning.pdf", "Faktura PDF"); free(b64); } free(resp); @@ -1021,33 +1407,8 @@ static int64_t iform_issue(struct iform *f) return id; } -static int64_t invoices_new(struct app *a) +static int64_t invoices_form_run(struct iform *f) { - 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 }, @@ -1056,66 +1417,196 @@ static int64_t invoices_new(struct app *a) { "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); + 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[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].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].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].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].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].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].value = f->notes; + ff[6].cap = sizeof f->notes; ff[6].kind = TUI_F_TEXT; - tui_rt_set_fields(&f.rt, ff, 7); + tui_rt_set_fields(&f->rt, ff, 7); + tui_rt_normalize(&f->rt); const char *hint = - "Enter = välj/ändra Tab = byta fält F5 = förhandsvisa" - " ^Enter/F9 = utfärda Esc = avbryt"; + "Enter = välj/ändra Tab = byta fält" + " ^O = åtgärder (utfärda, förhandsvisa) Esc = avbryt"; + static const struct tui_action acts[] = { + { "invoice.issue", "Utfärda", TUI_KEY_SUBMIT, 1, NULL }, + { "invoice.preview", "Förhandsvisa PDF", TUI_KEY_REFRESH, 1, NULL }, + }; int64_t out = 0; for (;;) { - int rr = tui_rt_run("Ny faktura", &f.rt, hint); + tui_set_actions(acts, 2); + int rr = tui_rt_run("Ny faktura", &f->rt, hint); if (rr == -1) break; if (rr == -2) { - iform_preview(&f); + iform_preview(f); continue; } if (rr == -3) { - out = iform_issue(&f); + out = iform_issue(f); if (out > 0) break; } } + return out; +} + +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); + } + int64_t out = invoices_form_run(&f); iform_customers_free(&f); return out; } +/* Duplicates an issued invoice: same customer, rows and references, with + the dates reset (invoice and delivery today, due today + payment days). */ +static int64_t invoices_new_from(struct app *a, int64_t id) +{ + char iargs[64]; + snprintf(iargs, sizeof iargs, "{\"id\":%lld}", (long long)id); + char *resp = + client_rpc(&a->conn, "invoice.get", a->session, a->org, iargs); + if (!resp || !client_ok(resp)) { + show_error("Faktura", resp); + free(resp); + return 0; + } + 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); + free(resp); + return 0; + } + int64_t cid = jint_val(resp, "result.customer_id", 0); + int found = -1; + for (size_t i = 0; i < f.ncust; i++) + if (f.cust_ids[i] == cid) { + found = (int)i; + break; + } + if (found < 0) { + tui_message("Ny faktura", + "Kunden är arkiverad eller finns inte kvar."); + iform_customers_free(&f); + free(resp); + return 0; + } + f.cust_sel = found; + iform_customer_changed(&f); + char *v = jstr_dup(resp, "result.our_ref"); + if (v) { + snprintf(f.our_ref, sizeof f.our_ref, "%s", v); + free(v); + } + v = jstr_dup(resp, "result.notes"); + if (v) { + snprintf(f.notes, sizeof f.notes, "%s", v); + free(v); + } + size_t nrows = jarr_size(resp, "result.rows"); + if (nrows > IFORM_ROWS) + nrows = IFORM_ROWS; + for (size_t i = 0; i < nrows; i++) { + struct irow *r = &f.rows[i]; + char path[64]; + snprintf(path, sizeof path, "result.rows.%zu.description", i); + char *desc = jstr_dup(resp, path); + if (desc) + snprintf(r->description, sizeof r->description, "%s", desc); + snprintf(path, sizeof path, "result.rows.%zu.is_text", i); + if (jbool_val(resp, path, 0)) { + free(desc); + continue; + } + 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 = jstr_dup(resp, path); + qty_sv(qm, r->quantity, sizeof r->quantity); + if (unit) + snprintf(r->unit, sizeof r->unit, "%s", unit); + char pbuf[24]; + tui_kr_format(price, pbuf, sizeof pbuf); + snprintf(r->price, sizeof r->price, "%s", pbuf); + snprintf(r->vat, sizeof r->vat, "%s", vat_label(vat)); + free(desc); + free(unit); + free(vat); + } + free(resp); + int64_t out = invoices_form_run(&f); + iform_customers_free(&f); + return out; +} + +#define INVOICES_NEXT_NUMBER TUI_KEY_ACTION(0) + static int invoices_key(void *ud, int ch) { struct app *a = ud; - if (ch != 'n') + if (ch != INVOICES_NEXT_NUMBER) return TUI_HOOK_NONE; char *resp = client_rpc(&a->conn, "invoice.sequence_get", a->session, a->org, "{}"); @@ -1187,21 +1678,28 @@ void invoices_screen(struct app *a) 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(path, sizeof path, "result.items.%zu.paid_date", i); + char *paid = jstr_dup(resp, path); + char nbuf[32], cbuf[256], abuf[40], stbuf[64]; 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); + if (paid && *paid) + snprintf(stbuf, sizeof stbuf, "betald %s", paid); + else + snprintf(stbuf, sizeof stbuf, "%s", + invoice_status_sv(status)); snprintf(line, sizeof line, "%s %s %s %s %s", nbuf, - date ? date : "", cbuf, abuf, - invoice_status_sv(status)); + date ? date : "", cbuf, abuf, stbuf); items[i] = xstrdup(line); free(date); free(cust); free(status); + free(paid); } free(resp); - items[n] = xstrdup("+ Ny faktura (^N)"); + items[n] = xstrdup("+ Ny faktura (n)"); } int start = 0; if (a->invoice_sel) { @@ -1212,6 +1710,12 @@ void invoices_screen(struct app *a) } } int cur = start; + static const struct tui_action lacts[] = { + { "invoice.new", "Ny faktura", 'n', 1, NULL }, + { "invoice.next_number", "Ändra nästa fakturanummer…", + INVOICES_NEXT_NUMBER, 1, NULL }, + }; + tui_set_actions(lacts, 2); int sel = tui_select_list_hook("Fakturor", items, (int)n + 1, start, 1, &cur, 1, NULL, 0, invoices_key, a); if (cur >= 0 && (size_t)cur < n) |
