#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; static const struct tui_action bacts[] = { { "bokslut.plan", "Visa bokslutsplan (torrkörning)", TUI_KEY_REFRESH, 1, NULL }, { "bokslut.post", "Bokför planen… (frågar först)", TUI_KEY_SUBMIT, 1, NULL }, }; tui_set_actions(bacts, can_edit ? 2 : 1); tui_form_hint("upp/ned/Tab = flytta Enter = ändra/utför" " → = åtgärder (bokslutsplan, bokför)" " Esc/q = tillbaka"); 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 res_exact; 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. Each account is rounded to whole kronor before it is summed (util_round_kr), so every line and total adds up; exact keeps öre for the rounding balance. */ static int64_t ar_bs_total(yyjson_val *res, int lo, int hi, const char *field, int exact) { 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 v = vint(row, field); if (lo >= 2000) v = -v; s += exact ? v : util_round_kr(v); } } return s; } static int64_t ar_bs_ib_sum(yyjson_val *res, int lo, int hi) { return ar_bs_total(res, lo, hi, "ib_ore", 0); } static int64_t ar_bs_sum(yyjson_val *res, int lo, int hi) { return ar_bs_total(res, lo, hi, "ub_ore", 0); } /* The krona that rounding leaves between the balance sheet's two sides, to be carried by Balanserat resultat. 2099 is left out: Årets resultat is the income statement's figure. A real imbalance stays visible. */ static int64_t ar_bs_rounding(yyjson_val *res, const char *field, int64_t result_kr, int64_t result_exact) { int64_t a_kr = ar_bs_total(res, 1000, 1999, field, 0); int64_t a_ex = ar_bs_total(res, 1000, 1999, field, 1); int64_t el_kr = ar_bs_total(res, 2000, 2098, field, 0) + ar_bs_total(res, 2100, 2999, field, 0) + result_kr; int64_t el_ex = ar_bs_total(res, 2000, 2098, field, 1) + ar_bs_total(res, 2100, 2999, field, 1) + result_exact; return (a_kr - el_kr) - util_round_kr(a_ex - el_ex); } /* One year's income statement from its general ledger. SIE-imported vouchers whose description starts with "Stäng" are the source system's closings and are skipped (the same rule as report.income_statement), so those years show their real figures instead of a zeroed P&L. Each account is rounded to whole kronor before it is summed. */ 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 (strcmp(vstr(row, "source"), "sie_import") == 0 && strncmp(vstr(row, "description"), "Stäng", strlen("Stäng")) == 0) continue; net += vint(row, "debit_ore") - vint(row, "credit_ore"); } int64_t disp = util_round_kr(-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; y->res_exact -= net; 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; else continue; y->res_exact -= net; } 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) + ar_bs_rounding(bs, "ib_ore", prev_ars, prev ? prev->res_exact : 0); int64_t br_ub = ar_bs_sum(bs, 2091, 2098) + ar_bs_rounding(bs, "ub_ore", ars, cur->res_exact); 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", br_ub + ars, br_ib + prev_ars, 1); int64_t eq_ub = ar_bs_sum(bs, 2000, 2090) + br_ub + ars; int64_t eq_ib = ar_bs_ib_sum(bs, 2000, 2090) + br_ib + prev_ars; ar_row(t, "Summa eget kapital", eq_ub, eq_ib, 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", eq_ub + ar_bs_sum(bs, 2100, 2999), eq_ib + ar_bs_ib_sum(bs, 2100, 2999), 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) { if (ui_web_block("Årsredovisning")) return; 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; } }