summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-18 23:41:59 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-18 23:41:59 +0200
commit49e44668c14abacef17f0f11be39cbd0faa7ecb9 (patch)
treedd8248143def675fd033abf390c06567f78d5c4c /src
parente9fc91dbc8314a25a0d71b7ee4344d3fc4ccfd5e (diff)
downloadbokf-49e44668c14abacef17f0f11be39cbd0faa7ecb9.tar.gz
bokf-49e44668c14abacef17f0f11be39cbd0faa7ecb9.zip
bokslut, INK2/SRU export and attachment markersv0.1.30
- bokslut.post: year-end bookings (manual entries, periodiseringsfond, skatt at a given rate, resultatdisposition) with a dry-run plan through the normal ledger path; the resultatrapport separates bokslutsdispositioner and skatt per K2. - sru.export: INFO.SRU + BLANKETTER.SRU (INK2/INK2R/INK2S) from the official 2025P4 field tables and the BAS mapping, with manual INK2S adjustments, submitter defaults and unmapped-account detection; the TUI writes both files from Rapporter -> Inkomstdeklaration. - voucher.list carries attachment_count and the voucher list marks vouchers with underlag with an x column. - date_prompt restores the cursor state so it stops blinking at the bottom after the report date prompts.
Diffstat (limited to 'src')
-rw-r--r--src/commands.c234
-rw-r--r--src/sru.c711
-rw-r--r--src/sru.h15
3 files changed, 959 insertions, 1 deletions
diff --git a/src/commands.c b/src/commands.c
index d7f3860..500c9a3 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -20,6 +20,7 @@
#include "reports.h"
#include "seed.h"
#include "sie.h"
+#include "sru.h"
#include "util.h"
#include "version.h"
@@ -3029,7 +3030,9 @@ static yyjson_mut_val *h_voucher_list(struct req *r)
"SELECT v.id,v.series,v.number,v.date,v.description,v.source,"
"v.corrects_voucher_id,"
"(SELECT count(*) FROM voucher_rows r WHERE r.org_id=v.org_id"
- " AND r.voucher_id=v.id)"
+ " AND r.voucher_id=v.id),"
+ "(SELECT count(*) FROM voucher_attachments va WHERE va.org_id=v.org_id"
+ " AND va.voucher_id=v.id)"
" FROM vouchers v WHERE v.org_id=?1"
" AND (?2=0 OR v.fiscal_year_id=?2)"
" AND (?3='' OR v.series=?3)"
@@ -3075,6 +3078,8 @@ static yyjson_mut_val *h_voucher_list(struct req *r)
sqlite3_column_int64(st, 6));
yyjson_mut_obj_add_int(r->rdoc, o, "row_count",
sqlite3_column_int64(st, 7));
+ yyjson_mut_obj_add_int(r->rdoc, o, "attachment_count",
+ sqlite3_column_int64(st, 8));
}
sqlite3_finalize(st);
yyjson_mut_val *out = yyjson_mut_obj(r->rdoc);
@@ -3635,6 +3640,23 @@ static yyjson_mut_val *h_report_vat(struct req *r)
return res;
}
+static yyjson_mut_val *h_sru_export(struct req *r)
+{
+ int64_t fy = 0;
+ if (req_fy(r, &fy) != 0)
+ return NULL;
+ char *err = NULL;
+ yyjson_mut_val *res =
+ sru_export(r->rdoc, r->db, r->org_id, fy, r->args, &err);
+ if (!res) {
+ yyjson_mut_val *e =
+ fail(r, "INVALID_ARGS", err ? err : "could not build the SRU files");
+ free(err);
+ return e;
+ }
+ return res;
+}
+
static yyjson_mut_val *h_report_general_ledger(struct req *r)
{
int64_t fy = 0;
@@ -3690,6 +3712,212 @@ static yyjson_mut_val *h_report_vat_eskd(struct req *r)
return res;
}
+/* True for P&L accounts that take part in the taxable result (3xxx-88xx). */
+static int bokslut_pl_account(const char *number)
+{
+ int n = atoi(number);
+ return n >= 3000 && n <= 8899;
+}
+
+static yyjson_mut_val *h_bokslut_post(struct req *r)
+{
+ int64_t fy_id = 0;
+ if (req_fy(r, &fy_id) != 0)
+ return NULL;
+ char fy_label[64] = "", fy_start[16] = "", fy_end[16] = "";
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(r->db,
+ "SELECT label,start_date,end_date FROM fiscal_years"
+ " WHERE org_id=?1 AND id=?2",
+ -1, &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, fy_id);
+ if (sqlite3_step(st) != SQLITE_ROW) {
+ sqlite3_finalize(st);
+ return fail(r, "NOT_FOUND", "fiscal year not found");
+ }
+ snprintf(fy_label, sizeof fy_label, "%s",
+ (const char *)sqlite3_column_text(st, 0));
+ snprintf(fy_start, sizeof fy_start, "%s",
+ (const char *)sqlite3_column_text(st, 1));
+ snprintf(fy_end, sizeof fy_end, "%s",
+ (const char *)sqlite3_column_text(st, 2));
+ sqlite3_finalize(st);
+ const char *date = arg_str(r->args, "date");
+ if (!date)
+ date = fy_end;
+ else if (!util_parse_iso_date(date))
+ return fail(r, "INVALID_ARGS", "date must be YYYY-MM-DD");
+
+ yyjson_val *earr = yyjson_obj_get(r->args, "entries");
+ if (earr && !yyjson_is_arr(earr))
+ return fail(r, "INVALID_ARGS", "entries must be an array");
+ size_t nent = earr ? yyjson_arr_size(earr) : 0;
+ if (nent > 31)
+ return fail(r, "INVALID_ARGS", "too many entries");
+ struct ledger_row erows[64];
+ size_t nerows = 0;
+ memset(erows, 0, sizeof erows);
+ for (size_t i = 0; i < nent; i++) {
+ yyjson_val *e = yyjson_arr_get(earr, i);
+ const char *da =
+ yyjson_get_str(yyjson_obj_get(e, "debit_account"));
+ const char *ca =
+ yyjson_get_str(yyjson_obj_get(e, "credit_account"));
+ const char *ed =
+ yyjson_get_str(yyjson_obj_get(e, "description"));
+ int64_t amt = 0;
+ yyjson_val *av = yyjson_obj_get(e, "amount_ore");
+ if (av && yyjson_is_int(av))
+ amt = yyjson_get_sint(av);
+ if (!da || !*da || !ca || !*ca || amt <= 0)
+ return fail(r, "INVALID_ARGS",
+ "each entry needs debit_account, credit_account and"
+ " amount_ore > 0");
+ erows[nerows++] = (struct ledger_row){ da, amt, 0, ed };
+ erows[nerows++] = (struct ledger_row){ ca, 0, amt, ed };
+ }
+ int64_t fond = 0;
+ arg_int(r->args, "periodiseringsfond_ore", &fond);
+ if (fond < 0)
+ return fail(r, "INVALID_ARGS", "periodiseringsfond_ore must be >= 0");
+ if (fond > 0) {
+ if (nerows + 2 > 64)
+ return fail(r, "INVALID_ARGS", "too many entries");
+ erows[nerows++] = (struct ledger_row){ "8811", fond, 0, NULL };
+ erows[nerows++] = (struct ledger_row){ "2110", 0, fond, NULL };
+ }
+
+ int64_t result_before = 0;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "SELECT COALESCE(SUM(r.credit_ore)-SUM(r.debit_ore),0)"
+ " FROM voucher_rows r"
+ " JOIN vouchers v ON v.org_id=r.org_id AND v.id=r.voucher_id"
+ " JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
+ " WHERE r.org_id=?1 AND v.fiscal_year_id=?2 AND v.series<>'IB'"
+ " AND a.type IN ('revenue','expense') AND a.number NOT LIKE '89%'",
+ -1, &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, fy_id);
+ if (sqlite3_step(st) == SQLITE_ROW)
+ result_before = sqlite3_column_int64(st, 0);
+ sqlite3_finalize(st);
+
+ int64_t delta = 0;
+ for (size_t i = 0; i < nerows; i++)
+ if (bokslut_pl_account(erows[i].account))
+ delta += erows[i].credit_ore - erows[i].debit_ore;
+
+ double tax_rate = 20.6;
+ yyjson_val *trv = yyjson_obj_get(r->args, "tax_rate");
+ if (trv) {
+ if (yyjson_is_real(trv))
+ tax_rate = yyjson_get_real(trv);
+ else if (yyjson_is_int(trv))
+ tax_rate = (double)yyjson_get_sint(trv);
+ else
+ return fail(r, "INVALID_ARGS", "tax_rate must be a number");
+ if (tax_rate < 0 || tax_rate > 100)
+ return fail(r, "INVALID_ARGS", "tax_rate must be 0-100");
+ }
+ long bp = (long)(tax_rate * 100.0 + 0.5);
+ int64_t taxable = result_before + delta;
+ int64_t tax = taxable > 0 ? taxable * bp / 10000 : 0;
+ int64_t after = taxable - tax;
+ int dispose = 1;
+ arg_bool(r->args, "dispose", &dispose);
+
+ struct {
+ const char *desc;
+ const struct ledger_row *rows;
+ size_t nrows;
+ } plan[3];
+ size_t np = 0;
+ if (nerows > 0)
+ plan[np++] = (typeof(plan[0])){ "Bokslutsdispositioner", erows,
+ nerows };
+ struct ledger_row taxrows[2] = {
+ { "8910", tax, 0, NULL }, { "2512", 0, tax, NULL }
+ };
+ if (tax != 0)
+ plan[np++] = (typeof(plan[0])){ "Skatt på årets resultat", taxrows, 2 };
+ struct ledger_row drows[2];
+ if (dispose && after != 0) {
+ if (after > 0) {
+ drows[0] = (struct ledger_row){ "8999", after, 0, NULL };
+ drows[1] = (struct ledger_row){ "2099", 0, after, NULL };
+ } else {
+ drows[0] = (struct ledger_row){ "2099", -after, 0, NULL };
+ drows[1] = (struct ledger_row){ "8999", 0, -after, NULL };
+ }
+ plan[np++] = (typeof(plan[0])){ "Resultatdisposition", drows, 2 };
+ }
+
+ yyjson_mut_val *vouchers = yyjson_mut_arr(r->rdoc);
+ for (size_t i = 0; i < np; i++) {
+ struct ledger_post_opts o;
+ memset(&o, 0, sizeof o);
+ o.org_id = r->org_id;
+ o.user_id = r->sess->user_id;
+ o.token_id = r->sess->token_id;
+ o.date = date;
+ o.description = plan[i].desc;
+ o.rows = plan[i].rows;
+ o.nrows = plan[i].nrows;
+ o.dry_run = r->dry_run;
+ struct ledger_error e;
+ char *json = NULL;
+ if (ledger_post(r->db, &o, &e, &json) != 0) {
+ yyjson_mut_val *res =
+ fail(r, e.code ? e.code : "INTERNAL", e.msg);
+ free(json);
+ return res;
+ }
+ yyjson_mut_val *vo = yyjson_mut_arr_add_obj(r->rdoc, vouchers);
+ yyjson_mut_obj_add_strcpy(r->rdoc, vo, "description", plan[i].desc);
+ yyjson_mut_val *rows = yyjson_mut_arr(r->rdoc);
+ for (size_t k = 0; k < plan[i].nrows; k++) {
+ yyjson_mut_val *ro = yyjson_mut_arr_add_obj(r->rdoc, rows);
+ yyjson_mut_obj_add_strcpy(r->rdoc, ro, "account",
+ plan[i].rows[k].account);
+ yyjson_mut_obj_add_int(r->rdoc, ro, "debit_ore",
+ plan[i].rows[k].debit_ore);
+ yyjson_mut_obj_add_int(r->rdoc, ro, "credit_ore",
+ plan[i].rows[k].credit_ore);
+ }
+ yyjson_mut_obj_add_val(r->rdoc, vo, "rows", rows);
+ yyjson_mut_val *pv = json_to_mut(r->rdoc, json);
+ free(json);
+ if (pv)
+ yyjson_mut_obj_add_val(r->rdoc, vo, "voucher", pv);
+ }
+ if (!r->dry_run) {
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "bokslut.post", reqjson, "OK", NULL);
+ free(reqjson);
+ }
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_val *fy = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_int(r->rdoc, fy, "id", fy_id);
+ yyjson_mut_obj_add_strcpy(r->rdoc, fy, "label", fy_label);
+ yyjson_mut_obj_add_strcpy(r->rdoc, fy, "start_date", fy_start);
+ yyjson_mut_obj_add_strcpy(r->rdoc, fy, "end_date", fy_end);
+ yyjson_mut_obj_add_val(r->rdoc, o, "fiscal_year", fy);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "date", date);
+ yyjson_mut_obj_add_int(r->rdoc, o, "result_before_ore", result_before);
+ yyjson_mut_obj_add_int(r->rdoc, o, "entries_ore", delta);
+ yyjson_mut_obj_add_int(r->rdoc, o, "taxable_ore", taxable);
+ yyjson_mut_obj_add_int(r->rdoc, o, "tax_ore", tax);
+ yyjson_mut_obj_add_int(r->rdoc, o, "result_after_ore", after);
+ yyjson_mut_obj_add_val(r->rdoc, o, "vouchers", vouchers);
+ return o;
+}
+
+
static unsigned char *read_file(const char *path, size_t *out_len)
{
FILE *f = fopen(path, "rb");
@@ -3899,6 +4127,8 @@ const struct command g_commands[] = {
{ "voucher.list", "List vouchers", PERM_READ, 1, 0, 0, h_voucher_list },
{ "voucher.correct", "Post an ändringsverifikat", PERM_WRITE, 1, 1, 1,
h_voucher_correct },
+ { "bokslut.post", "Year-end: dispositions, tax and result transfer",
+ PERM_WRITE, 1, 1, 1, h_bokslut_post },
{ "settings.get", "Read org settings", PERM_READ, 1, 0, 0,
h_settings_get },
{ "settings.set", "Change an org setting", PERM_WRITE, 1, 1, 1,
@@ -3937,6 +4167,8 @@ const struct command g_commands[] = {
h_report_voucher_list },
{ "report.vat_eskd", "eSKD XML for the momsdeklaration (ISO-8859-1)",
PERM_READ, 1, 0, 0, h_report_vat_eskd },
+ { "sru.export", "INK2 SRU files (INFO.SRU + BLANKETTER.SRU)",
+ PERM_READ, 1, 0, 0, h_sru_export },
{ "sie.export", "Export SIE 4 (PC8)", PERM_READ, 1, 0, 0, h_sie_export },
{ "sie.import", "Import SIE 4 into an empty fiscal year", PERM_WRITE, 1, 1,
1, h_sie_import },
diff --git a/src/sru.c b/src/sru.c
new file mode 100644
index 0000000..98a3c6d
--- /dev/null
+++ b/src/sru.c
@@ -0,0 +1,711 @@
+#include "sru.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "reports.h"
+#include "util.h"
+#include "version.h"
+
+static void set_err(char **err, const char *msg)
+{
+ if (err && !*err)
+ *err = xstrdup(msg);
+}
+
+static const char *jstr(yyjson_val *o, const char *k)
+{
+ yyjson_val *v = o ? yyjson_obj_get(o, k) : NULL;
+ return v && yyjson_is_str(v) ? yyjson_get_str(v) : NULL;
+}
+
+static int64_t jint(yyjson_val *o, const char *k)
+{
+ yyjson_val *v = o ? yyjson_obj_get(o, k) : NULL;
+ return v && yyjson_is_int(v) ? (int64_t)yyjson_get_int(v) : 0;
+}
+
+static const char *mjstr(yyjson_mut_val *o, const char *k)
+{
+ yyjson_mut_val *v = o ? yyjson_mut_obj_get(o, k) : NULL;
+ return v && yyjson_mut_is_str(v) ? yyjson_mut_get_str(v) : NULL;
+}
+
+static int64_t mjint(yyjson_mut_val *o, const char *k)
+{
+ yyjson_mut_val *v = o ? yyjson_mut_obj_get(o, k) : NULL;
+ return v && yyjson_mut_is_int(v) ? (int64_t)yyjson_mut_get_int(v) : 0;
+}
+
+static void add(struct buf *b, const char *s)
+{
+ buf_append(b, s, strlen(s));
+}
+
+/* ISO-8859-1 text; only the code points Latin-1 can hold are allowed. */
+static int add_latin1(struct buf *b, const char *utf8, char **err)
+{
+ for (const unsigned char *p = (const unsigned char *)utf8; *p;) {
+ uint32_t cp;
+ int len;
+ if (*p < 0x80) {
+ cp = *p;
+ len = 1;
+ } else if ((*p & 0xE0) == 0xC0) {
+ cp = *p & 0x1F;
+ len = 2;
+ } else if ((*p & 0xF0) == 0xE0) {
+ cp = *p & 0x0F;
+ len = 3;
+ } else {
+ set_err(err, "text is not valid UTF-8");
+ return -1;
+ }
+ for (int i = 1; i < len; i++) {
+ if ((p[i] & 0xC0) != 0x80) {
+ set_err(err, "text is not valid UTF-8");
+ return -1;
+ }
+ cp = (cp << 6) | (uint32_t)(p[i] & 0x3F);
+ }
+ if (cp > 0xFF) {
+ set_err(err, "text has a character outside ISO-8859-1");
+ return -1;
+ }
+ unsigned char c = (unsigned char)cp;
+ buf_append(b, &c, 1);
+ p += len;
+ }
+ return 0;
+}
+
+struct fld {
+ char code[8];
+ int64_t ore;
+};
+
+struct fldset {
+ struct fld v[64];
+ size_t n;
+};
+
+static void fld_add(struct fldset *s, const char *code, int64_t ore)
+{
+ if (!ore)
+ return;
+ for (size_t i = 0; i < s->n; i++) {
+ if (strcmp(s->v[i].code, code) == 0) {
+ s->v[i].ore += ore;
+ return;
+ }
+ }
+ if (s->n >= sizeof s->v / sizeof s->v[0])
+ return;
+ snprintf(s->v[s->n].code, sizeof s->v[s->n].code, "%s", code);
+ s->v[s->n].ore = ore;
+ s->n++;
+}
+
+static int64_t fld_get(const struct fldset *s, const char *code)
+{
+ for (size_t i = 0; i < s->n; i++)
+ if (strcmp(s->v[i].code, code) == 0)
+ return s->v[i].ore;
+ return 0;
+}
+
+/* ---------------- BAS ranges to INK2R field codes ---------------- */
+
+struct bmap {
+ int lo, hi;
+ const char *code;
+};
+
+static const struct bmap BAL_ASSETS[] = {
+ { 1010, 1079, "7201" }, { 1090, 1099, "7201" }, { 1080, 1089, "7202" },
+ { 1100, 1119, "7214" }, { 1130, 1179, "7214" }, { 1190, 1199, "7214" },
+ { 1120, 1129, "7216" }, { 1180, 1189, "7217" },
+ { 1200, 1299, "7215" },
+ { 1311, 1316, "7230" }, { 1330, 1338, "7231" }, { 1350, 1359, "7233" },
+ { 1380, 1389, "7233" }, { 1320, 1329, "7232" }, { 1340, 1349, "7232" },
+ { 1360, 1369, "7234" }, { 1370, 1379, "7235" }, { 1390, 1399, "7235" },
+ { 1410, 1419, "7241" }, { 1440, 1449, "7242" }, { 1450, 1469, "7243" },
+ { 1470, 1489, "7244" }, { 1490, 1499, "7245" }, { 1400, 1409, "7246" },
+ { 1500, 1519, "7251" }, { 1560, 1579, "7252" },
+ { 1620, 1620, "7262" },
+ { 1520, 1559, "7261" }, { 1580, 1599, "7261" }, { 1600, 1699, "7261" },
+ { 1700, 1799, "7263" },
+ { 1860, 1869, "7270" }, { 1800, 1859, "7271" }, { 1870, 1899, "7271" },
+ { 1900, 1999, "7281" },
+};
+
+static const struct bmap BAL_EL[] = {
+ { 2010, 2089, "7301" }, { 2090, 2099, "7302" },
+ { 2110, 2129, "7321" }, { 2150, 2159, "7322" }, { 2130, 2149, "7323" },
+ { 2160, 2199, "7323" },
+ { 2210, 2219, "7331" }, { 2220, 2229, "7332" }, { 2230, 2299, "7333" },
+ { 2320, 2329, "7350" }, { 2330, 2339, "7351" }, { 2340, 2359, "7352" },
+ { 2360, 2379, "7353" }, { 2380, 2399, "7354" },
+ { 2410, 2419, "7360" }, { 2420, 2439, "7361" }, { 2400, 2409, "7362" },
+ { 2450, 2459, "7363" }, { 2460, 2469, "7364" }, { 2440, 2449, "7365" },
+ { 2490, 2490, "7366" }, { 2470, 2479, "7367" },
+ { 2480, 2489, "7369" }, { 2491, 2499, "7369" }, { 2600, 2799, "7369" },
+ { 2800, 2899, "7369" }, { 2500, 2599, "7368" },
+ { 2900, 2999, "7370" },
+};
+
+struct imap {
+ int lo, hi;
+ const char *plus;
+ const char *minus;
+};
+
+static const struct imap INC_REV[] = {
+ { 3000, 3799, "7410", "7410" },
+ { 3800, 3899, "7412", "7412" },
+ { 3900, 3999, "7413", "7413" },
+ { 4900, 4999, "7411", "7510" },
+ { 8000, 8099, "7414", "7518" },
+ { 8100, 8199, "7415", "7519" },
+ { 8200, 8269, "7423", "7530" },
+ { 8270, 8299, "7416", "7520" },
+ { 8300, 8399, "7417", "7417" },
+ { 8820, 8820, "7419", "7419" },
+ { 8819, 8819, "7420", "7420" },
+ { 8850, 8850, "7421", "7526" },
+ { 8860, 8899, "7422", "7527" },
+};
+
+static const struct imap INC_EXP[] = {
+ { 4000, 4599, "7511", "7511" },
+ { 4600, 4699, "7512", "7512" },
+ { 5000, 6999, "7513", "7513" },
+ { 7000, 7699, "7514", "7514" },
+ { 7700, 7799, "7516", "7516" },
+ { 7800, 7899, "7515", "7515" },
+ { 7900, 7999, "7517", "7517" },
+ { 8400, 8499, "7522", "7522" },
+ { 8500, 8599, "7521", "7521" },
+ { 8830, 8830, "7524", "7524" },
+ { 8811, 8811, "7525", "7525" },
+ { 8910, 8989, "7528", "7528" },
+};
+
+static const char *const INK2R_ORDER[] = {
+ "7011", "7012", "7201", "7202", "7214", "7215", "7216", "7217", "7230",
+ "7231", "7233", "7232", "7234", "7235", "7241", "7242", "7243", "7244",
+ "7245", "7246", "7251", "7252", "7261", "7262", "7263", "7270", "7271",
+ "7281", "7301", "7302", "7321", "7322", "7323", "7331", "7332", "7333",
+ "7350", "7351", "7352", "7353", "7354", "7360", "7361", "7362", "7363",
+ "7364", "7365", "7366", "7367", "7369", "7368", "7370", "7410", "7411",
+ "7510", "7412", "7413", "7511", "7512", "7513", "7514", "7515", "7516",
+ "7517", "7414", "7518", "7415", "7519", "7423", "7530", "7416", "7520",
+ "7417", "7521", "7522", "7524", "7419", "7420", "7525", "7421", "7526",
+ "7422", "7527", "7528", "7450", "7550",
+};
+
+static const char *const INK2S_ORDER[] = {
+ "7011", "7012", "7650", "7750", "7651", "7652", "7653", "7751", "7764",
+ "7752", "7753", "7754", "7654", "7668", "7655", "7673", "7665", "7755",
+ "7656", "7756", "7657", "7658", "7757", "7758", "7659", "7660", "7759",
+ "7666", "7765", "7661", "7760", "7761", "7662", "7663", "7762", "7763",
+ "7671", "7672", "7670", "7770", "8020", "8021", "8023", "8026", "8022",
+ "8028", "8040", "8041", "8044", "8045",
+};
+
+static const char *const INK2_ORDER[] = {
+ "7011", "7012", "7104", "7114", "7131", "7132", "7133", "7153",
+ "7154", "7155", "7156", "80", "93", "84", "86", "95", "96", "97",
+ "98", "90", "1582",
+};
+
+/* INK2S fields that add to / subtract from the överskott (7670). */
+static const char *const INK2S_PLUS[] = {
+ "7650", "7651", "7652", "7653", "7654", "7668", "7655", "7673",
+ "7665", "7656", "7657", "7658", "7659", "7660", "7666", "7661",
+ "7662", "7663", "7671", "7672",
+};
+static const char *const INK2S_MINUS[] = {
+ "7750", "7751", "7764", "7752", "7753", "7754", "7755", "7756",
+ "7757", "7758", "7759", "7765", "7760", "7761", "7762", "7763",
+};
+
+static int in_list(const char *const *list, size_t n, const char *code)
+{
+ for (size_t i = 0; i < n; i++)
+ if (strcmp(list[i], code) == 0)
+ return 1;
+ return 0;
+}
+
+static void map_balance(yyjson_mut_val *arr, const struct bmap *tab,
+ size_t ntab, struct fldset *out,
+ struct fldset *unmapped)
+{
+ size_t n = yyjson_mut_arr_size(arr);
+ for (size_t i = 0; i < n; i++) {
+ yyjson_mut_val *row = yyjson_mut_arr_get(arr, i);
+ const char *acc = mjstr(row, "account");
+ int64_t ore = mjint(row, "amount_ore");
+ if (!acc || !ore)
+ continue;
+ int num = atoi(acc);
+ const char *code = NULL;
+ for (size_t t = 0; t < ntab; t++) {
+ if (num >= tab[t].lo && num <= tab[t].hi) {
+ code = tab[t].code;
+ break;
+ }
+ }
+ if (code)
+ fld_add(out, code, ore);
+ else
+ fld_add(unmapped, acc, ore);
+ }
+}
+
+static int64_t map_income(yyjson_mut_val *arr, const struct imap *tab,
+ size_t ntab, struct fldset *out,
+ struct fldset *unmapped)
+{
+ int64_t total = 0;
+ size_t n = yyjson_mut_arr_size(arr);
+ for (size_t i = 0; i < n; i++) {
+ yyjson_mut_val *row = yyjson_mut_arr_get(arr, i);
+ const char *acc = mjstr(row, "account");
+ int64_t ore = mjint(row, "amount_ore");
+ if (!acc || !ore)
+ continue;
+ int num = atoi(acc);
+ if (num >= 8990 && num <= 8999)
+ continue; /* the result transfer itself is not a field */
+ const struct imap *m = NULL;
+ for (size_t t = 0; t < ntab; t++) {
+ if (num >= tab[t].lo && num <= tab[t].hi) {
+ m = &tab[t];
+ break;
+ }
+ }
+ if (!m) {
+ fld_add(unmapped, acc, ore);
+ continue;
+ }
+ if (strcmp(m->plus, m->minus) == 0)
+ fld_add(out, m->plus, ore);
+ else if (ore >= 0)
+ fld_add(out, m->plus, ore);
+ else
+ fld_add(out, m->minus, -ore);
+ total += ore;
+ }
+ return total;
+}
+
+static void emit_fields(struct buf *b, const struct fldset *f,
+ const char *const *order, size_t norder)
+{
+ char line[64];
+ for (size_t i = 0; i < norder; i++) {
+ int64_t kr = fld_get(f, order[i]) / 100;
+ if (!kr)
+ continue;
+ snprintf(line, sizeof line, "#UPPGIFT %s %lld\n", order[i],
+ (long long)kr);
+ add(b, line);
+ }
+}
+
+static void now_stamp(char date[16], char hms[16])
+{
+ time_t t = time(NULL);
+ struct tm tm;
+ localtime_r(&t, &tm);
+ strftime(date, 16, "%Y%m%d", &tm);
+ strftime(hms, 16, "%H%M%S", &tm);
+}
+
+/* 10 or 12 digits (hyphens/spaces ignored) to SSSS...; 10 gets a 16 prefix. */
+static int org12(const char *raw, char out[13], char **err)
+{
+ char digits[16];
+ size_t n = 0;
+ for (const char *p = raw ? raw : ""; *p; p++)
+ if (*p >= '0' && *p <= '9' && n < sizeof digits - 1)
+ digits[n++] = *p;
+ digits[n] = '\0';
+ if (n == 10)
+ snprintf(out, 13, "16%s", digits);
+ else if (n == 12)
+ snprintf(out, 13, "%s", digits);
+ else {
+ set_err(err, "org_nr must be xxxxxx-xxxx; set it in Företagsuppgifter");
+ return -1;
+ }
+ return 0;
+}
+
+static void date_compact(const char *iso, char out[9])
+{
+ snprintf(out, 9, "%.4s%.2s%.2s", iso, iso + 5, iso + 8);
+}
+
+static yyjson_mut_val *file_json(yyjson_mut_doc *doc, const char *fname,
+ const struct buf *b)
+{
+ unsigned char raw[32];
+ char sha[65];
+ util_sha256(b->p, b->len, raw);
+ util_hex(raw, sizeof raw, sha);
+ char *b64 = util_b64(b->p, b->len);
+ yyjson_mut_val *o = yyjson_mut_obj(doc);
+ yyjson_mut_obj_add_strcpy(doc, o, "filename", fname);
+ yyjson_mut_obj_add_strcpy(doc, o, "content_base64", b64 ? b64 : "");
+ yyjson_mut_obj_add_strcpy(doc, o, "sha256", sha);
+ yyjson_mut_obj_add_int(doc, o, "size", (int64_t)b->len);
+ free(b64);
+ return o;
+}
+
+yyjson_mut_val *sru_export(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id,
+ int64_t fy_id, yyjson_val *args, char **err)
+{
+ char fy_start[16] = "", fy_end[16] = "";
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(db,
+ "SELECT COALESCE(name,''),COALESCE(org_nr,''),"
+ "COALESCE(address,''),COALESCE(postal_code,''),"
+ "COALESCE(city,''),COALESCE(email,''),"
+ "COALESCE(phone,'') FROM orgs WHERE id=?1",
+ -1, &st, NULL) != SQLITE_OK) {
+ set_err(err, "database error");
+ return NULL;
+ }
+ sqlite3_bind_int64(st, 1, org_id);
+ char org_name[128] = "", org_nr[32] = "", org_addr[128] = "";
+ char org_zip[16] = "", org_city[64] = "", org_mail[128] = "";
+ char org_phone[32] = "";
+ if (sqlite3_step(st) == SQLITE_ROW) {
+ snprintf(org_name, sizeof org_name, "%s",
+ (const char *)sqlite3_column_text(st, 0));
+ snprintf(org_nr, sizeof org_nr, "%s",
+ (const char *)sqlite3_column_text(st, 1));
+ snprintf(org_addr, sizeof org_addr, "%s",
+ (const char *)sqlite3_column_text(st, 2));
+ snprintf(org_zip, sizeof org_zip, "%s",
+ (const char *)sqlite3_column_text(st, 3));
+ snprintf(org_city, sizeof org_city, "%s",
+ (const char *)sqlite3_column_text(st, 4));
+ snprintf(org_mail, sizeof org_mail, "%s",
+ (const char *)sqlite3_column_text(st, 5));
+ snprintf(org_phone, sizeof org_phone, "%s",
+ (const char *)sqlite3_column_text(st, 6));
+ }
+ sqlite3_finalize(st);
+
+ if (sqlite3_prepare_v2(db,
+ "SELECT label,start_date,end_date FROM fiscal_years"
+ " WHERE org_id=?1 AND id=?2",
+ -1, &st, NULL) != SQLITE_OK) {
+ set_err(err, "database error");
+ return NULL;
+ }
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_int64(st, 2, fy_id);
+ if (sqlite3_step(st) != SQLITE_ROW) {
+ sqlite3_finalize(st);
+ set_err(err, "fiscal year not found");
+ return NULL;
+ }
+ snprintf(fy_start, sizeof fy_start, "%s",
+ (const char *)sqlite3_column_text(st, 1));
+ snprintf(fy_end, sizeof fy_end, "%s",
+ (const char *)sqlite3_column_text(st, 2));
+ sqlite3_finalize(st);
+
+ char org12buf[13];
+ if (org12(org_nr, org12buf, err) != 0)
+ return NULL;
+
+ int end_month = atoi(fy_end + 5);
+ char suffix = end_month <= 4 ? '1'
+ : end_month <= 6 ? '2'
+ : end_month <= 8 ? '3'
+ : '4';
+ int income_year = atoi(fy_end);
+ char type_base[8], type_ink2[16], type_r[16], type_s[16];
+ snprintf(type_base, sizeof type_base, "%dP%c", income_year, suffix);
+ snprintf(type_ink2, sizeof type_ink2, "INK2-%s", type_base);
+ snprintf(type_r, sizeof type_r, "INK2R-%s", type_base);
+ snprintf(type_s, sizeof type_s, "INK2S-%s", type_base);
+
+ yyjson_mut_val *bal = report_balance_sheet(doc, db, org_id, fy_id, NULL,
+ err);
+ if (!bal)
+ return NULL;
+ yyjson_mut_val *inc = report_income_statement(doc, db, org_id, fy_id,
+ NULL, NULL, err);
+ if (!inc)
+ return NULL;
+
+ struct fldset ink2r = { 0 }, ink2s = { 0 }, ink2 = { 0 };
+ struct fldset unmapped = { 0 };
+ map_balance(yyjson_mut_obj_get(yyjson_mut_obj_get(bal, "assets"),
+ "accounts"),
+ BAL_ASSETS, sizeof BAL_ASSETS / sizeof BAL_ASSETS[0], &ink2r,
+ &unmapped);
+ map_balance(yyjson_mut_obj_get(yyjson_mut_obj_get(bal, "equity"),
+ "accounts"),
+ BAL_EL, sizeof BAL_EL / sizeof BAL_EL[0], &ink2r, &unmapped);
+ map_balance(yyjson_mut_obj_get(yyjson_mut_obj_get(bal, "liabilities"),
+ "accounts"),
+ BAL_EL, sizeof BAL_EL / sizeof BAL_EL[0], &ink2r, &unmapped);
+ int64_t rev_total = map_income(
+ yyjson_mut_obj_get(yyjson_mut_obj_get(inc, "revenue"), "accounts"),
+ INC_REV, sizeof INC_REV / sizeof INC_REV[0], &ink2r, &unmapped);
+ int64_t exp_total = map_income(
+ yyjson_mut_obj_get(yyjson_mut_obj_get(inc, "expenses"), "accounts"),
+ INC_EXP, sizeof INC_EXP / sizeof INC_EXP[0], &ink2r, &unmapped);
+ int64_t result = rev_total - exp_total;
+ if (result > 0)
+ fld_add(&ink2r, "7450", result);
+ else if (result < 0)
+ fld_add(&ink2r, "7550", -result);
+
+ if (unmapped.n > 0) {
+ int ignore = 0;
+ yyjson_val *iv = yyjson_obj_get(args, "ignore_unmapped");
+ if (iv && yyjson_is_bool(iv))
+ ignore = yyjson_get_bool(iv);
+ if (!ignore) {
+ char msg[512];
+ snprintf(msg, sizeof msg, "unmapped accounts: ");
+ size_t used = strlen(msg);
+ for (size_t i = 0; i < unmapped.n && i < 10; i++)
+ used += (size_t)snprintf(msg + used, sizeof msg - used, "%s%s",
+ i ? ", " : "", unmapped.v[i].code);
+ snprintf(msg + used, sizeof msg - used,
+ " (pass ignore_unmapped:true to proceed)");
+ set_err(err, msg);
+ return NULL;
+ }
+ }
+
+ /* INK2S: derived result and tax, then the manual adjustments. The
+ överskott is summed from the whole-krona fields, like the
+ e-service recomputes it. */
+ int64_t result_kr = result / 100;
+ if (result_kr > 0)
+ fld_add(&ink2s, "7650", result_kr * 100);
+ else if (result_kr < 0)
+ fld_add(&ink2s, "7750", -result_kr * 100);
+ int64_t tax = fld_get(&ink2r, "7528");
+ if (tax)
+ fld_add(&ink2s, "7651", (tax / 100) * 100);
+ yyjson_val *adj = yyjson_obj_get(args, "adjustments");
+ if (adj) {
+ if (!yyjson_is_arr(adj)) {
+ set_err(err, "adjustments must be an array");
+ return NULL;
+ }
+ size_t n = yyjson_arr_size(adj);
+ for (size_t i = 0; i < n; i++) {
+ yyjson_val *a = yyjson_arr_get(adj, i);
+ const char *code = jstr(a, "code");
+ int64_t ore = jint(a, "amount_ore");
+ if (!code ||
+ !(in_list(INK2S_PLUS, sizeof INK2S_PLUS / sizeof INK2S_PLUS[0],
+ code) ||
+ in_list(INK2S_MINUS,
+ sizeof INK2S_MINUS / sizeof INK2S_MINUS[0], code))) {
+ set_err(err, "adjustments: unknown INK2S field code");
+ return NULL;
+ }
+ fld_add(&ink2s, code, (ore / 100) * 100);
+ }
+ }
+ int64_t overskott = 0;
+ for (size_t i = 0; i < sizeof INK2S_PLUS / sizeof INK2S_PLUS[0]; i++)
+ overskott += fld_get(&ink2s, INK2S_PLUS[i]) / 100;
+ for (size_t i = 0; i < sizeof INK2S_MINUS / sizeof INK2S_MINUS[0]; i++)
+ overskott -= fld_get(&ink2s, INK2S_MINUS[i]) / 100;
+ if (overskott > 0)
+ fld_add(&ink2s, "7670", overskott * 100);
+ else if (overskott < 0)
+ fld_add(&ink2s, "7770", -overskott * 100);
+ if (fld_get(&ink2s, "7670"))
+ fld_add(&ink2, "7104", fld_get(&ink2s, "7670"));
+ if (fld_get(&ink2s, "7770"))
+ fld_add(&ink2, "7114", fld_get(&ink2s, "7770"));
+
+ /* Submitter for INFO.SRU; defaults to the org itself. */
+ yyjson_val *sub = yyjson_obj_get(args, "submitter");
+ const char *s_name = jstr(sub, "name");
+ const char *s_org = jstr(sub, "org_nr");
+ const char *s_addr = jstr(sub, "address");
+ const char *s_zip = jstr(sub, "postnr");
+ const char *s_city = jstr(sub, "postort");
+ const char *s_contact = jstr(sub, "contact");
+ const char *s_mail = jstr(sub, "email");
+ const char *s_phone = jstr(sub, "phone");
+ if (!s_name)
+ s_name = org_name;
+ if (!s_org)
+ s_org = org_nr;
+ if (!s_addr)
+ s_addr = org_addr;
+ if (!s_zip)
+ s_zip = org_zip;
+ if (!s_city)
+ s_city = org_city;
+ if (!s_mail)
+ s_mail = org_mail;
+ if (!s_phone)
+ s_phone = org_phone;
+ char sub12[13];
+ if (org12(s_org, sub12, err) != 0)
+ return NULL;
+ if (!s_name[0] || !s_zip[0] || !s_city[0]) {
+ set_err(err, "submitter name, postnr and postort are required"
+ " (set them in Företagsuppgifter or pass submitter)");
+ return NULL;
+ }
+
+ char dnow[16], tnow[16], dstart[9], dend[9];
+ now_stamp(dnow, tnow);
+ date_compact(fy_start, dstart);
+ date_compact(fy_end, dend);
+
+ struct buf info;
+ buf_init(&info);
+ add(&info, "#DATABESKRIVNING_START\n#PRODUKT SRU\n");
+ {
+ char line[64];
+ snprintf(line, sizeof line, "#SKAPAD %s %s\n", dnow, tnow);
+ add(&info, line);
+ snprintf(line, sizeof line, "#PROGRAM bokf %s\n", BOKF_VERSION);
+ add(&info, line);
+ }
+ add(&info, "#FILNAMN BLANKETTER.SRU\n#DATABESKRIVNING_SLUT\n");
+ add(&info, "#MEDIELEV_START\n");
+ {
+ char line[64];
+ snprintf(line, sizeof line, "#ORGNR %s\n", sub12);
+ add(&info, line);
+ }
+ if (add_latin1(&info, "#NAMN ", err) != 0 ||
+ add_latin1(&info, s_name, err) != 0)
+ goto fail;
+ add(&info, "\n");
+ if (s_addr[0]) {
+ if (add_latin1(&info, "#ADRESS ", err) != 0 ||
+ add_latin1(&info, s_addr, err) != 0)
+ goto fail;
+ add(&info, "\n");
+ }
+ {
+ char line[64];
+ snprintf(line, sizeof line, "#POSTNR %s\n", s_zip);
+ add(&info, line);
+ }
+ if (add_latin1(&info, "#POSTORT ", err) != 0 ||
+ add_latin1(&info, s_city, err) != 0)
+ goto fail;
+ add(&info, "\n");
+ if (s_contact && s_contact[0]) {
+ if (add_latin1(&info, "#KONTAKT ", err) != 0 ||
+ add_latin1(&info, s_contact, err) != 0)
+ goto fail;
+ add(&info, "\n");
+ }
+ if (s_mail[0]) {
+ if (add_latin1(&info, "#EMAIL ", err) != 0 ||
+ add_latin1(&info, s_mail, err) != 0)
+ goto fail;
+ add(&info, "\n");
+ }
+ if (s_phone[0]) {
+ if (add_latin1(&info, "#TELEFON ", err) != 0 ||
+ add_latin1(&info, s_phone, err) != 0)
+ goto fail;
+ add(&info, "\n");
+ }
+ add(&info, "#MEDIELEV_SLUT\n");
+
+ int assisted = -1, audited = -1;
+ yyjson_val *av = yyjson_obj_get(args, "assisted");
+ if (av && yyjson_is_bool(av))
+ assisted = yyjson_get_bool(av);
+ yyjson_val *auv = yyjson_obj_get(args, "audited");
+ if (auv && yyjson_is_bool(auv))
+ audited = yyjson_get_bool(auv);
+
+ struct buf bl;
+ buf_init(&bl);
+ struct {
+ const char *type;
+ const struct fldset *f;
+ const char *const *order;
+ size_t norder;
+ } blocks[3] = {
+ { type_ink2, &ink2, INK2_ORDER,
+ sizeof INK2_ORDER / sizeof INK2_ORDER[0] },
+ { type_r, &ink2r, INK2R_ORDER,
+ sizeof INK2R_ORDER / sizeof INK2R_ORDER[0] },
+ { type_s, &ink2s, INK2S_ORDER,
+ sizeof INK2S_ORDER / sizeof INK2S_ORDER[0] },
+ };
+ for (size_t i = 0; i < 3; i++) {
+ char line[128];
+ snprintf(line, sizeof line, "#BLANKETT %s\n", blocks[i].type);
+ add(&bl, line);
+ snprintf(line, sizeof line, "#IDENTITET %s %s %s\n", org12buf, dnow,
+ tnow);
+ add(&bl, line);
+ if (add_latin1(&bl, "#NAMN ", err) != 0)
+ goto fail;
+ if (add_latin1(&bl, org_name, err) != 0)
+ goto fail;
+ add(&bl, "\n");
+ snprintf(line, sizeof line, "#UPPGIFT 7011 %s\n", dstart);
+ add(&bl, line);
+ snprintf(line, sizeof line, "#UPPGIFT 7012 %s\n", dend);
+ add(&bl, line);
+ emit_fields(&bl, blocks[i].f, blocks[i].order, blocks[i].norder);
+ if (i == 2) {
+ if (assisted >= 0)
+ add(&bl, assisted ? "#UPPGIFT 8040 X\n"
+ : "#UPPGIFT 8041 X\n");
+ if (audited >= 0)
+ add(&bl, audited ? "#UPPGIFT 8044 X\n"
+ : "#UPPGIFT 8045 X\n");
+ }
+ add(&bl, "#BLANKETTSLUT\n");
+ }
+ add(&bl, "#FIL_SLUT\n");
+
+ yyjson_mut_val *info_file = file_json(doc, "INFO.SRU", &info);
+ yyjson_mut_val *blanketter_file = file_json(doc, "BLANKETTER.SRU", &bl);
+ buf_free(&info);
+ buf_free(&bl);
+
+ yyjson_mut_val *o = yyjson_mut_obj(doc);
+ yyjson_mut_obj_add_strcpy(doc, o, "period", type_base);
+ yyjson_mut_obj_add_strcpy(doc, o, "blankett", type_ink2);
+ yyjson_mut_obj_add_val(doc, o, "info", info_file);
+ yyjson_mut_obj_add_val(doc, o, "blanketter", blanketter_file);
+ if (unmapped.n > 0) {
+ yyjson_mut_val *arr = yyjson_mut_arr(doc);
+ for (size_t i = 0; i < unmapped.n; i++) {
+ yyjson_mut_val *u = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strcpy(doc, u, "account", unmapped.v[i].code);
+ yyjson_mut_obj_add_int(doc, u, "amount_ore", unmapped.v[i].ore);
+ }
+ yyjson_mut_obj_add_val(doc, o, "unmapped", arr);
+ }
+ return o;
+
+fail:
+ buf_free(&info);
+ return NULL;
+}
diff --git a/src/sru.h b/src/sru.h
new file mode 100644
index 0000000..7b5a57a
--- /dev/null
+++ b/src/sru.h
@@ -0,0 +1,15 @@
+#ifndef BOKF_SRU_H
+#define BOKF_SRU_H
+
+#include <sqlite3.h>
+#include <stdint.h>
+
+#include "yyjson.h"
+
+/* Builds Skatteverket's SRU files (INFO.SRU + BLANKETTER.SRU with INK2,
+ INK2R and INK2S) for a fiscal year. Returns a result object with both
+ files base64-encoded, or NULL with *err set. */
+yyjson_mut_val *sru_export(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id,
+ int64_t fy_id, yyjson_val *args, char **err);
+
+#endif