summaryrefslogtreecommitdiff
path: root/src/sie.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-17 19:55:36 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-17 19:55:36 +0200
commit380195f7cd5e57acf2c1cf2bc41069e6b0b979ed (patch)
tree32a88fb22a7fbe8f1fd5c105156d1f928c93950d /src/sie.c
downloadbokf-0.1.0.tar.gz
bokf-0.1.0.zip
Initial commit: daemon, clients, docs, Docker deploy pipelinev0.1.0
Diffstat (limited to 'src/sie.c')
-rw-r--r--src/sie.c876
1 files changed, 876 insertions, 0 deletions
diff --git a/src/sie.c b/src/sie.c
new file mode 100644
index 0000000..05604d2
--- /dev/null
+++ b/src/sie.c
@@ -0,0 +1,876 @@
+#include "sie.h"
+
+#include <ctype.h>
+#include <math.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "ledger.h"
+#include "util.h"
+#include "version.h"
+
+static void set_err(char **err, const char *fmt, ...)
+ __attribute__((format(printf, 2, 3)));
+
+static void set_err(char **err, const char *fmt, ...)
+{
+ if (!err || *err)
+ return;
+ char buf[512];
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(buf, sizeof buf, fmt, ap);
+ va_end(ap);
+ *err = xstrdup(buf);
+}
+
+/* ------------------------------------------------------------------ */
+/* export */
+/* ------------------------------------------------------------------ */
+
+static void put(FILE *f, const char *s)
+{
+ unsigned char buf[4096];
+ size_t n = util_utf8_to_cp437(s, buf, sizeof buf);
+ fwrite(buf, 1, n, f);
+}
+
+static void put_quoted(FILE *f, const char *s)
+{
+ fputc('"', f);
+ for (const char *p = s; *p; p++)
+ fputc(*p == '"' ? '\'' : *p, f);
+ fputc('"', f);
+}
+
+static void put_amount(FILE *f, int64_t ore)
+{
+ int64_t v = ore;
+ if (v < 0) {
+ fputc('-', f);
+ v = -v;
+ }
+ fprintf(f, "%lld.%02lld", (long long)(v / 100), (long long)(v % 100));
+}
+
+static void put_date(FILE *f, const char *iso)
+{
+ if (!iso || strlen(iso) != 10)
+ return;
+ fwrite(iso, 4, 1, f);
+ fwrite(iso + 5, 2, 1, f);
+ fwrite(iso + 8, 2, 1, f);
+}
+
+int sie_export_file(sqlite3 *db, int64_t org_id, int64_t fy_id,
+ const char *path, char **err)
+{
+ char label[64] = "", start[16] = "", end[16] = "";
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ db,
+ "SELECT label,start_date,end_date FROM fiscal_years"
+ " WHERE org_id=?1 AND id=?2",
+ -1, &st, NULL) != SQLITE_OK) {
+ set_err(err, "database error");
+ return -1;
+ }
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_int64(st, 2, fy_id);
+ if (sqlite3_step(st) != SQLITE_ROW) {
+ sqlite3_finalize(st);
+ set_err(err, "fiscal year not found");
+ return -1;
+ }
+ snprintf(label, sizeof label, "%s", sqlite3_column_text(st, 0));
+ snprintf(start, sizeof start, "%s", sqlite3_column_text(st, 1));
+ snprintf(end, sizeof end, "%s", sqlite3_column_text(st, 2));
+ sqlite3_finalize(st);
+
+ char org_name[256] = "", org_nr[64] = "";
+ if (sqlite3_prepare_v2(db, "SELECT name,COALESCE(org_nr,'') FROM orgs WHERE id=?1",
+ -1, &st, NULL) != SQLITE_OK) {
+ set_err(err, "database error");
+ return -1;
+ }
+ sqlite3_bind_int64(st, 1, org_id);
+ if (sqlite3_step(st) == SQLITE_ROW) {
+ snprintf(org_name, sizeof org_name, "%s", sqlite3_column_text(st, 0));
+ snprintf(org_nr, sizeof org_nr, "%s", sqlite3_column_text(st, 1));
+ }
+ sqlite3_finalize(st);
+
+ FILE *f = fopen(path, "wb");
+ if (!f) {
+ set_err(err, "cannot create %s", path);
+ return -1;
+ }
+ time_t now = time(NULL);
+ struct tm tm;
+ gmtime_r(&now, &tm);
+ char gendate[16];
+ strftime(gendate, sizeof gendate, "%Y%m%d", &tm);
+
+ put(f, "#FLAGGA 0\n");
+ put(f, "#PROGRAM \"bokf\" \"" BOKF_VERSION "\"\n");
+ put(f, "#FORMAT PC8\n");
+ fprintf(f, "#GEN %s\n", gendate);
+ put(f, "#SIETYP 4\n");
+ put(f, "#FNR ");
+ put(f, label);
+ put(f, "\n");
+ put(f, "#ORGNR ");
+ put(f, org_nr);
+ put(f, "\n");
+ put(f, "#FNAMN ");
+ put_quoted(f, org_name);
+ put(f, "\n");
+ put(f, "#RAR 0 ");
+ put_date(f, start);
+ put(f, " ");
+ put_date(f, end);
+ put(f, "\n");
+
+ /* accounts and balances: IB = this year's series "IB" voucher(s) plus
+ all non-IB history before the year; UB = everything through the end;
+ RES = the year's non-IB movements. */
+ if (sqlite3_prepare_v2(
+ db,
+ "SELECT a.number,a.name,"
+ " COALESCE(SUM(CASE WHEN (v.series = 'IB'"
+ " AND v.fiscal_year_id = ?4) OR (v.series <> 'IB'"
+ " AND v.date < ?2) THEN r.debit_ore - r.credit_ore END),0),"
+ " COALESCE(SUM(CASE WHEN v.date <= ?3"
+ " THEN r.debit_ore - r.credit_ore END),0),"
+ " COALESCE(SUM(CASE WHEN v.series <> 'IB'"
+ " AND v.date >= ?2 AND v.date <= ?3"
+ " THEN r.debit_ore - r.credit_ore END),0)"
+ " FROM accounts a LEFT JOIN (voucher_rows r JOIN vouchers v"
+ " ON v.org_id=r.org_id AND v.id=r.voucher_id)"
+ " ON r.org_id=a.org_id AND r.account_id=a.id"
+ " WHERE a.org_id=?1 AND a.active=1"
+ " GROUP BY a.id ORDER BY a.number",
+ -1, &st, NULL) != SQLITE_OK) {
+ fclose(f);
+ set_err(err, "database error");
+ 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);
+ sqlite3_bind_int64(st, 4, fy_id);
+ while (sqlite3_step(st) == SQLITE_ROW) {
+ const char *number = (const char *)sqlite3_column_text(st, 0);
+ const char *name = (const char *)sqlite3_column_text(st, 1);
+ int64_t ib = sqlite3_column_int64(st, 2);
+ int64_t ub = sqlite3_column_int64(st, 3);
+ int64_t res = sqlite3_column_int64(st, 4);
+ put(f, "#KONTO ");
+ put(f, number);
+ put(f, " ");
+ put_quoted(f, name ? name : "");
+ put(f, "\n");
+ if (ib != 0) {
+ put(f, "#IB 0 ");
+ put(f, number);
+ put(f, " ");
+ put_amount(f, ib);
+ put(f, "\n");
+ }
+ if (ub != 0) {
+ put(f, "#UB 0 ");
+ put(f, number);
+ put(f, " ");
+ put_amount(f, ub);
+ put(f, "\n");
+ }
+ if (res != 0) {
+ put(f, "#RES 0 ");
+ put(f, number);
+ put(f, " ");
+ put_amount(f, res);
+ put(f, "\n");
+ }
+ }
+ sqlite3_finalize(st);
+
+ /* vouchers */
+ if (sqlite3_prepare_v2(
+ db,
+ "SELECT id,series,number,date,description FROM vouchers"
+ " WHERE org_id=?1 AND fiscal_year_id=?2 ORDER BY series,number",
+ -1, &st, NULL) != SQLITE_OK) {
+ fclose(f);
+ set_err(err, "database error");
+ return -1;
+ }
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_int64(st, 2, fy_id);
+ int64_t current_id = 0;
+ while (sqlite3_step(st) == SQLITE_ROW) {
+ current_id = sqlite3_column_int64(st, 0);
+ put(f, "#VER ");
+ put_quoted(f, (const char *)sqlite3_column_text(st, 1));
+ fprintf(f, " %lld ", (long long)sqlite3_column_int64(st, 2));
+ put_date(f, (const char *)sqlite3_column_text(st, 3));
+ put(f, " ");
+ put_quoted(f, (const char *)sqlite3_column_text(st, 4));
+ put(f, "\n{\n");
+ sqlite3_stmt *rs = NULL;
+ if (sqlite3_prepare_v2(
+ db,
+ "SELECT a.number,r.debit_ore,r.credit_ore,"
+ "COALESCE(r.description,''),v.date FROM voucher_rows r"
+ " JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
+ " JOIN vouchers v ON v.org_id=r.org_id AND v.id=r.voucher_id"
+ " WHERE r.org_id=?1 AND r.voucher_id=?2 ORDER BY r.line_no",
+ -1, &rs, NULL) == SQLITE_OK) {
+ sqlite3_bind_int64(rs, 1, org_id);
+ sqlite3_bind_int64(rs, 2, current_id);
+ while (sqlite3_step(rs) == SQLITE_ROW) {
+ put(f, "#TRANS ");
+ put(f, (const char *)sqlite3_column_text(rs, 0));
+ put(f, " {} ");
+ put_amount(f, sqlite3_column_int64(rs, 1) -
+ sqlite3_column_int64(rs, 2));
+ put(f, " ");
+ put_date(f, (const char *)sqlite3_column_text(rs, 4));
+ put(f, " ");
+ put_quoted(f, (const char *)sqlite3_column_text(rs, 3));
+ put(f, "\n");
+ }
+ sqlite3_finalize(rs);
+ }
+ put(f, "}\n");
+ }
+ sqlite3_finalize(st);
+
+ if (fclose(f) != 0) {
+ set_err(err, "cannot write %s", path);
+ return -1;
+ }
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* import */
+/* ------------------------------------------------------------------ */
+
+#define MAX_TOK 24
+
+static int tokenize(char *line, char *toks[], int max, int *out_n)
+{
+ int n = 0;
+ char *p = line;
+ while (*p) {
+ while (*p == ' ' || *p == '\t')
+ p++;
+ if (!*p)
+ break;
+ if (n >= max)
+ return -1;
+ if (*p == '"') {
+ p++;
+ char *w = p;
+ toks[n++] = w;
+ while (*p) {
+ if (*p == '"') {
+ if (p[1] == '"') {
+ *w++ = '"';
+ p += 2;
+ continue;
+ }
+ p++;
+ break;
+ }
+ *w++ = *p++;
+ }
+ *w = '\0';
+ } else if (*p == '{' || *p == '}') {
+ toks[n++] = p;
+ p++;
+ if (n < max && *p == '}') {
+ toks[n++] = p;
+ p++;
+ }
+ } else {
+ toks[n++] = p;
+ while (*p && *p != ' ' && *p != '\t')
+ p++;
+ if (*p)
+ *p++ = '\0';
+ }
+ }
+ *out_n = n;
+ return 0;
+}
+
+static int parse_amount(const char *s, int64_t *out)
+{
+ char *end = NULL;
+ double v = strtod(s, &end);
+ if (!end || *end || !isfinite(v))
+ return -1;
+ *out = (int64_t)llround(v * 100.0);
+ return 0;
+}
+
+static int sie_date(const char *s, char *out)
+{
+ if (!s || strlen(s) != 8)
+ return -1;
+ for (int i = 0; i < 8; i++)
+ if (!isdigit((unsigned char)s[i]))
+ return -1;
+ snprintf(out, 16, "%.4s-%.2s-%.2s", s, s + 4, s + 6);
+ return 0;
+}
+
+static const char *type_for_number(const char *number)
+{
+ int n = atoi(number);
+ int cls = n / 1000;
+ if (cls == 1)
+ return "asset";
+ if (cls == 2)
+ return (n >= 2000 && n <= 2099) ? "equity" : "liability";
+ if (cls == 3)
+ return "revenue";
+ if (cls >= 4 && cls <= 8)
+ return "expense";
+ return "asset";
+}
+
+struct import_row {
+ char account[16];
+ int64_t debit, credit;
+ char desc[256];
+};
+
+struct import_ver {
+ char series[16];
+ int64_t number;
+ char date[16];
+ char desc[512];
+ struct import_row *rows;
+ size_t nrows, cap;
+ int line_no;
+};
+
+static int ver_add_row(struct import_ver *v, const char *account, int64_t debit,
+ int64_t credit, const char *desc)
+{
+ if (v->nrows == v->cap) {
+ v->cap = v->cap ? v->cap * 2 : 8;
+ v->rows = xrealloc(v->rows, v->cap * sizeof *v->rows);
+ }
+ struct import_row *r = &v->rows[v->nrows++];
+ snprintf(r->account, sizeof r->account, "%s", account);
+ r->debit = debit;
+ r->credit = credit;
+ snprintf(r->desc, sizeof r->desc, "%s", desc ? desc : "");
+ return 0;
+}
+
+static int account_id(sqlite3 *db, int64_t org_id, const char *number,
+ int64_t *out)
+{
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ db, "SELECT id FROM accounts WHERE org_id=?1 AND number=?2", -1,
+ &st, NULL) != SQLITE_OK)
+ return -1;
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_text(st, 2, number, -1, SQLITE_TRANSIENT);
+ int found = 0;
+ if (sqlite3_step(st) == SQLITE_ROW) {
+ *out = sqlite3_column_int64(st, 0);
+ found = 1;
+ }
+ sqlite3_finalize(st);
+ return found ? 0 : -1;
+}
+
+static int ensure_account(sqlite3 *db, int64_t org_id, const char *number,
+ const char *name, int64_t *out, int *created)
+{
+ if (account_id(db, org_id, number, out) == 0)
+ return 0;
+ char ts[32];
+ util_iso8601(util_now(), ts, sizeof ts);
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ db,
+ "INSERT INTO accounts(org_id,number,name,type,created_at)"
+ " VALUES(?1,?2,?3,?4,?5)",
+ -1, &st, NULL) != SQLITE_OK)
+ return -1;
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_text(st, 2, number, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 3, name && *name ? name : number, -1,
+ SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 4, type_for_number(number), -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 5, ts, -1, SQLITE_TRANSIENT);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE)
+ return -1;
+ *out = sqlite3_last_insert_rowid(db);
+ if (created)
+ (*created)++;
+ return 0;
+}
+
+static int insert_import_voucher(sqlite3 *db, int64_t org_id, int64_t user_id,
+ int64_t fy_id, const char *fy_label,
+ struct import_ver *v, char **err)
+{
+ int64_t sum_d = 0, sum_c = 0;
+ for (size_t i = 0; i < v->nrows; i++) {
+ sum_d += v->rows[i].debit;
+ sum_c += v->rows[i].credit;
+ }
+ if (v->nrows == 0)
+ return 0;
+ if (sum_d != sum_c) {
+ set_err(err, "SIE line %d: voucher %s %lld does not balance",
+ v->line_no, v->series, (long long)v->number);
+ return -1;
+ }
+ if (!util_parse_iso_date(v->date)) {
+ set_err(err, "SIE line %d: bad voucher date %s", v->line_no, v->date);
+ return -1;
+ }
+ if (!v->desc[0])
+ snprintf(v->desc, sizeof v->desc, "Imported voucher %s %lld",
+ v->series, (long long)v->number);
+
+ unsigned char prev[32];
+ memset(prev, 0, sizeof prev);
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ db, "SELECT hash FROM vouchers WHERE org_id=?1 ORDER BY id DESC LIMIT 1",
+ -1, &st, NULL) != SQLITE_OK)
+ return -1;
+ sqlite3_bind_int64(st, 1, org_id);
+ if (sqlite3_step(st) == SQLITE_ROW) {
+ const void *b = sqlite3_column_blob(st, 0);
+ if (b && sqlite3_column_bytes(st, 0) == 32)
+ memcpy(prev, b, 32);
+ }
+ sqlite3_finalize(st);
+
+ struct ledger_row *lrows = xcalloc(v->nrows, sizeof *lrows);
+ for (size_t i = 0; i < v->nrows; i++) {
+ lrows[i].account = v->rows[i].account;
+ lrows[i].debit_ore = v->rows[i].debit;
+ lrows[i].credit_ore = v->rows[i].credit;
+ lrows[i].description = v->rows[i].desc[0] ? v->rows[i].desc : NULL;
+ }
+ unsigned char hash[32];
+ ledger_voucher_hash(prev, org_id, fy_label, v->series, v->number, v->date,
+ v->desc, lrows, v->nrows, hash);
+ free(lrows);
+
+ char ts[32];
+ util_iso8601(util_now(), ts, sizeof ts);
+ if (sqlite3_prepare_v2(
+ db,
+ "INSERT INTO vouchers(org_id,fiscal_year_id,series,number,date,"
+ "description,source,created_at,created_by_user,hash_prev,hash)"
+ " VALUES(?1,?2,?3,?4,?5,?6,'sie_import',?7,?8,?9,?10)",
+ -1, &st, NULL) != SQLITE_OK)
+ return -1;
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_int64(st, 2, fy_id);
+ sqlite3_bind_text(st, 3, v->series, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_int64(st, 4, v->number);
+ sqlite3_bind_text(st, 5, v->date, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 6, v->desc, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 7, ts, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_int64(st, 8, user_id);
+ sqlite3_bind_blob(st, 9, prev, 32, SQLITE_TRANSIENT);
+ sqlite3_bind_blob(st, 10, hash, 32, SQLITE_TRANSIENT);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE) {
+ set_err(err, "SIE line %d: %s", v->line_no, sqlite3_errmsg(db));
+ return -1;
+ }
+ int64_t vid = sqlite3_last_insert_rowid(db);
+
+ for (size_t i = 0; i < v->nrows; i++) {
+ int64_t aid = 0;
+ if (account_id(db, org_id, v->rows[i].account, &aid) != 0) {
+ set_err(err, "SIE line %d: account %s does not exist", v->line_no,
+ v->rows[i].account);
+ return -1;
+ }
+ if (sqlite3_prepare_v2(
+ db,
+ "INSERT INTO voucher_rows(org_id,voucher_id,line_no,account_id,"
+ "debit_ore,credit_ore,description) VALUES(?1,?2,?3,?4,?5,?6,?7)",
+ -1, &st, NULL) != SQLITE_OK)
+ return -1;
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_int64(st, 2, vid);
+ sqlite3_bind_int64(st, 3, (int64_t)i + 1);
+ sqlite3_bind_int64(st, 4, aid);
+ sqlite3_bind_int64(st, 5, v->rows[i].debit);
+ sqlite3_bind_int64(st, 6, v->rows[i].credit);
+ if (v->rows[i].desc[0])
+ sqlite3_bind_text(st, 7, v->rows[i].desc, -1, SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, 7);
+ rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE) {
+ set_err(err, "SIE line %d: %s", v->line_no, sqlite3_errmsg(db));
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int update_sequence(sqlite3 *db, int64_t org_id, int64_t fy_id,
+ const char *series, int64_t number)
+{
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ db,
+ "INSERT INTO sequences(org_id,fiscal_year_id,series,next_number)"
+ " VALUES(?1,?2,?3,?4)"
+ " ON CONFLICT(org_id,fiscal_year_id,series) DO UPDATE SET"
+ " next_number=MAX(next_number, excluded.next_number)",
+ -1, &st, NULL) != SQLITE_OK)
+ return -1;
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_int64(st, 2, fy_id);
+ sqlite3_bind_text(st, 3, series, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_int64(st, 4, number + 1);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ return rc == SQLITE_DONE ? 0 : -1;
+}
+
+int sie_import_content(sqlite3 *db, int64_t org_id, int64_t user_id,
+ const unsigned char *data, size_t len, int dry_run,
+ int64_t *out_fy_id, int64_t *out_vouchers,
+ int64_t *out_accounts, char **err)
+{
+ char *text = util_cp437_to_utf8(data, len);
+ char *save = NULL;
+ char *line = strtok_r(text, "\n", &save);
+ int line_no = 0;
+
+ char fy_start[16] = "", fy_end[16] = "";
+ struct import_ver ver;
+ memset(&ver, 0, sizeof ver);
+ int in_ver = 0;
+ int has_ib_ver = 0;
+ struct import_row *ib_rows = NULL;
+ size_t ib_n = 0, ib_cap = 0;
+ char **konto_numbers = NULL, **konto_names = NULL;
+ size_t konto_n = 0, konto_cap = 0;
+ int64_t max_number_by_series_seen = 0;
+ char last_series[16] = "";
+ int64_t vouchers = 0;
+ int rc = -1;
+ char *local_err = NULL;
+
+#define FAIL(...) \
+ do { \
+ set_err(&local_err, __VA_ARGS__); \
+ goto out; \
+ } while (0)
+
+ while (line) {
+ line_no++;
+ if (line[0] == '\0') {
+ line = strtok_r(NULL, "\n", &save);
+ continue;
+ }
+ char *lbuf = line;
+ char *toks[MAX_TOK];
+ int n = 0;
+ if (tokenize(lbuf, toks, MAX_TOK, &n) != 0)
+ FAIL("SIE line %d: too many tokens", line_no);
+ if (n == 0) {
+ line = strtok_r(NULL, "\n", &save);
+ continue;
+ }
+ if (toks[0][0] != '#') {
+ if (in_ver && strcmp(toks[0], "}") == 0)
+ in_ver = 0;
+ line = strtok_r(NULL, "\n", &save);
+ continue;
+ }
+ if (strcmp(toks[0], "#RAR") == 0 && n >= 4) {
+ if (sie_date(toks[2], fy_start) != 0 ||
+ sie_date(toks[3], fy_end) != 0)
+ FAIL("SIE line %d: bad #RAR dates", line_no);
+ } else if (strcmp(toks[0], "#KONTO") == 0 && n >= 2) {
+ const char *name = n >= 3 ? toks[2] : "";
+ if (konto_n == konto_cap) {
+ konto_cap = konto_cap ? konto_cap * 2 : 32;
+ konto_numbers = xrealloc(konto_numbers,
+ konto_cap * sizeof(char *));
+ konto_names =
+ xrealloc(konto_names, konto_cap * sizeof(char *));
+ }
+ konto_numbers[konto_n] = xstrdup(toks[1]);
+ konto_names[konto_n] = xstrdup(name);
+ konto_n++;
+ } else if (strcmp(toks[0], "#IB") == 0 && n >= 4) {
+ if (strcmp(toks[1], "0") == 0 && strcmp(toks[2], "0") != 0) {
+ int64_t amt = 0;
+ if (parse_amount(toks[3], &amt) != 0)
+ FAIL("SIE line %d: bad #IB amount", line_no);
+ if (amt != 0) {
+ if (ib_n == ib_cap) {
+ ib_cap = ib_cap ? ib_cap * 2 : 16;
+ ib_rows =
+ xrealloc(ib_rows, ib_cap * sizeof *ib_rows);
+ }
+ struct import_row *r = &ib_rows[ib_n++];
+ memset(r, 0, sizeof *r);
+ snprintf(r->account, sizeof r->account, "%s", toks[2]);
+ if (amt > 0)
+ r->debit = amt;
+ else
+ r->credit = -amt;
+ }
+ }
+ } else if (strcmp(toks[0], "#VER") == 0 && n >= 4) {
+ if (in_ver)
+ FAIL("SIE line %d: #VER without closing }", line_no);
+ memset(&ver, 0, sizeof ver); /* previous rows already freed */
+ snprintf(ver.series, sizeof ver.series, "%s", toks[1]);
+ if (strcmp(ver.series, "IB") == 0)
+ has_ib_ver = 1;
+ ver.number = strtoll(toks[2], NULL, 10);
+ ver.line_no = line_no;
+ if (sie_date(toks[3], ver.date) != 0)
+ FAIL("SIE line %d: bad #VER date", line_no);
+ if (n >= 5)
+ snprintf(ver.desc, sizeof ver.desc, "%s", toks[4]);
+ in_ver = 1;
+ } else if (strcmp(toks[0], "#TRANS") == 0 && n >= 3) {
+ /* rows are collected in the second pass */
+ }
+ line = strtok_r(NULL, "\n", &save);
+ }
+ if (in_ver)
+ FAIL("SIE: unterminated #VER block");
+
+ if (!fy_start[0]) {
+ FAIL("SIE file has no #RAR; cannot determine fiscal year");
+ }
+
+ if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK)
+ FAIL("database busy");
+
+ int64_t fy_id = 0;
+ char fy_label[64] = "";
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ db,
+ "SELECT id,label FROM fiscal_years WHERE org_id=?1 AND"
+ " start_date=?2 AND end_date=?3",
+ -1, &st, NULL) != SQLITE_OK)
+ FAIL("database error");
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_text(st, 2, fy_start, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 3, fy_end, -1, SQLITE_TRANSIENT);
+ if (sqlite3_step(st) == SQLITE_ROW) {
+ fy_id = sqlite3_column_int64(st, 0);
+ snprintf(fy_label, sizeof fy_label, "%s", sqlite3_column_text(st, 1));
+ }
+ sqlite3_finalize(st);
+ if (!fy_id) {
+ int sy, sm, sd, ey, em, ed2;
+ util_date_parse(fy_start, &sy, &sm, &sd);
+ util_date_parse(fy_end, &ey, &em, &ed2);
+ if (sm == 1 && sd == 1)
+ snprintf(fy_label, sizeof fy_label, "%d", ey);
+ else
+ snprintf(fy_label, sizeof fy_label, "%d/%d", sy, ey);
+ char ts[32];
+ util_iso8601(util_now(), ts, sizeof ts);
+ if (sqlite3_prepare_v2(
+ db,
+ "INSERT INTO fiscal_years(org_id,label,start_date,end_date,"
+ "created_at) VALUES(?1,?2,?3,?4,?5)",
+ -1, &st, NULL) != SQLITE_OK)
+ FAIL("database error");
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_text(st, 2, fy_label, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 3, fy_start, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 4, fy_end, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 5, ts, -1, SQLITE_TRANSIENT);
+ if (sqlite3_step(st) != SQLITE_DONE)
+ FAIL("could not create fiscal year");
+ sqlite3_finalize(st);
+ fy_id = sqlite3_last_insert_rowid(db);
+ if (!fy_label[0])
+ snprintf(fy_label, sizeof fy_label, "%s", fy_start);
+ }
+
+ int64_t existing = 0;
+ if (sqlite3_prepare_v2(
+ db, "SELECT count(*) FROM vouchers WHERE org_id=?1 AND fiscal_year_id=?2",
+ -1, &st, NULL) != SQLITE_OK)
+ FAIL("database error");
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_int64(st, 2, fy_id);
+ if (sqlite3_step(st) == SQLITE_ROW)
+ existing = sqlite3_column_int64(st, 0);
+ sqlite3_finalize(st);
+ if (existing > 0)
+ FAIL("target fiscal year already has %lld vouchers; import refused",
+ (long long)existing);
+
+ int created_accounts = 0;
+ for (size_t i = 0; i < konto_n; i++) {
+ int64_t aid = 0;
+ if (ensure_account(db, org_id, konto_numbers[i], konto_names[i], &aid,
+ &created_accounts) != 0)
+ FAIL("could not create account %s", konto_numbers[i]);
+ }
+ for (size_t i = 0; i < ib_n; i++) {
+ int64_t aid = 0;
+ if (ensure_account(db, org_id, ib_rows[i].account, NULL, &aid,
+ &created_accounts) != 0)
+ FAIL("could not create account %s", ib_rows[i].account);
+ }
+
+ if (ib_n > 0 && !has_ib_ver) {
+ struct import_ver ibver;
+ memset(&ibver, 0, sizeof ibver);
+ snprintf(ibver.series, sizeof ibver.series, "IB");
+ ibver.number = 1;
+ snprintf(ibver.date, sizeof ibver.date, "%s", fy_start);
+ snprintf(ibver.desc, sizeof ibver.desc, "IngÄende balanser (SIE)");
+ for (size_t i = 0; i < ib_n; i++) {
+ int64_t aid = 0;
+ (void)aid;
+ ver_add_row(&ibver, ib_rows[i].account, ib_rows[i].debit,
+ ib_rows[i].credit, NULL);
+ }
+ if (insert_import_voucher(db, org_id, user_id, fy_id, fy_label, &ibver,
+ &local_err) != 0)
+ goto out;
+ update_sequence(db, org_id, fy_id, "IB", 1);
+ free(ibver.rows);
+ vouchers++;
+ }
+
+ /* re-walk the text to post vouchers (rows were kept only per block) */
+ {
+ char *text2 = util_cp437_to_utf8(data, len);
+ char *save2 = NULL;
+ char *l2 = strtok_r(text2, "\n", &save2);
+ int recno = 0;
+ memset(&ver, 0, sizeof ver);
+ in_ver = 0;
+ while (l2) {
+ recno++;
+ if (l2[0] && l2[0] != ' ' && l2[0] != '\t') {
+ char *t2[MAX_TOK];
+ int m = 0;
+ if (tokenize(l2, t2, MAX_TOK, &m) == 0 && m > 0 &&
+ strcmp(t2[0], "#VER") == 0 && m >= 4) {
+ if (in_ver) {
+ if (insert_import_voucher(db, org_id, user_id, fy_id,
+ fy_label, &ver,
+ &local_err) != 0) {
+ free(text2);
+ goto out;
+ }
+ if (strlen(ver.series) > 0) {
+ snprintf(last_series, sizeof last_series, "%s",
+ ver.series);
+ max_number_by_series_seen = ver.number;
+ }
+ update_sequence(db, org_id, fy_id, ver.series,
+ ver.number);
+ vouchers++;
+ free(ver.rows);
+ memset(&ver, 0, sizeof ver);
+ }
+ snprintf(ver.series, sizeof ver.series, "%s", t2[1]);
+ ver.number = strtoll(t2[2], NULL, 10);
+ ver.line_no = recno;
+ sie_date(t2[3], ver.date);
+ if (m >= 5)
+ snprintf(ver.desc, sizeof ver.desc, "%s", t2[4]);
+ in_ver = 1;
+ } else if (m > 0 && strcmp(t2[0], "#TRANS") == 0 && in_ver &&
+ m >= 3) {
+ int64_t amt = 0;
+ for (int i = 2; i < m; i++) {
+ if (parse_amount(t2[i], &amt) == 0)
+ break;
+ }
+ ver_add_row(&ver, t2[1], amt > 0 ? amt : 0,
+ amt < 0 ? -amt : 0, "");
+ }
+ }
+ l2 = strtok_r(NULL, "\n", &save2);
+ }
+ if (in_ver) {
+ if (insert_import_voucher(db, org_id, user_id, fy_id, fy_label,
+ &ver, &local_err) != 0) {
+ free(text2);
+ goto out;
+ }
+ update_sequence(db, org_id, fy_id, ver.series, ver.number);
+ vouchers++;
+ free(ver.rows);
+ memset(&ver, 0, sizeof ver);
+ }
+ (void)last_series;
+ (void)max_number_by_series_seen;
+ free(text2);
+ }
+
+ if (dry_run) {
+ sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
+ } else if (sqlite3_exec(db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) {
+ FAIL("commit failed");
+ }
+ if (out_fy_id)
+ *out_fy_id = fy_id;
+ if (out_vouchers)
+ *out_vouchers = vouchers;
+ if (out_accounts)
+ *out_accounts = created_accounts;
+ rc = 0;
+
+out:
+ if (rc != 0) {
+ sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
+ if (err && local_err)
+ *err = local_err;
+ else
+ free(local_err);
+ } else {
+ free(local_err);
+ }
+ for (size_t i = 0; i < ib_n; i++)
+ (void)0;
+ free(ib_rows);
+ for (size_t i = 0; i < konto_n; i++) {
+ free(konto_numbers[i]);
+ free(konto_names[i]);
+ }
+ free(konto_numbers);
+ free(konto_names);
+ free(ver.rows);
+ free(text);
+ return rc;
+#undef FAIL
+}