aboutsummaryrefslogtreecommitdiff
path: root/src/seed.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/seed.c')
-rw-r--r--src/seed.c325
1 files changed, 325 insertions, 0 deletions
diff --git a/src/seed.c b/src/seed.c
new file mode 100644
index 0000000..cd1bf71
--- /dev/null
+++ b/src/seed.c
@@ -0,0 +1,325 @@
+#include "seed.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "config.h"
+#include "log.h"
+#include "util.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);
+}
+
+struct chart_entry {
+ const char *number;
+ const char *name;
+ const char *type;
+};
+
+/* Minimal fallback used only when no chart file can be read. The full
+ BAS chart ships in data/bas_k2.csv and data/bas_k3.csv. */
+static const struct chart_entry FALLBACK[] = {
+ { "1220", "Inventarier och verktyg", "asset" },
+ { "1240", "Bilar och andra transportmedel", "asset" },
+ { "1250", "Datorer", "asset" },
+ { "1510", "Kundfordringar", "asset" },
+ { "1650", "Momsfordran", "asset" },
+ { "1710", "Förutbetalda kostnader och upplupna intäkter", "asset" },
+ { "1930", "Företagskonto/checkkonto", "asset" },
+ { "1940", "Övriga bankkonton", "asset" },
+ { "2010", "Eget kapital", "equity" },
+ { "2013", "Egna uttag", "equity" },
+ { "2091", "Balanserad vinst eller förlust", "equity" },
+ { "2099", "Årets resultat", "equity" },
+ { "2440", "Leverantörsskulder", "liability" },
+ { "2510", "Skatteskulder", "liability" },
+ { "2610", "Utgående moms 25%", "liability" },
+ { "2620", "Utgående moms 12%", "liability" },
+ { "2630", "Utgående moms 6%", "liability" },
+ { "2640", "Ingående moms", "liability" },
+ { "2650", "Redovisningskonto för moms", "liability" },
+ { "2710", "Personalskatt", "liability" },
+ { "3001", "Försäljning varor 25%", "revenue" },
+ { "3002", "Försäljning tjänster 25%", "revenue" },
+ { "3021", "Försäljning varor 12%", "revenue" },
+ { "3022", "Försäljning tjänster 12%", "revenue" },
+ { "3031", "Försäljning varor 6%", "revenue" },
+ { "3032", "Försäljning tjänster 6%", "revenue" },
+ { "4010", "Inköp varor", "expense" },
+ { "5010", "Lokalhyra", "expense" },
+ { "5410", "Förbrukningsinventarier", "expense" },
+ { "5460", "Förbrukningsmaterial", "expense" },
+ { "6110", "Kontorsmaterial", "expense" },
+ { "6310", "Företagsförsäkringar", "expense" },
+ { "6530", "Redovisningstjänster", "expense" },
+ { "6540", "IT-tjänster", "expense" },
+ { "6570", "Bankkostnader", "expense" },
+ { "7010", "Löner till kollektivanställda", "expense" },
+ { "7510", "Arbetsgivaravgifter", "expense" },
+ { "8999", "Årets resultat", "expense" },
+};
+
+/* Splits one CSV line in place. Handles quoted fields and doubled quotes. */
+static int csv_split(char *line, char **fields, int max, int *out_n)
+{
+ int n = 0;
+ char *p = line;
+ while (*p && n < max) {
+ char *start = p;
+ char *w = p;
+ if (*p == '"') {
+ p++;
+ while (*p) {
+ if (*p == '"') {
+ if (p[1] == '"') {
+ *w++ = '"';
+ p += 2;
+ continue;
+ }
+ p++;
+ break;
+ }
+ *w++ = *p++;
+ }
+ *w = '\0';
+ if (*p == ',')
+ p++;
+ } else {
+ while (*p && *p != ',')
+ *w++ = *p++;
+ int had_comma = *p == ',';
+ if (had_comma)
+ p++;
+ *w = '\0';
+ }
+ fields[n++] = start;
+ }
+ *out_n = n;
+ return 0;
+}
+
+static int insert_account(sqlite3 *db, int64_t org_id, const char *number,
+ const char *name, const char *type, const char *ts)
+{
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ db,
+ "INSERT OR IGNORE 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, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 4, type, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 5, ts, -1, SQLITE_TRANSIENT);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ return rc == SQLITE_DONE ? 0 : -1;
+}
+
+static int valid_type(const char *t)
+{
+ return t && (!strcmp(t, "asset") || !strcmp(t, "liability") ||
+ !strcmp(t, "equity") || !strcmp(t, "revenue") ||
+ !strcmp(t, "expense"));
+}
+
+static FILE *open_chart(const char *configured, const char *fallback_name)
+{
+ if (configured && *configured) {
+ FILE *f = fopen(configured, "r");
+ if (f)
+ return f;
+ char alt[512];
+ snprintf(alt, sizeof alt, "data/%s", fallback_name);
+ f = fopen(alt, "r");
+ if (f)
+ return f;
+ }
+ return NULL;
+}
+
+static int seed_accounts(sqlite3 *db, int64_t org_id, const char *framework,
+ const char *ts, char **err)
+{
+ int is_k3 = framework && strcmp(framework, "K3") == 0;
+ const char *path = is_k3 ? g_cfg.chart_k3_file : g_cfg.chart_k2_file;
+ const char *name = is_k3 ? "bas_k3.csv" : "bas_k2.csv";
+ FILE *f = open_chart(path, name);
+ if (!f) {
+ log_warn("chart file not found; using built-in starter chart");
+ for (size_t i = 0; i < sizeof FALLBACK / sizeof FALLBACK[0]; i++)
+ if (insert_account(db, org_id, FALLBACK[i].number, FALLBACK[i].name,
+ FALLBACK[i].type, ts) != 0) {
+ set_err(err, "could not seed account %s", FALLBACK[i].number);
+ return -1;
+ }
+ return 0;
+ }
+ char *line = NULL;
+ size_t cap = 0;
+ ssize_t len;
+ int line_no = 0;
+ int count = 0;
+ while ((len = getline(&line, &cap, f)) > 0) {
+ line_no++;
+ if (len > 0 && line[len - 1] == '\n')
+ line[--len] = '\0';
+ if (len > 0 && line[len - 1] == '\r')
+ line[--len] = '\0';
+ char *fields[8];
+ int n = 0;
+ csv_split(line, fields, 8, &n);
+ if (n < 3)
+ continue;
+ if (line_no == 1 && strcmp(fields[0], "number") == 0)
+ continue;
+ if (!valid_type(fields[2]))
+ continue;
+ if (insert_account(db, org_id, fields[0], fields[1], fields[2], ts) !=
+ 0) {
+ set_err(err, "could not seed account %s (line %d)", fields[0],
+ line_no);
+ free(line);
+ fclose(f);
+ return -1;
+ }
+ count++;
+ }
+ free(line);
+ fclose(f);
+ log_info("seeded %d accounts from %s", count, path);
+ return 0;
+}
+
+struct vat_rule {
+ const char *box;
+ const char *match_type;
+ const char *pattern;
+ int sign;
+ int sort_order;
+};
+
+/* Starter mapping from BAS account ranges to momsdeklaration boxes. It
+ covers the common domestic cases only and must be reviewed against the
+ current Skatteverket blankett before filing. */
+static const struct vat_rule VAT_RULES[] = {
+ { "05", "range", "3001-3019", -1, 10 },
+ { "06", "range", "3021-3029", -1, 20 },
+ { "07", "range", "3031-3039", -1, 30 },
+ { "10", "range", "2610-2619", -1, 40 },
+ { "11", "range", "2620-2629", -1, 50 },
+ { "12", "range", "2630-2639", -1, 60 },
+ { "48", "range", "2640-2649", 1, 70 },
+};
+
+static int seed_rules(sqlite3 *db, int64_t org_id, char **err)
+{
+ for (size_t i = 0; i < sizeof VAT_RULES / sizeof VAT_RULES[0]; i++) {
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ db,
+ "INSERT INTO report_rules(org_id,report,box,match_type,pattern,"
+ "sign,sort_order) VALUES(?1,'vat',?2,?3,?4,?5,?6)",
+ -1, &st, NULL) != SQLITE_OK) {
+ set_err(err, "could not seed moms rules: %s", sqlite3_errmsg(db));
+ return -1;
+ }
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_text(st, 2, VAT_RULES[i].box, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 3, VAT_RULES[i].match_type, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 4, VAT_RULES[i].pattern, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_int(st, 5, VAT_RULES[i].sign);
+ sqlite3_bind_int(st, 6, VAT_RULES[i].sort_order);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE) {
+ set_err(err, "could not seed moms rules: %s", sqlite3_errmsg(db));
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int seed_default_fiscal_year(sqlite3 *db, int64_t org_id,
+ int fy_start_month, int64_t *out_fy,
+ char **err)
+{
+ time_t t = time(NULL);
+ struct tm tm;
+ gmtime_r(&t, &tm);
+ int y = tm.tm_year + 1900;
+ int m = tm.tm_mon + 1;
+ int start_year = m >= fy_start_month ? y : y - 1;
+
+ char start[16], end[16];
+ util_date_fmt(start_year, fy_start_month, 1, start, sizeof start);
+ util_date_add_months(start, 12, end, sizeof end);
+ util_date_add_days(end, -1, end, sizeof end);
+ int ey, em, ed;
+ util_date_parse(end, &ey, &em, &ed);
+
+ char label[32];
+ if (fy_start_month == 1)
+ snprintf(label, sizeof label, "%d", ey);
+ else
+ snprintf(label, sizeof label, "%d/%d", start_year, ey);
+
+ char ts[32];
+ util_iso8601(util_now(), ts, sizeof ts);
+ sqlite3_stmt *st = NULL;
+ 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) {
+ set_err(err, "could not create fiscal year: %s", sqlite3_errmsg(db));
+ return -1;
+ }
+ sqlite3_bind_int64(st, 1, 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, ts, -1, SQLITE_TRANSIENT);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE) {
+ set_err(err, "could not create fiscal year: %s", sqlite3_errmsg(db));
+ return -1;
+ }
+ if (out_fy)
+ *out_fy = sqlite3_last_insert_rowid(db);
+ log_info("org %lld: fiscal year %s (%s .. %s)", (long long)org_id, label,
+ start, end);
+ return 0;
+}
+
+int seed_org(sqlite3 *db, int64_t org_id, const char *framework,
+ int fy_start_month, int64_t *out_fy_id, char **err)
+{
+ char ts[32];
+ util_iso8601(util_now(), ts, sizeof ts);
+ if (seed_accounts(db, org_id, framework, ts, err) != 0)
+ return -1;
+ if (seed_rules(db, org_id, err) != 0)
+ return -1;
+ if (seed_default_fiscal_year(db, org_id, fy_start_month, out_fy_id, err) !=
+ 0)
+ return -1;
+ return 0;
+}