diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-17 19:55:36 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-17 19:55:36 +0200 |
| commit | 380195f7cd5e57acf2c1cf2bc41069e6b0b979ed (patch) | |
| tree | 32a88fb22a7fbe8f1fd5c105156d1f928c93950d /src/seed.c | |
| download | bokf-380195f7cd5e57acf2c1cf2bc41069e6b0b979ed.tar.gz bokf-380195f7cd5e57acf2c1cf2bc41069e6b0b979ed.zip | |
Initial commit: daemon, clients, docs, Docker deploy pipelinev0.1.0
Diffstat (limited to 'src/seed.c')
| -rw-r--r-- | src/seed.c | 325 |
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; +} |
