From ca267e6b1c727be254c6229fc1ed3e2c5a0d6065 Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Tue, 22 Sep 2026 22:48:14 +0200 Subject: sru, tui: round every account to whole kronor; fix the årsredovisning Stäng skip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- clients/screens_bokslut.c | 92 ++++++++++++++++++++++++++++------------------- clients/screens_reports.c | 2 +- docs/PROTOCOL.md | 14 +++++--- docs/STATE.md | 11 ++++++ src/reports.c | 5 ++- src/sru.c | 66 +++++++++++++++++++++++++--------- src/util.c | 5 +++ src/util.h | 5 +++ tests/test_core.c | 25 ++++++++++--- 9 files changed, 160 insertions(+), 65 deletions(-) diff --git a/clients/screens_bokslut.c b/clients/screens_bokslut.c index 96c6fe9..a4776c9 100644 --- a/clients/screens_bokslut.c +++ b/clients/screens_bokslut.c @@ -311,13 +311,17 @@ struct ar_year { char label[8]; char from[16], to[16]; int64_t netto, ror, res_fin, skatt, res, disp; + int64_t res_exact; int64_t fin_in, fin_out; int64_t groups[AR_GROUPS]; }; /* Balance-sheet amounts are shown the way an annual report reads them: - credit balances (equity, liabilities) as positive numbers. */ -static int64_t ar_bs_ib_sum(yyjson_val *res, int lo, int hi) + credit balances (equity, liabilities) as positive numbers. Each account + is rounded to whole kronor before it is summed (util_round_kr), so every + line and total adds up; exact keeps öre for the rounding balance. */ +static int64_t ar_bs_total(yyjson_val *res, int lo, int hi, const char *field, + int exact) { int64_t s = 0; if (!res) @@ -330,37 +334,45 @@ static int64_t ar_bs_ib_sum(yyjson_val *res, int lo, int hi) yyjson_val *row = yyjson_arr_get(arr, i); if (!acct_in(vstr(row, "account"), lo, hi)) continue; - int64_t ib = vint(row, "ib_ore"); - s += lo >= 2000 ? -ib : ib; + int64_t v = vint(row, field); + if (lo >= 2000) + v = -v; + s += exact ? v : util_round_kr(v); } } return s; } +static int64_t ar_bs_ib_sum(yyjson_val *res, int lo, int hi) +{ + return ar_bs_total(res, lo, hi, "ib_ore", 0); +} + static int64_t ar_bs_sum(yyjson_val *res, int lo, int hi) { - int64_t s = 0; - if (!res) - return 0; - for (int k = 0; k < 3; k++) { - yyjson_val *sec = yyjson_obj_get(res, BS_KEYS[k]); - yyjson_val *arr = sec ? yyjson_obj_get(sec, "accounts") : NULL; - size_t n = yyjson_arr_size(arr); - for (size_t i = 0; i < n; i++) { - yyjson_val *row = yyjson_arr_get(arr, i); - if (!acct_in(vstr(row, "account"), lo, hi)) - continue; - int64_t ub = vint(row, "ub_ore"); - s += lo >= 2000 ? -ub : ub; - } - } - return s; + return ar_bs_total(res, lo, hi, "ub_ore", 0); +} + +/* The krona that rounding leaves between the balance sheet's two sides, to + be carried by Balanserat resultat. 2099 is left out: Årets resultat is + the income statement's figure. A real imbalance stays visible. */ +static int64_t ar_bs_rounding(yyjson_val *res, const char *field, + int64_t result_kr, int64_t result_exact) +{ + int64_t a_kr = ar_bs_total(res, 1000, 1999, field, 0); + int64_t a_ex = ar_bs_total(res, 1000, 1999, field, 1); + int64_t el_kr = ar_bs_total(res, 2000, 2098, field, 0) + + ar_bs_total(res, 2100, 2999, field, 0) + result_kr; + int64_t el_ex = ar_bs_total(res, 2000, 2098, field, 1) + + ar_bs_total(res, 2100, 2999, field, 1) + result_exact; + return (a_kr - el_kr) - util_round_kr(a_ex - el_ex); } -/* One year's income statement from its general ledger. Vouchers whose - description starts with "Stäng" are the source system's closings in - imported history years and are skipped, so those years show their real - figures instead of a zeroed P&L. */ +/* One year's income statement from its general ledger. SIE-imported + vouchers whose description starts with "Stäng" are the source system's + closings and are skipped (the same rule as report.income_statement), so + those years show their real figures instead of a zeroed P&L. Each account + is rounded to whole kronor before it is summed. */ static void ar_load_year(struct ar_year *y, yyjson_val *gl) { memset(y, 0, sizeof *y); @@ -381,15 +393,18 @@ static void ar_load_year(struct ar_year *y, yyjson_val *gl) size_t nr = yyjson_arr_size(rows); for (size_t k = 0; k < nr; k++) { yyjson_val *row = yyjson_arr_get(rows, k); - if (strncmp(vstr(row, "description"), "Stäng", 7) == 0) + if (strcmp(vstr(row, "source"), "sie_import") == 0 && + strncmp(vstr(row, "description"), "Stäng", + strlen("Stäng")) == 0) continue; net += vint(row, "debit_ore") - vint(row, "credit_ore"); } - int64_t disp = -net; + int64_t disp = util_round_kr(-net); int done = 0; for (size_t g = 0; g < AR_GROUPS; g++) { if (acct_in(num, IS_GROUPS[g].lo, IS_GROUPS[g].hi)) { y->groups[g] += disp; + y->res_exact -= net; done = 1; break; } @@ -404,6 +419,9 @@ static void ar_load_year(struct ar_year *y, yyjson_val *gl) y->disp += disp; else if (n >= 8900 && n <= 8989) y->skatt += disp; + else + continue; + y->res_exact -= net; } int64_t rev = 0, cost = 0; for (size_t g = 0; g < AR_GROUPS; g++) @@ -590,8 +608,11 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, int64_t ak_ib = ar_bs_ib_sum(bs, 2081, 2089); int64_t ak_ub = ar_bs_sum(bs, 2081, 2089); - int64_t br_ib = ar_bs_ib_sum(bs, 2091, 2098); - int64_t br_ub = ar_bs_sum(bs, 2091, 2098); + int64_t br_ib = ar_bs_ib_sum(bs, 2091, 2098) + + ar_bs_rounding(bs, "ib_ore", prev_ars, + prev ? prev->res_exact : 0); + int64_t br_ub = ar_bs_sum(bs, 2091, 2098) + + ar_bs_rounding(bs, "ub_ore", ars, cur->res_exact); int64_t movement = br_ub - br_ib; int64_t paid_div = prev_ars - movement; if (paid_div < 0) @@ -749,13 +770,10 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, buf_line(t, MK_HEAD "Fritt eget kapital\n"); ar_row(t, "Balanserat resultat", br_ub, br_ib, 1); ar_row(t, "Årets resultat", ars, prev_ars, 1); - ar_row(t, "Summa fritt eget kapital", ar_bs_sum(bs, 2091, 2098) + ars, - ar_bs_ib_sum(bs, 2091, 2098) + prev_ars, 1); - ar_row(t, "Summa eget kapital", - ar_bs_sum(bs, 2000, 2090) + ar_bs_sum(bs, 2091, 2098) + ars, - ar_bs_ib_sum(bs, 2000, 2090) + ar_bs_ib_sum(bs, 2091, 2098) + - prev_ars, - 0); + ar_row(t, "Summa fritt eget kapital", br_ub + ars, br_ib + prev_ars, 1); + int64_t eq_ub = ar_bs_sum(bs, 2000, 2090) + br_ub + ars; + int64_t eq_ib = ar_bs_ib_sum(bs, 2000, 2090) + br_ib + prev_ars; + ar_row(t, "Summa eget kapital", eq_ub, eq_ib, 0); buf_line(t, "\n"); ar_row(t, "Obeskattade reserver", ar_bs_sum(bs, 2100, 2199), ar_bs_ib_sum(bs, 2100, 2199), 0); @@ -775,8 +793,8 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, int64_t skuld_p = ar_bs_ib_sum(bs, 2400, 2999); ar_row(t, "Summa kortfristiga skulder", skuld, skuld_p, 1); ar_row(t, "Summa eget kapital, avsättningar och skulder", - ar_bs_sum(bs, 2000, 2999) + ars, - ar_bs_ib_sum(bs, 2000, 2999) + prev_ars, 0); + eq_ub + ar_bs_sum(bs, 2100, 2999), + eq_ib + ar_bs_ib_sum(bs, 2100, 2999), 0); buf_line(t, "\n" MK_HEAD "Noter\n\nNot 1 Redovisnings- och" " värderingsprinciper\n"); diff --git a/clients/screens_reports.c b/clients/screens_reports.c index e6263a2..a06e914 100644 --- a/clients/screens_reports.c +++ b/clients/screens_reports.c @@ -819,7 +819,7 @@ static const struct { { "7366", "2.46 Kortfristiga skulder Växelskulder" }, { "7367", "2.47 Kortfristiga skulder Skulder till koncern-, intresse- och gemensamt styrda företag" }, { "7369", "2.48 Skulder till övriga företag som det finns ett ägarintresse i och övriga skulder" }, - { "7368", "2.49 Kortfristiga skulder Skattesskulder" }, + { "7368", "2.49 Kortfristiga skulder Skatteskulder" }, { "7370", "2.50 Kortfristiga skulder Upplupna kostnader och förutbetalda intäkter" }, { "7410", "3.1 Nettoomsättning" }, { "7411", "3.2 Förändring av lager av produkter i arbete, färdiga varor och pågående arbete för annans räkning. +" }, diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 0b69d53..3ddb03b 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -465,8 +465,9 @@ closed straight to 2099 and would otherwise net to zero; the TUI 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", -"saldo_ore"}]}]}`; accounts without IB or period movement are omitted, and +"number","date","description","row_description","source","debit_ore", +"credit_ore","saldo_ore"}]}]}` (`source` is the voucher's origin, e.g. +`sie_import`); accounts without IB or period movement are omitted, and `accounts` (array of account numbers) filters the list. `report.voucher_list` (verifikationslista) returns `{"fiscal_year","from", "to","last_voucher":{...},"vouchers":[{"id","series","number","date", @@ -476,8 +477,13 @@ returns account blocks: `INFO.SRU` (submitter, defaults to the org) and `BLANKETTER.SRU` with one INK2, INK2R and INK2S block each. The blankett type is derived from the fiscal year end (`P1`-`P4`), the org number is written as 12 -digits, amounts are whole kronor with öre truncated and the blankett's -printed sign, and zero fields are omitted. INK2R is mapped from the ledger +digits, amounts are whole kronor with the blankett's printed sign, and zero +fields are omitted. Every account is rounded to whole kronor (half away +from zero) before it is summed into its field, the same rule as the TUI +årsredovisning, so the two agree to the krona and the fields add up; 2099 +is replaced by the income statement's result in 7302, and the krona that +rounding can leave between the balance sheet's sides is carried by 7302 +(a real imbalance in the ledger stays visible). INK2R is mapped from the ledger via the official BAS ranges; INK2S takes the derived årets resultat and skatt plus `adjustments[]{code,amount_ore}` for manual tax adjustments and computes 7670/7770; INK2 carries 7104/7114 and the optional 8040-8045 diff --git a/docs/STATE.md b/docs/STATE.md index f481524..7f90303 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -110,6 +110,17 @@ unit tests and the docs consistency check. exactly on the filed 2025/26 UB; only öre diffs remain in history (2512 1,63, 2099 1,07, moms 0,56). 2021/22 was reopened for V 25 and must be closed again by the owner. +- **Whole-krona rounding (2026-09-22, not deployed)**: the SRU export and + the TUI årsredovisning round every account to whole kronor + (`util_round_kr`) and sum those, so their lines, totals and INK2 fields + agree to the krona (the SRU export used to truncate each field, the + årsredovisning rounded each line). The balance sheet's rounding krona goes + to Balanserat resultat / 7302, and 2099 is replaced by the year's result + in both. The årsredovisning's "Stäng" skip never matched (`strncmp` with 7 + bytes for the 6-byte "Stäng"), so imported years showed a zero result; it + now follows the server rule (SIE-imported only) via the new `source` field + on `report.general_ledger` rows. The total "Summa eget kapital, + avsättningar och skulder" no longer counts 2099 on top of the result. - **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 981d02c..265f2c2 100644 --- a/src/reports.c +++ b/src/reports.c @@ -574,7 +574,7 @@ yyjson_mut_val *report_general_ledger(yyjson_mut_doc *doc, sqlite3 *db, if (sqlite3_prepare_v2( db, "SELECT v.series,v.number,v.date,COALESCE(v.description,'')," - "r.debit_ore,r.credit_ore,COALESCE(r.description,'')" + "r.debit_ore,r.credit_ore,COALESCE(r.description,''),v.source" " 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" @@ -632,6 +632,9 @@ yyjson_mut_val *report_general_ledger(yyjson_mut_doc *doc, sqlite3 *db, yyjson_mut_obj_add_strcpy( doc, ro, "row_description", (const char *)sqlite3_column_text(rs, 6)); + yyjson_mut_obj_add_strcpy( + doc, ro, "source", + (const char *)sqlite3_column_text(rs, 7)); yyjson_mut_obj_add_int(doc, ro, "saldo_ore", saldo); } sqlite3_reset(rs); diff --git a/src/sru.c b/src/sru.c index 49916dc..8634c68 100644 --- a/src/sru.c +++ b/src/sru.c @@ -239,9 +239,15 @@ static int in_list(const char *const *list, size_t n, const char *code) return 0; } +/* Every account is rounded to whole kronor before it is added to its field, + so fields and totals agree to the krona with the annual report. 2099 is + left out: the equity carries the year's result from the income statement + instead, as the annual report does. *exact and *rounded (optional) sum the + mapped amounts before and after rounding. */ static void map_balance(yyjson_mut_val *arr, const struct bmap *tab, size_t ntab, struct fldset *out, - struct fldset *unmapped) + struct fldset *unmapped, int64_t *exact, + int64_t *rounded) { size_t n = yyjson_mut_arr_size(arr); for (size_t i = 0; i < n; i++) { @@ -251,6 +257,8 @@ static void map_balance(yyjson_mut_val *arr, const struct bmap *tab, if (!acc || !ore) continue; int num = atoi(acc); + if (num == 2099) + continue; const char *code = NULL; for (size_t t = 0; t < ntab; t++) { if (num >= tab[t].lo && num <= tab[t].hi) { @@ -258,16 +266,23 @@ static void map_balance(yyjson_mut_val *arr, const struct bmap *tab, break; } } - if (code) - fld_add(out, code, ore); - else + if (!code) { fld_add(unmapped, acc, ore); + continue; + } + int64_t kr = util_round_kr(ore); + fld_add(out, code, kr); + if (exact) + *exact += ore; + if (rounded) + *rounded += kr; } } +/* Returns the rounded total; *exact gets the unrounded one. */ static int64_t map_income(yyjson_mut_val *arr, const struct imap *tab, size_t ntab, struct fldset *out, - struct fldset *unmapped) + struct fldset *unmapped, int64_t *exact) { int64_t total = 0; size_t n = yyjson_mut_arr_size(arr); @@ -291,13 +306,15 @@ static int64_t map_income(yyjson_mut_val *arr, const struct imap *tab, fld_add(unmapped, acc, ore); continue; } + int64_t kr = util_round_kr(ore); if (strcmp(m->plus, m->minus) == 0) - fld_add(out, m->plus, ore); - else if (ore >= 0) - fld_add(out, m->plus, ore); + fld_add(out, m->plus, kr); + else if (kr >= 0) + fld_add(out, m->plus, kr); else - fld_add(out, m->minus, -ore); - total += ore; + fld_add(out, m->minus, -kr); + total += kr; + *exact += ore; } return total; } @@ -466,28 +483,43 @@ yyjson_mut_val *sru_export(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id, struct fldset ink2r = { 0 }, ink2s = { 0 }, ink2 = { 0 }; struct fldset unmapped = { 0 }; + int64_t a_exact = 0, a_kr = 0, el_exact = 0, el_kr = 0; map_balance(yyjson_mut_obj_get(yyjson_mut_obj_get(bal, "assets"), "accounts"), BAL_ASSETS, sizeof BAL_ASSETS / sizeof BAL_ASSETS[0], &ink2r, - &unmapped); + &unmapped, &a_exact, &a_kr); map_balance(yyjson_mut_obj_get(yyjson_mut_obj_get(bal, "equity"), "accounts"), - BAL_EL, sizeof BAL_EL / sizeof BAL_EL[0], &ink2r, &unmapped); + BAL_EL, sizeof BAL_EL / sizeof BAL_EL[0], &ink2r, &unmapped, + &el_exact, &el_kr); map_balance(yyjson_mut_obj_get(yyjson_mut_obj_get(bal, "liabilities"), "accounts"), - BAL_EL, sizeof BAL_EL / sizeof BAL_EL[0], &ink2r, &unmapped); + BAL_EL, sizeof BAL_EL / sizeof BAL_EL[0], &ink2r, &unmapped, + &el_exact, &el_kr); + int64_t rev_exact = 0, exp_exact = 0; int64_t rev_total = map_income( yyjson_mut_obj_get(yyjson_mut_obj_get(inc, "revenue"), "accounts"), - INC_REV, sizeof INC_REV / sizeof INC_REV[0], &ink2r, &unmapped); + INC_REV, sizeof INC_REV / sizeof INC_REV[0], &ink2r, &unmapped, + &rev_exact); int64_t exp_total = map_income( yyjson_mut_obj_get(yyjson_mut_obj_get(inc, "expenses"), "accounts"), - INC_EXP, sizeof INC_EXP / sizeof INC_EXP[0], &ink2r, &unmapped); + INC_EXP, sizeof INC_EXP / sizeof INC_EXP[0], &ink2r, &unmapped, + &exp_exact); int64_t result = rev_total - exp_total; if (result > 0) fld_add(&ink2r, "7450", result); else if (result < 0) fld_add(&ink2r, "7550", -result); + /* Fritt eget kapital carries the year's result; the rounding difference + (never a real imbalance) goes there too so the balance sheet balances + exactly like the annual report's. */ + int64_t res_exact = rev_exact - exp_exact; + int64_t plug = (a_kr - el_kr - result) - + util_round_kr(a_exact - el_exact - res_exact); + if (result || plug) + fld_add(&ink2r, "7302", result + plug); + if (unmapped.n > 0) { int ignore = 0; yyjson_val *iv = yyjson_obj_get(args, "ignore_unmapped"); @@ -517,7 +549,7 @@ yyjson_mut_val *sru_export(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id, fld_add(&ink2s, "7750", -result_kr * 100); int64_t tax = fld_get(&ink2r, "7528"); if (tax) - fld_add(&ink2s, "7651", (tax / 100) * 100); + fld_add(&ink2s, "7651", tax); yyjson_val *adj = yyjson_obj_get(args, "adjustments"); if (adj) { if (!yyjson_is_arr(adj)) { @@ -537,7 +569,7 @@ yyjson_mut_val *sru_export(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id, set_err(err, "adjustments: unknown INK2S field code"); return NULL; } - fld_add(&ink2s, code, (ore / 100) * 100); + fld_add(&ink2s, code, util_round_kr(ore)); } } int64_t overskott = 0; diff --git a/src/util.c b/src/util.c index 7882642..060d54c 100644 --- a/src/util.c +++ b/src/util.c @@ -575,3 +575,8 @@ size_t util_utf8_to_cp437(const char *in, unsigned char *out, size_t out_sz) } return o; } + +int64_t util_round_kr(int64_t ore) +{ + return ore >= 0 ? (ore + 50) / 100 * 100 : -((-ore + 50) / 100 * 100); +} diff --git a/src/util.h b/src/util.h index a94cf16..ccb0210 100644 --- a/src/util.h +++ b/src/util.h @@ -47,6 +47,11 @@ void util_date_add_months(const char *date, int months, char *out, size_t n); void util_date_add_days(const char *date, int days, char *out, size_t n); int util_date_valid(const char *s); +/* Öre rounded to whole kronor (half away from zero), still in öre. The + annual report and the SRU export round every account with it and sum the + results, so their lines, totals and fields agree to the krona. */ +int64_t util_round_kr(int64_t ore); + /* encoding conversion for SIE files (IBM PC8 / CP437) */ size_t util_utf8_to_cp437(const char *in, unsigned char *out, size_t out_sz); char *util_cp437_to_utf8(const unsigned char *in, size_t n); diff --git a/tests/test_core.c b/tests/test_core.c index c9c969e..071b5ce 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -1654,6 +1654,15 @@ static void test_imported_closings(struct tctx *t) free(sru); yyjson_doc_free(d); + /* the huvudbok tells imported rows apart, for the TUI årsredovisning */ + d = call(reqf("{\"v\":1,\"id\":\"ic9a\",\"cmd\":\"report.general_ledger\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"accounts\":" + "[\"3001\"]}}", + g_session, (int)org)); + CHECK_OK(d); + CHECK_STR(d, "result.accounts.0.rows.0.source", "sie_import"); + 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\":" @@ -4520,22 +4529,28 @@ static void test_sru(struct tctx *t) CHECK(strstr(sru, "#UPPGIFT 7410 1500") != NULL); CHECK(strstr(sru, "#UPPGIFT 7513 80") != NULL); CHECK(strstr(sru, "#UPPGIFT 7525 500") != NULL); - CHECK(strstr(sru, "#UPPGIFT 7528 168") != NULL); + /* every account rounds to whole kronor before it is summed, so the + fields add up: 1500 - 100 - 80 - 500 - 169 = 651 and + 1875 = 651 + 500 + 200 + 355 + 169 */ + CHECK(strstr(sru, "#UPPGIFT 7528 169") != NULL); CHECK(strstr(sru, "#UPPGIFT 7450 651") != NULL); CHECK(strstr(sru, "#UPPGIFT 7281 1875") != NULL); CHECK(strstr(sru, "#UPPGIFT 7369 355") != NULL); CHECK(strstr(sru, "#UPPGIFT 7321 500") != NULL); CHECK(strstr(sru, "#UPPGIFT 7302 651") != NULL); CHECK(strstr(sru, "#UPPGIFT 7650 651") != NULL); - CHECK(strstr(sru, "#UPPGIFT 7651 168") != NULL); + CHECK(strstr(sru, "#UPPGIFT 7651 169") != NULL); CHECK(strstr(sru, "#UPPGIFT 7653 100") != NULL); - CHECK(strstr(sru, "#UPPGIFT 7670 919") != NULL); - CHECK(strstr(sru, "#UPPGIFT 7104 919") != NULL); + CHECK(strstr(sru, "#UPPGIFT 7368 169") != NULL); + CHECK(strstr(sru, "#UPPGIFT 7670 920") != NULL); + CHECK(strstr(sru, "#UPPGIFT 7104 920") != NULL); CHECK(strstr(sru, "#FIL_SLUT") != NULL); free(sru); CHECK_STR(d, "result.from", "2026-01-01"); CHECK(find_amount(d, "result.ink2r", "code", "7281", "amount") == 1875); - CHECK(find_amount(d, "result.ink2s", "code", "7670", "amount") == 919); + CHECK(find_amount(d, "result.ink2s", "code", "7670", "amount") == 920); + CHECK(util_round_kr(150) == 200 && util_round_kr(149) == 100); + CHECK(util_round_kr(-150) == -200 && util_round_kr(-149) == -100); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"126\",\"cmd\":\"sru.export\"," -- cgit v1.3