From 49343ab72fa388aeba155c0fa605efdbfd8a345a Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Fri, 18 Sep 2026 21:07:38 +0200 Subject: bokftui: Huvudbok/Verifikationslista views, Räkenskapsår screen, ^Enter/^X MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The report menu gains Kapitas-style Huvudbok and Verifikationslista tables. "Byt räkenskapsår" is now "Räkenskapsår": the list shows öppen/stängd and ^A closes or reopens the highlighted year (via the new fiscal_year.reopen). Menus remember the selection, Mallar opens straight into the list (Enter edit, ^N new, d arkivera), report tables get column rules and aligned sums, and text_view clears stale lines. Footer hints write control keys as ^N/^A/^C/^R/^X, ^Enter saves or posts a form and ^X clears the focused row (F9 still works as a fallback). --- clients/bokftui.c | 516 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 380 insertions(+), 136 deletions(-) (limited to 'clients') diff --git a/clients/bokftui.c b/clients/bokftui.c index 9ea6eb6..b2d8e42 100644 --- a/clients/bokftui.c +++ b/clients/bokftui.c @@ -18,11 +18,15 @@ #include "version.h" #include "yyjson.h" -/* universal "new/add" hotkey: Ctrl+N (0x0e), the de-facto Linux convention */ +/* universal "new/add" hotkey: ^N (0x0e), the de-facto Linux convention */ #define KEY_CTRL_N 0x0e -/* Ctrl+C quits the application; Esc is navigation only */ +/* ^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) +/* ^C quits the application; Esc is navigation only */ static int g_quit = 0; -/* Ctrl+R re-execs the binary, keeping session, org, year and screen */ +/* ^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]; @@ -30,11 +34,11 @@ static char g_reload_scene[32]; static int ui_getch(void) { int ch = getch(); - if (ch == 3) { /* Ctrl+C */ + if (ch == 3) { /* ^C */ g_quit = 1; return 27; /* behave like "back" so screens unwind */ } - if (ch == 18) { /* Ctrl+R */ + if (ch == 18) { /* ^R */ g_reload = 1; snprintf(g_reload_scene, sizeof g_reload_scene, "%s", g_scene); return 27; @@ -47,7 +51,7 @@ struct app { char socket[256]; char session[128]; char username[64]; - char password[128]; /* typed or from BOKFD_PASSWORD, kept for Ctrl+R */ + char password[128]; /* typed or from BOKFD_PASSWORD, kept for ^R */ int64_t org; char org_name[128]; char role[32]; @@ -244,7 +248,7 @@ static int le_key(struct ledit *e, int ch) le_backspace(e); return 1; } - if (ch == 21) { /* Ctrl+U */ + if (ch == 21) { /* ^U */ le_clear(e); return 1; } @@ -560,8 +564,12 @@ static int text_view(const char *title, const char *text) int view = LINES - 4; for (;;) { frame(title); - for (int i = 0; i < view && top + i < n; i++) - mvaddnstr(2 + i, 2, lines[top + i], COLS - 4); + for (int i = 0; i < view; i++) { + move(2 + i, 2); + clrtoeol(); + if (top + i < n) + mvaddnstr(2 + i, 2, lines[top + i], COLS - 4); + } hints("piltangenter/PgUp/PgDn rullar F5 = uppdatera q = tillbaka"); refresh(); int ch = ui_getch(); @@ -594,13 +602,15 @@ static int text_view(const char *title, const char *text) } static int menu(const char *title, const char *const *items, int n, - int allow_new) + int allow_new, int *cursor) { - int sel = 0; + int sel = (cursor && *cursor >= 0 && *cursor < n) ? *cursor : 0; int top = 0; char gotobuf[12] = ""; int goto_active = 0; for (;;) { + if (cursor) + *cursor = sel; int view = (LINES - 4) / 2; if (view < 1) view = 1; @@ -631,10 +641,10 @@ static int menu(const char *title, const char *const *items, int n, } hints(allow_new ? "upp/ned, 1-9 = snabbval, g = gå till, PgUp/PgDn," - " Home/End, Enter Ctrl+N = ny Esc/q = tillbaka" - " Ctrl+C = avsluta" + " Home/End, Enter ^N = ny Esc/q = tillbaka" + " ^C = avsluta" : "upp/ned, 1-9 = snabbval, g = gå till, PgUp/PgDn," - " Home/End, Enter Esc/q = tillbaka Ctrl+C = avsluta"); + " Home/End, Enter Esc/q = tillbaka ^C = avsluta"); refresh(); int ch = ui_getch(); if (goto_active) { @@ -673,8 +683,12 @@ static int menu(const char *title, const char *const *items, int n, sel = 0; else if (ch == KEY_END) sel = n - 1; - else if (ch >= '1' && ch <= '9' && ch - '1' < n) - return ch - '1'; + else if (ch >= '1' && ch <= '9' && ch - '1' < n) { + sel = ch - '1'; + if (cursor) + *cursor = sel; + return sel; + } else if (ch == 'g' || ch == 'G') { goto_active = 1; gotobuf[0] = '\0'; @@ -688,9 +702,14 @@ static int menu(const char *title, const char *const *items, int n, } static int select_list(const char *title, char **items, int n, int start, - int allow_refresh, int *cursor, int allow_new) + int allow_refresh, int *cursor, int allow_new, + int allow_archive, int allow_toggle) { int sel = start; + if (sel < 0) + sel = 0; + if (n > 0 && sel >= n) + sel = n - 1; int top = 0; int view = LINES - 4; char gotobuf[12] = ""; @@ -732,11 +751,13 @@ static int select_list(const char *title, char **items, int n, int start, char hint[256]; snprintf(hint, sizeof hint, "upp/ned, 1-9 = hoppa, g = gå till, PgUp/PgDn, Home/End," - " Enter%s%s Esc/q = tillbaka", + " Enter%s%s%s%s Esc/q = tillbaka", allow_refresh ? " F5 = uppdatera" : "", - allow_new ? " Ctrl+N = ny" : ""); + allow_new ? " ^N = ny" : "", + allow_archive ? " d = arkivera" : "", + allow_toggle ? " ^A = öppna/stäng" : ""); snprintf(hint + strlen(hint), sizeof hint - strlen(hint), - " Ctrl+C = avsluta"); + " ^C = avsluta"); hints(hint); } refresh(); @@ -790,6 +811,10 @@ static int select_list(const char *title, char **items, int n, int start, return -2; else if (allow_new && ch == KEY_CTRL_N) return -4; + else if (allow_archive && (ch == 'd' || ch == 'D') && n > 0) + return -6; + else if (allow_toggle && ch == 1 && n > 0) + return -7; else if (ch == 27 || ch == 'q') return g_quit ? -5 : -1; } @@ -849,6 +874,17 @@ static void kr_format(int64_t ore, char *buf, size_t n) snprintf(buf, n, "%s%s,%02lld", neg ? "-" : "", out, v % 100); } +/* 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]; + 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) @@ -1092,7 +1128,7 @@ static int64_t fy_new_form(struct app *a) attroff(A_REVERSE); } } - hints("Tab = byta fält F5 = validera F9 = skapa Esc = avbryt"); + hints("Tab = byta fält F5 = validera ^Enter = skapa Esc = avbryt"); if (caret_y >= 0) move(caret_y, caret_x); refresh(); @@ -1112,7 +1148,7 @@ static int64_t fy_new_form(struct app *a) field_fresh = 1; continue; } - if (ch == KEY_F(5) || ch == KEY_F(9)) { + if (ch == KEY_F(5) || ch == KEY_CTRL_ENTER || ch == KEY_F(9)) { if (!label[0]) { message("Räkenskapsår", "Etiketten får inte vara tom."); continue; @@ -1164,10 +1200,12 @@ static int64_t fy_new_form(struct app *a) } /* Pick which fiscal year the screens work in. Shown as its real range so - broken fiscal years (not starting in January) are obvious. */ + 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; @@ -1188,6 +1226,7 @@ static void select_fiscal_year(struct app *a) } 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; @@ -1206,10 +1245,9 @@ static void select_fiscal_year(struct app *a) 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", - strcmp(status ? status : "", "closed") == 0 ? "stängd" - : "öppen"); + snprintf(stbuf, sizeof stbuf, "%s", closed[i] ? "stängd" : "öppen"); pad_field(stbuf, sizeof stbuf, 6); snprintf(line, sizeof line, "%-25s %s %s", ranges[i], stbuf, labels[i]); @@ -1221,14 +1259,18 @@ static void select_fiscal_year(struct app *a) free(edate); free(status); } - int sel = select_list("Välj räkenskapsår", lines, (int)n, sel_default, - 0, NULL, 1); + int start = cursor >= 0 ? cursor : sel_default; + if (start >= (int)n) + start = (int)n - 1; + int sel = select_list("Räkenskapsår", lines, (int)n, start, 0, + &cursor, 1, 0, 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); @@ -1236,8 +1278,41 @@ static void select_fiscal_year(struct app *a) 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 *ans = prompt(label, "n", 0); + 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)) + 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; @@ -1248,6 +1323,7 @@ static void select_fiscal_year(struct app *a) 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; @@ -1380,7 +1456,7 @@ static char *file_browser(struct app *a, const char *start_dir) } char title[1200]; snprintf(title, sizeof title, "Välj fil — %.200s", dir); - int sel = select_list(title, items, (int)n + 1, 0, 1, NULL, 0); + int sel = select_list(title, items, (int)n + 1, 0, 1, NULL, 0, 0, 0); for (size_t i = 0; i <= n; i++) free(items[i]); free(items); @@ -1509,7 +1585,7 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) frame("Verifikat"); for (int i = 0; i < view && top + i < n; i++) mvaddnstr(2 + i, 2, lines[top + i], COLS - 4); - hints("upp/ned F5 = uppdatera Ctrl+N = nytt verifikat c =" + hints("upp/ned F5 = uppdatera ^N = nytt verifikat c =" " rätta Esc/q = tillbaka"); refresh(); int ch = ui_getch(); @@ -1847,8 +1923,8 @@ static int64_t vouchers_new(struct app *a) attroff(A_BOLD); if (!balanced) mvprintw(7 + nrows + 2, 2, "Differens: %s", diff); - hints("F4 = mall Ctrl+F = bifoga fil Tab = byta fält F5 = validera" - " F7 = rensa rad F9 = bokför Esc = avbryt"); + hints("F4 = mall ^F = bifoga fil Tab = byta fält F5 = validera" + " ^X = rensa rad ^Enter = bokför Esc = avbryt"); if (caret_y >= 0) move(caret_y, caret_x); refresh(); @@ -1930,7 +2006,7 @@ static int64_t vouchers_new(struct app *a) free(ser); free(ds); } - int tsel = select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0); + int tsel = select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0, 0, 0); if (tsel >= 0) snprintf(tname, sizeof tname, "%s", names[tsel]); for (size_t i = 0; i < ln; i++) { @@ -2044,7 +2120,7 @@ static int64_t vouchers_new(struct app *a) } continue; } - if (ch == 6) { /* Ctrl+F: attach a file */ + if (ch == 6) { /* ^F: attach a file */ char *path = file_browser(a, a->attachment_dir); if (path) { struct stat sb; @@ -2098,7 +2174,7 @@ static int64_t vouchers_new(struct app *a) } continue; } - if (ch == KEY_F(7)) { + if (ch == KEY_CTRL_X) { if (field >= 3) { int ri = (field - 3) / 4; if (ri < nrows) { @@ -2112,7 +2188,7 @@ static int64_t vouchers_new(struct app *a) } /* build args for validate/post */ - int do_post = (ch == KEY_F(9)); + int do_post = (ch == KEY_CTRL_ENTER || ch == KEY_F(9)); int do_check = (ch == KEY_F(5)); if (do_post || do_check) { yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); @@ -2315,7 +2391,7 @@ static void vouchers_screen(struct app *a) } free(idstr); free(resp); - items[n] = xstrdup("+ Nytt verifikat (Ctrl+N)"); + items[n] = xstrdup("+ Nytt verifikat (^N)"); } size_t total = n + 1; int start = 0; @@ -2328,7 +2404,7 @@ static void vouchers_screen(struct app *a) } int cur = start; int sel = select_list("Verifikat", items, (int)total, start, 1, &cur, - 1); + 1, 0, 0); if (cur >= 0 && (size_t)cur < n) a->voucher_sel = ids[cur]; if (sel == -2) { @@ -2553,6 +2629,22 @@ static void buf_line(struct buf *b, const char *fmt, ...) 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 - 2) + width = (int)sizeof line - 2; + if (width < 1) + return; + memset(line, '-', (size_t)width); + line[width] = '\n'; + buf_append(b, line, (size_t)width + 1); +} + +/* 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) { @@ -2595,6 +2687,7 @@ static void bs_col_head(struct buf *t) { buf_line(t, " %5s %-48s %14s %14s %14s %14s\n", "", "", "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, @@ -2620,7 +2713,7 @@ static int bs_rows(struct buf *t, yyjson_val *res, int lo, int hi, amt_col(a2, sizeof a2, 14, ib); amt_col(a3, sizeof a3, 14, per); amt_col(a4, sizeof a4, 14, ub); - buf_line(t, "%-6s %s %s %s %s %s\n", acc, nm, a1, a2, a3, a4); + buf_line(t, " %5s %s %s %s %s %s\n", acc, nm, a1, a2, a3, a4); } sum->ib += ib; sum->per += per; @@ -2679,18 +2772,25 @@ static void fmt_balans(struct buf *t, const struct app *a, yyjson_val *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, "TILLGÅNGAR\n"); - buf_line(t, "Anläggningstillgångar\n"); + bs_col_head(t); + buf_line(t, "\nAnläggningstillgångar\n"); bs_rows(t, res, 1000, 1399, &anl); bs_sum_line(t, "Summa anläggningstillgångar", &anl); buf_line(t, "\nOmsättningstillgångar\n"); + int printed = 0; for (size_t i = 0; i < sizeof OMS / sizeof OMS[0]; i++) { - if (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, "\nEGET KAPITAL OCH SKULDER\n"); @@ -2710,15 +2810,18 @@ static void fmt_balans(struct buf *t, const struct app *a, yyjson_val *res) 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]; - pad_label(lbl, sizeof lbl, "Beräknat resultat", 36); + pad_label(lbl, sizeof lbl, "Beräknat resultat", 48); amt_col(zero, sizeof zero, 14, 0); amt_col(a1, sizeof a1, 14, result); - buf_line(t, "\n\n%-6s %s %s %s %s %s\n", "", lbl, zero, zero, a1, a1); + buf_line(t, "\n"); + buf_rule(t, BS_RULE_W); + buf_line(t, " %5s %s %s %s %s %s\n", "", lbl, zero, zero, a1, a1); } /* ---------------- resultaträkning ---------------- */ @@ -2841,11 +2944,13 @@ static void fmt_resultat(struct buf *t, const struct app *a, yyjson_val *res, buf_line(t, " %5s %-48s %15s %15s %15s\n", "", gr->section, "Period", "Ackumulerat", "Föreg. per."); + buf_rule(t, IS_RULE_W); buf_line(t, "\n"); buf_line(t, "%s\n", gr->title); } sect = gr->section; } else { + buf_line(t, "\n"); buf_line(t, "%s\n", gr->title); } int64_t sc = 0, sp = 0; @@ -2903,10 +3008,14 @@ static void fmt_resultat(struct buf *t, const struct app *a, yyjson_val *res, } int64_t ror = rev + exp, ror_pre = rev_pre + exp_pre; int64_t after = ror + fin, after_pre = ror_pre + fin_pre; - is_summary(t, "Rörelseresultat", ror, ror_pre, 1); + 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); is_summary(t, "Resultat före skatt", after, after_pre, 1); - is_summary(t, "Årets resultat", after, after_pre, 1); + buf_line(t, "\n"); + buf_rule(t, IS_RULE_W); + is_summary(t, "Årets resultat", after, after_pre, 0); if (has_booked) { buf_line(t, "Bokfört resultat\n"); for (int k = 0; k < 2; k++) { @@ -2930,10 +3039,14 @@ static void fmt_resultat(struct buf *t, const struct app *a, yyjson_val *res, } } 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", after + booked, - after_pre + booked_pre, 2); + after_pre + booked_pre, 0); } else { - is_summary(t, "Ej bokfört resultat", after, after_pre, 2); + buf_line(t, "\n"); + buf_rule(t, IS_RULE_W); + is_summary(t, "Ej bokfört resultat", after, after_pre, 0); } } @@ -2945,6 +3058,7 @@ static void fmt_saldobalans(struct buf *t, const struct app *a, fmt_head(t, a, "Saldobalans", res); buf_line(t, " %5s %-48s %14s %14s %14s %14s\n", "Konto", "Benämning", "Ingående", "Debet", "Kredit", "Utgående"); + 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++) { @@ -2965,7 +3079,9 @@ static void fmt_saldobalans(struct buf *t, const struct app *a, amt_col(a2, sizeof a2, 14, vint(tot, "debit_ore")); amt_col(a3, sizeof a3, 14, vint(tot, "credit_ore")); amt_col(a4, sizeof a4, 14, vint(tot, "ub_ore")); - buf_line(t, "\n %5s %s %s %s %s %s\n", "", lbl, a1, a2, a3, a4); + buf_line(t, "\n"); + buf_rule(t, BS_RULE_W); + buf_line(t, " %5s %s %s %s %s %s\n", "", lbl, a1, a2, a3, a4); } /* ---------------- momsdeklaration ---------------- */ @@ -3056,10 +3172,9 @@ static void fmt_moms(struct buf *t, const struct app *a, yyjson_val *res) break; } } - char amt[32] = ""; + char amt[48] = ""; if (ore) - snprintf(amt, sizeof amt, "%lld", - (long long)(ore / 100)); /* Kapitas truncates öre */ + kr_whole(ore, amt, sizeof amt); buf_line(t, "%s. %-66s %14s\n", vr->box, vr->label, amt); } int64_t ore49 = 0; @@ -3071,9 +3186,11 @@ static void fmt_moms(struct buf *t, const struct app *a, yyjson_val *res) } } buf_line(t, "\nG. %s\n", vat_section_title("G.")); - buf_line(t, "49. %-66s %14lld\n", - "Moms att betala (+) eller att få tillbaka (-)", - (long long)(ore49 / 100)); + buf_rule(t, 87); + char amt49[48]; + kr_whole(ore49, amt49, sizeof amt49); + buf_line(t, "49. %-66s %14s\n", + "Moms att betala (+) eller att få tillbaka (-)", amt49); } static int64_t prev_fy_id(struct app *a) @@ -3105,22 +3222,124 @@ static int64_t prev_fy_id(struct app *a) 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, "Senaste ver.: %s%lld\n\n", vstr(lv, "series"), + (long long)vint(lv, "number")); + buf_line(t, "%-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%s %s\n\n", vstr(ao, "account"), vstr(ao, "name")); + char a1[48]; + amt_col(a1, sizeof a1, 16, ib); + buf_line(t, " %-59s %16s\n", "Ingående balans:", a1); + buf_line(t, " %-59s %16s\n", "Ingående saldo:", 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"); + pad_label(nm, sizeof nm, txt, 40); + amt_col(dd, sizeof dd, 14, vint(ro, "debit_ore")); + 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 : ""); + 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]; + amt_col(s1, sizeof s1, 14, d); + amt_col(s2, sizeof s2, 14, c); + amt_col(s3, sizeof s3, 16, d - c); + buf_line(t, " %-59s %14s %14s %16s\n", "Omslutning:", s1, s2, + s3); + amt_col(s3, sizeof s3, 16, vint(ao, "ub_ore")); + buf_line(t, " %-59s %14s %14s %16s\n", "Utgående saldo:", 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, "Senaste ver.: %s%lld\n\n", vstr(lv, "series"), + (long long)vint(lv, "number")); + buf_line(t, "%-8s %-10s %-48s %14s %14s\n", "Ver.", "Datum/Konto", + "Benämning", "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, "%-8s %-10s %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]; + pad_label(nm, sizeof nm, vstr(ro, "name"), 48); + amt_col(dd, sizeof dd, 14, vint(ro, "debit_ore")); + 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]; + amt_col(s1, sizeof s1, 14, vint(tot, "debit_ore")); + amt_col(s2, sizeof s2, 14, vint(tot, "credit_ore")); + buf_line(t, "\n%-68s %14s %14s\n", "Omslutning:", s1, s2); +} + 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 = menu("Rapporter", report_items, 5, 0); + int sel = menu("Rapporter", report_items, 7, 0, &last_sel); if (sel < 0) return; - if (sel == 4) { + if (sel == 6) { accounts_report(a); continue; } @@ -3143,7 +3362,17 @@ static void reports_screen(struct app *a) snprintf(args, sizeof args, "{\"fiscal_year\":%lld}", (long long)a->fy); break; - case 3: { + 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 *from = date_prompt("Från (YYYY-MM-DD): ", a->fy_start); if (!from) @@ -3196,6 +3425,10 @@ static void reports_screen(struct app *a) 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); } @@ -3250,7 +3483,7 @@ static void inbox_screen(struct app *a) mvaddnstr(2 + (int)(i - (size_t)top), 2, items[i], COLS - 4); if (n == 0) mvaddstr(2, 2, "Inga obokförda underlag."); - hints("a/Ctrl+N = lägg till fil PgUp/PgDn, Home/End rullar F5 =" + hints("a/^N = lägg till fil PgUp/PgDn, Home/End rullar F5 =" " uppdatera Esc/q = tillbaka"); refresh(); int ch = ui_getch(); @@ -3324,10 +3557,11 @@ static void audit_screen(struct app *a) { static const char *const audit_items[] = { "Verifiera hashkedjan", "Senaste händelser" }; + static int last_sel = 0; for (;;) { if (g_quit) return; - int sel = menu("Revision", audit_items, 2, 0); + int sel = menu("Revision", audit_items, 2, 0, &last_sel); if (sel < 0) return; if (sel == 0) { @@ -3597,7 +3831,7 @@ static int template_form(struct app *a, const char *load_name) "x = belopp i kronor. Positivt blir debet, negativt kredit.", COLS - 4); attroff(A_DIM); - hints("Tab = byta fält F5 = validera F7 = rensa rad F9 = spara Esc = avbryt"); + hints("Tab = byta fält F5 = validera ^X = rensa rad ^Enter = spara Esc = avbryt"); if (caret_y >= 0) move(caret_y, caret_x); refresh(); @@ -3639,7 +3873,7 @@ static int template_form(struct app *a, const char *load_name) } continue; } - if (ch == KEY_F(7)) { + if (ch == KEY_CTRL_X) { if (field >= 3) { int ri = (field - 3) / 3; if (ri < nrows) { @@ -3653,7 +3887,7 @@ static int template_form(struct app *a, const char *load_name) } /* validation and save */ - if (ch == KEY_F(5) || ch == KEY_F(9)) { + if (ch == KEY_F(5) || ch == KEY_CTRL_ENTER || ch == KEY_F(9)) { const char *problem = NULL; char msg[256] = ""; if (!tname[0]) { @@ -3768,10 +4002,12 @@ static int template_form(struct app *a, const char *load_name) } } -static void template_archive_ui(struct app *a) +static void template_archive(struct app *a, const char *name) { - char *name = prompt("Mall att arkivera: ", "", 0); - if (!name || !*name) + char label[256]; + snprintf(label, sizeof label, "Arkivera '%s'? (j/n): ", name); + char *ans = prompt(label, "n", 0); + 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); @@ -3971,7 +4207,7 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) attroff(A_DIM); } } - hints("Tab = byta fält F5 = validera F7 = rensa rad F9 = spara Esc = avbryt"); + hints("Tab = byta fält F5 = validera ^X = rensa rad ^Enter = spara Esc = avbryt"); if (caret_y >= 0) move(caret_y, caret_x); refresh(); @@ -4007,7 +4243,7 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) } continue; } - if (ch == KEY_F(7)) { + if (ch == KEY_CTRL_X) { int ri = field / 3; if (ri < nrows) { memset(&rows[ri], 0, sizeof rows[ri]); @@ -4018,7 +4254,7 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) continue; } - if (ch == KEY_F(5) || ch == KEY_F(9)) { + if (ch == KEY_F(5) || ch == KEY_CTRL_ENTER || ch == KEY_F(9)) { const char *problem = NULL; char msg[256] = ""; int64_t sum = 0; @@ -4227,7 +4463,7 @@ static void ib_screen(struct app *a) attron(A_BOLD); mvprintw(LINES - 4, 2, "Summa: %s", sumbuf); attroff(A_BOLD); - hints("e/Ctrl+N = redigera PgUp/PgDn, Home/End rullar F5 =" + hints("e/^N = redigera PgUp/PgDn, Home/End rullar F5 =" " uppdatera Esc/q = tillbaka"); refresh(); int ch = ui_getch(); @@ -4353,9 +4589,9 @@ static void company_screen(struct app *a) } hints(owner ? "upp/ned Enter = ändra F5 = uppdatera" - " Esc/q = tillbaka Ctrl+C = avsluta" + " Esc/q = tillbaka ^C = avsluta" : "upp/ned F5 = uppdatera Esc/q = tillbaka" - " Ctrl+C = avsluta (endast ägare kan ändra)"); + " ^C = avsluta (endast ägare kan ändra)"); refresh(); int ch = ui_getch(); if (ch == KEY_UP && sel > 0) @@ -4458,7 +4694,7 @@ static void settings_screen(struct app *a) attroff(A_REVERSE); } hints("upp/ned Enter = ändra F5 = uppdatera Esc/q = tillbaka" - " Ctrl+C = avsluta"); + " ^C = avsluta"); refresh(); int ch = ui_getch(); if (ch == KEY_UP && sel > 0) @@ -4516,81 +4752,76 @@ static void settings_screen(struct app *a) static void templates_screen(struct app *a) { - static const char *const items[] = { "Mallar (lista/redigera)", "Ny mall", - "Arkivera mall" }; + int cursor = 0; for (;;) { if (g_quit) return; - int sel = menu("Mallar", items, 3, 1); - if (sel == -4) { - template_form(a, NULL); - continue; - } - if (sel < 0) + 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; - if (sel == 1) { - template_form(a, NULL); - continue; - } - if (sel == 2) { - template_archive_ui(a); - continue; } - for (;;) { - 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"); - if (n == 0) { - message("Mallar", "Inga mallar ännu."); - free(resp); - break; - } - char **names = xcalloc(n, sizeof(char *)); - char **lines = xcalloc(n, 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 : ""); - 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 s = select_list("Mallar", lines, (int)n, 0, 1, NULL, 0); - if (s >= 0) - template_form(a, names[s]); - for (size_t i = 0; i < n; i++) { + 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 : ""); + 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 = select_list("Mallar", lines, (int)n + 1, cursor, 1, &cursor, 1, + n > 0, 0); + if (s == -1) { + for (size_t i = 0; i <= n; i++) { free(names[i]); free(lines[i]); } free(names); free(lines); free(resp); - if (s == -2) - continue; - break; + 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; } } -/* Screen names for Ctrl+R restoration and --screen. Only the top-level +/* 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) { @@ -4647,16 +4878,17 @@ static void do_reload(struct app *a, const char *self) static const char *const MAIN_ITEMS[] = { "Verifikat", "Underlag (inkorg)", "Ingående balans", "Mallar", "Rapporter", "Revision", - "Inställningar", "Företagsuppgifter", "Byt räkenskapsår", + "Inställningar", "Företagsuppgifter", "Räkenskapsår", "Logga ut / avsluta", }; static void dashboard(struct app *a) { + static int last_sel = 0; for (;;) { if (g_reload) return; - int sel = menu("Bokf", MAIN_ITEMS, 10, 1); + int sel = menu("Bokf", MAIN_ITEMS, 10, 1, &last_sel); snprintf(g_scene, sizeof g_scene, "%s", "dashboard"); if (g_quit || g_reload) return; @@ -4892,7 +5124,7 @@ static int login_screen(struct app *a) if (field == 3) attroff(A_REVERSE | A_BOLD); } - hints("Tab = byta fält/knapp Enter = logga in Ctrl+C = avsluta"); + hints("Tab = byta fält/knapp Enter = logga in ^C = avsluta"); refresh(); if (field == 3) { @@ -5012,7 +5244,7 @@ static int select_org(struct app *a) free(role); } int sel = select_list("Välj organisation att representera", items, (int)n, - 0, 0, NULL, 0); + 0, 0, NULL, 0, 0, 0); for (size_t i = 0; i < n; i++) free(items[i]); free(items); @@ -5038,6 +5270,12 @@ static void usage(FILE *f) " --version\n"); } +static void reset_modify_keys(void) +{ + putp("\033[>4m"); + fflush(stdout); +} + int main(int argc, char **argv) { struct app app; @@ -5092,6 +5330,12 @@ int main(int argc, char **argv) cbreak(); noecho(); keypad(stdscr, TRUE); + /* ^Enter has no control code; enable xterm modifyOtherKeys and bind the + sequence it produces. Terminals without it ignore the request. */ + define_key("\033[27;5;13~", KEY_CTRL_ENTER); + putp("\033[>4;1m"); + fflush(stdout); + atexit(reset_modify_keys); curs_set(1); const char *env_session = getenv("BOKFD_SESSION"); -- cgit v1.3