diff options
Diffstat (limited to 'clients/screens_bokslut.c')
| -rw-r--r-- | clients/screens_bokslut.c | 998 |
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; + } +} |
