summaryrefslogtreecommitdiff
path: root/src/payslip.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-21 09:56:01 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-21 09:56:01 +0200
commit4488b6c2eb1a3ddf20e190ebd28953e4f3f01042 (patch)
treeb52812145147669d04f23a57d41b3f604104963f /src/payslip.c
parente692f0fbe16342195297048534ad1be227493107 (diff)
downloadbokf-4488b6c2eb1a3ddf20e190ebd28953e4f3f01042.tar.gz
bokf-4488b6c2eb1a3ddf20e190ebd28953e4f3f01042.zip
payroll: lönebesked PDF and mail (schema v11)
Diffstat (limited to 'src/payslip.c')
-rw-r--r--src/payslip.c298
1 files changed, 298 insertions, 0 deletions
diff --git a/src/payslip.c b/src/payslip.c
new file mode 100644
index 0000000..81ebded
--- /dev/null
+++ b/src/payslip.c
@@ -0,0 +1,298 @@
+#include "payslip.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 RULE_HEADER_Y 90.000
+#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 SIZE_TITLE 12.0
+
+#define ROW_STEP 14.7105
+#define INFO_LABEL_RIGHT 150.000
+#define INFO_VALUE_X 155.000
+#define INFO_BASE_Y 112.0000
+#define LABEL_DY 0.3874
+
+#define BAR_TABLE_Y 238.000
+#define BAR_TABLE_H 15.410
+#define TABLE_HEADER_Y 249.3786
+#define ROW_FIRST_Y 264.0891
+#define AMOUNT_LABEL_X 20.1015
+#define AMOUNT_RIGHT 574.892
+#define AMOUNT_RULE_Y (ROW_FIRST_Y + 2 * ROW_STEP + 10.0)
+#define NOTE_DY 7.5000
+
+#define RULE_FOOTER_Y 621.756
+#define FOOT_LABEL_Y 642.4721
+#define FOOT_NAME_Y 656.6028
+#define FOOT_ADDR_Y 671.3133
+#define FOOT_EMAIL_Y 671.3133
+#define FOOT_CITY_Y 686.0238
+#define FOOT_ORG_LABEL_X 263.8759
+#define FOOT_ORG_X 334.6266
+#define FOOT_ORG_Y 641.8922
+#define FOOT_PAGE_Y 819.7954
+#define FOOT_PAGE_RIGHT 567.4275
+
+#define WORDMARK_X 21.742
+#define TITLE_RIGHT 574.892
+#define TITLE_BASE_Y 69.1358
+
+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)
+{
+ char whole[32];
+ 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);
+ snprintf(out, n, "%s%s,%02llu", sign, whole, (unsigned long long)rest);
+}
+
+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 void draw_info(struct pdf *p, const struct payslip_data *d)
+{
+ static const char *const labels[5] = {
+ "Anställd", "Personnummer", "Period", "Utbetalningsdatum",
+ "Skattetabell",
+ };
+ char table[16];
+ const char *values[5];
+
+ snprintf(table, sizeof table, "%d kol %d", d->tax_table, d->tax_column);
+ values[0] = d->employee_name;
+ values[1] = d->personal_no_masked;
+ values[2] = d->period;
+ values[3] = d->pay_date;
+ values[4] = table;
+
+ 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]);
+ }
+}
+
+static void draw_amount_table(struct pdf *p, const struct payslip_data *d)
+{
+ char bruttolon[48], skatt[48], netto[48];
+
+ fmt_kronor(d->gross_ore, bruttolon, sizeof bruttolon, 1);
+ fmt_kronor(-d->tax_ore, skatt, sizeof skatt, 1);
+ fmt_kronor(d->net_ore, netto, sizeof netto, 1);
+
+ pdf_fill_rect(p, PAGE_LEFT, BAR_TABLE_Y, PAGE_RIGHT_X - PAGE_LEFT,
+ BAR_TABLE_H, INK);
+ text_at(p, AMOUNT_LABEL_X, TABLE_HEADER_Y, "HB", SIZE_VALUE, WHITE,
+ "Specifikation");
+ text_right(p, AMOUNT_RIGHT, TABLE_HEADER_Y, "H", SIZE_VALUE, WHITE,
+ d->run_ref);
+
+ text_at(p, AMOUNT_LABEL_X, ROW_FIRST_Y, "H", SIZE_VALUE, INK, "Bruttolön");
+ text_right(p, AMOUNT_RIGHT, ROW_FIRST_Y, "H", SIZE_VALUE, INK, bruttolon);
+ text_at(p, AMOUNT_LABEL_X, ROW_FIRST_Y + ROW_STEP, "H", SIZE_VALUE, INK,
+ "Preliminärskatt");
+ text_right(p, AMOUNT_RIGHT, ROW_FIRST_Y + ROW_STEP, "H", SIZE_VALUE, INK,
+ skatt);
+
+ pdf_line(p, PAGE_LEFT, AMOUNT_RULE_Y, PAGE_RIGHT_X, AMOUNT_RULE_Y, RULE_W,
+ RULE);
+
+ text_at(p, AMOUNT_LABEL_X, ROW_FIRST_Y + 3 * ROW_STEP, "HB", SIZE_VALUE,
+ INK, "Nettolön");
+ text_right(p, AMOUNT_RIGHT, ROW_FIRST_Y + 3 * ROW_STEP, "HB", SIZE_VALUE,
+ INK, netto);
+
+ {
+ char note[96];
+ int64_t whole = d->rate_bp / 100;
+ int64_t frac = d->rate_bp % 100;
+
+ if (frac)
+ snprintf(note, sizeof note,
+ "Arbetsgivaravgifter %lld,%02lld %% betalas av"
+ " arbetsgivaren.",
+ (long long)whole, (long long)frac);
+ else
+ snprintf(note, sizeof note,
+ "Arbetsgivaravgifter %lld %% betalas av arbetsgivaren.",
+ (long long)whole);
+ text_at(p, AMOUNT_LABEL_X, ROW_FIRST_Y + 4 * ROW_STEP + NOTE_DY, "H",
+ SIZE_VALUE, INK, note);
+ }
+}
+
+static void draw_footer(struct pdf *p, const struct payslip_data *d)
+{
+ pdf_line(p, PAGE_LEFT, RULE_FOOTER_Y, PAGE_RIGHT_X, RULE_FOOTER_Y, RULE_W,
+ RULE);
+ text_at(p, AMOUNT_LABEL_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, FOOT_ORG_LABEL_X, FOOT_LABEL_Y, "HB", SIZE_FOOT, INK, "Orgnr");
+ text_at(p, AMOUNT_LABEL_X, FOOT_NAME_Y, "H", SIZE_VALUE, INK,
+ d->employer.name);
+ text_at(p, INFO_VALUE_X, FOOT_NAME_Y, "H", SIZE_VALUE, INK,
+ d->employer.phone);
+ text_at(p, AMOUNT_LABEL_X, FOOT_ADDR_Y, "H", SIZE_VALUE, INK,
+ d->employer.address);
+ text_at(p, INFO_VALUE_X, FOOT_EMAIL_Y, "H", SIZE_VALUE, INK,
+ d->employer.email);
+ put_address(p, AMOUNT_LABEL_X, FOOT_CITY_Y, INK, SIZE_VALUE,
+ d->employer.postal_code, d->employer.city);
+ text_at(p, FOOT_ORG_X, FOOT_ORG_Y, "H", SIZE_VALUE, INK,
+ d->employer.org_nr);
+ text_at(p, FOOT_PAGE_RIGHT, FOOT_PAGE_Y, "H", SIZE_PAGE, INK, "1");
+}
+
+int payslip_render(const struct payslip_data *d, unsigned char **out,
+ size_t *len)
+{
+ struct pdf *p;
+
+ if (out)
+ *out = NULL;
+ if (len)
+ *len = 0;
+ if (!d || !out || !len)
+ return -1;
+
+ 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);
+ 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,
+ RULE);
+ draw_info(p, d);
+ draw_amount_table(p, d);
+ draw_footer(p, d);
+ *out = pdf_finish(p, len);
+ return *out ? 0 : -1;
+}