diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-18 23:41:59 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-18 23:41:59 +0200 |
| commit | 49e44668c14abacef17f0f11be39cbd0faa7ecb9 (patch) | |
| tree | dd8248143def675fd033abf390c06567f78d5c4c /src/commands.c | |
| parent | e9fc91dbc8314a25a0d71b7ee4344d3fc4ccfd5e (diff) | |
| download | bokf-49e44668c14abacef17f0f11be39cbd0faa7ecb9.tar.gz bokf-49e44668c14abacef17f0f11be39cbd0faa7ecb9.zip | |
bokslut, INK2/SRU export and attachment markersv0.1.30
- bokslut.post: year-end bookings (manual entries, periodiseringsfond,
skatt at a given rate, resultatdisposition) with a dry-run plan through
the normal ledger path; the resultatrapport separates
bokslutsdispositioner and skatt per K2.
- sru.export: INFO.SRU + BLANKETTER.SRU (INK2/INK2R/INK2S) from the
official 2025P4 field tables and the BAS mapping, with manual INK2S
adjustments, submitter defaults and unmapped-account detection; the TUI
writes both files from Rapporter -> Inkomstdeklaration.
- voucher.list carries attachment_count and the voucher list marks
vouchers with underlag with an x column.
- date_prompt restores the cursor state so it stops blinking at the bottom
after the report date prompts.
Diffstat (limited to 'src/commands.c')
| -rw-r--r-- | src/commands.c | 234 |
1 files changed, 233 insertions, 1 deletions
diff --git a/src/commands.c b/src/commands.c index d7f3860..500c9a3 100644 --- a/src/commands.c +++ b/src/commands.c @@ -20,6 +20,7 @@ #include "reports.h" #include "seed.h" #include "sie.h" +#include "sru.h" #include "util.h" #include "version.h" @@ -3029,7 +3030,9 @@ static yyjson_mut_val *h_voucher_list(struct req *r) "SELECT v.id,v.series,v.number,v.date,v.description,v.source," "v.corrects_voucher_id," "(SELECT count(*) FROM voucher_rows r WHERE r.org_id=v.org_id" - " AND r.voucher_id=v.id)" + " AND r.voucher_id=v.id)," + "(SELECT count(*) FROM voucher_attachments va WHERE va.org_id=v.org_id" + " AND va.voucher_id=v.id)" " FROM vouchers v WHERE v.org_id=?1" " AND (?2=0 OR v.fiscal_year_id=?2)" " AND (?3='' OR v.series=?3)" @@ -3075,6 +3078,8 @@ static yyjson_mut_val *h_voucher_list(struct req *r) sqlite3_column_int64(st, 6)); yyjson_mut_obj_add_int(r->rdoc, o, "row_count", sqlite3_column_int64(st, 7)); + yyjson_mut_obj_add_int(r->rdoc, o, "attachment_count", + sqlite3_column_int64(st, 8)); } sqlite3_finalize(st); yyjson_mut_val *out = yyjson_mut_obj(r->rdoc); @@ -3635,6 +3640,23 @@ static yyjson_mut_val *h_report_vat(struct req *r) return res; } +static yyjson_mut_val *h_sru_export(struct req *r) +{ + int64_t fy = 0; + if (req_fy(r, &fy) != 0) + return NULL; + char *err = NULL; + yyjson_mut_val *res = + sru_export(r->rdoc, r->db, r->org_id, fy, r->args, &err); + if (!res) { + yyjson_mut_val *e = + fail(r, "INVALID_ARGS", err ? err : "could not build the SRU files"); + free(err); + return e; + } + return res; +} + static yyjson_mut_val *h_report_general_ledger(struct req *r) { int64_t fy = 0; @@ -3690,6 +3712,212 @@ static yyjson_mut_val *h_report_vat_eskd(struct req *r) return res; } +/* True for P&L accounts that take part in the taxable result (3xxx-88xx). */ +static int bokslut_pl_account(const char *number) +{ + int n = atoi(number); + return n >= 3000 && n <= 8899; +} + +static yyjson_mut_val *h_bokslut_post(struct req *r) +{ + int64_t fy_id = 0; + if (req_fy(r, &fy_id) != 0) + return NULL; + char fy_label[64] = "", fy_start[16] = "", fy_end[16] = ""; + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2(r->db, + "SELECT label,start_date,end_date FROM fiscal_years" + " WHERE org_id=?1 AND id=?2", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, fy_id); + if (sqlite3_step(st) != SQLITE_ROW) { + sqlite3_finalize(st); + return fail(r, "NOT_FOUND", "fiscal year not found"); + } + snprintf(fy_label, sizeof fy_label, "%s", + (const char *)sqlite3_column_text(st, 0)); + snprintf(fy_start, sizeof fy_start, "%s", + (const char *)sqlite3_column_text(st, 1)); + snprintf(fy_end, sizeof fy_end, "%s", + (const char *)sqlite3_column_text(st, 2)); + sqlite3_finalize(st); + const char *date = arg_str(r->args, "date"); + if (!date) + date = fy_end; + else if (!util_parse_iso_date(date)) + return fail(r, "INVALID_ARGS", "date must be YYYY-MM-DD"); + + yyjson_val *earr = yyjson_obj_get(r->args, "entries"); + if (earr && !yyjson_is_arr(earr)) + return fail(r, "INVALID_ARGS", "entries must be an array"); + size_t nent = earr ? yyjson_arr_size(earr) : 0; + if (nent > 31) + return fail(r, "INVALID_ARGS", "too many entries"); + struct ledger_row erows[64]; + size_t nerows = 0; + memset(erows, 0, sizeof erows); + for (size_t i = 0; i < nent; i++) { + yyjson_val *e = yyjson_arr_get(earr, i); + const char *da = + yyjson_get_str(yyjson_obj_get(e, "debit_account")); + const char *ca = + yyjson_get_str(yyjson_obj_get(e, "credit_account")); + const char *ed = + yyjson_get_str(yyjson_obj_get(e, "description")); + int64_t amt = 0; + yyjson_val *av = yyjson_obj_get(e, "amount_ore"); + if (av && yyjson_is_int(av)) + amt = yyjson_get_sint(av); + if (!da || !*da || !ca || !*ca || amt <= 0) + return fail(r, "INVALID_ARGS", + "each entry needs debit_account, credit_account and" + " amount_ore > 0"); + erows[nerows++] = (struct ledger_row){ da, amt, 0, ed }; + erows[nerows++] = (struct ledger_row){ ca, 0, amt, ed }; + } + int64_t fond = 0; + arg_int(r->args, "periodiseringsfond_ore", &fond); + if (fond < 0) + return fail(r, "INVALID_ARGS", "periodiseringsfond_ore must be >= 0"); + if (fond > 0) { + if (nerows + 2 > 64) + return fail(r, "INVALID_ARGS", "too many entries"); + erows[nerows++] = (struct ledger_row){ "8811", fond, 0, NULL }; + erows[nerows++] = (struct ledger_row){ "2110", 0, fond, NULL }; + } + + int64_t result_before = 0; + if (sqlite3_prepare_v2( + r->db, + "SELECT COALESCE(SUM(r.credit_ore)-SUM(r.debit_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 v.fiscal_year_id=?2 AND v.series<>'IB'" + " AND a.type IN ('revenue','expense') AND a.number NOT LIKE '89%'", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, fy_id); + if (sqlite3_step(st) == SQLITE_ROW) + result_before = sqlite3_column_int64(st, 0); + sqlite3_finalize(st); + + int64_t delta = 0; + for (size_t i = 0; i < nerows; i++) + if (bokslut_pl_account(erows[i].account)) + delta += erows[i].credit_ore - erows[i].debit_ore; + + double tax_rate = 20.6; + yyjson_val *trv = yyjson_obj_get(r->args, "tax_rate"); + if (trv) { + if (yyjson_is_real(trv)) + tax_rate = yyjson_get_real(trv); + else if (yyjson_is_int(trv)) + tax_rate = (double)yyjson_get_sint(trv); + else + return fail(r, "INVALID_ARGS", "tax_rate must be a number"); + if (tax_rate < 0 || tax_rate > 100) + return fail(r, "INVALID_ARGS", "tax_rate must be 0-100"); + } + long bp = (long)(tax_rate * 100.0 + 0.5); + int64_t taxable = result_before + delta; + int64_t tax = taxable > 0 ? taxable * bp / 10000 : 0; + int64_t after = taxable - tax; + int dispose = 1; + arg_bool(r->args, "dispose", &dispose); + + struct { + const char *desc; + const struct ledger_row *rows; + size_t nrows; + } plan[3]; + size_t np = 0; + if (nerows > 0) + plan[np++] = (typeof(plan[0])){ "Bokslutsdispositioner", erows, + nerows }; + struct ledger_row taxrows[2] = { + { "8910", tax, 0, NULL }, { "2512", 0, tax, NULL } + }; + if (tax != 0) + plan[np++] = (typeof(plan[0])){ "Skatt på årets resultat", taxrows, 2 }; + struct ledger_row drows[2]; + if (dispose && after != 0) { + if (after > 0) { + drows[0] = (struct ledger_row){ "8999", after, 0, NULL }; + drows[1] = (struct ledger_row){ "2099", 0, after, NULL }; + } else { + drows[0] = (struct ledger_row){ "2099", -after, 0, NULL }; + drows[1] = (struct ledger_row){ "8999", 0, -after, NULL }; + } + plan[np++] = (typeof(plan[0])){ "Resultatdisposition", drows, 2 }; + } + + yyjson_mut_val *vouchers = yyjson_mut_arr(r->rdoc); + for (size_t i = 0; i < np; i++) { + struct ledger_post_opts o; + memset(&o, 0, sizeof o); + o.org_id = r->org_id; + o.user_id = r->sess->user_id; + o.token_id = r->sess->token_id; + o.date = date; + o.description = plan[i].desc; + o.rows = plan[i].rows; + o.nrows = plan[i].nrows; + o.dry_run = r->dry_run; + struct ledger_error e; + char *json = NULL; + if (ledger_post(r->db, &o, &e, &json) != 0) { + yyjson_mut_val *res = + fail(r, e.code ? e.code : "INTERNAL", e.msg); + free(json); + return res; + } + yyjson_mut_val *vo = yyjson_mut_arr_add_obj(r->rdoc, vouchers); + yyjson_mut_obj_add_strcpy(r->rdoc, vo, "description", plan[i].desc); + yyjson_mut_val *rows = yyjson_mut_arr(r->rdoc); + for (size_t k = 0; k < plan[i].nrows; k++) { + yyjson_mut_val *ro = yyjson_mut_arr_add_obj(r->rdoc, rows); + yyjson_mut_obj_add_strcpy(r->rdoc, ro, "account", + plan[i].rows[k].account); + yyjson_mut_obj_add_int(r->rdoc, ro, "debit_ore", + plan[i].rows[k].debit_ore); + yyjson_mut_obj_add_int(r->rdoc, ro, "credit_ore", + plan[i].rows[k].credit_ore); + } + yyjson_mut_obj_add_val(r->rdoc, vo, "rows", rows); + yyjson_mut_val *pv = json_to_mut(r->rdoc, json); + free(json); + if (pv) + yyjson_mut_obj_add_val(r->rdoc, vo, "voucher", pv); + } + if (!r->dry_run) { + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, + "bokslut.post", reqjson, "OK", NULL); + free(reqjson); + } + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_val *fy = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_int(r->rdoc, fy, "id", fy_id); + yyjson_mut_obj_add_strcpy(r->rdoc, fy, "label", fy_label); + yyjson_mut_obj_add_strcpy(r->rdoc, fy, "start_date", fy_start); + yyjson_mut_obj_add_strcpy(r->rdoc, fy, "end_date", fy_end); + yyjson_mut_obj_add_val(r->rdoc, o, "fiscal_year", fy); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "date", date); + yyjson_mut_obj_add_int(r->rdoc, o, "result_before_ore", result_before); + yyjson_mut_obj_add_int(r->rdoc, o, "entries_ore", delta); + yyjson_mut_obj_add_int(r->rdoc, o, "taxable_ore", taxable); + yyjson_mut_obj_add_int(r->rdoc, o, "tax_ore", tax); + yyjson_mut_obj_add_int(r->rdoc, o, "result_after_ore", after); + yyjson_mut_obj_add_val(r->rdoc, o, "vouchers", vouchers); + return o; +} + + static unsigned char *read_file(const char *path, size_t *out_len) { FILE *f = fopen(path, "rb"); @@ -3899,6 +4127,8 @@ const struct command g_commands[] = { { "voucher.list", "List vouchers", PERM_READ, 1, 0, 0, h_voucher_list }, { "voucher.correct", "Post an ändringsverifikat", PERM_WRITE, 1, 1, 1, h_voucher_correct }, + { "bokslut.post", "Year-end: dispositions, tax and result transfer", + PERM_WRITE, 1, 1, 1, h_bokslut_post }, { "settings.get", "Read org settings", PERM_READ, 1, 0, 0, h_settings_get }, { "settings.set", "Change an org setting", PERM_WRITE, 1, 1, 1, @@ -3937,6 +4167,8 @@ const struct command g_commands[] = { h_report_voucher_list }, { "report.vat_eskd", "eSKD XML for the momsdeklaration (ISO-8859-1)", PERM_READ, 1, 0, 0, h_report_vat_eskd }, + { "sru.export", "INK2 SRU files (INFO.SRU + BLANKETTER.SRU)", + PERM_READ, 1, 0, 0, h_sru_export }, { "sie.export", "Export SIE 4 (PC8)", PERM_READ, 1, 0, 0, h_sie_export }, { "sie.import", "Import SIE 4 into an empty fiscal year", PERM_WRITE, 1, 1, 1, h_sie_import }, |
