diff options
Diffstat (limited to 'src/cmd_bokslut.c')
| -rw-r--r-- | src/cmd_bokslut.c | 241 |
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] +}; |
