diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-22 20:01:43 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-22 20:01:43 +0200 |
| commit | a11dc0aaf7f99e9b606cf1676a04027f4435c596 (patch) | |
| tree | 1c89c21bf31c54b567dee741d03500f73f3a9691 | |
| parent | 3f07edf64f74cab68cb379e49841fdf6b24674e9 (diff) | |
| download | bokf-a11dc0aaf7f99e9b606cf1676a04027f4435c596.tar.gz bokf-a11dc0aaf7f99e9b606cf1676a04027f4435c596.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.md | 5 | ||||
| -rw-r--r-- | docs/STATE.md | 5 | ||||
| -rw-r--r-- | src/reports.c | 21 | ||||
| -rw-r--r-- | tests/test_core.c | 47 |
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" }, |
