aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cmd_bokslut.c9
-rw-r--r--src/cmd_invoices.c181
-rw-r--r--src/cmd_payroll.c12
-rw-r--r--src/cmd_settings.c64
-rw-r--r--src/db.c62
-rw-r--r--src/db.h5
-rw-r--r--src/invoice.c34
-rw-r--r--src/invoice.h2
-rw-r--r--src/ledger.c6
-rw-r--r--src/mail.c23
-rw-r--r--src/mail.h4
-rw-r--r--src/payslip.c62
-rw-r--r--src/payslip.h1
-rw-r--r--src/pdf.c36
-rw-r--r--src/pdf.h6
-rw-r--r--src/reports.c79
-rw-r--r--src/reports.h15
-rw-r--r--src/sie.c31
-rw-r--r--src/util.c13
-rw-r--r--src/util.h1
20 files changed, 539 insertions, 107 deletions
diff --git a/src/cmd_bokslut.c b/src/cmd_bokslut.c
index c813923..f0ddee9 100644
--- a/src/cmd_bokslut.c
+++ b/src/cmd_bokslut.c
@@ -88,6 +88,9 @@ static yyjson_mut_val *h_bokslut_post(struct req *r)
}
int64_t result_before = 0;
+ char ib_series[16];
+ db_setting_copy(r->db, r->org_id, "series_ib", "IB", ib_series,
+ sizeof ib_series);
if (sqlite3_prepare_v2(
r->db,
"SELECT COALESCE(SUM(r.credit_ore)-SUM(r.debit_ore),0)"
@@ -95,11 +98,13 @@ static yyjson_mut_val *h_bokslut_post(struct req *r)
" JOIN vouchers v ON v.org_id=r.org_id AND v.id=r.voucher_id"
" JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
" WHERE r.org_id=?1 AND v.fiscal_year_id=?2 AND v.series<>'IB'"
+ " AND v.series<>?3"
" AND a.type IN ('revenue','expense') AND a.number NOT LIKE '89%'",
-1, &st, NULL) != SQLITE_OK)
return db_error(r);
sqlite3_bind_int64(st, 1, r->org_id);
sqlite3_bind_int64(st, 2, fy_id);
+ sqlite3_bind_text(st, 3, ib_series, -1, SQLITE_TRANSIENT);
if (sqlite3_step(st) == SQLITE_ROW)
result_before = sqlite3_column_int64(st, 0);
sqlite3_finalize(st);
@@ -154,6 +159,9 @@ static yyjson_mut_val *h_bokslut_post(struct req *r)
plan[np++] = (typeof(plan[0])){ "Resultatdisposition", drows, 2 };
}
+ char series[16];
+ db_setting_copy(r->db, r->org_id, "series_bokslut", "Å", series,
+ sizeof series);
yyjson_mut_val *vouchers = yyjson_mut_arr(r->rdoc);
for (size_t i = 0; i < np; i++) {
struct ledger_post_opts o;
@@ -165,6 +173,7 @@ static yyjson_mut_val *h_bokslut_post(struct req *r)
o.description = plan[i].desc;
o.rows = plan[i].rows;
o.nrows = plan[i].nrows;
+ o.series = series;
o.dry_run = r->dry_run;
struct ledger_error e;
char *json = NULL;
diff --git a/src/cmd_invoices.c b/src/cmd_invoices.c
index 0892ac8..a9d3fb1 100644
--- a/src/cmd_invoices.c
+++ b/src/cmd_invoices.c
@@ -11,6 +11,7 @@
#include "db.h"
#include "invoice.h"
#include "ledger.h"
+#include "mail.h"
#include "pdf.h"
#include "secret.h"
#include "smtp.h"
@@ -139,6 +140,7 @@ struct draft_line {
const char *note;
const char *vat_code;
char account[16];
+ int is_text;
};
struct invoice_draft {
@@ -211,6 +213,21 @@ static int draft_line_parse(struct req *r, yyjson_val *item, size_t no,
failf(r, "INVALID_ARGS", "row %zu: description is required", no);
return -1;
}
+ int is_text = 0;
+ arg_bool(item, "text", &is_text);
+ if (is_text) {
+ l->article_no = NULL;
+ l->description = description;
+ l->quantity_milli = 1;
+ l->unit = "";
+ l->unit_price_ore = 0;
+ l->amount_ore = 0;
+ l->note = "";
+ l->vat_code = "0";
+ l->is_text = 1;
+ snprintf(l->account, sizeof l->account, "%s", default_account);
+ return 0;
+ }
const char *qty = arg_str(item, "quantity");
int64_t quantity_milli = 0;
if (parse_quantity(qty, &quantity_milli) != 0) {
@@ -363,6 +380,7 @@ struct invoice_view {
char seller_org_nr[64];
char seller_vat_nr[64];
char bankgiro[64];
+ char header_color[8];
char customer_name[256];
char customer_address[1024];
char customer_postal[64];
@@ -450,6 +468,9 @@ static int invoice_view_fill(struct req *r, const struct invoice_draft *d,
bankgiro && *bankgiro ? bankgiro : "");
free(bankgiro);
+ db_setting_copy(r->db, r->org_id, "document_header_color", "",
+ v->header_color, sizeof v->header_color);
+
v->lines = xcalloc(d->nlines, sizeof *v->lines);
for (size_t i = 0; i < d->nlines; i++) {
v->lines[i].article_no = d->lines[i].article_no;
@@ -460,6 +481,7 @@ static int invoice_view_fill(struct req *r, const struct invoice_draft *d,
v->lines[i].amount_ore = d->lines[i].amount_ore;
v->lines[i].note = d->lines[i].note;
v->lines[i].vat_code = d->lines[i].vat_code;
+ v->lines[i].is_text = d->lines[i].is_text;
}
v->doc.seller.name = v->seller_name;
@@ -471,6 +493,7 @@ static int invoice_view_fill(struct req *r, const struct invoice_draft *d,
v->doc.seller.org_nr = v->seller_org_nr;
v->doc.seller.vat_nr = v->seller_vat_nr;
v->doc.seller.bankgiro = v->bankgiro;
+ v->doc.header_color = v->header_color;
v->doc.customer.name = v->customer_name;
v->doc.customer.address = v->customer_address;
v->doc.customer.postal_code = v->customer_postal;
@@ -670,8 +693,8 @@ static int invoice_store_invoice(struct req *r, const struct invoice_draft *d,
r->db,
"INSERT INTO invoice_rows(org_id,invoice_id,line_no,article_no,"
"description,quantity_milli,unit,unit_price_ore,amount_ore,note,"
- "vat_code,account)"
- " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
+ "vat_code,account,is_text)"
+ " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13)",
-1, &st, NULL) != SQLITE_OK) {
db_error(r);
return -1;
@@ -689,6 +712,7 @@ static int invoice_store_invoice(struct req *r, const struct invoice_draft *d,
sqlite3_bind_text(st, 10, l->note ? l->note : "", -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 11, l->vat_code, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 12, l->account, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_int(st, 13, l->is_text);
rc = sqlite3_step(st);
sqlite3_finalize(st);
if (rc != SQLITE_DONE) {
@@ -821,6 +845,9 @@ static yyjson_mut_val *h_invoice_issue(struct req *r)
invoice_store_attachment(r, &v, pdf, pdf_len, &attachment_id) != 0)
goto done;
+ char series[16];
+ db_setting_copy(r->db, r->org_id, "series_invoice", "F", series,
+ sizeof series);
struct ledger_post_opts o;
memset(&o, 0, sizeof o);
o.org_id = r->org_id;
@@ -831,6 +858,7 @@ static yyjson_mut_val *h_invoice_issue(struct req *r)
o.rows = vrows;
o.nrows = vn;
o.source = "invoice";
+ o.series = series;
o.dry_run = r->dry_run;
o.already_in_tx = 1;
struct ledger_error e;
@@ -888,12 +916,12 @@ static yyjson_mut_val *invoice_row_json(struct req *r, sqlite3_stmt *st)
return db_row_json(r->rdoc, st,
"line_no:i,article_no:s,description:s,"
"quantity_milli:i,unit:s,unit_price_ore:i,"
- "amount_ore:i,note:s,vat_code:s,account:s");
+ "amount_ore:i,note:s,vat_code:s,account:s,is_text:b");
}
#define INVOICE_ROW_COLUMNS \
"line_no,article_no,description,quantity_milli,unit,unit_price_ore," \
- "amount_ore,note,vat_code,account"
+ "amount_ore,note,vat_code,account,is_text"
static yyjson_mut_val *h_invoice_get(struct req *r)
{
@@ -906,9 +934,13 @@ static yyjson_mut_val *h_invoice_get(struct req *r)
"SELECT i.id,i.customer_id,c.name,i.number,i.ocr,i.invoice_date,"
"i.due_date,i.delivery_date,i.your_ref,i.our_ref,i.notes,i.net_ore,"
"i.vat_ore,i.total_ore,i.status,i.document_id,i.voucher_id,"
- "i.last_sent_at,i.last_sent_to,i.created_at,i.created_by"
+ "i.last_sent_at,i.last_sent_to,i.created_at,i.created_by,"
+ "i.paid_date,i.payment_voucher_id,COALESCE(pv.series,''),"
+ "COALESCE(pv.number,0)"
" FROM invoices i JOIN customers c"
" ON c.org_id=i.org_id AND c.id=i.customer_id"
+ " LEFT JOIN vouchers pv"
+ " ON pv.org_id=i.org_id AND pv.id=i.payment_voucher_id"
" WHERE i.org_id=?1 AND i.id=?2",
-1, &st, NULL) != SQLITE_OK)
return db_error(r);
@@ -971,6 +1003,17 @@ static yyjson_mut_val *h_invoice_get(struct req *r)
sq(sqlite3_column_text(st, 19)));
yyjson_mut_obj_add_int(r->rdoc, o, "created_by",
sqlite3_column_int64(st, 20));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "paid_date",
+ sq(sqlite3_column_text(st, 21)));
+ if (sqlite3_column_type(st, 22) == SQLITE_NULL)
+ yyjson_mut_obj_add_null(r->rdoc, o, "payment_voucher_id");
+ else
+ yyjson_mut_obj_add_int(r->rdoc, o, "payment_voucher_id",
+ sqlite3_column_int64(st, 22));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "paid_voucher_series",
+ sq(sqlite3_column_text(st, 23)));
+ yyjson_mut_obj_add_int(r->rdoc, o, "paid_voucher_number",
+ sqlite3_column_int64(st, 24));
sqlite3_finalize(st);
yyjson_mut_val *rows = yyjson_mut_arr(r->rdoc);
@@ -1006,7 +1049,7 @@ static yyjson_mut_val *h_invoice_list(struct req *r)
r->db,
"SELECT i.id,i.number,i.ocr,i.customer_id,c.name,i.invoice_date,"
"i.due_date,i.total_ore,i.status,i.document_id,i.voucher_id,"
- "i.last_sent_at,i.last_sent_to"
+ "i.last_sent_at,i.last_sent_to,i.paid_date"
" FROM invoices i JOIN customers c"
" ON c.org_id=i.org_id AND c.id=i.customer_id"
" WHERE i.org_id=?1"
@@ -1059,6 +1102,8 @@ static yyjson_mut_val *h_invoice_list(struct req *r)
else
yyjson_mut_obj_add_strcpy(r->rdoc, o, "last_sent_to",
sq(sqlite3_column_text(st, 12)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "paid_date",
+ sq(sqlite3_column_text(st, 13)));
}
sqlite3_finalize(st);
yyjson_mut_val *out = yyjson_mut_obj(r->rdoc);
@@ -1211,6 +1256,10 @@ static yyjson_mut_val *h_invoice_send(struct req *r)
fail(r, "SMTP_NOT_CONFIGURED", "smtp_host and smtp_from must be set");
goto done;
}
+ if (!mail_addr_valid(smtp_from)) {
+ fail(r, "SMTP_NOT_CONFIGURED", "smtp_from must be an email address");
+ goto done;
+ }
const char *user = smtp_user && *smtp_user ? smtp_user : "";
if (*user) {
if (!smtp_password || !*smtp_password) {
@@ -1350,6 +1399,122 @@ static const struct cmd_arg args_invoice_sequence_set[] = {
{ "next_number", ARG_INT, 1, NULL, NULL, "Next invoice number" },
};
+static const struct cmd_arg args_invoice_pay[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Invoice id" },
+ { "voucher_id", ARG_INT, 1, NULL, NULL, "Payment voucher id" },
+};
+
+/* Marks an invoice paid and links the voucher that settles it. The voucher
+ must credit the invoice receivable account with the invoice total. */
+static yyjson_mut_val *h_invoice_pay(struct req *r)
+{
+ int64_t id = 0, voucher_id = 0;
+ if (!arg_int(r->args, "id", &id) || id <= 0)
+ return fail(r, "INVALID_ARGS", "id is required");
+ if (!arg_int(r->args, "voucher_id", &voucher_id) || voucher_id <= 0)
+ return fail(r, "INVALID_ARGS", "voucher_id is required");
+
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "SELECT number,total_ore,status,paid_date FROM invoices"
+ " WHERE org_id=?1 AND id=?2",
+ -1, &st, NULL) != SQLITE_OK)
+ return db_error(r);
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, id);
+ if (sqlite3_step(st) != SQLITE_ROW) {
+ sqlite3_finalize(st);
+ return fail(r, "NOT_FOUND", "invoice not found");
+ }
+ int64_t number = sqlite3_column_int64(st, 0);
+ int64_t total = sqlite3_column_int64(st, 1);
+ char status[16], paid[16];
+ snprintf(status, sizeof status, "%s", sq(sqlite3_column_text(st, 2)));
+ snprintf(paid, sizeof paid, "%s", sq(sqlite3_column_text(st, 3)));
+ sqlite3_finalize(st);
+ if (*paid)
+ return fail(r, "CONFLICT", "invoice is already paid");
+ if (strcmp(status, "credited") == 0)
+ return fail(r, "INVALID_ARGS", "a credited invoice cannot be paid");
+
+ char receivable[16];
+ db_setting_copy(r->db, r->org_id, "invoice_receivable_account", "1510",
+ receivable, sizeof receivable);
+
+ char date[16], series[16] = "";
+ int64_t vnumber = 0;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "SELECT date,series,number FROM vouchers WHERE org_id=?1 AND id=?2",
+ -1, &st, NULL) != SQLITE_OK)
+ return db_error(r);
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, voucher_id);
+ if (sqlite3_step(st) != SQLITE_ROW) {
+ sqlite3_finalize(st);
+ return fail(r, "NOT_FOUND", "voucher not found");
+ }
+ snprintf(date, sizeof date, "%s", sq(sqlite3_column_text(st, 0)));
+ snprintf(series, sizeof series, "%s", sq(sqlite3_column_text(st, 1)));
+ vnumber = sqlite3_column_int64(st, 2);
+ sqlite3_finalize(st);
+
+ if (sqlite3_prepare_v2(
+ r->db,
+ "SELECT COALESCE(SUM(r.credit_ore),0) FROM voucher_rows r"
+ " JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
+ " WHERE r.org_id=?1 AND r.voucher_id=?2 AND a.number=?3",
+ -1, &st, NULL) != SQLITE_OK)
+ return db_error(r);
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, voucher_id);
+ sqlite3_bind_text(st, 3, receivable, -1, SQLITE_TRANSIENT);
+ int64_t credited = 0;
+ if (sqlite3_step(st) == SQLITE_ROW)
+ credited = sqlite3_column_int64(st, 0);
+ sqlite3_finalize(st);
+ if (credited != total)
+ return failf(r, "INVALID_ARGS",
+ "the payment voucher must credit %s with the invoice"
+ " total (%lld), not (%lld)",
+ receivable, (long long)total, (long long)credited);
+
+ if (!r->dry_run) {
+ if (sqlite3_prepare_v2(
+ r->db,
+ "UPDATE invoices SET paid_date=?3,payment_voucher_id=?4"
+ " WHERE org_id=?1 AND id=?2 AND paid_date=''",
+ -1, &st, NULL) != SQLITE_OK)
+ return db_error(r);
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, id);
+ sqlite3_bind_text(st, 3, date, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_int64(st, 4, voucher_id);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE)
+ return db_sqlite_error(r);
+ if (sqlite3_changes(r->db) == 0)
+ return fail(r, "CONFLICT", "invoice is already paid");
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "invoice.pay", reqjson, "OK", NULL);
+ free(reqjson);
+ }
+
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_int(r->rdoc, o, "id", id);
+ yyjson_mut_obj_add_int(r->rdoc, o, "number", number);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "paid_date", date);
+ yyjson_mut_obj_add_int(r->rdoc, o, "payment_voucher_id", voucher_id);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "voucher_series", series);
+ yyjson_mut_obj_add_int(r->rdoc, o, "voucher_number", vnumber);
+ if (r->dry_run)
+ yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
+ return o;
+}
+
static const struct cmd_arg args_invoice_draft[] = {
{ "customer_id", ARG_INT, 1, NULL, NULL, "Customer id" },
{ "invoice_date", ARG_DATE, 1, NULL, NULL, "Invoice date (YYYY-MM-DD)" },
@@ -1360,7 +1525,7 @@ static const struct cmd_arg args_invoice_draft[] = {
{ "notes", ARG_STR, 0, NULL, NULL, "Free-text notes" },
{ "rows", ARG_JSON, 1, NULL, NULL,
"Array of {article_no,description,quantity,unit,unit_price_ore,note,"
- "vat_code,account}" },
+ "vat_code,account,text}; text rows carry only the description" },
};
static const struct cmd_arg args_invoice_get[] = {
@@ -1397,6 +1562,8 @@ const struct command g_cmd_invoices[] = {
h_invoice_pdf, CMD_ARGS(args_invoice_get) },
{ "invoice.send", "E-mail the stored invoice PDF to the customer",
PERM_WRITE, 1, 1, 1, h_invoice_send, CMD_ARGS(args_invoice_send) },
+ { "invoice.pay", "Mark an invoice paid with a payment voucher",
+ PERM_WRITE, 1, 1, 1, h_invoice_pay, CMD_ARGS(args_invoice_pay) },
};
const struct cmd_table g_cmd_table_invoices = {
diff --git a/src/cmd_payroll.c b/src/cmd_payroll.c
index b5db4f1..6dea293 100644
--- a/src/cmd_payroll.c
+++ b/src/cmd_payroll.c
@@ -491,6 +491,9 @@ static yyjson_mut_val *h_payroll_run_post(struct req *r)
char description[64];
snprintf(description, sizeof description, "Lönekörning %s", period);
+ char series[16];
+ db_setting_copy(r->db, r->org_id, "series_payroll", "L", series,
+ sizeof series);
struct ledger_post_opts o;
memset(&o, 0, sizeof o);
o.org_id = r->org_id;
@@ -501,6 +504,7 @@ static yyjson_mut_val *h_payroll_run_post(struct req *r)
o.rows = vrows;
o.nrows = vn;
o.source = "payroll";
+ o.series = series;
o.dry_run = r->dry_run;
o.already_in_tx = 1;
struct ledger_error e;
@@ -758,6 +762,7 @@ struct payslip_ctx {
char employer_org_nr[64];
char employer_phone[64];
char employer_email[256];
+ char header_color[8];
char filename[320];
int64_t voucher_id;
int64_t employee_id;
@@ -920,6 +925,8 @@ static int payslip_prepare(struct req *r, struct payslip_ctx *c)
struct payroll_cfg cfg;
payroll_cfg_load(r, &cfg);
snprintf(c->run_ref, sizeof c->run_ref, "Lönekörning %s", c->period);
+ db_setting_copy(r->db, r->org_id, "document_header_color", "",
+ c->header_color, sizeof c->header_color);
char safe_name[200];
snprintf(safe_name, sizeof safe_name, "%s", c->employee_name);
@@ -938,6 +945,7 @@ static int payslip_prepare(struct req *r, struct payslip_ctx *c)
c->d.employer.email = c->employer_email;
c->d.employee_name = c->employee_name;
c->d.personal_no_masked = c->personal_no;
+ c->d.header_color = c->header_color;
c->d.period = c->period;
c->d.pay_date = c->pay_date;
c->d.run_ref = c->run_ref;
@@ -1341,6 +1349,9 @@ static yyjson_mut_val *h_payroll_pay_tax(struct req *r)
if (db_exec(r->db, "BEGIN IMMEDIATE", NULL) != 0)
return fail(r, "DB_BUSY", "could not start transaction");
in_tx = 1;
+ char series[16];
+ db_setting_copy(r->db, r->org_id, "series_payroll", "L", series,
+ sizeof series);
struct ledger_post_opts o;
memset(&o, 0, sizeof o);
o.org_id = r->org_id;
@@ -1351,6 +1362,7 @@ static yyjson_mut_val *h_payroll_pay_tax(struct req *r)
o.rows = rows;
o.nrows = vn;
o.source = "payroll_tax";
+ o.series = series;
o.dry_run = r->dry_run;
o.already_in_tx = 1;
if (ledger_post(r->db, &o, &e, &voucher_json) != 0) {
diff --git a/src/cmd_settings.c b/src/cmd_settings.c
index 066dc55..5bac3d9 100644
--- a/src/cmd_settings.c
+++ b/src/cmd_settings.c
@@ -8,6 +8,7 @@
#include "audit.h"
#include "config.h"
#include "db.h"
+#include "mail.h"
#include "secret.h"
#include "util.h"
@@ -20,6 +21,9 @@ static yyjson_mut_val *h_settings_get(struct req *r)
yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
int have_default = 0, have_bank = 0, have_receivable = 0, have_revenue = 0;
int have_password = 0, have_security = 0;
+ int have_voucher = 0, have_invoice = 0, have_payroll = 0;
+ int have_bokslut = 0, have_ib = 0, have_header_color = 0;
+ char legacy_series[16] = "A";
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(
r->db, "SELECT key,value FROM settings WHERE org_id=?1", -1, &st,
@@ -35,22 +39,48 @@ static yyjson_mut_val *h_settings_get(struct req *r)
}
yyjson_mut_obj_add(o, yyjson_mut_strcpy(r->rdoc, k),
yyjson_mut_strcpy(r->rdoc, v ? v : ""));
- if (strcmp(k, "default_series") == 0)
+ if (strcmp(k, "default_series") == 0) {
have_default = 1;
- if (strcmp(k, "bank_account") == 0)
+ if (v && *v)
+ snprintf(legacy_series, sizeof legacy_series, "%s", v);
+ } else if (strcmp(k, "series_voucher") == 0) {
+ have_voucher = 1;
+ } else if (strcmp(k, "series_invoice") == 0) {
+ have_invoice = 1;
+ } else if (strcmp(k, "series_payroll") == 0) {
+ have_payroll = 1;
+ } else if (strcmp(k, "series_bokslut") == 0) {
+ have_bokslut = 1;
+ } else if (strcmp(k, "series_ib") == 0) {
+ have_ib = 1;
+ } else if (strcmp(k, "bank_account") == 0) {
have_bank = 1;
- if (strcmp(k, "invoice_receivable_account") == 0)
+ } else if (strcmp(k, "invoice_receivable_account") == 0) {
have_receivable = 1;
- if (strcmp(k, "invoice_revenue_account") == 0)
+ } else if (strcmp(k, "invoice_revenue_account") == 0) {
have_revenue = 1;
- if (strcmp(k, "smtp_security") == 0)
+ } else if (strcmp(k, "document_header_color") == 0) {
+ have_header_color = 1;
+ } else if (strcmp(k, "smtp_security") == 0) {
have_security = 1;
+ }
}
}
sqlite3_finalize(st);
}
if (!have_default)
yyjson_mut_obj_add_strcpy(r->rdoc, o, "default_series", "A");
+ if (!have_voucher)
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "series_voucher",
+ legacy_series);
+ if (!have_invoice)
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "series_invoice", "F");
+ if (!have_payroll)
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "series_payroll", "L");
+ if (!have_bokslut)
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "series_bokslut", "Å");
+ if (!have_ib)
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "series_ib", "IB");
if (!have_bank)
yyjson_mut_obj_add_strcpy(r->rdoc, o, "bank_account", "1930");
if (!have_receivable)
@@ -59,6 +89,9 @@ static yyjson_mut_val *h_settings_get(struct req *r)
if (!have_revenue)
yyjson_mut_obj_add_strcpy(r->rdoc, o, "invoice_revenue_account",
"3001");
+ if (!have_header_color)
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "document_header_color",
+ "#314c59");
if (!have_security)
yyjson_mut_obj_add_strcpy(r->rdoc, o, "smtp_security", "starttls");
yyjson_mut_obj_add_bool(r->rdoc, o, "smtp_password_set", have_password);
@@ -134,8 +167,13 @@ static yyjson_mut_val *h_settings_set(struct req *r)
return settings_result(r, key, "[redacted]", 0);
}
size_t maxlen;
- int digits_only = 0, bankgiro = 0, port = 0, security = 0;
- if (strcmp(key, "default_series") == 0)
+ int digits_only = 0, bankgiro = 0, port = 0, security = 0, color = 0;
+ if (strcmp(key, "default_series") == 0 ||
+ strcmp(key, "series_voucher") == 0 ||
+ strcmp(key, "series_invoice") == 0 ||
+ strcmp(key, "series_payroll") == 0 ||
+ strcmp(key, "series_bokslut") == 0 ||
+ strcmp(key, "series_ib") == 0)
maxlen = 8;
else if (strcmp(key, "attachment_dir") == 0 ||
strcmp(key, "smtp_host") == 0 ||
@@ -160,6 +198,9 @@ static yyjson_mut_val *h_settings_set(struct req *r)
bankgiro = 1;
} else if (strcmp(key, "invoice_our_ref") == 0) {
maxlen = 64;
+ } else if (strcmp(key, "document_header_color") == 0) {
+ maxlen = 7;
+ color = 1;
} else
return fail(r, "UNSUPPORTED", "unknown setting");
size_t len = strlen(value);
@@ -176,6 +217,8 @@ static yyjson_mut_val *h_settings_set(struct req *r)
return failf(r, "INVALID_ARGS",
"%s must not contain control characters", key);
}
+ if (color && !util_hex_color_valid(value))
+ return failf(r, "INVALID_ARGS", "%s must be #rrggbb", key);
if (port) {
long v = strtol(value, NULL, 10);
if (v < 1 || v > 65535)
@@ -185,6 +228,10 @@ static yyjson_mut_val *h_settings_set(struct req *r)
strcmp(value, "tls") != 0 && strcmp(value, "plain") != 0)
return failf(r, "INVALID_ARGS", "%s must be starttls, tls or plain",
key);
+ if ((strcmp(key, "smtp_from") == 0 ||
+ strcmp(key, "smtp_reply_to") == 0) &&
+ !mail_addr_valid(value))
+ return failf(r, "INVALID_ARGS", "%s must be an email address", key);
if (r->dry_run)
return settings_result(r, key, value, 1);
sqlite3_stmt *st = NULL;
@@ -215,7 +262,8 @@ static yyjson_mut_val *h_settings_set(struct req *r)
static const struct cmd_arg args_settings_set[] = {
{ "key", ARG_STR, 1, NULL, NULL,
"default_series, attachment_dir, bank_account,"
- " invoice_receivable_account, invoice_revenue_account, smtp_host,"
+ " invoice_receivable_account, invoice_revenue_account,"
+ " invoice_bankgiro, invoice_our_ref, document_header_color, smtp_host,"
" smtp_port, smtp_user, smtp_from, smtp_reply_to, smtp_security or"
" smtp_password" },
{ "value", ARG_STR, 0, NULL, NULL,
diff --git a/src/db.c b/src/db.c
index 95ecf3f..200fff8 100644
--- a/src/db.c
+++ b/src/db.c
@@ -351,6 +351,8 @@ static const char SCHEMA_V1[] =
" CHECK (status IN ('issued','credited')),"
" document_id INTEGER,"
" voucher_id INTEGER,"
+ " paid_date TEXT NOT NULL DEFAULT '',"
+ " payment_voucher_id INTEGER,"
" last_sent_at TEXT,"
" last_sent_to TEXT,"
" created_at TEXT NOT NULL,"
@@ -359,7 +361,9 @@ static const char SCHEMA_V1[] =
" UNIQUE (org_id, number),"
" FOREIGN KEY (org_id, customer_id) REFERENCES customers(org_id, id),"
" FOREIGN KEY (org_id, document_id) REFERENCES attachments(org_id, id),"
- " FOREIGN KEY (org_id, voucher_id) REFERENCES vouchers(org_id, id)"
+ " FOREIGN KEY (org_id, voucher_id) REFERENCES vouchers(org_id, id),"
+ " FOREIGN KEY (org_id, payment_voucher_id)"
+ " REFERENCES vouchers(org_id, id)"
") STRICT;\n"
"CREATE TABLE invoice_rows ("
@@ -377,6 +381,7 @@ static const char SCHEMA_V1[] =
" vat_code TEXT NOT NULL DEFAULT '25'"
" CHECK (vat_code IN ('25','12','6','0','rc','eu')),"
" account TEXT NOT NULL DEFAULT '',"
+ " is_text INTEGER NOT NULL DEFAULT 0,"
" UNIQUE (org_id, id),"
" UNIQUE (org_id, invoice_id, line_no),"
" FOREIGN KEY (org_id, invoice_id) REFERENCES invoices(org_id, id)"
@@ -911,6 +916,49 @@ static int db_upgrade_v11(sqlite3 *db, char **err)
err);
}
+static int db_column_exists(sqlite3 *db, const char *table, const char *col,
+ char **err)
+{
+ sqlite3_stmt *st = NULL;
+ char sql[160];
+ snprintf(sql, sizeof sql,
+ "SELECT count(*) FROM pragma_table_info('%s') WHERE name='%s'",
+ table, col);
+ if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
+ set_err(err, "database error");
+ return -1;
+ }
+ int have = sqlite3_step(st) == SQLITE_ROW && sqlite3_column_int(st, 0) > 0;
+ sqlite3_finalize(st);
+ return have;
+}
+
+static int db_add_column(sqlite3 *db, const char *table, const char *col,
+ const char *decl, char **err)
+{
+ int have = db_column_exists(db, table, col, err);
+ if (have != 0)
+ return have < 0 ? -1 : 0;
+ char sql[384];
+ snprintf(sql, sizeof sql, "ALTER TABLE %s ADD COLUMN %s %s", table, col,
+ decl);
+ return db_exec(db, sql, err);
+}
+
+/* v12: invoice text rows (is_text) and the payment link (paid_date,
+ payment_voucher_id). Fresh databases already carry the columns. */
+static int db_upgrade_v12(sqlite3 *db, char **err)
+{
+ if (db_add_column(db, "invoice_rows", "is_text",
+ "INTEGER NOT NULL DEFAULT 0", err) != 0)
+ return -1;
+ if (db_add_column(db, "invoices", "paid_date",
+ "TEXT NOT NULL DEFAULT ''", err) != 0)
+ return -1;
+ return db_add_column(db, "invoices", "payment_voucher_id", "INTEGER",
+ err);
+}
+
static int db_upgrade(sqlite3 *db, int from, char **err)
{
if (db_exec(db, "BEGIN IMMEDIATE", err) != 0)
@@ -955,6 +1003,10 @@ static int db_upgrade(sqlite3 *db, int from, char **err)
db_exec(db, "ROLLBACK", NULL);
return -1;
}
+ if (from < 12 && db_upgrade_v12(db, err) != 0) {
+ db_exec(db, "ROLLBACK", NULL);
+ return -1;
+ }
char *sql = sqlite3_mprintf(
"UPDATE meta SET value='%d' WHERE key='schema_version'",
BOKF_SCHEMA_VERSION);
@@ -1215,6 +1267,14 @@ char *db_setting(sqlite3 *db, int64_t org_id, const char *key)
return value;
}
+void db_setting_copy(sqlite3 *db, int64_t org_id, const char *key,
+ const char *def, char *out, size_t n)
+{
+ char *v = db_setting(db, org_id, key);
+ snprintf(out, n, "%s", v && *v ? v : def);
+ free(v);
+}
+
char *db_membership_role(sqlite3 *db, int64_t org_id, int64_t user_id)
{
sqlite3_stmt *st = NULL;
diff --git a/src/db.h b/src/db.h
index 4e16f62..b541f66 100644
--- a/src/db.h
+++ b/src/db.h
@@ -2,9 +2,10 @@
#define BOKF_DB_H
#include <sqlite3.h>
+#include <stddef.h>
#include <stdint.h>
-#define BOKF_SCHEMA_VERSION 11
+#define BOKF_SCHEMA_VERSION 12
int db_open(const char *path, sqlite3 **out, char **err);
int db_migrate(sqlite3 *db, char **err);
@@ -14,6 +15,8 @@ int db_create_user(sqlite3 *db, const char *username, const char *display_name,
char **err);
char *db_membership_role(sqlite3 *db, int64_t org_id, int64_t user_id);
char *db_setting(sqlite3 *db, int64_t org_id, const char *key);
+void db_setting_copy(sqlite3 *db, int64_t org_id, const char *key,
+ const char *def, char *out, size_t n);
int64_t db_count(sqlite3 *db, const char *sql);
int64_t db_last_id(sqlite3 *db);
diff --git a/src/invoice.c b/src/invoice.c
index 622c05a..1bdc0da 100644
--- a/src/invoice.c
+++ b/src/invoice.c
@@ -77,8 +77,12 @@
#define FOOT_CITY_Y 686.0238
#define FOOT_PAGE_Y 819.7954
-#define WORDMARK_X 21.742
+#define HEADER_NAME_X 21.742
#define FAKTURA_X 498.400
+#define HEADER_BASE_Y 69.1358
+#define HEADER_NAME_SIZE 16.0
+#define HEADER_NAME_MIN_SIZE 8.0
+#define HEADER_NAME_MAX_W 460.0
static void text_at(struct pdf *p, double x, double base, const char *font,
double size, const char *rgb, const char *s)
@@ -430,6 +434,24 @@ static void draw_table(struct pdf *p, const struct invoice_doc *d)
const char *s = l->description ? l->description : "";
size_t line = 0;
+ if (l->is_text) {
+ for (;;) {
+ const char *nl = strchr(s, '\n');
+
+ if (nl) {
+ put_desc_line(p, y + desc_offset(line), s,
+ (size_t)(nl - s));
+ s = nl + 1;
+ line++;
+ } else {
+ put_desc_line(p, y + desc_offset(line), s, strlen(s));
+ break;
+ }
+ }
+ y += (double)(line + 2) * ROW_STEP;
+ continue;
+ }
+
fmt_quantity(l->quantity_milli, qty, sizeof qty);
fmt_kronor(l->unit_price_ore, price, sizeof price, 0, 0);
fmt_kronor(l->amount_ore, amount, sizeof amount, 0, 0);
@@ -563,9 +585,13 @@ int invoice_render_pdf(const struct invoice_doc *d, unsigned char **out,
p = pdf_new();
pdf_page(p);
pdf_fill_rect(p, PAGE_LEFT, BAR_HEADER_Y, PAGE_RIGHT_X - PAGE_LEFT,
- BAR_HEADER_H, INK);
- draw_wordmark(p, WORDMARK_MAKANDRA, WORDMARK_X, 69.1358);
- draw_wordmark(p, WORDMARK_FAKTURA, FAKTURA_X, 69.1358);
+ BAR_HEADER_H,
+ util_hex_color_valid(d->header_color) ? d->header_color
+ : INK);
+ pdf_text_fit(p, HEADER_NAME_X, HEADER_BASE_Y, "HB", HEADER_NAME_SIZE,
+ HEADER_NAME_MIN_SIZE, HEADER_NAME_MAX_W, WHITE,
+ d->seller.name);
+ draw_wordmark(p, WORDMARK_FAKTURA, FAKTURA_X, HEADER_BASE_Y);
draw_info(p, d);
draw_customer(p, &d->customer);
draw_table(p, d);
diff --git a/src/invoice.h b/src/invoice.h
index c92677d..55d1e3b 100644
--- a/src/invoice.h
+++ b/src/invoice.h
@@ -33,6 +33,7 @@ struct invoice_line {
int64_t amount_ore;
const char *note;
const char *vat_code;
+ int is_text;
};
struct invoice_doc {
@@ -40,6 +41,7 @@ struct invoice_doc {
struct invoice_customer customer;
int64_t number;
const char *ocr;
+ const char *header_color; /* "#rrggbb"; NULL or invalid = default */
const char *invoice_date;
const char *due_date;
const char *delivery_date;
diff --git a/src/ledger.c b/src/ledger.c
index a22e15e..b9aad47 100644
--- a/src/ledger.c
+++ b/src/ledger.c
@@ -360,7 +360,11 @@ int ledger_post(sqlite3 *db, const struct ledger_post_opts *o,
char *series_owned = NULL;
const char *series = o->series;
if (!series || !*series) {
- series_owned = db_setting(db, o->org_id, "default_series");
+ series_owned = db_setting(db, o->org_id, "series_voucher");
+ if (!series_owned || !*series_owned) {
+ free(series_owned);
+ series_owned = db_setting(db, o->org_id, "default_series");
+ }
series = series_owned && *series_owned ? series_owned : "A";
}
if (!util_parse_iso_date(o->date)) {
diff --git a/src/mail.c b/src/mail.c
index ca35f6d..e1b2131 100644
--- a/src/mail.c
+++ b/src/mail.c
@@ -38,6 +38,21 @@ static void set_err(char *err, size_t errlen, const char *msg)
snprintf(err, errlen, "%s", msg);
}
+int mail_addr_valid(const char *addr)
+{
+ if (!addr || !*addr)
+ return 0;
+ const char *at = strchr(addr, '@');
+ if (!at || at == addr || !at[1] || strchr(at + 1, '@'))
+ return 0;
+ for (const char *p = addr; *p; p++) {
+ unsigned char ch = (unsigned char)*p;
+ if (ch <= 0x20 || ch == 0x7f || ch == '<' || ch == '>')
+ return 0;
+ }
+ return 1;
+}
+
static int mail_cfg_load(sqlite3 *db, int64_t org_id, struct mail_cfg *c,
char *err, size_t errlen)
{
@@ -54,6 +69,14 @@ static int mail_cfg_load(sqlite3 *db, int64_t org_id, struct mail_cfg *c,
set_err(err, errlen, "smtp_host and smtp_from must be set");
goto fail;
}
+ if (!mail_addr_valid(c->from)) {
+ set_err(err, errlen, "smtp_from must be an email address");
+ goto fail;
+ }
+ if (c->reply_to && *c->reply_to && !mail_addr_valid(c->reply_to)) {
+ set_err(err, errlen, "smtp_reply_to must be an email address");
+ goto fail;
+ }
if (c->user && *c->user) {
if (!c->password || !*c->password) {
set_err(err, errlen,
diff --git a/src/mail.h b/src/mail.h
index 5efade5..2c5da2f 100644
--- a/src/mail.h
+++ b/src/mail.h
@@ -14,6 +14,10 @@ struct mail_message {
size_t attach_len;
};
+/* True for an address the SMTP client can send to: one @, no spaces or
+ control characters, not empty. */
+int mail_addr_valid(const char *addr);
+
/* Checks the org's SMTP settings (including decrypting the stored
smtp_password) without sending. 0 configured, -1 not configured. */
int mail_config_check(sqlite3 *db, int64_t org_id, char *err, size_t errlen);
diff --git a/src/payslip.c b/src/payslip.c
index 81ebded..ee8e008 100644
--- a/src/payslip.c
+++ b/src/payslip.c
@@ -6,7 +6,6 @@
#include "pdf.h"
#include "util.h"
-#include "wordmark.h"
#define INK "#314c59"
#define WHITE "#ffffff"
@@ -52,9 +51,12 @@
#define FOOT_PAGE_Y 819.7954
#define FOOT_PAGE_RIGHT 567.4275
-#define WORDMARK_X 21.742
+#define HEADER_NAME_X 21.742
#define TITLE_RIGHT 574.892
#define TITLE_BASE_Y 69.1358
+#define HEADER_NAME_SIZE 16.0
+#define HEADER_NAME_MIN_SIZE 8.0
+#define HEADER_NAME_MAX_W 460.0
static void text_at(struct pdf *p, double x, double base, const char *font,
double size, const char *rgb, const char *s)
@@ -72,54 +74,6 @@ static void text_right(struct pdf *p, double right, double base,
rgb, s);
}
-/* pdf_path consumes SVG-style y-down paths; wordmark.h stores y-up outline
- coordinates, so negate the y of every point before drawing. */
-static void draw_wordmark(struct pdf *p, const char *path, double x, double y)
-{
- size_t n = strlen(path);
- char *flipped = xmalloc(2 * n + 2);
- char op = 0;
- int num = 0, in_num = 0, y_coord = 0;
- size_t o = 0;
-
- for (size_t i = 0; i < n; i++) {
- char c = path[i];
-
- if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
- if (c == 'M' || c == 'L' || c == 'C') {
- op = c;
- num = 0;
- } else if (c == 'Z') {
- op = 0;
- }
- flipped[o++] = c;
- in_num = 0;
- continue;
- }
- if (!in_num && (c == '-' || c == '+' || c == '.' ||
- (c >= '0' && c <= '9'))) {
- in_num = 1;
- y_coord = op && (num % 2 == 1);
- num++;
- if (y_coord) {
- if (c == '-') {
- i++;
- c = path[i];
- } else {
- flipped[o++] = '-';
- }
- }
- } else if (!(c == '-' || c == '+' || c == '.' ||
- (c >= '0' && c <= '9'))) {
- in_num = 0;
- }
- flipped[o++] = c;
- }
- flipped[o] = '\0';
- pdf_path(p, flipped, x, y, WHITE);
- free(flipped);
-}
-
static void group_digits(char *out, size_t n, uint64_t v)
{
char digits[24];
@@ -284,8 +238,12 @@ int payslip_render(const struct payslip_data *d, unsigned char **out,
p = pdf_new();
pdf_page(p);
pdf_fill_rect(p, PAGE_LEFT, BAR_HEADER_Y, PAGE_RIGHT_X - PAGE_LEFT,
- BAR_HEADER_H, INK);
- draw_wordmark(p, WORDMARK_MAKANDRA, WORDMARK_X, TITLE_BASE_Y);
+ BAR_HEADER_H,
+ util_hex_color_valid(d->header_color) ? d->header_color
+ : INK);
+ pdf_text_fit(p, HEADER_NAME_X, TITLE_BASE_Y, "HB", HEADER_NAME_SIZE,
+ HEADER_NAME_MIN_SIZE, HEADER_NAME_MAX_W, WHITE,
+ d->employer.name);
text_right(p, TITLE_RIGHT, TITLE_BASE_Y, "HB", SIZE_TITLE, WHITE,
"LÖNEBESKED");
pdf_line(p, PAGE_LEFT, RULE_HEADER_Y, PAGE_RIGHT_X, RULE_HEADER_Y, RULE_W,
diff --git a/src/payslip.h b/src/payslip.h
index 291b35d..581cd81 100644
--- a/src/payslip.h
+++ b/src/payslip.h
@@ -16,6 +16,7 @@ struct payslip_data {
} employer;
const char *employee_name;
const char *personal_no_masked;
+ const char *header_color; /* "#rrggbb"; NULL or invalid = default */
const char *period;
const char *pay_date;
int tax_table;
diff --git a/src/pdf.c b/src/pdf.c
index c6ae876..5718ed7 100644
--- a/src/pdf.c
+++ b/src/pdf.c
@@ -353,6 +353,42 @@ double pdf_font_ascent(const char *font, double size)
return size > 0 ? size * PDF_ASCENT / 1000.0 : 0.0;
}
+void pdf_text_fit(struct pdf *p, double x, double baseline, const char *font,
+ double size, double min_size, double max_w, const char *rgb,
+ const char *utf8)
+{
+ char *truncated = NULL;
+ const char *text = utf8;
+ double w;
+
+ if (!utf8 || !*utf8 || max_w <= 0)
+ return;
+ w = pdf_text_width(font, size, utf8);
+ if (w > max_w) {
+ size = size * max_w / w;
+ if (size < min_size)
+ size = min_size;
+ w = pdf_text_width(font, size, utf8);
+ }
+ if (w > max_w) {
+ size_t n = strlen(utf8);
+
+ truncated = xmalloc(n + 4);
+ memcpy(truncated, utf8, n);
+ while (n > 0) {
+ memcpy(truncated + n, "...", 4);
+ if (pdf_text_width(font, size, truncated) <= max_w)
+ break;
+ n--;
+ while (n > 0 && ((unsigned char)truncated[n] & 0xC0) == 0x80)
+ n--;
+ }
+ text = truncated;
+ }
+ pdf_text(p, x, baseline, font, size, rgb, text);
+ free(truncated);
+}
+
static int parse_num(const char **sp, double *out)
{
const char *s = *sp;
diff --git a/src/pdf.h b/src/pdf.h
index c6f95ac..9ed4595 100644
--- a/src/pdf.h
+++ b/src/pdf.h
@@ -20,6 +20,12 @@ void pdf_text(struct pdf *p, double x, double baseline, const char *font,
double size, const char *rgb, const char *utf8);
double pdf_text_width(const char *font, double size, const char *utf8);
double pdf_font_ascent(const char *font, double size);
+/* Draws utf8 at x/baseline, shrinking the size (down to min_size) to fit
+ max_w; if it still does not fit, truncates at a whole UTF-8 character so
+ the text ends with "...". */
+void pdf_text_fit(struct pdf *p, double x, double baseline, const char *font,
+ double size, double min_size, double max_w, const char *rgb,
+ const char *utf8);
void pdf_path(struct pdf *p, const char *path, double x, double y,
const char *rgb);
unsigned char *pdf_finish(struct pdf *p, size_t *len);
diff --git a/src/reports.c b/src/reports.c
index 1d4efdd..981d02c 100644
--- a/src/reports.c
+++ b/src/reports.c
@@ -4,6 +4,7 @@
#include <stdlib.h>
#include <string.h>
+#include "db.h"
#include "util.h"
struct fy_info {
@@ -57,27 +58,36 @@ static yyjson_mut_val *fy_json(yyjson_mut_doc *doc, const struct fy_info *fy)
return o;
}
-/* One row per account with IB and period movements. IB is the series "IB"
- voucher(s) of this fiscal year plus all non-IB history before `from`.
- Amounts signed: debit positive. */
+/* One row per account with IB and period movements; IB as defined by
+ REPORT_IB_ROW_SQL. skip_closings drops the source system's "Stäng ..."
+ closing vouchers (SIE-imported only) from the period movements: imported
+ years close the P&L accounts straight to 2099, so the year otherwise nets
+ to zero; the TUI årsredovisning uses the same convention. Amounts signed:
+ debit positive. */
static yyjson_mut_val *balance_query(yyjson_mut_doc *doc, sqlite3 *db,
int64_t org_id, int64_t fy_id,
const char *from, const char *to,
- const char *types_filter, char **err)
+ const char *types_filter,
+ int skip_closings, char **err)
{
- char sql[1280];
+ char ib_series[16];
+ db_setting_copy(db, org_id, "series_ib", "IB", ib_series,
+ sizeof ib_series);
+ char sql[2048];
snprintf(sql, sizeof sql,
"SELECT a.number,a.name,a.type,"
- " COALESCE(SUM(CASE WHEN v.series <> 'IB'"
+ " COALESCE(SUM(CASE WHEN v.series <> 'IB' AND v.series <> ?5"
+ " AND (?6 = 0 OR v.source <> 'sie_import'"
+ " OR COALESCE(v.description,'') NOT LIKE 'Stäng%%')"
" AND v.date BETWEEN ?2 AND ?3 THEN r.debit_ore END),0),"
- " COALESCE(SUM(CASE WHEN v.series <> 'IB'"
+ " COALESCE(SUM(CASE WHEN v.series <> 'IB' AND v.series <> ?5"
+ " AND (?6 = 0 OR v.source <> 'sie_import'"
+ " OR COALESCE(v.description,'') NOT LIKE 'Stäng%%')"
" AND v.date BETWEEN ?2 AND ?3 THEN r.credit_ore END),0),"
- " COALESCE(SUM(CASE WHEN (v.series = 'IB'"
- " AND v.fiscal_year_id = ?4) OR (v.series <> 'IB'"
- " AND v.date < ?2) THEN r.debit_ore END),0),"
- " COALESCE(SUM(CASE WHEN (v.series = 'IB'"
- " AND v.fiscal_year_id = ?4) OR (v.series <> 'IB'"
- " AND v.date < ?2) THEN r.credit_ore END),0)"
+ " COALESCE(SUM(CASE WHEN " REPORT_IB_ROW_SQL
+ " THEN r.debit_ore END),0),"
+ " COALESCE(SUM(CASE WHEN " REPORT_IB_ROW_SQL
+ " THEN r.credit_ore END),0)"
" FROM accounts a"
" LEFT JOIN (voucher_rows r JOIN vouchers v"
" ON v.org_id=r.org_id AND v.id=r.voucher_id)"
@@ -94,6 +104,8 @@ static yyjson_mut_val *balance_query(yyjson_mut_doc *doc, sqlite3 *db,
sqlite3_bind_text(st, 2, from, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 3, to, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(st, 4, fy_id);
+ sqlite3_bind_text(st, 5, ib_series, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_int(st, 6, skip_closings);
yyjson_mut_val *arr = yyjson_mut_arr(doc);
while (sqlite3_step(st) == SQLITE_ROW) {
const char *number = (const char *)sqlite3_column_text(st, 0);
@@ -128,7 +140,8 @@ yyjson_mut_val *report_trial_balance(yyjson_mut_doc *doc, sqlite3 *db,
if (!to)
to = fy.end;
- yyjson_mut_val *rows = balance_query(doc, db, org_id, fy.id, from, to, NULL, err);
+ yyjson_mut_val *rows = balance_query(doc, db, org_id, fy.id, from, to, NULL,
+ 0, err);
if (!rows)
return NULL;
@@ -187,7 +200,7 @@ yyjson_mut_val *report_income_statement(yyjson_mut_doc *doc, sqlite3 *db,
yyjson_mut_val *rows = balance_query(
doc, db, org_id, fy.id, from, to,
- " AND a.type IN ('revenue','expense')", err);
+ " AND a.type IN ('revenue','expense')", 1, err);
if (!rows)
return NULL;
@@ -244,7 +257,7 @@ yyjson_mut_val *report_balance_sheet(yyjson_mut_doc *doc, sqlite3 *db,
yyjson_mut_val *rows = balance_query(
doc, db, org_id, fy.id, fy.start, to,
- " AND a.type IN ('asset','liability','equity')", err);
+ " AND a.type IN ('asset','liability','equity')", 0, err);
if (!rows)
return NULL;
@@ -280,10 +293,11 @@ yyjson_mut_val *report_balance_sheet(yyjson_mut_doc *doc, sqlite3 *db,
yyjson_mut_arr_add_val(*target, copy);
}
- /* current year result belongs to equity */
+ /* current year result belongs to equity; the closings stay included so a
+ transferred result (2099) does not get counted twice */
yyjson_mut_val *inc = balance_query(
doc, db, org_id, fy.id, fy.start, to,
- " AND a.type IN ('revenue','expense')", err);
+ " AND a.type IN ('revenue','expense')", 0, err);
int64_t result = 0;
if (inc) {
size_t m = yyjson_mut_arr_size(inc);
@@ -365,6 +379,18 @@ static int vat_box_payable(const char *box)
#define VAT_MAX_BOXES 64
+/* Vouchers left out of the momsdeklaration: the momsomföring itself (any
+ voucher with a 2650 row moves the period's VAT to the redovisningskonto
+ and would zero the boxes) and a source system's imported "Stäng ..." year
+ closings, which zero the P&L underlag. */
+#define VAT_VOUCHER_SQL \
+ " AND NOT EXISTS (SELECT 1 FROM voucher_rows r2 JOIN accounts a2" \
+ " ON a2.org_id = r2.org_id AND a2.id = r2.account_id" \
+ " WHERE r2.org_id = v.org_id AND r2.voucher_id = v.id" \
+ " AND a2.number = '2650')" \
+ " AND NOT (v.source = 'sie_import'" \
+ " AND COALESCE(v.description,'') LIKE 'Stäng%')"
+
yyjson_mut_val *report_vat(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id,
const char *from, const char *to, char **err)
{
@@ -377,6 +403,9 @@ yyjson_mut_val *report_vat(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id,
int64_t amount;
} acc[VAT_MAX_BOXES];
size_t nacc = 0;
+ char ib_series[16];
+ db_setting_copy(db, org_id, "series_ib", "IB", ib_series,
+ sizeof ib_series);
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(
db,
@@ -401,21 +430,25 @@ yyjson_mut_val *report_vat(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id,
" JOIN vouchers v ON v.org_id=r.org_id AND v.id=r.voucher_id"
" JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
" WHERE r.org_id=?1 AND a.number=?2 AND v.series <> 'IB'"
- " AND v.date BETWEEN ?3 AND ?4";
+ " AND v.series <> ?5 AND v.date BETWEEN ?3 AND ?4"
+ VAT_VOUCHER_SQL;
else if (strcmp(mt, "type") == 0)
sql = "SELECT COALESCE(SUM(r.debit_ore),0),"
"COALESCE(SUM(r.credit_ore),0) FROM voucher_rows r"
" JOIN vouchers v ON v.org_id=r.org_id AND v.id=r.voucher_id"
" JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
" WHERE r.org_id=?1 AND a.type=?2 AND v.series <> 'IB'"
- " AND v.date BETWEEN ?3 AND ?4";
+ " AND v.series <> ?5 AND v.date BETWEEN ?3 AND ?4"
+ VAT_VOUCHER_SQL;
else
sql = "SELECT COALESCE(SUM(r.debit_ore),0),"
"COALESCE(SUM(r.credit_ore),0) FROM voucher_rows r"
" JOIN vouchers v ON v.org_id=r.org_id AND v.id=r.voucher_id"
" JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
" WHERE r.org_id=?1 AND CAST(a.number AS INTEGER) BETWEEN ?2"
- " AND ?3 AND v.series <> 'IB' AND v.date BETWEEN ?4 AND ?5";
+ " AND ?3 AND v.series <> 'IB' AND v.series <> ?6"
+ " AND v.date BETWEEN ?4 AND ?5"
+ VAT_VOUCHER_SQL;
if (sqlite3_prepare_v2(db, sql, -1, &qs, NULL) != SQLITE_OK)
break;
sqlite3_bind_int64(qs, 1, org_id);
@@ -423,6 +456,7 @@ yyjson_mut_val *report_vat(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id,
sqlite3_bind_text(qs, 2, pattern, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(qs, 3, from, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(qs, 4, to, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(qs, 5, ib_series, -1, SQLITE_TRANSIENT);
} else {
long lo = 0, hi = 0;
if (sscanf(pattern, "%ld-%ld", &lo, &hi) != 2) {
@@ -433,6 +467,7 @@ yyjson_mut_val *report_vat(yyjson_mut_doc *doc, sqlite3 *db, int64_t org_id,
sqlite3_bind_int64(qs, 3, hi);
sqlite3_bind_text(qs, 4, from, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(qs, 5, to, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(qs, 6, ib_series, -1, SQLITE_TRANSIENT);
}
if (sqlite3_step(qs) == SQLITE_ROW)
amount = (sqlite3_column_int64(qs, 0) -
@@ -532,7 +567,7 @@ yyjson_mut_val *report_general_ledger(yyjson_mut_doc *doc, sqlite3 *db,
if (!to)
to = fy.end;
yyjson_mut_val *bal =
- balance_query(doc, db, org_id, fy.id, from, to, NULL, err);
+ balance_query(doc, db, org_id, fy.id, from, to, NULL, 0, err);
if (!bal)
return NULL;
sqlite3_stmt *rs = NULL;
diff --git a/src/reports.h b/src/reports.h
index c4a6e4e..82c29e5 100644
--- a/src/reports.h
+++ b/src/reports.h
@@ -6,6 +6,21 @@
#include "yyjson.h"
+/* SQL condition: voucher row `r` of voucher `v` on account `a` belongs to the
+ opening balance of fiscal year ?4 (org ?1) for a period starting ?2; ?5 is
+ the configured IB series. Balance accounts carry all earlier history,
+ including earlier years' IB vouchers; P&L accounts restart at the year
+ start, so only this year's IB vouchers and movements before ?2 count. */
+#define REPORT_IB_ROW_SQL \
+ "((v.series = 'IB' OR v.series = ?5) AND (v.fiscal_year_id = ?4" \
+ " OR (v.date < (SELECT f.start_date FROM fiscal_years f" \
+ " WHERE f.org_id = ?1 AND f.id = ?4)" \
+ " AND a.type NOT IN ('revenue','expense'))))" \
+ " OR (v.series <> 'IB' AND v.series <> ?5 AND v.date < ?2" \
+ " AND (a.type NOT IN ('revenue','expense') OR v.date >=" \
+ " (SELECT f.start_date FROM fiscal_years f" \
+ " WHERE f.org_id = ?1 AND f.id = ?4)))"
+
yyjson_mut_val *report_trial_balance(yyjson_mut_doc *doc, sqlite3 *db,
int64_t org_id, int64_t fy_id,
const char *from, const char *to,
diff --git a/src/sie.c b/src/sie.c
index ba4c1a8..c2ce19a 100644
--- a/src/sie.c
+++ b/src/sie.c
@@ -8,8 +8,10 @@
#include <string.h>
#include <time.h>
+#include "db.h"
#include "ledger.h"
#include "log.h"
+#include "reports.h"
#include "util.h"
#include "version.h"
@@ -135,20 +137,21 @@ int sie_export_file(sqlite3 *db, int64_t org_id, int64_t fy_id,
put_date(f, end);
put(f, "\n");
- /* accounts and balances: IB = this year's series "IB" voucher(s) plus
- all non-IB history before the year; UB = everything through the end;
- RES = the year's non-IB movements. */
+ /* accounts and balances: IB per REPORT_IB_ROW_SQL, UB = IB plus the
+ year's non-IB movements, RES = those movements. #IB/#UB are written
+ for balance accounts only, #RES for P&L accounts only. */
+ char ib_series[16];
+ db_setting_copy(db, org_id, "series_ib", "IB", ib_series,
+ sizeof ib_series);
if (sqlite3_prepare_v2(
db,
"SELECT a.number,a.name,"
- " COALESCE(SUM(CASE WHEN (v.series = 'IB'"
- " AND v.fiscal_year_id = ?4) OR (v.series <> 'IB'"
- " AND v.date < ?2) THEN r.debit_ore - r.credit_ore END),0),"
- " COALESCE(SUM(CASE WHEN v.date <= ?3"
+ " COALESCE(SUM(CASE WHEN " REPORT_IB_ROW_SQL
" THEN r.debit_ore - r.credit_ore END),0),"
- " COALESCE(SUM(CASE WHEN v.series <> 'IB'"
+ " COALESCE(SUM(CASE WHEN v.series <> 'IB' AND v.series <> ?5"
" AND v.date >= ?2 AND v.date <= ?3"
- " THEN r.debit_ore - r.credit_ore END),0)"
+ " THEN r.debit_ore - r.credit_ore END),0),"
+ " a.type IN ('revenue','expense')"
" FROM accounts a LEFT JOIN (voucher_rows r JOIN vouchers v"
" ON v.org_id=r.org_id AND v.id=r.voucher_id)"
" ON r.org_id=a.org_id AND r.account_id=a.id"
@@ -163,12 +166,18 @@ int sie_export_file(sqlite3 *db, int64_t org_id, int64_t fy_id,
sqlite3_bind_text(st, 2, start, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 3, end, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(st, 4, fy_id);
+ sqlite3_bind_text(st, 5, ib_series, -1, SQLITE_TRANSIENT);
while (sqlite3_step(st) == SQLITE_ROW) {
const char *number = (const char *)sqlite3_column_text(st, 0);
const char *name = (const char *)sqlite3_column_text(st, 1);
int64_t ib = sqlite3_column_int64(st, 2);
- int64_t ub = sqlite3_column_int64(st, 3);
- int64_t res = sqlite3_column_int64(st, 4);
+ int64_t res = sqlite3_column_int64(st, 3);
+ int pl = sqlite3_column_int(st, 4);
+ int64_t ub = ib + res;
+ if (pl)
+ ib = ub = 0;
+ else
+ res = 0;
put(f, "#KONTO ");
put(f, number);
put(f, " ");
diff --git a/src/util.c b/src/util.c
index 9543875..7882642 100644
--- a/src/util.c
+++ b/src/util.c
@@ -276,6 +276,19 @@ char *util_str_trim(char *s)
return s;
}
+int util_hex_color_valid(const char *s)
+{
+ if (!s || s[0] != '#' || strlen(s) != 7)
+ return 0;
+ for (int i = 1; i < 7; i++) {
+ char c = s[i];
+ if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
+ (c >= 'A' && c <= 'F')))
+ return 0;
+ }
+ return 1;
+}
+
static int is_leap(int y)
{
return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
diff --git a/src/util.h b/src/util.h
index d854f76..a94cf16 100644
--- a/src/util.h
+++ b/src/util.h
@@ -32,6 +32,7 @@ int util_const_eq(const void *a, const void *b, size_t n);
int64_t util_now(void);
void util_iso8601(int64_t t, char *buf, size_t n);
char *util_str_trim(char *s);
+int util_hex_color_valid(const char *s);
int util_parse_iso_date(const char *s);
/* base64 (standard alphabet, padding required) */