diff options
Diffstat (limited to 'src/sie.c')
| -rw-r--r-- | src/sie.c | 876 |
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 +} |
