summaryrefslogtreecommitdiff
path: root/src/cmd_fiscal.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd_fiscal.c')
-rw-r--r--src/cmd_fiscal.c484
1 files changed, 484 insertions, 0 deletions
diff --git a/src/cmd_fiscal.c b/src/cmd_fiscal.c
new file mode 100644
index 0000000..94d2429
--- /dev/null
+++ b/src/cmd_fiscal.c
@@ -0,0 +1,484 @@
+#include "commands.h"
+#include "cmd_util.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "audit.h"
+#include "db.h"
+#include "util.h"
+
+/* ------------------------------------------------------------------ */
+/* fiscal years and period locks */
+/* ------------------------------------------------------------------ */
+
+static yyjson_mut_val *fy_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_strcpy(r->rdoc, o, "label",
+ sq(sqlite3_column_text(st, 1)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "start_date",
+ sq(sqlite3_column_text(st, 2)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "end_date",
+ sq(sqlite3_column_text(st, 3)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "status",
+ sq(sqlite3_column_text(st, 4)));
+ if (sqlite3_column_type(st, 5) == SQLITE_NULL)
+ yyjson_mut_obj_add_null(r->rdoc, o, "locked_until");
+ else
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "locked_until",
+ sq(sqlite3_column_text(st, 5)));
+ yyjson_mut_obj_add_int(r->rdoc, o, "dividend_ore",
+ sqlite3_column_int64(st, 6));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "events",
+ sq(sqlite3_column_text(st, 7)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "agm_date",
+ sq(sqlite3_column_text(st, 8)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "dividend_date",
+ sq(sqlite3_column_text(st, 9)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "employees",
+ sq(sqlite3_column_text(st, 10)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "notes",
+ sq(sqlite3_column_text(st, 11)));
+ return o;
+}
+
+#define FY_COLUMNS \
+ "id,label,start_date,end_date,status,locked_until,dividend_ore," \
+ "events,agm_date,dividend_date,employees,notes"
+
+static yyjson_mut_val *h_fiscal_year_list(struct req *r)
+{
+ yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "SELECT " FY_COLUMNS " FROM fiscal_years WHERE org_id=?1"
+ " ORDER BY start_date",
+ -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_arr_add_val(items, fy_json(r, st));
+ sqlite3_finalize(st);
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_val(r->rdoc, o, "items", items);
+ return o;
+}
+
+static yyjson_mut_val *h_fiscal_year_get(struct req *r)
+{
+ int64_t id = 0;
+ arg_int(r->args, "id", &id);
+ sqlite3_stmt *st = NULL;
+ const char *sql = id
+ ? "SELECT " FY_COLUMNS " FROM fiscal_years"
+ " WHERE org_id=?1 AND id=?2"
+ : "SELECT " FY_COLUMNS " FROM fiscal_years"
+ " WHERE org_id=?1 ORDER BY start_date DESC LIMIT 1";
+ 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)
+ sqlite3_bind_int64(st, 2, id);
+ yyjson_mut_val *o = NULL;
+ if (sqlite3_step(st) == SQLITE_ROW)
+ o = fy_json(r, st);
+ sqlite3_finalize(st);
+ if (!o)
+ return fail(r, "NOT_FOUND", "fiscal year not found");
+ return o;
+}
+
+static int fy_exists_in_range(sqlite3 *db, int64_t org_id, const char *start,
+ const char *end)
+{
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ db,
+ "SELECT count(*) FROM fiscal_years WHERE org_id=?1"
+ " AND NOT (end_date < ?2 OR start_date > ?3)",
+ -1, &st, NULL) != SQLITE_OK)
+ return 1;
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_text(st, 2, start, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 3, end, -1, SQLITE_TRANSIENT);
+ int64_t n = 0;
+ if (sqlite3_step(st) == SQLITE_ROW)
+ n = sqlite3_column_int64(st, 0);
+ sqlite3_finalize(st);
+ return n > 0;
+}
+
+static yyjson_mut_val *h_fiscal_year_open(struct req *r)
+{
+ const char *label = arg_str(r->args, "label");
+ const char *start = arg_str(r->args, "start_date");
+ const char *end = arg_str(r->args, "end_date");
+ if (!label || !*label)
+ return fail(r, "INVALID_ARGS", "label is required");
+ if (!util_parse_iso_date(start) || !util_parse_iso_date(end))
+ return fail(r, "INVALID_ARGS", "start_date and end_date must be YYYY-MM-DD");
+ if (strcmp(start, end) >= 0)
+ return fail(r, "INVALID_ARGS", "start_date must be before end_date");
+ if (fy_exists_in_range(r->db, r->org_id, start, end))
+ return fail(r, "CONFLICT", "fiscal year overlaps an existing one");
+ /* "Information om året" carries over from the latest earlier year, so
+ only what changes needs to be edited. Dates and the dividend are
+ year-specific and start empty. */
+ char prev_events[600] = "", prev_emp[64] = "", prev_notes[600] = "";
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "SELECT events,employees,notes FROM fiscal_years"
+ " WHERE org_id=?1 AND end_date < ?2"
+ " ORDER BY end_date DESC LIMIT 1",
+ -1, &st, NULL) == SQLITE_OK) {
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_text(st, 2, start, -1, SQLITE_TRANSIENT);
+ if (sqlite3_step(st) == SQLITE_ROW) {
+ snprintf(prev_events, sizeof prev_events, "%s",
+ (const char *)sqlite3_column_text(st, 0));
+ snprintf(prev_emp, sizeof prev_emp, "%s",
+ (const char *)sqlite3_column_text(st, 1));
+ snprintf(prev_notes, sizeof prev_notes, "%s",
+ (const char *)sqlite3_column_text(st, 2));
+ }
+ sqlite3_finalize(st);
+ }
+ char ts[32];
+ util_iso8601(util_now(), ts, sizeof ts);
+ if (sqlite3_prepare_v2(
+ r->db,
+ "INSERT INTO fiscal_years(org_id,label,start_date,end_date,"
+ "events,employees,notes,created_at)"
+ " VALUES(?1,?2,?3,?4,?5,?6,?7,?8)",
+ -1, &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_text(st, 2, label, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 3, start, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 4, end, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 5, prev_events, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 6, prev_emp, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 7, prev_notes, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 8, ts, -1, SQLITE_TRANSIENT);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE)
+ return fail(r, "CONFLICT", "could not create fiscal year");
+ int64_t id = db_last_id(r->db);
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "fiscal_year.open", reqjson, "OK", NULL);
+ free(reqjson);
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_int(r->rdoc, o, "id", id);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "label", label);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "start_date", start);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "end_date", end);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "status", "open");
+ return o;
+}
+
+static yyjson_mut_val *h_fiscal_year_close(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");
+ char ts[32];
+ util_iso8601(util_now(), ts, sizeof ts);
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "UPDATE fiscal_years SET status='closed', closed_at=?3,"
+ " closed_by=?4 WHERE org_id=?1 AND id=?2 AND status='open'",
+ -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);
+ sqlite3_bind_text(st, 3, ts, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_int64(st, 4, r->sess->user_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", "open 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.close", reqjson, "OK", NULL);
+ free(reqjson);
+ 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);
+}
+
+/* Fiscal-year metadata that is not a posting: the board's proposed
+ dividend and the year's material events, kept for the årsredovisning
+ draft. Both fields are optional; at least one must be given. */
+static yyjson_mut_val *h_fiscal_year_update(struct req *r)
+{
+ int64_t id = 0, dividend = 0;
+ if (!arg_int(r->args, "id", &id) || id <= 0)
+ return fail(r, "INVALID_ARGS", "id is required");
+ int have_div = arg_int(r->args, "dividend_ore", &dividend);
+ const char *events = arg_str(r->args, "events");
+ const char *agm = arg_str(r->args, "agm_date");
+ const char *pay = arg_str(r->args, "dividend_date");
+ const char *employees = arg_str(r->args, "employees");
+ const char *notes = arg_str(r->args, "notes");
+ if (!have_div && !events && !agm && !pay && !employees && !notes)
+ return fail(r, "INVALID_ARGS", "nothing to update");
+ if (have_div && dividend < 0)
+ return fail(r, "INVALID_ARGS", "dividend_ore must be >= 0");
+ if (agm && *agm && !util_parse_iso_date(agm))
+ return fail(r, "INVALID_ARGS", "agm_date must be YYYY-MM-DD");
+ if (pay && *pay && !util_parse_iso_date(pay))
+ return fail(r, "INVALID_ARGS", "dividend_date must be YYYY-MM-DD");
+ static const char *const skeys[4] = { "events", "agm_date",
+ "dividend_date", "employees" };
+ const char *svals[4] = { events, agm, pay, employees };
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(r->db,
+ "SELECT count(*) 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, id);
+ int64_t exists = 0;
+ if (sqlite3_step(st) == SQLITE_ROW)
+ exists = sqlite3_column_int64(st, 0);
+ sqlite3_finalize(st);
+ if (!exists)
+ return fail(r, "NOT_FOUND", "fiscal year not found");
+ if (r->dry_run) {
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
+ yyjson_mut_obj_add_int(r->rdoc, o, "id", id);
+ if (have_div)
+ yyjson_mut_obj_add_int(r->rdoc, o, "dividend_ore", dividend);
+ for (int i = 0; i < 4; i++)
+ if (svals[i])
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, skeys[i], svals[i]);
+ if (notes)
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "notes", notes);
+ return o;
+ }
+ if (sqlite3_prepare_v2(r->db,
+ "UPDATE fiscal_years SET"
+ " dividend_ore=CASE WHEN ?3<0 THEN dividend_ore"
+ " ELSE ?3 END,"
+ " events=COALESCE(?4,events),"
+ " agm_date=COALESCE(?5,agm_date),"
+ " dividend_date=COALESCE(?6,dividend_date),"
+ " employees=COALESCE(?7,employees),"
+ " notes=COALESCE(?8,notes)"
+ " 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);
+ sqlite3_bind_int64(st, 3, have_div ? dividend : -1);
+ for (int i = 0; i < 4; i++) {
+ if (svals[i])
+ sqlite3_bind_text(st, i + 4, svals[i], -1, SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, i + 4);
+ }
+ if (notes)
+ sqlite3_bind_text(st, 8, notes, -1, SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, 8);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE)
+ return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "fiscal_year.update", reqjson, "OK", NULL);
+ free(reqjson);
+ if (sqlite3_prepare_v2(r->db,
+ "SELECT " FY_COLUMNS " 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, id);
+ yyjson_mut_val *o = NULL;
+ if (sqlite3_step(st) == SQLITE_ROW)
+ o = fy_json(r, st);
+ sqlite3_finalize(st);
+ if (!o)
+ return fail(r, "NOT_FOUND", "fiscal year not found");
+ return o;
+}
+
+static yyjson_mut_val *h_period_lock(struct req *r)
+{
+ int64_t fy = 0;
+ arg_int(r->args, "fiscal_year", &fy);
+ const char *until = arg_str(r->args, "until");
+ if (fy <= 0 || !util_parse_iso_date(until))
+ return fail(r, "INVALID_ARGS",
+ "fiscal_year and until (YYYY-MM-DD) are required");
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "UPDATE fiscal_years SET locked_until=?3 WHERE org_id=?1 AND id=?2"
+ " AND ?3 BETWEEN start_date AND end_date",
+ -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, until, -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",
+ "fiscal year not found or until is outside it");
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "period.lock", reqjson, "OK", NULL);
+ free(reqjson);
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year", fy);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "locked_until", until);
+ return o;
+}
+
+static yyjson_mut_val *h_period_unlock(struct req *r)
+{
+ int64_t fy = 0;
+ arg_int(r->args, "fiscal_year", &fy);
+ if (fy <= 0)
+ return fail(r, "INVALID_ARGS", "fiscal_year is required");
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "UPDATE fiscal_years SET locked_until=NULL 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);
+ 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", "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,
+ "period.unlock", reqjson, "OK", NULL);
+ free(reqjson);
+ return yyjson_mut_obj(r->rdoc);
+}
+
+
+static const struct cmd_arg args_fiscal_year_get[] = {
+ { "id", ARG_INT, 0, NULL, NULL,
+ "Fiscal year id; defaults to the latest" },
+};
+
+static const struct cmd_arg args_fiscal_year_open[] = {
+ { "label", ARG_STR, 1, NULL, NULL, "Fiscal year label" },
+ { "start_date", ARG_DATE, 1, NULL, NULL, "Start date (YYYY-MM-DD)" },
+ { "end_date", ARG_DATE, 1, NULL, NULL, "End date (YYYY-MM-DD)" },
+};
+
+static const struct cmd_arg args_fiscal_year_close[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
+ { "confirm", ARG_BOOL, 1, NULL, NULL, "Must be true" },
+};
+
+static const struct cmd_arg args_fiscal_year_reopen[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
+ { "confirm", ARG_BOOL, 1, NULL, NULL, "Must be true" },
+};
+
+static const struct cmd_arg args_fiscal_year_update[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
+ { "dividend_ore", ARG_INT, 0, NULL, NULL, "Proposed dividend in öre" },
+ { "events", ARG_STR, 0, NULL, NULL, "Material events during the year" },
+ { "agm_date", ARG_STR, 0, NULL, NULL, "AGM date; empty clears" },
+ { "dividend_date", ARG_STR, 0, NULL, NULL,
+ "Dividend payment date; empty clears" },
+ { "employees", ARG_STR, 0, NULL, NULL, "Average number of employees" },
+ { "notes", ARG_STR, 0, NULL, NULL, "Other notes" },
+};
+
+static const struct cmd_arg args_period_lock[] = {
+ { "fiscal_year", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
+ { "until", ARG_DATE, 1, NULL, NULL, "Lock through this date, inclusive" },
+ { "reason", ARG_STR, 0, NULL, NULL, "Reason, recorded in the audit log" },
+};
+
+static const struct cmd_arg args_period_unlock[] = {
+ { "fiscal_year", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
+ { "reason", ARG_STR, 0, NULL, NULL, "Reason, recorded in the audit log" },
+};
+
+const struct command g_cmd_fiscal[] = {
+ { "fiscal_year.list", "List fiscal years", PERM_READ, 1, 0, 0,
+ h_fiscal_year_list, NULL, 0 },
+ { "fiscal_year.get", "Get a fiscal year", PERM_READ, 1, 0, 0,
+ h_fiscal_year_get, CMD_ARGS(args_fiscal_year_get) },
+ { "fiscal_year.open", "Open a new fiscal year", PERM_OWNER, 1, 1, 1,
+ h_fiscal_year_open, CMD_ARGS(args_fiscal_year_open) },
+ { "fiscal_year.close", "Close a fiscal year", PERM_OWNER, 1, 1, 0,
+ h_fiscal_year_close, CMD_ARGS(args_fiscal_year_close) },
+ { "fiscal_year.reopen", "Reopen a closed fiscal year", PERM_OWNER, 1, 1, 1,
+ h_fiscal_year_reopen, CMD_ARGS(args_fiscal_year_reopen) },
+ { "fiscal_year.update", "Update fiscal-year metadata (dividend)",
+ PERM_WRITE, 1, 1, 1, h_fiscal_year_update,
+ CMD_ARGS(args_fiscal_year_update) },
+ { "period.lock", "Lock a period through a date", PERM_OWNER, 1, 1, 1,
+ h_period_lock, CMD_ARGS(args_period_lock) },
+ { "period.unlock", "Remove a period lock", PERM_OWNER, 1, 1, 1,
+ h_period_unlock, CMD_ARGS(args_period_unlock) },
+};
+
+const struct cmd_table g_cmd_table_fiscal = {
+ g_cmd_fiscal, sizeof g_cmd_fiscal / sizeof g_cmd_fiscal[0]
+};