diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/commands.c | 81 | ||||
| -rw-r--r-- | src/reports.c | 229 | ||||
| -rw-r--r-- | src/reports.h | 7 |
3 files changed, 315 insertions, 2 deletions
diff --git a/src/commands.c b/src/commands.c index 64212f7..1034b31 100644 --- a/src/commands.c +++ b/src/commands.c @@ -1842,6 +1842,41 @@ static yyjson_mut_val *h_fiscal_year_close(struct req *r) return yyjson_mut_obj(r->rdoc); } +static yyjson_mut_val *h_fiscal_year_reopen(struct req *r) +{ + int64_t id = 0; + int confirm = 0; + arg_int(r->args, "id", &id); + arg_bool(r->args, "confirm", &confirm); + if (id <= 0 || !confirm) + return fail(r, "INVALID_ARGS", "id and confirm:true are required"); + if (r->dry_run) { + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); + return o; + } + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "UPDATE fiscal_years SET status='open', closed_at=NULL," + " closed_by=NULL WHERE org_id=?1 AND id=?2 AND status='closed'", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, id); + int rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + if (sqlite3_changes(r->db) == 0) + return fail(r, "NOT_FOUND", "closed fiscal year not found"); + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, + "fiscal_year.reopen", reqjson, "OK", NULL); + free(reqjson); + return yyjson_mut_obj(r->rdoc); +} + static yyjson_mut_val *h_period_lock(struct req *r) { int64_t fy = 0; @@ -3529,6 +3564,42 @@ static yyjson_mut_val *h_report_vat(struct req *r) return res; } +static yyjson_mut_val *h_report_general_ledger(struct req *r) +{ + int64_t fy = 0; + if (req_fy(r, &fy) != 0) + return NULL; + yyjson_val *accounts = yyjson_obj_get(r->args, "accounts"); + if (accounts && !yyjson_is_arr(accounts)) + return fail(r, "INVALID_ARGS", "accounts must be an array of strings"); + char *err = NULL; + yyjson_mut_val *res = report_general_ledger( + r->rdoc, r->db, r->org_id, fy, arg_str(r->args, "from"), + arg_str(r->args, "to"), accounts, &err); + if (!res) { + yyjson_mut_val *e = fail(r, "INTERNAL", err ? err : "report failed"); + free(err); + return e; + } + return res; +} + +static yyjson_mut_val *h_report_voucher_list(struct req *r) +{ + int64_t fy = 0; + if (req_fy(r, &fy) != 0) + return NULL; + char *err = NULL; + yyjson_mut_val *res = report_voucher_list( + r->rdoc, r->db, r->org_id, fy, arg_str(r->args, "series"), &err); + if (!res) { + yyjson_mut_val *e = fail(r, "INTERNAL", err ? err : "report failed"); + free(err); + return e; + } + return res; +} + static unsigned char *read_file(const char *path, size_t *out_len) { FILE *f = fopen(path, "rb"); @@ -3723,8 +3794,10 @@ const struct command g_commands[] = { h_fiscal_year_get }, { "fiscal_year.open", "Open a new fiscal year", PERM_OWNER, 1, 1, 1, h_fiscal_year_open }, - { "fiscal_year.close", "Close a fiscal year (irreversible)", PERM_OWNER, 1, - 1, 0, h_fiscal_year_close }, + { "fiscal_year.close", "Close a fiscal year", PERM_OWNER, 1, 1, 0, + h_fiscal_year_close }, + { "fiscal_year.reopen", "Reopen a closed fiscal year", PERM_OWNER, 1, 1, 1, + h_fiscal_year_reopen }, { "period.lock", "Lock a period through a date", PERM_OWNER, 1, 1, 1, h_period_lock }, { "period.unlock", "Remove a period lock", PERM_OWNER, 1, 1, 1, @@ -3764,6 +3837,10 @@ const struct command g_commands[] = { h_report_balance_sheet }, { "report.vat", "Momsdeklaration ruta för ruta", PERM_READ, 1, 0, 0, h_report_vat }, + { "report.general_ledger", "Huvudbok", PERM_READ, 1, 0, 0, + h_report_general_ledger }, + { "report.voucher_list", "Verifikationslista", PERM_READ, 1, 0, 0, + h_report_voucher_list }, { "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 }, diff --git a/src/reports.c b/src/reports.c index ee2bbe1..11c65f2 100644 --- a/src/reports.c +++ b/src/reports.c @@ -471,3 +471,232 @@ yyjson_mut_val *report_vat(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id, " before filing"); return res; } + +/* ---------------- huvudbok (general ledger) ---------------- */ + +static int str_in_arr(yyjson_val *arr, const char *s) +{ + if (!arr) + return 1; + size_t n = yyjson_arr_size(arr); + for (size_t i = 0; i < n; i++) { + yyjson_val *v = yyjson_arr_get(arr, i); + if (v && yyjson_is_str(v) && strcmp(yyjson_get_str(v), s) == 0) + return 1; + } + return 0; +} + +static yyjson_mut_val *last_voucher_json(yyjson_mut_doc *doc, sqlite3 *db, + int64_t org_id, int64_t fy_id) +{ + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + db, + "SELECT series,number FROM vouchers" + " WHERE org_id=?1 AND fiscal_year_id=?2" + " ORDER BY series DESC, number DESC LIMIT 1", + -1, &st, NULL) != SQLITE_OK) + return NULL; + sqlite3_bind_int64(st, 1, org_id); + sqlite3_bind_int64(st, 2, fy_id); + yyjson_mut_val *o = NULL; + if (sqlite3_step(st) == SQLITE_ROW) { + o = yyjson_mut_obj(doc); + yyjson_mut_obj_add_strcpy(doc, o, "series", + (const char *)sqlite3_column_text(st, 0)); + yyjson_mut_obj_add_int(doc, o, "number", sqlite3_column_int64(st, 1)); + } + sqlite3_finalize(st); + return o; +} + +yyjson_mut_val *report_general_ledger(yyjson_mut_doc *doc, sqlite3 *db, + int64_t org_id, int64_t fy_id, + const char *from, const char *to, + yyjson_val *accounts, char **err) +{ + struct fy_info fy; + if (load_fy(db, org_id, fy_id, &fy, err) != 0) + return NULL; + if (!from) + from = fy.start; + if (!to) + to = fy.end; + yyjson_mut_val *bal = + balance_query(doc, db, org_id, fy.id, from, to, NULL, err); + if (!bal) + return NULL; + sqlite3_stmt *rs = NULL; + if (sqlite3_prepare_v2( + db, + "SELECT v.series,v.number,v.date,COALESCE(v.description,'')," + "r.debit_ore,r.credit_ore,COALESCE(r.description,'')" + " 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 a.number=?2 AND v.fiscal_year_id=?3" + " AND v.series <> 'IB' AND v.date BETWEEN ?4 AND ?5" + " ORDER BY v.date,v.series,v.number,r.id", + -1, &rs, NULL) != SQLITE_OK) { + set_err(err, "database error"); + return NULL; + } + yyjson_mut_val *out = yyjson_mut_arr(doc); + size_t nbal = yyjson_mut_arr_size(bal); + for (size_t i = 0; i < nbal; i++) { + yyjson_mut_val *row = yyjson_mut_arr_get(bal, i); + const char *acc = + yyjson_mut_get_str(yyjson_mut_obj_get(row, "account")); + if (!acc || !*acc || !str_in_arr(accounts, acc)) + continue; + int64_t ib = yyjson_mut_get_int(yyjson_mut_obj_get(row, "ib_ore")); + int64_t d = yyjson_mut_get_int(yyjson_mut_obj_get(row, "debit_ore")); + int64_t c = yyjson_mut_get_int(yyjson_mut_obj_get(row, "credit_ore")); + if (ib == 0 && d == 0 && c == 0) + continue; + const char *name = + yyjson_mut_get_str(yyjson_mut_obj_get(row, "name")); + yyjson_mut_val *ao = yyjson_mut_arr_add_obj(doc, out); + yyjson_mut_obj_add_strcpy(doc, ao, "account", acc); + yyjson_mut_obj_add_strcpy(doc, ao, "name", name ? name : ""); + yyjson_mut_obj_add_int(doc, ao, "ib_ore", ib); + yyjson_mut_val *rows = yyjson_mut_arr(doc); + int64_t saldo = ib; + sqlite3_bind_int64(rs, 1, org_id); + sqlite3_bind_text(rs, 2, acc, -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(rs, 3, fy.id); + sqlite3_bind_text(rs, 4, from, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(rs, 5, to, -1, SQLITE_TRANSIENT); + while (sqlite3_step(rs) == SQLITE_ROW) { + int64_t rd = sqlite3_column_int64(rs, 4); + int64_t rc = sqlite3_column_int64(rs, 5); + saldo += rd - rc; + yyjson_mut_val *ro = yyjson_mut_arr_add_obj(doc, rows); + yyjson_mut_obj_add_strcpy( + doc, ro, "series", + (const char *)sqlite3_column_text(rs, 0)); + yyjson_mut_obj_add_int(doc, ro, "number", + sqlite3_column_int64(rs, 1)); + yyjson_mut_obj_add_strcpy( + doc, ro, "date", + (const char *)sqlite3_column_text(rs, 2)); + yyjson_mut_obj_add_strcpy( + doc, ro, "description", + (const char *)sqlite3_column_text(rs, 3)); + yyjson_mut_obj_add_int(doc, ro, "debit_ore", rd); + yyjson_mut_obj_add_int(doc, ro, "credit_ore", rc); + yyjson_mut_obj_add_strcpy( + doc, ro, "row_description", + (const char *)sqlite3_column_text(rs, 6)); + yyjson_mut_obj_add_int(doc, ro, "saldo_ore", saldo); + } + sqlite3_reset(rs); + yyjson_mut_obj_add_val(doc, ao, "rows", rows); + yyjson_mut_obj_add_int(doc, ao, "debit_ore", d); + yyjson_mut_obj_add_int(doc, ao, "credit_ore", c); + yyjson_mut_obj_add_int(doc, ao, "ub_ore", ib + d - c); + } + sqlite3_finalize(rs); + + yyjson_mut_val *o = yyjson_mut_obj(doc); + yyjson_mut_obj_add_val(doc, o, "fiscal_year", fy_json(doc, &fy)); + yyjson_mut_obj_add_strcpy(doc, o, "from", from); + yyjson_mut_obj_add_strcpy(doc, o, "to", to); + yyjson_mut_val *lv = last_voucher_json(doc, db, org_id, fy_id); + if (lv) + yyjson_mut_obj_add_val(doc, o, "last_voucher", lv); + yyjson_mut_obj_add_val(doc, o, "accounts", out); + return o; +} + +/* ---------------- verifikationslista (voucher list) ---------------- */ + +yyjson_mut_val *report_voucher_list(yyjson_mut_doc *doc, sqlite3 *db, + int64_t org_id, int64_t fy_id, + const char *series, char **err) +{ + struct fy_info fy; + if (load_fy(db, org_id, fy_id, &fy, err) != 0) + return NULL; + char sql[256]; + snprintf(sql, sizeof sql, + "SELECT id,series,number,date,COALESCE(description,'')" + " FROM vouchers WHERE org_id=?1 AND fiscal_year_id=?2%s" + " ORDER BY series,number", + series ? " AND series=?3" : ""); + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) { + set_err(err, "database error"); + return NULL; + } + sqlite3_bind_int64(st, 1, org_id); + sqlite3_bind_int64(st, 2, fy.id); + if (series) + sqlite3_bind_text(st, 3, series, -1, SQLITE_TRANSIENT); + sqlite3_stmt *rs = NULL; + if (sqlite3_prepare_v2( + db, + "SELECT a.number,a.name,r.debit_ore,r.credit_ore," + "COALESCE(r.description,'') FROM voucher_rows r" + " JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id" + " WHERE r.org_id=?1 AND r.voucher_id=?2 ORDER BY r.id", + -1, &rs, NULL) != SQLITE_OK) { + sqlite3_finalize(st); + set_err(err, "database error"); + return NULL; + } + yyjson_mut_val *vouchers = yyjson_mut_arr(doc); + int64_t t_debit = 0, t_credit = 0; + while (sqlite3_step(st) == SQLITE_ROW) { + int64_t vid = sqlite3_column_int64(st, 0); + yyjson_mut_val *vo = yyjson_mut_arr_add_obj(doc, vouchers); + yyjson_mut_obj_add_int(doc, vo, "id", vid); + yyjson_mut_obj_add_strcpy(doc, vo, "series", + (const char *)sqlite3_column_text(st, 1)); + yyjson_mut_obj_add_int(doc, vo, "number", sqlite3_column_int64(st, 2)); + yyjson_mut_obj_add_strcpy(doc, vo, "date", + (const char *)sqlite3_column_text(st, 3)); + yyjson_mut_obj_add_strcpy(doc, vo, "description", + (const char *)sqlite3_column_text(st, 4)); + yyjson_mut_val *vrows = yyjson_mut_arr(doc); + sqlite3_bind_int64(rs, 1, org_id); + sqlite3_bind_int64(rs, 2, vid); + while (sqlite3_step(rs) == SQLITE_ROW) { + yyjson_mut_val *ro = yyjson_mut_arr_add_obj(doc, vrows); + yyjson_mut_obj_add_strcpy( + doc, ro, "account", + (const char *)sqlite3_column_text(rs, 0)); + yyjson_mut_obj_add_strcpy( + doc, ro, "name", + (const char *)sqlite3_column_text(rs, 1)); + int64_t rd = sqlite3_column_int64(rs, 2); + int64_t rc = sqlite3_column_int64(rs, 3); + yyjson_mut_obj_add_int(doc, ro, "debit_ore", rd); + yyjson_mut_obj_add_int(doc, ro, "credit_ore", rc); + yyjson_mut_obj_add_strcpy( + doc, ro, "description", + (const char *)sqlite3_column_text(rs, 4)); + t_debit += rd; + t_credit += rc; + } + sqlite3_reset(rs); + yyjson_mut_obj_add_val(doc, vo, "rows", vrows); + } + sqlite3_finalize(rs); + sqlite3_finalize(st); + + yyjson_mut_val *o = yyjson_mut_obj(doc); + yyjson_mut_obj_add_val(doc, o, "fiscal_year", fy_json(doc, &fy)); + yyjson_mut_obj_add_strcpy(doc, o, "from", fy.start); + yyjson_mut_obj_add_strcpy(doc, o, "to", fy.end); + yyjson_mut_val *lv = last_voucher_json(doc, db, org_id, fy_id); + if (lv) + yyjson_mut_obj_add_val(doc, o, "last_voucher", lv); + yyjson_mut_obj_add_val(doc, o, "vouchers", vouchers); + yyjson_mut_val *tot = yyjson_mut_obj(doc); + yyjson_mut_obj_add_int(doc, tot, "debit_ore", t_debit); + yyjson_mut_obj_add_int(doc, tot, "credit_ore", t_credit); + yyjson_mut_obj_add_val(doc, o, "totals", tot); + return o; +} diff --git a/src/reports.h b/src/reports.h index 37d5f67..d4a0545 100644 --- a/src/reports.h +++ b/src/reports.h @@ -19,5 +19,12 @@ yyjson_mut_val *report_balance_sheet(yyjson_mut_doc *doc, sqlite3 *db, const char *to, char **err); yyjson_mut_val *report_vat(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id, const char *from, const char *to, char **err); +yyjson_mut_val *report_general_ledger(yyjson_mut_doc *doc, sqlite3 *db, + int64_t org_id, int64_t fy_id, + const char *from, const char *to, + yyjson_val *accounts, char **err); +yyjson_mut_val *report_voucher_list(yyjson_mut_doc *doc, sqlite3 *db, + int64_t org_id, int64_t fy_id, + const char *series, char **err); #endif |
