diff options
| -rw-r--r-- | docs/DECISIONS.md | 14 | ||||
| -rw-r--r-- | docs/PROTOCOL.md | 17 | ||||
| -rw-r--r-- | docs/STATE.md | 12 | ||||
| -rw-r--r-- | src/reports.c | 30 | ||||
| -rw-r--r-- | src/reports.h | 15 | ||||
| -rw-r--r-- | src/sie.c | 25 | ||||
| -rw-r--r-- | tests/test_core.c | 195 |
7 files changed, 233 insertions, 75 deletions
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index a109bda..5499610 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -335,13 +335,13 @@ kept verbatim from the STATE.md they were pruned from (2026-09-21). årsredovisning export flags incomplete jämförelsetal for those years and points to the previous year's annual report. **Amended 2026-09-22**: the income statement (and therefore INK2/SRU and the TUI resultatrapport) now - skips the same "Stäng ..." vouchers, so a year being declared shows its - real figures; the balance sheet keeps them (the result sits in 2099 and - must not be counted twice). The source's own #IB/#UB corrections that the - vouchers do not reproduce (e.g. Makandra's FY 2022/2023 result transfer - is 17 857.63 kr short of that year's P&L) remain a data divergence: the - derived balance sheet is off by that amount until a correction is booked - in the current year. + skips the same "Stäng ..." vouchers when they are SIE-imported, so a year + being declared shows its real figures; the balance sheet keeps them (the + result sits in 2099 and must not be counted twice). The source's own + #IB/#UB corrections that the vouchers do not reproduce (e.g. Makandra's FY + 2022/2023 result transfer is 17 857.63 kr short of that year's P&L) remain + a data divergence: the derived balance sheet is off by that amount until a + correction is booked in the current year. 11. ~~SIE import only into an empty fiscal year; consider broader import.~~ Chronological multi-year import works (CRLF, `#RAR 0`, zero rows, `#IB` rule handled); each year must still target an empty fiscal year. Note: diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 7d7a5e2..a28ff21 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -451,11 +451,17 @@ linked to that voucher and each item carries that `voucher_id`. | `sru.export` | `fiscal_year`, `adjustments?`, `submitter?`, `assisted?`, `audited?`, `ignore_unmapped?` | `INFO.SRU` + `BLANKETTER.SRU` (ISO-8859-1, base64) | All reports are pure reads, respect locks, and return JSON rows. Amounts are -öre. `report.income_statement` (and therefore `sru.export` and the TUI +öre. IB (ingående balans) for a balance account (asset, liability, equity) is +all earlier history, including earlier years' `IB` vouchers, plus this +year's `IB` vouchers; a P&L account (revenue, expense) restarts at zero at +every fiscal-year start, so its IB is only this year's `IB` vouchers and, +with a narrowed `from`, the year's movements before it. +`report.income_statement` (and therefore `sru.export` and the TUI resultatrapport) ignores the source system's `"Stäng ..."` closing vouchers -in imported history years, where the P&L accounts are closed straight to -2099 and would otherwise net to zero; the K2 report uses the same rule. -`report.general_ledger` (huvudbok) returns account blocks: +in SIE-imported years (`source:"sie_import"`), where the P&L accounts are +closed straight to 2099 and would otherwise net to zero; the TUI +årsredovisning uses the same rule. `report.general_ledger` (huvudbok) +returns account blocks: `{"fiscal_year","from","to","last_voucher":{...},"accounts":[{"account", "name","ib_ore","debit_ore","credit_ore","ub_ore","rows":[{"series", "number","date","description","row_description","debit_ore","credit_ore", @@ -523,7 +529,8 @@ support `dry_run`, which validates without writing. | `sie.import` | `content_base64` or `path`, `dry_run?` | one file per call; creates missing accounts and posts #VER as `source:"sie_import"`; only into an empty org fiscal year; `#IB` becomes an `IB` voucher when the year has no earlier history, otherwise the earlier vouchers carry the balances | SIE 4 files are written in CP437 with PC8 format, `#SIETYP 4`, `#FNR`, `#ORGNR`, -`#KONTO`, `#IB`, `#UB`, `#RES`, `#VER`, `#TRANS`. Import is the migration path +`#KONTO`, `#IB`, `#UB`, `#RES`, `#VER`, `#TRANS`. `#IB`/`#UB` are written for +balance accounts and `#RES` for P&L accounts, with IB as in §7.6. Import is the migration path from Fortnox/Visma/BL and must be dry-run first; it reports exactly what would be created. diff --git a/docs/STATE.md b/docs/STATE.md index 19f22f6..db68efd 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -17,7 +17,7 @@ unit tests and the docs consistency check. - **Deklaration 2025/2026 (org 2, 2026-09-22)**: the imported year is closed by the source's `Stäng intäktskonton/kostnadskonton`, so the resultatrapport (and the INK2/SRU derived from it) showed 0. `report.income_statement` now - skips those vouchers (branch `eff/imported-closings`), which gives + skips those vouchers (merged to `main`, not deployed), which gives **241 817,48 kr** resultat efter skatt för 2025/2026. In the TUI the "Bokfört resultat" section is absent for such years (the close goes straight to 2099). Two **data drifts** to settle with the accountant before filing: @@ -86,6 +86,16 @@ unit tests and the docs consistency check. `TUI-GUIDELINES.md`: drafts of encrypted fields (an employee's personnummer) and `Enter` on action rows. Spec in `TUI-GUIDELINES.md` "Interaction model"; decisions in `DECISIONS.md` #28. +- **IB carry-forward (2026-09-22, merged to `main`, not deployed)**: + reports and `sie.export` carry earlier years' `IB` vouchers into the + opening balance (Makandra's 2021 IB voucher held aktiekapital 2081/1940, + which vanished from every later year) and restart P&L accounts at each + fiscal-year start (they used to accumulate since 2021). SIE writes + `#IB`/`#UB` only for balance accounts and `#RES` only for P&L. Makandra's + opening balances now show the 17 857,63 kr 2022/23 gap openly (balance + 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`. - **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 0316abf..96b1344 100644 --- a/src/reports.c +++ b/src/reports.c @@ -58,12 +58,12 @@ static yyjson_mut_val *fy_json(yyjson_mut_doc *doc, const struct fy_info *fy) return o; } -/* One row per account with IB and period movements. IB is the series "IB" - voucher(s) of this fiscal year plus all non-IB history before `from`. - skip_closings drops the source system's "Stäng ..." closing vouchers from - the period movements (imported years close the P&L accounts straight to - 2099, so the year otherwise nets to zero); the K2 report and the SRU - export use the same convention. Amounts signed: debit positive. */ +/* One row per account with IB and period movements; IB as defined by + REPORT_IB_ROW_SQL. skip_closings drops the source system's "Stäng ..." + closing vouchers (SIE-imported only) from the period movements: imported + years close the P&L accounts straight to 2099, so the year otherwise nets + to zero; the TUI årsredovisning uses the same convention. Amounts signed: + debit positive. */ static yyjson_mut_val *balance_query(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id, int64_t fy_id, const char *from, const char *to, @@ -73,21 +73,21 @@ static yyjson_mut_val *balance_query(yyjson_mut_doc *doc, sqlite3 *db, char ib_series[16]; db_setting_copy(db, org_id, "series_ib", "IB", ib_series, sizeof ib_series); - char sql[1280]; + char sql[2048]; snprintf(sql, sizeof sql, "SELECT a.number,a.name,a.type," " COALESCE(SUM(CASE WHEN v.series <> 'IB' AND v.series <> ?5" - " AND (COALESCE(v.description,'') NOT LIKE 'Stäng%%' OR ?6 = 0)" + " AND (?6 = 0 OR v.source <> 'sie_import'" + " OR COALESCE(v.description,'') NOT LIKE 'Stäng%%')" " AND v.date BETWEEN ?2 AND ?3 THEN r.debit_ore END),0)," " COALESCE(SUM(CASE WHEN v.series <> 'IB' AND v.series <> ?5" - " AND (COALESCE(v.description,'') NOT LIKE 'Stäng%%' OR ?6 = 0)" + " AND (?6 = 0 OR v.source <> 'sie_import'" + " OR COALESCE(v.description,'') NOT LIKE 'Stäng%%')" " AND v.date BETWEEN ?2 AND ?3 THEN r.credit_ore END),0)," - " COALESCE(SUM(CASE WHEN ((v.series = 'IB' OR v.series = ?5)" - " AND v.fiscal_year_id = ?4) OR (v.series <> 'IB'" - " AND v.series <> ?5 AND v.date < ?2) THEN r.debit_ore END),0)," - " COALESCE(SUM(CASE WHEN ((v.series = 'IB' OR v.series = ?5)" - " AND v.fiscal_year_id = ?4) OR (v.series <> 'IB'" - " AND v.series <> ?5 AND v.date < ?2) THEN r.credit_ore END),0)" + " COALESCE(SUM(CASE WHEN " REPORT_IB_ROW_SQL + " THEN r.debit_ore END),0)," + " COALESCE(SUM(CASE WHEN " REPORT_IB_ROW_SQL + " THEN r.credit_ore END),0)" " FROM accounts a" " LEFT JOIN (voucher_rows r JOIN vouchers v" " ON v.org_id=r.org_id AND v.id=r.voucher_id)" diff --git a/src/reports.h b/src/reports.h index c4a6e4e..82c29e5 100644 --- a/src/reports.h +++ b/src/reports.h @@ -6,6 +6,21 @@ #include "yyjson.h" +/* SQL condition: voucher row `r` of voucher `v` on account `a` belongs to the + opening balance of fiscal year ?4 (org ?1) for a period starting ?2; ?5 is + the configured IB series. Balance accounts carry all earlier history, + including earlier years' IB vouchers; P&L accounts restart at the year + start, so only this year's IB vouchers and movements before ?2 count. */ +#define REPORT_IB_ROW_SQL \ + "((v.series = 'IB' OR v.series = ?5) AND (v.fiscal_year_id = ?4" \ + " OR (v.date < (SELECT f.start_date FROM fiscal_years f" \ + " WHERE f.org_id = ?1 AND f.id = ?4)" \ + " AND a.type NOT IN ('revenue','expense'))))" \ + " OR (v.series <> 'IB' AND v.series <> ?5 AND v.date < ?2" \ + " AND (a.type NOT IN ('revenue','expense') OR v.date >=" \ + " (SELECT f.start_date FROM fiscal_years f" \ + " WHERE f.org_id = ?1 AND f.id = ?4)))" + yyjson_mut_val *report_trial_balance(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id, int64_t fy_id, const char *from, const char *to, @@ -11,6 +11,7 @@ #include "db.h" #include "ledger.h" #include "log.h" +#include "reports.h" #include "util.h" #include "version.h" @@ -136,24 +137,21 @@ int sie_export_file(sqlite3 *db, int64_t org_id, int64_t fy_id, put_date(f, end); put(f, "\n"); - /* accounts and balances: IB = this year's IB-series voucher(s) plus all - non-IB history before the year; UB = everything through the end; - RES = the year's non-IB movements. */ + /* accounts and balances: IB per REPORT_IB_ROW_SQL, UB = IB plus the + year's non-IB movements, RES = those movements. #IB/#UB are written + for balance accounts only, #RES for P&L accounts only. */ char ib_series[16]; db_setting_copy(db, org_id, "series_ib", "IB", ib_series, sizeof ib_series); if (sqlite3_prepare_v2( db, "SELECT a.number,a.name," - " COALESCE(SUM(CASE WHEN ((v.series = 'IB' OR v.series = ?5)" - " AND v.fiscal_year_id = ?4) OR (v.series <> 'IB'" - " AND v.series <> ?5 AND v.date < ?2)" - " THEN r.debit_ore - r.credit_ore END),0)," - " COALESCE(SUM(CASE WHEN v.date <= ?3" + " COALESCE(SUM(CASE WHEN " REPORT_IB_ROW_SQL " THEN r.debit_ore - r.credit_ore END),0)," " COALESCE(SUM(CASE WHEN v.series <> 'IB' AND v.series <> ?5" " AND v.date >= ?2 AND v.date <= ?3" - " THEN r.debit_ore - r.credit_ore END),0)" + " THEN r.debit_ore - r.credit_ore END),0)," + " a.type IN ('revenue','expense')" " FROM accounts a LEFT JOIN (voucher_rows r JOIN vouchers v" " ON v.org_id=r.org_id AND v.id=r.voucher_id)" " ON r.org_id=a.org_id AND r.account_id=a.id" @@ -173,8 +171,13 @@ int sie_export_file(sqlite3 *db, int64_t org_id, int64_t fy_id, const char *number = (const char *)sqlite3_column_text(st, 0); const char *name = (const char *)sqlite3_column_text(st, 1); int64_t ib = sqlite3_column_int64(st, 2); - int64_t ub = sqlite3_column_int64(st, 3); - int64_t res = sqlite3_column_int64(st, 4); + int64_t res = sqlite3_column_int64(st, 3); + int pl = sqlite3_column_int(st, 4); + int64_t ub = ib + res; + if (pl) + ib = ub = 0; + else + res = 0; put(f, "#KONTO "); put(f, number); put(f, " "); diff --git a/tests/test_core.c b/tests/test_core.c index 1d86b2b..018b0aa 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -1575,44 +1575,30 @@ static void test_imported_closings(struct tctx *t) CHECK(org > 0); yyjson_doc_free(d); - d = call(reqf("{\"v\":1,\"id\":\"ic2\",\"cmd\":\"voucher.post\"," - "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" - "\"2026-02-01\",\"description\":\"Försäljning\",\"rows\":[" - "{\"account\":\"1930\",\"debit_ore\":125000}," - "{\"account\":\"3001\",\"credit_ore\":100000}," - "{\"account\":\"2611\",\"credit_ore\":25000}]}}", - g_session, (int)org)); - CHECK_OK(d); - yyjson_doc_free(d); - - d = call(reqf("{\"v\":1,\"id\":\"ic3\",\"cmd\":\"voucher.post\"," - "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" - "\"2026-02-02\",\"description\":\"Inköp\",\"rows\":[" - "{\"account\":\"5410\",\"debit_ore\":20000}," - "{\"account\":\"2640\",\"debit_ore\":5000}," - "{\"account\":\"1930\",\"credit_ore\":25000}]}}", - g_session, (int)org)); - CHECK_OK(d); - yyjson_doc_free(d); - /* The source system closes the P&L accounts straight to 2099; the income - statement must still show the year's real figures. */ - d = call(reqf("{\"v\":1,\"id\":\"ic4\",\"cmd\":\"voucher.post\"," - "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" - "\"2026-04-30\",\"description\":\"Stäng intäktskonton\"," - "\"rows\":[{\"account\":\"3001\",\"debit_ore\":100000}," - "{\"account\":\"2099\",\"credit_ore\":100000}]}}", - g_session, (int)org)); - CHECK_OK(d); - yyjson_doc_free(d); - - d = call(reqf("{\"v\":1,\"id\":\"ic5\",\"cmd\":\"voucher.post\"," - "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" - "\"2026-04-30\",\"description\":\"Stäng kostnadskonton\"," - "\"rows\":[{\"account\":\"2099\",\"debit_ore\":20000}," - "{\"account\":\"5410\",\"credit_ore\":20000}]}}", - g_session, (int)org)); + statement must still show the year's real figures. The file is PC8 + (CP437) like real exports: \x84 = ä, \x94 = ö. */ + static const char sie[] = + "#FLAGGA 0\n" + "#FORMAT PC8\n" + "#SIETYP 4\n" + "#FNAMN \"Import AB\"\n" + "#RAR 0 20260101 20261231\n" + "#VER \"V\" \"1\" 20260201 \"F\x94rs\x84" "ljning\"\n" + "{\n#TRANS 1930 {} 1250.00\n#TRANS 3001 {} -1000.00\n" + "#TRANS 2611 {} -250.00\n}\n" + "#VER \"V\" \"2\" 20260202 \"Ink\x94" "p\"\n" + "{\n#TRANS 5410 {} 200.00\n#TRANS 2640 {} 50.00\n" + "#TRANS 1930 {} -250.00\n}\n" + "#VER \"V\" \"3\" 20260430 \"St\x84" "ng intäktskonton\"\n" + "{\n#TRANS 3001 {} 1000.00\n#TRANS 2099 {} -1000.00\n}\n" + "#VER \"V\" \"4\" 20260430 \"St\x84" "ng kostnadskonton\"\n" + "{\n#TRANS 2099 {} 200.00\n#TRANS 5410 {} -200.00\n}\n"; + char *sie_b64 = util_b64((const unsigned char *)sie, strlen(sie)); + d = call_sie_import(g_session, org, sie_b64, 0); + free(sie_b64); CHECK_OK(d); + CHECK(jint(d, "result.vouchers") == 4); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"ic6\",\"cmd\":\"report.income_statement\"," @@ -1667,6 +1653,22 @@ static void test_imported_closings(struct tctx *t) CHECK(strstr(sru, "#UPPGIFT 7650 800") != NULL); free(sru); yyjson_doc_free(d); + + /* a voucher entered in bokf is never skipped, whatever its text */ + d = call(reqf("{\"v\":1,\"id\":\"ic10\",\"cmd\":\"voucher.post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" + "\"2026-05-02\",\"description\":\"Stängsel\",\"rows\":[" + "{\"account\":\"1930\",\"debit_ore\":100}," + "{\"account\":\"3001\",\"credit_ore\":100}]}}", + g_session, (int)org)); + CHECK_OK(d); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"ic11\",\"cmd\":\"report.income_statement\"," + "\"session\":\"%s\",\"org\":%d}", + g_session, (int)org)); + CHECK_OK(d); + CHECK(jint(d, "result.result_ore") == 80100); + yyjson_doc_free(d); } static void test_general_ledger(struct tctx *t) @@ -2175,6 +2177,126 @@ static void test_ib(struct tctx *t) yyjson_doc_free(d); } +/* Opening balances carry earlier years' IB vouchers; P&L accounts restart + at every year start (reports and SIE export). */ +static void test_ib_carry(struct tctx *t) +{ + (void)t; + yyjson_doc *d; + + d = call(reqf("{\"v\":1,\"id\":\"ibc1\",\"cmd\":\"org.create\"," + "\"session\":\"%s\",\"args\":{\"name\":\"Överföring AB\"}}", + g_session)); + CHECK_OK(d); + int org = (int)jint(d, "result.id"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"ibc2\",\"cmd\":\"fiscal_year.list\"," + "\"session\":\"%s\",\"org\":%d}", + g_session, org)); + CHECK_OK(d); + char start[11] = "", end[11] = ""; + snprintf(start, sizeof start, "%s", jstr(d, "result.items.0.start_date")); + snprintf(end, sizeof end, "%s", jstr(d, "result.items.0.end_date")); + yyjson_doc_free(d); + int y = atoi(end) + 1; + + const char *posts[][4] = { + { start, "IB", "1940", "2010" }, + { start, "V", "1930", "3001" }, + { end, "V", "8999", "2099" }, + }; + const int64_t amounts[] = { 2500000, 100000, 100000 }; + for (size_t i = 0; i < 3; i++) { + d = call(reqf("{\"v\":1,\"id\":\"ibc3\",\"cmd\":\"voucher.post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" + "\"%s\",\"description\":\"År 1\",\"series\":\"%s\"," + "\"rows\":[{\"account\":\"%s\",\"debit_ore\":%lld}," + "{\"account\":\"%s\",\"credit_ore\":%lld}]}}", + g_session, org, posts[i][0], posts[i][1], posts[i][2], + (long long)amounts[i], posts[i][3], + (long long)amounts[i])); + CHECK_OK(d); + yyjson_doc_free(d); + } + + d = call(reqf("{\"v\":1,\"id\":\"ibc4\",\"cmd\":\"fiscal_year.open\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"label\":\"%d\"," + "\"start_date\":\"%d-01-01\",\"end_date\":\"%d-12-31\"}}", + g_session, org, y, y, y)); + CHECK_OK(d); + int64_t fy2 = jint(d, "result.id"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"ibc5\",\"cmd\":\"voucher.post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" + "\"%d-02-01\",\"description\":\"År 2\",\"rows\":" + "[{\"account\":\"1930\",\"debit_ore\":5000}," + "{\"account\":\"3001\",\"credit_ore\":5000}]}}", + g_session, org, y)); + CHECK_OK(d); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ibc6\",\"cmd\":\"report.trial_balance\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" + "%lld}}", + g_session, org, (long long)fy2)); + CHECK_OK(d); + CHECK(find_amount(d, "result.accounts", "account", "1940", "ib_ore") == + 2500000); + CHECK(find_amount(d, "result.accounts", "account", "2010", "ib_ore") == + -2500000); + CHECK(find_amount(d, "result.accounts", "account", "2099", "ib_ore") == + -100000); + CHECK(find_amount(d, "result.accounts", "account", "3001", "ib_ore") == 0); + CHECK(find_amount(d, "result.accounts", "account", "8999", "ib_ore") == -1); + CHECK(jint(d, "result.totals.ib_ore") == 0); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ibc7\",\"cmd\":\"report.trial_balance\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" + "%lld,\"from\":\"%d-03-01\"}}", + g_session, org, (long long)fy2, y)); + CHECK_OK(d); + CHECK(find_amount(d, "result.accounts", "account", "3001", "ib_ore") == + -5000); + CHECK(find_amount(d, "result.accounts", "account", "1930", "ib_ore") == + 105000); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ibc8\",\"cmd\":\"report.balance_sheet\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" + "%lld}}", + g_session, org, (long long)fy2)); + CHECK_OK(d); + CHECK(find_amount(d, "result.equity.accounts", "account", "2010", + "amount_ore") == 2500000); + CHECK(jint(d, "result.assets.total_ore") == 2605000); + CHECK(jint(d, "result.assets.total_ore") == + jint(d, "result.liabilities.total_ore") + + jint(d, "result.equity.total_ore")); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ibc9\",\"cmd\":\"sie.export\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" + "%lld}}", + g_session, org, (long long)fy2)); + CHECK_OK(d); + FILE *sf = fopen(jstr(d, "result.path"), "rb"); + yyjson_doc_free(d); + CHECK(sf != NULL); + if (sf) { + char sbuf[65536]; + size_t sn = fread(sbuf, 1, sizeof sbuf - 1, sf); + sbuf[sn] = '\0'; + fclose(sf); + CHECK(strstr(sbuf, "#IB 0 2010 -25000.00") != NULL); + CHECK(strstr(sbuf, "#UB 0 1930 1050.00") != NULL); + CHECK(strstr(sbuf, "#RES 0 3001 -50.00") != NULL); + CHECK(strstr(sbuf, "#IB 0 3001") == NULL); + CHECK(strstr(sbuf, "#UB 0 3001") == NULL); + CHECK(strstr(sbuf, "#RES 0 1930") == NULL); + } +} + static void test_period_lock(struct tctx *t) { yyjson_doc *d; @@ -5611,6 +5733,7 @@ static const struct ttest TESTS[] = { { "attachments", test_attachments, "vouchers idempotency" }, { "templates", test_templates, "org_members" }, { "ib", test_ib, "org_members" }, + { "ib_carry", test_ib_carry, "org_members" }, { "period_lock", test_period_lock, "fiscal_years" }, { "sie_export", test_sie_export, "ib" }, { "sie_import", test_sie_import, "attachments sie_export" }, |
