package genpdf import ( 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) 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.GesundheitsbonusAbzug == "" { items = append(items, LineItem{ Lohnart: "1032", Beschreibung: "Gesundheitsbonus", Anzahl: 1, Betrag: 200.0, }) } 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 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) + "€" }