#include "seed.h" #include #include #include #include #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; }