From a5d3ddf6d512bc459cfa450c6b449c2e7a245373 Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Fri, 18 Sep 2026 14:32:23 +0200 Subject: reports: Kapitas-style TUI tables, corrected moms rules (schema v3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The report views dumped JSON; they now render Saldobalans, Resultatrapport (previous-year column, 89xx bokfört/ej bokfört), Balansrapport (Ing balans/Ing saldo/Period/Utg balans, Beräknat resultat) and Momsrapport ruta för ruta, with Swedish amount formatting (1 234,56). The moms starter rules missed 33xx sales, sent reverse-charge VAT 2614 to box 10 instead of 30 and had box 48 positive. Rules may now share a box and report.vat sums them; box 49 is the sum of the moms boxes only. Schema v3 replaces the rules for existing orgs. Verified on a copy of the live DB: 05=703 200, 10=175 800, 20=1 453, 30=851, 48=-1 030, 49=175 621, matching the Kapitas 2027 export. Ctrl+R reload passes --socket and auto-login no longer rewrites tui.conf; pty tests now run through scripts/tui-sandbox.sh so they cannot touch the real config, cache or bw session. --- src/db.c | 30 +++++++++++++++++++++++++ src/db.h | 2 +- src/reports.c | 71 +++++++++++++++++++++++++++++++++++++++++++++-------------- src/seed.c | 32 +++++++++++++++++++-------- src/seed.h | 3 +++ 5 files changed, 112 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/db.c b/src/db.c index c52d763..0de40ea 100644 --- a/src/db.c +++ b/src/db.c @@ -8,6 +8,7 @@ #include "auth.h" #include "config.h" #include "log.h" +#include "seed.h" #include "util.h" static const char SCHEMA_V1[] = @@ -336,6 +337,31 @@ int db_migrate(sqlite3 *db, char **err) } /* Forward-only upgrades for existing databases. */ + +/* v3: the moms starter rules missed 33xx sales, sent 2614 to box 10 and + had box 48 with the wrong sign. Rules were never editable, so replace + them with the corrected defaults for every org. */ +static int db_upgrade_v3(sqlite3 *db, char **err) +{ + if (db_exec(db, "DELETE FROM report_rules WHERE report='vat'", err) != 0) + return -1; + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2(db, "SELECT id FROM orgs", -1, &st, NULL) != + SQLITE_OK) { + set_err(err, "database error"); + return -1; + } + while (sqlite3_step(st) == SQLITE_ROW) { + int64_t id = sqlite3_column_int64(st, 0); + if (seed_vat_rules(db, id, err) != 0) { + sqlite3_finalize(st); + return -1; + } + } + sqlite3_finalize(st); + return 0; +} + static int db_upgrade(sqlite3 *db, int from, char **err) { if (db_exec(db, "BEGIN IMMEDIATE", err) != 0) @@ -344,6 +370,10 @@ static int db_upgrade(sqlite3 *db, int from, char **err) db_exec(db, "ROLLBACK", NULL); return -1; } + if (from < 3 && db_upgrade_v3(db, err) != 0) { + db_exec(db, "ROLLBACK", NULL); + return -1; + } char *sql = sqlite3_mprintf( "UPDATE meta SET value='%d' WHERE key='schema_version'", BOKF_SCHEMA_VERSION); diff --git a/src/db.h b/src/db.h index 2473d93..aa34c81 100644 --- a/src/db.h +++ b/src/db.h @@ -4,7 +4,7 @@ #include #include -#define BOKF_SCHEMA_VERSION 2 +#define BOKF_SCHEMA_VERSION 3 int db_open(const char *path, sqlite3 **out, char **err); int db_migrate(sqlite3 *db, char **err); diff --git a/src/reports.c b/src/reports.c index 1bfdcfb..ee2bbe1 100644 --- a/src/reports.c +++ b/src/reports.c @@ -328,13 +328,20 @@ struct box_label { }; static const struct box_label BOX_LABELS[] = { - { "05", "Momspliktig försäljning 25%" }, - { "06", "Momspliktig försäljning 12%" }, - { "07", "Momspliktig försäljning 6%" }, + { "05", "Momspliktig försäljning som inte ingår i ruta 06, 07 eller 08" }, + { "06", "Momspliktiga uttag" }, + { "07", "Beskattningsunderlag vid vinstmarginalbeskattning" }, { "10", "Utgående moms 25%" }, { "11", "Utgående moms 12%" }, { "12", "Utgående moms 6%" }, - { "48", "Ingående moms" }, + { "20", "Inköp av varor från annat EU-land" }, + { "21", "Inköp av tjänster från annat EU-land enligt huvudregeln" }, + { "30", "Utgående moms 25%" }, + { "31", "Utgående moms 12%" }, + { "32", "Utgående moms 6%" }, + { "41", "Försäljning när köparen är betalningsskyldig i Sverige" }, + { "48", "Ingående moms att dra av" }, + { "49", "Moms att betala eller få tillbaka" }, }; static const char *box_label(const char *box) @@ -345,6 +352,19 @@ static const char *box_label(const char *box) return "Ruta"; } +/* Boxes that affect box 49; every other box is only a base (underlag). */ +static int vat_box_payable(const char *box) +{ + static const char *const boxes[] = { "10", "11", "12", "30", "31", + "32", "48", "60", "61", "62" }; + for (size_t i = 0; i < sizeof boxes / sizeof boxes[0]; i++) + if (strcmp(box, boxes[i]) == 0) + return 1; + return 0; +} + +#define VAT_MAX_BOXES 64 + yyjson_mut_val *report_vat(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id, const char *from, const char *to, char **err) { @@ -352,8 +372,11 @@ yyjson_mut_val *report_vat(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id, set_err(err, "from and to must be YYYY-MM-DD"); return NULL; } - yyjson_mut_val *boxes = yyjson_mut_arr(doc); - int64_t out_total = 0, in_total = 0; + struct { + char box[8]; + int64_t amount; + } acc[VAT_MAX_BOXES]; + size_t nacc = 0; sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2( db, @@ -408,20 +431,36 @@ yyjson_mut_val *report_vat(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id, sqlite3_column_int64(qs, 1)) * sign; sqlite3_finalize(qs); - if (strcmp(box, "48") == 0) - in_total += amount; - else - out_total += amount; - yyjson_mut_val *o = yyjson_mut_arr_add_obj(doc, boxes); - yyjson_mut_obj_add_strcpy(doc, o, "box", box); - yyjson_mut_obj_add_strcpy(doc, o, "label", box_label(box)); - yyjson_mut_obj_add_int(doc, o, "amount_ore", amount); + size_t i = 0; + for (; i < nacc; i++) { + if (strcmp(acc[i].box, box) == 0) { + acc[i].amount += amount; + break; + } + } + if (i == nacc && nacc < VAT_MAX_BOXES) { + snprintf(acc[nacc].box, sizeof acc[nacc].box, "%s", box); + acc[nacc].amount = amount; + nacc++; + } } sqlite3_finalize(st); + + yyjson_mut_val *boxes = yyjson_mut_arr(doc); + int64_t payable = 0; + for (size_t i = 0; i < nacc; i++) { + yyjson_mut_val *o = yyjson_mut_arr_add_obj(doc, boxes); + yyjson_mut_obj_add_strcpy(doc, o, "box", acc[i].box); + yyjson_mut_obj_add_strcpy(doc, o, "label", box_label(acc[i].box)); + yyjson_mut_obj_add_int(doc, o, "amount_ore", acc[i].amount); + if (vat_box_payable(acc[i].box)) + payable += acc[i].amount; + } yyjson_mut_val *o = yyjson_mut_arr_add_obj(doc, boxes); yyjson_mut_obj_add_strcpy(doc, o, "box", "49"); - yyjson_mut_obj_add_strcpy(doc, o, "label", "Moms att betala eller få tillbaka"); - yyjson_mut_obj_add_int(doc, o, "amount_ore", out_total - in_total); + yyjson_mut_obj_add_strcpy(doc, o, "label", + "Moms att betala eller få tillbaka"); + yyjson_mut_obj_add_int(doc, o, "amount_ore", payable); yyjson_mut_val *res = yyjson_mut_obj(doc); yyjson_mut_obj_add_strcpy(doc, res, "from", from); diff --git a/src/seed.c b/src/seed.c index cd1bf71..7be0535 100644 --- a/src/seed.c +++ b/src/seed.c @@ -216,16 +216,25 @@ struct vat_rule { }; /* Starter mapping from BAS account ranges to momsdeklaration boxes. It - covers the common domestic cases only and must be reviewed against the - current Skatteverket blankett before filing. */ + covers the common domestic and EU cases; rules may share a box, the + report sums them. Must be reviewed against the current Skatteverket + blankett before filing. */ static const struct vat_rule VAT_RULES[] = { - { "05", "range", "3001-3019", -1, 10 }, - { "06", "range", "3021-3029", -1, 20 }, - { "07", "range", "3031-3039", -1, 30 }, - { "10", "range", "2610-2619", -1, 40 }, - { "11", "range", "2620-2629", -1, 50 }, - { "12", "range", "2630-2639", -1, 60 }, - { "48", "range", "2640-2649", 1, 70 }, + { "05", "range", "3000-3019", -1, 10 }, + { "05", "range", "3100-3199", -1, 11 }, + { "05", "range", "3300-3399", -1, 12 }, + { "41", "range", "3200-3299", -1, 20 }, + { "06", "range", "3020-3029", -1, 30 }, + { "07", "range", "3030-3039", -1, 40 }, + { "10", "range", "2610-2613", -1, 50 }, + { "11", "range", "2620-2623", -1, 60 }, + { "12", "range", "2630-2633", -1, 70 }, + { "20", "range", "4510-4529", 1, 80 }, + { "21", "range", "4530-4549", 1, 90 }, + { "30", "range", "2614-2619", -1, 100 }, + { "31", "range", "2624-2629", -1, 110 }, + { "32", "range", "2634-2639", -1, 120 }, + { "48", "range", "2640-2649", -1, 130 }, }; static int seed_rules(sqlite3 *db, int64_t org_id, char **err) @@ -256,6 +265,11 @@ static int seed_rules(sqlite3 *db, int64_t org_id, char **err) return 0; } +int seed_vat_rules(sqlite3 *db, int64_t org_id, char **err) +{ + return seed_rules(db, org_id, err); +} + static int seed_default_fiscal_year(sqlite3 *db, int64_t org_id, int fy_start_month, int64_t *out_fy, char **err) diff --git a/src/seed.h b/src/seed.h index 826391b..7192398 100644 --- a/src/seed.h +++ b/src/seed.h @@ -9,4 +9,7 @@ int seed_org(sqlite3 *db, int64_t org_id, const char *framework, int fy_start_month, int64_t *out_fy_id, char **err); +/* Inserts the default moms report rules for an org (no transaction). */ +int seed_vat_rules(sqlite3 *db, int64_t org_id, char **err); + #endif -- cgit v1.3