aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/invoice_check.c72
-rw-r--r--tests/pdf_check.c9
-rw-r--r--tests/test_core.c688
-rw-r--r--tests/test_tui.c95
4 files changed, 853 insertions, 11 deletions
diff --git a/tests/invoice_check.c b/tests/invoice_check.c
index 46b1621..e61a41d 100644
--- a/tests/invoice_check.c
+++ b/tests/invoice_check.c
@@ -29,8 +29,8 @@ static void eq64(const char *what, int64_t got, int64_t want)
static const struct invoice_line synthetic_lines[] = {
{ "", "Utvecklingsarbete\nPeriod 2026-01-01 tom 2026-01-31", 61000, "tim",
- 120000, 7320000, "", "25" },
- { "A-1", "Konsult", 12500, "tim", 80000, 1000000, "Omvänd moms", "rc" },
+ 120000, 7320000, "", "25", 0 },
+ { "A-1", "Konsult", 12500, "tim", 80000, 1000000, "Omvänd moms", "rc", 0 },
};
static void make_doc(struct invoice_doc *d)
@@ -65,12 +65,12 @@ static void make_doc(struct invoice_doc *d)
static void test_totals(void)
{
static const struct invoice_line lines[] = {
- { "", "a", 1000, "st", 1000, 100000, "", "25" },
- { "", "b", 1000, "st", 1000, 50000, "", "12" },
- { "", "c", 1000, "st", 1000, 10000, "", "6" },
- { "", "d", 1000, "st", 1000, 25000, "", "rc" },
- { "", "e", 1000, "st", 1000, 15000, "", "0" },
- { "", "f", 1000, "st", 1000, 4000, "", "eu" },
+ { "", "a", 1000, "st", 1000, 100000, "", "25", 0 },
+ { "", "b", 1000, "st", 1000, 50000, "", "12", 0 },
+ { "", "c", 1000, "st", 1000, 10000, "", "6", 0 },
+ { "", "d", 1000, "st", 1000, 25000, "", "rc", 0 },
+ { "", "e", 1000, "st", 1000, 15000, "", "0", 0 },
+ { "", "f", 1000, "st", 1000, 4000, "", "eu", 0 },
};
struct invoice_doc d;
struct invoice_totals t;
@@ -89,7 +89,7 @@ static void test_totals(void)
{
static const struct invoice_line half[] = {
- { "", "half", 1000, "st", 100, 50, "", "25" },
+ { "", "half", 1000, "st", 100, 50, "", "25", 0 },
};
d.lines = half;
@@ -129,6 +129,19 @@ static int contains(const unsigned char *data, size_t len, const char *needle)
return 0;
}
+static int count_of(const unsigned char *data, size_t len, const char *needle)
+{
+ size_t n = strlen(needle);
+ int count = 0;
+
+ if (!n)
+ return 0;
+ for (size_t i = 0; i + n <= len; i++)
+ if (memcmp(data + i, needle, n) == 0)
+ count++;
+ return count;
+}
+
static void test_render(const char *path)
{
struct invoice_doc d;
@@ -147,6 +160,8 @@ static void test_render(const char *path)
if (data) {
check(memcmp(data, "%PDF-1.4", 8) == 0, "render header");
check(contains(data, len, "Testleverant"), "render seller text");
+ check(count_of(data, len, "Testleverant") == 2,
+ "render seller name in header and footer");
check(contains(data, len, "Testkund AB"), "render customer text");
check(contains(data, len, "Summa att betala SEK"), "render summary");
check(contains(data, len, "Test Person"), "render our ref");
@@ -166,6 +181,45 @@ static void test_render(const char *path)
free(data);
{
+ unsigned char *colored = NULL;
+ size_t colored_len = 0;
+
+ d.header_color = "#ff0000";
+ check(invoice_render_pdf(&d, &colored, &colored_len) == 0,
+ "render custom colour returns 0");
+ if (colored)
+ check(contains(colored, colored_len, "1 0 0 rg"),
+ "render custom header colour");
+ free(colored);
+
+ colored = NULL;
+ d.header_color = "not-a-colour";
+ check(invoice_render_pdf(&d, &colored, &colored_len) == 0,
+ "render invalid colour returns 0");
+ if (colored)
+ check(!contains(colored, colored_len, "1 0 0 rg"),
+ "render invalid colour falls back");
+ free(colored);
+ d.header_color = NULL;
+ }
+ {
+ static char long_name[240];
+ unsigned char *long_pdf = NULL;
+ size_t long_len = 0;
+
+ memset(long_name, 'A', sizeof long_name - 1);
+ long_name[sizeof long_name - 1] = '\0';
+ d.seller.name = long_name;
+ check(invoice_render_pdf(&d, &long_pdf, &long_len) == 0,
+ "render long name returns 0");
+ if (long_pdf)
+ check(contains(long_pdf, long_len, "..."),
+ "render long name is truncated");
+ free(long_pdf);
+ d.seller.name = "Testleverantör AB";
+ }
+
+ {
unsigned char *bad = NULL;
size_t n = 0;
diff --git a/tests/pdf_check.c b/tests/pdf_check.c
index 4d56cab..4eaf24d 100644
--- a/tests/pdf_check.c
+++ b/tests/pdf_check.c
@@ -82,6 +82,12 @@ int main(int argc, char **argv)
pdf_line(p, 0, 80, 595, 80, 0.5, "#314c59");
pdf_line(p, 20, 100, 575, 100, 0.25, "#314c59");
+ pdf_text_fit(p, 20, 220, "HB", 16, 8, 200, "#314c59", "Fitted");
+ pdf_text_fit(p, 20, 240, "H", 10, 8, 40, "#314c59",
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
+ pdf_text_fit(p, 20, 260, "H", 10, 8, 40, "#314c59", NULL);
+ pdf_text_fit(p, 20, 260, "H", 10, 8, 40, "#314c59", "");
+
put_text(p, "MAKANDRA", 20, 70, "HB", 16, "#ffffff");
put_text(p, "FAKTURA", 575 - pdf_text_width("H", 16, "FAKTURA"), 70,
"H", 16, "#ffffff");
@@ -123,6 +129,9 @@ int main(int argc, char **argv)
check(contains(data, len, "1 0 obj"), "object 1");
check(contains(data, len, "6 0 obj"), "object 6");
check(!contains(data, len, "\r"), "no CR");
+ check(contains(data, len, "Fitted"), "text_fit keeps short text");
+ check(contains(data, len, "(AAAAAA...)"),
+ "text_fit truncates long text");
if (argc > 1) {
FILE *f = fopen(argv[1], "wb");
diff --git a/tests/test_core.c b/tests/test_core.c
index 12d42e5..c9c969e 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,147 @@ 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);
+ }
+
+ /* an IB adjustment balanced against a P&L account moves only the balance
+ side; the P&L account still opens at zero */
+ d = call(reqf("{\"v\":1,\"id\":\"ibc7a\",\"cmd\":\"voucher.post\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":"
+ "\"%d-01-01\",\"description\":\"IB-justering\","
+ "\"series\":\"IB\",\"rows\":["
+ "{\"account\":\"1930\",\"debit_ore\":700},"
+ "{\"account\":\"3001\",\"credit_ore\":700}]}}",
+ g_session, org, y));
+ CHECK_OK(d);
+ yyjson_doc_free(d);
+ d = call(reqf("{\"v\":1,\"id\":\"ibc7b\",\"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", "1930", "ib_ore") ==
+ 100700);
+ CHECK(find_amount(d, "result.accounts", "account", "3001", "ib_ore") == 0);
+ yyjson_doc_free(d);
+}
+
static void test_period_lock(struct tctx *t)
{
yyjson_doc *d;
@@ -2315,6 +2578,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 +2839,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 +2866,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 +3623,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 +3768,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 +4191,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 +5203,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 +5230,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 +5473,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 +5512,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 +5791,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 +5800,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" },
diff --git a/tests/test_tui.c b/tests/test_tui.c
index 84e610e..aa062fc 100644
--- a/tests/test_tui.c
+++ b/tests/test_tui.c
@@ -1,10 +1,13 @@
/* Unit tests for the TUI widget layer (clients/tui.c). Only the pure parts
are exercised here; drawing is smoke-tested over a pty. */
#include <stdio.h>
+#include <stdlib.h>
#include <string.h>
+#include <unistd.h>
#include <ncursesw/ncurses.h>
+#include "drafts.h"
#include "tui.h"
#include "util.h"
@@ -712,6 +715,96 @@ static void test_rt_footer(void)
CHECK(f[0] == '\0');
}
+static void test_actions(void)
+{
+ char buf[256];
+ struct tui_action acts[] = {
+ { "save", "Spara", KEY_F(9), 1, NULL },
+ { "del", "Radera utkast", 0, 0, "inget utkast" },
+ { "sec", "Sektion", 0, -1, NULL },
+ { "arch", "Arkivera", KEY_F(2), 1, NULL },
+ };
+
+ tui_action_label(&acts[0], buf, sizeof buf);
+ CHECK(strcmp(buf, "Spara") == 0);
+ tui_action_label(&acts[1], buf, sizeof buf);
+ CHECK(strcmp(buf, "Radera utkast (inget utkast)") == 0);
+
+ tui_action_hint(acts, 4, 1, buf, sizeof buf);
+ CHECK(strstr(buf, "F9 = Spara") != NULL);
+ CHECK(strstr(buf, "F2 = fler") != NULL);
+ CHECK(strstr(buf, "Arkivera") == NULL);
+
+ tui_action_hint(acts, 4, 0, buf, sizeof buf);
+ CHECK(strstr(buf, "F9 = Spara") != NULL);
+ CHECK(strstr(buf, "F2 = Arkivera F2 = fler") != NULL);
+ CHECK(strstr(buf, "Radera utkast") == NULL);
+
+ tui_action_hint(NULL, 0, 1, buf, sizeof buf);
+ CHECK(strcmp(buf, "F2 = fler") == 0);
+}
+
+static void test_drafts(void)
+{
+ char path[128];
+ struct drafts d, r;
+
+ snprintf(path, sizeof path, "/tmp/bokf-drafts-test-%d.json",
+ (int)getpid());
+ unlink(path);
+ drafts_init_path(&d, path);
+ CHECK(drafts_count(&d, 1, "customer") == 0);
+ CHECK(drafts_get(&d, 1, "customer", "ny-1") == NULL);
+
+ drafts_put(&d, 1, "customer", "ny-1", "{\"name\":\"A\"}");
+ CHECK(drafts_have(&d, 1, "customer", "ny-1"));
+ CHECK(drafts_count(&d, 1, "customer") == 1);
+ CHECK(strcmp(drafts_get(&d, 1, "customer", "ny-1"),
+ "{\"name\":\"A\"}") == 0);
+ drafts_put(&d, 1, "customer", "ny-1", "{\"name\":\"B\"}");
+ CHECK(strcmp(drafts_get(&d, 1, "customer", "ny-1"),
+ "{\"name\":\"B\"}") == 0);
+ drafts_put(&d, 2, "customer", "7", "{\"name\":\"C\"}");
+ drafts_put(&d, 1, "employee", "3", "{\"name\":\"D\"}");
+ CHECK(drafts_count(&d, 1, "customer") == 1);
+ CHECK(drafts_count(&d, 2, "customer") == 1);
+
+ drafts_init_path(&r, path);
+ CHECK(drafts_count(&r, 1, "customer") == 1);
+ CHECK(strcmp(drafts_get(&r, 1, "customer", "ny-1"),
+ "{\"name\":\"B\"}") == 0);
+ CHECK(strcmp(drafts_get(&r, 2, "customer", "7"),
+ "{\"name\":\"C\"}") == 0);
+ CHECK(strcmp(drafts_get(&r, 1, "employee", "3"),
+ "{\"name\":\"D\"}") == 0);
+ drafts_del(&r, 1, "customer", "ny-1");
+ CHECK(drafts_count(&r, 1, "customer") == 0);
+ drafts_free(&r);
+
+ drafts_init_path(&r, path);
+ CHECK(drafts_count(&r, 1, "customer") == 0);
+ CHECK(drafts_count(&r, 2, "customer") == 1);
+ drafts_free(&r);
+ drafts_free(&d);
+ unlink(path);
+
+ FILE *f = fopen(path, "w");
+ if (f) {
+ fputs("inte json", f);
+ fclose(f);
+ }
+ drafts_init_path(&r, path);
+ CHECK(drafts_count(&r, 1, "customer") == 0);
+ drafts_free(&r);
+ unlink(path);
+
+ char *a = drafts_new_id();
+ char *b = drafts_new_id();
+ CHECK(a && b && strncmp(a, "ny-", 3) == 0 && strcmp(a, b) != 0);
+ free(a);
+ free(b);
+}
+
int main(void)
{
test_disp_width();
@@ -733,6 +826,8 @@ int main(void)
test_rt_fields();
test_rt_focus();
test_rt_footer();
+ test_actions();
+ test_drafts();
printf("test_tui: %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}