diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-20 22:00:01 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-20 22:00:01 +0200 |
| commit | 10a535ace098144980d0ffd1593f6384c39cc221 (patch) | |
| tree | 53a7c6c5026d479c4b6d5fd4e5b341474cd190fa /clients/screens_vouchers.c | |
| parent | 3a5a6d3b04dc4e17a42225afb243eecb3502ead6 (diff) | |
| download | bokf-10a535ace098144980d0ffd1593f6384c39cc221.tar.gz bokf-10a535ace098144980d0ffd1593f6384c39cc221.zip | |
tui: split bokftui into screen files
Diffstat (limited to 'clients/screens_vouchers.c')
| -rw-r--r-- | clients/screens_vouchers.c | 958 |
1 files changed, 958 insertions, 0 deletions
diff --git a/clients/screens_vouchers.c b/clients/screens_vouchers.c new file mode 100644 index 0000000..d87cfd2 --- /dev/null +++ b/clients/screens_vouchers.c @@ -0,0 +1,958 @@ +#include <dirent.h> +#include <errno.h> +#include <locale.h> +#include <math.h> +#include <ncursesw/ncurses.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <time.h> +#include <unistd.h> + +#include "client.h" +#include "tui.h" +#include "formula.h" +#include "util.h" +#include "version.h" +#include "yyjson.h" +#include "ui.h" + +struct vdctx { + struct app *a; + int64_t id; + size_t natts; + const char *resp; + int corrected; + int want_refresh; + int posted_new; + int64_t posted_id; +}; + +static int vd_key(void *ud, int ch) +{ + struct vdctx *ctx = ud; + struct app *a = ctx->a; + if (ch == TUI_KEY_CTRL_N) { + int64_t nid = vouchers_new(a); + if (nid > 0) { + ctx->posted_id = nid; + ctx->posted_new = 1; + return TUI_HOOK_BACK; + } + return TUI_HOOK_STAY; + } + if (ch == 'c') { + char reasonbuf[512]; + char *reason = + tui_prompt_into(reasonbuf, sizeof reasonbuf, "Beskriv rättelsen: ", + "", 0) + ? reasonbuf + : NULL; + if (reason && *reason) { + 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, "voucher", ctx->id); + yyjson_mut_obj_add_strcpy(d, o, "description", reason); + char *ref = util_random_id("tui-", 8); + yyjson_mut_obj_add_strcpy(d, o, "client_ref", ref); + char *cargs = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + char *r = client_rpc(&a->conn, "voucher.correct", a->session, + a->org, cargs); + if (r && client_ok(r)) { + int64_t new_id = jint_val(r, "result.id", 0); + tui_message("Rättat", "Ändringsverifikat %lld skapat", + (long long)new_id); + ctx->corrected = 1; + } else { + show_error("Kunde inte rätta", r); + } + free(r); + free(cargs); + free(ref); + } + return TUI_HOOK_BACK; + } + if (ch == 6) { /* ^F: attach a file to this voucher */ + char *path = file_browser(a, a->attachment_dir); + if (path) { + struct stat sb; + if (stat(path, &sb) == 0 && + (long)sb.st_size > a->max_attachment_bytes) { + tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.", + (double)sb.st_size / (1024 * 1024), + a->max_attachment_bytes / (1024 * 1024)); + free(path); + } else { + char *b64 = read_file_b64(path); + if (!b64) { + tui_message("Fel", "Kunde inte läsa %s", path); + free(path); + } else { + const char *base = strrchr(path, '/'); + base = base ? base + 1 : path; + 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, "filename", base); + yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64); + yyjson_mut_obj_add_int(d, o, "voucher_id", ctx->id); + char *fargs = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + free(b64); + free(path); + char *r = client_rpc(&a->conn, "attachment.put", a->session, + a->org, fargs); + if (r && client_ok(r)) + tui_message("Underlag", "Bifogat."); + else + show_error("Kunde inte bifoga", r); + free(r); + free(fargs); + ctx->want_refresh = 1; + return TUI_HOOK_BACK; + } + } + } + return TUI_HOOK_STAY; + } + if (ch == 'f' && ctx->natts > 0) { + char **alines = xcalloc(ctx->natts, sizeof(char *)); + int64_t *aids = xcalloc(ctx->natts, sizeof(int64_t)); + for (size_t i = 0; i < ctx->natts; i++) { + char path[64]; + snprintf(path, sizeof path, "result.attachments.%zu.id", i); + aids[i] = jint_val(ctx->resp, path, 0); + snprintf(path, sizeof path, "result.attachments.%zu.filename", i); + char *fn = jstr_dup(ctx->resp, path); + alines[i] = xstrdup(fn ? fn : "(namnlös)"); + free(fn); + } + int acursor = 0; + 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]); + } else if (asel == -6 && acursor >= 0 && + (size_t)acursor < ctx->natts) { + char q[320]; + snprintf(q, sizeof q, "Ta bort '%s' från verifikatet? (j/n): ", + alines[acursor]); + char ansbuf[512]; + char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, q, "n", 0) + ? ansbuf + : NULL; + if (ans && (ans[0] == 'j' || ans[0] == 'J')) { + char uargs[64]; + snprintf(uargs, sizeof uargs, "{\"id\":%lld,\"voucher_id\":%lld}", + (long long)aids[acursor], (long long)ctx->id); + char *r = client_rpc(&a->conn, "attachment.unlink", a->session, + a->org, uargs); + if (r && client_ok(r)) { + tui_message("Underlag", "Länken borttagen."); + ctx->want_refresh = 1; + for (size_t i = 0; i < ctx->natts; i++) + free(alines[i]); + free(alines); + free(aids); + return TUI_HOOK_REFRESH; + } + show_error("Kunde inte ta bort", r); + free(r); + } + } + for (size_t i = 0; i < ctx->natts; i++) + free(alines[i]); + free(alines); + free(aids); + return TUI_HOOK_STAY; + } + return TUI_HOOK_NONE; +} + +static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) +{ + if (out_new) + *out_new = 0; + char args[64]; + snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id); + for (;;) { + char *resp = + client_rpc(&a->conn, "voucher.get", a->session, a->org, args); + if (!resp || !client_ok(resp)) { + show_error("Verifikat", resp); + free(resp); + return 0; + } + struct buf text; + buf_init(&text); + char *series = jstr_dup(resp, "result.series"); + char *date = jstr_dup(resp, "result.date"); + char *desc = jstr_dup(resp, "result.description"); + char *hash = jstr_dup(resp, "result.hash"); + int64_t number = jint_val(resp, "result.number", 0); + char line[1024]; + snprintf(line, sizeof line, "%s%lld %s %s\n\n", + 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); + 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.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.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); + 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)); + free(acc); + free(name); + } + size_t natts = jarr_size(resp, "result.attachments"); + if (natts) { + buf_append(&text, "\n", 1); + line[0] = MK_RULE[0]; + for (int i = 1; i <= 98; i++) + line[i] = '-'; + line[99] = '\n'; + buf_append(&text, line, 100); + buf_append(&text, MK_HEAD "Underlag:\n", 11); + for (size_t i = 0; i < natts; i++) { + char path[64]; + snprintf(path, sizeof path, "result.attachments.%zu.filename", + i); + char *fn = jstr_dup(resp, path); + snprintf(line, sizeof line, " %s\n", fn ? fn : ""); + buf_append(&text, line, strlen(line)); + free(fn); + } + } + if (hash) { + snprintf(line, sizeof line, "\nHash: %s\n", hash); + buf_append(&text, line, strlen(line)); + } + int64_t corrects = jint_val(resp, "result.corrects_voucher_id", 0); + if (corrects) { + snprintf(line, sizeof line, "Rättar verifikat: %lld\n", + (long long)corrects); + buf_append(&text, line, strlen(line)); + } + buf_append(&text, "\0", 1); + + struct vdctx ctx; + memset(&ctx, 0, sizeof ctx); + ctx.a = a; + ctx.id = id; + ctx.natts = natts; + ctx.resp = resp; + char hint[256]; + snprintf(hint, sizeof hint, + "^N = nytt verifikat c = rätta ^F = bifoga%s", + natts ? " f = underlag" : ""); + int want_refresh = 0, corrected = 0; + int ret = tui_pager_hook("Verifikat", (const char *)text.p, hint, 0, + vd_key, &ctx); + if (ret == 1) + want_refresh = 1; + if (ctx.want_refresh) + want_refresh = 1; + corrected = ctx.corrected; + int posted_new = ctx.posted_new; + int64_t posted_id = ctx.posted_id; + free(series); + free(date); + free(desc); + free(hash); + buf_free(&text); + free(resp); + if (posted_new) { + if (out_new) + *out_new = posted_id; + return 2; + } + if (want_refresh) + continue; + return corrected; + } +} + +struct tui_vrow { + char account[16]; + char debit[32]; + char credit[32]; + char text[64]; +}; + +struct acct_cache { + int loaded; + int64_t org; + char **numbers; + char **names; + size_t n; +}; + +static struct acct_cache g_accts; + +void acct_cache_free(void) +{ + for (size_t i = 0; i < g_accts.n; i++) { + free(g_accts.numbers[i]); + free(g_accts.names[i]); + } + free(g_accts.numbers); + free(g_accts.names); + memset(&g_accts, 0, sizeof g_accts); +} + +static void acct_cache_load(struct app *a) +{ + if (g_accts.loaded && g_accts.org == a->org) + return; + acct_cache_free(); + char *resp = client_rpc(&a->conn, "account.list", a->session, a->org, + "{\"active_only\":true}"); + if (!resp || !client_ok(resp)) { + free(resp); + return; + } + size_t n = jarr_size(resp, "result.items"); + g_accts.numbers = xcalloc(n ? n : 1, sizeof(char *)); + g_accts.names = xcalloc(n ? n : 1, sizeof(char *)); + for (size_t i = 0; i < n; i++) { + char path[64]; + snprintf(path, sizeof path, "result.items.%zu.number", i); + char *number = jstr_dup(resp, path); + snprintf(path, sizeof path, "result.items.%zu.name", i); + char *name = jstr_dup(resp, path); + g_accts.numbers[i] = number ? number : xstrdup(""); + g_accts.names[i] = name ? name : xstrdup(""); + } + g_accts.n = n; + g_accts.org = a->org; + g_accts.loaded = 1; + free(resp); +} + +const char *acct_name(struct app *a, const char *number) +{ + if (!number || !*number) + return NULL; + acct_cache_load(a); + for (size_t i = 0; i < g_accts.n; i++) + if (strcmp(g_accts.numbers[i], number) == 0) + return g_accts.names[i]; + return NULL; +} + +int acct_exists(struct app *a, const char *number) +{ + if (!number || !*number) + return 0; + acct_cache_load(a); + for (size_t i = 0; i < g_accts.n; i++) + if (strcmp(g_accts.numbers[i], number) == 0) + return 1; + return 0; +} + + +struct vform { + struct app *a; + struct tui_rt rt; + struct tui_vrow rows[64]; + int64_t att_ids[32]; + char att_names[32][80]; + int natt; + char date[16]; + char series[16]; + char desc[256]; + char client_ref[64]; +}; + +static void vform_cell(void *ud, int row, int col, char **buf, size_t *cap) +{ + struct vform *vf = ud; + struct tui_vrow *r = &vf->rows[row]; + if (col == 0) { + *buf = r->account; + *cap = sizeof r->account; + } else if (col == 2) { + *buf = r->debit; + *cap = sizeof r->debit; + } else if (col == 3) { + *buf = r->credit; + *cap = sizeof r->credit; + } else { + *buf = r->text; + *cap = sizeof r->text; + } +} + +static void vform_info(void *ud, int row, char *buf, size_t n) +{ + struct vform *vf = ud; + const char *nm = acct_name(vf->a, vf->rows[row].account); + snprintf(buf, n, "%s", nm ? nm : ""); +} + +static void vform_footer(void *ud, char *buf, size_t n) +{ + struct vform *vf = ud; + int64_t sum_d = 0, sum_c = 0; + for (int i = 0; i < vf->rt.nrows; i++) { + int64_t v; + if (tui_parse_kr(vf->rows[i].debit, &v) == 0) + sum_d += v; + if (tui_parse_kr(vf->rows[i].credit, &v) == 0) + sum_c += v; + } + char d1[32], c1[32], diff[32], line[1024]; + tui_kr_format(sum_d, d1, sizeof d1); + tui_kr_format(sum_c, c1, sizeof c1); + tui_kr_format(sum_d - sum_c, diff, sizeof diff); + int balanced = sum_d == sum_c; + struct buf t; + buf_init(&t); + if (vf->natt > 0) { + snprintf(line, sizeof line, "Bilagor: "); + for (int i = 0; i < vf->natt; i++) { + size_t used = strlen(line); + if (used + 1 >= sizeof line) + break; + snprintf(line + used, sizeof line - used, "%.79s%s", + vf->att_names[i], i + 1 < vf->natt ? ", " : ""); + } + buf_append(&t, line, strlen(line)); + buf_append(&t, "\n", 1); + } + snprintf(line, sizeof line, "Summa debet %12s kredit %12s %s", d1, c1, + balanced ? "I BALANS" : "DIFF"); + buf_append(&t, line, strlen(line)); + if (!balanced) { + snprintf(line, sizeof line, "\nDifferens: %s", diff); + buf_append(&t, line, strlen(line)); + } + buf_append(&t, "\0", 1); + snprintf(buf, n, "%s", (char *)t.p); + buf_free(&t); +} + +static void vform_attach(struct vform *vf) +{ + struct app *a = vf->a; + char *path = file_browser(a, a->attachment_dir); + if (!path) + return; + struct stat sb; + if (stat(path, &sb) == 0 && + (long)sb.st_size > a->max_attachment_bytes) { + tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.", + (double)sb.st_size / (1024 * 1024), + a->max_attachment_bytes / (1024 * 1024)); + free(path); + return; + } + char *b64 = read_file_b64(path); + if (!b64) { + tui_message("Bilaga", "Kunde inte läsa %s", path); + free(path); + return; + } + if (vf->natt >= 32) { + tui_message("Bilaga", "Max 32 bilagor per verifikat."); + free(b64); + free(path); + return; + } + const char *base = strrchr(path, '/'); + base = base ? base + 1 : path; + 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, "filename", base); + yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64); + char *args = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + size_t need = strlen(args) + 256; + char *raw = xmalloc(need); + snprintf(raw, need, + "{\"v\":1,\"id\":\"tui\",\"cmd\":\"attachment.put\"," + "\"session\":\"%s\",\"org\":%lld,\"args\":%s}", + a->session, (long long)a->org, args); + free(args); + char *r = NULL; + if (client_send_line(&a->conn, raw) == 0) + r = client_read_line(&a->conn); + free(raw); + if (r && client_ok(r)) { + vf->att_ids[vf->natt] = jint_val(r, "result.id", 0); + snprintf(vf->att_names[vf->natt], sizeof vf->att_names[vf->natt], + "%s", base); + vf->natt++; + } else { + show_error("Kunde inte bifoga filen", r); + } + free(r); + free(b64); + free(path); +} + +static void vform_template(struct vform *vf) +{ + struct app *a = vf->a; + char *lresp = client_rpc(&a->conn, "template.list", a->session, a->org, + "{\"active_only\":true}"); + if (!lresp || !client_ok(lresp)) { + show_error("Mallar", lresp); + free(lresp); + return; + } + size_t ln = jarr_size(lresp, "result.items"); + if (ln == 0) { + tui_message("Mallar", + "Inga mallar ännu. Skapa en under Mallar först."); + free(lresp); + return; + } + char **names = xcalloc(ln, sizeof(char *)); + char **lines = xcalloc(ln, sizeof(char *)); + for (size_t i = 0; i < ln; i++) { + char path[64], line[512]; + snprintf(path, sizeof path, "result.items.%zu.name", i); + char *nm = jstr_dup(lresp, path); + snprintf(path, sizeof path, "result.items.%zu.series", i); + char *ser = jstr_dup(lresp, path); + snprintf(path, sizeof path, "result.items.%zu.row_count", i); + int64_t rc = jint_val(lresp, path, 0); + snprintf(path, sizeof path, "result.items.%zu.description", i); + char *ds = jstr_dup(lresp, path); + char nmbuf[128]; + snprintf(nmbuf, sizeof nmbuf, "%s", nm ? nm : ""); + tui_pad_field(nmbuf, sizeof nmbuf, 24); + snprintf(line, sizeof line, "%s %-3s %2lld rader %s", nmbuf, + ser ? ser : "", (long long)rc, ds ? ds : ""); + names[i] = xstrdup(nm ? nm : ""); + lines[i] = xstrdup(line); + free(nm); + free(ser); + free(ds); + } + int tsel = tui_select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0, + NULL, 0); + char tname[128] = ""; + if (tsel >= 0) + snprintf(tname, sizeof tname, "%s", names[tsel]); + for (size_t i = 0; i < ln; i++) { + free(names[i]); + free(lines[i]); + } + free(names); + free(lines); + free(lresp); + if (tsel < 0) + return; + yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); + yyjson_mut_val *to = yyjson_mut_obj(d); + yyjson_mut_doc_set_root(d, to); + yyjson_mut_obj_add_strcpy(d, to, "name", tname); + char *targs = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + char *resp = + client_rpc(&a->conn, "template.get", a->session, a->org, targs); + free(targs); + if (!resp || !client_ok(resp)) { + show_error("Mall", resp); + free(resp); + return; + } + char xsbuf[512]; + char *xs = tui_prompt_into(xsbuf, sizeof xsbuf, "Belopp (x, kr): ", "", + 0) + ? xsbuf + : NULL; + double xv = 0; + if (xs && *xs && !parse_x_double(xs, &xv)) { + tui_message("Fel", "Ogiltigt belopp."); + free(resp); + return; + } + size_t n = jarr_size(resp, "result.rows"); + char **accts = xcalloc(n ? n : 1, sizeof(char *)); + char **forms = xcalloc(n ? n : 1, sizeof(char *)); + char **descs = xcalloc(n ? n : 1, sizeof(char *)); + struct template_row *in = xcalloc(n ? n : 1, sizeof *in); + for (size_t i = 0; i < n; i++) { + char path[64]; + snprintf(path, sizeof path, "result.rows.%zu.account", i); + accts[i] = jstr_dup(resp, path); + snprintf(path, sizeof path, "result.rows.%zu.formula", i); + forms[i] = jstr_dup(resp, path); + snprintf(path, sizeof path, "result.rows.%zu.description", i); + descs[i] = jstr_dup(resp, path); + in[i].account = accts[i] ? accts[i] : ""; + in[i].formula = forms[i] ? forms[i] : ""; + in[i].description = descs[i] && *descs[i] ? descs[i] : NULL; + } + struct resolved_row *out = xcalloc(n ? n : 1, sizeof *out); + size_t on = 0; + char ferr[256] = ""; + if (formula_resolve_rows(in, n, xv, out, &on, ferr, sizeof ferr) != 0) { + tui_message("Mall", "%s", + ferr[0] ? ferr : "kunde inte lösa mallen"); + } else { + memset(vf->rows, 0, sizeof vf->rows); + int use = (int)(on > 64 ? 64 : on); + for (int i = 0; i < use; i++) { + snprintf(vf->rows[i].account, sizeof vf->rows[i].account, "%s", + out[i].account); + char tmp[32]; + if (out[i].debit_ore) { + tui_kr_format(out[i].debit_ore, tmp, sizeof tmp); + snprintf(vf->rows[i].debit, sizeof vf->rows[i].debit, "%s", + tmp); + } else { + tui_kr_format(out[i].credit_ore, tmp, sizeof tmp); + snprintf(vf->rows[i].credit, sizeof vf->rows[i].credit, "%s", + tmp); + } + if (out[i].description[0]) + snprintf(vf->rows[i].text, sizeof vf->rows[i].text, "%s", + out[i].description); + } + vf->rt.nrows = use > 0 ? use : 1; + char *tds = jstr_dup(resp, "result.description"); + char *tser = jstr_dup(resp, "result.series"); + if (!vf->desc[0] && tds && *tds) + replace_x_into(tds, xv, vf->desc, sizeof vf->desc); + if (tser && *tser) + snprintf(vf->series, sizeof vf->series, "%s", tser); + free(tds); + free(tser); + vf->rt.row = 0; + vf->rt.col = 0; + vf->rt.focus = -1; + vf->rt.fresh = 1; + vf->rt.pos = (size_t)-1; + tui_rt_normalize(&vf->rt); + } + free(out); + for (size_t i = 0; i < n; i++) { + free(accts[i]); + free(forms[i]); + free(descs[i]); + } + free(accts); + free(forms); + free(descs); + free(in); + free(resp); +} + +/* Returns the posted voucher id, or 0 to stay in the form. */ +static int64_t vform_post(struct vform *vf, int dry) +{ + struct app *a = vf->a; + 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, "date", vf->date); + yyjson_mut_obj_add_strcpy(d, o, "description", vf->desc); + yyjson_mut_obj_add_strcpy(d, o, "series", vf->series); + yyjson_mut_obj_add_strcpy(d, o, "client_ref", vf->client_ref); + yyjson_mut_val *arr = yyjson_mut_arr(d); + for (int i = 0; i < vf->rt.nrows; i++) { + if (!vf->rows[i].account[0]) + continue; + int64_t dv = 0, cv = 0; + if (tui_parse_kr(vf->rows[i].debit, &dv) != 0 || + tui_parse_kr(vf->rows[i].credit, &cv) != 0) { + tui_message("Fel", "Ogiltigt belopp på rad %d", i + 1); + yyjson_mut_doc_free(d); + return 0; + } + yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); + yyjson_mut_obj_add_strcpy(d, ro, "account", vf->rows[i].account); + yyjson_mut_obj_add_int(d, ro, "debit_ore", dv); + yyjson_mut_obj_add_int(d, ro, "credit_ore", cv); + if (vf->rows[i].text[0]) + yyjson_mut_obj_add_strcpy(d, ro, "description", + vf->rows[i].text); + } + yyjson_mut_obj_add_val(d, o, "rows", arr); + if (vf->natt > 0) { + yyjson_mut_val *aa = yyjson_mut_arr(d); + for (int i = 0; i < vf->natt; i++) + yyjson_mut_arr_add_int(d, aa, vf->att_ids[i]); + yyjson_mut_obj_add_val(d, o, "attachment_ids", aa); + } + char *args = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + if (dry) { + char *raw = xmalloc(strlen(args) + 256); + sprintf(raw, + "{\"v\":1,\"id\":\"tui\",\"cmd\":\"voucher.post\"," + "\"session\":\"%s\",\"org\":%lld,\"dry_run\":true," + "\"args\":%s}", + a->session, (long long)a->org, args); + if (client_send_line(&a->conn, raw) == 0) { + char *r = client_read_line(&a->conn); + if (r && client_ok(r)) { + int64_t num = jint_val(r, "result.number", 0); + tui_message("Validering OK", + "Nummer %lld skulle tilldelas.", (long long)num); + } else { + show_error("Validering misslyckades", r); + } + free(r); + } else { + tui_message("Fel", "Kunde inte nå servern"); + } + free(raw); + } else { + char *r = client_rpc(&a->conn, "voucher.post", a->session, a->org, + args); + if (r && client_ok(r)) { + int64_t id = jint_val(r, "result.id", 0); + int64_t num = jint_val(r, "result.number", 0); + char *ser = jstr_dup(r, "result.series"); + int replayed = jbool_val(r, "result.replayed", 0); + tui_message("Bokfört", "%s%lld (id %lld)%s", ser ? ser : "", + (long long)num, (long long)id, + replayed ? " — redan bokfört (idempotent)" : ""); + free(ser); + free(r); + free(args); + return id; + } + show_error("Kunde inte bokföra", r); + free(r); + } + free(args); + return 0; +} + +static int vform_key(void *ud, int ch) +{ + struct vform *vf = ud; + if (ch == KEY_F(4)) { + vform_template(vf); + return TUI_HOOK_STAY; + } + if (ch == 6) { + vform_attach(vf); + return TUI_HOOK_STAY; + } + return TUI_HOOK_NONE; +} + +int64_t vouchers_new(struct app *a) +{ + struct vform vf; + memset(&vf, 0, sizeof vf); + vf.a = a; + snprintf(vf.date, sizeof vf.date, "%s", + a->fy_start[0] ? a->fy_start : "2026-01-01"); + snprintf(vf.series, sizeof vf.series, "%s", a->default_series); + char *ref = util_random_id("tui-", 8); + snprintf(vf.client_ref, sizeof vf.client_ref, "%s", ref); + free(ref); + int text_w = COLS - 60; + if (text_w < 8) + text_w = 8; + struct tui_rt_col cols[5] = { + { "Konto", 2, 6, TUI_RT_EDIT }, + { "Namn", 9, 24, TUI_RT_INFO }, + { "Debet", 34, 11, TUI_RT_EDIT }, + { "Kredit", 46, 11, TUI_RT_EDIT }, + { "Text", 58, text_w, TUI_RT_EDIT }, + }; + tui_rt_init(&vf.rt, 1, 64, 5, cols, 7, vform_cell, vform_info, &vf); + tui_rt_set_footer(&vf.rt, vform_footer); + tui_rt_set_key(&vf.rt, vform_key); + struct tui_form_field ff[3]; + memset(ff, 0, sizeof ff); + ff[0].label = "Datum"; + ff[0].value = vf.date; + ff[0].cap = sizeof vf.date; + ff[0].kind = TUI_F_DATE; + ff[1].label = "Serie"; + ff[1].value = vf.series; + ff[1].cap = sizeof vf.series; + ff[1].kind = TUI_F_TEXT; + ff[2].label = "Text"; + ff[2].value = vf.desc; + ff[2].cap = sizeof vf.desc; + ff[2].kind = TUI_F_TEXT; + tui_rt_set_fields(&vf.rt, ff, 3); + const char *hint = "Enter = ändra fält Tab = byta fält F4 = mall" + " ^F = bifoga fil F5 = validera ^X = rensa rad" + " ^Enter = bokför Esc = avbryt"; + for (;;) { + int rr = tui_rt_run("Nytt verifikat", &vf.rt, hint); + if (rr == -1) + return 0; + if (rr == -2) { + vform_post(&vf, 1); + continue; + } + if (rr == -3) { + int64_t id = vform_post(&vf, 0); + if (id > 0) + return id; + } + } +} + +void vouchers_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 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); + 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)"); + } + 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 cur = start; + int sel = tui_select_list("Verifikat", items, (int)total, start, 1, &cur, + 1, NULL, 0); + if (cur >= 0 && (size_t)cur < n) + a->voucher_sel = ids[cur]; + if (sel == -2) { + fetch = 1; + continue; + } + int64_t open_id = 0; + int want_new = (sel == -4); + if (sel >= 0) { + if ((size_t)sel == n) + want_new = 1; + else if ((size_t)sel < n) + open_id = ids[sel]; + } else if (sel != -4) { + for (size_t i = 0; i < n; i++) + free(items[i]); + free(items); + free(ids); + return; + } + if (want_new) { + open_id = vouchers_new(a); + if (open_id <= 0) + continue; + a->voucher_sel = open_id; + fetch = 1; + } + for (;;) { + int64_t nv = 0; + int act = voucher_detail(a, open_id, &nv); + if (act == 2) { + open_id = nv; + a->voucher_sel = nv; + fetch = 1; + continue; + } + if (act == 1) + fetch = 1; + break; + } + } +} |
