aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-18 14:32:23 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-18 14:32:23 +0200
commita5d3ddf6d512bc459cfa450c6b449c2e7a245373 (patch)
treee14c2b28e3cd6d7b744f301b5bf498f5feb8f03e /src
parent07b5e5890fedbf71a1503f984dfbc2baa872223c (diff)
downloadbokf-a5d3ddf6d512bc459cfa450c6b449c2e7a245373.tar.gz
bokf-a5d3ddf6d512bc459cfa450c6b449c2e7a245373.zip
reports: Kapitas-style TUI tables, corrected moms rules (schema v3)v0.1.26
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.
Diffstat (limited to 'src')
-rw-r--r--src/db.c30
-rw-r--r--src/db.h2
-rw-r--r--src/reports.c71
-rw-r--r--src/seed.c32
-rw-r--r--src/seed.h3
5 files changed, 112 insertions, 26 deletions
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 <sqlite3.h>
#include <stdint.h>
-#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