From 77c298b3b64f2493e509734df0084068ad24441f Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Sat, 19 Sep 2026 20:13:02 +0200 Subject: bokslut: TUI screen, K2 årsredovisning draft, storable dividend (schema v4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- clients/bokftui.c | 782 +++++++++++++++++++++++++++++++++++++++++++++++++++++- docs/PROTOCOL.md | 10 +- docs/SCHEMA.md | 1 + docs/STATE.md | 31 ++- src/commands.c | 70 ++++- src/db.c | 14 + src/db.h | 2 +- tests/test_core.c | 22 ++ 8 files changed, 910 insertions(+), 22 deletions(-) diff --git a/clients/bokftui.c b/clients/bokftui.c index 33b21de..69fe3a3 100644 --- a/clients/bokftui.c +++ b/clients/bokftui.c @@ -147,6 +147,9 @@ 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 acct_cache_free(void); static void acct_cache_load(struct app *a); @@ -3910,13 +3913,14 @@ static void reports_screen(struct app *a) "Verifikationslista", "Momsdeklaration", "Inkomstdeklaration (INK2/SRU)", + "Årsredovisning (K2)", "Kontolista (alla konton)", }; static int last_sel = 0; for (;;) { if (g_quit) return; - int sel = menu("Rapporter", report_items, 8, 0, &last_sel); + int sel = menu("Rapporter", report_items, 9, 0, &last_sel); if (sel < 0) return; if (sel == 6) { @@ -3954,6 +3958,10 @@ static void reports_screen(struct app *a) continue; } if (sel == 7) { + arsredovisning_screen(a); + continue; + } + if (sel == 8) { accounts_report(a); continue; } @@ -4652,12 +4660,13 @@ static int template_form(struct app *a, const char *load_name) rows[i].text); } yyjson_mut_obj_add_val(d, o, "rows", arr); - if (ch == KEY_F(5)) - yyjson_mut_obj_add_bool(d, o, "dry_run", true); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); const char *cmd = tpl_id ? "template.update" : "template.create"; - char *resp = client_rpc(&a->conn, cmd, a->session, a->org, args); + char *resp = ch == KEY_F(5) + ? rpc_dry(a, cmd, args) + : client_rpc(&a->conn, cmd, a->session, a->org, + args); free(args); if (resp && client_ok(resp)) { if (ch == KEY_F(5)) { @@ -5533,6 +5542,755 @@ static void templates_screen(struct app *a) } } +/* ------------------------------------------------------------------ */ +/* 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, "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%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]; + amt_col(a1, sizeof a1, 15, vint(r, "debit_ore")); + 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 (parse_kr(fond, &fond_ore) != 0) { + message("Bokslut", "Ogiltigt fondbelopp: %s", fond); + return; + } + if (parse_kr(rate, &rate_ore) != 0 || rate_ore < 0 || rate_ore > 10000) { + 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); + text_view("Bokslut - plan (torrkörning)", (const char *)t.p, NULL); + buf_free(&t); + yyjson_doc_free(d); + free(resp); + if (!do_post) + return; + char *ans = prompt("Bokför planen? (j/n): ", "n", 0); + 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")); + } + message("Bokslut", "%s", (const char *)m.p); + buf_free(&m); + yyjson_doc_free(pd); + free(presp); +} + +static void bokslut_screen(struct app *a) +{ + char fond[32] = "0,00"; + char rate[16] = "20,6"; + int sel = 0; + for (;;) { + if (g_quit) + return; + frame("Bokslut"); + attron(A_BOLD); + mvaddstr(4, 2, "Fält"); + mvaddstr(4, 34, "Värde"); + attroff(A_BOLD); + static const char *const labels[2] = { + "Periodiseringsfond (kr)", "Skattesats (%)" + }; + const char *vals[2] = { fond, rate }; + for (int i = 0; i < 2; i++) { + char lbl[64], line[128]; + snprintf(lbl, sizeof lbl, "%s", labels[i]); + pad_field(lbl, sizeof lbl, 31); + snprintf(line, sizeof line, "%s %s", lbl, vals[i]); + if (i == sel) + attron(A_REVERSE); + mvaddnstr(6 + i, 2, line, COLS - 4); + if (i == sel) + attroff(A_REVERSE); + } + mvaddstr(9, 2, "Fondbelopp i kr, tomt = ingen avsättning."); + mvaddstr(10, 2, "Skattesatsen tillämpas på resultat efter" + " bokslutsdispositioner."); + hints("upp/ned Enter = ändra F5 = visa plan ^Enter = bokför" + " Esc/q = tillbaka ^C = avsluta"); + refresh(); + int ch = ui_getch(); + if (ch == KEY_UP && sel > 0) + sel--; + else if (ch == KEY_DOWN && sel < 1) + sel++; + else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) { + char label[80]; + snprintf(label, sizeof label, "%s: ", labels[sel]); + char *v = prompt(label, vals[sel], 0); + if (!v) + continue; + int64_t ore = 0; + if (parse_kr(v, &ore) != 0 || + (sel == 1 && (ore < 0 || ore > 10000))) { + message("Bokslut", "Ogiltigt värde: %s", v); + continue; + } + if (sel == 0) + snprintf(fond, sizeof fond, "%s", v); + else + snprintf(rate, sizeof rate, "%s", v); + } else if (ch == KEY_F(5)) { + bokslut_plan(a, fond, rate, 0); + } else if (ch == KEY_CTRL_ENTER || ch == KEY_F(9)) { + bokslut_plan(a, fond, rate, 1); + } else if (ch == 27 || ch == 'q') { + return; + } + } +} + +/* ------------------------------------------------------------------ */ +/* K2 årsredovisning (draft text) */ +/* ------------------------------------------------------------------ */ + +static void ar_amounts(struct buf *t, const char *acct, const char *label, + int64_t cur, int64_t prev) +{ + char nm[160], a1[48], a2[48]; + pad_label(nm, sizeof nm, label, 48); + amt_col(a1, sizeof a1, 15, cur); + amt_col(a2, sizeof a2, 15, prev); + buf_line(t, " %-5s %s %s %s\n", acct ? acct : "", nm, a1, a2); +} + +static int64_t ar_is_sum(yyjson_val *res, int lo, int hi) +{ + int64_t s = 0; + if (!res) + return 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); + if (!acct_in(vstr(row, "account"), lo, hi)) + continue; + int64_t a = vint(row, "amount_ore"); + s += k == 0 ? a : -a; + } + } + return s; +} + +static void ar_is_rows(struct buf *t, yyjson_val *res, yyjson_val *prev, + int lo, int hi, int64_t *cur, int64_t *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); + ar_amounts(t, acc, vstr(row, "name"), c, p); + *cur += c; + *pre += p; + } + } +} + +static void ar_is_group(struct buf *t, yyjson_val *res, yyjson_val *prev, + const struct is_group *gr, int64_t *cur, + int64_t *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, 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); + ar_amounts(t, acc, vstr(row, "name"), c, p); + *cur += c; + *pre += p; + } + } +} + +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; +} + +/* Balance-sheet amounts are shown the way an annual report reads them: + credit balances (equity, liabilities) as positive numbers. */ +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; +} + +static int ar_bs_rows(struct buf *t, yyjson_val *res, int lo, int hi, + int64_t *cur, int64_t *pre) +{ + 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 ub = vint(row, "ub_ore"), ib = vint(row, "ib_ore"); + if (lo >= 2000) { + ub = -ub; + ib = -ib; + } + if (t) + ar_amounts(t, acc, vstr(row, "name"), ub, ib); + if (cur) + *cur += ub; + if (pre) + *pre += ib; + found = 1; + } + } + return found; +} + +static int ar_bs_group(struct buf *t, yyjson_val *res, const char *title, + const char *sumlabel, int lo, int hi, int64_t *cur, + int64_t *pre) +{ + int64_t c = 0, p = 0; + if (!ar_bs_rows(NULL, res, lo, hi, &c, &p)) + return 0; + buf_line(t, "%s\n", title); + ar_bs_rows(t, res, lo, hi, NULL, NULL); + ar_amounts(t, "", sumlabel, c, p); + if (cur) + *cur += c; + if (pre) + *pre += p; + return 1; +} + +static int64_t ar_is_result(yyjson_val *is) +{ + return ar_is_sum(is, 3000, 3999) + ar_is_sum(is, 4000, 8989); +} + +static int64_t ar_resultat(struct buf *t, yyjson_val *is, yyjson_val *prev, + const char *cur_lbl, const char *prev_lbl) +{ + buf_line(t, "\nResultaträkning\n\n"); + char h1[32], h2[32], blank[64]; + pad_label(blank, sizeof blank, "", 48); + pad_hdr(h1, sizeof h1, 15, cur_lbl); + pad_hdr(h2, sizeof h2, 15, prev_lbl); + buf_line(t, " %5s %s %s %s\n", "", blank, h1, h2); + buf_rule(t, 88); + const char *sect = NULL; + int64_t rev = 0, rev_pre = 0, exp = 0, exp_pre = 0; + int rev_groups = 0, exp_groups = 0, rev_summed = 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(is, gr)) + continue; + if (!sect || strcmp(sect, gr->section) != 0) { + if (sect && rev_groups && !rev_summed) { + ar_amounts(t, "", "Summa rörelseintäkter," + " lagerförändringar m.m.", rev, rev_pre); + rev_summed = 1; + buf_line(t, "\n"); + } + buf_line(t, "%s\n", gr->section); + sect = gr->section; + } + buf_line(t, "%s\n", gr->title); + int64_t sc = 0, sp = 0; + ar_is_group(t, is, prev, gr, &sc, &sp); + if (gr->rev) { + rev += sc; + rev_pre += sp; + rev_groups++; + } else { + exp += sc; + exp_pre += sp; + exp_groups++; + } + } + if (rev_groups && !rev_summed) + ar_amounts(t, "", "Summa rörelseintäkter, lagerförändringar m.m.", + rev, rev_pre); + if (exp_groups) + ar_amounts(t, "", "Summa rörelsekostnader", exp, exp_pre); + int64_t ror = rev + exp, ror_pre = rev_pre + exp_pre; + int64_t fin = 0, fin_pre = 0, disp = 0, disp_pre = 0; + int64_t tax = 0, tax_pre = 0; + if (is_range_has(is, 8000, 8599)) { + buf_line(t, "\nFinansiella poster\n"); + ar_is_rows(t, is, prev, 8000, 8599, &fin, &fin_pre); + ar_amounts(t, "", "Summa", fin, fin_pre); + } + int64_t after = ror + fin, after_pre = ror_pre + fin_pre; + buf_line(t, "\n"); + buf_rule(t, 88); + ar_amounts(t, "", "Rörelseresultat", ror, ror_pre); + ar_amounts(t, "", "Resultat efter finansiella poster", after, after_pre); + if (is_range_has(is, 8800, 8899)) { + buf_line(t, "\nBokslutsdispositioner\n"); + ar_is_rows(t, is, prev, 8800, 8899, &disp, &disp_pre); + ar_amounts(t, "", "Summa", disp, disp_pre); + } + int64_t btax = after + disp, btax_pre = after_pre + disp_pre; + ar_amounts(t, "", "Resultat före skatt", btax, btax_pre); + if (is_range_has(is, 8900, 8989)) { + buf_line(t, "\nSkatt på årets resultat\n"); + ar_is_rows(t, is, prev, 8900, 8989, &tax, &tax_pre); + } + int64_t ars = btax + tax, ars_pre = btax_pre + tax_pre; + buf_line(t, "\n"); + buf_rule(t, 88); + ar_amounts(t, "", "Årets resultat", ars, ars_pre); + return ars; +} + +static int64_t ar_equity(yyjson_val *bs, int64_t result) +{ + return ar_bs_sum(bs, 2000, 2090) + ar_bs_sum(bs, 2091, 2098) + result; +} + +static void ar_balans(struct buf *t, yyjson_val *bs, const char *cur_lbl, + const char *prev_lbl, int64_t ars, int64_t ars_pre) +{ + buf_line(t, "\nBalansräkning\n\n"); + char h1[32], h2[32], blank[64]; + pad_label(blank, sizeof blank, "", 48); + pad_hdr(h1, sizeof h1, 15, cur_lbl); + pad_hdr(h2, sizeof h2, 15, prev_lbl); + buf_line(t, " %5s %s %s %s\n", "", blank, h1, h2); + buf_rule(t, 88); + buf_line(t, "TILLGÅNGAR\n"); + int64_t anl = 0, anl_p = 0, oms = 0, oms_p = 0; + ar_bs_group(t, bs, "Anläggningstillgångar", + "Summa anläggningstillgångar", 1000, 1399, &anl, &anl_p); + buf_line(t, "\nOmsättningstillgångar\n"); + 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 }, + }; + for (size_t i = 0; i < sizeof OMS / sizeof OMS[0]; i++) + ar_bs_group(t, bs, OMS[i].title, OMS[i].sum, OMS[i].lo, OMS[i].hi, + &oms, &oms_p); + ar_amounts(t, "", "Summa omsättningstillgångar", oms, oms_p); + int64_t tok = anl + oms, tok_p = anl_p + oms_p; + buf_line(t, "\n"); + buf_rule(t, 88); + ar_amounts(t, "", "SUMMA TILLGÅNGAR", tok, tok_p); + buf_line(t, "\nEGET KAPITAL OCH SKULDER\n"); + int64_t eget = 0, eget_p = 0; + ar_bs_group(t, bs, "Bundet eget kapital", "Summa bundet eget kapital", + 2000, 2090, &eget, &eget_p); + buf_line(t, "\n"); + int64_t free_c = 0, free_p = 0; + ar_bs_rows(NULL, bs, 2091, 2098, &free_c, &free_p); + buf_line(t, "Fritt eget kapital\n"); + ar_bs_rows(t, bs, 2091, 2098, NULL, NULL); + ar_amounts(t, "", "Årets resultat", ars, ars_pre); + free_c += ars; + free_p += ars_pre; + ar_amounts(t, "", "Summa fritt eget kapital", free_c, free_p); + eget += free_c; + eget_p += free_p; + buf_line(t, "\n"); + ar_amounts(t, "", "Summa eget kapital", eget, eget_p); + int64_t skuld = 0, skuld_p = 0; + buf_line(t, "\n"); + if (ar_bs_group(t, bs, "Obeskattade reserver", + "Summa obeskattade reserver", 2100, 2199, &skuld, + &skuld_p)) + buf_line(t, "\n"); + if (ar_bs_group(t, bs, "Avsättningar", "Summa avsättningar", 2200, 2299, + &skuld, &skuld_p)) + buf_line(t, "\n"); + if (ar_bs_group(t, bs, "Långfristiga skulder", + "Summa långfristiga skulder", 2300, 2399, &skuld, + &skuld_p)) + buf_line(t, "\n"); + ar_bs_group(t, bs, "Kortfristiga skulder", "Summa kortfristiga skulder", + 2400, 2999, &skuld, &skuld_p); + buf_line(t, "\n"); + buf_rule(t, 88); + ar_amounts(t, "", "SUMMA EGET KAPITAL OCH SKULDER", eget + skuld, + eget_p + skuld_p); +} + +static void fmt_arsredovisning(struct buf *t, const struct app *a, + const char *org_nr, const char *city, + yyjson_val *is, yyjson_val *prev_is, + yyjson_val *bs, int64_t dividend) +{ + const char *cur_lbl = vstr(yyjson_obj_get(is, "fiscal_year"), "label"); + const char *prev_lbl = + prev_is ? vstr(yyjson_obj_get(prev_is, "fiscal_year"), "label") : ""; + int64_t ars = ar_is_result(is); + int64_t prev_ars = ar_is_result(prev_is); + buf_line(t, "ÅRSREDOVISNING\n\n%s\nOrganisationsnummer %s\n", + a->org_name, org_nr); + buf_line(t, "\nRäkenskapsåret %s - %s\n", a->fy_start, a->fy_end); + buf_line(t, "\nFörvaltningsberättelse\n\nVerksamhet\n"); + buf_line(t, "Bolaget bedriver [kort beskrivning av verksamheten]." + " Verksamheten bedrivs från %s.\n", city); + buf_line(t, "\nVäsentliga händelser under räkenskapsåret\n"); + buf_line(t, "[Beskriv väsentliga händelser, eller skriv att inga" + " väsentliga händelser har inträffat.]\n"); + buf_line(t, "\nEkonomisk översikt\n\n"); + buf_line(t, " %-40s %15s %15s\n", "", cur_lbl, prev_lbl); + buf_rule(t, 72); + ar_amounts(t, "", "Nettoomsättning", ar_is_sum(is, 3000, 3799), + prev_is ? ar_is_sum(prev_is, 3000, 3799) : 0); + int64_t ror = ar_is_sum(is, 3000, 3999) + ar_is_sum(is, 4000, 7999); + int64_t ror_p = prev_is ? ar_is_sum(prev_is, 3000, 3999) + + ar_is_sum(prev_is, 4000, 7999) + : 0; + ar_amounts(t, "", "Rörelseresultat", ror, ror_p); + ar_amounts(t, "", "Årets resultat", ars, prev_ars); + ar_amounts(t, "", "Balansomslutning", + vint(yyjson_obj_get(bs, "assets"), "total_ore"), + ar_bs_ib_sum(bs, 1000, 1999)); + ar_amounts(t, "", "Eget kapital", ar_equity(bs, ars), + ar_bs_ib_sum(bs, 2000, 2090) + ar_bs_ib_sum(bs, 2091, 2098) + + prev_ars); + if (prev_is && prev_ars == 0 && ar_is_sum(prev_is, 3000, 3799) == 0) + buf_line(t, "\n[Jämförelsetalen för %s är inte kompletta i den" + " löpande bokföringen\n(historikåret importerades med" + " källsystemets stängningsverifikat). Hämta dem från\n" + "föregående års årsredovisning.]\n", prev_lbl); + int64_t disponibelt = ar_bs_sum(bs, 2091, 2098) + ars; + buf_line(t, "\nFörslag till disposition av bolagets resultat\n\n"); + ar_amounts(t, "", "Balanserade vinstmedel", ar_bs_sum(bs, 2091, 2098), + 0); + ar_amounts(t, "", "Årets resultat", ars, 0); + ar_amounts(t, "", "Summa", disponibelt, 0); + { + char tmpd[48], tmpr[48]; + kr_format(dividend, tmpd, sizeof tmpd); + kr_format(disponibelt - dividend, tmpr, sizeof tmpr); + if (dividend > 0) + buf_line(t, "\nStyrelsen föreslår att vinstmedlen disponeras så" + " att\n till aktieägarna utdelas %s kr och\n" + " i ny räkning överförs %s kr.\n", + tmpd, tmpr); + else + buf_line(t, "\nStyrelsen föreslår att vinstmedlen disponeras så" + " att i ny räkning överförs %s kr.\n", tmpr); + } + ar_resultat(t, is, prev_is, cur_lbl, prev_lbl); + ar_balans(t, bs, cur_lbl, prev_lbl, ars, prev_ars); + buf_line(t, "\nNoter\n\nNot 1 Redovisningsprinciper\n"); + buf_line(t, "Årsredovisningen har upprättats enligt" + " årsredovisningslagen och BFNAR 2016:10 Årsredovisning i" + " mindre företag (K2). Tillgångar och skulder värderas till" + " anskaffningsvärde.\n"); + buf_line(t, "\nNot 2 Medelantal anställda\n"); + buf_line(t, "Medelantalet anställda under räkenskapsåret har uppgått" + " till [antal].\n"); + int64_t fond_avs = -(ar_is_sum(is, 8811, 8811)); + if (fond_avs != 0 || ar_bs_sum(bs, 2110, 2199) != 0) { + buf_line(t, "\nNot 3 Periodiseringsfond\n"); + buf_line(t, "Årets avsättning till periodiseringsfond uppgår till" + " %lld kr. Utgående balans uppgår till %lld kr.\n", + (long long)(fond_avs / 100), + (long long)(ar_bs_sum(bs, 2110, 2199) / 100)); + } + buf_line(t, "\nUnderskrifter\n\n%s den [datum]\n\n", city); + buf_line(t, "____________________________ " + "____________________________\n"); + buf_line(t, "[Namn], styrelseledamot " + "[Namn], styrelseledamot\n"); + buf_line(t, "\nRevisionsberättelse\n"); + buf_line(t, "[Bolaget har inte någon revisor; någon revisionsberättelse" + " har därför inte lämnats.]\n"); +} + +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 *dir = prompt("Spara årsredovisningen i: ", def, 0); + 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, "/"); + snprintf(full + strlen(full), sizeof full - strlen(full), + "Årsredovisning %.4s-%.4s.txt", a->fy_start, a->fy_end); + FILE *f = fopen(full, "wb"); + size_t n = strlen(text); + if (!f || fwrite(text, 1, n, f) != n) { + if (f) + fclose(f); + message("Årsredovisning", "Kunde inte skriva %s: %s", full, + strerror(errno)); + return; + } + fclose(f); + message("Årsredovisning", "Sparat: %s\n\nFilen är ett utkast i textform" + " att redigera vidare (t.ex. i Word).", full); +} + +static void arsredovisning_screen(struct app *a) +{ + int have_div = 0; + int64_t div_ore = 0; + for (;;) { + char args[64]; + snprintf(args, sizeof args, "{\"fiscal_year\":%lld}", + (long long)a->fy); + char *iresp = + client_rpc(&a->conn, "report.income_statement", a->session, + a->org, args); + char *bresp = client_rpc(&a->conn, "report.balance_sheet", a->session, + a->org, args); + char *oresp = + client_rpc(&a->conn, "org.get", a->session, a->org, "{}"); + if (!iresp || !client_ok(iresp) || !bresp || !client_ok(bresp)) { + show_error("Årsredovisning", + iresp && !client_ok(iresp) ? iresp : bresp); + free(iresp); + free(bresp); + free(oresp); + return; + } + char *pis_resp = NULL; + int64_t pf = prev_fy_id(a); + if (pf) { + char pargs[64]; + snprintf(pargs, sizeof pargs, "{\"fiscal_year\":%lld}", + (long long)pf); + pis_resp = client_rpc(&a->conn, "report.income_statement", + a->session, a->org, pargs); + } + yyjson_doc *id = parse(iresp), *bd = parse(bresp); + yyjson_doc *pid = pis_resp ? parse(pis_resp) : NULL; + yyjson_val *is = id ? jget(yyjson_doc_get_root(id), "result") : NULL; + yyjson_val *bs = bd ? jget(yyjson_doc_get_root(bd), "result") : NULL; + yyjson_val *pis = + pid ? jget(yyjson_doc_get_root(pid), "result") : NULL; + char *onr = oresp ? jstr_dup(oresp, "result.org_nr") : NULL; + char *city = oresp ? jstr_dup(oresp, "result.city") : NULL; + if (is && bs && !have_div) { + char fargs[48]; + snprintf(fargs, sizeof fargs, "{\"id\":%lld}", (long long)a->fy); + char *fresp = client_rpc(&a->conn, "fiscal_year.get", a->session, + a->org, fargs); + if (fresp && client_ok(fresp)) + div_ore = jint_val(fresp, "result.dividend_ore", 0); + free(fresp); + have_div = 1; + int64_t dispon = ar_bs_sum(bs, 2091, 2098) + ar_is_result(is); + char cur[48]; + kr_format(div_ore, cur, sizeof cur); + char *val = prompt("Föreslagen utdelning (kr): ", cur, 0); + if (val) { + int64_t v = 0; + char dtmp[48]; + kr_format(dispon, dtmp, sizeof dtmp); + if (parse_kr(val, &v) != 0) + message("Årsredovisning", "Ogiltigt belopp: %s", val); + else if (v > dispon) + message("Årsredovisning", "Utdelningen överstiger" + " disponibla medel (%s kr).", + dtmp); + else if (v != div_ore) { + char uargs[96]; + snprintf(uargs, sizeof uargs, + "{\"id\":%lld,\"dividend_ore\":%lld}", + (long long)a->fy, (long long)v); + char *uresp = client_rpc(&a->conn, "fiscal_year.update", + a->session, a->org, uargs); + if (uresp && client_ok(uresp)) + div_ore = v; + else + show_error("Årsredovisning", uresp); + free(uresp); + } + } + } + struct buf t; + buf_init(&t); + if (is && bs) + fmt_arsredovisning(&t, a, onr && *onr ? onr : "[org.nr]", + city && *city ? city : "[Ort]", is, pis, bs, + div_ore); + else + buf_line(&t, "Kunde inte läsa rapporterna."); + buf_append(&t, "", 1); + int again = + text_view("Å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); + yyjson_doc_free(id); + yyjson_doc_free(bd); + yyjson_doc_free(pid); + free(iresp); + free(bresp); + free(oresp); + free(pis_resp); + 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) @@ -5554,6 +6312,8 @@ static void open_scene(struct app *a, const char *scene) settings_screen(a); else if (strcmp(scene, "company") == 0) company_screen(a); + else if (strcmp(scene, "bokslut") == 0) + bokslut_screen(a); } static void do_reload(struct app *a, const char *self) @@ -5590,8 +6350,8 @@ 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", "Räkenskapsår", - "Logga ut / avsluta", + "Inställningar", "Företagsuppgifter", "Bokslut", + "Räkenskapsår", "Logga ut / avsluta", }; static void dashboard(struct app *a) @@ -5600,7 +6360,7 @@ static void dashboard(struct app *a) for (;;) { if (g_reload) return; - int sel = menu("Bokf", MAIN_ITEMS, 10, 1, &last_sel); + int sel = menu("Bokf", MAIN_ITEMS, 11, 1, &last_sel); snprintf(g_scene, sizeof g_scene, "%s", "dashboard"); if (g_quit || g_reload) return; @@ -5638,9 +6398,12 @@ static void dashboard(struct app *a) open_scene(a, "company"); break; case 8: - select_fiscal_year(a); + open_scene(a, "bokslut"); break; case 9: + select_fiscal_year(a); + break; + case 10: return; } } @@ -5978,7 +6741,8 @@ static void usage(FILE *f) " --fy ID select fiscal year directly\n" " --session ID reuse an open session (or BOKFD_SESSION)\n" " --screen NAME start in a view: dashboard, vouchers, inbox,\n" - " ib, templates, reports, audit, settings, company\n" + " ib, templates, reports, bokslut, audit,\n" + " settings, company\n" " --version\n"); } diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 8ee1b2c..6f617fd 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -288,6 +288,7 @@ Account `type` is one of `asset`, `liability`, `equity`, `revenue`, `expense`. | `fiscal_year.open` | `label`, `start_date`, `end_date` | owner | | `fiscal_year.close` | `id`, `confirm:true` | owner; audited | | `fiscal_year.reopen` | `id`, `confirm:true` | owner; undoes a close | +| `fiscal_year.update` | `id`, `dividend_ore` | bookkeeper; year metadata (proposed dividend), audited | | `period.lock` | `fiscal_year`, `until`, `reason?` | owner; `until` inclusive | | `period.unlock` | `fiscal_year`, `reason` | owner; audited with reason | @@ -470,8 +471,15 @@ commands. Implemented screens (0.1.0-dev): vouchers (Tab, dynamic rows, F7 clear row, F5 validate, F9 save); archive keeps the template but hides it from the list. - **Underlag** — inbox of unlinked attachments; `a` uploads a file. -- **Rapporter** — saldobalans, resultaträkning, balansräkning, moms and +- **Rapporter** — saldobalans, resultaträkning, balansräkning, moms, + inkomstdeklaration (INK2/SRU), årsredovisning (K2 text draft) and kontolista (all accounts with type, moms treatment, SRU and status). + `s` saves the SRU files, eSKD XML and the årsredovisning respectively. + The draft asks for the board's proposed dividend, kept per fiscal year + with `fiscal_year.update`. +- **Bokslut** — periodiseringsfond and tax rate fields; F5 shows the posting + plan as a `bokslut.post` dry run, `^Enter` (or F9) asks for confirmation + and posts the plan. - **Företagsuppgifter** — the org record (name, org number, VAT number, address, e-mail, phone, moms period, framework, fiscal-year start month), editable in place; only owners can save, others see it read-only. diff --git a/docs/SCHEMA.md b/docs/SCHEMA.md index db03e43..4fc4397 100644 --- a/docs/SCHEMA.md +++ b/docs/SCHEMA.md @@ -148,6 +148,7 @@ CREATE TABLE fiscal_years ( end_date TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open','closed')), locked_until TEXT, -- inclusive last locked date + dividend_ore INTEGER NOT NULL DEFAULT 0, -- board's proposed dividend created_at TEXT NOT NULL, closed_at TEXT, closed_by INTEGER REFERENCES users(id), diff --git a/docs/STATE.md b/docs/STATE.md index 5be11e7..b0f5b4a 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -1,7 +1,7 @@ # bokf — project state Snapshot for resuming work in a new session. Read with `AGENTS.md` (rules) -and `docs/TUI-GUIDELINES.md` (UI conventions). Dated 2026-09-18. +and `docs/TUI-GUIDELINES.md` (UI conventions). Dated 2026-09-19. ## Status @@ -86,8 +86,12 @@ server/protocol/ledger only. SHA-256 verified, text inline), attach to an existing voucher (`^F`), remove a link (`d` in the `f` picker) and link an inbox item to a voucher (`k`). -- Next backend milestone: **K2/SRU** (INK2/INK2R/INK2S + årsredovisning); - bokslut.post is done server-side, its TUI screen pending. +- K2/SRU is complete: `sru.export` with the INK2 view, `bokslut.post` with + the Bokslut screen, and a K2 årsredovisning text draft with a save action + in Rapporter. The draft asks for the board's proposed dividend and keeps it + per fiscal year (`fiscal_year.update`, schema v4); the stämma decision is + posted with a `Utdelning` template (D 2099/K 2898). Still missing before + filing: a command/TUI to edit the moms `report_rules` per org (below). - Moms `report_rules` seed is a corrected starter mapping (schema v3), but there is still no command/TUI to edit rules per org; add one before filing if the mapping needs adjustments (SCHEMA.md §10 promises owner editing). @@ -96,12 +100,17 @@ server/protocol/ledger only. 1. ~~eSKD file generation for momsdeklaration.~~ `report.vat_eskd` (eSKDUpload 6.0, ISO-8859-1, whole kronor) with a save action in the TUI momsrapport. -2. Bokslut automation: `bokslut.post` is in (entries/avskrivningar, +2. ~~Bokslut automation~~ done: `bokslut.post` (entries/avskrivningar, periodiseringsfond, skatt at a given rate, resultatdisposition, dry-run - plan, audited) and the resultatrapport separates bokslutsdispositioner - and skatt per K2. TUI screen still to do. -3. K2 årsredovisning document; ~~SRU files (INK2/INK2R/INK2S)~~ done as - `sru.export` (official 2025P4 field tables, BAS mapping, TUI save in + plan, audited), the TUI Bokslut screen (fond/rate, F5 plan, ^Enter posts + after confirmation) and the K2 resultatrapport order. +3. ~~K2 årsredovisning document~~ done as a text draft built in the TUI from + `report.income_statement`/`report.balance_sheet` (Förvaltningsberättelse, + K2 RR/BR with previous-year column and the result inside equity, noter, + underskrifter); `s` saves `Årsredovisning <år>.txt`. Placeholders mark + qualitative facts, and incomplete jämförelsetal for imported history + years are flagged in the document. ~~SRU files (INK2/INK2R/INK2S)~~ done + as `sru.export` (official 2025P4 field tables, BAS mapping, TUI save in Rapporter -> Inkomstdeklaration). 4. `audit.verify` must also verify the **voucher** hash chain (today only the audit chain is verified). @@ -119,8 +128,10 @@ server/protocol/ledger only. AGI/payroll if employees. 11a. Imported history years whose SIE contains the source's P&L closings ("Stäng intäktskonton/kostnadskonton") net to zero in the income - statement (Kapitas 2022-2026). The importer should skip those closing - verifications (keeping the resultatdisposition) or mark them; decide. + statement (Kapitas 2022-2026). **Decided 2026-09-19: no importer change.** + Locked years stay locked and the source's closings stay in the books; the + årsredovisning export flags incomplete jämförelsetal for those years and + points to the previous year's annual report. 11. ~~SIE import only into an empty fiscal year; consider broader import.~~ Chronological multi-year import works (CRLF, `#RAR 0`, zero rows, `#IB` rule handled); each year must still target an empty fiscal year. Note: diff --git a/src/commands.c b/src/commands.c index f03e0e4..62de0be 100644 --- a/src/commands.c +++ b/src/commands.c @@ -1696,10 +1696,12 @@ static yyjson_mut_val *fy_json(struct req *r, sqlite3_stmt *st) else yyjson_mut_obj_add_strcpy(r->rdoc, o, "locked_until", sq(sqlite3_column_text(st, 5))); + yyjson_mut_obj_add_int(r->rdoc, o, "dividend_ore", + sqlite3_column_int64(st, 6)); return o; } -#define FY_COLUMNS "id,label,start_date,end_date,status,locked_until" +#define FY_COLUMNS "id,label,start_date,end_date,status,locked_until,dividend_ore" static yyjson_mut_val *h_fiscal_year_list(struct req *r) { @@ -1878,6 +1880,70 @@ static yyjson_mut_val *h_fiscal_year_reopen(struct req *r) return yyjson_mut_obj(r->rdoc); } +/* Fiscal-year metadata that is not a posting: today only the board's + proposed dividend, kept for the årsredovisning draft. */ +static yyjson_mut_val *h_fiscal_year_update(struct req *r) +{ + int64_t id = 0, dividend = 0; + if (!arg_int(r->args, "id", &id) || id <= 0) + return fail(r, "INVALID_ARGS", "id is required"); + if (!arg_int(r->args, "dividend_ore", ÷nd)) + return fail(r, "INVALID_ARGS", "dividend_ore is required"); + if (dividend < 0) + return fail(r, "INVALID_ARGS", "dividend_ore must be >= 0"); + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2(r->db, + "SELECT count(*) FROM fiscal_years" + " WHERE org_id=?1 AND id=?2", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, id); + int64_t exists = 0; + if (sqlite3_step(st) == SQLITE_ROW) + exists = sqlite3_column_int64(st, 0); + sqlite3_finalize(st); + if (!exists) + return fail(r, "NOT_FOUND", "fiscal year not found"); + if (r->dry_run) { + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); + yyjson_mut_obj_add_int(r->rdoc, o, "id", id); + yyjson_mut_obj_add_int(r->rdoc, o, "dividend_ore", dividend); + return o; + } + if (sqlite3_prepare_v2(r->db, + "UPDATE fiscal_years SET dividend_ore=?3" + " WHERE org_id=?1 AND id=?2", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, id); + sqlite3_bind_int64(st, 3, dividend); + int rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, + "fiscal_year.update", reqjson, "OK", NULL); + free(reqjson); + if (sqlite3_prepare_v2(r->db, + "SELECT " FY_COLUMNS " FROM fiscal_years" + " WHERE org_id=?1 AND id=?2", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, id); + yyjson_mut_val *o = NULL; + if (sqlite3_step(st) == SQLITE_ROW) + o = fy_json(r, st); + sqlite3_finalize(st); + if (!o) + return fail(r, "NOT_FOUND", "fiscal year not found"); + return o; +} + static yyjson_mut_val *h_period_lock(struct req *r) { int64_t fy = 0; @@ -4119,6 +4185,8 @@ const struct command g_commands[] = { h_fiscal_year_close }, { "fiscal_year.reopen", "Reopen a closed fiscal year", PERM_OWNER, 1, 1, 1, h_fiscal_year_reopen }, + { "fiscal_year.update", "Update fiscal-year metadata (dividend)", + PERM_WRITE, 1, 1, 1, h_fiscal_year_update }, { "period.lock", "Lock a period through a date", PERM_OWNER, 1, 1, 1, h_period_lock }, { "period.unlock", "Remove a period lock", PERM_OWNER, 1, 1, 1, diff --git a/src/db.c b/src/db.c index 0de40ea..ebee37b 100644 --- a/src/db.c +++ b/src/db.c @@ -96,6 +96,7 @@ static const char SCHEMA_V1[] = " end_date TEXT NOT NULL," " status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open','closed'))," " locked_until TEXT," + " dividend_ore INTEGER NOT NULL DEFAULT 0," " created_at TEXT NOT NULL," " closed_at TEXT," " closed_by INTEGER REFERENCES users(id)," @@ -362,6 +363,15 @@ static int db_upgrade_v3(sqlite3 *db, char **err) return 0; } +/* v4: the årsredovisning keeps the board's proposed dividend per year. */ +static int db_upgrade_v4(sqlite3 *db, char **err) +{ + return db_exec(db, + "ALTER TABLE fiscal_years ADD COLUMN dividend_ore" + " INTEGER NOT NULL DEFAULT 0", + err); +} + static int db_upgrade(sqlite3 *db, int from, char **err) { if (db_exec(db, "BEGIN IMMEDIATE", err) != 0) @@ -374,6 +384,10 @@ static int db_upgrade(sqlite3 *db, int from, char **err) db_exec(db, "ROLLBACK", NULL); return -1; } + if (from < 4 && db_upgrade_v4(db, err) != 0) { + db_exec(db, "ROLLBACK", NULL); + return -1; + } char *sql = sqlite3_mprintf( "UPDATE meta SET value='%d' WHERE key='schema_version'", BOKF_SCHEMA_VERSION); diff --git a/src/db.h b/src/db.h index aa34c81..7353c3c 100644 --- a/src/db.h +++ b/src/db.h @@ -4,7 +4,7 @@ #include #include -#define BOKF_SCHEMA_VERSION 3 +#define BOKF_SCHEMA_VERSION 4 int db_open(const char *path, sqlite3 **out, char **err); int db_migrate(sqlite3 *db, char **err); diff --git a/tests/test_core.c b/tests/test_core.c index 6520d4b..0dfdd08 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -699,6 +699,28 @@ int main(void) CHECK(fy1 > 0); yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"46a\",\"cmd\":\"fiscal_year.update\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," + "\"dividend_ore\":123456}}", + g_session, (int)org_id, (long long)fy1)); + CHECK_OK(d); + CHECK(jint(d, "result.dividend_ore") == 123456); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"46b\",\"cmd\":\"fiscal_year.get\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", + g_session, (int)org_id, (long long)fy1)); + CHECK_OK(d); + CHECK(jint(d, "result.dividend_ore") == 123456); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"46c\",\"cmd\":\"fiscal_year.update\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," + "\"dividend_ore\":-1}}", + g_session, (int)org_id, (long long)fy1)); + CHECK_STR(d, "error.code", "INVALID_ARGS"); + yyjson_doc_free(d); + /* sale: 1250.00 kr incl 25% moms */ d = call(reqf("{\"v\":1,\"id\":\"47\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" -- cgit v1.3