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 --- src/reports.c | 5 ++++- src/sru.c | 66 ++++++++++++++++++++++++++++++++++++++++++++--------------- src/util.c | 5 +++++ src/util.h | 5 +++++ 4 files changed, 63 insertions(+), 18 deletions(-) (limited to 'src') 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); -- cgit v1.3