diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-21 09:56:01 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-21 09:56:01 +0200 |
| commit | 4488b6c2eb1a3ddf20e190ebd28953e4f3f01042 (patch) | |
| tree | b52812145147669d04f23a57d41b3f604104963f /src/payslip.c | |
| parent | e692f0fbe16342195297048534ad1be227493107 (diff) | |
| download | bokf-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.c | 298 |
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; +} |
