summaryrefslogtreecommitdiff
path: root/src/cmd_bokslut.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 20:11:19 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 20:11:19 +0200
commit8351fa7752bb8341ef405a37ff7c73142715e981 (patch)
treedde603d313f4f26934f2ce20a22dfd9b69324cf0 /src/cmd_bokslut.c
parentbeafdbe0d64340387d619c38554976410129f05b (diff)
downloadbokf-8351fa7752bb8341ef405a37ff7c73142715e981.tar.gz
bokf-8351fa7752bb8341ef405a37ff7c73142715e981.zip
commands: split the command table by domain
Diffstat (limited to 'src/cmd_bokslut.c')
-rw-r--r--src/cmd_bokslut.c241
1 files changed, 241 insertions, 0 deletions
diff --git a/src/cmd_bokslut.c b/src/cmd_bokslut.c
new file mode 100644
index 0000000..429ada4
--- /dev/null
+++ b/src/cmd_bokslut.c
@@ -0,0 +1,241 @@
+#include "commands.h"
+#include "cmd_util.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "audit.h"
+#include "db.h"
+#include "ledger.h"
+#include "util.h"
+
+/* 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 const struct cmd_arg args_bokslut_post[] = {
+ { "fiscal_year", ARG_INT, 0, NULL, NULL,
+ "Fiscal year id; defaults to the latest" },
+ { "entries", ARG_JSON, 0, NULL, NULL,
+ "Array of {debit_account,credit_account,amount_ore,description?}" },
+ { "periodiseringsfond_ore", ARG_INT, 0, NULL, NULL,
+ "Periodiseringsfond in öre" },
+ { "tax_rate", ARG_JSON, 0, "20.6", NULL, "Tax rate percent, 0-100" },
+ { "dispose", ARG_BOOL, 0, "true", NULL,
+ "Post the result transfer to 8999/2099" },
+ { "date", ARG_DATE, 0, NULL, NULL,
+ "Posting date; defaults to the fiscal year end" },
+};
+
+const struct command g_cmd_bokslut[] = {
+ { "bokslut.post", "Year-end: dispositions, tax and result transfer",
+ PERM_WRITE, 1, 1, 1, h_bokslut_post, CMD_ARGS(args_bokslut_post) },
+};
+
+const struct cmd_table g_cmd_table_bokslut = {
+ g_cmd_bokslut, sizeof g_cmd_bokslut / sizeof g_cmd_bokslut[0]
+};