diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-18 21:35:10 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-18 21:35:10 +0200 |
| commit | 251eb8c54c77029f0b43aa987ea50c44016147f3 (patch) | |
| tree | 2bf4284ce2daab191d48579a6c5a84882060d398 /src/reports.c | |
| parent | 64c0d5d79b31dafcbdb68ff938a92bd6f136d4a7 (diff) | |
| download | bokf-251eb8c54c77029f0b43aa987ea50c44016147f3.tar.gz bokf-251eb8c54c77029f0b43aa987ea50c44016147f3.zip | |
eskd: generate the momsdeklaration file (eSKDUpload 6.0)v0.1.28
report.vat_eskd builds the ISO-8859-1 XML from report.vat: whole
kronor with öre truncated like the blankett, box 48 positive as filed,
MomsBetala computed from the whole-krona boxes, and an optional
upplysning converted from UTF-8, XML-escaped and capped at 300
characters. The TUI momsrapport gains "s = spara eSKD", prompting for a
path and writing the bytes verbatim.
Verified against the FY2027 Kapitas report: 05=703200, 10=175800,
20=1453, 30=851, 48=1030, 49=175621.
Diffstat (limited to 'src/reports.c')
| -rw-r--r-- | src/reports.c | 230 |
1 files changed, 230 insertions, 0 deletions
diff --git a/src/reports.c b/src/reports.c index 11c65f2..4368eb0 100644 --- a/src/reports.c +++ b/src/reports.c @@ -700,3 +700,233 @@ yyjson_mut_val *report_voucher_list(yyjson_mut_doc *doc, sqlite3 *db, yyjson_mut_obj_add_val(doc, o, "totals", tot); return o; } + +/* ---------------- eSKD (momsdeklaration on file) ---------------- */ + +struct eskd_box { + const char *box; + const char *tag; +}; + +/* Skatteverket's eSKDUpload 6.0 tag order. */ +static const struct eskd_box ESKD_BOXES[] = { + { "05", "ForsMomsEjAnnan" }, + { "06", "UttagMoms" }, + { "07", "UlagMargbesk" }, + { "08", "HyrinkomstFriv" }, + { "10", "MomsUtgHog" }, + { "11", "MomsUtgMedel" }, + { "12", "MomsUtgLag" }, + { "20", "InkopVaruAnnatEg" }, + { "21", "InkopTjanstAnnatEg" }, + { "22", "InkopTjanstUtomEg" }, + { "23", "InkopVaruSverige" }, + { "24", "InkopTjanstSverige" }, + { "30", "MomsInkopUtgHog" }, + { "31", "MomsInkopUtgMedel" }, + { "32", "MomsInkopUtgLag" }, + { "35", "ForsVaruAnnatEg" }, + { "36", "ForsVaruUtomEg" }, + { "37", "InkopVaruMellan3p" }, + { "38", "ForsVaruMellan3p" }, + { "39", "ForsTjSkskAnnatEg" }, + { "40", "ForsTjOvrUtomEg" }, + { "41", "ForsKopareSkskSverige" }, + { "42", "ForsOvrigt" }, + { "50", "MomsUlagImport" }, + { "60", "MomsImportUtgHog" }, + { "61", "MomsImportUtgMedel" }, + { "62", "MomsImportUtgLag" }, +}; + +/* Öre to whole kronor, truncated toward zero like the paper blankett. */ +static int64_t eskd_kronor(int64_t ore) +{ + return ore / 100; +} + +static void eskd_add(struct buf *b, const char *s) +{ + buf_append(b, s, strlen(s)); +} + +static int eskd_append_text(struct buf *b, const char *utf8, char **err) +{ + int count = 0; + for (const unsigned char *p = (const unsigned char *)utf8; *p;) { + uint32_t cp; + int len; + if (*p < 0x80) { + cp = *p; + len = 1; + } else if ((*p & 0xE0) == 0xC0) { + cp = *p & 0x1F; + len = 2; + } else if ((*p & 0xF0) == 0xE0) { + cp = *p & 0x0F; + len = 3; + } else if ((*p & 0xF8) == 0xF0) { + cp = *p & 0x07; + len = 4; + } else { + set_err(err, "upplysning is not valid UTF-8"); + return -1; + } + for (int i = 1; i < len; i++) { + if ((p[i] & 0xC0) != 0x80) { + set_err(err, "upplysning is not valid UTF-8"); + return -1; + } + cp = (cp << 6) | (uint32_t)(p[i] & 0x3F); + } + if (cp > 0xFF) { + set_err(err, "upplysning contains a character outside ISO-8859-1"); + return -1; + } + unsigned char c = (unsigned char)cp; + if (++count > 300) { + set_err(err, "upplysning may be at most 300 characters"); + return -1; + } + if (c == '&') + eskd_add(b, "&"); + else if (c == '<') + eskd_add(b, "<"); + else if (c == '>') + eskd_add(b, ">"); + else + buf_append(b, &c, 1); + p += len; + } + return 0; +} + +static int eskd_org_nr(sqlite3 *db, int64_t org_id, char out[16], char **err) +{ + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2(db, + "SELECT 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); + char raw[32] = ""; + if (sqlite3_step(st) == SQLITE_ROW) + snprintf(raw, sizeof raw, "%s", + (const char *)sqlite3_column_text(st, 0)); + sqlite3_finalize(st); + char digits[16]; + size_t n = 0; + for (const char *p = raw; *p; p++) + if (*p >= '0' && *p <= '9' && n < sizeof digits - 1) + digits[n++] = *p; + digits[n] = '\0'; + if (n == 12) + memmove(digits, digits + 2, 11); + else if (n != 10) { + set_err(err, "org_nr must be xxxxxx-xxxx; set it in Företagsuppgifter"); + return -1; + } + snprintf(out, 16, "%.6s-%.4s", digits, digits + 6); + return 0; +} + +yyjson_mut_val *report_vat_eskd(yyjson_mut_doc *doc, sqlite3 *db, + int64_t org_id, const char *from, + const char *to, const char *upplysning, + char **err) +{ + if (!util_parse_iso_date(from) || !util_parse_iso_date(to)) { + set_err(err, "from and to must be YYYY-MM-DD"); + return NULL; + } + char orgnr[16]; + if (eskd_org_nr(db, org_id, orgnr, err) != 0) + return NULL; + yyjson_mut_val *vat = report_vat(doc, db, org_id, from, to, err); + if (!vat) + return NULL; + + int64_t box[64]; + memset(box, 0, sizeof box); + yyjson_mut_val *boxes = yyjson_mut_obj_get(vat, "boxes"); + size_t nb = yyjson_mut_arr_size(boxes); + for (size_t i = 0; i < nb; i++) { + yyjson_mut_val *b = yyjson_mut_arr_get(boxes, i); + const char *code = + yyjson_mut_get_str(yyjson_mut_obj_get(b, "box")); + if (!code || !code[0] || !code[1] || code[2]) + continue; + int hi = (code[0] - '0') * 10 + (code[1] - '0'); + if (hi >= 0 && hi < 64) + box[hi] = yyjson_mut_get_int(yyjson_mut_obj_get(b, "amount_ore")); + } + static const int out_boxes[] = { 10, 11, 12, 30, 31, 32, 60, 61, 62 }; + int64_t out_whole = 0; + for (size_t i = 0; i < sizeof out_boxes / sizeof out_boxes[0]; i++) + out_whole += eskd_kronor(box[out_boxes[i]]); + /* box 48 is signed like the blankett (negative when deducting). */ + int64_t ing = -eskd_kronor(box[48]); + int64_t betala = out_whole - ing; + char period[8]; + snprintf(period, sizeof period, "%.4s%.2s", to, to + 5); + + struct buf xml; + buf_init(&xml); + eskd_add(&xml, "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"); + eskd_add(&xml, "<eSKDUpload Version=\"6.0\">\n"); + char line[160]; + int n = snprintf(line, sizeof line, "<OrgNr>%s</OrgNr>\n", orgnr); + buf_append(&xml, line, (size_t)n); + eskd_add(&xml, "<Moms>\n"); + n = snprintf(line, sizeof line, "<Period>%s</Period>\n", period); + buf_append(&xml, line, (size_t)n); + for (size_t i = 0; i < sizeof ESKD_BOXES / sizeof ESKD_BOXES[0]; i++) { + int hi = + (ESKD_BOXES[i].box[0] - '0') * 10 + (ESKD_BOXES[i].box[1] - '0'); + int64_t kr = eskd_kronor(box[hi]); + if (kr == 0) + continue; + n = snprintf(line, sizeof line, "<%s>%lld</%s>\n", ESKD_BOXES[i].tag, + (long long)kr, ESKD_BOXES[i].tag); + buf_append(&xml, line, (size_t)n); + } + if (ing != 0) { + n = snprintf(line, sizeof line, "<MomsIngAvdr>%lld</MomsIngAvdr>\n", + (long long)ing); + buf_append(&xml, line, (size_t)n); + } + n = snprintf(line, sizeof line, "<MomsBetala>%lld</MomsBetala>\n", + (long long)betala); + buf_append(&xml, line, (size_t)n); + if (upplysning && *upplysning) { + eskd_add(&xml, "<TextUpplysningMoms>"); + if (eskd_append_text(&xml, upplysning, err) != 0) { + buf_free(&xml); + return NULL; + } + eskd_add(&xml, "</TextUpplysningMoms>\n"); + } + eskd_add(&xml, "</Moms>\n</eSKDUpload>\n"); + + unsigned char sha_raw[32]; + char sha[65]; + util_sha256(xml.p, xml.len, sha_raw); + util_hex(sha_raw, sizeof sha_raw, sha); + char *b64 = util_b64(xml.p, xml.len); + yyjson_mut_val *o = yyjson_mut_obj(doc); + yyjson_mut_obj_add_strcpy(doc, o, "org_nr", orgnr); + yyjson_mut_obj_add_strcpy(doc, o, "period", period); + yyjson_mut_obj_add_strcpy(doc, o, "from", from); + yyjson_mut_obj_add_strcpy(doc, o, "to", to); + char fname[32]; + snprintf(fname, sizeof fname, "eskd_%s.xml", period); + yyjson_mut_obj_add_strcpy(doc, o, "filename", fname); + yyjson_mut_obj_add_strcpy(doc, o, "content_base64", b64 ? b64 : ""); + yyjson_mut_obj_add_strcpy(doc, o, "sha256", sha); + yyjson_mut_obj_add_int(doc, o, "size", (int64_t)xml.len); + free(b64); + buf_free(&xml); + return o; +} |
