diff options
Diffstat (limited to 'clients/screens_vouchers.c')
| -rw-r--r-- | clients/screens_vouchers.c | 437 |
1 files changed, 320 insertions, 117 deletions
diff --git a/clients/screens_vouchers.c b/clients/screens_vouchers.c index 5e1b006..454486e 100644 --- a/clients/screens_vouchers.c +++ b/clients/screens_vouchers.c @@ -20,6 +20,7 @@ #include "version.h" #include "yyjson.h" #include "ui.h" +#include "vlist.h" struct vdctx { struct app *a; @@ -30,13 +31,22 @@ struct vdctx { int want_refresh; int posted_new; int64_t posted_id; + int can_prev, can_next; /* a neighbour exists in the list order */ + int nav; /* -1/+1: show the previous/next voucher */ }; static int vd_key(void *ud, int ch) { struct vdctx *ctx = ud; struct app *a = ctx->a; - if (ch == TUI_KEY_CTRL_N) { + if (ch == KEY_UP || ch == KEY_DOWN) { + int dir = ch == KEY_UP ? -1 : 1; + if (!(dir < 0 ? ctx->can_prev : ctx->can_next)) + return TUI_HOOK_STAY; + ctx->nav = dir; + return TUI_HOOK_BACK; + } + if (ch == 'n') { int64_t nid = vouchers_new(a); if (nid > 0) { ctx->posted_id = nid; @@ -78,7 +88,7 @@ static int vd_key(void *ud, int ch) } return TUI_HOOK_BACK; } - if (ch == 6) { /* ^F: attach a file to this voucher */ + if (ch == 'a') { /* attach a file to this voucher */ char *path = file_browser(a, a->attachment_dir); if (path) { struct stat sb; @@ -137,7 +147,7 @@ static int vd_key(void *ud, int ch) int asel = tui_select_list("Underlag", alines, (int)ctx->natts, 0, 0, &acursor, 0, "ta bort", 0); if (asel >= 0) { - attachment_download(a, aids[asel]); + attachment_view_or_download(a, aids[asel]); } else if (asel == -6 && acursor >= 0 && (size_t)acursor < ctx->natts) { char q[320]; @@ -175,10 +185,48 @@ static int vd_key(void *ud, int ch) return TUI_HOOK_NONE; } -static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) +/* Column widths of the voucher detail; header and rows share them. */ +enum { VD_ACCT_W = 6, VD_NAME_W = 40, VD_AMT_W = 14 }; + +static void vd_amount(int64_t ore, char *buf, size_t n) +{ + if (ore) + tui_kr_format(ore, buf, n); + else + buf[0] = '\0'; +} + +/* One detail line: " konto benämning debet kredit text". Every column + is padded by display width so åäö never shift the ones after it. */ +static void vd_line(struct buf *out, const char *mark, const char *acct, + const char *name, const char *debit, const char *credit, + const char *text) +{ + char acol[64], ncol[256], dcol[64], ccol[64], line[1024]; + snprintf(acol, sizeof acol, "%s", acct); + tui_pad_field(acol, sizeof acol, VD_ACCT_W); + snprintf(ncol, sizeof ncol, "%s", name); + tui_pad_field(ncol, sizeof ncol, VD_NAME_W); + tui_pad_hdr(dcol, sizeof dcol, VD_AMT_W, debit); + tui_pad_hdr(ccol, sizeof ccol, VD_AMT_W, credit); + int n = snprintf(line, sizeof line, "%s %s %s %s %s %s", mark, acol, + ncol, dcol, ccol, text); + while (n > 0 && line[n - 1] == ' ') + n--; + buf_append(out, line, (size_t)n); + buf_append(out, "\n", 1); +} + +/* Returns 0 back, 1 after a correction, 2 when a new voucher was posted + (*out_new), 3 to show the neighbour in *nav (-1/+1) in the list order. + pos/total place the voucher in that order (pos -1: not in the list). */ +static int voucher_detail(struct app *a, int64_t id, int64_t *out_new, + int pos, int total, int *nav) { if (out_new) *out_new = 0; + if (nav) + *nav = 0; char args[64]; snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id); for (;;) { @@ -201,34 +249,38 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) series ? series : "", (long long)number, date ? date : "", desc ? desc : ""); buf_append(&text, line, strlen(line)); - int hdr = snprintf( - line, sizeof line, MK_HEAD " %-6s %-34s D %12s K %12s\n", - "Konto", "Benämning", "Debet", "Kredit"); - buf_append(&text, line, (size_t)hdr); + vd_line(&text, MK_HEAD, "Konto", "Benämning", "Debet", "Kredit", + "Text"); size_t nrows = jarr_size(resp, "result.rows"); + int64_t sum_d = 0, sum_c = 0; for (size_t i = 0; i < nrows; i++) { char path[64]; snprintf(path, sizeof path, "result.rows.%zu.account", i); char *acc = jstr_dup(resp, path); snprintf(path, sizeof path, "result.rows.%zu.name", i); char *name = jstr_dup(resp, path); + snprintf(path, sizeof path, "result.rows.%zu.description", i); + char *rtext = jstr_dup(resp, path); snprintf(path, sizeof path, "result.rows.%zu.debit_ore", i); int64_t debit = jint_val(resp, path, 0); snprintf(path, sizeof path, "result.rows.%zu.credit_ore", i); int64_t credit = jint_val(resp, path, 0); + sum_d += debit; + sum_c += credit; char d[32], c[32]; - tui_kr_format(debit, d, sizeof d); - tui_kr_format(credit, c, sizeof c); - char acol[32], ncol[256]; - snprintf(acol, sizeof acol, "%s", acc ? acc : ""); - tui_pad_field(acol, sizeof acol, 6); - snprintf(ncol, sizeof ncol, "%s", name ? name : ""); - tui_pad_field(ncol, sizeof ncol, 34); - snprintf(line, sizeof line, " %s %s D %12s K %12s\n", acol, - ncol, d, c); - buf_append(&text, line, strlen(line)); + vd_amount(debit, d, sizeof d); + vd_amount(credit, c, sizeof c); + vd_line(&text, "", acc ? acc : "", name ? name : "", d, c, + rtext ? rtext : ""); free(acc); free(name); + free(rtext); + } + { + char d[32], c[32]; + tui_kr_format(sum_d, d, sizeof d); + tui_kr_format(sum_c, c, sizeof c); + vd_line(&text, MK_DIM, "", "Summa", d, c, ""); } size_t natts = jarr_size(resp, "result.attachments"); if (natts) { @@ -267,13 +319,36 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) ctx.id = id; ctx.natts = natts; ctx.resp = resp; - char hint[256]; + ctx.can_prev = pos > 0; + ctx.can_next = pos >= 0 && pos + 1 < total; + char hint[256], title[96]; snprintf(hint, sizeof hint, - "^N = nytt verifikat c = rätta ^F = bifoga%s", + "%sn = nytt c = rätta a = bifoga%s", + pos >= 0 ? "upp/ned = föregående/nästa " : "", natts ? " f = underlag" : ""); + struct tui_action acts[] = { + { "voucher.prev", "Föregående verifikat", KEY_UP, + ctx.can_prev, "första i listan" }, + { "voucher.next", "Nästa verifikat", KEY_DOWN, ctx.can_next, + "sista i listan" }, + { "voucher.correct", "Rätta (ändringsverifikat)…", 'c', 1, NULL }, + { "voucher.attach", "Bifoga fil…", 'a', 1, NULL }, + { "voucher.files", "Underlag…", 'f', natts ? 1 : 0, + "inga underlag" }, + { "voucher.new", "Nytt verifikat", 'n', 1, NULL }, + }; + /* stepping needs a place in the list order */ + tui_set_actions(pos >= 0 ? acts : acts + 2, + pos >= 0 ? 6 : 4); + if (pos >= 0) + snprintf(title, sizeof title, "Verifikat %s%lld (%d av %d)", + series ? series : "", (long long)number, pos + 1, total); + else + snprintf(title, sizeof title, "Verifikat %s%lld", + series ? series : "", (long long)number); int want_refresh = 0, corrected = 0; - int ret = tui_pager_hook("Verifikat", (const char *)text.p, hint, 0, - vd_key, &ctx); + int ret = tui_pager_hook(title, (const char *)text.p, hint, + TUI_PAGER_NO_ARROWS, vd_key, &ctx); if (ret == 1) want_refresh = 1; if (ctx.want_refresh) @@ -294,6 +369,11 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) } if (want_refresh) continue; + if (ctx.nav && !corrected) { + if (nav) + *nav = ctx.nav; + return 3; + } return corrected; } } @@ -743,6 +823,23 @@ static int64_t vform_post(struct vform *vf, int dry) replayed ? " — redan bokfört (idempotent)" : ""); free(ser); free(r); + if (strcmp(vf->series, a->default_series) != 0) { + yyjson_mut_doc *sd = yyjson_mut_doc_new(NULL); + yyjson_mut_val *so = yyjson_mut_obj(sd); + yyjson_mut_doc_set_root(sd, so); + yyjson_mut_obj_add_strcpy(sd, so, "key", "series_voucher"); + yyjson_mut_obj_add_strcpy(sd, so, "value", vf->series); + char *sargs = yyjson_mut_write(sd, 0, NULL); + yyjson_mut_doc_free(sd); + if (sargs) { + char *sr = client_rpc(&a->conn, "settings.set", + a->session, a->org, sargs); + free(sr); + free(sargs); + } + snprintf(a->default_series, sizeof a->default_series, "%s", + vf->series); + } free(args); return id; } @@ -753,14 +850,25 @@ static int64_t vform_post(struct vform *vf, int dry) return 0; } +#define VFORM_TEMPLATE TUI_KEY_ACTION(0) +#define VFORM_ATTACH TUI_KEY_ACTION(1) + +static const struct tui_action VFORM_ACTS[] = { + { "voucher.post", "Bokför", TUI_KEY_SUBMIT, 1, NULL }, + { "voucher.validate", "Validera (torrkörning)", TUI_KEY_REFRESH, 1, + NULL }, + { "voucher.template", "Hämta mall…", VFORM_TEMPLATE, 1, NULL }, + { "voucher.attach", "Bifoga fil…", VFORM_ATTACH, 1, NULL }, +}; + static int vform_key(void *ud, int ch) { struct vform *vf = ud; - if (ch == KEY_F(4)) { + if (ch == VFORM_TEMPLATE) { vform_template(vf); return TUI_HOOK_STAY; } - if (ch == 6) { + if (ch == VFORM_ATTACH) { vform_attach(vf); return TUI_HOOK_STAY; } @@ -789,21 +897,33 @@ int64_t vouchers_new_prefill(struct app *a, const struct voucher_prefill *p) if (p->description && *p->description) snprintf(vf.desc, sizeof vf.desc, "%s", p->description); if (p->bank_account && *p->bank_account) { - snprintf(vf.rows[0].account, sizeof vf.rows[0].account, "%s", - p->bank_account); int64_t amt = p->amount_ore < 0 ? -p->amount_ore : p->amount_ore; char tmp[32]; tui_kr_format(amt, tmp, sizeof tmp); - if (p->amount_ore > 0) + snprintf(vf.rows[0].account, sizeof vf.rows[0].account, "%s", + p->bank_account); + if (p->counter_account && *p->counter_account && + p->amount_ore > 0) { + snprintf(vf.rows[0].debit, sizeof vf.rows[0].debit, "%s", + tmp); + snprintf(vf.rows[1].account, sizeof vf.rows[1].account, "%s", + p->counter_account); + snprintf(vf.rows[1].credit, sizeof vf.rows[1].credit, "%s", + tmp); + } else if (p->amount_ore > 0) { snprintf(vf.rows[0].debit, sizeof vf.rows[0].debit, "%s", tmp); - else if (p->amount_ore < 0) + } else if (p->amount_ore < 0) { snprintf(vf.rows[0].credit, sizeof vf.rows[0].credit, "%s", tmp); + } } - if (p->description && *p->description) + if (p->description && *p->description) { snprintf(vf.rows[0].text, sizeof vf.rows[0].text, "%s", p->description); + snprintf(vf.rows[1].text, sizeof vf.rows[1].text, "%s", + p->description); + } } int text_w = COLS - 60; if (text_w < 8) @@ -834,10 +954,11 @@ int64_t vouchers_new_prefill(struct app *a, const struct voucher_prefill *p) ff[2].kind = TUI_F_TEXT; tui_rt_set_fields(&vf.rt, ff, 3); tui_rt_normalize(&vf.rt); - const char *hint = "Enter = ändra fält Tab = byta fält F4 = mall" - " ^F = bifoga fil F5 = validera ^X = rensa rad" - " ^Enter/F9 = bokför Esc = avbryt"; + const char *hint = "Tab = byta fält Enter = ändra fält" + " ^O = åtgärder (bokför, validera, mall, bifoga)" + " ^X = rensa rad Esc = avbryt"; for (;;) { + tui_set_actions(VFORM_ACTS, 4); int rr = tui_rt_run("Nytt verifikat", &vf.rt, hint); if (rr == -1) return 0; @@ -853,99 +974,172 @@ int64_t vouchers_new_prefill(struct app *a, const struct voucher_prefill *p) } } +struct vlctx { + struct app *a; + int resort; +}; + +static int vlist_key(void *ud, int ch) +{ + struct vlctx *l = ud; + if (ch != 's') + return TUI_HOOK_NONE; + l->a->voucher_sort = (l->a->voucher_sort + 1) % VSORT_COUNT; + config_save(l->a); + l->resort = 1; + return TUI_HOOK_REFRESH; +} + +static void vlist_free(struct vlist_item *v, size_t n) +{ + for (size_t i = 0; i < n; i++) + free(v[i].desc); + free(v); +} + +/* All vouchers of the current year, page by page. NULL after an error + (already shown). */ +static struct vlist_item *vlist_fetch(struct app *a, size_t *out_n) +{ + struct vlist_item *v = NULL; + size_t n = 0, cap = 0; + int64_t cursor = 0; + for (;;) { + char largs[128]; + snprintf(largs, sizeof largs, + "{\"limit\":1000,\"fiscal_year\":%lld,\"cursor\":%lld}", + (long long)a->fy, (long long)cursor); + char *resp = client_rpc(&a->conn, "voucher.list", a->session, a->org, + largs); + if (!resp || !client_ok(resp)) { + show_error("Verifikat", resp); + free(resp); + vlist_free(v, n); + return NULL; + } + size_t m = jarr_size(resp, "result.items"); + if (n + m > cap) { + cap = (n + m) * 2; + v = xrealloc(v, cap * sizeof *v); + } + for (size_t i = 0; i < m; i++) { + struct vlist_item *it = &v[n + i]; + memset(it, 0, sizeof *it); + char path[64]; + snprintf(path, sizeof path, "result.items.%zu.id", i); + it->id = jint_val(resp, path, 0); + snprintf(path, sizeof path, "result.items.%zu.number", i); + it->number = jint_val(resp, path, 0); + snprintf(path, sizeof path, "result.items.%zu.attachment_count", + i); + it->attachments = (int)jint_val(resp, path, 0); + snprintf(path, sizeof path, "result.items.%zu.series", i); + char *ser = jstr_dup(resp, path); + snprintf(it->series, sizeof it->series, "%s", ser ? ser : ""); + free(ser); + snprintf(path, sizeof path, "result.items.%zu.date", i); + char *date = jstr_dup(resp, path); + snprintf(it->date, sizeof it->date, "%s", date ? date : ""); + free(date); + snprintf(path, sizeof path, "result.items.%zu.description", i); + it->desc = jstr_dup(resp, path); + } + n += m; + int64_t next = jint_val(resp, "result.next_cursor", 0); + free(resp); + if (!next || m == 0) + break; + cursor = next; + } + *out_n = n; + return v ? v : xcalloc(1, sizeof *v); +} + +/* List lines for v (plus the trailing "new" row); the caller frees them. */ +static char **vlist_lines(const struct vlist_item *v, size_t n) +{ + int idw = 4; + for (size_t i = 0; i < n; i++) { + char idbuf[48]; + int l = snprintf(idbuf, sizeof idbuf, "%s%lld", v[i].series, + (long long)v[i].number); + if (l > idw) + idw = l; + } + char **items = xcalloc(n + 1, sizeof(char *)); + for (size_t i = 0; i < n; i++) { + char idbuf[48], line[512]; + snprintf(idbuf, sizeof idbuf, "%s%lld", v[i].series, + (long long)v[i].number); + snprintf(line, sizeof line, " %c %-*s %s %s", + v[i].attachments ? 'x' : ' ', idw, idbuf, v[i].date, + v[i].desc ? v[i].desc : ""); + items[i] = xstrdup(line); + } + items[n] = xstrdup("+ Nytt verifikat (n)"); + return items; +} + +static void vlist_lines_free(char **items, size_t n) +{ + for (size_t i = 0; items && i <= n; i++) + free(items[i]); + free(items); +} + void vouchers_screen(struct app *a) { + struct vlist_item *v = NULL; char **items = NULL; - int64_t *ids = NULL; size_t n = 0; - int fetch = 1; + int fetch = 1, resort = 0; for (;;) { - if (g_quit) { - for (size_t i = 0; i < n; i++) - free(items[i]); - free(items); - free(ids); - return; - } + if (g_quit) + break; if (fetch) { - for (size_t i = 0; i < n; i++) - free(items[i]); - free(items); - free(ids); + vlist_lines_free(items, n); + vlist_free(v, n); items = NULL; - ids = NULL; n = 0; - fetch = 0; - char largs[96]; - snprintf(largs, sizeof largs, - "{\"limit\":200,\"fiscal_year\":%lld}", - (long long)a->fy); - char *resp = client_rpc(&a->conn, "voucher.list", a->session, a->org, - largs); - if (!resp || !client_ok(resp)) { - show_error("Verifikat", resp); - free(resp); + v = vlist_fetch(a, &n); + if (!v) return; - } - n = jarr_size(resp, "result.items"); - items = xcalloc(n + 1, sizeof(char *)); - ids = xcalloc(n ? n : 1, sizeof(int64_t)); - char **idstr = xcalloc(n ? n : 1, sizeof(char *)); - int idw = 4; - for (size_t i = 0; i < n; i++) { - char path[64]; - snprintf(path, sizeof path, "result.items.%zu.id", i); - ids[i] = jint_val(resp, path, 0); - snprintf(path, sizeof path, "result.items.%zu.series", i); - char *ser = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.number", i); - int64_t number = jint_val(resp, path, 0); - char idbuf[32]; - snprintf(idbuf, sizeof idbuf, "%s%lld", ser ? ser : "", - (long long)number); - idstr[i] = xstrdup(idbuf); - int l = (int)strlen(idbuf); - if (l > idw) - idw = l; - free(ser); - } - for (size_t i = 0; i < n; i++) { - char path[64], line[512]; - snprintf(path, sizeof path, "result.items.%zu.date", i); - char *date = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.description", i); - char *desc = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.attachment_count", - i); - int atts = (int)jint_val(resp, path, 0); - snprintf(line, sizeof line, " %c %-*s %s %s", - atts ? 'x' : ' ', idw, idstr[i], date ? date : "", - desc ? desc : ""); - items[i] = xstrdup(line); - free(date); - free(desc); - free(idstr[i]); - } - free(idstr); - free(resp); - items[n] = xstrdup("+ Nytt verifikat (^N)"); + fetch = 0; + resort = 1; + } + if (resort) { + vlist_sort(v, n, a->voucher_sort); + vlist_lines_free(items, items ? n : 0); + items = vlist_lines(v, n); + resort = 0; } - size_t total = n + 1; int start = 0; if (a->voucher_sel) { - for (size_t i = 0; i < n; i++) - if (ids[i] == a->voucher_sel) { - start = (int)i; - break; - } + int i = vlist_index(v, n, a->voucher_sel); + if (i >= 0) + start = i; } + char title[96]; + snprintf(title, sizeof title, "Verifikat (sorterade på %s, s = byt)", + vlist_sort_label(a->voucher_sort)); + struct vlctx lctx = { a, 0 }; int cur = start; - int sel = tui_select_list("Verifikat", items, (int)total, start, 1, &cur, - 1, NULL, 0); + struct tui_action lacts[] = { + { "voucher.new", "Nytt verifikat", 'n', 1, NULL }, + { "voucher.sort", "Byt sortering", 's', 1, NULL }, + }; + tui_set_actions(lacts, 2); + tui_list_hint_extra("s = sortera"); + int sel = tui_select_list_hook(title, items, (int)n + 1, start, 1, + &cur, 1, NULL, 0, vlist_key, &lctx); + tui_list_hint_extra(NULL); if (cur >= 0 && (size_t)cur < n) - a->voucher_sel = ids[cur]; + a->voucher_sel = v[cur].id; if (sel == -2) { - fetch = 1; + if (lctx.resort) + resort = 1; + else + fetch = 1; continue; } int64_t open_id = 0; @@ -953,14 +1147,10 @@ void vouchers_screen(struct app *a) if (sel >= 0) { if ((size_t)sel == n) want_new = 1; - else if ((size_t)sel < n) - open_id = ids[sel]; + else + open_id = v[sel].id; } else if (sel != -4) { - for (size_t i = 0; i < n; i++) - free(items[i]); - free(items); - free(ids); - return; + break; } if (want_new) { open_id = vouchers_new(a); @@ -971,7 +1161,18 @@ void vouchers_screen(struct app *a) } for (;;) { int64_t nv = 0; - int act = voucher_detail(a, open_id, &nv); + int nav = 0; + /* a voucher posted since the fetch is not in v: no stepping */ + int pos = fetch ? -1 : vlist_index(v, n, open_id); + int act = voucher_detail(a, open_id, &nv, pos, (int)n, &nav); + if (act == 3) { + int next = vlist_step(n, pos, nav); + if (next < 0) + continue; + open_id = v[next].id; + a->voucher_sel = open_id; + continue; + } if (act == 2) { open_id = nv; a->voucher_sel = nv; @@ -983,4 +1184,6 @@ void vouchers_screen(struct app *a) break; } } + vlist_lines_free(items, n); + vlist_free(v, n); } |
