aboutsummaryrefslogtreecommitdiff
path: root/clients/screens_vouchers.c
diff options
context:
space:
mode:
Diffstat (limited to 'clients/screens_vouchers.c')
-rw-r--r--clients/screens_vouchers.c958
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;
+ }
+ }
+}