aboutsummaryrefslogtreecommitdiff
path: root/src/reports.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/reports.c')
-rw-r--r--src/reports.c230
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, "&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;
+}