#include "commands.h" #include "cmd_util.h" #include #include #include #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 db_error(r); 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; char ib_series[16]; db_setting_copy(r->db, r->org_id, "series_ib", "IB", ib_series, sizeof ib_series); 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 v.series<>?3" " AND a.type IN ('revenue','expense') AND a.number NOT LIKE '89%'", -1, &st, NULL) != SQLITE_OK) return db_error(r); sqlite3_bind_int64(st, 1, r->org_id); sqlite3_bind_int64(st, 2, fy_id); sqlite3_bind_text(st, 3, ib_series, -1, SQLITE_TRANSIENT); 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 }; } char series[16]; db_setting_copy(r->db, r->org_id, "series_bokslut", "Å", series, sizeof series); 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.series = series; 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] };