summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-22 20:01:43 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-22 20:01:43 +0200
commita11dc0aaf7f99e9b606cf1676a04027f4435c596 (patch)
tree1c89c21bf31c54b567dee741d03500f73f3a9691
parent3f07edf64f74cab68cb379e49841fdf6b24674e9 (diff)
downloadbokf-078c219e84d5fbd05b6f3a2e00d4aa367d7b1551.tar.gz
bokf-078c219e84d5fbd05b6f3a2e00d4aa367d7b1551.zip
reports: VAT report skips the momsomföring and imported closingsv0.1.65
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--docs/PROTOCOL.md5
-rw-r--r--docs/STATE.md5
-rw-r--r--src/reports.c21
-rw-r--r--tests/test_core.c47
4 files changed, 74 insertions, 4 deletions
diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md
index a28ff21..75ae963 100644
--- a/docs/PROTOCOL.md
+++ b/docs/PROTOCOL.md
@@ -492,7 +492,10 @@ like the blankett; box 48 positive as filed) and returns `{"org_nr",
bytes are ISO-8859-1, so write them verbatim to a `.xml` file. Both cover
the whole fiscal year; the ledger's
period can be narrowed with `from`/`to`. `report.vat` returns `{"from","to","boxes":[{"box":"05","label":"...","amount_ore":...}],"note"}`.
-Rules sharing a box are summed into a single entry. `box 49` is the sum of
+Rules sharing a box are summed into a single entry. The momsomföring
+itself (any voucher with a row on 2650) and SIE-imported `"Stäng ..."` year
+closings are left out, so a period that includes its own VAT settlement
+still reports the underlying boxes. `box 49` is the sum of
the moms boxes (`10`,`11`,`12`,`30`,`31`,`32`,`48`,`60`,`61`,`62`), so box 48
is signed like the blankett (ingående moms negative); underlag boxes do not
change what is payable.
diff --git a/docs/STATE.md b/docs/STATE.md
index db68efd..639d594 100644
--- a/docs/STATE.md
+++ b/docs/STATE.md
@@ -96,6 +96,11 @@ unit tests and the docs consistency check.
accounts' IB sums to that instead of 0). Merged together with
`eff/imported-closings`, whose "Stäng" skip now applies only to
`sie_import` vouchers. `test_core` has `ib_carry` and `imported_closings`.
+- **Momsrapport (2026-09-22, merged to `main`, not deployed)**:
+ `report.vat` (and `report.vat_eskd`) skips the momsomföring (vouchers with
+ a 2650 row) and SIE-imported `Stäng` closings. Makandra's 2025/26 report
+ was all zeros because V107 (Momsdeklaration) and V109 were counted; the
+ rules themselves were fine. `test_core` has `vat_settlement`.
- **Mail configuration**: Makandra AB (org 2) has **no** `smtp_*` settings
in bokf, so `invoice.send` there is `SMTP_NOT_CONFIGURED` (fine if
invoices are sent elsewhere — set them up when wanted). Mock AB (org 1)
diff --git a/src/reports.c b/src/reports.c
index 96b1344..981d02c 100644
--- a/src/reports.c
+++ b/src/reports.c
@@ -379,6 +379,18 @@ static int vat_box_payable(const char *box)
#define VAT_MAX_BOXES 64
+/* Vouchers left out of the momsdeklaration: the momsomföring itself (any
+ voucher with a 2650 row moves the period's VAT to the redovisningskonto
+ and would zero the boxes) and a source system's imported "Stäng ..." year
+ closings, which zero the P&L underlag. */
+#define VAT_VOUCHER_SQL \
+ " AND NOT EXISTS (SELECT 1 FROM voucher_rows r2 JOIN accounts a2" \
+ " ON a2.org_id = r2.org_id AND a2.id = r2.account_id" \
+ " WHERE r2.org_id = v.org_id AND r2.voucher_id = v.id" \
+ " AND a2.number = '2650')" \
+ " AND NOT (v.source = 'sie_import'" \
+ " AND COALESCE(v.description,'') LIKE 'Stäng%')"
+
yyjson_mut_val *report_vat(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id,
const char *from, const char *to, char **err)
{
@@ -418,14 +430,16 @@ yyjson_mut_val *report_vat(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id,
" 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 a.number=?2 AND v.series <> 'IB'"
- " AND v.series <> ?5 AND v.date BETWEEN ?3 AND ?4";
+ " AND v.series <> ?5 AND v.date BETWEEN ?3 AND ?4"
+ VAT_VOUCHER_SQL;
else if (strcmp(mt, "type") == 0)
sql = "SELECT COALESCE(SUM(r.debit_ore),0),"
"COALESCE(SUM(r.credit_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 a.type=?2 AND v.series <> 'IB'"
- " AND v.series <> ?5 AND v.date BETWEEN ?3 AND ?4";
+ " AND v.series <> ?5 AND v.date BETWEEN ?3 AND ?4"
+ VAT_VOUCHER_SQL;
else
sql = "SELECT COALESCE(SUM(r.debit_ore),0),"
"COALESCE(SUM(r.credit_ore),0) FROM voucher_rows r"
@@ -433,7 +447,8 @@ yyjson_mut_val *report_vat(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id,
" JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
" WHERE r.org_id=?1 AND CAST(a.number AS INTEGER) BETWEEN ?2"
" AND ?3 AND v.series <> 'IB' AND v.series <> ?6"
- " AND v.date BETWEEN ?4 AND ?5";
+ " AND v.date BETWEEN ?4 AND ?5"
+ VAT_VOUCHER_SQL;
if (sqlite3_prepare_v2(db, sql, -1, &qs, NULL) != SQLITE_OK)
break;
sqlite3_bind_int64(qs, 1, org_id);
diff --git a/tests/test_core.c b/tests/test_core.c
index 018b0aa..54e73f5 100644
--- a/tests/test_core.c
+++ b/tests/test_core.c
@@ -4170,6 +4170,52 @@ static void test_moms_rules(struct tctx *t)
yyjson_doc_free(d);
}
+/* The momsomföring (26xx -> 2650) inside the period must not zero the boxes. */
+static void test_vat_settlement(struct tctx *t)
+{
+ (void)t;
+ yyjson_doc *d;
+
+ d = call(reqf("{\"v\":1,\"id\":\"vs1\",\"cmd\":\"org.create\","
+ "\"session\":\"%s\",\"args\":{\"name\":\"Omföring AB\"}}",
+ g_session));
+ CHECK_OK(d);
+ int org = (int)jint(d, "result.id");
+ yyjson_doc_free(d);
+
+ const char *rows[] = {
+ "{\"account\":\"1930\",\"debit_ore\":125000},"
+ "{\"account\":\"3001\",\"credit_ore\":100000},"
+ "{\"account\":\"2611\",\"credit_ore\":25000}",
+ "{\"account\":\"6540\",\"debit_ore\":8000},"
+ "{\"account\":\"2641\",\"debit_ore\":2000},"
+ "{\"account\":\"1930\",\"credit_ore\":10000}",
+ "{\"account\":\"2611\",\"debit_ore\":25000},"
+ "{\"account\":\"2641\",\"credit_ore\":2000},"
+ "{\"account\":\"2650\",\"credit_ore\":23000}",
+ };
+ const char *dates[] = { "2026-03-01", "2026-03-02", "2026-03-31" };
+ for (size_t i = 0; i < 3; i++) {
+ d = call(reqf("{\"v\":1,\"id\":\"vs2\",\"cmd\":\"voucher.post\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":"
+ "\"%s\",\"description\":\"Moms\",\"rows\":[%s]}}",
+ g_session, org, dates[i], rows[i]));
+ CHECK_OK(d);
+ yyjson_doc_free(d);
+ }
+
+ d = call(reqf("{\"v\":1,\"id\":\"vs3\",\"cmd\":\"report.vat\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"from\":\"2026-03-01\",\"to\":\"2026-03-31\"}}",
+ g_session, org));
+ CHECK_OK(d);
+ CHECK(vat_box(d, "05") == 100000);
+ CHECK(vat_box(d, "10") == 25000);
+ CHECK(vat_box(d, "48") == -2000);
+ CHECK(vat_box(d, "49") == 23000);
+ yyjson_doc_free(d);
+}
+
static void test_rules_editor(struct tctx *t)
{
yyjson_doc *d;
@@ -5746,6 +5792,7 @@ static const struct ttest TESTS[] = {
{ "invoice_send", test_invoice_send, "invoices" },
{ "moms_rules", test_moms_rules, "org_members" },
{ "rules_editor", test_rules_editor, "moms_rules" },
+ { "vat_settlement", test_vat_settlement, "org_members" },
{ "bokslut", test_bokslut, "moms_rules" },
{ "sru", test_sru, "bokslut" },
{ "year_close", test_year_close, "fiscal_years" },