summaryrefslogtreecommitdiff
path: root/src/cmd_templates.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd_templates.c')
-rw-r--r--src/cmd_templates.c539
1 files changed, 539 insertions, 0 deletions
diff --git a/src/cmd_templates.c b/src/cmd_templates.c
new file mode 100644
index 0000000..a134d96
--- /dev/null
+++ b/src/cmd_templates.c
@@ -0,0 +1,539 @@
+#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 "util.h"
+
+/* ------------------------------------------------------------------ */
+/* voucher templates (konteringsmallar) */
+/* ------------------------------------------------------------------ */
+
+struct tpl_row_in {
+ int64_t account_id;
+ char account[16];
+ char formula[256];
+ char desc[128];
+};
+static int parse_template_rows(struct req *r, yyjson_val *rowsv,
+ struct tpl_row_in **out, size_t *out_n)
+{
+ *out = NULL;
+ *out_n = 0;
+ 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);
+ if (n == 0 || n > 100)
+ return fail(r, "INVALID_ARGS",
+ "a template needs between 1 and 100 rows")
+ ? -1
+ : -1;
+ struct tpl_row_in *rows = xcalloc(n, 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");
+ yyjson_val *fv = yyjson_obj_get(item, "formula");
+ if (!av || !yyjson_is_str(av) || !fv || !yyjson_is_str(fv)) {
+ free(rows);
+ failf(r, "INVALID_ARGS",
+ "row %zu: account and formula are required", k + 1);
+ return -1;
+ }
+ const char *account = yyjson_get_str(av);
+ const char *formula = yyjson_get_str(fv);
+ if (!formula_valid(formula)) {
+ free(rows);
+ failf(r, "INVALID_ARGS", "row %zu: ogiltig formel '%s'", k + 1,
+ formula);
+ return -1;
+ }
+ 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) {
+ free(rows);
+ return fail(r, "INTERNAL", "database error") ? -1 : -1;
+ }
+ 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);
+ free(rows);
+ failf(r, "ACCOUNT_NOT_FOUND", "row %zu: account %s does not exist",
+ k + 1, account);
+ return -1;
+ }
+ rows[k].account_id = sqlite3_column_int64(st, 0);
+ sqlite3_finalize(st);
+ snprintf(rows[k].account, sizeof rows[k].account, "%s", account);
+ snprintf(rows[k].formula, sizeof rows[k].formula, "%s", formula);
+ yyjson_val *dv = yyjson_obj_get(item, "description");
+ if (dv && yyjson_is_str(dv))
+ snprintf(rows[k].desc, sizeof rows[k].desc, "%s",
+ yyjson_get_str(dv));
+ k++;
+ }
+ *out = rows;
+ *out_n = k;
+ return 0;
+}
+
+static yyjson_mut_val *tpl_rows_json(struct req *r, sqlite3_stmt *st)
+{
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_int(r->rdoc, o, "line_no", sqlite3_column_int64(st, 0));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "account",
+ sq(sqlite3_column_text(st, 1)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "name",
+ sq(sqlite3_column_text(st, 2)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "formula",
+ sq(sqlite3_column_text(st, 3)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "description",
+ sq(sqlite3_column_text(st, 4)));
+ return o;
+}
+
+static yyjson_mut_val *h_template_list(struct req *r)
+{
+ int active_only = 0;
+ arg_bool(r->args, "active_only", &active_only);
+ yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
+ sqlite3_stmt *st = NULL;
+ const char *sql =
+ active_only
+ ? "SELECT t.id,t.name,t.series,t.description,t.active,"
+ "(SELECT count(*) FROM voucher_template_rows tr"
+ " WHERE tr.org_id=t.org_id AND tr.template_id=t.id)"
+ " FROM voucher_templates t WHERE t.org_id=?1 AND t.active=1"
+ " ORDER BY t.name"
+ : "SELECT t.id,t.name,t.series,t.description,t.active,"
+ "(SELECT count(*) FROM voucher_template_rows tr"
+ " WHERE tr.org_id=t.org_id AND tr.template_id=t.id)"
+ " FROM voucher_templates t WHERE t.org_id=?1 ORDER BY t.name";
+ if (sqlite3_prepare_v2(r->db, sql, -1, &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ while (sqlite3_step(st) == SQLITE_ROW) {
+ yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items);
+ yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "name",
+ sq(sqlite3_column_text(st, 1)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "series",
+ sq(sqlite3_column_text(st, 2)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "description",
+ sq(sqlite3_column_text(st, 3)));
+ yyjson_mut_obj_add_bool(r->rdoc, o, "active",
+ sqlite3_column_int(st, 4) != 0);
+ yyjson_mut_obj_add_int(r->rdoc, o, "row_count",
+ sqlite3_column_int64(st, 5));
+ }
+ sqlite3_finalize(st);
+ yyjson_mut_val *out = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_val(r->rdoc, out, "items", items);
+ return out;
+}
+
+static yyjson_mut_val *h_template_get(struct req *r)
+{
+ int64_t id = 0;
+ arg_int(r->args, "id", &id);
+ const char *name = arg_str(r->args, "name");
+ if (id <= 0 && !name)
+ return fail(r, "INVALID_ARGS", "id or name is required");
+ sqlite3_stmt *st = NULL;
+ const char *sql =
+ id > 0 ? "SELECT id,name,series,description,active"
+ " FROM voucher_templates WHERE org_id=?1 AND id=?2"
+ : "SELECT id,name,series,description,active"
+ " FROM voucher_templates WHERE org_id=?1 AND name=?2";
+ if (sqlite3_prepare_v2(r->db, sql, -1, &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ if (id > 0)
+ sqlite3_bind_int64(st, 2, id);
+ else
+ sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT);
+ if (sqlite3_step(st) != SQLITE_ROW) {
+ sqlite3_finalize(st);
+ return fail(r, "NOT_FOUND", "template not found");
+ }
+ int64_t tpl = sqlite3_column_int64(st, 0);
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_int(r->rdoc, o, "id", tpl);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "name",
+ sq(sqlite3_column_text(st, 1)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "series",
+ sq(sqlite3_column_text(st, 2)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "description",
+ sq(sqlite3_column_text(st, 3)));
+ yyjson_mut_obj_add_bool(r->rdoc, o, "active",
+ sqlite3_column_int(st, 4) != 0);
+ sqlite3_finalize(st);
+
+ yyjson_mut_val *rows = yyjson_mut_arr(r->rdoc);
+ if (sqlite3_prepare_v2(
+ r->db,
+ "SELECT tr.line_no,a.number,a.name,tr.formula,"
+ "COALESCE(tr.description,'') FROM voucher_template_rows tr"
+ " JOIN accounts a ON a.org_id=tr.org_id AND a.id=tr.account_id"
+ " WHERE tr.org_id=?1 AND tr.template_id=?2 ORDER BY tr.line_no",
+ -1, &st, NULL) == SQLITE_OK) {
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, tpl);
+ while (sqlite3_step(st) == SQLITE_ROW)
+ yyjson_mut_arr_add_val(rows, tpl_rows_json(r, st));
+ sqlite3_finalize(st);
+ }
+ yyjson_mut_obj_add_val(r->rdoc, o, "rows", rows);
+ return o;
+}
+
+static yyjson_mut_val *h_template_create(struct req *r)
+{
+ const char *name = arg_str(r->args, "name");
+ const char *series = arg_str(r->args, "series");
+ const char *description = arg_str(r->args, "description");
+ if (!name || !*name)
+ return fail(r, "INVALID_ARGS", "name is required");
+ char *series_owned = NULL;
+ if (!series || !*series) {
+ series_owned = db_setting(r->db, r->org_id, "default_series");
+ series = series_owned && *series_owned ? series_owned : "A";
+ }
+ if (!description)
+ description = "";
+ struct tpl_row_in *rows = NULL;
+ size_t nrows = 0;
+ if (parse_template_rows(
+ r, r->args ? yyjson_obj_get(r->args, "rows") : NULL, &rows,
+ &nrows) != 0)
+ return NULL;
+
+ if (r->dry_run) {
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", name);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "series", series);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "description", description);
+ yyjson_mut_val *arr = yyjson_mut_arr(r->rdoc);
+ for (size_t i = 0; i < nrows; i++) {
+ yyjson_mut_val *ro = yyjson_mut_arr_add_obj(r->rdoc, arr);
+ yyjson_mut_obj_add_strcpy(r->rdoc, ro, "account", rows[i].account);
+ yyjson_mut_obj_add_strcpy(r->rdoc, ro, "formula", rows[i].formula);
+ yyjson_mut_obj_add_strcpy(r->rdoc, ro, "description", rows[i].desc);
+ }
+ yyjson_mut_obj_add_val(r->rdoc, o, "rows", arr);
+ yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
+ free(rows);
+ free(series_owned);
+ return o;
+ }
+
+ char ts[32];
+ util_iso8601(util_now(), ts, sizeof ts);
+ if (db_exec(r->db, "BEGIN IMMEDIATE", NULL) != 0) {
+ free(rows);
+ free(series_owned);
+ return fail(r, "DB_BUSY", "could not start transaction");
+ }
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(r->db,
+ "INSERT INTO voucher_templates(org_id,name,series,"
+ "description,created_at) VALUES(?1,?2,?3,?4,?5)",
+ -1, &st, NULL) != SQLITE_OK) {
+ db_exec(r->db, "ROLLBACK", NULL);
+ free(rows);
+ return fail(r, "INTERNAL", "database error");
+ }
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 3, series, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 4, description, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 5, ts, -1, SQLITE_TRANSIENT);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE) {
+ db_exec(r->db, "ROLLBACK", NULL);
+ free(rows);
+ if ((rc & 0xff) == SQLITE_CONSTRAINT)
+ return fail(r, "CONFLICT", "a template with that name exists");
+ return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
+ }
+ int64_t tpl = db_last_id(r->db);
+ for (size_t i = 0; i < nrows; i++) {
+ if (sqlite3_prepare_v2(
+ r->db,
+ "INSERT INTO voucher_template_rows(org_id,template_id,line_no,"
+ "account_id,formula,description) VALUES(?1,?2,?3,?4,?5,?6)",
+ -1, &st, NULL) != SQLITE_OK) {
+ db_exec(r->db, "ROLLBACK", NULL);
+ free(rows);
+ return fail(r, "INTERNAL", "database error");
+ }
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, tpl);
+ sqlite3_bind_int64(st, 3, (int64_t)i + 1);
+ sqlite3_bind_int64(st, 4, rows[i].account_id);
+ sqlite3_bind_text(st, 5, rows[i].formula, -1, SQLITE_TRANSIENT);
+ if (rows[i].desc[0])
+ sqlite3_bind_text(st, 6, rows[i].desc, -1, SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, 6);
+ rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE) {
+ db_exec(r->db, "ROLLBACK", NULL);
+ free(rows);
+ return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
+ }
+ }
+ free(rows);
+ if (db_exec(r->db, "COMMIT", NULL) != 0)
+ return fail(r, "INTERNAL", "commit failed");
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "template.create", reqjson, "OK", NULL);
+ free(reqjson);
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_int(r->rdoc, o, "id", tpl);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", name);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "series", series);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "description", description);
+ free(series_owned);
+ return o;
+}
+
+static yyjson_mut_val *h_template_update(struct req *r)
+{
+ int64_t id = 0;
+ arg_int(r->args, "id", &id);
+ const char *name_arg = arg_str(r->args, "name");
+ if (id <= 0 && !name_arg)
+ return fail(r, "INVALID_ARGS", "id or name is required");
+ struct tpl_head head;
+ char *err = NULL;
+ if (load_template(r->db, r->org_id, id, name_arg, &head, &err) != 0) {
+ yyjson_mut_val *res =
+ fail(r, "NOT_FOUND", err ? err : "template not found");
+ free(err);
+ return res;
+ }
+ const char *new_name = arg_str(r->args, "name");
+ const char *series = arg_str(r->args, "series");
+ const char *description = arg_str(r->args, "description");
+ int active = -1;
+ arg_bool(r->args, "active", &active);
+ yyjson_val *rowsv = r->args ? yyjson_obj_get(r->args, "rows") : NULL;
+ struct tpl_row_in *rows = NULL;
+ size_t nrows = 0;
+ if (rowsv) {
+ if (parse_template_rows(r, rowsv, &rows, &nrows) != 0)
+ return NULL;
+ }
+ if (!new_name && !series && !description && active < 0 && !rowsv) {
+ free(rows);
+ return fail(r, "INVALID_ARGS", "nothing to update");
+ }
+
+ if (r->dry_run) {
+ free(rows);
+ return h_template_get(r);
+ }
+
+ char ts[32];
+ util_iso8601(util_now(), ts, sizeof ts);
+ if (db_exec(r->db, "BEGIN IMMEDIATE", NULL) != 0) {
+ free(rows);
+ return fail(r, "DB_BUSY", "could not start transaction");
+ }
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "UPDATE voucher_templates SET"
+ " name=COALESCE(?2,name), series=COALESCE(?3,series),"
+ " description=COALESCE(?4,description),"
+ " active=CASE WHEN ?5<0 THEN active ELSE ?5 END, updated_at=?6"
+ " WHERE org_id=?1 AND id=?7",
+ -1, &st, NULL) != SQLITE_OK) {
+ db_exec(r->db, "ROLLBACK", NULL);
+ free(rows);
+ return fail(r, "INTERNAL", "database error");
+ }
+ sqlite3_bind_int64(st, 1, r->org_id);
+ if (new_name)
+ sqlite3_bind_text(st, 2, new_name, -1, SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, 2);
+ if (series)
+ sqlite3_bind_text(st, 3, series, -1, SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, 3);
+ if (description)
+ sqlite3_bind_text(st, 4, description, -1, SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, 4);
+ sqlite3_bind_int(st, 5, active);
+ sqlite3_bind_text(st, 6, ts, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_int64(st, 7, head.id);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE) {
+ db_exec(r->db, "ROLLBACK", NULL);
+ free(rows);
+ return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
+ }
+ if (rowsv) {
+ if (sqlite3_prepare_v2(
+ r->db,
+ "DELETE FROM voucher_template_rows WHERE org_id=?1"
+ " AND template_id=?2",
+ -1, &st, NULL) != SQLITE_OK) {
+ db_exec(r->db, "ROLLBACK", NULL);
+ free(rows);
+ return fail(r, "INTERNAL", "database error");
+ }
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, head.id);
+ rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE) {
+ db_exec(r->db, "ROLLBACK", NULL);
+ free(rows);
+ return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
+ }
+ for (size_t i = 0; i < nrows; i++) {
+ if (sqlite3_prepare_v2(
+ r->db,
+ "INSERT INTO voucher_template_rows(org_id,template_id,"
+ "line_no,account_id,formula,description)"
+ " VALUES(?1,?2,?3,?4,?5,?6)",
+ -1, &st, NULL) != SQLITE_OK) {
+ db_exec(r->db, "ROLLBACK", NULL);
+ free(rows);
+ return fail(r, "INTERNAL", "database error");
+ }
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, head.id);
+ sqlite3_bind_int64(st, 3, (int64_t)i + 1);
+ sqlite3_bind_int64(st, 4, rows[i].account_id);
+ sqlite3_bind_text(st, 5, rows[i].formula, -1, SQLITE_TRANSIENT);
+ if (rows[i].desc[0])
+ sqlite3_bind_text(st, 6, rows[i].desc, -1, SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, 6);
+ rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE) {
+ db_exec(r->db, "ROLLBACK", NULL);
+ free(rows);
+ return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
+ }
+ }
+ }
+ free(rows);
+ if (db_exec(r->db, "COMMIT", NULL) != 0)
+ return fail(r, "INTERNAL", "commit failed");
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "template.update", reqjson, "OK", NULL);
+ free(reqjson);
+ return h_template_get(r);
+}
+
+static yyjson_mut_val *h_template_archive(struct req *r)
+{
+ int64_t id = 0;
+ arg_int(r->args, "id", &id);
+ const char *name = arg_str(r->args, "name");
+ if (id <= 0 && !name)
+ return fail(r, "INVALID_ARGS", "id or name is required");
+ sqlite3_stmt *st = NULL;
+ const char *sql =
+ id > 0 ? "UPDATE voucher_templates SET active=0 WHERE org_id=?1"
+ " AND id=?2"
+ : "UPDATE voucher_templates SET active=0 WHERE org_id=?1"
+ " AND name=?2";
+ if (sqlite3_prepare_v2(r->db, sql, -1, &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ if (id > 0)
+ sqlite3_bind_int64(st, 2, id);
+ else
+ sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT);
+ 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", "template not found");
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "template.archive", reqjson, "OK", NULL);
+ free(reqjson);
+ return yyjson_mut_obj(r->rdoc);
+}
+
+
+static const struct cmd_arg args_template_list[] = {
+ { "active_only", ARG_BOOL, 0, NULL, NULL, "Only active templates" },
+};
+
+static const struct cmd_arg args_template_get[] = {
+ { "id", ARG_INT, 0, NULL, NULL, "Template id" },
+ { "name", ARG_STR, 0, NULL, NULL, "Template name" },
+};
+
+static const struct cmd_arg args_template_create[] = {
+ { "name", ARG_STR, 1, NULL, NULL, "Template name" },
+ { "series", ARG_STR, 0, NULL, NULL,
+ "Number series; defaults to settings.default_series" },
+ { "description", ARG_STR, 0, NULL, NULL,
+ "Voucher text; {x} is replaced with the amount" },
+ { "rows", ARG_JSON, 1, NULL, NULL,
+ "Array of {account,formula,description?}" },
+};
+
+static const struct cmd_arg args_template_update[] = {
+ { "id", ARG_INT, 0, NULL, NULL, "Template id" },
+ { "name", ARG_STR, 0, NULL, NULL,
+ "Template name, or the new name when id is given" },
+ { "series", ARG_STR, 0, NULL, NULL, "Number series" },
+ { "description", ARG_STR, 0, NULL, NULL, "Voucher text" },
+ { "active", ARG_BOOL, 0, NULL, NULL, "Active flag" },
+ { "rows", ARG_JSON, 0, NULL, NULL,
+ "Replacement rows, {account,formula,description?}" },
+};
+
+static const struct cmd_arg args_template_archive[] = {
+ { "id", ARG_INT, 0, NULL, NULL, "Template id" },
+ { "name", ARG_STR, 0, NULL, NULL, "Template name" },
+};
+
+const struct command g_cmd_templates[] = {
+ { "template.list", "List voucher templates", PERM_READ, 1, 0, 0,
+ h_template_list, CMD_ARGS(args_template_list) },
+ { "template.get", "Get a voucher template with its rows", PERM_READ, 1, 0,
+ 0, h_template_get, CMD_ARGS(args_template_get) },
+ { "template.create", "Create a voucher template", PERM_WRITE, 1, 1, 1,
+ h_template_create, CMD_ARGS(args_template_create) },
+ { "template.update", "Update a voucher template", PERM_WRITE, 1, 1, 1,
+ h_template_update, CMD_ARGS(args_template_update) },
+ { "template.archive", "Archive a voucher template", PERM_WRITE, 1, 1, 1,
+ h_template_archive, CMD_ARGS(args_template_archive) },
+};
+
+const struct cmd_table g_cmd_table_templates = {
+ g_cmd_templates, sizeof g_cmd_templates / sizeof g_cmd_templates[0]
+};