From 10a535ace098144980d0ffd1593f6384c39cc221 Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Sun, 20 Sep 2026 22:00:01 +0200 Subject: tui: split bokftui into screen files --- clients/bokftui.c | 7487 +---------------------------------------------------- 1 file changed, 29 insertions(+), 7458 deletions(-) (limited to 'clients/bokftui.c') diff --git a/clients/bokftui.c b/clients/bokftui.c index f00bdc1..84d1a18 100644 --- a/clients/bokftui.c +++ b/clients/bokftui.c @@ -19,7352 +19,44 @@ #include "util.h" #include "version.h" #include "yyjson.h" +#include "ui.h" -/* universal "new/add" hotkey: ^N (0x0e), the de-facto Linux convention */ -#define KEY_CTRL_N 0x0e -/* ^X clears the focused row in row editors */ -#define KEY_CTRL_X 0x18 -/* ^Enter saves/posts the form; sent via xterm modifyOtherKeys */ -#define KEY_CTRL_ENTER (KEY_MAX + 1) -/* Pager markup: headings, dimmed derived lines and full-width rules. */ -#define MK_HEAD "\001" -#define MK_DIM "\002" -#define MK_RULE "\003" -#define MK_STICKY "\004" -/* Menu item prefix for a non-selectable section header. */ -#define MK_SECTION MK_HEAD -/* ^C quits the application; Esc is navigation only */ -static int g_quit = 0; -/* ^R re-execs the binary, keeping session, org, year and screen */ -static int g_reload = 0; -static char g_scene[32] = "dashboard"; -static char g_reload_scene[32]; - -static int ui_getch(void) -{ - int ch = getch(); - if (ch == 3) { /* ^C */ - g_quit = 1; - return 27; /* behave like "back" so screens unwind */ - } - if (ch == 18) { /* ^R */ - g_reload = 1; - snprintf(g_reload_scene, sizeof g_reload_scene, "%s", g_scene); - return 27; - } - return ch; -} - -static void request_quit(void) -{ - g_quit = 1; -} - -struct app { - struct client_conn conn; - char socket[256]; - char session[128]; - char username[64]; - char password[128]; /* typed or from BOKFD_PASSWORD, kept for ^R */ - int64_t org; - char org_name[128]; - char role[32]; - int64_t fy; - int64_t voucher_sel; /* last selected voucher id in the list view */ - int64_t invoice_sel; /* last selected invoice id in the list view */ - int64_t customer_sel; /* last selected customer id in the list view */ - char default_series[16]; - char attachment_dir[256]; - long max_attachment_bytes; - char fy_label[64]; - char fy_start[16]; - char fy_end[16]; -}; - -/* ------------------------------------------------------------------ */ -/* json helpers */ -/* ------------------------------------------------------------------ */ - -static yyjson_val *jget(yyjson_val *root, const char *path) -{ - char tmp[128]; - snprintf(tmp, sizeof tmp, "%s", path); - yyjson_val *v = root; - char *save = NULL; - for (char *tok = strtok_r(tmp, ".", &save); tok; - tok = strtok_r(NULL, ".", &save)) { - if (!v) - return NULL; - if (yyjson_is_obj(v)) - v = yyjson_obj_get(v, tok); - else if (yyjson_is_arr(v)) { - char *end = NULL; - long idx = strtol(tok, &end, 10); - if (!end || *end) - return NULL; - v = yyjson_arr_get(v, (size_t)idx); - } else { - return NULL; - } - } - return v; -} - -static yyjson_doc *parse(const char *resp) -{ - return resp ? yyjson_read(resp, strlen(resp), 0) : NULL; -} - -static char *jstr_dup(const char *resp, const char *path) -{ - yyjson_doc *d = parse(resp); - yyjson_val *v = d ? jget(yyjson_doc_get_root(d), path) : NULL; - char *out = v && yyjson_is_str(v) ? xstrdup(yyjson_get_str(v)) : NULL; - yyjson_doc_free(d); - return out; -} - -static int64_t jint_val(const char *resp, const char *path, int64_t def) -{ - yyjson_doc *d = parse(resp); - yyjson_val *v = d ? jget(yyjson_doc_get_root(d), path) : NULL; - int64_t out = v && yyjson_is_int(v) ? yyjson_get_int(v) : def; - yyjson_doc_free(d); - return out; -} - -static size_t jarr_size(const char *resp, const char *path) -{ - yyjson_doc *d = parse(resp); - yyjson_val *v = d ? jget(yyjson_doc_get_root(d), path) : NULL; - size_t n = v && yyjson_is_arr(v) ? yyjson_arr_size(v) : 0; - yyjson_doc_free(d); - return n; -} - -static int jbool_val(const char *resp, const char *path, int def) -{ - yyjson_doc *d = parse(resp); - yyjson_val *v = d ? jget(yyjson_doc_get_root(d), path) : NULL; - int out = v && yyjson_is_bool(v) ? yyjson_get_bool(v) : def; - yyjson_doc_free(d); - return out; -} - -/* ------------------------------------------------------------------ */ -/* ui primitives */ -/* ------------------------------------------------------------------ */ - -static char g_server_version[32]; - -static void tui_log(const char *fmt, ...) - __attribute__((format(printf, 1, 2))); - -static void show_error(const char *title, const char *resp); -static void bokslut_screen(struct app *a); -static void arsredovisning_screen(struct app *a); -static char *rpc_dry(struct app *a, const char *cmd, const char *args); -static int64_t vouchers_new(struct app *a); -static void config_mkdirs(const char *path); - -/* Whole kronor with space thousands, öre truncated like the blankett. */ -static void kr_whole(int64_t ore, char *buf, size_t n) -{ - char tmp[48]; - tui_kr_format((ore / 100) * 100, tmp, sizeof tmp); - char *comma = strchr(tmp, ','); - if (comma) - *comma = '\0'; - snprintf(buf, n, "%s", tmp); -} - -static int parse_x_double(const char *s, double *out) -{ - if (!s) - return 0; - char buf[64]; - size_t j = 0; - for (const char *p = s; *p && j < sizeof buf - 1; p++) { - if (*p == ' ' || *p == '\t') - continue; - buf[j++] = (*p == ',') ? '.' : *p; - } - buf[j] = '\0'; - if (!j) - return 0; - char *end = NULL; - double v = strtod(buf, &end); - if (!end || *end) - return 0; - *out = v; - return 1; -} - -static void replace_x_into(const char *src, double x, char *dst, size_t cap) -{ - size_t o = 0; - for (const char *p = src; *p && o + 1 < cap;) { - if (p[0] == '{' && p[1] == 'x' && p[2] == '}') { - char tmp[64]; - snprintf(tmp, sizeof tmp, "%.2f", x); - for (size_t k = 0; tmp[k] && o + 1 < cap; k++) - dst[o++] = tmp[k]; - p += 3; - } else { - dst[o++] = *p++; - } - } - dst[o] = '\0'; -} - -/* Display width in terminal columns, counting UTF-8 lead bytes. Good enough - for Swedish text; combining marks are ignored. */ - - -/* ------------------------------------------------------------------ */ -/* app helpers */ -/* ------------------------------------------------------------------ */ - -static void app_refresh_context(struct app *a) -{ - char args[64]; - snprintf(args, sizeof args, "{\"org\":%lld}", (long long)a->org); - char *resp = client_rpc(&a->conn, "session.use_org", a->session, 0, args); - free(resp); - - resp = client_rpc(&a->conn, "org.get", a->session, a->org, "{}"); - if (resp && client_ok(resp)) { - char *name = jstr_dup(resp, "result.name"); - if (name) { - snprintf(a->org_name, sizeof a->org_name, "%s", name); - free(name); - } - } - free(resp); - - resp = client_rpc(&a->conn, "org.list", a->session, 0, "{}"); - if (resp) { - size_t n = jarr_size(resp, "result.items"); - for (size_t i = 0; i < n; i++) { - char path[64]; - snprintf(path, sizeof path, "result.items.%zu.id", i); - if (jint_val(resp, path, -1) == a->org) { - snprintf(path, sizeof path, "result.items.%zu.role", i); - char *role = jstr_dup(resp, path); - if (role) { - snprintf(a->role, sizeof a->role, "%s", role); - free(role); - } - } - } - } - free(resp); - - snprintf(a->default_series, sizeof a->default_series, "%s", "A"); - a->attachment_dir[0] = '\0'; - resp = client_rpc(&a->conn, "settings.get", a->session, a->org, "{}"); - if (resp && client_ok(resp)) { - char *ser = jstr_dup(resp, "result.default_series"); - if (ser && *ser) - snprintf(a->default_series, sizeof a->default_series, "%s", ser); - free(ser); - char *dir = jstr_dup(resp, "result.attachment_dir"); - if (dir && *dir) - snprintf(a->attachment_dir, sizeof a->attachment_dir, "%s", dir); - free(dir); - } - free(resp); - - a->max_attachment_bytes = 10 * 1024 * 1024; - resp = client_rpc(&a->conn, "meta", NULL, 0, NULL); - if (resp && client_ok(resp)) { - char *ver = jstr_dup(resp, "result.version"); - if (ver && *ver) - snprintf(g_server_version, sizeof g_server_version, "%s", ver); - free(ver); - int64_t v = - jint_val(resp, "result.limits.max_attachment_bytes", 0); - if (v > 0) - a->max_attachment_bytes = (long)v; - } - free(resp); - - resp = client_rpc(&a->conn, "fiscal_year.get", a->session, a->org, "{}"); - if (resp && client_ok(resp)) { - a->fy = jint_val(resp, "result.id", 0); - char *label = jstr_dup(resp, "result.label"); - char *start = jstr_dup(resp, "result.start_date"); - char *end = jstr_dup(resp, "result.end_date"); - if (label) - snprintf(a->fy_label, sizeof a->fy_label, "%s", label); - if (start) - snprintf(a->fy_start, sizeof a->fy_start, "%s", start); - if (end) - snprintf(a->fy_end, sizeof a->fy_end, "%s", end); - free(label); - free(start); - free(end); - } - free(resp); -} - -static void update_status(struct app *a) -{ - char st[512], right[160]; - snprintf(st, sizeof st, "%s | %s - %s | %s | %s", a->org_name, - a->fy_start, a->fy_end, a->role, a->username); - if (g_server_version[0]) - snprintf(right, sizeof right, "tui %s srv %s", BOKF_VERSION, - g_server_version); - else - snprintf(right, sizeof right, "tui %s", BOKF_VERSION); - tui_set_status(st, right); -} - - -/* Create a new fiscal year. Returns the new id (>0) or 0. */ -static int64_t fy_new_form(struct app *a) -{ - static int sel = 0; - char label[32] = "", start[16] = "", end[16] = ""; - char *resp = - client_rpc(&a->conn, "fiscal_year.list", a->session, a->org, "{}"); - if (resp && client_ok(resp)) { - size_t n = jarr_size(resp, "result.items"); - char max_end[16] = ""; - for (size_t i = 0; i < n; i++) { - char path[64]; - snprintf(path, sizeof path, "result.items.%zu.end_date", i); - char *e = jstr_dup(resp, path); - if (e && strcmp(e, max_end) > 0) - snprintf(max_end, sizeof max_end, "%s", e); - free(e); - } - if (max_end[0]) - util_date_add_days(max_end, 1, start, sizeof start); - } - free(resp); - if (!start[0]) { - time_t t = time(NULL); - struct tm tm; - gmtime_r(&t, &tm); - int y = tm.tm_year + 1900; - if (y < 1900 || y > 2200) - y = 2026; - snprintf(start, sizeof start, "%04d-01-01", y); - } - util_date_add_months(start, 12, end, sizeof end); - util_date_add_days(end, -1, end, sizeof end); - int sy = 0, sm = 0, sd = 0, ey = 0, em = 0, ed = 0; - util_date_parse(start, &sy, &sm, &sd); - util_date_parse(end, &ey, &em, &ed); - if (sm == 1 && sd == 1) - snprintf(label, sizeof label, "%d", ey); - else - snprintf(label, sizeof label, "%d/%d", sy, ey); - - struct tui_form_field ff[3]; - for (;;) { - memset(ff, 0, sizeof ff); - ff[0].label = "Etikett"; - ff[0].value = label; - ff[0].cap = sizeof label; - ff[0].kind = TUI_F_TEXT; - ff[1].label = "Startdatum"; - ff[1].value = start; - ff[1].cap = sizeof start; - ff[1].kind = TUI_F_DATE; - ff[2].label = "Slutdatum"; - ff[2].value = end; - ff[2].cap = sizeof end; - ff[2].kind = TUI_F_DATE; - int r = tui_form_run("Nytt räkenskapsår", ff, 3, 1, &sel); - if (r == TUI_FORM_BACK) - return 0; - if (r != TUI_FORM_REFRESH && r != TUI_FORM_SUBMIT) - continue; /* a field was edited; keep the form up */ - if (!label[0]) { - tui_message("Räkenskapsår", "Etiketten får inte vara tom."); - continue; - } - if (!util_parse_iso_date(start) || !util_parse_iso_date(end)) { - tui_message("Räkenskapsår", - "Start- och slutdatum måste vara ÅÅÅÅ-MM-DD."); - continue; - } - if (strcmp(start, end) >= 0) { - tui_message("Räkenskapsår", - "Startdatum måste ligga före slutdatum."); - continue; - } - if (r == TUI_FORM_REFRESH) { - tui_message("Validering OK", "%s %s - %s", label, start, end); - continue; - } - 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, "label", label); - yyjson_mut_obj_add_strcpy(d, o, "start_date", start); - yyjson_mut_obj_add_strcpy(d, o, "end_date", end); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *oresp = client_rpc(&a->conn, "fiscal_year.open", a->session, - a->org, args); - free(args); - if (oresp && client_ok(oresp)) { - int64_t id = jint_val(oresp, "result.id", 0); - tui_message("Räkenskapsår", "%s skapat.", label); - free(oresp); - return id; - } - show_error("Kunde inte skapa räkenskapsåret", oresp); - free(oresp); - } -} - -/* Pick which fiscal year the screens work in. Shown as its real range so - broken fiscal years (not starting in January) are obvious. Ctrl+A closes - or reopens the highlighted year. */ -static void select_fiscal_year(struct app *a) -{ - int64_t prefer = a->fy; - int cursor = -1; - for (;;) { - if (g_quit) - return; - char *resp = - client_rpc(&a->conn, "fiscal_year.list", a->session, a->org, "{}"); - if (!resp || !client_ok(resp)) { - show_error("Räkenskapsår", resp); - free(resp); - return; - } - size_t n = jarr_size(resp, "result.items"); - if (n == 0) { - free(resp); - int64_t id = fy_new_form(a); - if (id > 0) - prefer = id; - continue; - } - char **lines = xcalloc(n, sizeof(char *)); - int64_t *ids = xcalloc(n, sizeof(int64_t)); - int *closed = xcalloc(n, sizeof(int)); - char (*ranges)[40] = xcalloc(n, sizeof *ranges); - char (*labels)[64] = xcalloc(n, sizeof *labels); - int sel_default = 0; - 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.label", i); - char *label = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.start_date", i); - char *sdate = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.end_date", i); - char *edate = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.status", i); - char *status = jstr_dup(resp, path); - snprintf(ranges[i], sizeof ranges[i], "%s - %s", - sdate ? sdate : "?", edate ? edate : "?"); - snprintf(labels[i], sizeof labels[i], "%s", label ? label : ""); - closed[i] = strcmp(status ? status : "", "closed") == 0; - char line[256], stbuf[16]; - snprintf(stbuf, sizeof stbuf, "%s", closed[i] ? "stängd" : "öppen"); - tui_pad_field(stbuf, sizeof stbuf, 6); - snprintf(line, sizeof line, "%-25s %s %s", ranges[i], stbuf, - labels[i]); - lines[i] = xstrdup(line); - if (ids[i] == prefer) - sel_default = (int)i; - free(label); - free(sdate); - free(edate); - free(status); - } - int start = cursor >= 0 ? cursor : sel_default; - if (start >= (int)n) - start = (int)n - 1; - int sel = tui_select_list("Räkenskapsår", lines, (int)n, start, 0, - &cursor, 1, NULL, 1); - for (size_t i = 0; i < n; i++) - free(lines[i]); - free(lines); - free(resp); - if (sel == -4) { - free(ids); - free(closed); - free(ranges); - free(labels); - int64_t id = fy_new_form(a); - if (id > 0) - prefer = id; - continue; - } - if (sel == -7) { - if (cursor >= 0 && (size_t)cursor < n) { - char label[192]; - snprintf(label, sizeof label, "%s räkenskapsåret %s? (j/n): ", - closed[cursor] ? "Återöppna" : "Avsluta", - labels[cursor]); - char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, label, "n", 0) ? ansbuf : NULL; - if (ans && (ans[0] == 'j' || ans[0] == 'J')) { - char args[64]; - snprintf(args, sizeof args, - "{\"id\":%lld,\"confirm\":true}", - (long long)ids[cursor]); - char *r = client_rpc(&a->conn, - closed[cursor] - ? "fiscal_year.reopen" - : "fiscal_year.close", - a->session, a->org, args); - if (r && client_ok(r)) - tui_message("Räkenskapsår", "Räkenskapsåret %s är %s.", - labels[cursor], - closed[cursor] ? "återöppnat" : "avslutat"); - else - show_error("Kunde inte ändra status", r); - free(r); - } - } - free(ids); - free(closed); - free(ranges); - free(labels); - continue; - } - if (sel < 0) { - free(ids); - free(closed); - free(ranges); - free(labels); - return; - } - a->fy = ids[sel]; - snprintf(a->fy_label, sizeof a->fy_label, "%s", labels[sel]); - /* range is "start - end" */ - snprintf(a->fy_start, sizeof a->fy_start, "%.10s", ranges[sel]); - snprintf(a->fy_end, sizeof a->fy_end, "%s", ranges[sel] + 13); - free(ids); - free(closed); - free(ranges); - free(labels); - a->voucher_sel = 0; - update_status(a); - return; - } -} - -static void show_error(const char *title, const char *resp) -{ - char *code = jstr_dup(resp, "error.code"); - char *msg = jstr_dup(resp, "error.message"); - if ((!code || !*code) && (!msg || !*msg)) { - tui_message(title, "%s", resp && *resp - ? resp - : "Inget svar från servern (kör daemonen?)"); - } else { - tui_message(title, "%s: %s", code && *code ? code : "fel", - msg && *msg ? msg : "okänt fel"); - } - free(code); - free(msg); -} - -static char *read_file_b64(const char *path) -{ - FILE *f = fopen(path, "rb"); - if (!f) - return NULL; - struct buf b; - buf_init(&b); - unsigned char chunk[65536]; - size_t rn; - while ((rn = fread(chunk, 1, sizeof chunk, f)) > 0) - buf_append(&b, chunk, rn); - int bad = ferror(f); - fclose(f); - if (bad) { - buf_free(&b); - return NULL; - } - char *b64 = util_b64(b.p ? b.p : (const unsigned char *)"", b.len); - buf_free(&b); - return b64; -} - -/* ------------------------------------------------------------------ */ -/* screens */ -/* ------------------------------------------------------------------ */ - -struct fentry { - char name[300]; - int isdir; -}; - -static int fentry_cmp(const void *a, const void *b) -{ - const struct fentry *x = a; - const struct fentry *y = b; - if (x->isdir != y->isdir) - return y->isdir - x->isdir; - return strcasecmp(x->name, y->name); -} - -/* Expands a leading ~ to $HOME (no ~user support). */ -static void expand_path(const char *in, char *out, size_t n) -{ - if (in && in[0] == '~' && (in[1] == '\0' || in[1] == '/')) { - const char *home = getenv("HOME"); - if (home) - snprintf(out, n, "%s%s", home, in + 1); - else - snprintf(out, n, "%s", in[1] ? in + 2 : "."); - } else { - snprintf(out, n, "%s", in ? in : "."); - } -} - -/* File browser for choosing underlag. Starts in start_dir or $HOME. - Returns a malloc'd path, or NULL on cancel. */ -static char *file_browser(struct app *a, const char *start_dir) -{ - (void)a; - if (g_quit) - return NULL; - char dir[1024]; - const char *start = (start_dir && *start_dir) ? start_dir : getenv("HOME"); - expand_path(start, dir, sizeof dir); - if (!dir[0]) - snprintf(dir, sizeof dir, "."); - for (;;) { - DIR *d = opendir(dir); - if (!d) { - tui_message("Filväljare", "Kan inte öppna %s", dir); - return NULL; - } - struct fentry *ents = NULL; - size_t n = 0, cap = 0; - struct dirent *de; - while ((de = readdir(d)) != NULL) { - if (strcmp(de->d_name, ".") == 0 || - strcmp(de->d_name, "..") == 0) - continue; /* ".." is the first item in the list */ - if (de->d_name[0] == '.') - continue; /* hidden files */ - char full[1600]; - snprintf(full, sizeof full, "%.1200s/%.299s", dir, de->d_name); - struct stat st; - if (stat(full, &st) != 0) - continue; - if (n == cap) { - cap = cap ? cap * 2 : 64; - ents = xrealloc(ents, cap * sizeof *ents); - } - snprintf(ents[n].name, sizeof ents[n].name, "%s", de->d_name); - ents[n].isdir = S_ISDIR(st.st_mode); - n++; - if (n >= 2000) - break; - } - closedir(d); - qsort(ents, n, sizeof *ents, fentry_cmp); - char **items = xcalloc(n + 1, sizeof(char *)); - items[0] = xstrdup(".. (uppåt)"); - for (size_t i = 0; i < n; i++) { - char line[340]; - snprintf(line, sizeof line, "%s%s", ents[i].name, - ents[i].isdir ? "/" : ""); - items[i + 1] = xstrdup(line); - } - char title[1200]; - snprintf(title, sizeof title, "Välj fil — %.200s", dir); - int sel = tui_select_list(title, items, (int)n + 1, 0, 1, NULL, 0, NULL, 0); - for (size_t i = 0; i <= n; i++) - free(items[i]); - free(items); - if (sel < 0) { - free(ents); - return NULL; - } - if (sel == 0) { - char *slash = strrchr(dir, '/'); - if (slash && slash != dir) - *slash = '\0'; - else if (slash == dir) - dir[1] = '\0'; - free(ents); - continue; - } - size_t idx = (size_t)sel - 1; - char full[1600]; - snprintf(full, sizeof full, "%.1200s/%.299s", dir, ents[idx].name); - if (ents[idx].isdir) { - snprintf(dir, sizeof dir, "%.1023s", full); - free(ents); - continue; - } - free(ents); - return xstrdup(full); - } -} - -/* Valid UTF-8 without NUL bytes: good enough to call a receipt text. */ -static int bytes_look_text(const unsigned char *p, size_t n) -{ - for (size_t i = 0; i < n;) { - unsigned char c = p[i]; - if (c == 0) - return 0; - if (c < 0x80) { - i++; - continue; - } - int len; - if ((c & 0xE0) == 0xC0) - len = 2; - else if ((c & 0xF0) == 0xE0) - len = 3; - else if ((c & 0xF8) == 0xF0) - len = 4; - else - return 0; - if (i + (size_t)len > n) - return 0; - for (int k = 1; k < len; k++) - if ((p[i + k] & 0xC0) != 0x80) - return 0; - i += (size_t)len; - } - return 1; -} - -/* Fetch one attachment, save it to a prompted path and verify its hash. - Text attachments are shown inline after saving. */ -static void attachment_download(struct app *a, int64_t att_id) -{ - char args[64]; - snprintf(args, sizeof args, "{\"id\":%lld}", (long long)att_id); - char *resp = - client_rpc(&a->conn, "attachment.get", a->session, a->org, args); - if (!resp || !client_ok(resp)) { - show_error("Kunde inte hämta underlaget", resp); - free(resp); - return; - } - char *fn = jstr_dup(resp, "result.filename"); - char *mime = jstr_dup(resp, "result.mime"); - char *sha = jstr_dup(resp, "result.sha256"); - char *b64 = jstr_dup(resp, "result.content_base64"); - unsigned char *data = NULL; - size_t n = 0; - if (!b64 || util_b64_decode(b64, strlen(b64), &data, &n) != 0) { - tui_message("Underlag", "Kunde inte avkoda innehållet."); - goto done; - } - const char *home = getenv("HOME"); - char def[600], dl[512]; - snprintf(dl, sizeof dl, "%s/Downloads", home && *home ? home : "."); - struct stat sb; - if (stat(dl, &sb) != 0 || !S_ISDIR(sb.st_mode)) - snprintf(dl, sizeof dl, "%s", home && *home ? home : "."); - snprintf(def, sizeof def, "%s/%s", dl, fn && *fn ? fn : "underlag"); - char pathbuf[512]; char *path = tui_prompt_into(pathbuf, sizeof pathbuf, "Spara underlag: ", def, 0) ? pathbuf : NULL; - if (!path || !*path) - goto done; - char full[600]; - if (path[0] == '~' && (path[1] == '/' || path[1] == '\0')) - snprintf(full, sizeof full, "%s%s", home ? home : "", path + 1); - else - snprintf(full, sizeof full, "%s", path); - if (stat(full, &sb) == 0) { - char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, "Filen finns, skriv över? (j/n): ", "n", 0) ? ansbuf : NULL; - if (!ans || (ans[0] != 'j' && ans[0] != 'J')) - goto done; - } - FILE *f = fopen(full, "wb"); - if (!f || fwrite(data, 1, n, f) != n) { - tui_message("Underlag", "Kunde inte skriva %s: %s", full, strerror(errno)); - if (f) - fclose(f); - goto done; - } - fclose(f); - unsigned char raw[32]; - char hex[65]; - util_sha256(data, n, raw); - util_hex(raw, sizeof raw, hex); - if (sha && *sha && strcmp(hex, sha) != 0) - tui_message("Underlag", "VARNING: kontrollsumman stämmer inte.\nSparat: %s", - full); - else - tui_message("Underlag", "Sparat: %s\n(%zu byte)", full, n); - int is_text = (mime && strncmp(mime, "text/", 5) == 0) || - (n > 0 && bytes_look_text(data, n)); - if (is_text && n <= 256 * 1024) { - char *copy = xmalloc(n + 1); - memcpy(copy, data, n); - copy[n] = '\0'; - tui_pager("Underlag", copy, NULL); - free(copy); - } -done: - free(data); - free(b64); - free(sha); - free(mime); - free(fn); - free(resp); -} - -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; - -static 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); -} - -static 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; -} - -static 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; -} - -static 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; - } - } -} - -static 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; - } - } -} - -static const char *type_sv(const char *t) -{ - if (!t) - return ""; - if (strcmp(t, "asset") == 0) - return "Tillgång"; - if (strcmp(t, "liability") == 0) - return "Skuld"; - if (strcmp(t, "equity") == 0) - return "Eget kapital"; - if (strcmp(t, "revenue") == 0) - return "Intäkt"; - if (strcmp(t, "expense") == 0) - return "Kostnad"; - return t; -} - -/* Moms treatment for an account: the stored vat_code when set, otherwise - derived from the BAS account name (which carries the rate). Accounts - whose treatment depends on the transaction stay blank. */ -static const char *vat_display(const char *name, const char *vat_code, - char *buf, size_t cap) -{ - if (vat_code && *vat_code) { - snprintf(buf, cap, "%s", vat_code); - return buf; - } - if (!name || !*name) - return ""; - int pct = 0; - for (const char *p = name; *p;) { - if (*p >= '0' && *p <= '9') { - const char *q = p; - int v = 0; - while (*q >= '0' && *q <= '9') { - v = v * 10 + (*q - '0'); - q++; - } - while (*q == ' ') - q++; - if (*q == '%') { - pct = v; - break; - } - p = q; - continue; - } - p++; - } - int in = strstr(name, "Ingående moms") != NULL; - int out = strstr(name, "Utgående moms") != NULL; - int reverse = strstr(name, "omvänd") != NULL; - if (in || out) { - if (pct) - snprintf(buf, cap, "%s %d%%", in ? "Ing" : "Utg", pct); - else - snprintf(buf, cap, "%s", in ? "Ing" : "Utg"); - return buf; - } - if (pct) { - snprintf(buf, cap, "%d%%%s", pct, reverse ? " omv" : ""); - return buf; - } - if (strstr(name, "momsfri") || strstr(name, "momsfritt") || - strstr(name, "momsefri")) - return "Momsfri"; - if (strstr(name, "Redovisningskonto för moms")) - return "Avräkning"; - if (reverse) - return "Omvänd"; - if (strstr(name, "moms")) - return "Moms"; - return ""; -} - -/* Kontolista: every account with the details that apply to it. */ -static void accounts_report(struct app *a) -{ - for (;;) { - char *resp = client_rpc(&a->conn, "account.list", a->session, a->org, - "{\"active_only\":false}"); - if (!resp || !client_ok(resp)) { - show_error("Kontolista", resp); - free(resp); - return; - } - size_t n = jarr_size(resp, "result.items"); - int name_w = COLS - 45; - if (name_w < 12) - name_w = 12; - struct buf text; - buf_init(&text); - char line[2048]; - int hdr = snprintf(line, sizeof line, - MK_STICKY "%-6s %-*s %-12s %-5s %-6s %-8s\n", - "Konto", name_w, "Namn", "Typ", "Aktiv", "SRU", - "Moms"); - buf_append(&text, line, (size_t)hdr); - int sep_w = 6 + 1 + name_w + 1 + 12 + 1 + 5 + 1 + 6 + 1 + 8; - for (int i = 0; i < sep_w && i < (int)sizeof line - 1; i++) - line[i] = '-'; - line[sep_w < (int)sizeof line ? sep_w : (int)sizeof line - 1] = '\n'; - buf_append(&text, line, (size_t)(sep_w < (int)sizeof line ? sep_w + 1 - : (int)sizeof line)); - int64_t active_count = 0; - 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); - snprintf(path, sizeof path, "result.items.%zu.type", i); - char *type = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.active", i); - int active = jbool_val(resp, path, 0); - snprintf(path, sizeof path, "result.items.%zu.sru_code", i); - char *sru = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.vat_code", i); - char *vat = jstr_dup(resp, path); - char ncol[512]; - char vatbuf[32]; - char tibuf[32]; - snprintf(ncol, sizeof ncol, "%s", name ? name : ""); - tui_pad_field(ncol, sizeof ncol, name_w); - snprintf(tibuf, sizeof tibuf, "%s", type_sv(type)); - tui_pad_field(tibuf, sizeof tibuf, 12); - snprintf(line, sizeof line, "%-6s %s %s %-5s %-6s %-8s\n", - number ? number : "", ncol, tibuf, - active ? "ja" : "nej", sru ? sru : "", - vat_display(name, vat, vatbuf, sizeof vatbuf)); - buf_append(&text, line, strlen(line)); - if (active) - active_count++; - free(number); - free(name); - free(type); - free(sru); - free(vat); - } - snprintf(line, sizeof line, "\n%lld konton, varav %lld aktiva.\n", - (long long)n, (long long)active_count); - buf_append(&text, line, strlen(line)); - buf_append(&text, "\0", 1); - int again = tui_pager("Kontolista", (char *)text.p, NULL); - buf_free(&text); - free(resp); - if (!again) - return; - } -} - -/* ------------------------------------------------------------------ */ -/* report formatting (Kapitas-like text tables) */ -/* ------------------------------------------------------------------ */ - -static const char *vstr(yyjson_val *o, const char *k) -{ - yyjson_val *v = o ? yyjson_obj_get(o, k) : NULL; - return v && yyjson_is_str(v) ? yyjson_get_str(v) : ""; -} - -static int64_t vint(yyjson_val *o, const char *k) -{ - yyjson_val *v = o ? yyjson_obj_get(o, k) : NULL; - return v && yyjson_is_int(v) ? (int64_t)yyjson_get_int(v) : 0; -} - -static void buf_line(struct buf *b, const char *fmt, ...) - __attribute__((format(printf, 2, 3))); - -static void buf_line(struct buf *b, const char *fmt, ...) -{ - char line[1024]; - va_list ap; - va_start(ap, fmt); - int n = vsnprintf(line, sizeof line, fmt, ap); - va_end(ap); - if (n < 0) - return; - buf_append(b, line, (size_t)n < sizeof line ? (size_t)n : sizeof line - 1); -} - -static void buf_rule(struct buf *b, int width) -{ - char line[128]; - if (width > (int)sizeof line - 3) - width = (int)sizeof line - 3; - if (width < 1) - return; - line[0] = MK_RULE[0]; - memset(line + 1, '-', (size_t)width); - line[width + 1] = '\n'; - buf_append(b, line, (size_t)width + 2); -} - -/* prefix (8) + label (48) + amount columns */ -#define BS_RULE_W (8 + 48 + 4 * 14) -#define IS_RULE_W (8 + 48 + 3 * 15) - -static void fmt_head(struct buf *t, const struct app *a, const char *title, - yyjson_val *res) -{ - yyjson_val *fy = yyjson_obj_get(res, "fiscal_year"); - const char *start = vstr(fy, "start_date"); - const char *end = vstr(fy, "end_date"); - buf_line(t, MK_HEAD "%s\n%s\n\n", title, a->org_name); - buf_line(t, MK_DIM "Räkenskapsår: %s - %s\n", start, end); - buf_line(t, MK_DIM "Period: %s - %s\n\n", start, end); -} - -static int acct_in(const char *number, int lo, int hi) -{ - int n = atoi(number); - return n >= lo && n <= hi; -} - -/* Pad to `width` display columns without truncating (labels may be longer). */ -static void pad_min(char *buf, size_t n, int width) -{ - int w = tui_disp_width(buf); - size_t len = strlen(buf); - while (w < width && len + 1 < n) { - buf[len++] = ' '; - buf[len] = '\0'; - w++; - } -} - -/* Right-align `text` in `width` display columns (headers with å/ä/ö). */ - -/* ---------------- balansräkning ---------------- */ - -static const char *const BS_KEYS[3] = { "assets", "equity", "liabilities" }; - -struct bs_sum { - int64_t ib, per, ub; -}; - -static void bs_col_head(struct buf *t, int sticky) -{ - buf_line(t, "%s %5s %-48s %14s %14s %14s %14s\n", - sticky ? MK_STICKY MK_HEAD : "", "", "", "Ing balans", "Ing saldo", - "Period", "Utg balans"); - buf_rule(t, BS_RULE_W); -} - -static int bs_rows(struct buf *t, yyjson_val *res, int lo, int hi, - struct bs_sum *sum) -{ - int found = 0; - for (int k = 0; k < 3; k++) { - yyjson_val *sec = yyjson_obj_get(res, BS_KEYS[k]); - yyjson_val *arr = sec ? yyjson_obj_get(sec, "accounts") : NULL; - size_t n = yyjson_arr_size(arr); - for (size_t i = 0; i < n; i++) { - yyjson_val *row = yyjson_arr_get(arr, i); - const char *acc = vstr(row, "account"); - if (!acct_in(acc, lo, hi)) - continue; - int64_t ib = vint(row, "ib_ore"); - int64_t ub = vint(row, "ub_ore"); - int64_t per = ub - ib; - if (t) { - char nm[128], a1[48], a2[48], a3[48], a4[48]; - tui_pad_label(nm, sizeof nm, vstr(row, "name"), 48); - tui_amt_col(a1, sizeof a1, 14, ib); - tui_amt_col(a2, sizeof a2, 14, ib); - tui_amt_col(a3, sizeof a3, 14, per); - tui_amt_col(a4, sizeof a4, 14, ub); - buf_line(t, " %5s %s %s %s %s %s\n", acc, nm, a1, a2, a3, a4); - } - sum->ib += ib; - sum->per += per; - sum->ub += ub; - found = 1; - } - } - return found; -} - -static void bs_sum_line(struct buf *t, const char *label, - const struct bs_sum *s) -{ - char lbl[64], a1[48], a2[48], a3[48], a4[48]; - tui_pad_label(lbl, sizeof lbl, label, 48); - tui_amt_col(a1, sizeof a1, 14, s->ib); - tui_amt_col(a2, sizeof a2, 14, s->ib); - tui_amt_col(a3, sizeof a3, 14, s->per); - tui_amt_col(a4, sizeof a4, 14, s->ub); - buf_line(t, MK_DIM " %5s %s %s %s %s %s\n", "", lbl, a1, a2, a3, a4); -} - -static int bs_group(struct buf *t, yyjson_val *res, const char *title, - const char *sumlabel, int lo, int hi, struct bs_sum *acc) -{ - struct bs_sum probe = { 0, 0, 0 }; - if (!bs_rows(NULL, res, lo, hi, &probe)) - return 0; - buf_line(t, MK_HEAD "%s\n", title); - struct bs_sum s = { 0, 0, 0 }; - bs_rows(t, res, lo, hi, &s); - bs_sum_line(t, sumlabel, &s); - if (acc) { - acc->ib += s.ib; - acc->per += s.per; - acc->ub += s.ub; - } - return 1; -} - -static void fmt_balans(struct buf *t, const struct app *a, yyjson_val *res) -{ - static const struct { - const char *title; - const char *sum; - int lo, hi; - } OMS[] = { - { "Varulager", "Summa varulager", 1400, 1499 }, - { "Kortfristiga fordringar", "Summa kortfristiga fordringar", 1500, - 1699 }, - { "Kortfristiga placeringar", "Summa kortfristiga placeringar", 1700, - 1899 }, - { "Kassa och bank", "Summa kassa och bank", 1900, 1999 }, - }; - fmt_head(t, a, "Balansrapport", res); - struct bs_sum anl = { 0, 0, 0 }, oms = { 0, 0, 0 }; - struct bs_sum eget = { 0, 0, 0 }, skuld = { 0, 0, 0 }; - buf_line(t, MK_HEAD "TILLGÅNGAR\n"); - bs_col_head(t, 1); - buf_line(t, "\n" MK_HEAD "Anläggningstillgångar\n"); - bs_rows(t, res, 1000, 1399, &anl); - bs_sum_line(t, "Summa anläggningstillgångar", &anl); - buf_line(t, "\n" MK_HEAD "Omsättningstillgångar\n"); - int printed = 0; - for (size_t i = 0; i < sizeof OMS / sizeof OMS[0]; i++) { - struct bs_sum probe = { 0, 0, 0 }; - if (!bs_rows(NULL, res, OMS[i].lo, OMS[i].hi, &probe)) - continue; - if (printed) - buf_line(t, "\n"); - bs_group(t, res, OMS[i].title, OMS[i].sum, OMS[i].lo, OMS[i].hi, - &oms); - printed = 1; - } - bs_sum_line(t, "Summa omsättningstillgångar", &oms); - struct bs_sum as = { anl.ib + oms.ib, anl.per + oms.per, anl.ub + oms.ub }; - buf_rule(t, BS_RULE_W); - bs_sum_line(t, "Summa tillgångar", &as); - - buf_line(t, "\n" MK_HEAD "EGET KAPITAL OCH SKULDER\n"); - bs_col_head(t, 0); - buf_line(t, "\n" MK_HEAD "Eget kapital\n"); - bs_group(t, res, "Bundet eget kapital", "Summa bundet eget kapital", 2000, - 2090, &eget); - buf_line(t, "\n"); - bs_group(t, res, "Fritt eget kapital", "Summa fritt eget kapital", 2091, - 2099, &eget); - bs_sum_line(t, "Summa eget kapital", &eget); - buf_line(t, "\n"); - if (bs_group(t, res, "Långfristiga skulder", "Summa långfristiga skulder", - 2100, 2399, &skuld)) - buf_line(t, "\n"); - bs_group(t, res, "Kortfristiga skulder", "Summa kortfristiga skulder", - 2400, 2999, &skuld); - struct bs_sum es = { eget.ib + skuld.ib, eget.per + skuld.per, - eget.ub + skuld.ub }; - buf_rule(t, BS_RULE_W); - bs_sum_line(t, "Summa eget kapital och skulder", &es); - - yyjson_val *eq = yyjson_obj_get(res, "equity"); - int64_t result = eq ? vint(eq, "result_ore") : 0; - char lbl[64], zero[48], a1[48]; - tui_pad_label(lbl, sizeof lbl, "Beräknat resultat", 48); - tui_amt_col(zero, sizeof zero, 14, 0); - tui_amt_col(a1, sizeof a1, 14, result); - buf_line(t, "\n"); - buf_rule(t, BS_RULE_W); - buf_line(t, MK_DIM " %5s %s %s %s %s %s\n", "", lbl, zero, zero, a1, a1); -} - -/* ---------------- resultaträkning ---------------- */ - -struct is_group { - const char *section; - const char *title; - int lo, hi; - int rev; -}; - -static const struct is_group IS_GROUPS[] = { - { "Rörelseintäkter, lagerförändringar m.m.", "Nettoomsättning", 3000, 3799, - 1 }, - { "Rörelseintäkter, lagerförändringar m.m.", "Övriga rörelseintäkter", - 3800, 3999, 1 }, - { "Rörelsekostnader", "Råvaror och förnödenheter", 4000, 4999, 0 }, - { "Rörelsekostnader", "Övriga externa kostnader", 5000, 6999, 0 }, - { "Rörelsekostnader", "Personalkostnader", 7000, 7699, 0 }, - { "Rörelsekostnader", "Avskrivningar och nedskrivningar", 7700, 7899, 0 }, - { "Rörelsekostnader", "Övriga rörelsekostnader", 7900, 7999, 0 }, -}; - -static const char *const IS_KEYS[2] = { "revenue", "expenses" }; - -static int is_group_has(yyjson_val *res, const struct is_group *g) -{ - for (int k = 0; k < 2; k++) { - yyjson_val *sec = yyjson_obj_get(res, IS_KEYS[k]); - yyjson_val *arr = sec ? yyjson_obj_get(sec, "accounts") : NULL; - size_t n = yyjson_arr_size(arr); - for (size_t i = 0; i < n; i++) - if (acct_in(vstr(yyjson_arr_get(arr, i), "account"), g->lo, - g->hi)) - return 1; - } - return 0; -} - -static int64_t is_prev_lookup(yyjson_val *prev, const char *account, - int negate) -{ - if (!prev) - return 0; - for (int k = 0; k < 2; k++) { - yyjson_val *sec = yyjson_obj_get(prev, IS_KEYS[k]); - yyjson_val *arr = sec ? yyjson_obj_get(sec, "accounts") : NULL; - size_t n = yyjson_arr_size(arr); - for (size_t i = 0; i < n; i++) { - yyjson_val *row = yyjson_arr_get(arr, i); - if (strcmp(vstr(row, "account"), account) == 0) { - int64_t a = vint(row, "amount_ore"); - return negate ? -a : a; - } - } - } - return 0; -} - -static void is_summary(struct buf *t, const char *label, int64_t cur, - int64_t prev, int blank) -{ - char lbl[64], a1[48], a2[48]; - if (blank) - buf_line(t, "\n"); - tui_pad_label(lbl, sizeof lbl, label, 48); - tui_amt_col(a1, sizeof a1, 15, cur); - tui_amt_col(a2, sizeof a2, 15, prev); - buf_line(t, MK_DIM " %5s %s %s %s %s\n", "", lbl, a1, a1, a2); -} - -static void is_print_group(struct buf *t, yyjson_val *res, yyjson_val *prev, - const struct is_group *gr, int64_t *cur, - int64_t *pre) -{ - int64_t sc = 0, sp = 0; - for (int k = 0; k < 2; k++) { - yyjson_val *sec = yyjson_obj_get(res, IS_KEYS[k]); - yyjson_val *arr = sec ? yyjson_obj_get(sec, "accounts") : NULL; - size_t n = yyjson_arr_size(arr); - for (size_t i = 0; i < n; i++) { - yyjson_val *row = yyjson_arr_get(arr, i); - const char *acc = vstr(row, "account"); - if (!acct_in(acc, gr->lo, gr->hi)) - continue; - int64_t c = gr->rev ? vint(row, "amount_ore") - : -vint(row, "amount_ore"); - int64_t p = is_prev_lookup(prev, acc, !gr->rev); - char nm[128], a1[48], a2[48]; - tui_pad_label(nm, sizeof nm, vstr(row, "name"), 48); - tui_amt_col(a1, sizeof a1, 15, c); - tui_amt_col(a2, sizeof a2, 15, p); - buf_line(t, " %5s %s %s %s %s\n", acc, nm, a1, a1, a2); - sc += c; - sp += p; - } - } - is_summary(t, "Summa", sc, sp, 0); - *cur = sc; - *pre = sp; -} - -static int is_range_has(yyjson_val *res, int lo, int hi) -{ - for (int k = 0; k < 2; k++) { - yyjson_val *sec = yyjson_obj_get(res, IS_KEYS[k]); - yyjson_val *arr = sec ? yyjson_obj_get(sec, "accounts") : NULL; - size_t n = yyjson_arr_size(arr); - for (size_t i = 0; i < n; i++) - if (acct_in(vstr(yyjson_arr_get(arr, i), "account"), lo, hi)) - return 1; - } - return 0; -} - -/* Rows in one account range, printed with the previous-year column; the - signed display sums are added to *sum / *sum_pre. */ -static void is_range_rows(struct buf *t, yyjson_val *res, yyjson_val *prev, - int lo, int hi, int64_t *sum, int64_t *sum_pre) -{ - for (int k = 0; k < 2; k++) { - yyjson_val *sec = yyjson_obj_get(res, IS_KEYS[k]); - yyjson_val *arr = sec ? yyjson_obj_get(sec, "accounts") : NULL; - size_t n = yyjson_arr_size(arr); - for (size_t i = 0; i < n; i++) { - yyjson_val *row = yyjson_arr_get(arr, i); - const char *acc = vstr(row, "account"); - if (!acct_in(acc, lo, hi)) - continue; - int64_t c = k == 0 ? vint(row, "amount_ore") - : -vint(row, "amount_ore"); - int64_t p = is_prev_lookup(prev, acc, k != 0); - char nm[128], a1[48], a2[48]; - tui_pad_label(nm, sizeof nm, vstr(row, "name"), 48); - tui_amt_col(a1, sizeof a1, 15, c); - tui_amt_col(a2, sizeof a2, 15, p); - buf_line(t, " %5s %s %s %s %s\n", acc, nm, a1, a1, a2); - *sum += c; - *sum_pre += p; - } - } -} - -static void fmt_resultat(struct buf *t, const struct app *a, yyjson_val *res, - yyjson_val *prev) -{ - fmt_head(t, a, "Resultatrapport", res); - const char *sect = NULL; - int64_t rev = 0, rev_pre = 0, exp = 0, exp_pre = 0; - for (size_t g = 0; g < sizeof IS_GROUPS / sizeof IS_GROUPS[0]; g++) { - const struct is_group *gr = &IS_GROUPS[g]; - if (!is_group_has(res, gr)) - continue; - if (!sect || strcmp(sect, gr->section) != 0) { - if (sect) { - is_summary(t, "Summa rörelseintäkter, lagerförändringar m.m.", - rev, rev_pre, 0); - buf_line(t, "\n"); - buf_line(t, MK_HEAD "%s\n", gr->section); - } else { - char lbl[80], h1[32], h2[32], h3[32]; - tui_pad_label(lbl, sizeof lbl, gr->section, 48); - tui_pad_hdr(h1, sizeof h1, 15, "Period"); - tui_pad_hdr(h2, sizeof h2, 15, "Ackumulerat"); - tui_pad_hdr(h3, sizeof h3, 15, "Föreg. per."); - buf_line(t, MK_STICKY MK_HEAD " %5s %s %s %s %s\n", "", lbl, - h1, h2, h3); - buf_rule(t, IS_RULE_W); - buf_line(t, "\n"); - buf_line(t, MK_HEAD "%s\n", gr->title); - } - sect = gr->section; - } else { - buf_line(t, "\n"); - buf_line(t, MK_HEAD "%s\n", gr->title); - } - int64_t sc = 0, sp = 0; - is_print_group(t, res, prev, gr, &sc, &sp); - if (gr->rev) { - rev += sc; - rev_pre += sp; - } else { - exp += sc; - exp_pre += sp; - } - } - is_summary(t, "Summa rörelsekostnader", exp, exp_pre, 0); - - int64_t fin = 0, fin_pre = 0, disp = 0, disp_pre = 0; - int64_t tax = 0, tax_pre = 0, booked = 0, booked_pre = 0; - int has_disp = is_range_has(res, 8800, 8899); - int has_tax = is_range_has(res, 8900, 8989); - int has_booked = is_range_has(res, 8999, 8999); - if (is_range_has(res, 8000, 8599)) { - buf_line(t, "\n" MK_HEAD "Finansiella poster\n"); - is_range_rows(t, res, prev, 8000, 8599, &fin, &fin_pre); - is_summary(t, "Summa", fin, fin_pre, 0); - } - int64_t ror = rev + exp, ror_pre = rev_pre + exp_pre; - int64_t after = ror + fin, after_pre = ror_pre + fin_pre; - buf_line(t, "\n"); - buf_rule(t, IS_RULE_W); - is_summary(t, "Rörelseresultat", ror, ror_pre, 0); - is_summary(t, "Resultat efter finansiella poster", after, after_pre, 1); - if (has_disp) { - buf_line(t, "\n" MK_HEAD "Bokslutsdispositioner\n"); - is_range_rows(t, res, prev, 8800, 8899, &disp, &disp_pre); - is_summary(t, "Summa", disp, disp_pre, 0); - } - int64_t before_tax = after + disp, before_tax_pre = after_pre + disp_pre; - is_summary(t, "Resultat före skatt", before_tax, before_tax_pre, 1); - if (has_tax) { - buf_line(t, "\n" MK_HEAD "Skatt\n"); - is_range_rows(t, res, prev, 8900, 8989, &tax, &tax_pre); - is_summary(t, "Summa", tax, tax_pre, 0); - } - int64_t ars = before_tax + tax, ars_pre = before_tax_pre + tax_pre; - buf_line(t, "\n"); - buf_rule(t, IS_RULE_W); - is_summary(t, "Årets resultat", ars, ars_pre, 0); - if (has_booked) { - buf_line(t, "\n" MK_HEAD "Bokfört resultat\n"); - is_range_rows(t, res, prev, 8999, 8999, &booked, &booked_pre); - is_summary(t, "Summa", booked, booked_pre, 0); - buf_line(t, "\n"); - buf_rule(t, IS_RULE_W); - is_summary(t, "Ej bokfört resultat", ars + booked, - ars_pre + booked_pre, 0); - } else { - buf_line(t, "\n"); - buf_rule(t, IS_RULE_W); - is_summary(t, "Ej bokfört resultat", ars, ars_pre, 0); - } -} - -/* ---------------- saldobalans ---------------- */ - -static void fmt_saldobalans(struct buf *t, const struct app *a, - yyjson_val *res) -{ - fmt_head(t, a, "Saldobalans", res); - char nmh[64], h1[32], h2[32], h3[32], h4[32]; - tui_pad_label(nmh, sizeof nmh, "Benämning", 48); - tui_pad_hdr(h1, sizeof h1, 14, "Ingående"); - tui_pad_hdr(h2, sizeof h2, 14, "Debet"); - tui_pad_hdr(h3, sizeof h3, 14, "Kredit"); - tui_pad_hdr(h4, sizeof h4, 14, "Utgående"); - buf_line(t, MK_STICKY MK_HEAD " %5s %s %s %s %s %s\n", "Konto", nmh, h1, - h2, h3, h4); - buf_rule(t, BS_RULE_W); - yyjson_val *arr = yyjson_obj_get(res, "accounts"); - size_t n = yyjson_arr_size(arr); - for (size_t i = 0; i < n; i++) { - yyjson_val *row = yyjson_arr_get(arr, i); - char nm[128], a1[48], a2[48], a3[48], a4[48]; - tui_pad_label(nm, sizeof nm, vstr(row, "name"), 48); - tui_amt_col(a1, sizeof a1, 14, vint(row, "ib_ore")); - tui_amt_col(a2, sizeof a2, 14, vint(row, "debit_ore")); - tui_amt_col(a3, sizeof a3, 14, vint(row, "credit_ore")); - tui_amt_col(a4, sizeof a4, 14, vint(row, "ub_ore")); - buf_line(t, " %5s %s %s %s %s %s\n", vstr(row, "account"), nm, a1, - a2, a3, a4); - } - yyjson_val *tot = yyjson_obj_get(res, "totals"); - char lbl[64], a1[48], a2[48], a3[48], a4[48]; - tui_pad_label(lbl, sizeof lbl, "Summa", 48); - tui_amt_col(a1, sizeof a1, 14, vint(tot, "ib_ore")); - tui_amt_col(a2, sizeof a2, 14, vint(tot, "debit_ore")); - tui_amt_col(a3, sizeof a3, 14, vint(tot, "credit_ore")); - tui_amt_col(a4, sizeof a4, 14, vint(tot, "ub_ore")); - buf_line(t, "\n"); - buf_rule(t, BS_RULE_W); - buf_line(t, MK_DIM " %5s %s %s %s %s %s\n", "", lbl, a1, a2, a3, a4); -} - -/* ---------------- momsdeklaration ---------------- */ - -struct vat_row { - const char *section; - const char *box; - const char *label; -}; - -static const struct vat_row VAT_ROWS[] = { - { "A.", "05", - "Momspliktig försäljning som inte ingår i ruta 06, 07 eller 08" }, - { "A.", "06", "Momspliktiga uttag" }, - { "A.", "07", "Beskattningsunderlag vid vinstmarginalbeskattning" }, - { "A.", "08", "Hyresinkomster vid frivillig beskattning" }, - { "B.", "10", "Utgående moms 25%" }, - { "B.", "11", "Utgående moms 12%" }, - { "B.", "12", "Utgående moms 6%" }, - { "C.", "20", "Inköp av varor från annat EU-land" }, - { "C.", "21", "Inköp av tjänster från annat EU-land enligt huvudregeln" }, - { "C.", "22", "Inköp av tjänster från land utanför EU" }, - { "C.", "23", "Inköp av varor i Sverige som köparen är betalningsskyldig för" }, - { "C.", "24", "Övriga inköp av tjänster i Sverige som köparen är betalningsskyldig för" }, - { "D.", "30", "Utgående moms 25%" }, - { "D.", "31", "Utgående moms 12%" }, - { "D.", "32", "Utgående moms 6%" }, - { "H.", "50", "Beskattningsunderlag vid import" }, - { "I.", "60", "Utgående moms 25%" }, - { "I.", "61", "Utgående moms 12%" }, - { "I.", "62", "Utgående moms 6%" }, - { "E.", "35", "Försäljning av varor till annat EU-land" }, - { "E.", "36", "Försäljning av varor utanför EU" }, - { "E.", "37", "Mellanmans inköp av varor vid trepartshandel" }, - { "E.", "38", "Mellanmans försäljning av varor vid trepartshandel" }, - { "E.", "39", "Försäljning av tjänster till beskattningsbar person i annat EU-land enligt huvudregeln" }, - { "E.", "40", "Övrig försäljning av tjänster som tillhandahållits utomlands" }, - { "E.", "41", "Försäljning när köparen är betalningsskyldig i Sverige" }, - { "E.", "42", "Övrig försäljning m.m." }, - { "F.", "48", "Ingående moms att dra av" }, - { "G.", "49", "Moms att betala (+) eller att få tillbaka (-)" }, -}; - -static const char *vat_section_title(const char *s) -{ - if (strcmp(s, "A.") == 0) - return "Momspliktig försäljning eller uttag exklusive moms"; - if (strcmp(s, "B.") == 0) - return "Utgående moms på försäljning eller uttag i ruta 05 - 08"; - if (strcmp(s, "C.") == 0) - return "Momspliktiga inköp vid omvänd skattskyldighet"; - if (strcmp(s, "D.") == 0) - return "Utgående moms på inköp i ruta 20 - 24"; - if (strcmp(s, "H.") == 0) - return "Import"; - if (strcmp(s, "I.") == 0) - return "Utgående moms på import i ruta 50"; - if (strcmp(s, "E.") == 0) - return "Försäljning m.m. som är undantagen från moms"; - if (strcmp(s, "F.") == 0) - return "Ingående moms"; - return "Moms att betala eller få tillbaka"; -} - -static void fmt_moms(struct buf *t, const struct app *a, yyjson_val *res) -{ - buf_line(t, MK_HEAD "Momsrapport\n\n%s\n\n", a->org_name); - buf_line(t, MK_DIM "Räkenskapsår: %s - %s\n", a->fy_start, a->fy_end); - buf_line(t, MK_DIM "Period: %s - %s\n", vstr(res, "from"), - vstr(res, "to")); - const char *sect = NULL; - yyjson_val *boxes = yyjson_obj_get(res, "boxes"); - size_t n = yyjson_arr_size(boxes); - for (size_t i = 0; i < sizeof VAT_ROWS / sizeof VAT_ROWS[0]; i++) { - const struct vat_row *vr = &VAT_ROWS[i]; - if (strcmp(vr->box, "49") == 0) - continue; /* last */ - if (!sect || strcmp(sect, vr->section) != 0) { - sect = vr->section; - buf_line(t, "\n" MK_HEAD "%s %s\n", vr->section, - vat_section_title(vr->section)); - } - int64_t ore = 0; - for (size_t j = 0; j < n; j++) { - yyjson_val *b = yyjson_arr_get(boxes, j); - if (strcmp(vstr(b, "box"), vr->box) == 0) { - ore = vint(b, "amount_ore"); - break; - } - } - char amt[48] = "", lbl[192]; - if (ore) - kr_whole(ore, amt, sizeof amt); - snprintf(lbl, sizeof lbl, "%s", vr->label); - pad_min(lbl, sizeof lbl, 66); - buf_line(t, "%s. %s %14s\n", vr->box, lbl, amt); - } - int64_t ore49 = 0; - for (size_t j = 0; j < n; j++) { - yyjson_val *b = yyjson_arr_get(boxes, j); - if (strcmp(vstr(b, "box"), "49") == 0) { - ore49 = vint(b, "amount_ore"); - break; - } - } - buf_line(t, "\n" MK_HEAD "G. %s\n", vat_section_title("G.")); - buf_rule(t, 87); - char amt49[48], lbl49[192]; - kr_whole(ore49, amt49, sizeof amt49); - snprintf(lbl49, sizeof lbl49, "%s", - "Moms att betala (+) eller att få tillbaka (-)"); - pad_min(lbl49, sizeof lbl49, 66); - buf_line(t, MK_DIM "49. %s %14s\n", lbl49, amt49); -} - -static int64_t prev_fy_id(struct app *a) -{ - char *resp = - client_rpc(&a->conn, "fiscal_year.list", a->session, a->org, "{}"); - if (!resp || !client_ok(resp)) { - free(resp); - return 0; - } - size_t n = jarr_size(resp, "result.items"); - int64_t best = 0; - char best_end[16] = ""; - for (size_t i = 0; i < n; i++) { - char path[64]; - snprintf(path, sizeof path, "result.items.%zu.end_date", i); - char *e = jstr_dup(resp, path); - if (e && strcmp(e, a->fy_start) < 0 && strcmp(e, best_end) > 0) { - snprintf(path, sizeof path, "result.items.%zu.id", i); - int64_t id = jint_val(resp, path, 0); - if (id) { - best = id; - snprintf(best_end, sizeof best_end, "%s", e); - } - } - free(e); - } - free(resp); - return best; -} - -/* ---------------- huvudbok ---------------- */ - -static void fmt_huvudbok(struct buf *t, const struct app *a, yyjson_val *res) -{ - fmt_head(t, a, "Huvudbok", res); - yyjson_val *lv = yyjson_obj_get(res, "last_voucher"); - if (lv) - buf_line(t, MK_DIM "Senaste ver.: %s%lld\n\n", vstr(lv, "series"), - (long long)vint(lv, "number")); - buf_line(t, MK_STICKY MK_HEAD "%-8s %-58s %14s %14s %16s\n", "Konto", "", - "Debet", "Kredit", "Saldo"); - buf_rule(t, 114); - yyjson_val *accs = yyjson_obj_get(res, "accounts"); - size_t n = yyjson_arr_size(accs); - for (size_t i = 0; i < n; i++) { - yyjson_val *ao = yyjson_arr_get(accs, i); - int64_t ib = vint(ao, "ib_ore"); - int64_t d = vint(ao, "debit_ore"); - int64_t c = vint(ao, "credit_ore"); - buf_line(t, "\n" MK_HEAD "%s %s\n\n", vstr(ao, "account"), - vstr(ao, "name")); - char a1[48]; - tui_amt_col(a1, sizeof a1, 16, ib); - char lbl[96]; - tui_pad_label(lbl, sizeof lbl, "Ingående balans:", 59); - buf_line(t, MK_DIM " %s %14s %14s %16s\n", lbl, "", "", a1); - tui_pad_label(lbl, sizeof lbl, "Ingående saldo:", 59); - buf_line(t, MK_DIM " %s %14s %14s %16s\n", lbl, "", "", a1); - buf_line(t, "\n"); - yyjson_val *rows = yyjson_obj_get(ao, "rows"); - size_t nr = yyjson_arr_size(rows); - for (size_t k = 0; k < nr; k++) { - yyjson_val *ro = yyjson_arr_get(rows, k); - char ver[32], nm[256], dd[48], cc[48], deb[48], kre[48], sal[48]; - snprintf(ver, sizeof ver, "%s%lld", vstr(ro, "series"), - (long long)vint(ro, "number")); - const char *txt = vstr(ro, "row_description"); - if (!*txt) - txt = vstr(ro, "description"); - tui_pad_label(nm, sizeof nm, txt, 40); - tui_amt_col(dd, sizeof dd, 14, vint(ro, "debit_ore")); - tui_amt_col(cc, sizeof cc, 14, vint(ro, "credit_ore")); - snprintf(deb, sizeof deb, "%s", vint(ro, "debit_ore") ? dd : ""); - snprintf(kre, sizeof kre, "%s", vint(ro, "credit_ore") ? cc : ""); - tui_amt_col(sal, sizeof sal, 16, vint(ro, "saldo_ore")); - buf_line(t, " %-7s %-10s %s %14s %14s %16s\n", ver, - vstr(ro, "date"), nm, deb, kre, sal); - } - char s1[48], s2[48], s3[48]; - tui_amt_col(s1, sizeof s1, 14, d); - tui_amt_col(s2, sizeof s2, 14, c); - tui_amt_col(s3, sizeof s3, 16, d - c); - tui_pad_label(lbl, sizeof lbl, "Omslutning:", 59); - buf_line(t, MK_DIM " %s %14s %14s %16s\n", lbl, s1, s2, s3); - tui_amt_col(s3, sizeof s3, 16, vint(ao, "ub_ore")); - tui_pad_label(lbl, sizeof lbl, "Utgående saldo:", 59); - buf_line(t, MK_DIM " %s %14s %14s %16s\n", lbl, s1, s2, s3); - } -} - -/* ---------------- verifikationslista ---------------- */ - -static void fmt_verifikationslista(struct buf *t, const struct app *a, - yyjson_val *res) -{ - fmt_head(t, a, "Verifikationslista", res); - yyjson_val *lv = yyjson_obj_get(res, "last_voucher"); - if (lv) - buf_line(t, MK_DIM "Senaste ver.: %s%lld\n\n", vstr(lv, "series"), - (long long)vint(lv, "number")); - char nmh[64]; - tui_pad_label(nmh, sizeof nmh, "Benämning", 48); - buf_line(t, MK_STICKY MK_HEAD "%-8s %-11s %s %14s %14s\n", "Ver.", - "Datum/Konto", nmh, "Debet", "Kredit"); - buf_rule(t, 98); - yyjson_val *vs = yyjson_obj_get(res, "vouchers"); - size_t n = yyjson_arr_size(vs); - for (size_t i = 0; i < n; i++) { - yyjson_val *vo = yyjson_arr_get(vs, i); - char ver[32]; - snprintf(ver, sizeof ver, "%s%lld", vstr(vo, "series"), - (long long)vint(vo, "number")); - buf_line(t, MK_HEAD "%-8s %-11s %s\n", ver, vstr(vo, "date"), - vstr(vo, "description")); - yyjson_val *rows = yyjson_obj_get(vo, "rows"); - size_t nr = yyjson_arr_size(rows); - for (size_t k = 0; k < nr; k++) { - yyjson_val *ro = yyjson_arr_get(rows, k); - char nm[256], dd[48], cc[48], deb[48], kre[48]; - tui_pad_label(nm, sizeof nm, vstr(ro, "name"), 48); - tui_amt_col(dd, sizeof dd, 14, vint(ro, "debit_ore")); - tui_amt_col(cc, sizeof cc, 14, vint(ro, "credit_ore")); - snprintf(deb, sizeof deb, "%s", vint(ro, "debit_ore") ? dd : ""); - snprintf(kre, sizeof kre, "%s", vint(ro, "credit_ore") ? cc : ""); - buf_line(t, " %-10s %s %14s %14s\n", vstr(ro, "account"), - nm, deb, kre); - } - } - yyjson_val *tot = yyjson_obj_get(res, "totals"); - char s1[48], s2[48]; - tui_amt_col(s1, sizeof s1, 14, vint(tot, "debit_ore")); - tui_amt_col(s2, sizeof s2, 14, vint(tot, "credit_ore")); - buf_line(t, "\n" MK_DIM "%-69s %14s %14s\n", "Omslutning:", s1, s2); -} - -static void eskd_save(struct app *a, const char *from, const char *to) -{ - const char *home = getenv("HOME"); - char period[8], def[512], full[512]; - snprintf(period, sizeof period, "%.4s%.2s", to, to + 5); - snprintf(def, sizeof def, "%s/eskd_%s.xml", home && *home ? home : ".", - period); - char pathbuf[512]; char *path = tui_prompt_into(pathbuf, sizeof pathbuf, "Spara eSKD-fil: ", def, 0) ? pathbuf : NULL; - if (!path || !*path) - return; - if (path[0] == '~' && (path[1] == '/' || path[1] == '\0')) - snprintf(full, sizeof full, "%s%s", home ? home : "", path + 1); - else - snprintf(full, sizeof full, "%s", path); - char args[64]; - snprintf(args, sizeof args, "{\"from\":\"%s\",\"to\":\"%s\"}", from, to); - char *resp = - client_rpc(&a->conn, "report.vat_eskd", a->session, a->org, args); - if (!resp || !client_ok(resp)) { - show_error("Kunde inte skapa eSKD", resp); - free(resp); - return; - } - char *b64 = jstr_dup(resp, "result.content_base64"); - unsigned char *data = NULL; - size_t n = 0; - if (!b64 || util_b64_decode(b64, strlen(b64), &data, &n) != 0) { - tui_message("eSKD", "Kunde inte avkoda filen från servern."); - } else { - FILE *f = fopen(full, "wb"); - if (!f || fwrite(data, 1, n, f) != n) { - tui_message("eSKD", "Kunde inte skriva %s: %s", full, strerror(errno)); - if (f) - fclose(f); - } else { - fclose(f); - tui_message("eSKD", "Sparat: %s\n(%zu byte, ISO-8859-1)", full, n); - } - } - free(data); - free(b64); - free(resp); -} - -/* Official fältnamn (short) for the INK2/INK2R/INK2S codes; 2025P4. */ -static const struct { - const char *code; - const char *label; -} SRU_LABELS[] = { - { "7011", "Räkenskapsårets början" }, - { "7012", "Räkenskapsårets slut" }, - { "7104", "1.1 Överskott av näringsverksamhet" }, - { "7114", "1.2 Underskott av näringsverksamhet" }, - { "7131", "1.3 Kreditinstituts underlag för riskskatt" }, - { "7132", "1.4 Underlag för särskild löneskatt på pensionskostnader" }, - { "7133", "1.5 Negativt underlag för särskild löneskatt på pensionskostnader" }, - { "7153", "1.6 a Försäkringsföretag m.fl. samt avsatt till pensioner 15 %" }, - { "7154", "1.6 b Utländska pensions- försäkringar 15 %" }, - { "7155", "1.7 a Försäkringsföretag m.fl 30 %" }, - { "7156", "1.7 b Utländska kapitalförsäkringar 30 %" }, - { "80", "1.8 Småhus/ ägarlägenhet" }, - { "93", "1.9 Hyreshus: bostäder" }, - { "84", "1.10 Småhus/ägarlägenhet: tomtmark, byggnad under uppförande" }, - { "86", "1.11 Hyreshus: tomtmark, bostäder under uppförande" }, - { "95", "1.12 Hyreshus: lokaler" }, - { "96", "1.13 Industrienhet och elproduktionsenhet: värme- kraftverk" }, - { "97", "1.14 Elproduktionsenhet, vattenkraftverk" }, - { "98", "1.15 Elproduktionsenhet, vindkraftverk" }, - { "90", "Övriga upplysningar på bilaga" }, - { "1582", "1.16 Förnybar el (kilowattimmar)" }, - { "7011", "Räkenskapsårets början" }, - { "7012", "Räkenskapsårets slut" }, - { "7201", "2.1 Immateriella anläggningstillgångar Koncessioner, patent, licenser, varumärken, hyresrätter, goodwill och liknande rättigheter" }, - { "7202", "2.2 Immateriella anläggningstillgångar Förskott avseende immateriella anläggningstillgångar" }, - { "7214", "2.3 Materiella anläggningstillgångar Byggnader och mark" }, - { "7215", "2.4 Materiella anläggningstillgångar Maskiner, inventarier och övriga materiella anläggningstillgångar" }, - { "7216", "2.5 Materiella anläggningstillgångar Förbättringsutgifter på annans fastighet" }, - { "7217", "2.6 Materiella anläggningstillgångar Pågående nyanläggningar och förskott avseende materiella anläggningstillgångar" }, - { "7230", "2.7 Finansiella anläggningstillgångar Andelar i koncernföretag" }, - { "7231", "2.8 Finansiella anläggningstillgångar Andelar i intresseföretag och gemensamt styrda företag" }, - { "7233", "2.9 Ägarintressen i övriga företag och andra långfristiga värdepappersinnehav" }, - { "7232", "2.10 Finansiella anläggningstillgångar Fordringar hos koncern-, intresse- och gemensamt styrda företag" }, - { "7234", "2.11 Finansiella anläggningstillgångar Lån till delägare eller närstående" }, - { "7235", "2.12 Fordringar hos övriga företag som det finns ett ägarintresse i och andra långfristiga fordringar" }, - { "7241", "2.13 Varulager m.m. Råvaror och förnödenheter" }, - { "7242", "2.14 Varulager Varor under tillverkning" }, - { "7243", "2.15 Varulager Färdiga varor och handelsvaror" }, - { "7244", "2.16 Varulager Övriga lagertillgångar" }, - { "7245", "2.17 Varulager Pågående arbeten för annans räkning" }, - { "7246", "2.18 Varulager Förskott till leverantörer" }, - { "7251", "2.19 Kortfristiga fordringar Kundfordringar" }, - { "7252", "2.20 Kortfristiga fordringar Fordringar hos koncern-, intresse- och gemensamt styrda företag" }, - { "7261", "2.21 Fordringar hos övriga företag som det finns ett ägarintresse i och övriga fordringar" }, - { "7262", "2.22 Kortfristiga fordringar Upparbetad men ej fakturerad intäkt" }, - { "7263", "2.23 Kortfristiga fordringar Förutbetalda kostnader och upplupna intäkter" }, - { "7270", "2.24 Kortfristiga placeringar Andelar i koncernföretag" }, - { "7271", "2.25 Kortfristiga placeringar Övriga kortfristiga placeringar" }, - { "7281", "2.26 Kassa och bank Kassa, bank och redovisningsmedel" }, - { "7301", "2.27 Eget kapital Bundet eget kapital" }, - { "7302", "2.28 Eget kapital Fritt eget kapital" }, - { "7321", "2.29 Obeskattade reserver Periodiseringsfonder" }, - { "7322", "2.30 Obeskattade reserver Ackumulerade överavskrivningar" }, - { "7323", "2.31 Obeskattade reserver Övriga obeskattade reserver" }, - { "7331", "2.32 Avsättningar Avsättningar för pensioner och liknande förpliktelser enligt lag (1967:531)..." }, - { "7332", "2.33 Avsättningar Övriga avsättningar för pensioner och liknande förpliktelser" }, - { "7333", "2.34 Avsättningar Övriga avsättningar" }, - { "7350", "2.35 Långfristiga skulder Obligationslån" }, - { "7351", "2.36 Långfristiga skulder Checkräkningskredit" }, - { "7352", "2.37 Långfristiga skulder Övriga skulder till kreditinstitut" }, - { "7353", "2.38 Långfristiga skulder Skulder till koncern-, intresse- och gemensamt styrda företag" }, - { "7354", "2.39 Skulder till övriga företag som det finns ett ägarintresse i och övriga skulder" }, - { "7360", "2.40 Kortfristiga skulder Checkräkningskredit" }, - { "7361", "2.41 Kortfristiga skulder Övriga skulder till kreditinstitut" }, - { "7362", "2.42 Kortfristiga skulder Förskott från kunder" }, - { "7363", "2.43 Kortfristiga skulder Pågående arbeten för annans räkning" }, - { "7364", "2.44 Kortfristiga skulder Fakturerad men ej upparbetad intäkt" }, - { "7365", "2.45 Kortfristiga skulder Leverantörsskulder" }, - { "7366", "2.46 Kortfristiga skulder Växelskulder" }, - { "7367", "2.47 Kortfristiga skulder Skulder till koncern-, intresse- och gemensamt styrda företag" }, - { "7369", "2.48 Skulder till övriga företag som det finns ett ägarintresse i och övriga skulder" }, - { "7368", "2.49 Kortfristiga skulder Skattesskulder" }, - { "7370", "2.50 Kortfristiga skulder Upplupna kostnader och förutbetalda intäkter" }, - { "7410", "3.1 Nettoomsättning" }, - { "7411", "3.2 Förändring av lager av produkter i arbete, färdiga varor och pågående arbete för annans räkning. +" }, - { "7510", "3.2 Förändring av lager av produkter i arbete, färdiga varor och pågående arbete för annans räkning. -" }, - { "7412", "3.3 Aktiverat arbete för egen räkning" }, - { "7413", "3.4 Övriga rörelseintäkter" }, - { "7511", "3.5 Råvaror och förnödenheter" }, - { "7512", "3.6 Handelsvaror" }, - { "7513", "3.7 Övriga externa kostnader" }, - { "7514", "3.8 Personalkostnader" }, - { "7515", "3.9 Av- och nedskrivningar av materiella och immateriella anläggningstillgångar" }, - { "7516", "3.10 Nedskrivningar av omsättningstillgångar utöver normala nedskrivningar" }, - { "7517", "3.11 Övriga rörelsekostnader" }, - { "7414", "3.12 Resultat från andelar i koncernföretag +" }, - { "7518", "3.12 Resultat från andelar i koncernföretag -" }, - { "7415", "3.13 Resultat från andelar i intresseföretag och gemensamt styrda företag +" }, - { "7519", "3.13 Resultat från andelar i intresseföretag och gemensamt styrda företag -" }, - { "7423", "3.14 Resultat från övriga företag som det finns ett ägarintresse i +" }, - { "7530", "3.14 Resultat från övriga företag som det finns ett ägarintresse i -" }, - { "7416", "3.15 Resultat från övriga finansiella anläggningstillgångar +" }, - { "7520", "3.15 Resultat från övriga finansiella anläggningstillgångar -" }, - { "7417", "3.16 Övriga ränteintäkter och liknande resultatposter" }, - { "7521", "3.17 Nedskrivning av finansiella anläggningstillgångar och kortfristiga placeringar" }, - { "7522", "3.18 Räntekostnader och liknande resultatposter" }, - { "7524", "3.19 Lämnade koncernbidrag" }, - { "7419", "3.20 Mottagna koncernbidrag" }, - { "7420", "3.21 Återföring av periodiseringsfond" }, - { "7525", "3.22 Avsättning till periodiseringsfond" }, - { "7421", "3.23 Förändring av överavskrivningar +" }, - { "7526", "3.23 Förändring av överavskrivningar -" }, - { "7422", "3.24 Övriga bokslutsdispositioner +" }, - { "7527", "3.24 Övriga bokslutsdispositioner -" }, - { "7528", "3.25 Skatt på årets resultat" }, - { "7450", "3.26 Årets resultat, vinst (flyttas till p. 4.1)" }, - { "7550", "3.27 Årets resultat, förlust (flyttas till p. 4.2)" }, - { "7011", "Räkenskapsårets början" }, - { "7012", "Räkenskapsårets slut" }, - { "7650", "4.1 Årets resultat, vinst" }, - { "7750", "4.2 Årets resultat, förlust" }, - { "7651", "4.3a. Bokförda kostnader som inte ska dras av: a. Skatt på årets resultat" }, - { "7652", "b. Bokförda kostnader som inte ska dras av: b. Nedskrivning av finansiella tillgångar" }, - { "7653", "c. Bokförda kostnader som inte ska dras av: c. Andra bokförda kostnader" }, - { "7751", "4.4a. Kostnader som ska dras av men som inte ingår i det redovisade resultatet: a. Lämnade koncernbidrag" }, - { "7764", "b. Kostnader som ska dras av men som inte ingår i det redovisade resultatet: b. Andra ej bokförda kostnader" }, - { "7752", "4.5a. Bokförda intäkter som inte ska tas upp: a. Ackordsvinster" }, - { "7753", "b. Bokförda intäkter som inte ska tas upp: b. Utdelning" }, - { "7754", "c. Bokförda intäkter som inte ska tas upp: c. Andra bokförda intäkter" }, - { "7654", "4.6a. Intäkter som ska tas upp men som inte ingår i det redovisade resultatet a. Beräknad schablonintäkt på periodiseringsfonder vid beskattningsårets ingång" }, - { "7668", "b. Beräknad schablonintäkt på fondandelar ägda vid kalenderårets ingång" }, - { "7655", "c. Intäkter som ska tas upp men som inte ingår i det redovisade resultatet: c. Mottagna koncernbidrag" }, - { "7673", "d. Intäkter som ska tas upp men som inte ingår i det redovisade resultatet: d. Uppräknat belopp vid återföring av periodiseringsfond" }, - { "7665", "e. Intäkter som ska tas upp men som inte ingår i det redovisade resultatet: e. Andra ej bokförda intäkter" }, - { "7755", "4.7a. Avyttring av delägarrätter: a. Bokförd vinst" }, - { "7656", "b. Avyttring av delägarrätter: b. Bokförd förlust" }, - { "7756", "c. Avyttring av delägarrätter: c. Uppskov med kapitalvinst enligt blankett N4" }, - { "7657", "d. Avyttring av delägarrätter: d. Återfört uppskov med kapitalvinst enligt blankett N4" }, - { "7658", "e. Avyttring av delägarrätter: e. Kapitalvinst för beskattningsåret" }, - { "7757", "f. Avyttring av delägarrätter: f. Kapitalförlust som ska dras av" }, - { "7758", "4.8a. Andel i handelsbolag (inkl. avyttring): a. Bokförd intäkt/vinst" }, - { "7659", "b. Andel i handelsbolag (inkl. avyttring): b. Skattemässigt överskott enligt N3B" }, - { "7660", "c. Andel i handelsbolag (inkl. avyttring): c. Bokförd kostnad/förlust" }, - { "7759", "d. Andel i handelsbolag (inkl. avyttring): d. Skattemässigt underskott enligt N3B" }, - { "7666", "4.9 Skattemässig justering av bokfört resultat för avskrivningar på byggnader och annan fast egendom samt restvärdesavskrivning på maskiner och inventarier (+)" }, - { "7765", "4.9 Skattemässig justering av bokfört resultat för avskrivningar på byggnader och annan fast egendom samt restvärdesavskrivning på maskiner och inventarier (-)" }, - { "7661", "4.10 Skattemässig justering av bokfört resultat vid avyttring av näringsfastighet och näringsbostadsrätt" }, - { "7760", "4.10 Skattemässig korrigering av bokfört resultat vid avyttring av näringsfastighet och näringsbostadsrätt: -" }, - { "7761", "4.11 Skogs-/substansminskningsavdrag (specificeras på blankett N8)" }, - { "7662", "4.12 Återföringar vid avyttring av fastighet t.ex. värdeminskningsavdrag, skogsavdrag och substansminskningsavdrag..." }, - { "7663", "4.13 Andra skattemässiga justeringar av resultatet: +" }, - { "7762", "4.13 Andra skattemässiga justeringar av resultatet: -" }, - { "7763", "4.14a. Underskott: a. Outnyttjat underskott från föregående år" }, - { "7671", "4.14b. Reduktion av outnyttjat underskott med hänsyn till beloppsspärr, ackord eller konkurs" }, - { "7672", "4.14c. Reduktion av outnyttjat underskott med hänsyn till koncernbidragsspärr, fusionsspärr m.m. (beloppet ska också tas upp vid p. 1.2. på sid. 1)" }, - { "7670", "4.15 Överskott (flyttas till p. 1.1 på sid. 1)" }, - { "7770", "4.16 Underskott (flyttas till p. 1.2 på sid. 1)" }, - { "8020", "4.17 Årets begärda och tidigare års medgivna värdeminskningsavdrag som finns vid beskattningsårets utgång avseende byggnader." }, - { "8021", "4.18 Årets begärda och tidigare års medgivna värdeminskningsavdrag som finns vid beskattningsårets utgång avseende markanläggningar." }, - { "8023", "4.19 Vid restvärdesavskrivning: återförda belopp för av- och nedskrivning, försäljning, utrangering" }, - { "8026", "4.20 Lån från aktieägare (fysisk person) vid räkenskapsårets utgång" }, - { "8022", "4.21 Pensionskostnader (som ingår i p. 3.8)" }, - { "8028", "4.22 Koncernbidragsspärrat och fusionsspärrat underskott m.m. (frivillig uppgift)" }, - { "8040", "Uppdragstagare (t.ex. redovisningskonsult) har biträtt vid upprättandet av årsredovisningen: Ja" }, - { "8041", "Uppdragstagare (t.ex. redovisningskonsult) har biträtt vid upprättandet av årsredovisningen: Nej" }, - { "8044", "Årsredovisningen har varit föremål för revision: Ja" }, - { "8045", "Årsredovisningen har varit föremål för revision: Nej" }, -}; - -static const char *sru_label(const char *code) -{ - for (size_t i = 0; i < sizeof SRU_LABELS / sizeof SRU_LABELS[0]; i++) - if (strcmp(SRU_LABELS[i].code, code) == 0) - return SRU_LABELS[i].label; - return code; -} - -static void sru_rows(struct buf *t, yyjson_val *arr) -{ - size_t n = yyjson_arr_size(arr); - for (size_t i = 0; i < n; i++) { - yyjson_val *row = yyjson_arr_get(arr, i); - char lbl[256], amt[48]; - tui_pad_label(lbl, sizeof lbl, sru_label(vstr(row, "code")), 66); - tui_kr_format(vint(row, "amount") * 100, amt, sizeof amt); - buf_line(t, "%s %14s\n", lbl, amt); - } -} - -static void fmt_ink2(struct buf *t, const struct app *a, yyjson_val *res) -{ - buf_line(t, MK_HEAD "Inkomstdeklaration 2 (INK2)\n%s\n\n", a->org_name); - buf_line(t, MK_DIM "Räkenskapsår: %s - %s\n", vstr(res, "from"), - vstr(res, "to")); - buf_line(t, MK_DIM "Blankett: %s\n", vstr(res, "period")); - buf_line(t, "\n" MK_HEAD "INK2 - huvudblankett\n"); - buf_rule(t, 81); - sru_rows(t, yyjson_obj_get(res, "ink2")); - buf_line(t, "\n" MK_HEAD "INK2R - räkenskapsschema\n"); - buf_rule(t, 81); - sru_rows(t, yyjson_obj_get(res, "ink2r")); - buf_line(t, "\n" MK_HEAD "INK2S - skattemässiga justeringar\n"); - buf_rule(t, 81); - sru_rows(t, yyjson_obj_get(res, "ink2s")); - yyjson_val *un = yyjson_obj_get(res, "unmapped"); - size_t nu = yyjson_arr_size(un); - if (nu) { - buf_line(t, "\n" MK_HEAD "Konton utan SRU-mappning (måste åtgärdas):\n"); - for (size_t i = 0; i < nu; i++) { - yyjson_val *row = yyjson_arr_get(un, i); - char amt[48]; - tui_kr_format(vint(row, "amount_ore"), amt, sizeof amt); - buf_line(t, " %s %s\n", vstr(row, "account"), amt); - } - } -} - -static void sru_save(struct app *a) -{ - const char *home = getenv("HOME"); - char def[512]; - snprintf(def, sizeof def, "%s/Downloads", home && *home ? home : "."); - struct stat sb; - if (stat(def, &sb) != 0 || !S_ISDIR(sb.st_mode)) - snprintf(def, sizeof def, "%s", home && *home ? home : "."); - char dirbuf[512]; char *dir = tui_prompt_into(dirbuf, sizeof dirbuf, "Spara SRU-filer i: ", def, 0) ? dirbuf : NULL; - if (!dir || !*dir) - return; - char full[512]; - if (dir[0] == '~' && (dir[1] == '/' || dir[1] == '\0')) - snprintf(full, sizeof full, "%s%s", home ? home : "", dir + 1); - else - snprintf(full, sizeof full, "%s", dir); - char args[64]; - snprintf(args, sizeof args, "{\"fiscal_year\":%lld}", (long long)a->fy); - char *resp = client_rpc(&a->conn, "sru.export", a->session, a->org, args); - if (!resp || !client_ok(resp)) { - show_error("Kunde inte skapa SRU-filerna", resp); - free(resp); - return; - } - const char *keys[2] = { "result.info.content_base64", - "result.blanketter.content_base64" }; - const char *names[2] = { "INFO.SRU", "BLANKETTER.SRU" }; - char saved[2][600] = { "", "" }; - int ok = 0; - for (int i = 0; i < 2; i++) { - char *b64 = jstr_dup(resp, keys[i]); - unsigned char *data = NULL; - size_t n = 0; - if (!b64 || util_b64_decode(b64, strlen(b64), &data, &n) != 0) { - free(b64); - continue; - } - snprintf(saved[i], sizeof saved[i], "%s/%s", full, names[i]); - FILE *f = fopen(saved[i], "wb"); - if (f && fwrite(data, 1, n, f) == n) { - fclose(f); - ok++; - } else { - if (f) - fclose(f); - tui_message("SRU", "Kunde inte skriva %s: %s", saved[i], - strerror(errno)); - } - free(data); - free(b64); - } - free(resp); - if (ok == 2) - tui_message("SRU", "Sparat:\n%s\n%s\n\nLadda upp båda i Skatteverkets" - " filöverföring.", - saved[0], saved[1]); -} - -static void ink2_screen(struct app *a) -{ - char sargs[64]; - snprintf(sargs, sizeof sargs, "{\"fiscal_year\":%lld}", (long long)a->fy); - for (;;) { - char *resp = client_rpc(&a->conn, "sru.export", a->session, a->org, - sargs); - if (!resp || !client_ok(resp)) { - show_error("Inkomstdeklaration", resp); - free(resp); - break; - } - yyjson_doc *sd = parse(resp); - yyjson_val *sres = sd ? jget(yyjson_doc_get_root(sd), "result") : NULL; - struct buf out; - buf_init(&out); - if (sres) - fmt_ink2(&out, a, sres); - else - buf_line(&out, "%s", resp); - buf_append(&out, "", 1); - int sagain = tui_pager("Rapport", (const char *)out.p, "s = spara SRU"); - buf_free(&out); - yyjson_doc_free(sd); - free(resp); - if (sagain == 2) - sru_save(a); - if (!sagain) - break; - } -} - -static void reports_screen(struct app *a) -{ - static const char *const report_items[] = { - "Saldobalans (trial balance)", - "Resultaträkning", - "Balansräkning", - "Huvudbok", - "Verifikationslista", - "Momsdeklaration", - "Kontolista (alla konton)", - }; - static int last_sel = 0; - for (;;) { - if (g_quit) - return; - int sel = tui_menu("Rapporter", report_items, 7, 0, &last_sel); - if (sel < 0) - return; - if (sel == 6) { - accounts_report(a); - continue; - } - char args[256]; - char vat_from[16] = "", vat_to[16] = ""; - const char *cmd = NULL; - switch (sel) { - case 0: - cmd = "report.trial_balance"; - snprintf(args, sizeof args, - "{\"include_zero\":false,\"fiscal_year\":%lld}", - (long long)a->fy); - break; - case 1: - cmd = "report.income_statement"; - snprintf(args, sizeof args, "{\"fiscal_year\":%lld}", - (long long)a->fy); - break; - case 2: - cmd = "report.balance_sheet"; - snprintf(args, sizeof args, "{\"fiscal_year\":%lld}", - (long long)a->fy); - break; - case 3: - cmd = "report.general_ledger"; - snprintf(args, sizeof args, "{\"fiscal_year\":%lld}", - (long long)a->fy); - break; - case 4: - cmd = "report.voucher_list"; - snprintf(args, sizeof args, "{\"fiscal_year\":%lld}", - (long long)a->fy); - break; - case 5: { - cmd = "report.vat"; - char frombuf[16]; char *from = tui_date_prompt_into(frombuf, sizeof frombuf, "Från (YYYY-MM-DD): ", a->fy_start) ? frombuf : NULL; - if (!from) - continue; - char f[16]; - snprintf(f, sizeof f, "%s", from); - char tobuf[16]; char *to = tui_date_prompt_into(tobuf, sizeof tobuf, "Till (YYYY-MM-DD): ", a->fy_end) ? tobuf : NULL; - if (!to) - continue; - snprintf(vat_from, sizeof vat_from, "%s", f); - snprintf(vat_to, sizeof vat_to, "%s", to); - snprintf(args, sizeof args, - "{\"from\":\"%s\",\"to\":\"%s\"}", f, to); - break; - } - } - for (;;) { - char *resp = client_rpc(&a->conn, cmd, a->session, a->org, args); - if (!resp || !client_ok(resp)) { - show_error("Rapport", resp); - free(resp); - break; - } - yyjson_doc *d = parse(resp); - yyjson_val *res = - d ? jget(yyjson_doc_get_root(d), "result") : NULL; - struct buf out; - buf_init(&out); - if (!res) { - buf_line(&out, "%s", resp); - } else if (sel == 0) { - fmt_saldobalans(&out, a, res); - } else if (sel == 1) { - yyjson_doc *pd = NULL; - yyjson_val *prev = NULL; - int64_t pf = prev_fy_id(a); - if (pf) { - char pargs[64]; - snprintf(pargs, sizeof pargs, "{\"fiscal_year\":%lld}", - (long long)pf); - char *presp = client_rpc(&a->conn, - "report.income_statement", - a->session, a->org, pargs); - if (presp && client_ok(presp)) { - pd = parse(presp); - prev = pd ? jget(yyjson_doc_get_root(pd), "result") - : NULL; - } - free(presp); - } - fmt_resultat(&out, a, res, prev); - yyjson_doc_free(pd); - } else if (sel == 2) { - fmt_balans(&out, a, res); - } else if (sel == 3) { - fmt_huvudbok(&out, a, res); - } else if (sel == 4) { - fmt_verifikationslista(&out, a, res); - } else { - fmt_moms(&out, a, res); - } - buf_append(&out, "", 1); - int again = tui_pager("Rapport", (const char *)out.p, - sel == 5 ? "s = spara eSKD" : NULL); - buf_free(&out); - yyjson_doc_free(d); - free(resp); - if (again == 2 && sel == 5) { - eskd_save(a, vat_from, vat_to); - break; - } - if (!again) - break; - } - } -} - -/* Pick a voucher in the active fiscal year; returns its id or 0. */ -static int64_t pick_voucher(struct app *a) -{ - char args[96]; - snprintf(args, sizeof args, "{\"fiscal_year\":%lld,\"limit\":200}", - (long long)a->fy); - char *resp = - client_rpc(&a->conn, "voucher.list", a->session, a->org, args); - if (!resp || !client_ok(resp)) { - show_error("Verifikat", resp); - free(resp); - return 0; - } - size_t n = jarr_size(resp, "result.items"); - if (n == 0) { - tui_message("Verifikat", "Inga verifikat i räkenskapsåret."); - free(resp); - return 0; - } - char **items = xcalloc(n, sizeof(char *)); - int64_t *ids = xcalloc(n, sizeof(int64_t)); - for (size_t i = 0; i < n; i++) { - char path[64], ver[32], line[512]; - 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 *series = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.number", i); - int64_t number = jint_val(resp, path, 0); - 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(ver, sizeof ver, "%s%lld", series ? series : "", - (long long)number); - snprintf(line, sizeof line, "%-8s %-10s %s", ver, date ? date : "", - desc ? desc : ""); - items[i] = xstrdup(line); - free(series); - free(date); - free(desc); - } - int sel = tui_select_list("Välj verifikat", items, (int)n, 0, 1, NULL, 0, - NULL, 0); - int64_t out = sel >= 0 ? ids[sel] : 0; - for (size_t i = 0; i < n; i++) - free(items[i]); - free(items); - free(ids); - free(resp); - return out; -} - -static void inbox_upload(struct app *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("Fel", "Kunde inte läsa %s", path); - 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); - free(b64); - size_t reqlen = strlen(args) + 256; - char *raw = xmalloc(reqlen); - snprintf(raw, reqlen, - "{\"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)) - tui_message("Underlag", "Sparat."); - else - show_error("Kunde inte spara", r); - free(r); - free(path); -} - -struct inboxctx { - struct app *a; - int64_t *ids; - size_t n; - int cursor; -}; - -static int inbox_key(void *ud, int ch) -{ - struct inboxctx *ctx = ud; - struct app *a = ctx->a; - int64_t sel_id = (ctx->cursor >= 0 && (size_t)ctx->cursor < ctx->n) - ? ctx->ids[ctx->cursor] - : 0; - if (ch == 'k' && sel_id) { - int64_t vid = pick_voucher(a); - if (vid) { - char kargs[64]; - snprintf(kargs, sizeof kargs, "{\"id\":%lld,\"voucher_id\":%lld}", - (long long)sel_id, (long long)vid); - char *r = client_rpc(&a->conn, "attachment.link", a->session, - a->org, kargs); - if (r && client_ok(r)) - tui_message("Underlag", "Kopplat till verifikatet."); - else - show_error("Kunde inte koppla", r); - free(r); - } - return TUI_HOOK_REFRESH; - } - if (ch == 'a' || ch == 'A') { - inbox_upload(a); - return TUI_HOOK_REFRESH; - } - return TUI_HOOK_NONE; -} - -static void inbox_screen(struct app *a) -{ - int cursor = 0; - for (;;) { - if (g_quit) - return; - char *resp = client_rpc(&a->conn, "attachment.list", a->session, a->org, - "{\"unlinked\":true,\"limit\":200}"); - if (!resp || !client_ok(resp)) { - show_error("Underlag", resp); - free(resp); - return; - } - size_t n = jarr_size(resp, "result.items"); - char **items = xcalloc(n ? n : 1, sizeof(char *)); - int64_t *ids = xcalloc(n ? n : 1, sizeof(int64_t)); - for (size_t i = 0; i < n; i++) { - char path[64], line[512]; - snprintf(path, sizeof path, "result.items.%zu.id", i); - ids[i] = jint_val(resp, path, 0); - snprintf(path, sizeof path, "result.items.%zu.filename", i); - char *fn = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.size_bytes", i); - int64_t size = jint_val(resp, path, 0); - char fnbuf[256]; - snprintf(fnbuf, sizeof fnbuf, "%s", fn ? fn : ""); - tui_pad_field(fnbuf, sizeof fnbuf, 40); - snprintf(line, sizeof line, "%s %8lld byte", fnbuf, - (long long)size); - items[i] = xstrdup(line); - free(fn); - } - struct inboxctx ctx = { a, ids, n, cursor }; - int sel = tui_select_list_hook("Underlag (inkorg)", items, (int)n, - cursor, 1, &cursor, 1, NULL, 0, - inbox_key, &ctx); - int64_t sel_id = - (sel >= 0 && (size_t)sel < n) ? ids[sel] : 0; - for (size_t i = 0; i < n; i++) - free(items[i]); - free(items); - free(ids); - free(resp); - if (sel == -1) - return; - if (sel == -2) - continue; - if (sel == -4) { - inbox_upload(a); - continue; - } - if (sel_id) - attachment_download(a, sel_id); - } -} - -/* ------------------------------------------------------------------ */ -/* bankavstämning */ -/* ------------------------------------------------------------------ */ - -struct bank_match { - int64_t voucher_id; - char series[16]; - int64_t number; -}; - -struct bank_sugg { - int64_t voucher_id; - char series[16]; - int64_t number; - char date[16]; - int64_t amount_ore; -}; - -struct bank_tx { - int64_t id; - char booked_at[16]; - char text[256]; - int64_t amount_ore; - struct bank_match *matches; - size_t nmatches; - struct bank_sugg *suggestions; - size_t nsuggestions; -}; - -static void bank_ver(char *buf, size_t n, const char *series, int64_t number) -{ - snprintf(buf, n, "%s%lld", series ? series : "", (long long)number); -} - -/* Unmatched first, then newest booked_at first. */ -static int bank_tx_cmp(const void *pa, const void *pb) -{ - const struct bank_tx *x = pa, *y = pb; - int xm = x->nmatches > 0, ym = y->nmatches > 0; - if (xm != ym) - return xm - ym; - int c = strcmp(y->booked_at, x->booked_at); - if (c) - return c; - return x->id < y->id ? -1 : x->id > y->id; -} - -static void bank_txs_free(struct bank_tx *txs, size_t n) -{ - for (size_t i = 0; i < n; i++) { - free(txs[i].matches); - free(txs[i].suggestions); - } - free(txs); -} - -static void bank_tx_row(const struct bank_tx *t, char *line, size_t cap) -{ - char text[256], amt[48], tail[64] = ""; - snprintf(text, sizeof text, "%s", t->text); - tui_pad_field(text, sizeof text, 32); - tui_amt_col(amt, sizeof amt, 13, t->amount_ore); - if (t->nmatches > 0) { - char ver[32]; - bank_ver(ver, sizeof ver, t->matches[0].series, - t->matches[0].number); - if (t->nmatches > 1) - snprintf(tail, sizeof tail, "→ %s +%zu", ver, t->nmatches - 1); - else - snprintf(tail, sizeof tail, "→ %s", ver); - } else if (t->nsuggestions > 0) { - char ver[32]; - bank_ver(ver, sizeof ver, t->suggestions[0].series, - t->suggestions[0].number); - snprintf(tail, sizeof tail, "(förslag %s)", ver); - } - snprintf(line, cap, "%s %s %s %s %s", t->nmatches > 0 ? "✓" : "·", - t->booked_at, text, amt, tail); -} - -/* Enter on an unmatched row: suggest vouchers, or pick one freely. */ -static void bank_match_tx(struct app *a, const struct bank_tx *t) -{ - char **items = xcalloc(t->nsuggestions + 1, sizeof(char *)); - for (size_t i = 0; i < t->nsuggestions; i++) { - const struct bank_sugg *s = &t->suggestions[i]; - char ver[32], amt[48], diff[48], line[256]; - bank_ver(ver, sizeof ver, s->series, s->number); - tui_amt_col(amt, sizeof amt, 13, s->amount_ore); - tui_amt_col(diff, sizeof diff, 13, t->amount_ore - s->amount_ore); - snprintf(line, sizeof line, "%s %-8s %s %s", s->date, ver, amt, - diff); - items[i] = xstrdup(line); - } - items[t->nsuggestions] = xstrdup("Välj annat verifikat…"); - int sel = tui_select_list("Matcha transaktion", items, - (int)t->nsuggestions + 1, 0, 0, NULL, 0, NULL, - 0); - for (size_t i = 0; i <= t->nsuggestions; i++) - free(items[i]); - free(items); - if (sel < 0) - return; - int64_t vid; - if ((size_t)sel < t->nsuggestions) { - vid = t->suggestions[sel].voucher_id; - } else { - vid = pick_voucher(a); - if (!vid) - return; - } - char args[64]; - snprintf(args, sizeof args, - "{\"transaction_id\":%lld,\"voucher_id\":%lld}", - (long long)t->id, (long long)vid); - char *resp = client_rpc(&a->conn, "bank.match", a->session, a->org, args); - if (!resp || !client_ok(resp)) { - show_error("Bankavstämning", resp); - free(resp); - return; - } - int64_t diff = jint_val(resp, "result.difference_ore", 0); - if (diff) { - char dbuf[48]; - tui_kr_format(diff, dbuf, sizeof dbuf); - tui_message("Bankavstämning", "Matchat. Differens: %s", dbuf); - } else { - tui_message("Bankavstämning", "Matchat."); - } - free(resp); -} - -static void bank_unmatch_tx(struct app *a, const struct bank_tx *t) -{ - if (t->nmatches == 0) - return; - if (t->nmatches == 1) { - if (!tui_confirm("Bankavstämning", - "Koppla bort transaktionen %s från verifikatet?", - t->text)) - return; - } else if (!tui_confirm("Bankavstämning", - "Koppla bort transaktionen %s från %zu verifikat?", - t->text, t->nmatches)) { - return; - } - int ok = 1; - for (size_t i = 0; i < t->nmatches; i++) { - char args[64]; - snprintf(args, sizeof args, - "{\"transaction_id\":%lld,\"voucher_id\":%lld}", - (long long)t->id, (long long)t->matches[i].voucher_id); - char *resp = client_rpc(&a->conn, "bank.unmatch", a->session, a->org, - args); - if (!resp || !client_ok(resp)) { - show_error("Bankavstämning", resp); - ok = 0; - } - free(resp); - } - if (ok) - tui_message("Bankavstämning", "Matchningen borttagen."); -} - -static void bank_import_file(struct app *a) -{ - char *path = file_browser(a, a->attachment_dir); - if (!path) - return; - char *b64 = read_file_b64(path); - if (!b64) { - tui_message("Fel", "Kunde inte läsa %s", path); - free(path); - return; - } - free(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, "format", "seb"); - yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64); - yyjson_mut_obj_add_strcpy(d, o, "account", "1930"); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - free(b64); - if (!args) - return; - char *resp = rpc_dry(a, "bank.import", args); - if (!resp || !client_ok(resp)) { - show_error("Bankavstämning", resp); - free(resp); - free(args); - return; - } - int64_t imported = jint_val(resp, "result.imported", 0); - int64_t duplicates = jint_val(resp, "result.duplicates", 0); - free(resp); - if (!tui_confirm("Bankavstämning", - "%lld transaktioner (%lld dubbletter). Importera?", - (long long)imported, (long long)duplicates)) { - free(args); - return; - } - resp = client_rpc(&a->conn, "bank.import", a->session, a->org, args); - free(args); - if (resp && client_ok(resp)) - tui_message("Bankavstämning", "%lld transaktioner (%lld dubbletter).", - (long long)jint_val(resp, "result.imported", 0), - (long long)jint_val(resp, "result.duplicates", 0)); - else - show_error("Bankavstämning", resp); - free(resp); -} - -struct bankctx { - struct app *a; - struct bank_tx *txs; - size_t n; - int cursor; -}; - -static int bank_key(void *ud, int ch) -{ - struct bankctx *ctx = ud; - if (ch == 'a' || ch == 'A') { - bank_import_file(ctx->a); - return TUI_HOOK_REFRESH; - } - if ((ch == 'u' || ch == 'U') && ctx->cursor >= 0 && - (size_t)ctx->cursor < ctx->n) { - bank_unmatch_tx(ctx->a, &ctx->txs[ctx->cursor]); - return TUI_HOOK_REFRESH; - } - return TUI_HOOK_NONE; -} - -static void bank_screen(struct app *a) -{ - int64_t keep_id = 0; - for (;;) { - if (g_quit) - return; - char *resp = client_rpc(&a->conn, "bank.list", a->session, a->org, - "{\"status\":\"all\",\"limit\":500}"); - if (!resp || !client_ok(resp)) { - show_error("Bankavstämning", resp); - free(resp); - return; - } - yyjson_doc *doc = parse(resp); - yyjson_val *items = - doc ? jget(yyjson_doc_get_root(doc), "result.items") : NULL; - size_t n = items && yyjson_is_arr(items) ? yyjson_arr_size(items) : 0; - struct bank_tx *txs = xcalloc(n ? n : 1, sizeof *txs); - for (size_t i = 0; i < n; i++) { - yyjson_val *it = yyjson_arr_get(items, i); - txs[i].id = vint(it, "id"); - snprintf(txs[i].booked_at, sizeof txs[i].booked_at, "%s", - vstr(it, "booked_at")); - snprintf(txs[i].text, sizeof txs[i].text, "%s", vstr(it, "text")); - txs[i].amount_ore = vint(it, "amount_ore"); - yyjson_val *ms = yyjson_obj_get(it, "matches"); - size_t nm = ms && yyjson_is_arr(ms) ? yyjson_arr_size(ms) : 0; - if (nm > 0) { - txs[i].matches = xcalloc(nm, sizeof *txs[i].matches); - txs[i].nmatches = nm; - for (size_t k = 0; k < nm; k++) { - yyjson_val *m = yyjson_arr_get(ms, k); - txs[i].matches[k].voucher_id = vint(m, "voucher_id"); - snprintf(txs[i].matches[k].series, - sizeof txs[i].matches[k].series, "%s", - vstr(m, "series")); - txs[i].matches[k].number = vint(m, "number"); - } - } - yyjson_val *ss = yyjson_obj_get(it, "suggestions"); - size_t ns = ss && yyjson_is_arr(ss) ? yyjson_arr_size(ss) : 0; - if (ns > 0) { - txs[i].suggestions = xcalloc(ns, sizeof *txs[i].suggestions); - txs[i].nsuggestions = ns; - for (size_t k = 0; k < ns; k++) { - yyjson_val *s = yyjson_arr_get(ss, k); - txs[i].suggestions[k].voucher_id = vint(s, "voucher_id"); - snprintf(txs[i].suggestions[k].series, - sizeof txs[i].suggestions[k].series, "%s", - vstr(s, "series")); - txs[i].suggestions[k].number = vint(s, "number"); - snprintf(txs[i].suggestions[k].date, - sizeof txs[i].suggestions[k].date, "%s", - vstr(s, "date")); - txs[i].suggestions[k].amount_ore = vint(s, "amount_ore"); - } - } - } - qsort(txs, n, sizeof *txs, bank_tx_cmp); - int start = 0; - for (size_t i = 0; i < n; i++) - if (txs[i].id == keep_id) - start = (int)i; - int64_t unmatched = jint_val(resp, "result.summary.unmatched", -1); - if (unmatched < 0) { - unmatched = 0; - for (size_t i = 0; i < n; i++) - if (txs[i].nmatches == 0) - unmatched++; - } - char title[64]; - snprintf(title, sizeof title, "Bankavstämning (%lld omatchade)", - (long long)unmatched); - size_t rows = n > 0 ? n : 1; - char **lines = xcalloc(rows, sizeof(char *)); - if (n == 0) { - lines[0] = xstrdup("+ Importera bankfil (^N)"); - } else { - for (size_t i = 0; i < n; i++) { - char line[512]; - bank_tx_row(&txs[i], line, sizeof line); - lines[i] = xstrdup(line); - } - } - struct bankctx ctx = { a, txs, n, start }; - int sel = tui_select_list_hook(title, lines, (int)rows, start, 1, - &ctx.cursor, 1, NULL, 0, bank_key, - &ctx); - for (size_t i = 0; i < rows; i++) - free(lines[i]); - free(lines); - if (sel == -1) { - bank_txs_free(txs, n); - free(resp); - yyjson_doc_free(doc); - return; - } - if (sel >= 0 && (size_t)sel < n) { - keep_id = txs[sel].id; - if (txs[sel].nmatches == 0) - bank_match_tx(a, &txs[sel]); - } else if (sel >= 0) { - bank_import_file(a); - } - bank_txs_free(txs, n); - free(resp); - yyjson_doc_free(doc); - } -} - -static void audit_verify_run(struct app *a, int full) -{ - char *resp = client_rpc(&a->conn, "audit.verify", a->session, a->org, - full ? "{\"full\":true}" : "{}"); - if (!resp || !client_ok(resp)) { - show_error("Revision", resp); - free(resp); - return; - } - int64_t checked = jint_val(resp, "result.checked", 0); - int64_t vouchers = jint_val(resp, "result.vouchers_checked", 0); - int is_ok = jbool_val(resp, "result.ok", 0); - if (is_ok) { - if (full) - tui_message("Hashkedjor", - "Kedjorna är intakta.\n%lld verifikat och %lld " - "händelser kontrollerade.\n%lld underlag " - "hash-kontrollerade.", - (long long)vouchers, (long long)checked, - (long long)jint_val(resp, "result.attachments_checked", - 0)); - else - tui_message("Hashkedjor", - "Kedjorna är intakta.\n%lld verifikat och %lld " - "händelser kontrollerade.", - (long long)vouchers, (long long)checked); - } else { - int64_t bad_v = jint_val(resp, "result.first_bad_voucher_id", 0); - int64_t bad_a = jint_val(resp, "result.first_bad_seq", 0); - int64_t unbal = jint_val(resp, "result.first_unbalanced_voucher_id", 0); - int64_t bad_att = - jint_val(resp, "result.first_bad_attachment_id", 0); - char detail[320] = ""; - if (bad_v) - snprintf(detail + strlen(detail), sizeof detail - strlen(detail), - "Första trasiga verifikat: %lld\n", (long long)bad_v); - if (bad_a) - snprintf(detail + strlen(detail), sizeof detail - strlen(detail), - "Första trasiga händelse: %lld\n", (long long)bad_a); - if (unbal) - snprintf(detail + strlen(detail), sizeof detail - strlen(detail), - "Första obalanserade verifikat: %lld\n", - (long long)unbal); - if (bad_att) - snprintf(detail + strlen(detail), sizeof detail - strlen(detail), - "Första trasiga underlag: %lld\n", (long long)bad_att); - tui_message("Hashkedjor", "KEDJORNA ÄR BRUTNA!\n%s", detail); - } - free(resp); -} - -static void audit_screen(struct app *a) -{ - static const char *const audit_items[] = { "Verifiera hashkedjan", - "Verifiera allt (inkl. underlag)", - "Senaste händelser" }; - static int last_sel = 0; - for (;;) { - if (g_quit) - return; - int sel = tui_menu("Revision", audit_items, 3, 0, &last_sel); - if (sel < 0) - return; - if (sel == 0) { - audit_verify_run(a, 0); - } else if (sel == 1) { - audit_verify_run(a, 1); - } else { - for (;;) { - char *resp = client_rpc(&a->conn, "audit.list", a->session, - a->org, "{\"limit\":200}"); - if (!resp || !client_ok(resp)) { - show_error("Revision", resp); - free(resp); - break; - } - struct buf text; - buf_init(&text); - size_t n = jarr_size(resp, "result.items"); - for (size_t i = 0; i < n; i++) { - char path[64], line[512]; - snprintf(path, sizeof path, "result.items.%zu.at", i); - char *at = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.action", i); - char *action = jstr_dup(resp, path); - snprintf(path, sizeof path, - "result.items.%zu.result_code", i); - char *code = jstr_dup(resp, path); - snprintf(path, sizeof path, - "result.items.%zu.actor_user_id", i); - int64_t actor = jint_val(resp, path, 0); - snprintf(line, sizeof line, "%s anv %lld %-22s %s\n", - at ? at : "", (long long)actor, - action ? action : "", code ? code : ""); - buf_append(&text, line, strlen(line)); - free(at); - free(action); - free(code); - } - buf_append(&text, "\0", 1); - int again = tui_pager("Behandlingshistorik", (char *)text.p, NULL); - buf_free(&text); - free(resp); - if (!again) - break; - } - } - } -} - -struct tui_trow { - char account[16]; - char formula[64]; - char text[64]; -}; - -struct tplform { - struct app *a; - struct tui_rt rt; - struct tui_trow rows[64]; - int nrows; - char name[128]; - char desc[256]; - char series[16]; - int64_t id; -}; - -static void tpl_cell(void *ud, int row, int col, char **buf, size_t *cap) -{ - struct tplform *tf = ud; - struct tui_trow *r = &tf->rows[row]; - if (col == 0) { - *buf = r->account; - *cap = sizeof r->account; - } else if (col == 2) { - *buf = r->formula; - *cap = sizeof r->formula; - } else { - *buf = r->text; - *cap = sizeof r->text; - } -} - -static void tpl_info(void *ud, int row, char *buf, size_t n) -{ - struct tplform *tf = ud; - const char *nm = acct_name(tf->a, tf->rows[row].account); - snprintf(buf, n, "%s", nm ? nm : ""); -} - -static void tpl_footer(void *ud, char *buf, size_t n) -{ - (void)ud; - snprintf(buf, n, "x = belopp i kronor. Positivt blir debet, negativt" - " kredit."); -} - -/* F5 dry-runs; Ctrl+Enter creates/updates. Returns 1 when saved. */ -static int tpl_save(struct tplform *tf, int dry) -{ - const char *problem = NULL; - char msg[256] = ""; - if (!tf->name[0]) - problem = "Namn saknas."; - int used = 0; - for (int i = 0; i < tf->rt.nrows && !problem; i++) { - if (!tf->rows[i].account[0]) - continue; - if (!acct_exists(tf->a, tf->rows[i].account)) { - snprintf(msg, sizeof msg, "Konto %.15s finns inte (rad %d).", - tf->rows[i].account, i + 1); - problem = msg; - break; - } - if (!tf->rows[i].formula[0] || !formula_valid(tf->rows[i].formula)) { - snprintf(msg, sizeof msg, - "Ogiltig formel på rad %d: '%.60s'.", i + 1, - tf->rows[i].formula); - problem = msg; - break; - } - used++; - } - if (!problem && used == 0) - problem = "Mallen behöver minst en rad."; - if (problem) { - tui_message("Mall", "%s", problem); - return 0; - } - - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - if (tf->id) - yyjson_mut_obj_add_int(d, o, "id", tf->id); - yyjson_mut_obj_add_strcpy(d, o, "name", tf->name); - yyjson_mut_obj_add_strcpy(d, o, "series", tf->series); - yyjson_mut_obj_add_strcpy(d, o, "description", tf->desc); - yyjson_mut_val *arr = yyjson_mut_arr(d); - for (int i = 0; i < tf->rt.nrows; i++) { - if (!tf->rows[i].account[0]) - continue; - yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); - yyjson_mut_obj_add_strcpy(d, ro, "account", tf->rows[i].account); - yyjson_mut_obj_add_strcpy(d, ro, "formula", tf->rows[i].formula); - if (tf->rows[i].text[0]) - yyjson_mut_obj_add_strcpy(d, ro, "description", - tf->rows[i].text); - } - yyjson_mut_obj_add_val(d, o, "rows", arr); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - const char *cmd = tf->id ? "template.update" : "template.create"; - char *resp = dry ? rpc_dry(tf->a, cmd, args) - : client_rpc(&tf->a->conn, cmd, tf->a->session, - tf->a->org, args); - free(args); - if (resp && client_ok(resp)) { - if (dry) { - tui_message("Validering OK", "Mallen är korrekt (%d rader).", - used); - } else { - tui_message("Mall", "Mallen '%s' sparad.", tf->name); - free(resp); - return 1; - } - } else { - show_error("Kunde inte spara mallen", resp); - } - free(resp); - return 0; -} - -/* Template editor: a form, not a wizard. load_name == NULL creates a new - template, otherwise the named template is loaded and saved via - template.update. */ -static int template_form(struct app *a, const char *load_name) -{ - struct tplform tf; - memset(&tf, 0, sizeof tf); - tf.a = a; - snprintf(tf.series, sizeof tf.series, "%s", a->default_series); - if (load_name && *load_name) { - 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", load_name); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *resp = - client_rpc(&a->conn, "template.get", a->session, a->org, args); - free(args); - if (!resp || !client_ok(resp)) { - show_error("Mall", resp); - free(resp); - return 0; - } - tf.id = jint_val(resp, "result.id", 0); - char *nm = jstr_dup(resp, "result.name"); - char *ser = jstr_dup(resp, "result.series"); - char *ds = jstr_dup(resp, "result.description"); - if (nm) - snprintf(tf.name, sizeof tf.name, "%s", nm); - if (ser && *ser) - snprintf(tf.series, sizeof tf.series, "%s", ser); - if (ds) - snprintf(tf.desc, sizeof tf.desc, "%s", ds); - free(nm); - free(ser); - free(ds); - size_t n = jarr_size(resp, "result.rows"); - if (n > 64) - n = 64; - for (size_t i = 0; i < n; 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.formula", i); - char *form = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.rows.%zu.description", i); - char *rd = jstr_dup(resp, path); - snprintf(tf.rows[i].account, sizeof tf.rows[i].account, "%s", - acc ? acc : ""); - snprintf(tf.rows[i].formula, sizeof tf.rows[i].formula, "%s", - form ? form : ""); - snprintf(tf.rows[i].text, sizeof tf.rows[i].text, "%s", - rd ? rd : ""); - free(acc); - free(form); - free(rd); - } - tf.nrows = n > 0 ? (int)n : 1; - free(resp); - } - int text_w = COLS - 57; - if (text_w < 8) - text_w = 8; - struct tui_rt_col cols[4] = { - { "Konto", 2, 6, TUI_RT_EDIT }, - { "Namn", 9, 24, TUI_RT_INFO }, - { "Formel", 34, 20, TUI_RT_EDIT }, - { "Radtext", 55, text_w, TUI_RT_EDIT }, - }; - tui_rt_init(&tf.rt, tf.nrows > 0 ? tf.nrows : 1, 64, 4, cols, 7, tpl_cell, - tpl_info, &tf); - tui_rt_set_footer(&tf.rt, tpl_footer); - struct tui_form_field ff[3]; - memset(ff, 0, sizeof ff); - ff[0].label = "Namn"; - ff[0].value = tf.name; - ff[0].cap = sizeof tf.name; - ff[0].kind = TUI_F_TEXT; - ff[1].label = "Serie"; - ff[1].value = tf.series; - ff[1].cap = sizeof tf.series; - ff[1].kind = TUI_F_TEXT; - ff[2].label = "Text"; - ff[2].value = tf.desc; - ff[2].cap = sizeof tf.desc; - ff[2].kind = TUI_F_TEXT; - tui_rt_set_fields(&tf.rt, ff, 3); - const char *title = load_name ? "Redigera mall" : "Ny mall"; - const char *hint = "Enter = ändra fält Tab = byta fält F5 = validera" - " ^X = rensa rad ^Enter = spara Esc = avbryt"; - for (;;) { - int rr = tui_rt_run(title, &tf.rt, hint); - if (rr == -1) - return 0; - if (rr == -2) { - tpl_save(&tf, 1); - continue; - } - if (rr == -3 && tpl_save(&tf, 0)) - return 1; - } -} - -static void template_archive(struct app *a, const char *name) -{ - char label[256]; - snprintf(label, sizeof label, "Arkivera '%s'? (j/n): ", name); - char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, label, "n", 0) ? ansbuf : NULL; - if (!ans || (ans[0] != 'j' && ans[0] != 'J')) - return; - 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", name); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *resp = - client_rpc(&a->conn, "template.archive", a->session, a->org, args); - free(args); - if (resp && client_ok(resp)) - tui_message("Mall", "Mallen '%s' arkiverad.", name); - else - show_error("Kunde inte arkivera", resp); - free(resp); -} - -/* ------------------------------------------------------------------ */ -/* ingående balans (series IB) */ -/* ------------------------------------------------------------------ */ - -struct tui_ibrow { - char account[16]; - char amount[32]; - char text[64]; -}; - -/* Loads the net IB per account for the active fiscal year. */ -/* Effective opening balances of the selected year: the year's IB vouchers - plus all earlier movements, exactly what the reports show. */ -static int ib_load(struct app *a, char ***out_acc, int64_t **out_amt, - int *out_n) -{ - *out_acc = NULL; - *out_amt = NULL; - *out_n = 0; - char args[128]; - snprintf(args, sizeof args, - "{\"fiscal_year\":%lld,\"include_zero\":true}", - (long long)a->fy); - char *resp = client_rpc(&a->conn, "report.trial_balance", a->session, - a->org, args); - if (!resp || !client_ok(resp)) { - show_error("Ingående balans", resp); - free(resp); - return -1; - } - size_t n = jarr_size(resp, "result.accounts"); - char **accs = xcalloc(n ? n : 1, sizeof(char *)); - int64_t *amts = xcalloc(n ? n : 1, sizeof(int64_t)); - int count = 0; - for (size_t i = 0; i < n; i++) { - char path[64]; - snprintf(path, sizeof path, "result.accounts.%zu.ib_ore", i); - int64_t ib = jint_val(resp, path, 0); - if (ib == 0) - continue; - snprintf(path, sizeof path, "result.accounts.%zu.account", i); - char *acc = jstr_dup(resp, path); - if (!acc) - continue; - accs[count] = acc; - amts[count] = ib; - count++; - } - free(resp); - *out_acc = accs; - *out_amt = amts; - *out_n = count; - return 0; -} - -static int64_t ib_old_lookup(char **accs, int64_t *amts, int n, - const char *account) -{ - for (int i = 0; i < n; i++) - if (strcmp(accs[i], account) == 0) - return amts[i]; - return 0; -} - -struct ibform { - struct app *a; - struct tui_rt rt; - struct tui_ibrow rows[64]; - char **old_acc; - int64_t *old_amt; - int nold; -}; - -static void ib_cell(void *ud, int row, int col, char **buf, size_t *cap) -{ - struct ibform *ib = ud; - struct tui_ibrow *r = &ib->rows[row]; - if (col == 0) { - *buf = r->account; - *cap = sizeof r->account; - } else if (col == 2) { - *buf = r->amount; - *cap = sizeof r->amount; - } else { - *buf = r->text; - *cap = sizeof r->text; - } -} - -static void ib_info(void *ud, int row, char *buf, size_t n) -{ - struct ibform *ib = ud; - const char *nm = acct_name(ib->a, ib->rows[row].account); - snprintf(buf, n, "%s", nm ? nm : ""); -} - -static void ib_footer(void *ud, char *buf, size_t n) -{ - (void)ud; - snprintf(buf, n, "Positiva belopp = debet, negativa = kredit. Summan ska" - " bli noll."); -} - -static const char *ib_check(struct ibform *ib, int *used_out, char *msg, - size_t msglen) -{ - int64_t sum = 0; - int used = 0; - for (int i = 0; i < ib->rt.nrows; i++) { - if (!ib->rows[i].account[0]) - continue; - if (!acct_exists(ib->a, ib->rows[i].account)) { - snprintf(msg, msglen, "Konto %.15s finns inte (rad %d).", - ib->rows[i].account, i + 1); - return msg; - } - double kr = 0; - if (ib->rows[i].amount[0] && - !parse_x_double(ib->rows[i].amount, &kr)) { - snprintf(msg, msglen, "Ogiltigt belopp på rad %d: '%.20s'.", i + 1, - ib->rows[i].amount); - return msg; - } - sum += (int64_t)llround(kr * 100.0); - used++; - } - if (used == 0) - return "Ange minst ett konto."; - if (sum != 0) { - snprintf(msg, msglen, "Summan måste bli noll (nu %lld öre).", - (long long)sum); - return msg; - } - *used_out = used; - return NULL; -} - -/* Posts the delta against the existing IB so nothing is ever edited. */ -static int ib_post_delta(struct ibform *ib) -{ - struct app *a = ib->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", a->fy_start); - yyjson_mut_obj_add_strcpy(d, o, "description", "Ingående balans"); - yyjson_mut_obj_add_strcpy(d, o, "series", "IB"); - char *ref = util_random_id("tui-", 8); - yyjson_mut_obj_add_strcpy(d, o, "client_ref", ref); - free(ref); - yyjson_mut_val *arr = yyjson_mut_arr(d); - int posted = 0; - for (int i = 0; i < ib->rt.nrows; i++) { - if (!ib->rows[i].account[0]) - continue; - double kr = 0; - if (ib->rows[i].amount[0] && - !parse_x_double(ib->rows[i].amount, &kr)) - kr = 0; - int64_t new_amt = (int64_t)llround(kr * 100.0); - int64_t prev_amt = - ib_old_lookup(ib->old_acc, ib->old_amt, ib->nold, - ib->rows[i].account); - int64_t delta = new_amt - prev_amt; - if (delta == 0) - continue; - yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); - yyjson_mut_obj_add_strcpy(d, ro, "account", ib->rows[i].account); - yyjson_mut_obj_add_int(d, ro, "debit_ore", delta > 0 ? delta : 0); - yyjson_mut_obj_add_int(d, ro, "credit_ore", delta < 0 ? -delta : 0); - if (ib->rows[i].text[0]) - yyjson_mut_obj_add_strcpy(d, ro, "description", - ib->rows[i].text); - posted++; - } - for (int i = 0; i < ib->nold; i++) { - int still = 0; - for (int j = 0; j < ib->rt.nrows; j++) - if (strcmp(ib->rows[j].account, ib->old_acc[i]) == 0) { - still = 1; - break; - } - if (!still && ib->old_amt[i] != 0) { - yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); - yyjson_mut_obj_add_strcpy(d, ro, "account", ib->old_acc[i]); - yyjson_mut_obj_add_int(d, ro, "debit_ore", - ib->old_amt[i] < 0 ? -ib->old_amt[i] : 0); - yyjson_mut_obj_add_int(d, ro, "credit_ore", - ib->old_amt[i] > 0 ? ib->old_amt[i] : 0); - posted++; - } - } - if (posted == 0) { - yyjson_mut_doc_free(d); - tui_message("Ingående balans", "Ingen ändring."); - return 0; - } - yyjson_mut_obj_add_val(d, o, "rows", arr); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *resp = - client_rpc(&a->conn, "voucher.post", a->session, a->org, args); - free(args); - if (resp && client_ok(resp)) { - int64_t num = jint_val(resp, "result.number", 0); - tui_message("Ingående balans", - "Sparat som IB %lld (%d justerade rader).", - (long long)num, posted); - free(resp); - return 1; - } - show_error("Kunde inte spara ingående balans", resp); - free(resp); - return 0; -} - -static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) -{ - struct ibform ib; - memset(&ib, 0, sizeof ib); - ib.a = a; - ib.old_acc = old_acc; - ib.old_amt = old_amt; - ib.nold = nold; - if (nold > 63) { - tui_message("Ingående balans", - "För många konton (%d) för att redigera här.", nold); - return 0; - } - for (int i = 0; i < nold && i < 63; i++) { - snprintf(ib.rows[i].account, sizeof ib.rows[i].account, "%s", - old_acc[i]); - tui_kr_format(old_amt[i], ib.rows[i].amount, - sizeof ib.rows[i].amount); - } - int text_w = COLS - 51; - if (text_w < 6) - text_w = 6; - struct tui_rt_col cols[4] = { - { "Konto", 2, 6, TUI_RT_EDIT }, - { "Namn", 9, 24, TUI_RT_INFO }, - { "Belopp", 34, 14, TUI_RT_EDIT }, - { "Text", 49, text_w, TUI_RT_EDIT }, - }; - tui_rt_init(&ib.rt, nold > 0 ? nold : 1, 64, 4, cols, 5, ib_cell, ib_info, - &ib); - tui_rt_set_footer(&ib.rt, ib_footer); - - for (;;) { - int rr = tui_rt_run("Ingående balans", &ib.rt, - "Tab = byta fält F5 = validera ^X = rensa rad" - " ^Enter = spara Esc = avbryt"); - if (rr == -1) - return 0; - char msg[256]; - int used = 0; - const char *problem = ib_check(&ib, &used, msg, sizeof msg); - if (problem) { - tui_message("Ingående balans", "%s", problem); - continue; - } - if (rr == -2) { - tui_message("Validering OK", "%d konton, summan är noll.", used); - continue; - } - if (ib_post_delta(&ib)) - return 1; - } -} - -struct ibpager { - struct app *a; - char **accs; - int64_t *amts; - int n; -}; - -static int ib_view_key(void *ud, int ch) -{ - struct ibpager *p = ud; - if (ch == 'e' || ch == 'E' || ch == TUI_KEY_CTRL_N) { - if (ib_form(p->a, p->accs, p->amts, p->n)) - return TUI_HOOK_REFRESH; - return TUI_HOOK_STAY; - } - return TUI_HOOK_NONE; -} - -/* The list is a read-only table with no selection: tui_pager shows the - columns and the sum as plain text, and the e/^N keys run the editor. */ -static void ib_screen(struct app *a) -{ - for (;;) { - char **accs = NULL; - int64_t *amts = NULL; - int n = 0; - if (ib_load(a, &accs, &amts, &n) != 0) - return; - struct buf t; - buf_init(&t); - buf_line(&t, "Räkenskapsår %s ingående balans (effektiv)\n\n", - a->fy_label); - buf_line(&t, "%-7s%-53s%s\n\n", "Konto", "Namn", "Belopp"); - int64_t sum = 0; - for (int i = 0; i < n; i++) { - sum += amts[i]; - char amount[32], nbuf[128]; - tui_kr_format(amts[i], amount, sizeof amount); - const char *nm = acct_name(a, accs[i]); - snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : ""); - tui_pad_field(nbuf, sizeof nbuf, 50); - buf_line(&t, "%-6s %s %14s\n", accs[i], nbuf, amount); - } - if (n == 0) - buf_line(&t, "Ingen ingående balans registrerad ännu.\n"); - char sumbuf[32]; - tui_kr_format(sum, sumbuf, sizeof sumbuf); - buf_line(&t, "\nSumma: %s\n", sumbuf); - buf_append(&t, "\0", 1); - struct ibpager p = { a, accs, amts, n }; - int ret = tui_pager_hook("Ingående balans", (const char *)t.p, - "e/^N = redigera", 0, ib_view_key, &p); - buf_free(&t); - for (int i = 0; i < n; i++) - free(accs[i]); - free(accs); - free(amts); - if (ret == 0) - return; - } -} - -struct company_field { - const char *key; - const char *label; - int numeric; /* 0 = text, 1 = month (1-12), 2 = unsigned integer */ -}; - -static const struct company_field COMPANY_FIELDS[] = { - { "name", "Företagsnamn", 0 }, - { "org_nr", "Organisationsnummer", 0 }, - { "vat_nr", "Momsregistreringsnummer", 0 }, - { "address", "Adress", 0 }, - { "postal_code", "Postnummer", 0 }, - { "city", "Ort", 0 }, - { "country", "Land", 0 }, - { "email", "E-post", 0 }, - { "phone", "Telefon", 0 }, - { "description", "Verksamhetsbeskrivning", 0 }, - { "shares", "Antal aktier", 2 }, - { "moms_period", "Momsperiod (month/quarter/year)", 0 }, - { "framework", "Regelverk (K2/K3)", 0 }, - { "fiscal_year_start_month", "Startmånad räkenskapsår (1-12)", 1 }, -}; - -static int company_field_valid(const struct company_field *f, const char *v) -{ - if (f->numeric) { - char *end = NULL; - long n = strtol(v, &end, 10); - if (!end || *end != '\0') - return 0; - return f->numeric == 2 ? n >= 0 : (n >= 1 && n <= 12); - } - if (strcmp(f->key, "moms_period") == 0) - return strcmp(v, "month") == 0 || strcmp(v, "quarter") == 0 || - strcmp(v, "year") == 0; - if (strcmp(f->key, "framework") == 0) - return strcmp(v, "K2") == 0 || strcmp(v, "K3") == 0; - return 1; -} - -/* Board members shown in the årsredovisning (name + title). */ -static void board_screen(struct app *a) -{ - int owner = a->role[0] && strcmp(a->role, "owner") == 0; - int cursor = 0; - for (;;) { - if (g_quit) - return; - char *resp = client_rpc(&a->conn, "board.list", a->session, a->org, - "{}"); - if (!resp || !client_ok(resp)) { - show_error("Styrelseledamöter", resp); - free(resp); - return; - } - size_t n = jarr_size(resp, "result.items"); - int rows = (int)n + (owner ? 1 : 0); - char **lines = xcalloc(rows ? rows : 1, sizeof(char *)); - char **names = xcalloc(n + 1, sizeof(char *)); - char **titles = xcalloc(n + 1, sizeof(char *)); - int64_t *ids = xcalloc(n + 1, sizeof(int64_t)); - for (size_t i = 0; i < n; i++) { - char path[64], line[512]; - snprintf(path, sizeof path, "result.items.%zu.id", i); - ids[i] = jint_val(resp, path, 0); - snprintf(path, sizeof path, "result.items.%zu.name", i); - names[i] = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.title", i); - titles[i] = jstr_dup(resp, path); - snprintf(line, sizeof line, "%s %s", - titles[i] ? titles[i] : "", names[i] ? names[i] : ""); - lines[i] = xstrdup(line); - } - if (owner) - lines[n] = xstrdup("+ Ny ledamot (^N)"); - int s = tui_select_list("Styrelseledamöter", lines, rows, cursor, 1, - &cursor, owner ? 1 : 0, - owner && n > 0 ? "ta bort" : NULL, 0); - if (s == -6 && cursor >= 0 && (size_t)cursor < n) { - char q[320]; - snprintf(q, sizeof q, "Ta bort '%s'? (j/n): ", lines[cursor]); - 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 args[64]; - snprintf(args, sizeof args, "{\"id\":%lld}", - (long long)ids[cursor]); - char *r = client_rpc(&a->conn, "board.remove", a->session, - a->org, args); - if (r && client_ok(r)) { - tui_message("Styrelseledamöter", "Borttagen."); - } else { - show_error("Kunde inte ta bort", r); - } - free(r); - } - } else if (s == -4 || (s >= 0 && owner && (size_t)s == n)) { - char namebuf[512] = "", titlebuf[512] = ""; - int ok_name = tui_prompt_into(namebuf, sizeof namebuf, "Namn: ", - "", 0); - int ok_title = ok_name && namebuf[0] - ? tui_prompt_into(titlebuf, sizeof titlebuf, - "Titel: ", - "Styrelseledamot", 0) - : 0; - if (ok_name && namebuf[0] && ok_title && titlebuf[0]) { - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - yyjson_mut_obj_add_strcpy(d, o, "name", namebuf); - yyjson_mut_obj_add_strcpy(d, o, "title", titlebuf); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *r = client_rpc(&a->conn, "board.add", a->session, - a->org, args); - free(args); - if (r && client_ok(r)) - tui_message("Styrelseledamöter", "Tillagd."); - else - show_error("Kunde inte lägga till", r); - free(r); - } - } else if (s >= 0 && (size_t)s < n) { - if (!owner) { - tui_message("Styrelseledamöter", - "Endast ägare kan ändra styrelseledamöterna."); - } else { - char namebuf[512] = "", titlebuf[512] = ""; - int ok_name = tui_prompt_into(namebuf, sizeof namebuf, - "Namn: ", - names[s] ? names[s] : "", 0); - int ok_title = ok_name && namebuf[0] - ? tui_prompt_into( - titlebuf, sizeof titlebuf, "Titel: ", - titles[s] ? titles[s] : "", 0) - : 0; - if (ok_name && namebuf[0] && ok_title && titlebuf[0]) { - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - yyjson_mut_obj_add_int(d, o, "id", ids[s]); - yyjson_mut_obj_add_strcpy(d, o, "name", namebuf); - yyjson_mut_obj_add_strcpy(d, o, "title", titlebuf); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *r = client_rpc(&a->conn, "board.update", a->session, - a->org, args); - free(args); - if (r && client_ok(r)) - tui_message("Styrelseledamöter", "Sparad."); - else - show_error("Kunde inte spara", r); - free(r); - } - } - } - int back = s == -1; - for (int i = 0; i < rows; i++) - free(lines[i]); - free(lines); - for (size_t i = 0; i < n; i++) { - free(names[i]); - free(titles[i]); - } - free(names); - free(titles); - free(ids); - free(resp); - if (back) - return; - } -} - -static void company_screen(struct app *a) -{ - static int sel = 0; - const int nf = (int)(sizeof COMPANY_FIELDS / sizeof COMPANY_FIELDS[0]); - const int ninv = 2; - static const char *const invkeys[2] = { "invoice_bankgiro", - "invoice_our_ref" }; - static const char *const invlabels[2] = { "Bankgiro", - "Vår referens (faktura)" }; - int owner = a->role[0] && strcmp(a->role, "owner") == 0; - for (;;) { - if (g_quit) - return; - char *resp = client_rpc(&a->conn, "org.get", a->session, a->org, "{}"); - if (!resp || !client_ok(resp)) { - show_error("Företagsuppgifter", resp); - free(resp); - return; - } - char *vals[20]; - struct tui_form_field ff[20]; - memset(ff, 0, sizeof ff); - for (int i = 0; i < nf; i++) { - char path[64]; - vals[i] = xcalloc(512, 1); - snprintf(path, sizeof path, "result.%s", COMPANY_FIELDS[i].key); - if (COMPANY_FIELDS[i].numeric) { - snprintf(vals[i], 512, "%lld", - (long long)jint_val(resp, path, 0)); - } else { - char *v = jstr_dup(resp, path); - snprintf(vals[i], 512, "%s", v ? v : ""); - free(v); - } - ff[i].label = COMPANY_FIELDS[i].label; - ff[i].value = vals[i]; - ff[i].cap = 512; - ff[i].kind = TUI_F_TEXT; - } - free(resp); - char *sresp = - client_rpc(&a->conn, "settings.get", a->session, a->org, "{}"); - for (int i = 0; i < ninv; i++) { - char path[64]; - int idx = nf + i; - vals[idx] = xcalloc(512, 1); - snprintf(path, sizeof path, "result.%s", invkeys[i]); - char *v = sresp ? jstr_dup(sresp, path) : NULL; - snprintf(vals[idx], 512, "%s", v ? v : ""); - free(v); - ff[idx].label = invlabels[i]; - ff[idx].value = vals[idx]; - ff[idx].cap = 512; - ff[idx].kind = TUI_F_TEXT; - } - free(sresp); - ff[nf + ninv].label = "Styrelseledamöter"; - ff[nf + ninv].kind = TUI_F_ACTION; - int r = tui_form_run("Företagsuppgifter", ff, nf + ninv + 1, owner, - &sel); - if (r >= 0 && r < nf) { - if (!company_field_valid(&COMPANY_FIELDS[r], vals[r])) { - tui_message("Ogiltigt värde", "%s", COMPANY_FIELDS[r].label); - } else { - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - if (COMPANY_FIELDS[r].numeric) - yyjson_mut_obj_add_int(d, o, COMPANY_FIELDS[r].key, - strtoll(vals[r], NULL, 10)); - else - yyjson_mut_obj_add_strcpy(d, o, COMPANY_FIELDS[r].key, - vals[r]); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *rr = client_rpc(&a->conn, "org.update", a->session, - a->org, args); - free(args); - if (!rr || !client_ok(rr)) - show_error("Kunde inte spara", rr); - free(rr); - } - } else if (r >= nf && r < nf + ninv) { - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - yyjson_mut_obj_add_strcpy(d, o, "key", invkeys[r - nf]); - yyjson_mut_obj_add_strcpy(d, o, "value", vals[r]); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *rr = client_rpc(&a->conn, "settings.set", a->session, - a->org, args); - free(args); - if (!rr || !client_ok(rr)) - show_error("Kunde inte spara", rr); - free(rr); - } - if (r == nf + ninv) - board_screen(a); - for (int i = 0; i < nf + ninv; i++) - free(vals[i]); - if (r == TUI_FORM_BACK) - return; - } -} - -static const char *const SETTING_KEYS[] = { - "default_series", - "attachment_dir", - "invoice_receivable_account", - "invoice_revenue_account", - "smtp_host", - "smtp_port", - "smtp_user", - "smtp_from", - "smtp_reply_to", - "smtp_security", - "smtp_password", -}; -static const char *const SETTING_LABELS[] = { - "Standardserie", - "Bilagornas mapp", - "Fordringskonto", - "Intäktskonto", - "SMTP-server", - "SMTP-port", - "SMTP-användare", - "SMTP-avsändare", - "SMTP-svarsadress", - "SMTP-säkerhet", - "SMTP-lösenord", -}; -static const char *const SECURITY_CHOICES[] = { "starttls", "tls", "plain" }; - -static void settings_screen(struct app *a) -{ - static int sel = 0; - const int nset = (int)(sizeof SETTING_KEYS / sizeof SETTING_KEYS[0]); - for (;;) { - if (g_quit) - return; - char *resp = - client_rpc(&a->conn, "settings.get", a->session, a->org, "{}"); - if (!resp || !client_ok(resp)) { - show_error("Inställningar", resp); - free(resp); - return; - } - int pw_set = jbool_val(resp, "result.smtp_password_set", 0); - char pwstatus[192]; - snprintf(pwstatus, sizeof pwstatus, - "SMTP-lösenord (lämna tomt för oförändrat): %s", - pw_set ? "satt" : "inte satt"); - char *vals[16]; - struct tui_form_field ff[16]; - memset(ff, 0, sizeof ff); - int sec_choice = 0; - for (int i = 0; i < nset; i++) { - char path[64], *v; - vals[i] = xcalloc(512, 1); - snprintf(path, sizeof path, "result.%s", SETTING_KEYS[i]); - v = jstr_dup(resp, path); - snprintf(vals[i], 512, "%s", v ? v : ""); - free(v); - const char *key = SETTING_KEYS[i]; - ff[i].label = SETTING_LABELS[i]; - ff[i].value = vals[i]; - ff[i].cap = 512; - ff[i].kind = TUI_F_TEXT; - if (strcmp(key, "smtp_security") == 0) { - ff[i].kind = TUI_F_CHOICE; - ff[i].choices = SECURITY_CHOICES; - ff[i].nchoices = 3; - ff[i].choice = &sec_choice; - for (int k = 0; k < 3; k++) - if (strcmp(vals[i], SECURITY_CHOICES[k]) == 0) - sec_choice = k; - } else if (strcmp(key, "smtp_password") == 0) { - ff[i].mask = 1; - } - } - free(resp); - struct tui_form_action acts[1]; - memset(acts, 0, sizeof acts); - acts[0].label = pwstatus; - acts[0].enabled = -1; /* dim status row, not selectable */ - int r = tui_form_run_actions( - "Inställningar", ff, nset, 1, acts, 1, - "upp/ned/Home/End Enter = ändra F5 = uppdatera" - " Esc/q = tillbaka ^C = avsluta", - &sel); - if (r >= 0 && r < nset) { - const char *key = SETTING_KEYS[r]; - const char *val = vals[r]; - if (strcmp(key, "smtp_security") == 0) - val = SECURITY_CHOICES[sec_choice]; - if (strcmp(key, "smtp_password") == 0 && !*vals[r]) { - /* empty means "keep the stored secret" */ - } else { - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - yyjson_mut_obj_add_strcpy(d, o, "key", key); - yyjson_mut_obj_add_strcpy(d, o, "value", val); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *rr = client_rpc(&a->conn, "settings.set", a->session, - a->org, args); - free(args); - if (rr && client_ok(rr)) { - snprintf(vals[r], 512, "%s", val); - if (strcmp(key, "default_series") == 0) - snprintf(a->default_series, - sizeof a->default_series, "%s", val); - else if (strcmp(key, "attachment_dir") == 0) - snprintf(a->attachment_dir, - sizeof a->attachment_dir, "%s", val); - } else { - show_error("Kunde inte spara inställningen", rr); - } - free(rr); - } - } - for (int i = 0; i < nset; i++) - free(vals[i]); - if (r == TUI_FORM_BACK) - return; - } -} - -/* ------------------------------------------------------------------ */ -/* invoicing: Fakturor, Kunder */ -/* ------------------------------------------------------------------ */ - -static void today_iso(char *buf, size_t n) -{ - time_t now = time(NULL); - struct tm tm; - localtime_r(&now, &tm); - char tmp[64]; - snprintf(tmp, sizeof tmp, "%04d-%02d-%02d", tm.tm_year + 1900, - tm.tm_mon + 1, tm.tm_mday); - size_t len = strlen(tmp); - if (len >= n) - len = n ? n - 1 : 0; - memcpy(buf, tmp, len); - if (n) - buf[len] = '\0'; -} - -/* Directory for generated invoice PDFs. */ -static void invoice_cache_dir(char *buf, size_t n) -{ - const char *cache = getenv("XDG_CACHE_HOME"); - if (cache && *cache) { - snprintf(buf, n, "%s/bokf", cache); - return; - } - snprintf(buf, n, "/tmp"); -} - -/* Keep a customer-supplied name from escaping the directory. */ -static void safe_fname(const char *in, char *out, size_t n) -{ - size_t o = 0; - for (const char *p = in ? in : ""; *p && o + 1 < n; p++) { - unsigned char c = (unsigned char)*p; - if (c < 32 || c == '/') - c = '-'; - out[o++] = (char)c; - } - out[o] = '\0'; -} - -static int write_pdf_b64(const char *b64, const char *path) -{ - unsigned char *data = NULL; - size_t len = 0; - if (!b64 || util_b64_decode(b64, strlen(b64), &data, &len) != 0) - return -1; - FILE *fp = fopen(path, "wb"); - if (!fp) { - free(data); - return -1; - } - size_t wrote = fwrite(data, 1, len, fp); - fclose(fp); - free(data); - return wrote == len ? 0 : -1; -} - -/* Open a saved PDF with xdg-open when a desktop session is present; - otherwise (or on fork failure) show where it was saved. */ -static void open_pdf(const char *path) -{ - const char *display = getenv("DISPLAY"); - const char *wayland = getenv("WAYLAND_DISPLAY"); - if ((!display || !*display) && (!wayland || !*wayland)) { - tui_message("Faktura PDF", "Sparad: %s", path); - return; - } - pid_t pid = fork(); - if (pid == 0) { - pid_t gc = fork(); - if (gc == 0) { - setsid(); - execlp("xdg-open", "xdg-open", path, (char *)NULL); - _exit(127); - } - _exit(gc < 0 ? 1 : 0); - } - if (pid > 0) - waitpid(pid, NULL, 0); - else - tui_message("Faktura PDF", "Sparad: %s", path); -} - -static void save_and_open_pdf(const char *b64, const char *filename) -{ - char dir[512], path[700], safe[600]; - invoice_cache_dir(dir, sizeof dir); - safe_fname(filename, safe, sizeof safe); - if (snprintf(path, sizeof path, "%s/%s", dir, safe) >= (int)sizeof path) { - tui_message("Faktura PDF", "Sökvägen blir för lång."); - return; - } - config_mkdirs(path); - if (write_pdf_b64(b64, path) != 0) { - tui_message("Faktura PDF", "Kunde inte spara %s", path); - return; - } - open_pdf(path); -} - -static const char *invoice_status_sv(const char *s) -{ - if (s && strcmp(s, "credited") == 0) - return "krediterad"; - return "utfärdad"; -} - -static void qty_sv(int64_t milli, char *buf, size_t n) -{ - int64_t whole = milli / 1000; - int64_t frac = milli % 1000; - char tmp[64]; - if (frac == 0) { - snprintf(tmp, sizeof tmp, "%lld", (long long)whole); - } else { - if (frac < 0) - frac = -frac; - char fr[16]; - snprintf(fr, sizeof fr, "%03lld", (long long)frac); - while (fr[0] && fr[strlen(fr) - 1] == '0') - fr[strlen(fr) - 1] = '\0'; - snprintf(tmp, sizeof tmp, "%lld,%s", (long long)whole, fr); - } - size_t len = strlen(tmp); - if (len >= n) - len = n ? n - 1 : 0; - memcpy(buf, tmp, len); - if (n) - buf[len] = '\0'; -} - -static const char *vat_label(const char *code) -{ - if (!code) - return ""; - if (strcmp(code, "25") == 0) - return "25%"; - if (strcmp(code, "12") == 0) - return "12%"; - if (strcmp(code, "6") == 0) - return "6%"; - if (strcmp(code, "0") == 0) - return "0%"; - if (strcmp(code, "rc") == 0) - return "Omvänd moms"; - if (strcmp(code, "eu") == 0) - return "EU"; - return code; -} - -static const char *vat_code_of(const char *s) -{ - if (!s || !*s) - return "25"; - if (strcmp(s, "25") == 0 || strcmp(s, "25%") == 0) - return "25"; - if (strcmp(s, "12") == 0 || strcmp(s, "12%") == 0) - return "12"; - if (strcmp(s, "6") == 0 || strcmp(s, "6%") == 0) - return "6"; - if (strcmp(s, "0") == 0 || strcmp(s, "0%") == 0) - return "0"; - if (strcasecmp(s, "rc") == 0 || strcasecmp(s, "Omvänd moms") == 0) - return "rc"; - if (strcasecmp(s, "eu") == 0 || strcasecmp(s, "EU") == 0) - return "eu"; - return NULL; -} - -static const char *unit_code_of(const char *s) -{ - if (!s || !*s) - return "st"; - if (strcasecmp(s, "st") == 0) - return "st"; - if (strcasecmp(s, "tim") == 0) - return "tim"; - if (strcasecmp(s, "dag") == 0) - return "dag"; - if (strcasecmp(s, "km") == 0) - return "km"; - return NULL; -} - -static int unit_index(const char *s) -{ - static const char *const opts[] = { "st", "tim", "dag", "km" }; - if (s && *s) { - for (int i = 0; i < 4; i++) - if (strcmp(s, opts[i]) == 0) - return i; - } - return 0; -} - -static int qty_is_valid(const char *s) -{ - if (!s || !*s) - return 0; - int sep = 0, frac = 0, intd = 0, nonzero = 0; - for (const char *p = s; *p; p++) { - if (*p == ',' || *p == '.') { - if (sep) - return 0; - sep = 1; - continue; - } - if (*p < '0' || *p > '9') - return 0; - if (*p != '0') - nonzero = 1; - if (sep) { - frac++; - if (frac > 3) - return 0; - } else if (++intd > 15) { - return 0; - } - } - if (!intd || (sep && frac == 0)) - return 0; - return nonzero; -} - -/* ---------------------------- Kunder ------------------------------ */ - -static int customer_save(struct app *a, int64_t id, const char *name, - const char *address, const char *postal, - const char *city, const char *vat, const char *email, - const char *your_ref, const char *payment, - const char *notes, int dry) -{ - if (!name || !*name) { - tui_message("Kund", "Namn krävs."); - return 0; - } - if (!payment || !*payment) { - tui_message("Kund", "Betalningsdagar krävs."); - return 0; - } - for (const char *p = payment; *p; p++) { - if (*p < '0' || *p > '9') { - tui_message("Kund", "Betalningsdagar ska vara ett heltal."); - return 0; - } - } - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - if (id > 0) - yyjson_mut_obj_add_int(d, o, "id", id); - yyjson_mut_obj_add_strcpy(d, o, "name", name); - yyjson_mut_obj_add_strcpy(d, o, "address", address); - yyjson_mut_obj_add_strcpy(d, o, "postal_code", postal); - yyjson_mut_obj_add_strcpy(d, o, "city", city); - yyjson_mut_obj_add_strcpy(d, o, "vat_nr", vat); - yyjson_mut_obj_add_strcpy(d, o, "email", email); - yyjson_mut_obj_add_strcpy(d, o, "your_ref", your_ref); - yyjson_mut_obj_add_int(d, o, "payment_days", strtoll(payment, NULL, 10)); - yyjson_mut_obj_add_strcpy(d, o, "notes", notes); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - const char *cmd = id > 0 ? "customer.update" : "customer.create"; - char *r = dry ? rpc_dry(a, cmd, args) - : client_rpc(&a->conn, cmd, a->session, a->org, args); - free(args); - int ok = r && client_ok(r); - if (ok) - tui_message("Kund", dry ? "Uppgifterna är giltiga." : "Sparat."); - else - show_error(dry ? "Kunden kunde inte valideras" : "Kunde inte spara", - r); - free(r); - return ok; -} - -static void customer_form(struct app *a, int64_t id) -{ - char name[256] = "", address[512] = "", postal[32] = "", city[128] = ""; - char vat[64] = "", email[256] = "", your_ref[128] = ""; - char payment[16] = "30", notes[2048] = ""; - if (id > 0) { - char args[64]; - snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id); - char *resp = - client_rpc(&a->conn, "customer.get", a->session, a->org, args); - if (!resp || !client_ok(resp)) { - show_error("Kund", resp); - free(resp); - return; - } - struct { - const char *key; - char *dst; - size_t cap; - } fields[] = { - { "name", name, sizeof name }, - { "address", address, sizeof address }, - { "postal_code", postal, sizeof postal }, - { "city", city, sizeof city }, - { "vat_nr", vat, sizeof vat }, - { "email", email, sizeof email }, - { "your_ref", your_ref, sizeof your_ref }, - { "notes", notes, sizeof notes }, - }; - for (size_t i = 0; i < sizeof fields / sizeof fields[0]; i++) { - char path[64], *v; - snprintf(path, sizeof path, "result.%s", fields[i].key); - v = jstr_dup(resp, path); - if (v) { - snprintf(fields[i].dst, fields[i].cap, "%s", v); - free(v); - } - } - snprintf(payment, sizeof payment, "%lld", - (long long)jint_val(resp, "result.payment_days", 30)); - free(resp); - } - const char *labels[] = { "Namn", "Adress", - "Postnummer", "Ort", - "Momsreg.nr", "E-post", - "Er referens", "Betalningsdagar", - "Anteckningar" }; - char *vals[] = { name, address, postal, city, vat, - email, your_ref, payment, notes }; - size_t caps[] = { sizeof name, sizeof address, sizeof postal, - sizeof city, sizeof vat, sizeof email, - sizeof your_ref, sizeof payment, sizeof notes }; - struct tui_form_field ff[10]; - memset(ff, 0, sizeof ff); - for (int i = 0; i < 9; i++) { - ff[i].label = labels[i]; - ff[i].value = vals[i]; - ff[i].cap = caps[i]; - ff[i].kind = TUI_F_TEXT; - } - ff[9].label = ""; - ff[9].value = "Spara"; - ff[9].kind = TUI_F_ACTION; - static int sel = 0; - for (;;) { - int r = tui_form_run(id > 0 ? "Kund" : "Ny kund", ff, 10, 1, &sel); - if (r == TUI_FORM_BACK) - return; - if (r == TUI_FORM_REFRESH) { - customer_save(a, id, name, address, postal, city, vat, email, - your_ref, payment, notes, 1); - continue; - } - if (r == TUI_FORM_SUBMIT || r == 9) { - if (customer_save(a, id, name, address, postal, city, vat, email, - your_ref, payment, notes, 0)) - return; - } - } -} - -static void customer_archive_toggle(struct app *a, int64_t id, const char *name, - int active) -{ - if (!tui_confirm("Kund", active ? "Arkivera kund %s?" : - "Återaktivera kund %s?", - name ? name : "")) - return; - char args[128]; - snprintf(args, sizeof args, "{\"id\":%lld,\"active\":%s}", (long long)id, - active ? "false" : "true"); - char *r = - client_rpc(&a->conn, "customer.archive", a->session, a->org, args); - if (r && client_ok(r)) - tui_message("Kund", active ? "Arkiverad." : "Återaktiverad."); - else - show_error("Kund", r); - free(r); -} - -static void customers_screen(struct app *a) -{ - char **items = NULL; - char **names = NULL; - int64_t *ids = NULL; - unsigned char *active = NULL; - size_t n = 0; - int fetch = 1; - for (;;) { - if (g_quit) { - for (size_t i = 0; i < n; i++) { - free(items[i]); - free(names[i]); - } - free(items); - free(names); - free(ids); - free(active); - return; - } - if (fetch) { - for (size_t i = 0; i < n; i++) { - free(items[i]); - free(names[i]); - } - free(items); - free(names); - free(ids); - free(active); - items = NULL; - names = NULL; - ids = NULL; - active = NULL; - n = 0; - fetch = 0; - char *resp = client_rpc(&a->conn, "customer.list", a->session, - a->org, "{}"); - if (!resp || !client_ok(resp)) { - show_error("Kunder", resp); - free(resp); - return; - } - n = jarr_size(resp, "result.items"); - items = xcalloc(n + 1, sizeof(char *)); - names = xcalloc(n + 1, sizeof(char *)); - ids = xcalloc(n ? n : 1, sizeof(int64_t)); - active = xcalloc(n + 1, 1); - for (size_t i = 0; i < n; i++) { - char path[64], line[768]; - snprintf(path, sizeof path, "result.items.%zu.id", i); - ids[i] = jint_val(resp, path, 0); - snprintf(path, sizeof path, "result.items.%zu.active", i); - active[i] = (unsigned char)jbool_val(resp, path, 1); - snprintf(path, sizeof path, "result.items.%zu.name", i); - char *name = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.postal_code", i); - char *postal = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.city", i); - char *city = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.vat_nr", i); - char *vat = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.payment_days", i); - int64_t days = jint_val(resp, path, 0); - char nbuf[256], pbuf[256], vbuf[80]; - snprintf(nbuf, sizeof nbuf, "%s", name ? name : ""); - tui_pad_field(nbuf, sizeof nbuf, 30); - snprintf(pbuf, sizeof pbuf, "%s %s", postal ? postal : "", - city ? city : ""); - tui_pad_field(pbuf, sizeof pbuf, 30); - snprintf(vbuf, sizeof vbuf, "%s", vat ? vat : ""); - tui_pad_field(vbuf, sizeof vbuf, 18); - snprintf(line, sizeof line, "%s %s %s %3lld dagar %s", nbuf, - pbuf, vbuf, (long long)days, - active[i] ? "" : "(arkiverad)"); - items[i] = xstrdup(line); - names[i] = xstrdup(name ? name : ""); - free(name); - free(postal); - free(city); - free(vat); - } - free(resp); - items[n] = xstrdup("+ Ny kund (^N)"); - } - int start = 0; - if (a->customer_sel) { - for (size_t i = 0; i < n; i++) - if (ids[i] == a->customer_sel) { - start = (int)i; - break; - } - } - int cur = start; - int sel = tui_select_list("Kunder", items, (int)n + 1, start, 1, &cur, - 1, "arkivera/återaktivera", 0); - if (cur >= 0 && (size_t)cur < n) - a->customer_sel = ids[cur]; - if (sel == -1) { - for (size_t i = 0; i < n; i++) { - free(items[i]); - free(names[i]); - } - free(items); - free(names); - free(ids); - free(active); - return; - } - if (sel == -2) { - fetch = 1; - continue; - } - if (sel == -6 && cur >= 0 && (size_t)cur < n) { - customer_archive_toggle(a, ids[cur], names[cur], - active[cur] != 0); - fetch = 1; - continue; - } - if (sel == -4 || (sel >= 0 && (size_t)sel == n)) { - customer_form(a, 0); - fetch = 1; - continue; - } - if (sel >= 0 && (size_t)sel < n) { - customer_form(a, ids[sel]); - fetch = 1; - continue; - } - } -} - -/* --------------------------- Fakturor ----------------------------- */ - -struct invctx { - struct app *a; - int64_t id; - int64_t number; - int64_t customer_id; - char customer[256]; -}; - -static void inv_detail_pdf(struct invctx *ctx) -{ - char args[64]; - snprintf(args, sizeof args, "{\"id\":%lld}", (long long)ctx->id); - char *resp = client_rpc(&ctx->a->conn, "invoice.pdf", ctx->a->session, - ctx->a->org, args); - if (!resp || !client_ok(resp)) { - show_error("Kunde inte hämta PDF", resp); - free(resp); - return; - } - char *b64 = jstr_dup(resp, "result.content_base64"); - if (b64) { - char fname[800]; - snprintf(fname, sizeof fname, "Faktura %lld %s.pdf", - (long long)ctx->number, ctx->customer); - save_and_open_pdf(b64, fname); - free(b64); - } - free(resp); -} - -static void inv_detail_send(struct invctx *ctx) -{ - struct app *a = ctx->a; - char email[256] = ""; - if (ctx->customer_id > 0) { - char cargs[64]; - snprintf(cargs, sizeof cargs, "{\"id\":%lld}", - (long long)ctx->customer_id); - char *c = - client_rpc(&a->conn, "customer.get", a->session, a->org, cargs); - if (c && client_ok(c)) { - char *e = jstr_dup(c, "result.email"); - if (e) { - snprintf(email, sizeof email, "%s", e); - free(e); - } - } - free(c); - } - if (!email[0]) { - tui_message("Skicka faktura", "Kunden har ingen e-postadress."); - return; - } - if (!tui_confirm("Skicka faktura", "Skicka faktura %lld till %s?", - (long long)ctx->number, email)) - return; - char args[64]; - snprintf(args, sizeof args, "{\"id\":%lld}", (long long)ctx->id); - char *r = - client_rpc(&a->conn, "invoice.send", a->session, a->org, args); - if (r && client_ok(r)) { - char *to = jstr_dup(r, "result.sent_to"); - tui_message("Skickat", "Faktura %lld skickad till %s.", - (long long)ctx->number, to && *to ? to : email); - free(to); - } else { - show_error("Skicka faktura", r); - } - free(r); -} - -static int inv_key(void *ud, int ch) -{ - struct invctx *ctx = ud; - if (ch == 'p') { - inv_detail_pdf(ctx); - return TUI_HOOK_STAY; - } - if (ch == 's') { - inv_detail_send(ctx); - return TUI_HOOK_STAY; - } - return TUI_HOOK_NONE; -} - -static void invoices_detail(struct app *a, int64_t id) -{ - char args[64]; - snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id); - for (;;) { - char *resp = - client_rpc(&a->conn, "invoice.get", a->session, a->org, args); - if (!resp || !client_ok(resp)) { - show_error("Faktura", resp); - free(resp); - return; - } - int64_t number = jint_val(resp, "result.number", 0); - int64_t customer_id = jint_val(resp, "result.customer_id", 0); - int64_t net = jint_val(resp, "result.net_ore", 0); - int64_t vat = jint_val(resp, "result.vat_ore", 0); - int64_t total = jint_val(resp, "result.total_ore", 0); - char *cust = jstr_dup(resp, "result.customer_name"); - char *status = jstr_dup(resp, "result.status"); - char *date = jstr_dup(resp, "result.invoice_date"); - char *due = jstr_dup(resp, "result.due_date"); - char *delivery = jstr_dup(resp, "result.delivery_date"); - char *your_ref = jstr_dup(resp, "result.your_ref"); - char *our_ref = jstr_dup(resp, "result.our_ref"); - char *notes = jstr_dup(resp, "result.notes"); - char *ocr = jstr_dup(resp, "result.ocr"); - char *sent_at = jstr_dup(resp, "result.last_sent_at"); - char *sent_to = jstr_dup(resp, "result.last_sent_to"); - struct buf t; - buf_init(&t); - buf_line(&t, MK_HEAD "Faktura %lld %s\n", (long long)number, - invoice_status_sv(status)); - buf_line(&t, MK_DIM "%s\n\n", cust ? cust : ""); - buf_line(&t, "Fakturadatum: %s\n", date ? date : ""); - buf_line(&t, "Förfallodatum: %s\n", due ? due : ""); - if (delivery && *delivery) - buf_line(&t, "Leveransdatum: %s\n", delivery); - if (your_ref && *your_ref) - buf_line(&t, "Er referens: %s\n", your_ref); - if (our_ref && *our_ref) - buf_line(&t, "Vår referens: %s\n", our_ref); - if (ocr && *ocr) - buf_line(&t, "OCR: %s\n", ocr); - if (sent_at && *sent_at) - buf_line(&t, "Skickad: %s%s%s\n", sent_at, - sent_to && *sent_to ? " till " : "", - sent_to && *sent_to ? sent_to : ""); - if (notes && *notes) - buf_line(&t, "\nFritext:\n%s\n", notes); - buf_line(&t, "\n"); - buf_line(&t, - MK_STICKY MK_HEAD - "%-34s %8s %-6s %12s %-13s %-18s %12s\n", - "Beskrivning", "Antal", "Enhet", "À-pris", "Moms", "Anm", - "Belopp"); - size_t nrows = jarr_size(resp, "result.rows"); - for (size_t i = 0; i < nrows; i++) { - char path[64]; - snprintf(path, sizeof path, "result.rows.%zu.description", i); - char *desc = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.rows.%zu.quantity_milli", i); - int64_t qm = jint_val(resp, path, 0); - snprintf(path, sizeof path, "result.rows.%zu.unit", i); - char *unit = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.rows.%zu.unit_price_ore", i); - int64_t price = jint_val(resp, path, 0); - snprintf(path, sizeof path, "result.rows.%zu.vat_code", i); - char *vat_code = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.rows.%zu.note", i); - char *note = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.rows.%zu.amount_ore", i); - int64_t amount = jint_val(resp, path, 0); - char dbuf[256], qbuf[24], qcol[24], ubuf[24], pcol[40], - vbuf[24], nbuf[128], acol[40]; - snprintf(dbuf, sizeof dbuf, "%s", desc ? desc : ""); - tui_pad_field(dbuf, sizeof dbuf, 34); - qty_sv(qm, qbuf, sizeof qbuf); - tui_pad_hdr(qcol, sizeof qcol, 8, qbuf); - snprintf(ubuf, sizeof ubuf, "%s", unit ? unit : ""); - tui_pad_field(ubuf, sizeof ubuf, 6); - tui_amt_col(pcol, sizeof pcol, 12, price); - snprintf(vbuf, sizeof vbuf, "%s", vat_label(vat_code)); - tui_pad_field(vbuf, sizeof vbuf, 13); - snprintf(nbuf, sizeof nbuf, "%s", note ? note : ""); - tui_pad_field(nbuf, sizeof nbuf, 18); - tui_amt_col(acol, sizeof acol, 12, amount); - buf_line(&t, "%s %s %s %s %s %s %s\n", dbuf, qcol, ubuf, pcol, - vbuf, nbuf, acol); - free(desc); - free(unit); - free(vat_code); - free(note); - } - char netc[40], vatc[40], totc[40]; - tui_amt_col(netc, sizeof netc, 14, net); - tui_amt_col(vatc, sizeof vatc, 14, vat); - tui_amt_col(totc, sizeof totc, 14, total); - buf_line(&t, "\n%46s %14s\n", "Netto", netc); - buf_line(&t, "%46s %14s\n", "Moms", vatc); - buf_line(&t, MK_HEAD "%46s %14s\n", "Att betala", totc); - buf_append(&t, "\0", 1); - struct invctx ctx; - memset(&ctx, 0, sizeof ctx); - ctx.a = a; - ctx.id = id; - ctx.number = number; - ctx.customer_id = customer_id; - snprintf(ctx.customer, sizeof ctx.customer, "%s", cust ? cust : ""); - int ret = tui_pager_hook("Faktura", (const char *)t.p, - "p = visa PDF s = skicka", 0, inv_key, - &ctx); - free(cust); - free(status); - free(date); - free(due); - free(delivery); - free(your_ref); - free(our_ref); - free(notes); - free(ocr); - free(sent_at); - free(sent_to); - buf_free(&t); - free(resp); - if (ret != 1) - return; - } -} - -#define IFORM_ROWS 64 - -struct irow { - char description[128]; - char quantity[16]; - char unit[8]; - char price[24]; - char vat[16]; - char note[64]; -}; - -struct iform { - struct app *a; - struct tui_rt rt; - struct irow rows[IFORM_ROWS]; - char date[16]; - char due[16]; - char delivery[16]; - char your_ref[64]; - char our_ref[64]; - char notes[256]; - char auto_due[16]; - char auto_ref[64]; - size_t ncust; - int64_t *cust_ids; - int *cust_days; - char **cust_names; - char **cust_emails; - char **cust_refs; - int cust_sel; - int issued; -}; - -static void iform_customers_free(struct iform *f) -{ - for (size_t i = 0; i < f->ncust; i++) { - free(f->cust_names[i]); - free(f->cust_emails[i]); - free(f->cust_refs[i]); - } - free(f->cust_names); - free(f->cust_emails); - free(f->cust_refs); - free(f->cust_ids); - free(f->cust_days); - f->cust_names = NULL; - f->cust_emails = NULL; - f->cust_refs = NULL; - f->cust_ids = NULL; - f->cust_days = NULL; - f->ncust = 0; -} - -static int iform_load_customers(struct iform *f) -{ - char *resp = client_rpc(&f->a->conn, "customer.list", f->a->session, - f->a->org, "{\"active_only\":true}"); - if (!resp || !client_ok(resp)) { - show_error("Kunder", resp); - free(resp); - return -1; - } - size_t n = jarr_size(resp, "result.items"); - f->cust_ids = xcalloc(n ? n : 1, sizeof(int64_t)); - f->cust_days = xcalloc(n ? n : 1, sizeof(int)); - f->cust_names = xcalloc(n ? n : 1, sizeof(char *)); - f->cust_emails = xcalloc(n ? n : 1, sizeof(char *)); - f->cust_refs = xcalloc(n ? n : 1, sizeof(char *)); - for (size_t i = 0; i < n; i++) { - char path[64]; - snprintf(path, sizeof path, "result.items.%zu.id", i); - f->cust_ids[i] = jint_val(resp, path, 0); - snprintf(path, sizeof path, "result.items.%zu.payment_days", i); - f->cust_days[i] = (int)jint_val(resp, path, 30); - snprintf(path, sizeof path, "result.items.%zu.name", i); - char *nm = jstr_dup(resp, path); - f->cust_names[i] = nm ? nm : xstrdup(""); - snprintf(path, sizeof path, "result.items.%zu.email", i); - char *em = jstr_dup(resp, path); - f->cust_emails[i] = em ? em : xstrdup(""); - snprintf(path, sizeof path, "result.items.%zu.your_ref", i); - char *rf = jstr_dup(resp, path); - f->cust_refs[i] = rf ? rf : xstrdup(""); - } - f->ncust = n; - free(resp); - return 0; -} - -/* Re-apply the customer-derived defaults only while the user has not - changed them (they still equal what the previous customer produced). */ -static void iform_customer_changed(struct iform *f) -{ - if (f->ncust == 0 || f->cust_sel < 0 || (size_t)f->cust_sel >= f->ncust) - return; - char today[16], newdue[16]; - today_iso(today, sizeof today); - util_date_add_days(today, f->cust_days[f->cust_sel], newdue, - sizeof newdue); - if (!f->due[0] || strcmp(f->due, f->auto_due) == 0) - snprintf(f->due, sizeof f->due, "%s", newdue); - snprintf(f->auto_due, sizeof f->auto_due, "%s", newdue); - const char *ref = f->cust_refs[f->cust_sel]; - if (!f->your_ref[0] || strcmp(f->your_ref, f->auto_ref) == 0) - snprintf(f->your_ref, sizeof f->your_ref, "%s", ref ? ref : ""); - snprintf(f->auto_ref, sizeof f->auto_ref, "%s", ref ? ref : ""); -} - -static void iform_cell(void *ud, int row, int col, char **buf, size_t *cap) -{ - struct iform *f = ud; - struct irow *r = &f->rows[row]; - if (col == 0) { - *buf = r->description; - *cap = sizeof r->description; - } else if (col == 1) { - *buf = r->quantity; - *cap = sizeof r->quantity; - } else if (col == 2) { - *buf = r->unit; - *cap = sizeof r->unit; - } else if (col == 3) { - *buf = r->price; - *cap = sizeof r->price; - } else if (col == 4) { - *buf = r->vat; - *cap = sizeof r->vat; - } else { - *buf = r->note; - *cap = sizeof r->note; - } -} - -static const char *const UNIT_CHOICES[] = { "st", "tim", "dag", "km" }; -static const char *const VAT_LABELS[] = { "25%", "12%", "6%", - "0%", "Omvänd moms", "EU" }; -static const char *const VAT_CODES[] = { "25", "12", "6", "0", "rc", "eu" }; - -static int vat_index(const char *s) -{ - const char *code = vat_code_of(s); - if (!code) - return 0; - for (int i = 0; i < 6; i++) - if (strcmp(code, VAT_CODES[i]) == 0) - return i; - return 0; -} - -static int iform_key(void *ud, int ch) -{ - struct iform *f = ud; - struct tui_rt *rt = &f->rt; - if (ch != '\n' && ch != '\r' && ch != KEY_ENTER) - return TUI_HOOK_NONE; - if (rt->focus == 0) { - if (f->ncust == 0) { - tui_message("Ny faktura", "Inga kunder. Skapa en under Register " - "→ Kunder först."); - return TUI_HOOK_STAY; - } - int c = tui_choice_prompt("Kund: ", - (const char *const *)f->cust_names, - (int)f->ncust, f->cust_sel); - if (c >= 0) { - int changed = c != f->cust_sel; - f->cust_sel = c; - if (changed) - iform_customer_changed(f); - tui_rt_normalize(rt); - rt->focus = tui_rt_focus_step(0, rt->nfields, &rt->row, &rt->col, - rt->nrows, rt->neditable, 1); - } - return TUI_HOOK_STAY; - } - if (rt->focus < 0 && rt->row >= 0 && rt->row < IFORM_ROWS) { - struct irow *r = &f->rows[rt->row]; - if (rt->col == 2) { - int c = tui_choice_prompt("Enhet: ", UNIT_CHOICES, 4, - unit_index(r->unit)); - if (c >= 0) - snprintf(r->unit, sizeof r->unit, "%s", UNIT_CHOICES[c]); - return TUI_HOOK_STAY; - } - if (rt->col == 4) { - int c = tui_choice_prompt("Moms: ", VAT_LABELS, 6, - vat_index(r->vat)); - if (c >= 0) - snprintf(r->vat, sizeof r->vat, "%s", VAT_LABELS[c]); - return TUI_HOOK_STAY; - } - } - return TUI_HOOK_NONE; -} - -static char *iform_draft_args(struct iform *f, char *err, size_t errn) -{ - if (f->ncust == 0 || f->cust_sel < 0 || (size_t)f->cust_sel >= f->ncust) { - snprintf(err, errn, "Välj en kund."); - return NULL; - } - if (!util_parse_iso_date(f->date)) { - snprintf(err, errn, "Ogiltigt fakturadatum (ÅÅÅÅ-MM-DD)."); - return NULL; - } - if (!util_parse_iso_date(f->due)) { - snprintf(err, errn, "Ogiltigt förfallodatum (ÅÅÅÅ-MM-DD)."); - return NULL; - } - if (f->delivery[0] && !util_parse_iso_date(f->delivery)) { - snprintf(err, errn, "Ogiltigt leveransdatum (ÅÅÅÅ-MM-DD)."); - return NULL; - } - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - yyjson_mut_obj_add_int(d, o, "customer_id", f->cust_ids[f->cust_sel]); - yyjson_mut_obj_add_strcpy(d, o, "invoice_date", f->date); - yyjson_mut_obj_add_strcpy(d, o, "due_date", f->due); - yyjson_mut_obj_add_strcpy(d, o, "delivery_date", f->delivery); - yyjson_mut_obj_add_strcpy(d, o, "your_ref", f->your_ref); - yyjson_mut_obj_add_strcpy(d, o, "our_ref", f->our_ref); - yyjson_mut_obj_add_strcpy(d, o, "notes", f->notes); - yyjson_mut_val *arr = yyjson_mut_arr(d); - int used = 0; - for (int i = 0; i < f->rt.nrows; i++) { - struct irow *r = &f->rows[i]; - if (!r->description[0] && !r->quantity[0] && !r->unit[0] && - !r->price[0] && !r->vat[0] && !r->note[0]) - continue; - if (!r->description[0]) { - snprintf(err, errn, "Rad %d: beskrivning saknas.", i + 1); - yyjson_mut_doc_free(d); - return NULL; - } - const char *qty = r->quantity[0] ? r->quantity : "1"; - if (!qty_is_valid(qty)) { - snprintf(err, errn, - "Rad %d: antal måste vara ett positivt tal med högst " - "3 decimaler.", - i + 1); - yyjson_mut_doc_free(d); - return NULL; - } - int64_t price = 0; - if (tui_parse_kr(r->price, &price) != 0) { - snprintf(err, errn, "Rad %d: ogiltigt à-pris.", i + 1); - yyjson_mut_doc_free(d); - return NULL; - } - const char *unit = unit_code_of(r->unit); - if (!unit) { - snprintf(err, errn, "Rad %d: ogiltig enhet (st, tim, dag, km).", - i + 1); - yyjson_mut_doc_free(d); - return NULL; - } - const char *vat = vat_code_of(r->vat); - if (!vat) { - snprintf(err, errn, "Rad %d: ogiltig momskod.", i + 1); - yyjson_mut_doc_free(d); - return NULL; - } - yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); - yyjson_mut_obj_add_strcpy(d, ro, "description", r->description); - yyjson_mut_obj_add_strcpy(d, ro, "quantity", qty); - yyjson_mut_obj_add_strcpy(d, ro, "unit", unit); - yyjson_mut_obj_add_int(d, ro, "unit_price_ore", price); - yyjson_mut_obj_add_strcpy(d, ro, "note", r->note); - yyjson_mut_obj_add_strcpy(d, ro, "vat_code", vat); - used++; - } - if (used == 0) { - snprintf(err, errn, "Minst en rad krävs."); - yyjson_mut_doc_free(d); - return NULL; - } - yyjson_mut_obj_add_val(d, o, "rows", arr); - char *out = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - return out; -} - -static void iform_preview(struct iform *f) -{ - char err[256]; - char *args = iform_draft_args(f, err, sizeof err); - if (!args) { - tui_message("Faktura", "%s", err); - return; - } - char *resp = client_rpc(&f->a->conn, "invoice.preview", f->a->session, - f->a->org, args); - free(args); - if (!resp || !client_ok(resp)) { - show_error("Faktura", resp); - free(resp); - return; - } - char *b64 = jstr_dup(resp, "result.content_base64"); - if (b64) { - save_and_open_pdf(b64, "Faktura förhandsvisning.pdf"); - free(b64); - } - free(resp); -} - -static int64_t iform_issue(struct iform *f) -{ - if (f->issued) - return 0; - char err[256]; - char *args = iform_draft_args(f, err, sizeof err); - if (!args) { - tui_message("Faktura", "%s", err); - return 0; - } - char *resp = client_rpc(&f->a->conn, "invoice.issue", f->a->session, - f->a->org, args); - free(args); - if (!resp || !client_ok(resp)) { - show_error("Faktura", resp); - free(resp); - return 0; - } - int64_t id = jint_val(resp, "result.id", 0); - int64_t number = jint_val(resp, "result.number", 0); - free(resp); - if (id <= 0) - return 0; - f->issued = 1; - const char *email = f->cust_emails[f->cust_sel]; - if (email && *email) { - if (tui_confirm("Skicka faktura", "Skicka faktura %lld till %s?", - (long long)number, email)) { - char args2[64]; - snprintf(args2, sizeof args2, "{\"id\":%lld}", (long long)id); - char *r = client_rpc(&f->a->conn, "invoice.send", f->a->session, - f->a->org, args2); - if (r && client_ok(r)) { - char *to = jstr_dup(r, "result.sent_to"); - tui_message("Skickat", "Faktura %lld skickad till %s.", - (long long)number, to && *to ? to : email); - free(to); - } else { - show_error("Skicka faktura", r); - } - free(r); - } - } else { - tui_message("Faktura", "Faktura %lld skapad.", (long long)number); - } - return id; -} - -static int64_t invoices_new(struct app *a) -{ - struct iform f; - memset(&f, 0, sizeof f); - f.a = a; - today_iso(f.date, sizeof f.date); - snprintf(f.delivery, sizeof f.delivery, "%s", f.date); - snprintf(f.due, sizeof f.due, "%s", f.date); - snprintf(f.auto_due, sizeof f.auto_due, "%s", f.due); - if (iform_load_customers(&f) != 0) { - iform_customers_free(&f); - return 0; - } - char *resp = - client_rpc(&a->conn, "settings.get", a->session, a->org, "{}"); - if (resp && client_ok(resp)) { - char *v = jstr_dup(resp, "result.invoice_our_ref"); - if (v) { - snprintf(f.our_ref, sizeof f.our_ref, "%s", v); - free(v); - } - } - free(resp); - if (f.ncust > 0) { - f.cust_sel = 0; - iform_customer_changed(&f); - } - struct tui_rt_col cols[6] = { - { "Beskrivning", 2, 34, TUI_RT_EDIT }, - { "Antal", 37, 8, TUI_RT_EDIT }, - { "Enhet", 46, 6, TUI_RT_EDIT }, - { "À-pris", 53, 14, TUI_RT_EDIT }, - { "Moms", 68, 14, TUI_RT_EDIT }, - { "Anm", 83, 32, TUI_RT_EDIT }, - }; - tui_rt_init(&f.rt, 1, IFORM_ROWS, 6, cols, 0, iform_cell, NULL, &f); - tui_rt_set_key(&f.rt, iform_key); - struct tui_form_field ff[7]; - memset(ff, 0, sizeof ff); - ff[0].label = "Kund"; - ff[0].kind = TUI_F_CHOICE; - ff[0].choices = (const char *const *)f.cust_names; - ff[0].nchoices = (int)f.ncust; - ff[0].choice = &f.cust_sel; - ff[1].label = "Fakturadatum"; - ff[1].value = f.date; - ff[1].cap = sizeof f.date; - ff[1].kind = TUI_F_DATE; - ff[2].label = "Förfallodatum"; - ff[2].value = f.due; - ff[2].cap = sizeof f.due; - ff[2].kind = TUI_F_DATE; - ff[3].label = "Leveransdatum"; - ff[3].value = f.delivery; - ff[3].cap = sizeof f.delivery; - ff[3].kind = TUI_F_DATE; - ff[4].label = "Er referens"; - ff[4].value = f.your_ref; - ff[4].cap = sizeof f.your_ref; - ff[4].kind = TUI_F_TEXT; - ff[5].label = "Vår referens"; - ff[5].value = f.our_ref; - ff[5].cap = sizeof f.our_ref; - ff[5].kind = TUI_F_TEXT; - ff[6].label = "Fritext"; - ff[6].value = f.notes; - ff[6].cap = sizeof f.notes; - ff[6].kind = TUI_F_TEXT; - tui_rt_set_fields(&f.rt, ff, 7); - const char *hint = - "Enter = välj/ändra Tab = byta fält F5 = förhandsvisa" - " ^Enter = utfärda Esc = avbryt"; - int64_t out = 0; - for (;;) { - int rr = tui_rt_run("Ny faktura", &f.rt, hint); - if (rr == -1) - break; - if (rr == -2) { - iform_preview(&f); - continue; - } - if (rr == -3) { - out = iform_issue(&f); - if (out > 0) - break; - } - } - iform_customers_free(&f); - return out; -} - -static int invoices_key(void *ud, int ch) -{ - struct app *a = ud; - if (ch != 'n') - return TUI_HOOK_NONE; - char *resp = client_rpc(&a->conn, "invoice.sequence_get", a->session, - a->org, "{}"); - char def[16] = "1"; - if (resp && client_ok(resp)) - snprintf(def, sizeof def, "%lld", - (long long)jint_val(resp, "result.next_number", 1)); - free(resp); - char buf[16] = ""; - if (!tui_prompt_into(buf, sizeof buf, "Nästa fakturanummer", def, 0)) - return TUI_HOOK_STAY; - char args[64]; - snprintf(args, sizeof args, "{\"next_number\":%s}", buf); - resp = client_rpc(&a->conn, "invoice.sequence_set", a->session, a->org, - args); - if (resp && client_ok(resp)) - tui_message("Fakturor", "Nästa fakturanummer: %s.", buf); - else - show_error("Fakturor", resp); - free(resp); - return TUI_HOOK_REFRESH; -} - -static void invoices_screen(struct app *a) -{ - char **items = NULL; - int64_t *ids = NULL; - size_t n = 0; - int fetch = 1; - for (;;) { - if (g_quit) { - for (size_t i = 0; i < n; i++) - free(items[i]); - free(items); - free(ids); - return; - } - if (fetch) { - for (size_t i = 0; i < n; i++) - free(items[i]); - free(items); - free(ids); - items = NULL; - ids = NULL; - n = 0; - fetch = 0; - char *resp = client_rpc(&a->conn, "invoice.list", a->session, - a->org, "{\"limit\":200}"); - if (!resp || !client_ok(resp)) { - show_error("Fakturor", resp); - free(resp); - return; - } - n = jarr_size(resp, "result.items"); - items = xcalloc(n + 1, sizeof(char *)); - ids = xcalloc(n ? n : 1, sizeof(int64_t)); - for (size_t i = 0; i < n; i++) { - char path[64], line[768]; - snprintf(path, sizeof path, "result.items.%zu.id", i); - ids[i] = jint_val(resp, path, 0); - snprintf(path, sizeof path, "result.items.%zu.number", i); - int64_t number = jint_val(resp, path, 0); - snprintf(path, sizeof path, "result.items.%zu.invoice_date", i); - char *date = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.customer_name", - i); - char *cust = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.total_ore", i); - int64_t total = jint_val(resp, path, 0); - snprintf(path, sizeof path, "result.items.%zu.status", i); - char *status = jstr_dup(resp, path); - char nbuf[32], cbuf[256], abuf[40]; - snprintf(nbuf, sizeof nbuf, "Faktura %lld", (long long)number); - snprintf(cbuf, sizeof cbuf, "%s", cust ? cust : ""); - tui_pad_field(cbuf, sizeof cbuf, 28); - tui_amt_col(abuf, sizeof abuf, 14, total); - snprintf(line, sizeof line, "%s %s %s %s %s", nbuf, - date ? date : "", cbuf, abuf, - invoice_status_sv(status)); - items[i] = xstrdup(line); - free(date); - free(cust); - free(status); - } - free(resp); - items[n] = xstrdup("+ Ny faktura (^N)"); - } - int start = 0; - if (a->invoice_sel) { - for (size_t i = 0; i < n; i++) - if (ids[i] == a->invoice_sel) { - start = (int)i; - break; - } - } - int cur = start; - int sel = tui_select_list_hook("Fakturor", items, (int)n + 1, start, 1, - &cur, 1, NULL, 0, invoices_key, a); - if (cur >= 0 && (size_t)cur < n) - a->invoice_sel = ids[cur]; - if (sel == -1) { - for (size_t i = 0; i < n; i++) - free(items[i]); - free(items); - free(ids); - return; - } - if (sel == -2) { - fetch = 1; - continue; - } - if (sel == -4 || (sel >= 0 && (size_t)sel == n)) { - int64_t nid = invoices_new(a); - if (nid > 0) - a->invoice_sel = nid; - fetch = 1; - continue; - } - if (sel >= 0 && (size_t)sel < n) { - invoices_detail(a, ids[sel]); - continue; - } - } -} - -static void templates_screen(struct app *a) -{ - int cursor = 0; - for (;;) { - if (g_quit) - return; - char *resp = client_rpc(&a->conn, "template.list", a->session, a->org, - "{\"active_only\":true}"); - if (!resp || !client_ok(resp)) { - show_error("Mallar", resp); - free(resp); - return; - } - size_t n = jarr_size(resp, "result.items"); - char **names = xcalloc(n + 1, sizeof(char *)); - char **lines = xcalloc(n + 1, sizeof(char *)); - for (size_t i = 0; i < n; i++) { - char path[64], line[512]; - snprintf(path, sizeof path, "result.items.%zu.name", i); - char *nm = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.series", i); - char *ser = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.row_count", i); - int64_t rc = jint_val(resp, path, 0); - snprintf(path, sizeof path, "result.items.%zu.description", i); - char *ds = jstr_dup(resp, 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); - } - names[n] = xstrdup(""); - lines[n] = xstrdup("+ Ny mall (^N)"); - int s = tui_select_list("Mallar", lines, (int)n + 1, cursor, 1, &cursor, 1, - n > 0 ? "arkivera" : NULL, 0); - if (s == -1) { - for (size_t i = 0; i <= n; i++) { - free(names[i]); - free(lines[i]); - } - free(names); - free(lines); - free(resp); - return; - } - if (s == -6) { - if (cursor >= 0 && (size_t)cursor < n) - template_archive(a, names[cursor]); - } else if (s == -4 || (s >= 0 && (size_t)s == n)) { - template_form(a, NULL); - } else if (s >= 0) { - template_form(a, names[s]); - } - for (size_t i = 0; i <= n; i++) { - free(names[i]); - free(lines[i]); - } - free(names); - free(lines); - free(resp); - if (s == -5) - return; - } -} - -/* ------------------------------------------------------------------ */ -/* momsregler (the vat report's report_rules) */ -/* ------------------------------------------------------------------ */ - -static const char *const RULE_TYPES[] = { "account", "range", "type" }; -#define RULE_NTYPES ((int)(sizeof RULE_TYPES / sizeof RULE_TYPES[0])) -static const char *const RULE_SIGNS[] = { "+", "-" }; -#define RULE_NSIGNS ((int)(sizeof RULE_SIGNS / sizeof RULE_SIGNS[0])) - -struct rule { - int64_t id; - char box[16]; - char match_type[16]; - char pattern[128]; - int sign; - int64_t sort_order; -}; - -/* F5 dry-runs; Ctrl+Enter creates/updates. Returns 1 when saved. */ -static int rule_save(struct app *a, int64_t id, const char *box, int mti, - const char *pattern, int signi, const char *sort, int dry) -{ - char *end = NULL; - long long sort_order = strtoll(sort, &end, 10); - if (!sort[0] || !end || *end) { - tui_message("Momsregel", "Sortering måste vara ett heltal."); - return 0; - } - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - if (id) - yyjson_mut_obj_add_int(d, o, "id", id); - yyjson_mut_obj_add_strcpy(d, o, "report", "vat"); - yyjson_mut_obj_add_strcpy(d, o, "box", box); - yyjson_mut_obj_add_strcpy(d, o, "match_type", RULE_TYPES[mti]); - yyjson_mut_obj_add_strcpy(d, o, "pattern", pattern); - yyjson_mut_obj_add_int(d, o, "sign", signi == 1 ? -1 : 1); - yyjson_mut_obj_add_int(d, o, "sort_order", (int64_t)sort_order); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - const char *cmd = id ? "report.rule_update" : "report.rule_create"; - char *resp = dry ? rpc_dry(a, cmd, args) - : client_rpc(&a->conn, cmd, a->session, a->org, args); - free(args); - if (resp && client_ok(resp)) { - if (dry) { - tui_message("Validering OK", "Regeln är korrekt."); - } else { - tui_message("Momsregler", "Regeln sparad."); - free(resp); - return 1; - } - } else { - show_error("Momsregler", resp); - } - free(resp); - return 0; -} - -static int rule_form(struct app *a, const struct rule *r) -{ - static int sel = 0; - char box[16] = "", pattern[128] = "", sort[16] = "10"; - int mti = 0, signi = 0; - if (r) { - snprintf(box, sizeof box, "%s", r->box); - for (int i = 0; i < RULE_NTYPES; i++) - if (strcmp(RULE_TYPES[i], r->match_type) == 0) - mti = i; - snprintf(pattern, sizeof pattern, "%s", r->pattern); - signi = r->sign < 0 ? 1 : 0; - snprintf(sort, sizeof sort, "%lld", (long long)r->sort_order); - } - struct tui_form_field ff[5]; - memset(ff, 0, sizeof ff); - ff[0].label = "Ruta"; - ff[0].value = box; - ff[0].cap = sizeof box; - ff[0].kind = TUI_F_TEXT; - ff[1].label = "Typ"; - ff[1].kind = TUI_F_CHOICE; - ff[1].choices = RULE_TYPES; - ff[1].nchoices = RULE_NTYPES; - ff[1].choice = &mti; - ff[2].label = "Mönster"; - ff[2].value = pattern; - ff[2].cap = sizeof pattern; - ff[2].kind = TUI_F_TEXT; - ff[3].label = "Tecken"; - ff[3].kind = TUI_F_CHOICE; - ff[3].choices = RULE_SIGNS; - ff[3].nchoices = RULE_NSIGNS; - ff[3].choice = &signi; - ff[4].label = "Sortering"; - ff[4].value = sort; - ff[4].cap = sizeof sort; - ff[4].kind = TUI_F_TEXT; - const char *title = r ? "Redigera momsregel" : "Ny momsregel"; - for (;;) { - int rr = tui_form_run(title, ff, 5, 1, &sel); - if (rr == TUI_FORM_BACK) - return 0; - if (rr == TUI_FORM_REFRESH) { - rule_save(a, r ? r->id : 0, box, mti, pattern, signi, sort, 1); - continue; - } - if (rr == TUI_FORM_SUBMIT) { - if (rule_save(a, r ? r->id : 0, box, mti, pattern, signi, sort, 0)) - return 1; - continue; - } - } -} - -static void rule_delete(struct app *a, const struct rule *r) -{ - if (!tui_confirm("Momsregler", "Ta bort regeln %s %s %s?", r->box, - r->match_type, r->pattern)) - return; - char args[64]; - snprintf(args, sizeof args, "{\"id\":%lld}", (long long)r->id); - char *resp = - client_rpc(&a->conn, "report.rule_delete", a->session, a->org, args); - if (resp && client_ok(resp)) - tui_message("Momsregler", "Regeln borttagen."); - else - show_error("Momsregler", resp); - free(resp); -} - -static void rules_screen(struct app *a) -{ - int cursor = 0; - for (;;) { - if (g_quit) - return; - char *resp = client_rpc(&a->conn, "report.rule_list", a->session, - a->org, "{\"report\":\"vat\"}"); - if (!resp || !client_ok(resp)) { - show_error("Momsregler", resp); - free(resp); - return; - } - size_t n = jarr_size(resp, "result.items"); - struct rule *rules = xcalloc(n ? n : 1, sizeof *rules); - char **items = xcalloc(n + 1, sizeof(char *)); - for (size_t i = 0; i < n; i++) { - char path[64], line[512], boxbuf[24], patbuf[96]; - snprintf(path, sizeof path, "result.items.%zu.id", i); - rules[i].id = jint_val(resp, path, 0); - snprintf(path, sizeof path, "result.items.%zu.box", i); - char *sbox = jstr_dup(resp, path); - snprintf(rules[i].box, sizeof rules[i].box, "%s", - sbox ? sbox : ""); - free(sbox); - snprintf(path, sizeof path, "result.items.%zu.match_type", i); - char *mt = jstr_dup(resp, path); - snprintf(rules[i].match_type, sizeof rules[i].match_type, "%s", - mt ? mt : ""); - free(mt); - snprintf(path, sizeof path, "result.items.%zu.pattern", i); - char *pat = jstr_dup(resp, path); - snprintf(rules[i].pattern, sizeof rules[i].pattern, "%s", - pat ? pat : ""); - free(pat); - snprintf(path, sizeof path, "result.items.%zu.sign", i); - rules[i].sign = (int)jint_val(resp, path, 1); - snprintf(path, sizeof path, "result.items.%zu.sort_order", i); - rules[i].sort_order = jint_val(resp, path, 0); - - snprintf(boxbuf, sizeof boxbuf, "%s", rules[i].box); - tui_pad_field(boxbuf, sizeof boxbuf, 6); - snprintf(patbuf, sizeof patbuf, "%s", rules[i].pattern); - tui_pad_field(patbuf, sizeof patbuf, 34); - snprintf(line, sizeof line, "%s %-8s %s %-2s %6lld", boxbuf, - rules[i].match_type, patbuf, - rules[i].sign < 0 ? "-" : "+", - (long long)rules[i].sort_order); - items[i] = xstrdup(line); - } - items[n] = xstrdup("+ Ny regel (^N)"); - int s = tui_select_list("Momsregler", items, (int)n + 1, cursor, 1, - &cursor, 1, n > 0 ? "ta bort" : NULL, 0); - for (size_t i = 0; i <= n; i++) - free(items[i]); - free(items); - free(resp); - if (s == -1) { - free(rules); - return; - } - if (s == -6) { - if (cursor >= 0 && (size_t)cursor < n) - rule_delete(a, &rules[cursor]); - } else if (s == -4 || (s >= 0 && (size_t)s == n)) { - rule_form(a, NULL); - } else if (s >= 0 && (size_t)s < n) { - rule_form(a, &rules[s]); - } - free(rules); - } -} - -/* ------------------------------------------------------------------ */ -/* bokslut */ -/* ------------------------------------------------------------------ */ - -/* client_rpc() cannot carry the protocol's top-level dry_run, so build - the request by hand. */ -static char *rpc_dry(struct app *a, const char *cmd, const char *args) -{ - size_t n = strlen(args) + strlen(cmd) + strlen(a->session) + 160; - char *raw = xmalloc(n); - snprintf(raw, n, - "{\"v\":1,\"id\":\"tui\",\"cmd\":\"%s\",\"session\":\"%s\"," - "\"org\":%lld,\"dry_run\":true,\"args\":%s}", - cmd, a->session, (long long)a->org, args); - char *r = NULL; - if (client_send_line(&a->conn, raw) == 0) - r = client_read_line(&a->conn); - free(raw); - return r; -} - -static void fmt_bokslut_plan(struct buf *t, yyjson_val *res) -{ - yyjson_val *fy = yyjson_obj_get(res, "fiscal_year"); - if (fy) - buf_line(t, MK_HEAD "Bokslutsplan %s %s - %s\n", vstr(fy, "label"), - vstr(fy, "start_date"), vstr(fy, "end_date")); - yyjson_val *vs = yyjson_obj_get(res, "vouchers"); - size_t n = yyjson_arr_size(vs); - if (n == 0) { - buf_line(t, "\nInget att bokföra.\n"); - return; - } - char pser[16] = ""; - long long pnum = 0; - for (size_t i = 0; i < n; i++) { - yyjson_val *v = yyjson_arr_get(vs, i); - yyjson_val *pv = yyjson_obj_get(v, "voucher"); - const char *ser = vstr(pv, "series"); - long long num = (long long)vint(pv, "number"); - if (strcmp(ser, pser) != 0) { - snprintf(pser, sizeof pser, "%s", ser); - pnum = num; - } else if (num <= pnum) { - num = pnum + 1; - } - pnum = num; - buf_line(t, "\n" MK_HEAD "%s%lld %s\n", pser, num, - vstr(v, "description")); - yyjson_val *rows = yyjson_obj_get(v, "rows"); - size_t nr = yyjson_arr_size(rows); - for (size_t k = 0; k < nr; k++) { - yyjson_val *r = yyjson_arr_get(rows, k); - char a1[48], a2[48]; - tui_amt_col(a1, sizeof a1, 15, vint(r, "debit_ore")); - tui_amt_col(a2, sizeof a2, 15, vint(r, "credit_ore")); - buf_line(t, " %-5s %s %s\n", vstr(r, "account"), a1, a2); - } - } -} - -static void bokslut_plan(struct app *a, const char *fond, const char *rate, - int do_post) -{ - int64_t fond_ore = 0, rate_ore = 0; - if (tui_parse_kr(fond, &fond_ore) != 0) { - tui_message("Bokslut", "Ogiltigt fondbelopp: %s", fond); - return; - } - if (tui_parse_kr(rate, &rate_ore) != 0 || rate_ore < 0 || rate_ore > 10000) { - tui_message("Bokslut", "Ogiltig skattesats: %s", rate); - return; - } - char args[160]; - snprintf(args, sizeof args, - "{\"fiscal_year\":%lld,\"periodiseringsfond_ore\":%lld," - "\"tax_rate\":%.2f}", - (long long)a->fy, (long long)fond_ore, - (double)rate_ore / 100.0); - char *resp = rpc_dry(a, "bokslut.post", args); - if (!resp || !client_ok(resp)) { - show_error("Bokslut", resp); - free(resp); - return; - } - yyjson_doc *d = parse(resp); - yyjson_val *res = d ? jget(yyjson_doc_get_root(d), "result") : NULL; - struct buf t; - buf_init(&t); - if (res) - fmt_bokslut_plan(&t, res); - else - buf_line(&t, "%s", resp); - buf_append(&t, "", 1); - tui_pager("Bokslut - plan (torrkörning)", (const char *)t.p, NULL); - buf_free(&t); - yyjson_doc_free(d); - free(resp); - if (!do_post) - return; - char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, "Bokför planen? (j/n): ", "n", 0) ? ansbuf : NULL; - if (!ans || !(ans[0] == 'j' || ans[0] == 'J')) - return; - char *presp = client_rpc(&a->conn, "bokslut.post", a->session, a->org, - args); - if (!presp || !client_ok(presp)) { - show_error("Bokslut", presp); - free(presp); - return; - } - yyjson_doc *pd = parse(presp); - yyjson_val *pr = pd ? jget(yyjson_doc_get_root(pd), "result") : NULL; - struct buf m; - buf_init(&m); - buf_line(&m, "Bokslutet är bokfört:"); - yyjson_val *vs = pr ? yyjson_obj_get(pr, "vouchers") : NULL; - size_t n = yyjson_arr_size(vs); - for (size_t i = 0; i < n; i++) { - yyjson_val *v = yyjson_arr_get(vs, i); - yyjson_val *pv = yyjson_obj_get(v, "voucher"); - buf_line(&m, "\n %s%lld %s", vstr(pv, "series"), - (long long)vint(pv, "number"), vstr(v, "description")); - } - tui_message("Bokslut", "%s", (const char *)m.p); - buf_free(&m); - yyjson_doc_free(pd); - free(presp); -} - -/* Bokslut: the year-end hub. The year's årsredovisning details (fields 0-5, - saved one by one with fiscal_year.update) and the local periodiseringsfond - and tax rate inputs plus a status row and the year-end actions, all in one - view. New years inherit the stable fields. */ -static void bokslut_screen(struct app *a) -{ - static int sel = 0; - static const char *const labels[6] = { - "Väsentliga händelser", - "Årsstämma (YYYY-MM-DD)", - "Föreslagen utdelning (kr)", - "Utdelningens datum (YYYY-MM-DD)", - "Medelantal anställda", - "Övriga noter (t.ex. revisor)", - }; - static const char *const keys[6] = { "events", "agm_date", "dividend_ore", - "dividend_date", "employees", - "notes" }; - const int nf = 6; - char fond[64] = "0,00", rate[16] = "20,6"; - int can_edit = strcmp(a->role, "owner") == 0 || - strcmp(a->role, "bookkeeper") == 0; - for (;;) { - if (g_quit) - return; - char args[48]; - snprintf(args, sizeof args, "{\"id\":%lld}", (long long)a->fy); - char *resp = client_rpc(&a->conn, "fiscal_year.get", a->session, - a->org, args); - if (!resp || !client_ok(resp)) { - show_error("Bokslut", resp); - free(resp); - return; - } - char *fystat = jstr_dup(resp, "result.status"); - int closed = fystat && strcmp(fystat, "closed") == 0; - free(fystat); - char *vals[6]; - struct tui_form_field ff[8]; - int64_t div_ore = jint_val(resp, "result.dividend_ore", 0); - memset(ff, 0, sizeof ff); - for (int i = 0; i < nf; i++) { - vals[i] = xcalloc(512, 1); - if (i != 2) { - char path[64], *v; - snprintf(path, sizeof path, "result.%s", keys[i]); - v = jstr_dup(resp, path); - snprintf(vals[i], 512, "%s", v ? v : ""); - free(v); - } - ff[i].label = labels[i]; - ff[i].value = vals[i]; - ff[i].cap = 512; - } - free(resp); - - /* A closed year counts as posted; an open one only when the tax - voucher that bokslut.post creates is already in the books. */ - int posted = closed; - if (!closed) { - char vargs[160]; - snprintf(vargs, sizeof vargs, - "{\"fiscal_year\":%lld," - "\"text\":\"Skatt på årets resultat\",\"limit\":1}", - (long long)a->fy); - char *vresp = client_rpc(&a->conn, "voucher.list", a->session, - a->org, vargs); - if (vresp && client_ok(vresp)) - posted = jarr_size(vresp, "result.items") > 0; - free(vresp); - } - - char status_line[96]; - snprintf(status_line, sizeof status_line, "Bokslut bokfört: %s", - posted ? "ja" : "nej"); - const char *reason = NULL; - if (closed) - reason = "året är stängt"; - else if (posted) - reason = "bokslutet är redan bokfört"; - else if (!can_edit) - reason = "du har inte behörighet"; - struct tui_form_action acts[5] = { - { status_line, -1, NULL }, - { "Årsredovisning (K2)", 1, NULL }, - { "Inkomstdeklaration (INK2/SRU)", 1, NULL }, - { "Bokslutsplan (torrkörning)", 1, NULL }, - { "Bokför bokslut", reason ? 0 : 1, reason }, - }; - - ff[0].kind = TUI_F_TEXT; - ff[1].kind = TUI_F_DATE; - ff[2].kind = TUI_F_AMOUNT; - ff[2].ore = &div_ore; - ff[3].kind = TUI_F_DATE; - ff[4].kind = TUI_F_TEXT; - ff[5].kind = TUI_F_TEXT; - ff[6].label = "Periodiseringsfond (kr)"; - ff[6].value = fond; - ff[6].cap = sizeof fond; - ff[6].kind = TUI_F_TEXT; - ff[7].label = "Skattesats (%)"; - ff[7].value = rate; - ff[7].cap = sizeof rate; - ff[7].kind = TUI_F_TEXT; - tui_form_hint("upp/ned/Tab = flytta Enter = ändra/utför F5 = visa" - " bokslutsplan ^Enter = bokför planen (frågar" - " först) Esc/q = tillbaka ^C = avsluta"); - int r = tui_form_run_actions("Bokslut", ff, nf + 2, can_edit, acts, 5, - NULL, &sel); - if (r >= 0 && r < nf) { - 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, "id", a->fy); - if (r == 2) - yyjson_mut_obj_add_int(d, o, "dividend_ore", div_ore); - else - yyjson_mut_obj_add_strcpy(d, o, keys[r], vals[r]); - char *uargs = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *rr = client_rpc(&a->conn, "fiscal_year.update", a->session, - a->org, uargs); - free(uargs); - if (!rr || !client_ok(rr)) - show_error("Kunde inte spara", rr); - free(rr); - } - for (int i = 0; i < nf; i++) - free(vals[i]); - if (r == TUI_FORM_ACTION + 1) { - arsredovisning_screen(a); - continue; - } - if (r == TUI_FORM_ACTION + 2) { - ink2_screen(a); - continue; - } - if (r == TUI_FORM_ACTION + 3) { - bokslut_plan(a, fond, rate, 0); - continue; - } - if (r == TUI_FORM_ACTION + 4) { - bokslut_plan(a, fond, rate, 1); - continue; - } - if (r == TUI_FORM_REFRESH) - bokslut_plan(a, fond, rate, 0); - else if (r == TUI_FORM_SUBMIT) - bokslut_plan(a, fond, rate, 1); - else if (r == TUI_FORM_BACK) - return; - } -} - -/* ------------------------------------------------------------------ */ -/* K2 årsredovisning (draft text) */ -/* ------------------------------------------------------------------ */ - -/* Whole kronor with space thousands; rounded half away from zero. */ - -/* Thousands of kronor for the flerårsöversikt. */ -static long long ar_tkr(int64_t ore) -{ - return (long long)(ore >= 0 ? (ore + 50000) / 100000 - : (ore - 50000) / 100000); -} - -#define AR_YEARS 5 -#define AR_GROUPS 7 - -struct ar_year { - char label[8]; - char from[16], to[16]; - int64_t netto, ror, res_fin, skatt, res, disp; - int64_t fin_in, fin_out; - int64_t groups[AR_GROUPS]; +/* Screen names for ^R restoration and --screen. Only the top-level + views; sub-screens unwind to their parent. */ +struct scene { + const char *name; + void (*fn)(struct app *); }; -/* Balance-sheet amounts are shown the way an annual report reads them: - credit balances (equity, liabilities) as positive numbers. */ -static int64_t ar_bs_ib_sum(yyjson_val *res, int lo, int hi) -{ - int64_t s = 0; - if (!res) - return 0; - for (int k = 0; k < 3; k++) { - yyjson_val *sec = yyjson_obj_get(res, BS_KEYS[k]); - yyjson_val *arr = sec ? yyjson_obj_get(sec, "accounts") : NULL; - size_t n = yyjson_arr_size(arr); - for (size_t i = 0; i < n; i++) { - yyjson_val *row = yyjson_arr_get(arr, i); - if (!acct_in(vstr(row, "account"), lo, hi)) - continue; - int64_t ib = vint(row, "ib_ore"); - s += lo >= 2000 ? -ib : ib; - } - } - return s; -} - -static int64_t ar_bs_sum(yyjson_val *res, int lo, int hi) -{ - int64_t s = 0; - if (!res) - return 0; - for (int k = 0; k < 3; k++) { - yyjson_val *sec = yyjson_obj_get(res, BS_KEYS[k]); - yyjson_val *arr = sec ? yyjson_obj_get(sec, "accounts") : NULL; - size_t n = yyjson_arr_size(arr); - for (size_t i = 0; i < n; i++) { - yyjson_val *row = yyjson_arr_get(arr, i); - if (!acct_in(vstr(row, "account"), lo, hi)) - continue; - int64_t ub = vint(row, "ub_ore"); - s += lo >= 2000 ? -ub : ub; - } - } - return s; -} - -/* One year's income statement from its general ledger. Vouchers whose - description starts with "Stäng" are the source system's closings in - imported history years and are skipped, so those years show their real - figures instead of a zeroed P&L. */ -static void ar_load_year(struct ar_year *y, yyjson_val *gl) -{ - memset(y, 0, sizeof *y); - yyjson_val *fy = yyjson_obj_get(gl, "fiscal_year"); - snprintf(y->label, sizeof y->label, "%.4s", vstr(fy, "start_date")); - snprintf(y->from, sizeof y->from, "%s", vstr(fy, "start_date")); - snprintf(y->to, sizeof y->to, "%s", vstr(fy, "end_date")); - yyjson_val *accts = yyjson_obj_get(gl, "accounts"); - size_t na = yyjson_arr_size(accts); - for (size_t i = 0; i < na; i++) { - yyjson_val *acct = yyjson_arr_get(accts, i); - const char *num = vstr(acct, "account"); - int n = atoi(num); - if (n < 3000 || n > 8999) - continue; - int64_t net = 0; - yyjson_val *rows = yyjson_obj_get(acct, "rows"); - size_t nr = yyjson_arr_size(rows); - for (size_t k = 0; k < nr; k++) { - yyjson_val *row = yyjson_arr_get(rows, k); - if (strncmp(vstr(row, "description"), "Stäng", 7) == 0) - continue; - net += vint(row, "debit_ore") - vint(row, "credit_ore"); - } - int64_t disp = -net; - int done = 0; - for (size_t g = 0; g < AR_GROUPS; g++) { - if (acct_in(num, IS_GROUPS[g].lo, IS_GROUPS[g].hi)) { - y->groups[g] += disp; - done = 1; - break; - } - } - if (done) - continue; - if (n >= 8000 && n <= 8399) - y->fin_in += disp; - else if (n >= 8400 && n <= 8599) - y->fin_out += disp; - else if (n >= 8800 && n <= 8899) - y->disp += disp; - else if (n >= 8900 && n <= 8989) - y->skatt += disp; - } - int64_t rev = 0, cost = 0; - for (size_t g = 0; g < AR_GROUPS; g++) - if (IS_GROUPS[g].rev) - rev += y->groups[g]; - else - cost += y->groups[g]; - y->netto = y->groups[0]; - y->ror = rev + cost; - y->res_fin = y->ror + y->fin_in + y->fin_out; - y->res = y->res_fin + y->disp + y->skatt; -} - -static void ar_amt_col(char *buf, size_t n, int width, int64_t ore) -{ - char tmp[40]; - if (n == 0) - return; - tui_kr0_format(ore, tmp, sizeof tmp); - if ((size_t)width >= n) - width = (int)n - 1; - if (width < 1) - width = 1; - if (strlen(tmp) >= n) - tmp[n - 1] = '\0'; - snprintf(buf, n, "%*s", width, tmp); -} - -/* Sum and balance rows are derived figures and read as secondary. */ -static int ar_dim_label(const char *label) -{ - return strncmp(label, "Summa", 5) == 0 || strcmp(label, "Totalt") == 0 || - strncmp(label, "Belopp vid", 10) == 0 || - strcmp(label, "Årets resultat") == 0 || - strcmp(label, "Resultat före skatt") == 0 || - strcmp(label, "Rörelseresultat") == 0 || - strcmp(label, "Resultat efter finansiella poster") == 0; -} - -static void ar_row(struct buf *t, const char *label, int64_t cur, int64_t prev, - int indent) -{ - char nm[160], padded[200], a1[40], a2[40]; - snprintf(nm, sizeof nm, "%s%s", indent ? " " : "", label); - tui_pad_label(padded, sizeof padded, nm, 48); - ar_amt_col(a1, sizeof a1, 16, cur); - ar_amt_col(a2, sizeof a2, 16, prev); - buf_line(t, "%s%s %s %s\n", ar_dim_label(label) ? MK_DIM : "", padded, - a1, a2); -} - -/* Equity table: four amount columns. */ -static void ar_eq_row(struct buf *t, const char *label, int64_t ak, - int64_t br, int64_t ar, int indent) -{ - char nm[160], padded[200], c1[32], c2[32], c3[32], c4[32]; - snprintf(nm, sizeof nm, "%s%s", indent ? " " : "", label); - tui_pad_label(padded, sizeof padded, nm, 34); - ar_amt_col(c1, sizeof c1, 12, ak); - ar_amt_col(c2, sizeof c2, 14, br); - ar_amt_col(c3, sizeof c3, 14, ar); - ar_amt_col(c4, sizeof c4, 14, ak + br + ar); - buf_line(t, "%s%s %s %s %s %s\n", ar_dim_label(label) ? MK_DIM : "", - padded, c1, c2, c3, c4); -} - -static void ar_wrap(struct buf *t, const char *text) -{ - char line[128] = ""; - const char *p = text; - while (*p) { - while (*p == ' ' || *p == '\n' || *p == '\t') - p++; - if (!*p) - break; - size_t n = strcspn(p, " \n\t"); - size_t ll = strlen(line); - if (ll + (ll ? 1 : 0) + n > 76) { - buf_line(t, "%s\n", line); - ll = 0; - line[0] = '\0'; - } - if (ll) { - line[ll++] = ' '; - line[ll] = '\0'; - } - if (n > sizeof line - 1 - ll) - n = sizeof line - 1 - ll; - memcpy(line + ll, p, n); - line[ll + n] = '\0'; - p += n; - } - if (line[0]) - buf_line(t, "%s\n", line); -} - -/* "2026-06-04" -> "4 juni 2026"; anything else becomes a placeholder. */ -static void ar_sv_date(const char *iso, char *out, size_t n) -{ - static const char *const months[12] = { - "januari", "februari", "mars", "april", "maj", "juni", "juli", - "augusti", "september", "oktober", "november", "december" - }; - int y = 0, m = 0, d = 0; - if (!iso || strlen(iso) != 10 || sscanf(iso, "%d-%d-%d", &y, &m, &d) != 3 || - m < 1 || m > 12 || d < 1 || d > 31) { - snprintf(out, n, "[datum]"); - return; - } - snprintf(out, n, "%d %s %d", d, months[m - 1], y); -} - -static void fmt_arsredovisning(struct buf *t, const struct app *a, - const char *org_nr, const char *city, - const char *description, int64_t shares, - yyjson_val *fyj, const char *board_text, - const char *first_board, yyjson_val *bs, - struct ar_year *years, int nyears) -{ - struct ar_year *cur = &years[nyears - 1]; - struct ar_year *prev = nyears > 1 ? &years[nyears - 2] : NULL; - int64_t ars = cur->res; - int64_t prev_ars = prev ? prev->res : 0; - const char *events = vstr(fyj, "events"); - const char *employees = vstr(fyj, "employees"); - const char *notes = vstr(fyj, "notes"); - int64_t dividend = vint(fyj, "dividend_ore"); - char agm_sv[64], pay_sv[64]; - ar_sv_date(vstr(fyj, "agm_date"), agm_sv, sizeof agm_sv); - ar_sv_date(vstr(fyj, "dividend_date"), pay_sv, sizeof pay_sv); - - buf_line(t, MK_HEAD "ÅRSREDOVISNING\n\n%s\nOrg nr %s\n\n", a->org_name, - org_nr); - buf_line(t, "Årsredovisning för räkenskapsåret %s - %s\n\n", a->fy_start, - a->fy_end); - buf_line(t, "Styrelsen avger följande årsredovisning.\n\n"); - buf_line(t, "Om inte annat särskilt anges, redovisas alla belopp i hela" - " kronor.\n"); - - buf_line(t, "\n" MK_HEAD "Fastställelseintyg\n\n"); - buf_line(t, "Undertecknad styrelseledamot i %s intygar, dels att denna" - " kopia av\nårsredovisningen stämmer överens med originalet," - " dels att resultaträkningen\noch balansräkningen har" - " fastställts på årsstämma den %s. Årsstämman\nbeslutade" - " att godkänna styrelsens förslag till hur vinsten ska" - " fördelas.\n\n", a->org_name, agm_sv); - buf_line(t, "%s %s\n\nNamnteckning:" - " ___________________________\n\nNamnförtydligande: %s\n", - city, agm_sv, first_board); - - buf_line(t, "\n" MK_HEAD "Förvaltningsberättelse\n\nVerksamheten\n\n" - "Allmänt om verksamheten\n"); - if (description && *description) - ar_wrap(t, description); - else - buf_line(t, "[Kort beskrivning av verksamheten saknas — fyll i under" - " Företagsuppgifter.]\n"); - buf_line(t, "\nBolaget har sitt säte i %s.\n", city); - buf_line(t, "\n" MK_HEAD "Väsentliga händelser under räkenskapsåret\n"); - if (events && *events) - ar_wrap(t, events); - else - buf_line(t, "[Beskriv väsentliga händelser.]\n"); - - if (nyears > 1) { - char row[512]; - int off = snprintf(row, sizeof row, "%-38s", "Flerårsöversikt"); - for (int i = 0; i < nyears; i++) - off += snprintf(row + off, sizeof row - (size_t)off, "%9s", - years[i].label); - buf_line(t, "\n" MK_HEAD "%s\n\n", row); - off = snprintf(row, sizeof row, "%-38s", "Nettoomsättning, tkr"); - for (int i = 0; i < nyears; i++) - off += snprintf(row + off, sizeof row - (size_t)off, "%9lld", - ar_tkr(years[i].netto)); - buf_line(t, "%s\n", row); - off = snprintf(row, sizeof row, "%-38s", - "Resultat efter finansiella poster, tkr"); - for (int i = 0; i < nyears; i++) - off += snprintf(row + off, sizeof row - (size_t)off, "%9lld", - ar_tkr(years[i].res_fin)); - buf_line(t, "%s\n", row); - } - - int64_t ak_ib = ar_bs_ib_sum(bs, 2081, 2089); - int64_t ak_ub = ar_bs_sum(bs, 2081, 2089); - int64_t br_ib = ar_bs_ib_sum(bs, 2091, 2098); - int64_t br_ub = ar_bs_sum(bs, 2091, 2098); - int64_t movement = br_ub - br_ib; - int64_t paid_div = prev_ars - movement; - if (paid_div < 0) - paid_div = 0; - int64_t balanced = prev_ars - paid_div; - if (prev) { - char row[512]; - int off = snprintf(row, sizeof row, "%-34s", "Förändringar i eget" - " kapital"); - off += snprintf(row + off, sizeof row - (size_t)off, "%12s", - "Aktiekapital"); - off += snprintf(row + off, sizeof row - (size_t)off, "%14s", - "Balanserat"); - off += snprintf(row + off, sizeof row - (size_t)off, "%14s", - "Årets"); - off += snprintf(row + off, sizeof row - (size_t)off, "%14s", - "Totalt"); - buf_line(t, "\n" MK_HEAD "%s\n", row); - buf_line(t, "%s\n", " resultat" - " resultat"); - int has_disp = movement != 0; - int64_t ar_ib = has_disp ? prev_ars : 0; - ar_eq_row(t, "Belopp vid årets ingång", ak_ib, br_ib, ar_ib, 0); - if (has_disp) { - buf_line(t, "Resultatdisposition enligt årsstämman\n"); - ar_eq_row(t, "Utdelning", 0, -paid_div, 0, 1); - ar_eq_row(t, "Balanseras i ny räkning", 0, balanced, -prev_ars, 1); - } - ar_eq_row(t, "Årets resultat", 0, 0, ars, 0); - ar_eq_row(t, "Belopp vid årets utgång", ak_ub, br_ub, ars, 0); - } - - int64_t dispon = br_ub + ars; - buf_line(t, "\n" MK_HEAD "Resultatdisposition\n\nTill årsstämmans" - " förfogande står följande vinstmedel:\n"); - ar_row(t, "Balanserat resultat", br_ub, 0, 1); - ar_row(t, "Årets resultat", ars, 0, 1); - ar_row(t, "Totalt", dispon, 0, 1); - buf_line(t, "\nStyrelsen föreslår att vinstmedlen disponeras enligt" - " följande:\n"); - ar_row(t, "Utdelning till ägarna", dividend, 0, 1); - ar_row(t, "Balanseras i ny räkning", dispon - dividend, 0, 1); - ar_row(t, "Totalt", dispon, 0, 1); - - if (dividend > 0) { - buf_line(t, "\n" MK_HEAD "Styrelsens yttrande över den föreslagna" - " vinstutdelningen\n\n"); - if (shares > 0) { - char per[48]; - tui_kr_format((dividend + shares / 2) / shares, per, sizeof per); - buf_line(t, "Utdelning per aktie: %s kr. Datum för utdelning är" - " %s.\n\n", per, pay_sv); - } else { - buf_line(t, "Datum för utdelning är %s.\n\n", pay_sv); - } - buf_line(t, "Likviditeten i bolaget bedöms kunna upprätthållas på en" - " likaledes betryggande nivå.\n\n"); - buf_line(t, "Styrelsens uppfattning är att den föreslagna utdelningen" - " ej hindrar bolaget\nfrån att fullgöra sina förpliktelser" - " på kort och lång sikt, ej heller att fullgöra\nerforderliga" - " investeringar. Den föreslagna utdelningen kan därmed" - " försvaras med\nhänsyn till vad som anförs i ABL 17 kap." - " 3 § 2 st. (försiktighetsregeln).\n"); - } - - buf_line(t, "\n" MK_HEAD "Resultaträkning\nSamtliga belopp anges i" - " svenska kronor.\n\n"); - { - char h1[40], h2[40], row[512]; - snprintf(h1, sizeof h1, "%s - %s", cur->from, cur->to); - if (prev) - snprintf(h2, sizeof h2, "%s - %s", prev->from, prev->to); - else - h2[0] = '\0'; - int off = snprintf(row, sizeof row, "%-48s", ""); - off += snprintf(row + off, sizeof row - (size_t)off, "%16s", h1); - if (h2[0]) - off += snprintf(row + off, sizeof row - (size_t)off, "%17s", h2); - buf_line(t, "%s\n", row); - } - buf_line(t, MK_HEAD "Rörelseintäkter, lagerförändringar m.m.\n"); - ar_row(t, "Nettoomsättning", cur->groups[0], - prev ? prev->groups[0] : 0, 1); - int64_t rev_cur = cur->groups[0] + cur->groups[1]; - int64_t rev_pre = prev ? prev->groups[0] + prev->groups[1] : 0; - ar_row(t, "Summa Rörelseintäkter, lagerförändringar m.m.", rev_cur, - rev_pre, 1); - buf_line(t, "\n" MK_HEAD "Rörelsekostnader\n"); - int64_t cost_cur = 0, cost_pre = 0; - for (size_t g = 2; g < AR_GROUPS; g++) { - int64_t c = cur->groups[g], p = prev ? prev->groups[g] : 0; - if (c || p) - ar_row(t, IS_GROUPS[g].title, c, p, 1); - cost_cur += c; - cost_pre += p; - } - ar_row(t, "Summa Rörelsens kostnader", cost_cur, cost_pre, 1); - buf_line(t, "\n"); - ar_row(t, "Rörelseresultat", cur->ror, prev ? prev->ror : 0, 0); - if (cur->fin_in || (prev && prev->fin_in)) - ar_row(t, "Ränteintäkter", cur->fin_in, prev ? prev->fin_in : 0, 0); - if (cur->fin_out || (prev && prev->fin_out)) - ar_row(t, "Räntekostnader", cur->fin_out, prev ? prev->fin_out : 0, 0); - ar_row(t, "Resultat efter finansiella poster", cur->res_fin, - prev ? prev->res_fin : 0, 0); - int64_t btax = cur->res_fin + cur->disp; - int64_t btax_pre = prev ? prev->res_fin + prev->disp : 0; - if (cur->disp || (prev && prev->disp)) { - buf_line(t, "\n" MK_HEAD "Bokslutsdispositioner\n"); - ar_row(t, "Summa", cur->disp, prev ? prev->disp : 0, 1); - } - ar_row(t, "Resultat före skatt", btax, btax_pre, 0); - if (cur->skatt || (prev && prev->skatt)) - ar_row(t, "Skatt på årets resultat", cur->skatt, - prev ? prev->skatt : 0, 0); - buf_line(t, "\n"); - ar_row(t, "Årets resultat", ars, prev_ars, 0); - - buf_line(t, "\n" MK_HEAD "Balansräkning\n\n"); - { - char h1[40], h2[40], row[256]; - snprintf(h1, sizeof h1, "%s", cur->to); - if (prev) - snprintf(h2, sizeof h2, "%s", prev->to); - else - h2[0] = '\0'; - int off = snprintf(row, sizeof row, "%-48s", ""); - off += snprintf(row + off, sizeof row - (size_t)off, "%16s", h1); - if (h2[0]) - off += snprintf(row + off, sizeof row - (size_t)off, "%17s", h2); - buf_line(t, "%s\n", row); - } - buf_line(t, MK_HEAD "Tillgångar\nAnläggningstillgångar\n"); - ar_row(t, "Summa anläggningstillgångar", ar_bs_sum(bs, 1000, 1399), - ar_bs_ib_sum(bs, 1000, 1399), 1); - buf_line(t, MK_HEAD "Omsättningstillgångar\n"); - int64_t v = ar_bs_sum(bs, 1400, 1499), vp = ar_bs_ib_sum(bs, 1400, 1499); - if (v || vp) - ar_row(t, "Varulager", v, vp, 0); - ar_row(t, "Kortfristiga fordringar", ar_bs_sum(bs, 1500, 1699), - ar_bs_ib_sum(bs, 1500, 1699), 0); - v = ar_bs_sum(bs, 1700, 1899); - vp = ar_bs_ib_sum(bs, 1700, 1899); - if (v || vp) - ar_row(t, "Kortfristiga placeringar", v, vp, 0); - ar_row(t, "Kassa och bank", ar_bs_sum(bs, 1900, 1999), - ar_bs_ib_sum(bs, 1900, 1999), 0); - ar_row(t, "Summa tillgångar", ar_bs_sum(bs, 1000, 1999), - ar_bs_ib_sum(bs, 1000, 1999), 0); - buf_line(t, "\n" MK_HEAD "Eget kapital och skulder\nBundet eget" - " kapital\n"); - ar_row(t, "Aktiekapital", ak_ub, ak_ib, 1); - ar_row(t, "Summa bundet eget kapital", ar_bs_sum(bs, 2000, 2090), - ar_bs_ib_sum(bs, 2000, 2090), 1); - buf_line(t, MK_HEAD "Fritt eget kapital\n"); - ar_row(t, "Balanserat resultat", br_ub, br_ib, 1); - ar_row(t, "Årets resultat", ars, prev_ars, 1); - ar_row(t, "Summa fritt eget kapital", ar_bs_sum(bs, 2091, 2098) + ars, - ar_bs_ib_sum(bs, 2091, 2098) + prev_ars, 1); - ar_row(t, "Summa eget kapital", - ar_bs_sum(bs, 2000, 2090) + ar_bs_sum(bs, 2091, 2098) + ars, - ar_bs_ib_sum(bs, 2000, 2090) + ar_bs_ib_sum(bs, 2091, 2098) + - prev_ars, - 0); - buf_line(t, "\n"); - ar_row(t, "Obeskattade reserver", ar_bs_sum(bs, 2100, 2199), - ar_bs_ib_sum(bs, 2100, 2199), 0); - ar_row(t, "Avsättningar", ar_bs_sum(bs, 2200, 2299), - ar_bs_ib_sum(bs, 2200, 2299), 0); - buf_line(t, MK_HEAD "Kortfristiga skulder\n"); - ar_row(t, "Skatteskulder", ar_bs_sum(bs, 2500, 2599), - ar_bs_ib_sum(bs, 2500, 2599), 1); - ar_row(t, "Mervärdesskatt", ar_bs_sum(bs, 2600, 2699), - ar_bs_ib_sum(bs, 2600, 2699), 1); - ar_row(t, "Personalskatter och sociala avgifter", - ar_bs_sum(bs, 2700, 2799), ar_bs_ib_sum(bs, 2700, 2799), 1); - ar_row(t, "Övriga kortfristiga skulder", ar_bs_sum(bs, 2800, 2999) + - ar_bs_sum(bs, 2400, 2499), - ar_bs_ib_sum(bs, 2800, 2999) + ar_bs_ib_sum(bs, 2400, 2499), 1); - int64_t skuld = ar_bs_sum(bs, 2400, 2999); - int64_t skuld_p = ar_bs_ib_sum(bs, 2400, 2999); - ar_row(t, "Summa kortfristiga skulder", skuld, skuld_p, 1); - ar_row(t, "Summa eget kapital, avsättningar och skulder", - ar_bs_sum(bs, 2000, 2999) + ars, - ar_bs_ib_sum(bs, 2000, 2999) + prev_ars, 0); - - buf_line(t, "\n" MK_HEAD "Noter\n\nNot 1 Redovisnings- och" - " värderingsprinciper\n"); - buf_line(t, "Årsredovisningen är upprättad i enlighet med" - " årsredovisningslagen och\nBFNAR 2016:10 Årsredovisning i" - " mindre företag.\n"); - buf_line(t, "\n" MK_HEAD "Not 2 Medelantal anställda\n"); - buf_line(t, "Medelantalet anställda under räkenskapsåret har uppgått" - " till %s.\n", employees && *employees ? employees - : "[antal]"); - if (notes && *notes) { - buf_line(t, "\n" MK_HEAD "Not 3 Övriga upplysningar\n"); - ar_wrap(t, notes); - } - - buf_line(t, "\n" MK_HEAD "Underskrifter\n\n%s %s\n\n", city, agm_sv); - if (board_text && *board_text) - buf_line(t, "%s", board_text); - else - buf_line(t, "[Lägg till styrelseledamöter under" - " Företagsuppgifter.]\n"); -} - -/* The pager text carries style markers that must not reach the saved file. */ -static void strip_markup(const char *in, struct buf *out) -{ - for (const char *p = in; *p;) { - const char *nl = strchr(p, '\n'); - size_t len = nl ? (size_t)(nl - p) : strlen(p); - if (len && p[0] >= TUI_MARK_HEADING && p[0] <= TUI_MARK_STICKY) { - p++; - len--; - } - buf_append(out, p, len); - if (!nl) - break; - buf_append(out, "\n", 1); - p = nl + 1; - } -} - -static void arsredovisning_save(struct app *a, const char *text) -{ - const char *home = getenv("HOME"); - char def[512]; - snprintf(def, sizeof def, "%s/Downloads", home && *home ? home : "."); - struct stat sb; - if (stat(def, &sb) != 0 || !S_ISDIR(sb.st_mode)) - snprintf(def, sizeof def, "%s", home && *home ? home : "."); - char dirbuf[512]; char *dir = tui_prompt_into(dirbuf, sizeof dirbuf, "Spara årsredovisningen i: ", def, 0) ? dirbuf : NULL; - if (!dir || !*dir) - return; - char full[640]; - if (dir[0] == '~' && (dir[1] == '/' || dir[1] == '\0')) - snprintf(full, sizeof full, "%s%s", home ? home : "", dir + 1); - else - snprintf(full, sizeof full, "%s", dir); - size_t fl = strlen(full); - if (fl && full[fl - 1] != '/') - snprintf(full + fl, sizeof full - fl, "/"); - char name[80]; - snprintf(name, sizeof name, "%s", a->fy_label); - for (char *p = name; *p; p++) - if (*p == '/') - *p = '-'; - snprintf(full + strlen(full), sizeof full - strlen(full), - "Årsredovisning %s.txt", *name ? name : "utkast"); - struct buf clean; - buf_init(&clean); - strip_markup(text, &clean); - FILE *f = fopen(full, "wb"); - size_t n = clean.len; - if (!f || fwrite(clean.p, 1, n, f) != n) { - if (f) - fclose(f); - buf_free(&clean); - tui_message("Årsredovisning", "Kunde inte skriva %s: %s", full, - strerror(errno)); - return; - } - fclose(f); - buf_free(&clean); - tui_message("Årsredovisning", "Sparat: %s\n\nFilen är ett utkast i textform" - " att redigera vidare (t.ex. i Word).", full); -} +static const struct scene SCENES[] = { + { "dashboard", dashboard }, + { "vouchers", vouchers_screen }, + { "inbox", inbox_screen }, + { "bank", bank_screen }, + { "invoices", invoices_screen }, + { "customers", customers_screen }, + { "ib", ib_screen }, + { "templates", templates_screen }, + { "rules", rules_screen }, + { "reports", reports_screen }, + { "audit", audit_screen }, + { "settings", settings_screen }, + { "company", company_screen }, + { "bokslut", bokslut_screen }, + { "yearinfo", bokslut_screen }, /* ^R compatibility */ +}; -static void arsredovisning_screen(struct app *a) +void open_scene(struct app *a, const char *name) { - for (;;) { - char *fresp = client_rpc(&a->conn, "fiscal_year.list", a->session, - a->org, "{}"); - if (!fresp || !client_ok(fresp)) { - show_error("Årsredovisning", fresp); - free(fresp); - return; - } - struct ar_year years[AR_YEARS]; - int nyears = 0; - { - yyjson_doc *fd = parse(fresp); - yyjson_val *items = - fd ? jget(yyjson_doc_get_root(fd), "result.items") : NULL; - size_t n = yyjson_arr_size(items); - int idx = -1; - for (size_t i = 0; i < n; i++) { - yyjson_val *it = yyjson_arr_get(items, i); - if (vint(it, "id") == a->fy) - idx = (int)i; - } - if (idx < 0) - idx = (int)n - 1; - int start = idx - (AR_YEARS - 1); - if (start < 0) - start = 0; - for (int i = start; i <= idx && nyears < AR_YEARS; i++) { - yyjson_val *it = yyjson_arr_get(items, (size_t)i); - char gargs[64]; - snprintf(gargs, sizeof gargs, "{\"fiscal_year\":%lld}", - (long long)vint(it, "id")); - char *gresp = client_rpc(&a->conn, "report.general_ledger", - a->session, a->org, gargs); - if (!gresp || !client_ok(gresp)) { - show_error("Årsredovisning", gresp); - free(gresp); - yyjson_doc_free(fd); - free(fresp); - return; - } - yyjson_doc *gd = parse(gresp); - yyjson_val *g = - gd ? jget(yyjson_doc_get_root(gd), "result") : NULL; - if (g) - ar_load_year(&years[nyears++], g); - yyjson_doc_free(gd); - free(gresp); - } - yyjson_doc_free(fd); - } - free(fresp); - - char bargs[64]; - snprintf(bargs, sizeof bargs, "{\"fiscal_year\":%lld}", - (long long)a->fy); - char *bresp = client_rpc(&a->conn, "report.balance_sheet", a->session, - a->org, bargs); - char *oresp = client_rpc(&a->conn, "org.get", a->session, a->org, - "{}"); - char *yresp = client_rpc(&a->conn, "fiscal_year.get", a->session, - a->org, "{}"); - char *mresp = client_rpc(&a->conn, "board.list", a->session, a->org, - "{}"); - if (!bresp || !client_ok(bresp) || !oresp || !client_ok(oresp) || - !yresp || !client_ok(yresp) || !mresp || !client_ok(mresp) || - nyears == 0) { - show_error("Årsredovisning", bresp && !client_ok(bresp) ? bresp - : oresp); - free(bresp); - free(oresp); - free(yresp); - free(mresp); + snprintf(g_scene, sizeof g_scene, "%s", name); + for (size_t i = 0; i < sizeof SCENES / sizeof SCENES[0]; i++) { + if (strcmp(name, SCENES[i].name) == 0) { + SCENES[i].fn(a); return; } - yyjson_doc *bd = parse(bresp), *od = parse(oresp), *yd = parse(yresp); - yyjson_val *bs = bd ? jget(yyjson_doc_get_root(bd), "result") : NULL; - yyjson_val *org = od ? jget(yyjson_doc_get_root(od), "result") : NULL; - yyjson_val *fyj = yd ? jget(yyjson_doc_get_root(yd), "result") : NULL; - char *onr = org ? jstr_dup(oresp, "result.org_nr") : NULL; - char *city = org ? jstr_dup(oresp, "result.city") : NULL; - char *desc = org ? jstr_dup(oresp, "result.description") : NULL; - int64_t shares = org ? jint_val(oresp, "result.shares", 0) : 0; - char board_text[1024] = ""; - char first_board[128] = "[namn]"; - size_t nb = jarr_size(mresp, "result.items"); - for (size_t i = 0; i < nb && strlen(board_text) < 900; i++) { - char path[64]; - snprintf(path, sizeof path, "result.items.%zu.title", i); - char *ti = jstr_dup(mresp, path); - snprintf(path, sizeof path, "result.items.%zu.name", i); - char *nm = jstr_dup(mresp, path); - char line[256]; - snprintf(line, sizeof line, "%s %s\n\n", ti ? ti : "", - nm ? nm : ""); - strncat(board_text, line, - sizeof board_text - strlen(board_text) - 1); - if (i == 0) - snprintf(first_board, sizeof first_board, "%s", - nm && *nm ? nm : "[namn]"); - free(ti); - free(nm); - } - struct buf t; - buf_init(&t); - if (bs && nyears > 0) - fmt_arsredovisning(&t, a, onr && *onr ? onr : "[org.nr]", - city && *city ? city : "[Ort]", - desc ? desc : "", shares, fyj, board_text, - first_board, bs, years, nyears); - else - buf_line(&t, "Kunde inte läsa rapporterna."); - buf_append(&t, "", 1); - int again = - tui_pager("Årsredovisning (K2)", (const char *)t.p, "s = spara"); - if (again == 2) - arsredovisning_save(a, (const char *)t.p); - buf_free(&t); - free(onr); - free(city); - free(desc); - yyjson_doc_free(bd); - yyjson_doc_free(od); - yyjson_doc_free(yd); - free(bresp); - free(oresp); - free(yresp); - free(mresp); - if (again != 1 && again != 2) - return; } } -/* Screen names for ^R restoration and --screen. Only the top-level - views; sub-screens unwind to their parent. */ -static void open_scene(struct app *a, const char *scene) -{ - snprintf(g_scene, sizeof g_scene, "%s", scene); - if (strcmp(scene, "vouchers") == 0) - vouchers_screen(a); - else if (strcmp(scene, "inbox") == 0) - inbox_screen(a); - else if (strcmp(scene, "bank") == 0) - bank_screen(a); - else if (strcmp(scene, "invoices") == 0) - invoices_screen(a); - else if (strcmp(scene, "customers") == 0) - customers_screen(a); - else if (strcmp(scene, "ib") == 0) - ib_screen(a); - else if (strcmp(scene, "templates") == 0) - templates_screen(a); - else if (strcmp(scene, "rules") == 0) - rules_screen(a); - else if (strcmp(scene, "reports") == 0) - reports_screen(a); - else if (strcmp(scene, "audit") == 0) - audit_screen(a); - else if (strcmp(scene, "settings") == 0) - settings_screen(a); - else if (strcmp(scene, "company") == 0) - company_screen(a); - else if (strcmp(scene, "bokslut") == 0 || - strcmp(scene, "yearinfo") == 0) /* yearinfo: ^R compatibility */ - bokslut_screen(a); -} - static void do_reload(struct app *a, const char *self) { char orgbuf[32], fybuf[32]; @@ -7395,93 +87,6 @@ static void do_reload(struct app *a, const char *self) execvp(argv[0], argv); fprintf(stderr, "bokftui: kunde inte ladda om: %s\n", strerror(errno)); } - -static const char *const MAIN_ITEMS[] = { - MK_SECTION "Bokföring", - "Verifikat", "Underlag (inkorg)", "Bankavstämning", - MK_SECTION "Fakturering", - "Fakturor", - MK_SECTION "Rapporter & bokslut", - "Rapporter", "Bokslut", - MK_SECTION "Register", - "Mallar", "Kunder", "Momsregler", "Företagsuppgifter", "Inställningar", - MK_SECTION "Räkenskapsår", - "Ingående balans", "Räkenskapsår", - MK_SECTION "Övrigt", - "Revision", "Logga ut / avsluta", -}; - -static void dashboard(struct app *a) -{ - static int last_sel = 0; - for (;;) { - if (g_reload) - return; - int sel = tui_menu("Bokf", MAIN_ITEMS, - (int)(sizeof MAIN_ITEMS / sizeof MAIN_ITEMS[0]), 1, - &last_sel); - snprintf(g_scene, sizeof g_scene, "%s", "dashboard"); - if (g_quit || g_reload) - return; - if (sel == -4) { - vouchers_new(a); - continue; - } - if (sel == -5) - return; - if (sel < 0) - continue; /* Esc/back at the top level never exits */ - switch (sel) { - case 1: - open_scene(a, "vouchers"); - break; - case 2: - open_scene(a, "inbox"); - break; - case 3: - open_scene(a, "bank"); - break; - case 5: - open_scene(a, "invoices"); - break; - case 7: - open_scene(a, "reports"); - break; - case 8: - open_scene(a, "bokslut"); - break; - case 10: - open_scene(a, "templates"); - break; - case 11: - open_scene(a, "customers"); - break; - case 12: - open_scene(a, "rules"); - break; - case 13: - open_scene(a, "company"); - break; - case 14: - open_scene(a, "settings"); - break; - case 16: - open_scene(a, "ib"); - break; - case 17: - select_fiscal_year(a); - break; - case 19: - open_scene(a, "audit"); - break; - case 20: - return; - default: - break; /* section header */ - } - } -} - /* ------------------------------------------------------------------ */ /* login */ /* ------------------------------------------------------------------ */ @@ -7501,7 +106,7 @@ static void config_path(char *buf, size_t n) home && *home ? home : "."); } -static void config_mkdirs(const char *path) +void config_mkdirs(const char *path) { char tmp[640]; snprintf(tmp, sizeof tmp, "%s", path); @@ -7556,40 +161,6 @@ static void config_save(const struct app *a) chmod(path, 0600); } -/* Small append-only log for connection/reload problems. */ -static void tui_log(const char *fmt, ...) - __attribute__((format(printf, 1, 2))); - -static void tui_log(const char *fmt, ...) -{ - char path[600], dir[512]; - const char *cache = getenv("XDG_CACHE_HOME"); - if (cache && *cache) - snprintf(dir, sizeof dir, "%s/bokf", cache); - else { - const char *home = getenv("HOME"); - snprintf(dir, sizeof dir, "%s/.cache/bokf", home && *home ? home : "."); - } - if (snprintf(path, sizeof path, "%s/tui.log", dir) >= (int)sizeof path) - return; - config_mkdirs(path); - FILE *f = fopen(path, "a"); - if (!f) - return; - time_t now = time(NULL); - struct tm tm; - gmtime_r(&now, &tm); - fprintf(f, "%04d-%02d-%02dT%02d:%02d:%02dZ ", - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, - tm.tm_min, tm.tm_sec); - va_list ap; - va_start(ap, fmt); - vfprintf(f, fmt, ap); - va_end(ap); - fputc('\n', f); - fclose(f); -} - /* Connects and opens a session. Returns 0 on success, -2 when the connection failed and -1 when the server rejected the login. */ static int try_login(struct app *a, const char *user, const char *pass, -- cgit v1.3