diff options
Diffstat (limited to 'tests/test_core.c')
| -rw-r--r-- | tests/test_core.c | 667 |
1 files changed, 665 insertions, 2 deletions
diff --git a/tests/test_core.c b/tests/test_core.c index 12d42e5..54e73f5 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -40,6 +40,18 @@ static void mark_failed(void) g_failed[g_failed_n++] = g_test; } +static int count_of(const char *text, const char *needle) +{ + size_t n = strlen(needle); + int count = 0; + + if (!n) + return 0; + for (const char *p = text; (p = strstr(p, needle)) != NULL; p++) + count++; + return count; +} + struct tctx { char tmpdir[512]; char backupdir[512]; @@ -1549,6 +1561,116 @@ static void test_reports(struct tctx *t) yyjson_doc_free(d); } +static void test_imported_closings(struct tctx *t) +{ + yyjson_doc *d; + int64_t org = 0; + + (void)t; + d = call(reqf("{\"v\":1,\"id\":\"ic1\",\"cmd\":\"org.create\"," + "\"session\":\"%s\",\"args\":{\"name\":\"Import AB\"}}", + g_session)); + CHECK_OK(d); + org = jint(d, "result.id"); + CHECK(org > 0); + yyjson_doc_free(d); + + /* The source system closes the P&L accounts straight to 2099; the income + statement must still show the year's real figures. The file is PC8 + (CP437) like real exports: \x84 = ä, \x94 = ö. */ + static const char sie[] = + "#FLAGGA 0\n" + "#FORMAT PC8\n" + "#SIETYP 4\n" + "#FNAMN \"Import AB\"\n" + "#RAR 0 20260101 20261231\n" + "#VER \"V\" \"1\" 20260201 \"F\x94rs\x84" "ljning\"\n" + "{\n#TRANS 1930 {} 1250.00\n#TRANS 3001 {} -1000.00\n" + "#TRANS 2611 {} -250.00\n}\n" + "#VER \"V\" \"2\" 20260202 \"Ink\x94" "p\"\n" + "{\n#TRANS 5410 {} 200.00\n#TRANS 2640 {} 50.00\n" + "#TRANS 1930 {} -250.00\n}\n" + "#VER \"V\" \"3\" 20260430 \"St\x84" "ng intäktskonton\"\n" + "{\n#TRANS 3001 {} 1000.00\n#TRANS 2099 {} -1000.00\n}\n" + "#VER \"V\" \"4\" 20260430 \"St\x84" "ng kostnadskonton\"\n" + "{\n#TRANS 2099 {} 200.00\n#TRANS 5410 {} -200.00\n}\n"; + char *sie_b64 = util_b64((const unsigned char *)sie, strlen(sie)); + d = call_sie_import(g_session, org, sie_b64, 0); + free(sie_b64); + CHECK_OK(d); + CHECK(jint(d, "result.vouchers") == 4); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ic6\",\"cmd\":\"report.income_statement\"," + "\"session\":\"%s\",\"org\":%d}", + g_session, (int)org)); + CHECK_OK(d); + CHECK(jint(d, "result.result_ore") == 80000); + CHECK(find_amount(d, "result.revenue.accounts", "account", "3001", + "amount_ore") == 100000); + CHECK(find_amount(d, "result.expenses.accounts", "account", "5410", + "amount_ore") == 20000); + yyjson_doc_free(d); + + /* The result sits in 2099, so the balance sheet must not count it twice. */ + d = call(reqf("{\"v\":1,\"id\":\"ic7\",\"cmd\":\"report.balance_sheet\"," + "\"session\":\"%s\",\"org\":%d}", + g_session, (int)org)); + CHECK_OK(d); + /* 2640 is chart-typed as a liability (debit balance shows negative) */ + CHECK(jint(d, "result.assets.total_ore") == 100000); + CHECK(jint(d, "result.liabilities.total_ore") == 20000); + CHECK(jint(d, "result.equity.total_ore") == 80000); + CHECK(jint(d, "result.assets.total_ore") == + jint(d, "result.liabilities.total_ore") + + jint(d, "result.equity.total_ore")); + CHECK(find_amount(d, "result.equity.accounts", "account", "2099", + "amount_ore") == 80000); + yyjson_doc_free(d); + + /* the declaration path: the SRU result must not be zeroed either */ + d = call(reqf("{\"v\":1,\"id\":\"ic8\",\"cmd\":\"org.update\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"org_nr\":\"559331-2126\",\"postal_code\":\"192 48\"," + "\"city\":\"Sollentuna\"}}", + g_session, (int)org)); + CHECK_OK(d); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ic9\",\"cmd\":\"sru.export\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{}}", + g_session, (int)org)); + CHECK_OK(d); + const char *sb64 = jstr(d, "result.blanketter.content_base64"); + unsigned char *srub = NULL; + size_t srul = 0; + CHECK(sb64 && util_b64_decode(sb64, strlen(sb64), &srub, &srul) == 0); + char *sru = xmalloc(srul + 1); + memcpy(sru, srub, srul); + sru[srul] = '\0'; + free(srub); + CHECK(strstr(sru, "#UPPGIFT 7450 800") != NULL); + CHECK(strstr(sru, "#UPPGIFT 7650 800") != NULL); + free(sru); + yyjson_doc_free(d); + + /* a voucher entered in bokf is never skipped, whatever its text */ + d = call(reqf("{\"v\":1,\"id\":\"ic10\",\"cmd\":\"voucher.post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" + "\"2026-05-02\",\"description\":\"Stängsel\",\"rows\":[" + "{\"account\":\"1930\",\"debit_ore\":100}," + "{\"account\":\"3001\",\"credit_ore\":100}]}}", + g_session, (int)org)); + CHECK_OK(d); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"ic11\",\"cmd\":\"report.income_statement\"," + "\"session\":\"%s\",\"org\":%d}", + g_session, (int)org)); + CHECK_OK(d); + CHECK(jint(d, "result.result_ore") == 80100); + yyjson_doc_free(d); +} + static void test_general_ledger(struct tctx *t) { yyjson_doc *d; @@ -2055,6 +2177,126 @@ static void test_ib(struct tctx *t) yyjson_doc_free(d); } +/* Opening balances carry earlier years' IB vouchers; P&L accounts restart + at every year start (reports and SIE export). */ +static void test_ib_carry(struct tctx *t) +{ + (void)t; + yyjson_doc *d; + + d = call(reqf("{\"v\":1,\"id\":\"ibc1\",\"cmd\":\"org.create\"," + "\"session\":\"%s\",\"args\":{\"name\":\"Överföring AB\"}}", + g_session)); + CHECK_OK(d); + int org = (int)jint(d, "result.id"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"ibc2\",\"cmd\":\"fiscal_year.list\"," + "\"session\":\"%s\",\"org\":%d}", + g_session, org)); + CHECK_OK(d); + char start[11] = "", end[11] = ""; + snprintf(start, sizeof start, "%s", jstr(d, "result.items.0.start_date")); + snprintf(end, sizeof end, "%s", jstr(d, "result.items.0.end_date")); + yyjson_doc_free(d); + int y = atoi(end) + 1; + + const char *posts[][4] = { + { start, "IB", "1940", "2010" }, + { start, "V", "1930", "3001" }, + { end, "V", "8999", "2099" }, + }; + const int64_t amounts[] = { 2500000, 100000, 100000 }; + for (size_t i = 0; i < 3; i++) { + d = call(reqf("{\"v\":1,\"id\":\"ibc3\",\"cmd\":\"voucher.post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" + "\"%s\",\"description\":\"År 1\",\"series\":\"%s\"," + "\"rows\":[{\"account\":\"%s\",\"debit_ore\":%lld}," + "{\"account\":\"%s\",\"credit_ore\":%lld}]}}", + g_session, org, posts[i][0], posts[i][1], posts[i][2], + (long long)amounts[i], posts[i][3], + (long long)amounts[i])); + CHECK_OK(d); + yyjson_doc_free(d); + } + + d = call(reqf("{\"v\":1,\"id\":\"ibc4\",\"cmd\":\"fiscal_year.open\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"label\":\"%d\"," + "\"start_date\":\"%d-01-01\",\"end_date\":\"%d-12-31\"}}", + g_session, org, y, y, y)); + CHECK_OK(d); + int64_t fy2 = jint(d, "result.id"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"ibc5\",\"cmd\":\"voucher.post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" + "\"%d-02-01\",\"description\":\"År 2\",\"rows\":" + "[{\"account\":\"1930\",\"debit_ore\":5000}," + "{\"account\":\"3001\",\"credit_ore\":5000}]}}", + g_session, org, y)); + CHECK_OK(d); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ibc6\",\"cmd\":\"report.trial_balance\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" + "%lld}}", + g_session, org, (long long)fy2)); + CHECK_OK(d); + CHECK(find_amount(d, "result.accounts", "account", "1940", "ib_ore") == + 2500000); + CHECK(find_amount(d, "result.accounts", "account", "2010", "ib_ore") == + -2500000); + CHECK(find_amount(d, "result.accounts", "account", "2099", "ib_ore") == + -100000); + CHECK(find_amount(d, "result.accounts", "account", "3001", "ib_ore") == 0); + CHECK(find_amount(d, "result.accounts", "account", "8999", "ib_ore") == -1); + CHECK(jint(d, "result.totals.ib_ore") == 0); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ibc7\",\"cmd\":\"report.trial_balance\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" + "%lld,\"from\":\"%d-03-01\"}}", + g_session, org, (long long)fy2, y)); + CHECK_OK(d); + CHECK(find_amount(d, "result.accounts", "account", "3001", "ib_ore") == + -5000); + CHECK(find_amount(d, "result.accounts", "account", "1930", "ib_ore") == + 105000); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ibc8\",\"cmd\":\"report.balance_sheet\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" + "%lld}}", + g_session, org, (long long)fy2)); + CHECK_OK(d); + CHECK(find_amount(d, "result.equity.accounts", "account", "2010", + "amount_ore") == 2500000); + CHECK(jint(d, "result.assets.total_ore") == 2605000); + CHECK(jint(d, "result.assets.total_ore") == + jint(d, "result.liabilities.total_ore") + + jint(d, "result.equity.total_ore")); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ibc9\",\"cmd\":\"sie.export\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" + "%lld}}", + g_session, org, (long long)fy2)); + CHECK_OK(d); + FILE *sf = fopen(jstr(d, "result.path"), "rb"); + yyjson_doc_free(d); + CHECK(sf != NULL); + if (sf) { + char sbuf[65536]; + size_t sn = fread(sbuf, 1, sizeof sbuf - 1, sf); + sbuf[sn] = '\0'; + fclose(sf); + CHECK(strstr(sbuf, "#IB 0 2010 -25000.00") != NULL); + CHECK(strstr(sbuf, "#UB 0 1930 1050.00") != NULL); + CHECK(strstr(sbuf, "#RES 0 3001 -50.00") != NULL); + CHECK(strstr(sbuf, "#IB 0 3001") == NULL); + CHECK(strstr(sbuf, "#UB 0 3001") == NULL); + CHECK(strstr(sbuf, "#RES 0 1930") == NULL); + } +} + static void test_period_lock(struct tctx *t) { yyjson_doc *d; @@ -2315,6 +2557,197 @@ static void test_settings(struct tctx *t) CHECK_OK(d); CHECK_STR(d, "result.attachment_dir", "/tmp/bilagor"); yyjson_doc_free(d); + + /* smtp addresses are checked when they are saved */ + d = call(reqf("{\"v\":1,\"id\":\"smtpaddr1\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"smtp_from\",\"value\":\"Anders Bergsten\"}}", + g_session, (int)t->org_id)); + CHECK_STR(d, "error.code", "INVALID_ARGS"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"smtpaddr2\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"smtp_reply_to\",\"value\":\"inte en adress\"}}", + g_session, (int)t->org_id)); + CHECK_STR(d, "error.code", "INVALID_ARGS"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"smtpaddr3\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"smtp_from\",\"value\":\"anders@example.com\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + CHECK_STR(d, "result.value", "anders@example.com"); + yyjson_doc_free(d); + + /* the invoice header colour must be #rrggbb */ + d = call(reqf("{\"v\":1,\"id\":\"hdr0\",\"cmd\":\"settings.get\"," + "\"session\":\"%s\",\"org\":%d}", + g_session, (int)t->org_id)); + CHECK_OK(d); + CHECK_STR(d, "result.document_header_color", "#314c59"); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"hdr1\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"document_header_color\",\"value\":\"#1a2b3c\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + CHECK_STR(d, "result.value", "#1a2b3c"); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"hdr2\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"document_header_color\",\"value\":\"1a2b3c\"}}", + g_session, (int)t->org_id)); + CHECK_STR(d, "error.code", "INVALID_ARGS"); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"hdr3\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"document_header_color\",\"value\":\"#12345\"}}", + g_session, (int)t->org_id)); + CHECK_STR(d, "error.code", "INVALID_ARGS"); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"hdr4\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"document_header_color\",\"value\":\"#1234567\"}}", + g_session, (int)t->org_id)); + CHECK_STR(d, "error.code", "INVALID_ARGS"); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"hdr5\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"document_header_color\",\"value\":\"#GGGGGG\"}}", + g_session, (int)t->org_id)); + CHECK_STR(d, "error.code", "INVALID_ARGS"); + yyjson_doc_free(d); +} + +static void test_series(struct tctx *t) +{ + yyjson_doc *d; + (void)t; + + /* own org: the series settings and their vouchers must not disturb the + shared test org's counts and defaults */ + d = call(reqf("{\"v\":1,\"id\":\"sv0\",\"cmd\":\"org.create\"," + "\"session\":\"%s\",\"args\":{\"name\":\"Serie AB\"," + "\"org_nr\":\"5560001111\"}}", + g_session)); + CHECK_OK(d); + int org = (int)jint(d, "result.id"); + CHECK(org > 0); + yyjson_doc_free(d); + + /* the legacy default_series seeds series_voucher; the other features + have their own conventional defaults */ + d = call(reqf("{\"v\":1,\"id\":\"sv1a\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"default_series\",\"value\":\"V-\"}}", + g_session, org)); + CHECK_OK(d); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"sv1\",\"cmd\":\"settings.get\"," + "\"session\":\"%s\",\"org\":%d}", + g_session, org)); + CHECK_OK(d); + CHECK_STR(d, "result.series_voucher", "V-"); + CHECK_STR(d, "result.series_invoice", "F"); + CHECK_STR(d, "result.series_payroll", "L"); + CHECK_STR(d, "result.series_bokslut", "Å"); + CHECK_STR(d, "result.series_ib", "IB"); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"sv2\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"series_foo\",\"value\":\"X\"}}", + g_session, org)); + CHECK_STR(d, "error.code", "UNSUPPORTED"); + yyjson_doc_free(d); + + /* an explicit per-feature voucher series wins over the legacy key */ + d = call(reqf("{\"v\":1,\"id\":\"sv3\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"series_voucher\",\"value\":\"S-\"}}", + g_session, org)); + CHECK_OK(d); + CHECK_STR(d, "result.value", "S-"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"sv4\",\"cmd\":\"voucher.post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" + "\"2026-09-20\",\"description\":\"Serieserie\",\"rows\":" + "[{\"account\":\"1930\",\"debit_ore\":100},{\"account\":" + "\"3001\",\"credit_ore\":100}]}}", + g_session, org)); + CHECK_OK(d); + CHECK_STR(d, "result.series", "S-"); + yyjson_doc_free(d); + + /* invoices post in the invoice series */ + d = call(reqf("{\"v\":1,\"id\":\"sv5\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"series_invoice\",\"value\":\"F2\"}}", + g_session, org)); + CHECK_OK(d); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"sv6\",\"cmd\":\"customer.create\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":" + "\"Serie Test AB\"}}", + g_session, org)); + CHECK_OK(d); + int64_t cust = jint(d, "result.id"); + CHECK(cust > 0); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"sv7\",\"cmd\":\"invoice.issue\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":" + "%lld,\"invoice_date\":\"2026-09-20\",\"due_date\":" + "\"2026-10-20\",\"rows\":[{\"description\":\"Konsult\"," + "\"quantity\":\"1\",\"unit_price_ore\":100000," + "\"vat_code\":\"25\"}]}}", + g_session, org, (long long)cust)); + CHECK_OK(d); + int64_t voucher = jint(d, "result.voucher_id"); + CHECK(voucher > 0); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"sv8\",\"cmd\":\"voucher.get\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", + g_session, org, (long long)voucher)); + CHECK_OK(d); + CHECK_STR(d, "result.series", "F2"); + yyjson_doc_free(d); + + /* the configured IB series counts as ingående balans */ + d = call(reqf("{\"v\":1,\"id\":\"sv9\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"series_ib\",\"value\":\"IBX\"}}", + g_session, org)); + CHECK_OK(d); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"sv10\",\"cmd\":\"report.trial_balance\"," + "\"session\":\"%s\",\"org\":%d}", + g_session, org)); + CHECK_OK(d); + int64_t ib_before = find_amount(d, "result.accounts", "account", "1930", + "ib_ore"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"sv11\",\"cmd\":\"voucher.post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" + "\"2026-01-01\",\"description\":\"IB serietest\"," + "\"series\":\"IBX\",\"rows\":[{\"account\":\"1930\"," + "\"debit_ore\":700},{\"account\":\"2010\"," + "\"credit_ore\":700}]}}", + g_session, org)); + CHECK_OK(d); + CHECK_STR(d, "result.series", "IBX"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"sv12\",\"cmd\":\"report.trial_balance\"," + "\"session\":\"%s\",\"org\":%d}", + g_session, org)); + CHECK_OK(d); + CHECK(find_amount(d, "result.accounts", "account", "1930", "ib_ore") == + ib_before + 700); + yyjson_doc_free(d); } static void test_smtp_settings(struct tctx *t) @@ -2385,7 +2818,7 @@ static void test_smtp_settings(struct tctx *t) d = call(reqf("{\"v\":1,\"id\":\"110\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" - "{\"key\":\"smtp_from\",\"value\":\"Bokf AB <a@b.se>\"}}", + "{\"key\":\"smtp_from\",\"value\":\"faktura@example.se\"}}", g_session, (int)t->org_id)); CHECK_OK(d); yyjson_doc_free(d); @@ -2412,7 +2845,7 @@ static void test_smtp_settings(struct tctx *t) CHECK_STR(d, "result.smtp_host", "smtp.example.se"); CHECK_STR(d, "result.smtp_port", "587"); CHECK_STR(d, "result.smtp_user", "faktura@example.se"); - CHECK_STR(d, "result.smtp_from", "Bokf AB <a@b.se>"); + CHECK_STR(d, "result.smtp_from", "faktura@example.se"); CHECK_STR(d, "result.smtp_reply_to", "svar@example.se"); CHECK_STR(d, "result.smtp_security", "tls"); yyjson_doc_free(d); @@ -3169,6 +3602,41 @@ static void test_invoices(struct tctx *t) CHECK(stored && preview_b64 && strcmp(stored, preview_b64) == 0); yyjson_doc_free(d); + /* the invoice header colour setting reaches the rendered document */ + d = call(reqf("{\"v\":1,\"id\":\"354a\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"document_header_color\",\"value\":\"#ff0000\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"354b\",\"cmd\":\"invoice.preview\"," + "\"session\":\"%s\",\"org\":%d,\"args\":%s}", + g_session, (int)t->org_id, draft_args)); + CHECK_OK(d); + const char *red_b64 = jstr(d, "result.content_base64"); + unsigned char *red_pdf = NULL; + size_t red_len = 0; + CHECK(red_b64 && + util_b64_decode(red_b64, strlen(red_b64), &red_pdf, &red_len) == 0); + if (red_pdf) { + char *red_text = xmalloc(red_len + 1); + + memcpy(red_text, red_pdf, red_len); + red_text[red_len] = '\0'; + CHECK(strstr(red_text, "1 0 0 rg") != NULL); + free(red_text); + } + free(red_pdf); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"354c\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"document_header_color\",\"value\":\"#314c59\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"355\",\"cmd\":\"invoice.issue\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":" "%lld,\"invoice_date\":\"2026-09-21\",\"due_date\":" @@ -3279,6 +3747,120 @@ static void test_invoices(struct tctx *t) free(preview_b64); } +static void test_invoice_extras(struct tctx *t) +{ + yyjson_doc *d; + + d = call(reqf("{\"v\":1,\"id\":\"ex1\",\"cmd\":\"customer.create\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":" + "\"Extras AB\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + int64_t cust = jint(d, "result.id"); + CHECK(cust > 0); + yyjson_doc_free(d); + + /* a text row contributes nothing and is flagged in the response */ + d = call(reqf("{\"v\":1,\"id\":\"ex2\",\"cmd\":\"invoice.issue\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":" + "%lld,\"invoice_date\":\"2026-09-21\",\"due_date\":" + "\"2026-10-21\",\"rows\":[{\"description\":\"Rubrik\"," + "\"text\":true},{\"description\":\"Konsult\"," + "\"quantity\":\"1\",\"unit\":\"st\",\"unit_price_ore\":" + "100000,\"vat_code\":\"25\"}]}}", + g_session, (int)t->org_id, (long long)cust)); + CHECK_OK(d); + int64_t inv = jint(d, "result.id"); + CHECK(jint(d, "result.total_ore") == 125000); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ex3\",\"cmd\":\"invoice.get\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", + g_session, (int)t->org_id, (long long)inv)); + CHECK_OK(d); + CHECK(jbool(d, "result.rows.0.is_text")); + CHECK_STR(d, "result.rows.0.description", "Rubrik"); + CHECK(jint(d, "result.rows.0.amount_ore") == 0); + CHECK(!jbool(d, "result.rows.1.is_text")); + CHECK(jint(d, "result.rows.1.amount_ore") == 100000); + CHECK_STR(d, "result.paid_date", ""); + yyjson_doc_free(d); + + /* an all-text draft has no total */ + d = call(reqf("{\"v\":1,\"id\":\"ex4\",\"cmd\":\"invoice.issue\"," + "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":" + "{\"customer_id\":%lld,\"invoice_date\":\"2026-09-21\"," + "\"due_date\":\"2026-10-21\",\"rows\":[{\"description\":" + "\"Bara text\",\"text\":true}]}}", + g_session, (int)t->org_id, (long long)cust)); + CHECK_STR(d, "error.code", "INVALID_ARGS"); + yyjson_doc_free(d); + + /* the payment voucher must credit the receivable with the total */ + d = call(reqf("{\"v\":1,\"id\":\"ex5\",\"cmd\":\"voucher.post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" + "\"2026-09-21\",\"description\":\"Fel belopp\",\"rows\":" + "[{\"account\":\"1930\",\"debit_ore\":100},{\"account\":" + "\"1510\",\"credit_ore\":100}]}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + int64_t badvoucher = jint(d, "result.id"); + CHECK(badvoucher > 0); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"ex6\",\"cmd\":\"invoice.pay\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," + "\"voucher_id\":%lld}}", + g_session, (int)t->org_id, (long long)inv, + (long long)badvoucher)); + CHECK_STR(d, "error.code", "INVALID_ARGS"); + yyjson_doc_free(d); + + /* the right amount passes, also as a dry run first */ + d = call(reqf("{\"v\":1,\"id\":\"ex7\",\"cmd\":\"voucher.post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" + "\"2026-09-22\",\"description\":\"Betalning\",\"rows\":" + "[{\"account\":\"1930\",\"debit_ore\":125000}," + "{\"account\":\"1510\",\"credit_ore\":125000}]}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + int64_t voucher = jint(d, "result.id"); + CHECK(voucher > 0); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"ex8\",\"cmd\":\"invoice.pay\"," + "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":" + "{\"id\":%lld,\"voucher_id\":%lld}}", + g_session, (int)t->org_id, (long long)inv, + (long long)voucher)); + CHECK_OK(d); + CHECK(jbool(d, "result.dry_run")); + CHECK_STR(d, "result.paid_date", "2026-09-22"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"ex9\",\"cmd\":\"invoice.pay\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," + "\"voucher_id\":%lld}}", + g_session, (int)t->org_id, (long long)inv, + (long long)voucher)); + CHECK_OK(d); + CHECK_STR(d, "result.paid_date", "2026-09-22"); + const char *ser = jstr(d, "result.voucher_series"); + CHECK(ser && *ser); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"ex10\",\"cmd\":\"invoice.get\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", + g_session, (int)t->org_id, (long long)inv)); + CHECK_OK(d); + CHECK_STR(d, "result.paid_date", "2026-09-22"); + CHECK(jint(d, "result.payment_voucher_id") == voucher); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"ex11\",\"cmd\":\"invoice.pay\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," + "\"voucher_id\":%lld}}", + g_session, (int)t->org_id, (long long)inv, + (long long)voucher)); + CHECK_STR(d, "error.code", "CONFLICT"); + yyjson_doc_free(d); +} + static void test_invoice_send(struct tctx *t) { yyjson_doc *d; @@ -3588,6 +4170,52 @@ static void test_moms_rules(struct tctx *t) yyjson_doc_free(d); } +/* The momsomföring (26xx -> 2650) inside the period must not zero the boxes. */ +static void test_vat_settlement(struct tctx *t) +{ + (void)t; + yyjson_doc *d; + + d = call(reqf("{\"v\":1,\"id\":\"vs1\",\"cmd\":\"org.create\"," + "\"session\":\"%s\",\"args\":{\"name\":\"Omföring AB\"}}", + g_session)); + CHECK_OK(d); + int org = (int)jint(d, "result.id"); + yyjson_doc_free(d); + + const char *rows[] = { + "{\"account\":\"1930\",\"debit_ore\":125000}," + "{\"account\":\"3001\",\"credit_ore\":100000}," + "{\"account\":\"2611\",\"credit_ore\":25000}", + "{\"account\":\"6540\",\"debit_ore\":8000}," + "{\"account\":\"2641\",\"debit_ore\":2000}," + "{\"account\":\"1930\",\"credit_ore\":10000}", + "{\"account\":\"2611\",\"debit_ore\":25000}," + "{\"account\":\"2641\",\"credit_ore\":2000}," + "{\"account\":\"2650\",\"credit_ore\":23000}", + }; + const char *dates[] = { "2026-03-01", "2026-03-02", "2026-03-31" }; + for (size_t i = 0; i < 3; i++) { + d = call(reqf("{\"v\":1,\"id\":\"vs2\",\"cmd\":\"voucher.post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" + "\"%s\",\"description\":\"Moms\",\"rows\":[%s]}}", + g_session, org, dates[i], rows[i])); + CHECK_OK(d); + yyjson_doc_free(d); + } + + d = call(reqf("{\"v\":1,\"id\":\"vs3\",\"cmd\":\"report.vat\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"from\":\"2026-03-01\",\"to\":\"2026-03-31\"}}", + g_session, org)); + CHECK_OK(d); + CHECK(vat_box(d, "05") == 100000); + CHECK(vat_box(d, "10") == 25000); + CHECK(vat_box(d, "48") == -2000); + CHECK(vat_box(d, "49") == 23000); + yyjson_doc_free(d); +} + static void test_rules_editor(struct tctx *t) { yyjson_doc *d; @@ -4554,6 +5182,12 @@ static void test_payroll(struct tctx *t) yyjson_doc_free(d); /* post: one balanced voucher, the run and its lines */ + d = call(reqf("{\"v\":1,\"id\":\"p13a\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"series_payroll\",\"value\":\"L9\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"p13\",\"cmd\":\"payroll.run_post\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"period\":\"2026-08\",\"pay_date\":\"2026-08-25\"}}", @@ -4575,6 +5209,13 @@ static void test_payroll(struct tctx *t) g_session, (int)t->org_id, (long long)voucher)); CHECK_OK(d); CHECK_STR(d, "result.source", "payroll"); + CHECK_STR(d, "result.series", "L9"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"p14b\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"series_payroll\",\"value\":\"L\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); yyjson_doc_free(d); char sql[512]; @@ -4811,6 +5452,14 @@ static void test_payslip(struct tctx *t) CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); + /* the shared document header colour reaches the payslip too */ + d = call(reqf("{\"v\":1,\"id\":\"ps5b\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"document_header_color\",\"value\":\"#ff0000\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + yyjson_doc_free(d); + char req[512]; snprintf(req, sizeof req, "{\"v\":1,\"id\":\"ps6\",\"cmd\":\"payroll.payslip\"," @@ -4842,12 +5491,21 @@ static void test_payslip(struct tctx *t) CHECK(strstr(text, "-4 208,00") != NULL); CHECK(strstr(text, "Arbetsgivaravgifter 31,42 % betalas av" " arbetsgivaren.") != NULL); + CHECK(strstr(text, "1 0 0 rg") != NULL); + CHECK(count_of(text, "AB Ett") == 2); free(text); } free(pdf); } yyjson_doc_free(rd); free(raw); + + d = call(reqf("{\"v\":1,\"id\":\"ps7\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"key\":" + "\"document_header_color\",\"value\":\"#314c59\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + yyjson_doc_free(d); } static void test_payslip_mail(struct tctx *t) @@ -5112,6 +5770,7 @@ static const struct ttest TESTS[] = { { "vouchers", test_vouchers, "org_members" }, { "voucher_errors", test_voucher_errors, "org_members" }, { "reports", test_reports, "vouchers" }, + { "imported_closings", test_imported_closings, "org_members" }, { "general_ledger", test_general_ledger, "vouchers" }, { "voucher_list", test_voucher_list, "vouchers" }, { "vat_eskd", test_vat_eskd, "vouchers" }, @@ -5120,16 +5779,20 @@ static const struct ttest TESTS[] = { { "attachments", test_attachments, "vouchers idempotency" }, { "templates", test_templates, "org_members" }, { "ib", test_ib, "org_members" }, + { "ib_carry", test_ib_carry, "org_members" }, { "period_lock", test_period_lock, "fiscal_years" }, { "sie_export", test_sie_export, "ib" }, { "sie_import", test_sie_import, "attachments sie_export" }, { "settings", test_settings, "org_members" }, + { "series", test_series, "settings" }, { "smtp_settings", test_smtp_settings, "org_members" }, { "bank", test_bank, "settings" }, { "invoices", test_invoices, "org_members" }, + { "invoice_extras", test_invoice_extras, "invoices" }, { "invoice_send", test_invoice_send, "invoices" }, { "moms_rules", test_moms_rules, "org_members" }, { "rules_editor", test_rules_editor, "moms_rules" }, + { "vat_settlement", test_vat_settlement, "org_members" }, { "bokslut", test_bokslut, "moms_rules" }, { "sru", test_sru, "bokslut" }, { "year_close", test_year_close, "fiscal_years" }, |
