aboutsummaryrefslogtreecommitdiff
path: root/clients/screens_bokslut.c
diff options
context:
space:
mode:
Diffstat (limited to 'clients/screens_bokslut.c')
-rw-r--r--clients/screens_bokslut.c998
1 files changed, 998 insertions, 0 deletions
diff --git a/clients/screens_bokslut.c b/clients/screens_bokslut.c
new file mode 100644
index 0000000..41649ce
--- /dev/null
+++ b/clients/screens_bokslut.c
@@ -0,0 +1,998 @@
+#include <dirent.h>
+#include <errno.h>
+#include <locale.h>
+#include <math.h>
+#include <ncursesw/ncurses.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#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"
+ " Företagsuppgifter.]\n");
+ buf_line(t, "\nBolaget har sitt säte i %s.\n", city);
+ buf_line(t, "\n" MK_HEAD "Väsentliga händelser under räkenskapsåret\n");
+ if (events && *events)
+ ar_wrap(t, events);
+ else
+ buf_line(t, "[Beskriv väsentliga händelser.]\n");
+
+ if (nyears > 1) {
+ char row[512];
+ int off = snprintf(row, sizeof row, "%-38s", "Flerårsöversikt");
+ for (int i = 0; i < nyears; i++)
+ off += snprintf(row + off, sizeof row - (size_t)off, "%9s",
+ years[i].label);
+ buf_line(t, "\n" MK_HEAD "%s\n\n", row);
+ off = snprintf(row, sizeof row, "%-38s", "Nettoomsättning, tkr");
+ for (int i = 0; i < nyears; i++)
+ off += snprintf(row + off, sizeof row - (size_t)off, "%9lld",
+ ar_tkr(years[i].netto));
+ buf_line(t, "%s\n", row);
+ off = snprintf(row, sizeof row, "%-38s",
+ "Resultat efter finansiella poster, tkr");
+ for (int i = 0; i < nyears; i++)
+ off += snprintf(row + off, sizeof row - (size_t)off, "%9lld",
+ ar_tkr(years[i].res_fin));
+ buf_line(t, "%s\n", row);
+ }
+
+ int64_t ak_ib = ar_bs_ib_sum(bs, 2081, 2089);
+ int64_t ak_ub = ar_bs_sum(bs, 2081, 2089);
+ int64_t br_ib = ar_bs_ib_sum(bs, 2091, 2098);
+ int64_t br_ub = ar_bs_sum(bs, 2091, 2098);
+ int64_t movement = br_ub - br_ib;
+ int64_t paid_div = prev_ars - movement;
+ if (paid_div < 0)
+ paid_div = 0;
+ int64_t balanced = prev_ars - paid_div;
+ if (prev) {
+ char row[512];
+ int off = snprintf(row, sizeof row, "%-34s", "Förändringar i eget"
+ " kapital");
+ off += snprintf(row + off, sizeof row - (size_t)off, "%12s",
+ "Aktiekapital");
+ off += snprintf(row + off, sizeof row - (size_t)off, "%14s",
+ "Balanserat");
+ off += snprintf(row + off, sizeof row - (size_t)off, "%14s",
+ "Årets");
+ off += snprintf(row + off, sizeof row - (size_t)off, "%14s",
+ "Totalt");
+ buf_line(t, "\n" MK_HEAD "%s\n", row);
+ buf_line(t, "%s\n", " resultat"
+ " resultat");
+ int has_disp = movement != 0;
+ int64_t ar_ib = has_disp ? prev_ars : 0;
+ ar_eq_row(t, "Belopp vid årets ingång", ak_ib, br_ib, ar_ib, 0);
+ if (has_disp) {
+ buf_line(t, "Resultatdisposition enligt årsstämman\n");
+ ar_eq_row(t, "Utdelning", 0, -paid_div, 0, 1);
+ ar_eq_row(t, "Balanseras i ny räkning", 0, balanced, -prev_ars, 1);
+ }
+ ar_eq_row(t, "Årets resultat", 0, 0, ars, 0);
+ ar_eq_row(t, "Belopp vid årets utgång", ak_ub, br_ub, ars, 0);
+ }
+
+ int64_t dispon = br_ub + ars;
+ buf_line(t, "\n" MK_HEAD "Resultatdisposition\n\nTill årsstämmans"
+ " förfogande står följande vinstmedel:\n");
+ ar_row(t, "Balanserat resultat", br_ub, 0, 1);
+ ar_row(t, "Årets resultat", ars, 0, 1);
+ ar_row(t, "Totalt", dispon, 0, 1);
+ buf_line(t, "\nStyrelsen föreslår att vinstmedlen disponeras enligt"
+ " följande:\n");
+ ar_row(t, "Utdelning till ägarna", dividend, 0, 1);
+ ar_row(t, "Balanseras i ny räkning", dispon - dividend, 0, 1);
+ ar_row(t, "Totalt", dispon, 0, 1);
+
+ if (dividend > 0) {
+ buf_line(t, "\n" MK_HEAD "Styrelsens yttrande över den föreslagna"
+ " vinstutdelningen\n\n");
+ if (shares > 0) {
+ char per[48];
+ tui_kr_format((dividend + shares / 2) / shares, per, sizeof per);
+ buf_line(t, "Utdelning per aktie: %s kr. Datum för utdelning är"
+ " %s.\n\n", per, pay_sv);
+ } else {
+ buf_line(t, "Datum för utdelning är %s.\n\n", pay_sv);
+ }
+ buf_line(t, "Likviditeten i bolaget bedöms kunna upprätthållas på en"
+ " likaledes betryggande nivå.\n\n");
+ buf_line(t, "Styrelsens uppfattning är att den föreslagna utdelningen"
+ " ej hindrar bolaget\nfrån att fullgöra sina förpliktelser"
+ " på kort och lång sikt, ej heller att fullgöra\nerforderliga"
+ " investeringar. Den föreslagna utdelningen kan därmed"
+ " försvaras med\nhänsyn till vad som anförs i ABL 17 kap."
+ " 3 § 2 st. (försiktighetsregeln).\n");
+ }
+
+ buf_line(t, "\n" MK_HEAD "Resultaträkning\nSamtliga belopp anges i"
+ " svenska kronor.\n\n");
+ {
+ char h1[40], h2[40], row[512];
+ snprintf(h1, sizeof h1, "%s - %s", cur->from, cur->to);
+ if (prev)
+ snprintf(h2, sizeof h2, "%s - %s", prev->from, prev->to);
+ else
+ h2[0] = '\0';
+ int off = snprintf(row, sizeof row, "%-48s", "");
+ off += snprintf(row + off, sizeof row - (size_t)off, "%16s", h1);
+ if (h2[0])
+ off += snprintf(row + off, sizeof row - (size_t)off, "%17s", h2);
+ buf_line(t, "%s\n", row);
+ }
+ buf_line(t, MK_HEAD "Rörelseintäkter, lagerförändringar m.m.\n");
+ ar_row(t, "Nettoomsättning", cur->groups[0],
+ prev ? prev->groups[0] : 0, 1);
+ int64_t rev_cur = cur->groups[0] + cur->groups[1];
+ int64_t rev_pre = prev ? prev->groups[0] + prev->groups[1] : 0;
+ ar_row(t, "Summa Rörelseintäkter, lagerförändringar m.m.", rev_cur,
+ rev_pre, 1);
+ buf_line(t, "\n" MK_HEAD "Rörelsekostnader\n");
+ int64_t cost_cur = 0, cost_pre = 0;
+ for (size_t g = 2; g < AR_GROUPS; g++) {
+ int64_t c = cur->groups[g], p = prev ? prev->groups[g] : 0;
+ if (c || p)
+ ar_row(t, IS_GROUPS[g].title, c, p, 1);
+ cost_cur += c;
+ cost_pre += p;
+ }
+ ar_row(t, "Summa Rörelsens kostnader", cost_cur, cost_pre, 1);
+ buf_line(t, "\n");
+ ar_row(t, "Rörelseresultat", cur->ror, prev ? prev->ror : 0, 0);
+ if (cur->fin_in || (prev && prev->fin_in))
+ ar_row(t, "Ränteintäkter", cur->fin_in, prev ? prev->fin_in : 0, 0);
+ if (cur->fin_out || (prev && prev->fin_out))
+ ar_row(t, "Räntekostnader", cur->fin_out, prev ? prev->fin_out : 0, 0);
+ ar_row(t, "Resultat efter finansiella poster", cur->res_fin,
+ prev ? prev->res_fin : 0, 0);
+ int64_t btax = cur->res_fin + cur->disp;
+ int64_t btax_pre = prev ? prev->res_fin + prev->disp : 0;
+ if (cur->disp || (prev && prev->disp)) {
+ buf_line(t, "\n" MK_HEAD "Bokslutsdispositioner\n");
+ ar_row(t, "Summa", cur->disp, prev ? prev->disp : 0, 1);
+ }
+ ar_row(t, "Resultat före skatt", btax, btax_pre, 0);
+ if (cur->skatt || (prev && prev->skatt))
+ ar_row(t, "Skatt på årets resultat", cur->skatt,
+ prev ? prev->skatt : 0, 0);
+ buf_line(t, "\n");
+ ar_row(t, "Årets resultat", ars, prev_ars, 0);
+
+ buf_line(t, "\n" MK_HEAD "Balansräkning\n\n");
+ {
+ char h1[40], h2[40], row[256];
+ snprintf(h1, sizeof h1, "%s", cur->to);
+ if (prev)
+ snprintf(h2, sizeof h2, "%s", prev->to);
+ else
+ h2[0] = '\0';
+ int off = snprintf(row, sizeof row, "%-48s", "");
+ off += snprintf(row + off, sizeof row - (size_t)off, "%16s", h1);
+ if (h2[0])
+ off += snprintf(row + off, sizeof row - (size_t)off, "%17s", h2);
+ buf_line(t, "%s\n", row);
+ }
+ buf_line(t, MK_HEAD "Tillgångar\nAnläggningstillgångar\n");
+ ar_row(t, "Summa anläggningstillgångar", ar_bs_sum(bs, 1000, 1399),
+ ar_bs_ib_sum(bs, 1000, 1399), 1);
+ buf_line(t, MK_HEAD "Omsättningstillgångar\n");
+ int64_t v = ar_bs_sum(bs, 1400, 1499), vp = ar_bs_ib_sum(bs, 1400, 1499);
+ if (v || vp)
+ ar_row(t, "Varulager", v, vp, 0);
+ ar_row(t, "Kortfristiga fordringar", ar_bs_sum(bs, 1500, 1699),
+ ar_bs_ib_sum(bs, 1500, 1699), 0);
+ v = ar_bs_sum(bs, 1700, 1899);
+ vp = ar_bs_ib_sum(bs, 1700, 1899);
+ if (v || vp)
+ ar_row(t, "Kortfristiga placeringar", v, vp, 0);
+ ar_row(t, "Kassa och bank", ar_bs_sum(bs, 1900, 1999),
+ ar_bs_ib_sum(bs, 1900, 1999), 0);
+ ar_row(t, "Summa tillgångar", ar_bs_sum(bs, 1000, 1999),
+ ar_bs_ib_sum(bs, 1000, 1999), 0);
+ buf_line(t, "\n" MK_HEAD "Eget kapital och skulder\nBundet eget"
+ " kapital\n");
+ ar_row(t, "Aktiekapital", ak_ub, ak_ib, 1);
+ ar_row(t, "Summa bundet eget kapital", ar_bs_sum(bs, 2000, 2090),
+ ar_bs_ib_sum(bs, 2000, 2090), 1);
+ buf_line(t, MK_HEAD "Fritt eget kapital\n");
+ ar_row(t, "Balanserat resultat", br_ub, br_ib, 1);
+ ar_row(t, "Årets resultat", ars, prev_ars, 1);
+ ar_row(t, "Summa fritt eget kapital", ar_bs_sum(bs, 2091, 2098) + ars,
+ ar_bs_ib_sum(bs, 2091, 2098) + prev_ars, 1);
+ ar_row(t, "Summa eget kapital",
+ ar_bs_sum(bs, 2000, 2090) + ar_bs_sum(bs, 2091, 2098) + ars,
+ ar_bs_ib_sum(bs, 2000, 2090) + ar_bs_ib_sum(bs, 2091, 2098) +
+ prev_ars,
+ 0);
+ buf_line(t, "\n");
+ ar_row(t, "Obeskattade reserver", ar_bs_sum(bs, 2100, 2199),
+ ar_bs_ib_sum(bs, 2100, 2199), 0);
+ ar_row(t, "Avsättningar", ar_bs_sum(bs, 2200, 2299),
+ ar_bs_ib_sum(bs, 2200, 2299), 0);
+ buf_line(t, MK_HEAD "Kortfristiga skulder\n");
+ ar_row(t, "Skatteskulder", ar_bs_sum(bs, 2500, 2599),
+ ar_bs_ib_sum(bs, 2500, 2599), 1);
+ ar_row(t, "Mervärdesskatt", ar_bs_sum(bs, 2600, 2699),
+ ar_bs_ib_sum(bs, 2600, 2699), 1);
+ ar_row(t, "Personalskatter och sociala avgifter",
+ ar_bs_sum(bs, 2700, 2799), ar_bs_ib_sum(bs, 2700, 2799), 1);
+ ar_row(t, "Övriga kortfristiga skulder", ar_bs_sum(bs, 2800, 2999) +
+ ar_bs_sum(bs, 2400, 2499),
+ ar_bs_ib_sum(bs, 2800, 2999) + ar_bs_ib_sum(bs, 2400, 2499), 1);
+ int64_t skuld = ar_bs_sum(bs, 2400, 2999);
+ int64_t skuld_p = ar_bs_ib_sum(bs, 2400, 2999);
+ ar_row(t, "Summa kortfristiga skulder", skuld, skuld_p, 1);
+ ar_row(t, "Summa eget kapital, avsättningar och skulder",
+ ar_bs_sum(bs, 2000, 2999) + ars,
+ ar_bs_ib_sum(bs, 2000, 2999) + prev_ars, 0);
+
+ buf_line(t, "\n" MK_HEAD "Noter\n\nNot 1 Redovisnings- och"
+ " värderingsprinciper\n");
+ buf_line(t, "Årsredovisningen är upprättad i enlighet med"
+ " årsredovisningslagen och\nBFNAR 2016:10 Årsredovisning i"
+ " mindre företag.\n");
+ buf_line(t, "\n" MK_HEAD "Not 2 Medelantal anställda\n");
+ buf_line(t, "Medelantalet anställda under räkenskapsåret har uppgått"
+ " till %s.\n", employees && *employees ? employees
+ : "[antal]");
+ if (notes && *notes) {
+ buf_line(t, "\n" MK_HEAD "Not 3 Övriga upplysningar\n");
+ ar_wrap(t, notes);
+ }
+
+ buf_line(t, "\n" MK_HEAD "Underskrifter\n\n%s %s\n\n", city, agm_sv);
+ if (board_text && *board_text)
+ buf_line(t, "%s", board_text);
+ else
+ buf_line(t, "[Lägg till styrelseledamöter under"
+ " Företagsuppgifter.]\n");
+}
+
+/* The pager text carries style markers that must not reach the saved file. */
+static void strip_markup(const char *in, struct buf *out)
+{
+ for (const char *p = in; *p;) {
+ const char *nl = strchr(p, '\n');
+ size_t len = nl ? (size_t)(nl - p) : strlen(p);
+ if (len && p[0] >= TUI_MARK_HEADING && p[0] <= TUI_MARK_STICKY) {
+ p++;
+ len--;
+ }
+ buf_append(out, p, len);
+ if (!nl)
+ break;
+ buf_append(out, "\n", 1);
+ p = nl + 1;
+ }
+}
+
+static void arsredovisning_save(struct app *a, const char *text)
+{
+ const char *home = getenv("HOME");
+ char def[512];
+ snprintf(def, sizeof def, "%s/Downloads", home && *home ? home : ".");
+ struct stat sb;
+ if (stat(def, &sb) != 0 || !S_ISDIR(sb.st_mode))
+ snprintf(def, sizeof def, "%s", home && *home ? home : ".");
+ char dirbuf[512]; char *dir = tui_prompt_into(dirbuf, sizeof dirbuf, "Spara årsredovisningen i: ", def, 0) ? dirbuf : NULL;
+ if (!dir || !*dir)
+ return;
+ char full[640];
+ if (dir[0] == '~' && (dir[1] == '/' || dir[1] == '\0'))
+ snprintf(full, sizeof full, "%s%s", home ? home : "", dir + 1);
+ else
+ snprintf(full, sizeof full, "%s", dir);
+ size_t fl = strlen(full);
+ if (fl && full[fl - 1] != '/')
+ snprintf(full + fl, sizeof full - fl, "/");
+ char name[80];
+ snprintf(name, sizeof name, "%s", a->fy_label);
+ for (char *p = name; *p; p++)
+ if (*p == '/')
+ *p = '-';
+ snprintf(full + strlen(full), sizeof full - strlen(full),
+ "Årsredovisning %s.txt", *name ? name : "utkast");
+ struct buf clean;
+ buf_init(&clean);
+ strip_markup(text, &clean);
+ FILE *f = fopen(full, "wb");
+ size_t n = clean.len;
+ if (!f || fwrite(clean.p, 1, n, f) != n) {
+ if (f)
+ fclose(f);
+ buf_free(&clean);
+ tui_message("Årsredovisning", "Kunde inte skriva %s: %s", full,
+ strerror(errno));
+ return;
+ }
+ fclose(f);
+ buf_free(&clean);
+ tui_message("Årsredovisning", "Sparat: %s\n\nFilen är ett utkast i textform"
+ " att redigera vidare (t.ex. i Word).", full);
+}
+
+static 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;
+ }
+}