aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/DECISIONS.md14
-rw-r--r--docs/PROTOCOL.md17
-rw-r--r--docs/STATE.md12
-rw-r--r--src/reports.c30
-rw-r--r--src/reports.h15
-rw-r--r--src/sie.c25
-rw-r--r--tests/test_core.c195
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,
diff --git a/src/sie.c b/src/sie.c
index 6e0eab4..c2ce19a 100644
--- a/src/sie.c
+++ b/src/sie.c
@@ -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" },