aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-18 21:35:10 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-18 21:35:10 +0200
commit251eb8c54c77029f0b43aa987ea50c44016147f3 (patch)
tree2bf4284ce2daab191d48579a6c5a84882060d398
parent64c0d5d79b31dafcbdb68ff938a92bd6f136d4a7 (diff)
downloadbokf-0.1.28.tar.gz
bokf-0.1.28.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.
-rw-r--r--clients/bokftui.c77
-rw-r--r--docs/COMPLIANCE.md3
-rw-r--r--docs/PROTOCOL.md8
-rw-r--r--docs/STATE.md7
-rw-r--r--src/commands.c21
-rw-r--r--src/reports.c230
-rw-r--r--src/reports.h4
-rw-r--r--tests/test_core.c29
8 files changed, 369 insertions, 10 deletions
diff --git a/clients/bokftui.c b/clients/bokftui.c
index eedf5fc..ddff00b 100644
--- a/clients/bokftui.c
+++ b/clients/bokftui.c
@@ -540,7 +540,8 @@ static char *prompt(const char *label, const char *def, int mask)
return buf;
}
-static int text_view(const char *title, const char *text)
+static int text_view(const char *title, const char *text,
+ const char *action_hint)
{
int ret = 0;
int nlines = 0;
@@ -570,7 +571,17 @@ static int text_view(const char *title, const char *text)
if (top + i < n)
mvaddnstr(2 + i, 2, lines[top + i], COLS - 4);
}
- hints("piltangenter/PgUp/PgDn rullar F5 = uppdatera q = tillbaka");
+ char hint[256];
+ if (action_hint)
+ snprintf(hint, sizeof hint,
+ "piltangenter/PgUp/PgDn rullar F5 = uppdatera %s "
+ "q = tillbaka",
+ action_hint);
+ else
+ snprintf(hint, sizeof hint,
+ "piltangenter/PgUp/PgDn rullar F5 = uppdatera "
+ "q = tillbaka");
+ hints(hint);
refresh();
int ch = ui_getch();
if (ch == 'q' || ch == 27)
@@ -579,6 +590,10 @@ static int text_view(const char *title, const char *text)
ret = 1;
break;
}
+ if (ch == 's' && action_hint) {
+ ret = 2;
+ break;
+ }
if (ch == KEY_DOWN && top + view < n)
top++;
else if (ch == KEY_UP && top > 0)
@@ -2590,7 +2605,7 @@ static void accounts_report(struct app *a)
(long long)n, (long long)active_count);
buf_append(&text, line, strlen(line));
buf_append(&text, "\0", 1);
- int again = text_view("Kontolista", (char *)text.p);
+ int again = text_view("Kontolista", (char *)text.p, NULL);
buf_free(&text);
free(resp);
if (!again)
@@ -3358,6 +3373,50 @@ static void fmt_verifikationslista(struct buf *t, const struct app *a,
buf_line(t, "\n%-69s %14s %14s\n", "Omslutning:", s1, s2);
}
+static void eskd_save(struct app *a, const char *from, const char *to)
+{
+ const char *home = getenv("HOME");
+ char period[8], def[512], full[512];
+ snprintf(period, sizeof period, "%.4s%.2s", to, to + 5);
+ snprintf(def, sizeof def, "%s/eskd_%s.xml", home && *home ? home : ".",
+ period);
+ char *path = prompt("Spara eSKD-fil: ", def, 0);
+ if (!path || !*path)
+ return;
+ if (path[0] == '~' && (path[1] == '/' || path[1] == '\0'))
+ snprintf(full, sizeof full, "%s%s", home ? home : "", path + 1);
+ else
+ snprintf(full, sizeof full, "%s", path);
+ char args[64];
+ snprintf(args, sizeof args, "{\"from\":\"%s\",\"to\":\"%s\"}", from, to);
+ char *resp =
+ client_rpc(&a->conn, "report.vat_eskd", a->session, a->org, args);
+ if (!resp || !client_ok(resp)) {
+ show_error("Kunde inte skapa eSKD", resp);
+ free(resp);
+ return;
+ }
+ char *b64 = jstr_dup(resp, "result.content_base64");
+ unsigned char *data = NULL;
+ size_t n = 0;
+ if (!b64 || util_b64_decode(b64, strlen(b64), &data, &n) != 0) {
+ message("eSKD", "Kunde inte avkoda filen från servern.");
+ } else {
+ FILE *f = fopen(full, "wb");
+ if (!f || fwrite(data, 1, n, f) != n) {
+ message("eSKD", "Kunde inte skriva %s: %s", full, strerror(errno));
+ if (f)
+ fclose(f);
+ } else {
+ fclose(f);
+ message("eSKD", "Sparat: %s\n(%zu byte, ISO-8859-1)", full, n);
+ }
+ }
+ free(data);
+ free(b64);
+ free(resp);
+}
+
static void reports_screen(struct app *a)
{
static const char *const report_items[] = {
@@ -3381,6 +3440,7 @@ static void reports_screen(struct app *a)
continue;
}
char args[256];
+ char vat_from[16] = "", vat_to[16] = "";
const char *cmd = NULL;
switch (sel) {
case 0:
@@ -3419,6 +3479,8 @@ static void reports_screen(struct app *a)
char *to = date_prompt("Till (YYYY-MM-DD): ", a->fy_end);
if (!to)
continue;
+ snprintf(vat_from, sizeof vat_from, "%s", f);
+ snprintf(vat_to, sizeof vat_to, "%s", to);
snprintf(args, sizeof args,
"{\"from\":\"%s\",\"to\":\"%s\"}", f, to);
break;
@@ -3470,10 +3532,15 @@ static void reports_screen(struct app *a)
fmt_moms(&out, a, res);
}
buf_append(&out, "", 1);
- int again = text_view("Rapport", (const char *)out.p);
+ int again = text_view("Rapport", (const char *)out.p,
+ sel == 5 ? "s = spara eSKD" : NULL);
buf_free(&out);
yyjson_doc_free(d);
free(resp);
+ if (again == 2 && sel == 5) {
+ eskd_save(a, vat_from, vat_to);
+ break;
+ }
if (!again)
break;
}
@@ -3648,7 +3715,7 @@ static void audit_screen(struct app *a)
free(code);
}
buf_append(&text, "\0", 1);
- int again = text_view("Behandlingshistorik", (char *)text.p);
+ int again = text_view("Behandlingshistorik", (char *)text.p, NULL);
buf_free(&text);
free(resp);
if (!again)
diff --git a/docs/COMPLIANCE.md b/docs/COMPLIANCE.md
index a152cd4..abc6247 100644
--- a/docs/COMPLIANCE.md
+++ b/docs/COMPLIANCE.md
@@ -153,7 +153,8 @@ approval by any authority — none exists.
1. Ledger core + protocol + CLI + TUI (MVP).
2. SIE round-trip test suite and migration dry-run against files exported from
Fortnox/Visma/BL.
-3. eSKD file for momsdeklaration; AGI file for payroll.
+3. ~~eSKD file for momsdeklaration~~ (`report.vat_eskd`, upload in
+ Skatteverket's e-service); AGI file for payroll.
4. K2 årsredovisning document generation + SRU files for INK2.
5. Invoicing and reskontra; bank import (CSV, then PSD2).
6. Peppol e-invoicing, if still relevant when it is due.
diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md
index b8ec759..e3e629b 100644
--- a/docs/PROTOCOL.md
+++ b/docs/PROTOCOL.md
@@ -367,6 +367,7 @@ vouchers at posting time (`attachment_ids`) or afterwards via
| `report.general_ledger` | `fiscal_year`, `accounts?`, `from?`, `to?` | huvudbok |
| `report.voucher_list` | `fiscal_year`, `series?` | grundbok/verifikationslista |
| `report.vat` | `from`, `to`, `period_type?` | momsdeklaration ruta för ruta |
+| `report.vat_eskd` | `from`, `to`, `upplysning?` | eSKD XML (ISO-8859-1) as `content_base64` |
All reports are pure reads, respect locks, and return JSON rows. Amounts are
öre. `report.general_ledger` (huvudbok) returns account blocks:
@@ -379,7 +380,12 @@ All reports are pure reads, respect locks, and return JSON rows. Amounts are
"to","last_voucher":{...},"vouchers":[{"id","series","number","date",
"description","rows":[{"account","name","debit_ore","credit_ore",
"description"}]}],"totals":{"debit_ore","credit_ore"}}` and takes an
-optional `series` filter. Both cover the whole fiscal year; the ledger's
+`series` filter. `report.vat_eskd` builds Skatteverket's `eSKDUpload`
+Version 6.0 XML for the period ending at `to` (whole kronor, öre truncated
+like the blankett; box 48 positive as filed) and returns `{"org_nr",
+"period","from","to","filename","sha256","size","content_base64"}`; the
+bytes are ISO-8859-1, so write them verbatim to a `.xml` file. Both cover
+the whole fiscal year; the ledger's
period can be narrowed with `from`/`to`. `report.vat` returns `{"from","to","boxes":[{"box":"05","label":"...","amount_ore":...}],"note"}`.
Rules sharing a box are summed into a single entry. `box 49` is the sum of
the moms boxes (`10`,`11`,`12`,`30`,`31`,`32`,`48`,`60`,`61`,`62`), so box 48
diff --git a/docs/STATE.md b/docs/STATE.md
index b158de8..0e5d36e 100644
--- a/docs/STATE.md
+++ b/docs/STATE.md
@@ -86,15 +86,16 @@ server/protocol/ledger only.
`attachment.put {voucher_id}`): proposed TUI actions — `Ctrl+F` in the
voucher detail to upload+link, and selecting an inbox item and pressing a
key to link it to a voucher picked from a list. Waiting for a go-ahead.
-- Priority between **eSKD moms filing** and **bokslut/K2+SRU** for the next
- backend milestone (eSKD was suggested first).
+- Next backend milestone: **bokslut automation** vs **K2/SRU** (eSKD is
+ done: `report.vat_eskd` + `s = spara eSKD` in the momsrapport).
- Moms `report_rules` seed is a corrected starter mapping (schema v3), but
there is still no command/TUI to edit rules per org; add one before filing
if the mapping needs adjustments (SCHEMA.md §10 promises owner editing).
## Backlog (prioritized, from COMPLIANCE.md §10 and the audit)
-1. eSKD file generation for momsdeklaration.
+1. ~~eSKD file generation for momsdeklaration.~~ `report.vat_eskd` (eSKDUpload
+ 6.0, ISO-8859-1, whole kronor) with a save action in the TUI momsrapport.
2. Bokslut automation (avskrivningar, periodiseringsfond, skatt,
resultatdisposition).
3. K2 årsredovisning document + SRU files (INK2/INK2R/INK2S).
diff --git a/src/commands.c b/src/commands.c
index 1034b31..e16ba75 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -3600,6 +3600,25 @@ static yyjson_mut_val *h_report_voucher_list(struct req *r)
return res;
}
+static yyjson_mut_val *h_report_vat_eskd(struct req *r)
+{
+ const char *from = arg_str(r->args, "from");
+ const char *to = arg_str(r->args, "to");
+ if (!from || !to)
+ return fail(r, "INVALID_ARGS", "from and to are required");
+ char *err = NULL;
+ yyjson_mut_val *res =
+ report_vat_eskd(r->rdoc, r->db, r->org_id, from, to,
+ arg_str(r->args, "upplysning"), &err);
+ if (!res) {
+ yyjson_mut_val *e = fail(r, "INVALID_ARGS",
+ err ? err : "could not build the eSKD file");
+ free(err);
+ return e;
+ }
+ return res;
+}
+
static unsigned char *read_file(const char *path, size_t *out_len)
{
FILE *f = fopen(path, "rb");
@@ -3841,6 +3860,8 @@ const struct command g_commands[] = {
h_report_general_ledger },
{ "report.voucher_list", "Verifikationslista", PERM_READ, 1, 0, 0,
h_report_voucher_list },
+ { "report.vat_eskd", "eSKD XML for the momsdeklaration (ISO-8859-1)",
+ PERM_READ, 1, 0, 0, h_report_vat_eskd },
{ "sie.export", "Export SIE 4 (PC8)", PERM_READ, 1, 0, 0, h_sie_export },
{ "sie.import", "Import SIE 4 into an empty fiscal year", PERM_WRITE, 1, 1,
1, h_sie_import },
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, "&amp;");
+ else if (c == '<')
+ eskd_add(b, "&lt;");
+ else if (c == '>')
+ eskd_add(b, "&gt;");
+ 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;
+}
diff --git a/src/reports.h b/src/reports.h
index d4a0545..c4a6e4e 100644
--- a/src/reports.h
+++ b/src/reports.h
@@ -26,5 +26,9 @@ yyjson_mut_val *report_general_ledger(yyjson_mut_doc *doc, sqlite3 *db,
yyjson_mut_val *report_voucher_list(yyjson_mut_doc *doc, sqlite3 *db,
int64_t org_id, int64_t fy_id,
const char *series, char **err);
+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);
#endif
diff --git a/tests/test_core.c b/tests/test_core.c
index 5740db3..aa2bd4a 100644
--- a/tests/test_core.c
+++ b/tests/test_core.c
@@ -820,6 +820,35 @@ int main(void)
CHECK(yyjson_arr_size(jget(d, "result.vouchers")) == 0);
yyjson_doc_free(d);
+ /* eSKD: ISO-8859-1 XML for the momsdeklaration */
+ d = call(reqf("{\"v\":1,\"id\":\"54e\",\"cmd\":\"report.vat_eskd\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"from\":\"2026-01-01\",\"to\":\"2026-01-31\","
+ "\"upplysning\":\"Blå\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.org_nr", "559331-2126");
+ CHECK_STR(d, "result.period", "202601");
+ char *b64 = jstr(d, "result.content_base64");
+ unsigned char *xmlb = NULL;
+ size_t xmll = 0;
+ CHECK(b64 && util_b64_decode(b64, strlen(b64), &xmlb, &xmll) == 0);
+ char *xml = xmalloc(xmll + 1);
+ memcpy(xml, xmlb, xmll);
+ xml[xmll] = '\0';
+ free(xmlb);
+ CHECK(strstr(xml, "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>") !=
+ NULL);
+ CHECK(strstr(xml, "<OrgNr>559331-2126</OrgNr>") != NULL);
+ CHECK(strstr(xml, "<Period>202601</Period>") != NULL);
+ CHECK(strstr(xml, "<ForsMomsEjAnnan>1000</ForsMomsEjAnnan>") != NULL);
+ CHECK(strstr(xml, "<MomsUtgHog>250</MomsUtgHog>") != NULL);
+ CHECK(strstr(xml, "<MomsBetala>250</MomsBetala>") != NULL);
+ CHECK(strstr(xml, "<TextUpplysningMoms>Bl\xe5</TextUpplysningMoms>") !=
+ NULL);
+ free(xml);
+ yyjson_doc_free(d);
+
d = call(reqf("{\"v\":1,\"id\":\"54\",\"cmd\":\"report.trial_balance\","
"\"session\":\"%s\",\"org\":%d}",
g_session, (int)org_id));