diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-20 15:08:58 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-20 15:08:58 +0200 |
| commit | fb21e8efa65662dcb6c2cc7ed693dea91888c3a7 (patch) | |
| tree | 43f0662b2c61dd596e2be78fdd107463e8aa9591 /src/invoice.c | |
| parent | 480640a747b81cb39deb1ec24e6e199a9c837fbe (diff) | |
| download | bokf-fb21e8efa65662dcb6c2cc7ed693dea91888c3a7.tar.gz bokf-fb21e8efa65662dcb6c2cc7ed693dea91888c3a7.zip | |
invoice: render the document as PDF
Diffstat (limited to 'src/invoice.c')
| -rw-r--r-- | src/invoice.c | 576 |
1 files changed, 576 insertions, 0 deletions
diff --git a/src/invoice.c b/src/invoice.c new file mode 100644 index 0000000..622c05a --- /dev/null +++ b/src/invoice.c @@ -0,0 +1,576 @@ +#include "invoice.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "pdf.h" +#include "util.h" +#include "wordmark.h" + +#define INK "#314c59" +#define WHITE "#ffffff" +#define RULE "#000000" + +#define PAGE_LEFT 17.300 +#define PAGE_RIGHT_X 577.700 +#define BAR_HEADER_Y 53.300 +#define BAR_HEADER_H 22.416 +#define BAR_TABLE_Y 242.436 +#define BAR_TABLE_H 15.410 +#define RULE_SUMMARY_Y 507.574 +#define RULE_FOOTER_Y 621.756 +#define RULE_W 0.7005 + +#define SIZE_VALUE 9.1065 +#define SIZE_LABEL 7.2852 +#define SIZE_FOOT 6.3746 +#define SIZE_PAGE 9.75 + +#define ROW_STEP 14.7105 +#define DESC_FIRST_STEP 29.4210 + +#define INFO_VALUE_X 118.8722 +#define INFO_LABEL_RIGHT 114.010 +#define INFO_BASE_Y 101.1053 +#define LABEL_DY 0.3874 +#define DRY_LABEL_Y 172.2070 +#define DRY_LINE_STEP 10.4716 +#define DRY_LINE_Y 174.0293 +#define OUR_REF_Y 209.6830 +#define YOUR_REF_Y 224.3935 + +#define ADDR_X 370.3521 +#define ADDR_BASE_Y 115.8158 + +#define TABLE_HEADER_Y 253.8146 +#define COL_ARTICLE_X 20.1015 +#define COL_DESC_X 118.8722 +#define COL_QTY_RIGHT 365.446 +#define COL_UNIT_X 370.3521 +#define COL_PRICE_RIGHT 433.392 +#define COL_NOTE_X 442.4121 +#define COL_ANM_RIGHT 504.147 +#define COL_AMOUNT_RIGHT 574.892 +#define ROW_FIRST_Y 268.5251 + +#define SUM_LABEL_RIGHT 329.760 +#define SUM_VALUE_RIGHT 365.446 +#define SUM_R_LABEL 504.170 +#define SUM_R_VALUE 574.892 +#define SUM_Y_A 518.6040 +#define SUM_Y_B 533.3146 +#define SUM_Y_C 548.0251 +#define SUM_Y_D 569.0401 +#define SUM_Y_E 583.7506 +#define SUM_Y_OCR 598.4612 +#define SUM_Y_TOTAL 615.2373 + +#define FOOT_LABEL_Y 642.4721 +#define FOOT_ORG_Y 641.8922 +#define FOOT_NAME_Y 656.6028 +#define FOOT_VAT_Y 655.8664 +#define FOOT_MOMSY 657.1827 +#define FOOT_ADDR_Y 671.3133 +#define FOOT_EMAIL_Y 671.3133 +#define FOOT_FSKATT_Y 671.8932 +#define FOOT_CITY_Y 686.0238 +#define FOOT_PAGE_Y 819.7954 + +#define WORDMARK_X 21.742 +#define FAKTURA_X 498.400 + +static void text_at(struct pdf *p, double x, double base, const char *font, + double size, const char *rgb, const char *s) +{ + if (s && *s) + pdf_text(p, x, base, font, size, rgb, s); +} + +static void text_right(struct pdf *p, double right, double base, + const char *font, double size, const char *rgb, + const char *s) +{ + if (s && *s) + pdf_text(p, right - pdf_text_width(font, size, s), base, font, size, + 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]; + int len = snprintf(digits, sizeof digits, "%llu", + (unsigned long long)v); + size_t o = 0; + + if (len < 0) + len = 0; + for (int i = 0; i < len && o + 1 < n; i++) { + if (i > 0 && (len - i) % 3 == 0) + out[o++] = ' '; + out[o++] = digits[i]; + } + out[o] = '\0'; +} + +static uint64_t ore_abs(int64_t v) +{ + return v < 0 ? (uint64_t)(-(v + 1)) + 1 : (uint64_t)v; +} + +static void fmt_kronor(int64_t ore, char *out, size_t n, int grouped, + int always_cents) +{ + char whole[48]; + uint64_t kr = ore_abs(ore) / 100; + uint64_t rest = ore_abs(ore) % 100; + const char *sign = ore < 0 ? "-" : ""; + + if (grouped) + group_digits(whole, sizeof whole, kr); + else + snprintf(whole, sizeof whole, "%llu", (unsigned long long)kr); + if (rest || always_cents) + snprintf(out, n, "%s%s,%02llu", sign, whole, + (unsigned long long)rest); + else + snprintf(out, n, "%s%s", sign, whole); +} + +static void fmt_quantity(int64_t milli, char *out, size_t n) +{ + uint64_t v = ore_abs(milli); + uint64_t whole = v / 1000; + uint64_t frac = v % 1000; + const char *sign = milli < 0 ? "-" : ""; + + if (!frac) + snprintf(out, n, "%s%llu", sign, (unsigned long long)whole); + else if (frac % 100 == 0) + snprintf(out, n, "%s%llu,%llu", sign, (unsigned long long)whole, + (unsigned long long)(frac / 100)); + else if (frac % 10 == 0) + snprintf(out, n, "%s%llu,%02llu", sign, (unsigned long long)whole, + (unsigned long long)(frac / 10)); + else + snprintf(out, n, "%s%llu,%03llu", sign, (unsigned long long)whole, + (unsigned long long)frac); +} + +static int64_t vat_part(int64_t net, int64_t rate) +{ + int64_t v = net * rate; + + if (v >= 0) + return (v + 50) / 100; + return -((-v + 50) / 100); +} + +void invoice_totals(const struct invoice_doc *d, struct invoice_totals *t) +{ + if (!t) + return; + memset(t, 0, sizeof *t); + if (!d || (!d->lines && d->nlines)) + return; + for (size_t i = 0; i < d->nlines; i++) { + const struct invoice_line *l = &d->lines[i]; + const char *code = l->vat_code ? l->vat_code : "25"; + + t->net_ore += l->amount_ore; + if (strcmp(code, "25") == 0) + t->net_25 += l->amount_ore; + else if (strcmp(code, "12") == 0) + t->net_12 += l->amount_ore; + else if (strcmp(code, "6") == 0) + t->net_6 += l->amount_ore; + else + t->net_free += l->amount_ore; + } + t->vat_ore = vat_part(t->net_25, 25) + vat_part(t->net_12, 12) + + vat_part(t->net_6, 6); + t->total_ore = t->net_ore + t->vat_ore; +} + +int invoice_ocr_check(const char *number_digits) +{ + size_t len; + int sum = 0; + + if (!number_digits) + return -1; + len = strlen(number_digits); + if (!len) + return 0; + for (size_t i = 0; i < len; i++) + if (number_digits[i] < '0' || number_digits[i] > '9') + return -1; + for (size_t i = 0; i < len; i++) { + int d = number_digits[len - 1 - i] - '0'; + + if (i % 2 == 0) { + d *= 2; + if (d > 9) + d -= 9; + } + sum += d; + } + return (10 - sum % 10) % 10; +} + +static size_t count_lines(const char *s) +{ + size_t n = 1; + + if (!s) + return n; + for (const char *p = s; *p; p++) + if (*p == '\n') + n++; + return n; +} + +static double desc_offset(size_t line) +{ + if (line == 0) + return 0.0; + if (line == 1) + return DESC_FIRST_STEP; + return DESC_FIRST_STEP + (double)(line - 1) * ROW_STEP; +} + +static void put_desc_line(struct pdf *p, double y, const char *s, size_t len) +{ + char buf[512]; + + if (!len) + return; + snprintf(buf, sizeof buf, "%.*s", (int)len, s); + pdf_text(p, COL_DESC_X, y, "H", SIZE_VALUE, INK, buf); +} + +static void put_address(struct pdf *p, double x, double y, const char *rgb, + double size, const char *postal, const char *city) +{ + char buf[128]; + + if (!postal) + postal = ""; + if (!city) + city = ""; + if (*postal && *city) + snprintf(buf, sizeof buf, "%s %s", postal, city); + else + snprintf(buf, sizeof buf, "%s%s", postal, city); + if (*buf) + pdf_text(p, x, y, "H", size, rgb, buf); +} + +static int doc_fits(const struct invoice_doc *d) +{ + double y = ROW_FIRST_Y; + + for (size_t i = 0; i < d->nlines; i++) { + const char *s = d->lines[i].description; + size_t n = count_lines(s); + + if (!s) + s = ""; + for (size_t k = 0; k < n; k++) { + if (y + desc_offset(k) > RULE_SUMMARY_Y - 4.0) + return 0; + s = strchr(s, '\n'); + if (s) + s++; + } + y += (double)(n + 1) * ROW_STEP; + } + return 1; +} + +static void draw_info(struct pdf *p, const struct invoice_doc *d) +{ + static const char *const labels[5] = { + "Fakturanummer", "Fakturadatum", "Leveransdatum", "Ert momsreg.nr", + "Betalningsvilkor", + }; + char number[32], payment[32]; + const char *values[5]; + + snprintf(number, sizeof number, "%lld", (long long)d->number); + snprintf(payment, sizeof payment, "%d dagar", d->payment_days); + values[0] = number; + values[1] = d->invoice_date; + values[2] = d->delivery_date; + values[3] = d->customer.vat_nr; + values[4] = payment; + + for (size_t i = 0; i < 5; i++) { + double y = INFO_BASE_Y + (double)i * ROW_STEP; + + text_right(p, INFO_LABEL_RIGHT, y + LABEL_DY, "HB", SIZE_LABEL, INK, + labels[i]); + text_at(p, INFO_VALUE_X, y, "H", SIZE_VALUE, INK, values[i]); + } + text_right(p, INFO_LABEL_RIGHT, DRY_LABEL_Y, "HB", SIZE_LABEL, INK, + "Dröjsmålsränta"); + text_at(p, INFO_VALUE_X, DRY_LINE_Y, "H", SIZE_VALUE, INK, + "Vid betalning efter"); + text_at(p, INFO_VALUE_X, DRY_LINE_Y + DRY_LINE_STEP, "H", SIZE_VALUE, INK, + "förfallodagen debiteras"); + text_at(p, INFO_VALUE_X, DRY_LINE_Y + 2 * DRY_LINE_STEP, "H", SIZE_VALUE, + INK, "ränta enligt räntelagen"); + text_right(p, INFO_LABEL_RIGHT, OUR_REF_Y + LABEL_DY, "HB", SIZE_LABEL, + INK, "Vår referens"); + text_at(p, INFO_VALUE_X, OUR_REF_Y, "H", SIZE_VALUE, INK, d->our_ref); + text_right(p, INFO_LABEL_RIGHT, YOUR_REF_Y + LABEL_DY, "HB", SIZE_LABEL, + INK, "Er referens"); + text_at(p, INFO_VALUE_X, YOUR_REF_Y, "H", SIZE_VALUE, INK, d->your_ref); +} + +static void draw_customer(struct pdf *p, const struct invoice_customer *c) +{ + const char *s = c->address ? c->address : ""; + double y = ADDR_BASE_Y; + + text_at(p, ADDR_X, INFO_BASE_Y, "HB", SIZE_VALUE, INK, "Fakturaadress"); + text_at(p, ADDR_X, y, "H", SIZE_VALUE, INK, c->name); + y += ROW_STEP; + for (;;) { + const char *nl = strchr(s, '\n'); + + if (nl) { + char buf[512]; + + snprintf(buf, sizeof buf, "%.*s", (int)(nl - s), s); + if (*buf) + pdf_text(p, ADDR_X, y, "H", SIZE_VALUE, INK, buf); + y += ROW_STEP; + s = nl + 1; + } else { + if (*s) + pdf_text(p, ADDR_X, y, "H", SIZE_VALUE, INK, s); + break; + } + } + put_address(p, ADDR_X, y + ROW_STEP, INK, SIZE_VALUE, c->postal_code, + c->city); +} + +static void draw_table(struct pdf *p, const struct invoice_doc *d) +{ + double y = ROW_FIRST_Y; + + pdf_fill_rect(p, PAGE_LEFT, BAR_TABLE_Y, PAGE_RIGHT_X - PAGE_LEFT, + BAR_TABLE_H, INK); + text_at(p, COL_ARTICLE_X, TABLE_HEADER_Y, "HB", SIZE_VALUE, WHITE, + "Art.nr"); + text_at(p, COL_DESC_X, TABLE_HEADER_Y, "HB", SIZE_VALUE, WHITE, + "Beskrivning"); + text_right(p, COL_QTY_RIGHT, TABLE_HEADER_Y, "HB", SIZE_VALUE, WHITE, + "Antal"); + text_right(p, COL_PRICE_RIGHT, TABLE_HEADER_Y, "HB", SIZE_VALUE, WHITE, + "á-pris"); + text_right(p, COL_ANM_RIGHT, TABLE_HEADER_Y, "HB", SIZE_VALUE, WHITE, + "Anm"); + text_right(p, COL_AMOUNT_RIGHT, TABLE_HEADER_Y, "HB", SIZE_VALUE, WHITE, + "Belopp"); + + for (size_t i = 0; i < d->nlines; i++) { + const struct invoice_line *l = &d->lines[i]; + char qty[32], price[48], amount[48]; + const char *s = l->description ? l->description : ""; + size_t line = 0; + + 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); + + 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; + } + } + text_at(p, COL_ARTICLE_X, y, "H", SIZE_VALUE, INK, l->article_no); + text_right(p, COL_QTY_RIGHT, y, "H", SIZE_VALUE, INK, qty); + text_at(p, COL_UNIT_X, y, "H", SIZE_VALUE, INK, l->unit); + text_right(p, COL_PRICE_RIGHT, y, "H", SIZE_VALUE, INK, price); + text_at(p, COL_NOTE_X, y, "H", SIZE_VALUE, INK, l->note); + text_right(p, COL_AMOUNT_RIGHT, y, "H", SIZE_VALUE, INK, amount); + + y += (double)(line + 2) * ROW_STEP; + } +} + +static void draw_summary(struct pdf *p, const struct invoice_doc *d, + const struct invoice_totals *t) +{ + char value[64], total[64]; + + pdf_line(p, PAGE_LEFT, RULE_SUMMARY_Y, PAGE_RIGHT_X, RULE_SUMMARY_Y, + RULE_W, RULE); + + text_right(p, SUM_LABEL_RIGHT, SUM_Y_A + LABEL_DY, "H", SIZE_LABEL, INK, + "Momsunderlag"); + text_right(p, SUM_R_LABEL, SUM_Y_A + LABEL_DY, "H", SIZE_LABEL, INK, + "Belopp före moms"); + fmt_kronor(t->net_ore, value, sizeof value, 0, 0); + text_right(p, SUM_R_VALUE, SUM_Y_A, "H", SIZE_VALUE, INK, value); + + text_right(p, SUM_LABEL_RIGHT, SUM_Y_B + LABEL_DY, "H", SIZE_LABEL, INK, + "Moms 25%"); + fmt_kronor(t->net_25, value, sizeof value, 0, 0); + text_right(p, SUM_VALUE_RIGHT, SUM_Y_B, "H", SIZE_VALUE, INK, value); + text_right(p, SUM_R_LABEL, SUM_Y_B + LABEL_DY, "H", SIZE_LABEL, INK, + "Total moms"); + fmt_kronor(t->vat_ore, value, sizeof value, 0, 0); + text_right(p, SUM_R_VALUE, SUM_Y_B, "H", SIZE_VALUE, INK, value); + + text_right(p, SUM_LABEL_RIGHT, SUM_Y_C + LABEL_DY, "H", SIZE_LABEL, INK, + "Moms 12%"); + fmt_kronor(t->net_12, value, sizeof value, 0, 0); + text_right(p, SUM_VALUE_RIGHT, SUM_Y_C, "H", SIZE_VALUE, INK, value); + text_right(p, SUM_R_LABEL, SUM_Y_C + LABEL_DY, "H", SIZE_LABEL, INK, + "Öresutjämning"); + + text_right(p, SUM_LABEL_RIGHT, SUM_Y_D + LABEL_DY, "H", SIZE_LABEL, INK, + "Moms 6%"); + fmt_kronor(t->net_6, value, sizeof value, 0, 0); + text_right(p, SUM_VALUE_RIGHT, SUM_Y_D, "H", SIZE_VALUE, INK, value); + text_right(p, SUM_R_LABEL, SUM_Y_D + LABEL_DY, "H", SIZE_LABEL, INK, + "Förfallodatum"); + text_right(p, SUM_R_VALUE, SUM_Y_D, "H", SIZE_VALUE, INK, d->due_date); + + text_right(p, SUM_LABEL_RIGHT, SUM_Y_E + LABEL_DY, "H", SIZE_LABEL, INK, + "Momsfritt"); + fmt_kronor(t->net_free, value, sizeof value, 0, 0); + text_right(p, SUM_VALUE_RIGHT, SUM_Y_E, "H", SIZE_VALUE, INK, value); + text_right(p, SUM_R_LABEL, SUM_Y_E + LABEL_DY, "H", SIZE_LABEL, INK, + "Bankgiro"); + text_right(p, SUM_R_VALUE, SUM_Y_E, "H", SIZE_VALUE, INK, + d->seller.bankgiro); + + text_right(p, SUM_R_LABEL, SUM_Y_OCR, "H", SIZE_VALUE, INK, "OCR"); + text_right(p, SUM_R_VALUE, SUM_Y_OCR, "H", SIZE_VALUE, INK, d->ocr); + + text_right(p, SUM_R_LABEL, SUM_Y_TOTAL, "HB", SIZE_VALUE, INK, + "Summa att betala SEK"); + fmt_kronor(t->total_ore, total, sizeof total, 1, 1); + text_right(p, SUM_R_VALUE, SUM_Y_TOTAL, "HB", SIZE_VALUE, INK, total); +} + +static void draw_footer(struct pdf *p, const struct invoice_doc *d) +{ + const struct invoice_seller *s = &d->seller; + + pdf_line(p, PAGE_LEFT, RULE_FOOTER_Y, PAGE_RIGHT_X, RULE_FOOTER_Y, RULE_W, + RULE); + text_at(p, COL_ARTICLE_X, FOOT_LABEL_Y, "HB", SIZE_FOOT, INK, "Adress"); + text_at(p, INFO_VALUE_X, FOOT_LABEL_Y, "HB", SIZE_FOOT, INK, "Kontakt"); + text_at(p, 263.8759, FOOT_LABEL_Y, "HB", SIZE_FOOT, INK, "Orgnr"); + text_at(p, COL_ARTICLE_X, FOOT_NAME_Y, "H", SIZE_VALUE, INK, s->name); + if (s->phone && *s->phone) { + char phone[64]; + + snprintf(phone, sizeof phone, " %s", s->phone); + pdf_text(p, INFO_VALUE_X, FOOT_NAME_Y, "H", SIZE_VALUE, INK, phone); + } + text_at(p, COL_ARTICLE_X, FOOT_ADDR_Y, "H", SIZE_VALUE, INK, s->address); + text_at(p, INFO_VALUE_X, FOOT_EMAIL_Y, "H", SIZE_VALUE, INK, s->email); + put_address(p, COL_ARTICLE_X, FOOT_CITY_Y, INK, SIZE_VALUE, s->postal_code, + s->city); + text_at(p, 334.6266, FOOT_ORG_Y, "H", SIZE_VALUE, INK, s->org_nr); + text_at(p, 263.8759, FOOT_MOMSY, "HB", SIZE_FOOT, INK, "Momsregnr"); + text_at(p, 334.6266, FOOT_VAT_Y, "H", SIZE_VALUE, INK, s->vat_nr); + text_at(p, 263.8759, FOOT_FSKATT_Y, "HB", SIZE_FOOT, INK, + "Godkänd för F-skatt"); + text_at(p, 567.4275, FOOT_PAGE_Y, "H", SIZE_PAGE, INK, "1"); +} + +int invoice_render_pdf(const struct invoice_doc *d, unsigned char **out, + size_t *len) +{ + struct invoice_totals t; + struct pdf *p; + + if (out) + *out = NULL; + if (len) + *len = 0; + if (!d || !out || !len) + return -1; + if (d->nlines && !d->lines) + return -1; + if (!doc_fits(d)) + return -1; + + invoice_totals(d, &t); + 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); + draw_info(p, d); + draw_customer(p, &d->customer); + draw_table(p, d); + draw_summary(p, d, &t); + draw_footer(p, d); + *out = pdf_finish(p, len); + return *out ? 0 : -1; +} |
