diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-20 20:11:19 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-20 20:11:19 +0200 |
| commit | 8351fa7752bb8341ef405a37ff7c73142715e981 (patch) | |
| tree | dde603d313f4f26934f2ce20a22dfd9b69324cf0 /src/cmd_vouchers.c | |
| parent | beafdbe0d64340387d619c38554976410129f05b (diff) | |
| download | bokf-8351fa7752bb8341ef405a37ff7c73142715e981.tar.gz bokf-8351fa7752bb8341ef405a37ff7c73142715e981.zip | |
commands: split the command table by domain
Diffstat (limited to 'src/cmd_vouchers.c')
| -rw-r--r-- | src/cmd_vouchers.c | 625 |
1 files changed, 625 insertions, 0 deletions
diff --git a/src/cmd_vouchers.c b/src/cmd_vouchers.c new file mode 100644 index 0000000..3e30544 --- /dev/null +++ b/src/cmd_vouchers.c @@ -0,0 +1,625 @@ +#include "commands.h" +#include "cmd_util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "audit.h" +#include "db.h" +#include "formula.h" +#include "ledger.h" +#include "util.h" + +/* ------------------------------------------------------------------ */ +/* vouchers */ +/* ------------------------------------------------------------------ */ + +static int parse_rows(struct req *r, yyjson_val *rowsv, struct ledger_row **out, + size_t *out_n) +{ + if (!rowsv || !yyjson_is_arr(rowsv)) + return fail(r, "INVALID_ARGS", "rows must be an array") ? -1 : -1; + size_t n = yyjson_arr_size(rowsv); + struct ledger_row *rows = xcalloc(n ? n : 1, sizeof *rows); + size_t k = 0; + yyjson_arr_iter it = yyjson_arr_iter_with(rowsv); + yyjson_val *item; + while ((item = yyjson_arr_iter_next(&it))) { + if (!yyjson_is_obj(item)) { + free(rows); + fail(r, "INVALID_ARGS", "each row must be an object"); + return -1; + } + yyjson_val *av = yyjson_obj_get(item, "account"); + if (!av || !yyjson_is_str(av)) { + free(rows); + failf(r, "INVALID_ARGS", "row %zu: account is required", k + 1); + return -1; + } + yyjson_val *dv = yyjson_obj_get(item, "debit_ore"); + yyjson_val *cv = yyjson_obj_get(item, "credit_ore"); + if ((dv && !yyjson_is_int(dv)) || (cv && !yyjson_is_int(cv))) { + free(rows); + failf(r, "INVALID_ARGS", "row %zu: amounts must be integer öre", + k + 1); + return -1; + } + rows[k].account = yyjson_get_str(av); + rows[k].debit_ore = dv ? yyjson_get_int(dv) : 0; + rows[k].credit_ore = cv ? yyjson_get_int(cv) : 0; + yyjson_val *d = yyjson_obj_get(item, "description"); + rows[k].description = d && yyjson_is_str(d) ? yyjson_get_str(d) : NULL; + k++; + } + *out = rows; + *out_n = k; + return 0; +} + +static int parse_attachment_ids(struct req *r, int64_t **out, size_t *out_n) +{ + yyjson_val *av = r->args ? yyjson_obj_get(r->args, "attachment_ids") : NULL; + *out = NULL; + *out_n = 0; + if (!av) + return 0; + if (!yyjson_is_arr(av)) + return fail(r, "INVALID_ARGS", "attachment_ids must be an array") ? -1 + : -1; + size_t n = yyjson_arr_size(av); + int64_t *ids = xcalloc(n ? n : 1, sizeof *ids); + size_t k = 0; + yyjson_arr_iter it = yyjson_arr_iter_with(av); + yyjson_val *item; + while ((item = yyjson_arr_iter_next(&it))) { + if (!yyjson_is_int(item)) { + free(ids); + fail(r, "INVALID_ARGS", "attachment_ids must be integers"); + return -1; + } + ids[k++] = yyjson_get_int(item); + } + *out = ids; + *out_n = k; + return 0; +} + +static yyjson_mut_val *h_voucher_post(struct req *r) +{ + const char *date = arg_str(r->args, "date"); + const char *description = arg_str(r->args, "description"); + const char *series = arg_str(r->args, "series"); + const char *client_ref = arg_str(r->args, "client_ref"); + int64_t corrects = 0; + arg_int(r->args, "corrects_voucher", &corrects); + if (!date) + return fail(r, "INVALID_ARGS", "date is required"); + + /* template application: {template, x} instead of {rows} */ + int64_t tpl_id = 0; + const char *tpl_name = NULL; + yyjson_val *tv = r->args ? yyjson_obj_get(r->args, "template") : NULL; + if (tv) { + if (yyjson_is_int(tv)) + tpl_id = yyjson_get_int(tv); + else if (yyjson_is_str(tv)) + tpl_name = yyjson_get_str(tv); + else + return fail(r, "INVALID_ARGS", "template must be an id or a name"); + if (r->args && yyjson_obj_get(r->args, "rows")) + return fail(r, "INVALID_ARGS", + "use either rows or template, not both"); + } + double x = 0; + yyjson_val *xv = r->args ? yyjson_obj_get(r->args, "x") : NULL; + if (xv) { + if (yyjson_is_str(xv)) { + int ok = 0; + x = parse_kr_double(yyjson_get_str(xv), &ok); + if (!ok) + return fail(r, "INVALID_ARGS", "x must be a number in kronor"); + } else if (yyjson_is_real(xv)) { + x = yyjson_get_real(xv); + } else if (yyjson_is_int(xv)) { + x = (double)yyjson_get_int(xv); + } else { + return fail(r, "INVALID_ARGS", "x must be a number in kronor"); + } + } + + struct ledger_row *rows = NULL; + size_t nrows = 0; + char *owned_desc = NULL; + char owned_series[16] = ""; + int rows_owned = 0; + + if (tv) { + struct tpl_head head; + char *err = NULL; + if (load_template(r->db, r->org_id, tpl_id, tpl_name, &head, &err) != + 0) { + yyjson_mut_val *res = + fail(r, "NOT_FOUND", err ? err : "template not found"); + free(err); + return res; + } + struct tpl_loaded *trows = NULL; + size_t ntrows = 0; + if (load_template_rows(r->db, r->org_id, head.id, &trows, &ntrows, + &err) != 0) { + yyjson_mut_val *res = + fail(r, "INTERNAL", err ? err : "could not load template"); + free(err); + return res; + } + struct template_row *in = + xcalloc(ntrows ? ntrows : 1, sizeof *in); + for (size_t i = 0; i < ntrows; i++) { + in[i].account = trows[i].account; + in[i].formula = trows[i].formula; + in[i].description = trows[i].desc[0] ? trows[i].desc : NULL; + } + struct resolved_row *resolved = + xcalloc(ntrows ? ntrows : 1, sizeof *resolved); + char ferr[256] = ""; + if (formula_resolve_rows(in, ntrows, x, resolved, &nrows, ferr, + sizeof ferr) != 0) { + free(in); + free(resolved); + free(trows); + return fail(r, "INVALID_ARGS", + ferr[0] ? ferr : "template could not be resolved"); + } + free(in); + free(trows); + rows = xcalloc(nrows, sizeof *rows); + for (size_t i = 0; i < nrows; i++) { + rows[i].account = xstrdup(resolved[i].account); + rows[i].debit_ore = resolved[i].debit_ore; + rows[i].credit_ore = resolved[i].credit_ore; + rows[i].description = + resolved[i].description[0] ? xstrdup(resolved[i].description) + : NULL; + } + free(resolved); + rows_owned = 1; + if (!series && head.series[0]) + snprintf(owned_series, sizeof owned_series, "%s", head.series); + if (!description && head.description[0]) + owned_desc = replace_x(head.description, x); + if (!description && !owned_desc) + owned_desc = xstrdup(head.name); + } else { + if (!description) + return fail(r, "INVALID_ARGS", + "date and description are required"); + if (parse_rows(r, r->args ? yyjson_obj_get(r->args, "rows") : NULL, + &rows, &nrows) != 0) + return NULL; + } + + int64_t *att = NULL; + size_t natt = 0; + if (parse_attachment_ids(r, &att, &natt) != 0) { + if (rows_owned) + for (size_t i = 0; i < nrows; i++) { + free((char *)rows[i].account); + free((char *)rows[i].description); + } + free(rows); + free(owned_desc); + return NULL; + } + + 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 = description ? description : owned_desc; + o.series = series ? series + : (owned_series[0] ? owned_series : NULL); + o.rows = rows; + o.nrows = nrows; + o.corrects_voucher_id = corrects; + o.attachment_ids = att; + o.n_attachments = natt; + o.client_ref = client_ref; + o.dry_run = r->dry_run; + + struct ledger_error e; + char *json = NULL; + int rc = ledger_post(r->db, &o, &e, &json); + if (rows_owned) + for (size_t i = 0; i < nrows; i++) { + free((char *)rows[i].account); + free((char *)rows[i].description); + } + free(rows); + free(att); + free(owned_desc); + if (rc != 0) { + if (e.has_details) { + yyjson_mut_val *details = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_int(r->rdoc, details, "difference_ore", + e.difference_ore); + r->err_details = details; + } + return fail(r, e.code ? e.code : "INTERNAL", e.msg); + } + + 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, + "voucher.post", reqjson, "OK", NULL); + free(reqjson); + } + yyjson_mut_val *res = json_to_mut(r->rdoc, json); + free(json); + if (!res) + return fail(r, "INTERNAL", "could not serialize result"); + return res; +} + +static yyjson_mut_val *voucher_json(struct req *r, sqlite3_stmt *st) +{ + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0)); + yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_id", + sqlite3_column_int64(st, 1)); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "series", + sq(sqlite3_column_text(st, 2))); + yyjson_mut_obj_add_int(r->rdoc, o, "number", sqlite3_column_int64(st, 3)); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "date", + sq(sqlite3_column_text(st, 4))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "description", + sq(sqlite3_column_text(st, 5))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "source", + sq(sqlite3_column_text(st, 6))); + if (sqlite3_column_type(st, 7) == SQLITE_NULL) + yyjson_mut_obj_add_null(r->rdoc, o, "corrects_voucher_id"); + else + yyjson_mut_obj_add_int(r->rdoc, o, "corrects_voucher_id", + sqlite3_column_int64(st, 7)); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "created_at", + sq(sqlite3_column_text(st, 8))); + yyjson_mut_obj_add_int(r->rdoc, o, "created_by_user", + sqlite3_column_int64(st, 9)); + char hex[65]; + const void *hb = sqlite3_column_blob(st, 10); + if (hb && sqlite3_column_bytes(st, 10) == 32) { + util_hex((const unsigned char *)hb, 32, hex); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "hash_prev", hex); + } + const void *vb = sqlite3_column_blob(st, 11); + if (vb && sqlite3_column_bytes(st, 11) == 32) { + util_hex((const unsigned char *)vb, 32, hex); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "hash", hex); + } + return o; +} + +#define VOUCHER_COLUMNS \ + "id,fiscal_year_id,series,number,date,description,source," \ + "corrects_voucher_id,created_at,created_by_user,hash_prev,hash" + +static yyjson_mut_val *h_voucher_get(struct req *r) +{ + int64_t id = 0; + if (!arg_int(r->args, "id", &id) || id <= 0) + return fail(r, "INVALID_ARGS", "id is required"); + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "SELECT " VOUCHER_COLUMNS " FROM vouchers 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, id); + if (sqlite3_step(st) != SQLITE_ROW) { + sqlite3_finalize(st); + return fail(r, "NOT_FOUND", "voucher not found"); + } + yyjson_mut_val *o = voucher_json(r, st); + sqlite3_finalize(st); + + yyjson_mut_val *rows = yyjson_mut_arr(r->rdoc); + if (sqlite3_prepare_v2( + r->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.line_no", + -1, &st, NULL) == SQLITE_OK) { + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, id); + while (sqlite3_step(st) == SQLITE_ROW) { + yyjson_mut_val *ro = yyjson_mut_arr_add_obj(r->rdoc, rows); + yyjson_mut_obj_add_strcpy(r->rdoc, ro, "account", + sq(sqlite3_column_text(st, 0))); + yyjson_mut_obj_add_strcpy(r->rdoc, ro, "name", + sq(sqlite3_column_text(st, 1))); + yyjson_mut_obj_add_int(r->rdoc, ro, "debit_ore", + sqlite3_column_int64(st, 2)); + yyjson_mut_obj_add_int(r->rdoc, ro, "credit_ore", + sqlite3_column_int64(st, 3)); + yyjson_mut_obj_add_strcpy(r->rdoc, ro, "description", + sq(sqlite3_column_text(st, 4))); + } + sqlite3_finalize(st); + } + yyjson_mut_obj_add_val(r->rdoc, o, "rows", rows); + + yyjson_mut_val *atts = yyjson_mut_arr(r->rdoc); + if (sqlite3_prepare_v2( + r->db, + "SELECT a.id,a.filename FROM voucher_attachments va" + " JOIN attachments a ON a.org_id=va.org_id AND a.id=va.attachment_id" + " WHERE va.org_id=?1 AND va.voucher_id=?2 ORDER BY a.id", + -1, &st, NULL) == SQLITE_OK) { + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, id); + while (sqlite3_step(st) == SQLITE_ROW) { + yyjson_mut_val *ao = yyjson_mut_arr_add_obj(r->rdoc, atts); + yyjson_mut_obj_add_int(r->rdoc, ao, "id", + sqlite3_column_int64(st, 0)); + yyjson_mut_obj_add_strcpy(r->rdoc, ao, "filename", + sq(sqlite3_column_text(st, 1))); + } + sqlite3_finalize(st); + } + yyjson_mut_obj_add_val(r->rdoc, o, "attachments", atts); + return o; +} + +static yyjson_mut_val *h_voucher_list(struct req *r) +{ + int64_t fy = 0, cursor = 0, limit = 100, account_id = 0; + arg_int(r->args, "fiscal_year", &fy); + arg_int(r->args, "cursor", &cursor); + arg_int(r->args, "limit", &limit); + if (limit < 1) + limit = 100; + if (limit > 1000) + limit = 1000; + const char *series = arg_str(r->args, "series"); + const char *from = arg_str(r->args, "from"); + const char *to = arg_str(r->args, "to"); + const char *text = arg_str(r->args, "text"); + const char *account = arg_str(r->args, "account"); + if (account) { + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "SELECT id FROM accounts WHERE org_id=?1 AND number=?2", -1, + &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_text(st, 2, account, -1, SQLITE_TRANSIENT); + if (sqlite3_step(st) != SQLITE_ROW) { + sqlite3_finalize(st); + return fail(r, "NOT_FOUND", "account not found"); + } + account_id = sqlite3_column_int64(st, 0); + sqlite3_finalize(st); + } + + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "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)," + "(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)" + " AND (?4='' OR v.date>=?4)" + " AND (?5='' OR v.date<=?5)" + " AND (?6=0 OR EXISTS(SELECT 1 FROM voucher_rows r2" + " WHERE r2.org_id=v.org_id AND r2.voucher_id=v.id" + " AND r2.account_id=?6))" + " AND (?7='' OR v.description LIKE '%'||?7||'%')" + " AND v.id>?8 ORDER BY v.id LIMIT ?9", + -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); + sqlite3_bind_text(st, 3, series ? series : "", -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 4, from ? from : "", -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 5, to ? to : "", -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(st, 6, account_id); + sqlite3_bind_text(st, 7, text ? text : "", -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(st, 8, cursor); + sqlite3_bind_int64(st, 9, limit); + yyjson_mut_val *items = yyjson_mut_arr(r->rdoc); + int64_t last = cursor, n = 0; + while (sqlite3_step(st) == SQLITE_ROW) { + n++; + last = sqlite3_column_int64(st, 0); + yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items); + yyjson_mut_obj_add_int(r->rdoc, o, "id", last); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "series", + sq(sqlite3_column_text(st, 1))); + yyjson_mut_obj_add_int(r->rdoc, o, "number", + sqlite3_column_int64(st, 2)); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "date", + sq(sqlite3_column_text(st, 3))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "description", + sq(sqlite3_column_text(st, 4))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "source", + sq(sqlite3_column_text(st, 5))); + if (sqlite3_column_type(st, 6) == SQLITE_NULL) + yyjson_mut_obj_add_null(r->rdoc, o, "corrects_voucher_id"); + else + yyjson_mut_obj_add_int(r->rdoc, o, "corrects_voucher_id", + 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); + yyjson_mut_obj_add_val(r->rdoc, out, "items", items); + if (n == limit) + yyjson_mut_obj_add_int(r->rdoc, out, "next_cursor", last); + else + yyjson_mut_obj_add_null(r->rdoc, out, "next_cursor"); + return out; +} + +static yyjson_mut_val *h_voucher_correct(struct req *r) +{ + int64_t id = 0; + const char *description = arg_str(r->args, "description"); + const char *date = arg_str(r->args, "date"); + const char *client_ref = arg_str(r->args, "client_ref"); + if (!arg_int(r->args, "voucher", &id) || id <= 0) + return fail(r, "INVALID_ARGS", "voucher is required"); + if (!description || !*description) + return fail(r, "INVALID_ARGS", "description is required"); + + sqlite3_stmt *st = NULL; + char series[16] = "", orig_date[16] = ""; + if (sqlite3_prepare_v2( + r->db, + "SELECT series,date FROM vouchers 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, id); + if (sqlite3_step(st) != SQLITE_ROW) { + sqlite3_finalize(st); + return fail(r, "NOT_FOUND", "voucher not found"); + } + snprintf(series, sizeof series, "%s", sqlite3_column_text(st, 0)); + snprintf(orig_date, sizeof orig_date, "%s", sqlite3_column_text(st, 1)); + sqlite3_finalize(st); + + struct ledger_row *rows = NULL; + size_t nrows = 0, cap = 0; + if (sqlite3_prepare_v2( + r->db, + "SELECT a.number,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.line_no", + -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); + while (sqlite3_step(st) == SQLITE_ROW) { + if (nrows == cap) { + cap = cap ? cap * 2 : 8; + rows = xrealloc(rows, cap * sizeof *rows); + } + memset(&rows[nrows], 0, sizeof rows[nrows]); + rows[nrows].account = xstrdup(sq(sqlite3_column_text(st, 0))); + rows[nrows].debit_ore = sqlite3_column_int64(st, 2); + rows[nrows].credit_ore = sqlite3_column_int64(st, 1); + const unsigned char *d = sqlite3_column_text(st, 3); + rows[nrows].description = d && *d ? xstrdup((const char *)d) : NULL; + nrows++; + } + sqlite3_finalize(st); + if (nrows == 0) + return fail(r, "INTERNAL", "voucher has no rows"); + + 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 ? date : orig_date; + o.description = description; + o.series = series; + o.rows = rows; + o.nrows = nrows; + o.corrects_voucher_id = id; + o.client_ref = client_ref; + o.dry_run = r->dry_run; + + struct ledger_error e; + char *json = NULL; + int rc = ledger_post(r->db, &o, &e, &json); + for (size_t i = 0; i < nrows; i++) { + free((char *)rows[i].account); + free((char *)rows[i].description); + } + free(rows); + if (rc != 0) + return fail(r, e.code ? e.code : "INTERNAL", e.msg); + 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, + "voucher.correct", reqjson, "OK", NULL); + free(reqjson); + } + yyjson_mut_val *res = json_to_mut(r->rdoc, json); + free(json); + if (!res) + return fail(r, "INTERNAL", "could not serialize result"); + return res; +} + + +static const struct cmd_arg args_voucher_post[] = { + { "date", ARG_DATE, 1, NULL, NULL, "Voucher date (YYYY-MM-DD)" }, + { "description", ARG_STR, 0, NULL, NULL, "Voucher text" }, + { "series", ARG_STR, 0, NULL, NULL, + "Number series; defaults to settings.default_series" }, + { "client_ref", ARG_STR, 0, NULL, NULL, + "Idempotency key, unique per org" }, + { "corrects_voucher", ARG_INT, 0, NULL, NULL, + "Voucher id this one corrects" }, + { "rows", ARG_JSON, 0, NULL, NULL, + "Array of {account,debit_ore,credit_ore,description?}" }, + { "template", ARG_JSON, 0, NULL, NULL, + "Template id or name; alternative to rows" }, + { "x", ARG_JSON, 0, NULL, NULL, "Template variable in kronor" }, + { "attachment_ids", ARG_JSON, 0, NULL, NULL, + "Array of attachment ids to link" }, +}; + +static const struct cmd_arg args_voucher_get[] = { + { "id", ARG_INT, 1, NULL, NULL, "Voucher id" }, +}; + +static const struct cmd_arg args_voucher_list[] = { + { "fiscal_year", ARG_INT, 0, NULL, NULL, + "Fiscal year id; defaults to the latest" }, + { "from", ARG_DATE, 0, NULL, NULL, "Earliest date" }, + { "to", ARG_DATE, 0, NULL, NULL, "Latest date" }, + { "series", ARG_STR, 0, NULL, NULL, "Number series filter" }, + { "account", ARG_STR, 0, NULL, NULL, "Account number filter" }, + { "text", ARG_STR, 0, NULL, NULL, "Description substring filter" }, + { "cursor", ARG_INT, 0, NULL, NULL, "Cursor from next_cursor" }, + { "limit", ARG_INT, 0, "100", NULL, "Page size, 1-1000" }, +}; + +static const struct cmd_arg args_voucher_correct[] = { + { "voucher", ARG_INT, 1, NULL, NULL, "Voucher id to correct" }, + { "description", ARG_STR, 1, NULL, NULL, "Correction text" }, + { "date", ARG_DATE, 0, NULL, NULL, "Defaults to the original date" }, + { "client_ref", ARG_STR, 0, NULL, NULL, "Idempotency key" }, +}; + +const struct command g_cmd_vouchers[] = { + { "voucher.post", "Post an immutable voucher (rows or template+x)", + PERM_WRITE, 1, 1, 1, h_voucher_post, CMD_ARGS(args_voucher_post) }, + { "voucher.get", "Get a voucher with rows", PERM_READ, 1, 0, 0, + h_voucher_get, CMD_ARGS(args_voucher_get) }, + { "voucher.list", "List vouchers", PERM_READ, 1, 0, 0, h_voucher_list, + CMD_ARGS(args_voucher_list) }, + { "voucher.correct", "Post an ändringsverifikat", PERM_WRITE, 1, 1, 1, + h_voucher_correct, CMD_ARGS(args_voucher_correct) }, +}; + +const struct cmd_table g_cmd_table_vouchers = { + g_cmd_vouchers, sizeof g_cmd_vouchers / sizeof g_cmd_vouchers[0] +}; |
