#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "client.h" #include "tui.h" #include "formula.h" #include "util.h" #include "version.h" #include "yyjson.h" #include "ui.h" static void arsredovisning_screen(struct app *a); /* ------------------------------------------------------------------ */ /* bokslut */ /* ------------------------------------------------------------------ */ 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. */ 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]; }; /* 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" " Bolaget.]\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 Bolaget.]\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 void arsredovisning_screen(struct app *a) { 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); 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; } }