avicenna/service/data-hub/business-engine/internal/genpdf/generate.go

345 lines
9.0 KiB
Go

package genpdf
import (
"business-engine/internal/database"
domain "business-engine/internal/domain"
"fmt"
"slices"
"time"
"codeberg.org/go-pdf/fpdf"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
// LineItem repräsentiert eine einzelne Position in der Abrechnung.
type LineItem struct {
Lohnart string
Beschreibung string
Anzahl float64
Betrag float64
Faktor float64
}
type StatementInfo struct {
GearbeiteteStunden float64
GearbeiteteTage float64
Urlaubsgeld float64
Krankheitstage float64
}
// Total berechnet den Gesamtwert der Position.
func (item LineItem) Total() float64 {
if item.Faktor > 0 {
return item.Anzahl * item.Betrag * (item.Faktor / 100)
}
return item.Anzahl * item.Betrag
}
func GenerateSalaryStatement(stmt domain.PayrollStatement, filename string, month time.Time) error {
pdf := fpdf.New("P", "mm", "A4", "")
pdf.AddUTF8Font("DejaVu", "", "DejaVuSans.ttf")
pdf.AddUTF8Font("DejaVu", "B", "DejaVuSans-Bold.ttf")
pdf.AddPage()
pdf.SetFont("DejaVu", "", 12)
pdf.Cell(0, 10, month.Format("01-2006"))
pdf.Ln(12)
pdf.SetDrawColor(200, 200, 200)
pdf.SetLineWidth(0.5)
x, y := pdf.GetXY()
pdf.Line(x, y, x+40, y)
pdf.Ln(10)
pdf.SetFont("DejaVu", "B", 16)
title := fmt.Sprintf("Gehaltsaufstellung für %s %s", stmt.Vorname, stmt.Nachname)
pdf.Cell(0, 10, title)
pdf.Ln(15)
drawInfoBlock(pdf, StatementInfo{
GearbeiteteStunden: stmt.GearbeiteteStunden,
GearbeiteteTage: stmt.GearbeiteteTage,
Urlaubsgeld: stmt.Urlaubsgeld,
Krankheitstage: stmt.Krankheitstage,
})
pdf.Ln(15)
drawHeader(pdf)
items := genLineItemsFromStatement(stmt)
var gesamtbetrag float64
for _, item := range items {
drawRow(pdf, item)
gesamtbetrag += item.Total()
}
drawSummary(pdf, gesamtbetrag)
drawSickdays(pdf, stmt.SickdaysYear)
if err := pdf.OutputFileAndClose(filename); err != nil {
return err
}
if pdf.Err() {
return pdf.Error()
}
return nil
}
func genLineItemsFromStatement(stmt domain.PayrollStatement) []LineItem {
var items []LineItem
// --- Gehalt & Stunden ---
if stmt.Vertragsstunden > 0 && stmt.VertragStundenlohn > 0 {
items = append(items, LineItem{
Lohnart: "0005",
Beschreibung: "Gehalt",
Anzahl: stmt.Vertragsstunden,
Betrag: stmt.VertragStundenlohn,
})
}
if stmt.Ueberstunden > 0 && stmt.VertragStundenlohn > 0 {
items = append(items, LineItem{
Lohnart: "1250",
Beschreibung: "Überstundenvergütung",
Anzahl: stmt.Ueberstunden,
Betrag: stmt.VertragStundenlohn,
})
}
if stmt.Verpflegungspauschale > 0 {
items = append(items, LineItem{
Lohnart: "0227",
Beschreibung: "Verpflegungsmehrauf. 14€",
Anzahl: stmt.Verpflegungspauschale,
Betrag: 14.00,
})
}
if stmt.Urlaubsgeld > 0 {
items = append(items, LineItem{
Lohnart: "0052",
Beschreibung: "Urlaubsgeld Angestellte",
Anzahl: stmt.Urlaubsgeld,
Betrag: 44.00,
})
}
if stmt.TourengeldBonus > 0 {
items = append(items, LineItem{
Lohnart: "5019",
Beschreibung: "Bonus Tourengeld",
Anzahl: 1,
Betrag: stmt.TourengeldBonus,
})
}
if stmt.Tourengeld > 0 {
items = append(items, LineItem{
Lohnart: "1030",
Beschreibung: "Tourengeld",
Anzahl: stmt.Tourengeld,
Betrag: 2.5,
})
}
if stmt.PuenktlichkeitspraemieAnzahl > 0 {
items = append(items, LineItem{
Lohnart: "0109",
Beschreibung: "Verpflegung 25% (Pünktlichkeit)",
Anzahl: stmt.PuenktlichkeitspraemieAnzahl,
Betrag: 14.0,
})
}
if stmt.Gesundheitsbonus > 0 {
items = append(items, LineItem{
Lohnart: "1032",
Beschreibung: "Gesundheitsbonus",
Anzahl: 1,
Betrag: stmt.Gesundheitsbonus,
})
}
if stmt.PScheinZulage > 0 {
items = append(items, LineItem{
Lohnart: "1024",
Beschreibung: "Zulage Personenbeförd.",
Anzahl: stmt.PScheinZulage,
Betrag: 5.0,
})
}
if stmt.Wochenendzuschlag15 > 0 {
items = append(items, LineItem{
Lohnart: "1063",
Beschreibung: "Samstagszuschlag 15%",
Anzahl: stmt.Wochenendzuschlag15,
Betrag: stmt.VertragStundenlohn,
Faktor: 15,
})
}
if stmt.Sonntagszuschlag50 > 0 {
items = append(items, LineItem{
Lohnart: "0581",
Beschreibung: "Sonntagszuschlag 50%",
Anzahl: stmt.Sonntagszuschlag50,
Betrag: stmt.VertragStundenlohn,
Faktor: 50,
})
}
if stmt.Feiertagszuschlag125 > 0 {
items = append(items, LineItem{
Lohnart: "0551",
Beschreibung: "Feiertagszuschlag 125%",
Anzahl: stmt.Feiertagszuschlag125,
Betrag: stmt.VertragStundenlohn,
Faktor: 125,
})
}
if stmt.Nachtzuschlag25 > 0 {
items = append(items, LineItem{
Lohnart: "0505",
Beschreibung: "Nachtzuschl. 20:00-24:00h",
Anzahl: stmt.Nachtzuschlag25,
Betrag: stmt.VertragStundenlohn,
Faktor: 25,
})
}
if stmt.Nachtzuschlag40 > 0 {
items = append(items, LineItem{
Lohnart: "0501",
Beschreibung: "Nachtzuschl. 0:00-4:00h",
Anzahl: stmt.Nachtzuschlag40,
Betrag: stmt.VertragStundenlohn,
Faktor: 40,
})
}
slices.SortFunc(items, func(a, b LineItem) int {
return int(b.Total() - a.Total())
})
return items
}
func drawInfoBlock(pdf *fpdf.Fpdf, info StatementInfo) {
const padding = 3.0
const elementHeight = 4.0 + 6.0
x, y := pdf.GetXY()
drawLabelWithValue(pdf, x+padding, y, "Anw.-Std.", formatFloat(info.GearbeiteteStunden))
drawLabelWithValue(pdf, x+padding+18, y, "Anw.-Tage.", formatFloat(info.GearbeiteteTage))
pdf.Line(x+padding+38, y-padding, x+padding+38, y+elementHeight+padding)
drawLabelWithValue(pdf, x+padding+40, y, "Urlaub", formatFloat(float64(info.Urlaubsgeld)))
drawLabelWithValue(pdf, x+padding+55, y, "Krank.-Tage", formatFloat(float64(info.Krankheitstage)))
rectX := x
rectY := y - padding
rectWidth := 4*20 + 2*padding - 5
rectHeight := elementHeight + 2*padding
pdf.SetFillColor(180, 200, 220)
pdf.Rect(rectX, rectY, rectWidth, rectHeight, "D")
}
func drawLabelWithValue(pdf *fpdf.Fpdf, x, y float64, label, value string) {
const labelHeight = 4.0
const valueheight = 6.0
pdf.SetFont("DejaVu", "", 8)
pdf.SetTextColor(80, 100, 120)
pdf.SetXY(x, y)
pdf.Cell(20, labelHeight, label)
pdf.SetFont("DejaVu", "", 10)
pdf.SetTextColor(0, 0, 0)
pdf.SetXY(x, y+labelHeight)
pdf.Cell(20, valueheight, value)
}
func drawHeader(pdf *fpdf.Fpdf) {
pdf.SetFont("DejaVu", "B", 10)
pdf.SetFillColor(240, 240, 240)
pdf.SetX(10)
pdf.CellFormat(20, 7, "Lohnart", "1", 0, "C", true, 0, "")
pdf.SetX(30)
pdf.CellFormat(70, 7, "Bezeichnung", "1", 0, "L", true, 0, "")
pdf.SetX(100)
pdf.CellFormat(25, 7, "Menge", "1", 0, "R", true, 0, "")
pdf.SetX(125)
pdf.CellFormat(25, 7, "%-Zuschlag", "1", 0, "R", true, 0, "")
pdf.SetX(150)
pdf.CellFormat(25, 7, "Betrag", "1", 0, "R", true, 0, "")
pdf.SetX(175)
pdf.CellFormat(30, 7, "Gesamt", "1", 0, "R", true, 0, "")
pdf.Ln(7)
}
func drawRow(pdf *fpdf.Fpdf, item LineItem) {
pdf.SetFont("DejaVu", "", 10)
pdf.SetX(10)
pdf.CellFormat(20, 7, item.Lohnart, "1", 0, "C", false, 0, "")
pdf.SetX(30)
pdf.CellFormat(70, 7, item.Beschreibung, "1", 0, "L", false, 0, "")
pdf.SetX(100)
pdf.CellFormat(25, 7, formatFloat(item.Anzahl), "1", 0, "R", false, 0, "")
pdf.SetX(125)
pdf.CellFormat(25, 7, formatFloat(item.Faktor), "1", 0, "R", false, 0, "")
pdf.SetX(150)
pdf.CellFormat(25, 7, formatCurrency(item.Betrag), "1", 0, "R", false, 0, "")
pdf.SetX(175)
pdf.CellFormat(30, 7, formatCurrency(item.Total()), "1", 0, "R", false, 0, "")
pdf.Ln(7)
}
func drawSummary(pdf *fpdf.Fpdf, total float64) {
pdf.SetFont("DejaVu", "B", 11)
pdf.SetY(pdf.GetY() + 5) // Etwas Abstand
pdf.SetX(120)
pdf.CellFormat(50, 8, "Gesamtbetrag:", "T", 0, "L", false, 0, "")
pdf.SetX(170)
pdf.CellFormat(30, 8, formatCurrency(total), "T", 0, "R", false, 0, "")
}
func drawSickdays(pdf *fpdf.Fpdf, sickdays []database.ListSickdaysForEmployeeRow) {
// 1. Abstand und Überschrift
pdf.Ln(10)
pdf.SetFont("DejaVu", "B", 11)
// Kein Rahmen, Zeilenumbruch nach der Zelle
pdf.CellFormat(0, 10, "Krankmeldungen", "", 1, "L", false, 0, "")
// 2. Fallback: Wenn keine Krankmeldungen vorhanden sind
if len(sickdays) == 0 {
pdf.SetFont("DejaVu", "", 10)
text := "- Keine Einträge vorhanden"
pdf.CellFormat(0, 6, text, "", 1, "L", false, 0, "")
pdf.Ln(5)
return
}
// 3. Liste zeichne
pdf.SetFont("DejaVu", "", 10)
for _, sick := range sickdays {
startStr := sick.Start.Format("02.01.2006")
endStr := sick.End.Format("02.01.2006")
// Formatierung: "- 01.01.2023 bis 05.01.2023: 4 Tage"
text := fmt.Sprintf("- %s bis %s: %d Tage", startStr, endStr, sick.Days)
// Zeichnen der Zeile
pdf.CellFormat(0, 6, text, "", 1, "L", false, 0, "")
}
// Kleiner Abstand nach der Liste
pdf.Ln(5)
}
func formatFloat(f float64) string {
p := message.NewPrinter(language.German)
str := p.Sprintf("%.2f", f)
return str //strings.Replace(str, ".", ",", 1)
}
func formatCurrency(val float64) string {
return formatFloat(val) + "€"
}