implement payroll generator v0.9

This commit is contained in:
Marcel Arndt
2025-11-06 11:38:59 +01:00
parent 319e7f3614
commit 4abe9e44d8
27 changed files with 4273 additions and 0 deletions
@@ -0,0 +1,122 @@
package excelread
import (
"business-engine/internal/database"
"fmt"
"os"
"slices"
"strings"
"time"
"github.com/gocarina/gocsv"
)
func sortSliceByDate[T any](slice []T, getDate func(T) time.Time) {
compare := func(a, b T) int {
aDate := getDate(a)
bDate := getDate(b)
if aDate.Before(bDate) {
return -1
}
if aDate.After(bDate) {
return 1
}
return 0
}
slices.SortFunc(slice, compare)
}
// Zu kalkulieren
// - Puenktlichkeitspraemie
// - Tourengeld Anzahl
// - Tourengeld Bonus
func GeneratePayrollStatement(data EmployeeDataRow, input PayrollInputData) PayrollStatement {
isKTW := slices.IndexFunc(input.Plannedtimes, func(pt database.ListPlannedTimesForEmployeeInPeriodRow) bool {
return strings.Contains(pt.Department.String, "KTW")
}) >= 0 && slices.IndexFunc(input.Plannedtimes, func(pt database.ListPlannedTimesForEmployeeInPeriodRow) bool {
return strings.Contains(pt.Department.String, "Leitstelle")
}) == -1
statement := PayrollStatement{
Nachname: data.Nachname,
Vorname: data.Vorname,
Verpflegungspauschale: data.AuszahlenVerpflegungspauschaleTage,
VertragStundenlohn: input.ContractSalaryHour,
Vertragsstunden: data.IstSollVertragsStunden,
GearbeiteteStunden: data.GearbeiteteStunden,
GearbeiteteTage: data.GearbeiteteTage,
Krankheitstage: data.Krankheitstage,
Ueberstunden: data.IstSollPlusMinusStunden,
Wochenendzuschlag15: data.AuszahlenZuschlag15Stunden,
Sonntagszuschlag50: data.AuszahlenZuschlag50Stunden,
Nachtzuschlag25: data.AuszahlenZuschlag25Stunden,
Nachtzuschlag40: data.AuszahlenZuschlag40Stunden,
}
if data.Krankheitstage > 0 || !isKTW {
statement.GesundheitsbonusAbzug = "ja"
}
if isKTW && data.GenommeneUrlaubstage > 0 {
statement.Urlaubsgeld = data.GenommeneUrlaubstage
}
if isKTW && data.AuszahlenAnzahlPSchein > 0 {
statement.PScheinZulage = data.AuszahlenAnzahlPSchein
}
if isKTW && input.Tourengeld > 0 {
statement.Tourengeld = float64(input.Tourengeld)
}
if isKTW && len(input.Worktimes) > 0 && len(input.Plannedtimes) > 0 {
ptGetStartDate := func(pt database.ListPlannedTimesForEmployeeInPeriodRow) time.Time {
return *pt.StartDate
}
wtGetStartDate := func(pt database.WorkTime) time.Time {
return *pt.StartDate
}
sortSliceByDate(input.Plannedtimes, ptGetStartDate)
sortSliceByDate(input.Worktimes, wtGetStartDate)
var employeeOnTime []EmployeeOnTime
for _, pt := range input.Plannedtimes {
for _, wt := range input.Worktimes {
if isSameDay(ptGetStartDate(pt), wtGetStartDate(wt)) && (wtGetStartDate(wt).Before(ptGetStartDate(pt)) || wtGetStartDate(wt) == ptGetStartDate(pt)) {
statement.PuenktlichkeitspraemieAnzahl++
employeeOnTime = append(employeeOnTime, EmployeeOnTime{
EmployeeId: fmt.Sprintf("%s %s", data.Vorname, data.Nachname),
WorkTime: *wt.StartDate,
PlannedTime: *pt.StartDate,
IsOnTime: true,
})
}
}
}
if data.Vorname == "David" && data.Nachname == "Hastedt" {
GenerateEmployeesOnTime(employeeOnTime)
}
}
return statement
}
func GenerateEmployeesOnTime(employeesontime []EmployeeOnTime) error {
file, err := os.Create("employees_on_time.csv")
if err != nil {
return err
}
defer file.Close()
if err := gocsv.MarshalFile(&employeesontime, file); err != nil {
return err
}
return nil
}
// isSameDay prüft, ob zwei Zeitpunkte am selben Kalendertag liegen.
func isSameDay(t1, t2 time.Time) bool {
return t1.Year() == t2.Year() && t1.Month() == t2.Month() && t1.Day() == t2.Day()
}
@@ -0,0 +1,210 @@
package excelread
import (
"encoding/csv"
"fmt"
"log"
"os"
"strconv"
"strings"
)
// mapRowToStruct konvertiert eine einzelne CSV-Zeile (als []string)
// in die EmployeeDataRow-Struct.
func mapRowToStruct(row []string) EmployeeDataRow {
data := EmployeeDataRow{}
data.Personalnummer = parseInt(row[0])
data.Nachname = strings.TrimSpace(row[1])
data.Vorname = strings.TrimSpace(row[2])
data.Vertragsart = strings.TrimSpace(row[3])
data.HatPersonenbefoerderungsschein = strings.TrimSpace(row[4])
data.HatRechtAufZuschlaege = strings.TrimSpace(row[5])
data.Vertragsstart = strings.TrimSpace(row[6])
data.VertragstageProWoche = parseFloat(row[7])
data.VertragsstundenProWoche = parseFloat(row[8])
data.VertragsstundenMax = strings.TrimSpace(row[9])
data.UrlaubstageProJahr = parseFloat(row[10])
data.GearbeiteteTage = parseFloat(row[11])
data.GearbeiteteStunden = parseFloat(row[12])
data.GenommeneUrlaubstage = parseFloat(row[13])
data.GenommeneUrlaubsstunden = parseFloat(row[14])
data.Krankheitstage = parseFloat(row[15])
data.Krankheitsstunden = parseFloat(row[16])
// data.KrankengeldTage = parseFloat(row[17])
// data.KrankengeldStunden = parseFloat(row[18])
// data.UnbezahlterUrlaubTage = parseFloat(row[19])
// data.UnbezahlterUrlaubStunden = parseFloat(row[20])
data.IstSollGesamtTage = parseFloat(row[17])
data.IstSollGesamtStunden = parseFloat(row[18])
data.IstSollVertragsTage = parseFloat(row[19])
data.IstSollVertragsStunden = parseFloat(row[20])
data.IstSollPlusMinusStunden = parseFloat(row[21])
data.BerechnungMehrstunden = parseFloat(row[22])
data.BerechnungUeberstunden = parseFloat(row[23])
data.BerechnungAnzahlPSchein = parseFloat(row[24])
data.BerechnungKrankOhneAUTage = parseFloat(row[25])
data.BerechnungKrankOhneAUStunden = parseFloat(row[26])
data.BerechnungNachtzuschlag25Tage = parseFloat(row[27])
data.BerechnungNachtzuschlag25Stunden = parseFloat(row[28])
data.BerechnungNachtzuschlag40Tage = parseFloat(row[29])
data.BerechnungNachtzuschlag40Stunden = parseFloat(row[30])
data.BerechnungSonntagszuschlag50Tage = parseFloat(row[31])
data.BerechnungSonntagszuschlag50Stunden = parseFloat(row[32])
data.BerechnungVerpflegungspauschale14Tage = parseFloat(row[33])
data.BerechnungSamstagszuschlag15Tage = parseFloat(row[34])
data.BerechnungSamstagszuschlag15Stunden = parseFloat(row[35])
data.BerechnungZeitausgleichStart = parseFloat(row[36])
data.BerechnungZeitausgleichPlusMinus = parseFloat(row[37])
data.BerechnungZeitausgleichAenderungen = parseFloat(row[38])
data.BerechnungZeitausgleichAuszahlen = parseFloat(row[39])
data.BerechnungZeitausgleichJetzt = parseFloat(row[40])
data.SaldenUrlaubStart = parseFloat(row[41])
data.SaldenUrlaubAbbau = parseFloat(row[42])
data.SaldenUrlaubKorrektur = parseFloat(row[43])
data.SaldenUrlaubJetzt = parseFloat(row[44])
data.SaldenMehrstunden = parseFloat(row[45])
data.SaldenUeberstunden = parseFloat(row[46])
data.AuszahlenFahrtkostenKm = parseFloat(row[47])
data.AuszahlenVerpflegungspauschaleTage = parseFloat(row[48])
data.AuszahlenAnzahlPSchein = parseFloat(row[49])
data.AuszahlenZuschlag25Stunden = parseFloat(row[50])
data.AuszahlenZuschlag40Stunden = parseFloat(row[51])
data.AuszahlenZuschlag50Stunden = parseFloat(row[52])
data.AuszahlenZuschlag14Stunden = parseFloat(row[53])
data.AuszahlenZuschlag15Stunden = parseFloat(row[54])
return data
}
// map from: July 2025
// func mapRowToStruct(row []string) EmployeeDataRow {
// data := EmployeeDataRow{}
// data.Personalnummer = parseInt(row[0])
// data.Nachname = strings.TrimSpace(row[1])
// data.Vorname = strings.TrimSpace(row[2])
// data.Vertragsart = strings.TrimSpace(row[3])
// data.HatPersonenbefoerderungsschein = strings.TrimSpace(row[4])
// data.HatRechtAufZuschlaege = strings.TrimSpace(row[5])
// data.Vertragsstart = strings.TrimSpace(row[6])
// data.VertragstageProWoche = parseFloat(row[7])
// data.VertragsstundenProWoche = parseFloat(row[8])
// data.VertragsstundenMax = strings.TrimSpace(row[9])
// data.UrlaubstageProJahr = parseFloat(row[10])
// data.GearbeiteteTage = parseFloat(row[11])
// data.GearbeiteteStunden = parseFloat(row[12])
// data.GenommeneUrlaubstage = parseFloat(row[13])
// data.GenommeneUrlaubsstunden = parseFloat(row[14])
// data.Krankheitstage = parseFloat(row[15])
// data.Krankheitsstunden = parseFloat(row[16])
// data.KrankengeldTage = parseFloat(row[17])
// data.KrankengeldStunden = parseFloat(row[18])
// data.UnbezahlterUrlaubTage = parseFloat(row[19])
// data.UnbezahlterUrlaubStunden = parseFloat(row[20])
// data.IstSollGesamtTage = parseFloat(row[21])
// data.IstSollGesamtStunden = parseFloat(row[22])
// data.IstSollVertragsTage = parseFloat(row[23])
// data.IstSollVertragsStunden = parseFloat(row[24])
// data.IstSollPlusMinusStunden = parseFloat(row[25])
// data.BerechnungMehrstunden = parseFloat(row[26])
// data.BerechnungUeberstunden = parseFloat(row[27])
// data.BerechnungAnzahlPSchein = parseFloat(row[28])
// data.BerechnungKrankOhneAUTage = parseFloat(row[29])
// data.BerechnungKrankOhneAUStunden = parseFloat(row[30])
// data.BerechnungNachtzuschlag25Tage = parseFloat(row[31])
// data.BerechnungNachtzuschlag25Stunden = parseFloat(row[32])
// data.BerechnungNachtzuschlag40Tage = parseFloat(row[33])
// data.BerechnungNachtzuschlag40Stunden = parseFloat(row[34])
// data.BerechnungSonntagszuschlag50Tage = parseFloat(row[35])
// data.BerechnungSonntagszuschlag50Stunden = parseFloat(row[36])
// data.BerechnungVerpflegungspauschale14Tage = parseFloat(row[37])
// data.BerechnungSamstagszuschlag15Tage = parseFloat(row[38])
// data.BerechnungSamstagszuschlag15Stunden = parseFloat(row[39])
// data.BerechnungZeitausgleichStart = parseFloat(row[40])
// data.BerechnungZeitausgleichPlusMinus = parseFloat(row[41])
// data.BerechnungZeitausgleichAenderungen = parseFloat(row[42])
// data.BerechnungZeitausgleichAuszahlen = parseFloat(row[43])
// data.BerechnungZeitausgleichJetzt = parseFloat(row[44])
// data.SaldenUrlaubStart = parseFloat(row[45])
// data.SaldenUrlaubAbbau = parseFloat(row[46])
// data.SaldenUrlaubJetzt = parseFloat(row[47])
// data.SaldenMehrstunden = parseFloat(row[48])
// data.SaldenUeberstunden = parseFloat(row[49])
// data.AuszahlenFahrtkostenKm = parseFloat(row[50])
// data.AuszahlenVerpflegungspauschaleTage = parseFloat(row[51])
// data.AuszahlenAnzahlPSchein = parseFloat(row[52])
// data.AuszahlenZuschlag25Stunden = parseFloat(row[53])
// data.AuszahlenZuschlag40Stunden = parseFloat(row[54])
// data.AuszahlenZuschlag50Stunden = parseFloat(row[55])
// data.AuszahlenZuschlag14Stunden = parseFloat(row[56])
// data.AuszahlenZuschlag15Stunden = parseFloat(row[57])
// return data
// }
func ReadEmployeeData() []EmployeeDataRow {
content, err := os.ReadFile("daten.csv")
if err != nil {
log.Fatalf("Fehler beim Lesen der Datei: %v", err)
}
reader := csv.NewReader(strings.NewReader(string(content)))
reader.Comma = ';'
reader.FieldsPerRecord = -1
records, err := reader.ReadAll()
if err != nil {
log.Fatalf("Fehler beim Parsen der CSV: %v", err)
}
if len(records) < 5 {
log.Fatal("Die CSV-Datei hat nicht genügend Zeilen.")
}
dataRows := records[3 : len(records)-1]
var employees []EmployeeDataRow
for i, row := range dataRows {
fmt.Printf("%v", len(row))
if row[1] != "Shahrasebi" {
employee := mapRowToStruct(row)
employees = append(employees, employee)
fmt.Printf("Zeile %d geladen: %s, %s\n", i+4, employee.Nachname, employee.Vorname)
}
}
fmt.Printf("\n--- Insgesamt %d Mitarbeiterdatensätze erfolgreich geladen. ---\n", len(employees))
return employees
}
// Helfer-Funktion, um Strings mit Komma als Dezimaltrennzeichen in float64 umzuwandeln.
func parseFloat(s string) float64 {
s = strings.TrimSpace(s)
if s == "" {
return 0.0
}
// Ersetze das Komma durch einen Punkt für die Standard-Konvertierung
s = strings.Replace(s, ",", ".", 1)
val, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0.0 // Bei Fehler einfach 0 zurückgeben
}
return val
}
// Helfer-Funktion, um einen String in einen Integer umzuwandeln.
func parseInt(s string) int {
s = strings.TrimSpace(s)
if s == "" {
return 0
}
val, err := strconv.Atoi(s)
if err != nil {
return 0
}
return val
}
@@ -0,0 +1,131 @@
package excelread
import (
"business-engine/internal/database"
"fmt"
"time"
)
// EmployeeDataRow repräsentiert eine einzelne, vollständige Datenzeile aus der CSV.
type EmployeeDataRow struct {
Personalnummer int `json:"personalnummer"`
Nachname string `json:"nachname"`
Vorname string `json:"vorname"`
Vertragsart string `json:"vertragsart"`
HatPersonenbefoerderungsschein string `json:"hatPersonenbefoerderungsschein"`
HatRechtAufZuschlaege string `json:"hatRechtAufZuschlaege"`
Vertragsstart string `json:"vertragsstart"`
VertragstageProWoche float64 `json:"vertragstageProWoche"`
VertragsstundenProWoche float64 `json:"vertragsstundenProWoche"`
VertragsstundenMax string `json:"vertragsstundenMax"`
UrlaubstageProJahr float64 `json:"urlaubstageProJahr"`
GearbeiteteTage float64 `json:"gearbeiteteTage"`
GearbeiteteStunden float64 `json:"gearbeiteteStunden"`
GenommeneUrlaubstage float64 `json:"genommeneUrlaubstage"`
GenommeneUrlaubsstunden float64 `json:"genommeneUrlaubsstunden"`
Krankheitstage float64 `json:"krankheitstage"`
Krankheitsstunden float64 `json:"krankheitsstunden"`
KrankengeldTage float64 `json:"krankengeldTage"`
KrankengeldStunden float64 `json:"krankengeldStunden"`
UnbezahlterUrlaubTage float64 `json:"unbezahlterUrlaubTage"`
UnbezahlterUrlaubStunden float64 `json:"unbezahlterUrlaubStunden"`
EinbehaltungGesamtTage float64 `json:"einbehaltungGesamtTage"`
EinbehaltungGesamtStunden float64 `json:"einbehaltungGesamtStunden"`
IstSollGesamtTage float64 `json:"istSollGesamtTage"`
IstSollGesamtStunden float64 `json:"istSollGesamtStunden"`
IstSollVertragsTage float64 `json:"istSollVertragsTage"`
IstSollVertragsStunden float64 `json:"istSollVertragsStunden"`
IstSollPlusMinusStunden float64 `json:"istSollPlusMinusStunden"`
BerechnungMehrstunden float64 `json:"berechnungMehrstunden"`
BerechnungUeberstunden float64 `json:"berechnungUeberstunden"`
BerechnungAnzahlPSchein float64 `json:"berechnungAnzahlPSchein"`
BerechnungKrankOhneAUTage float64 `json:"berechnungKrankOhneAUTage"`
BerechnungKrankOhneAUStunden float64 `json:"berechnungKrankOhneAUStunden"`
BerechnungNachtzuschlag25Tage float64 `json:"berechnungNachtzuschlag25Tage"`
BerechnungNachtzuschlag25Stunden float64 `json:"berechnungNachtzuschlag25Stunden"`
BerechnungNachtzuschlag40Tage float64 `json:"berechnungNachtzuschlag40Tage"`
BerechnungNachtzuschlag40Stunden float64 `json:"berechnungNachtzuschlag40Stunden"`
BerechnungSonntagszuschlag50Tage float64 `json:"berechnungSonntagszuschlag50Tage"`
BerechnungSonntagszuschlag50Stunden float64 `json:"berechnungSonntagszuschlag50Stunden"`
BerechnungVerpflegungspauschale14Tage float64 `json:"berechnungVerpflegungspauschale14Tage"`
BerechnungSamstagszuschlag15Tage float64 `json:"berechnungSamstagszuschlag15Tage"`
BerechnungSamstagszuschlag15Stunden float64 `json:"berechnungSamstagszuschlag15Stunden"`
BerechnungZeitausgleichStart float64 `json:"berechnungZeitausgleichStart"`
BerechnungZeitausgleichPlusMinus float64 `json:"berechnungZeitausgleichPlusMinus"`
BerechnungZeitausgleichAenderungen float64 `json:"berechnungZeitausgleichAenderungen"`
BerechnungZeitausgleichAuszahlen float64 `json:"berechnungZeitausgleichAuszahlen"`
BerechnungZeitausgleichJetzt float64 `json:"berechnungZeitausgleichJetzt"`
SaldenUrlaubStart float64 `json:"saldenUrlaubStart"`
SaldenUrlaubAbbau float64 `json:"saldenUrlaubAbbau"`
SaldenUrlaubKorrektur float64 `json:"saldenUrlaubKorrektur"`
SaldenUrlaubJetzt float64 `json:"saldenUrlaubJetzt"`
SaldenMehrstunden float64 `json:"saldenMehrstunden"`
SaldenUeberstunden float64 `json:"saldenUeberstunden"`
AuszahlenFahrtkostenKm float64 `json:"auszahlenFahrtkostenKm"`
AuszahlenVerpflegungspauschaleTage float64 `json:"auszahlenVerpflegungspauschaleTage"`
AuszahlenAnzahlPSchein float64 `json:"auszahlenAnzahlPSchein"`
AuszahlenZuschlag25Stunden float64 `json:"auszahlenZuschlag25Stunden"`
AuszahlenZuschlag40Stunden float64 `json:"auszahlenZuschlag40Stunden"`
AuszahlenZuschlag50Stunden float64 `json:"auszahlenZuschlag50Stunden"`
AuszahlenZuschlag14Stunden float64 `json:"auszahlenZuschlag14Stunden"`
AuszahlenZuschlag15Stunden float64 `json:"auszahlenZuschlag15Stunden"`
}
type PayrollInputData struct {
ContractSalaryHour float64
Tourengeld float64
Worktimes []database.WorkTime
Plannedtimes []database.ListPlannedTimesForEmployeeInPeriodRow
}
func NewPayrollInputData(salaryHour float64, tourengeld float64, worktimes []database.WorkTime, plannedtimes []database.ListPlannedTimesForEmployeeInPeriodRow) PayrollInputData {
return PayrollInputData{
ContractSalaryHour: salaryHour,
Tourengeld: tourengeld,
Worktimes: worktimes,
Plannedtimes: plannedtimes,
}
}
type PayrollStatement struct {
Nachname string `json:"nachname" csv:"Nachname"`
Vorname string `json:"vorname" csv:"Vorname"`
Verpflegungspauschale float64 `json:"verpflegungspauschale" csv:"Verpflegungspauschale"`
VertragStundenlohn float64 `json:"stundenlohn" csv:"Vertr. Stundenlohn"`
Vertragsstunden float64 `json:"vertragsstunden" csv:"Arbeitsstunden lt. Vertrag"`
GearbeiteteStunden float64 `json:"gearbeiteteStunden" csv:"Gearbeitet Stunden"`
GearbeiteteTage float64 `json:"gearbeiteteTage" csv:"Gearbeitet Tage"`
PuenktlichkeitspraemieAnzahl float64 `json:"puenktlichkeitspraemieAnzahl" csv:"Pünktlichkeitsprämie anzahl"`
Krankheitstage float64 `json:"krankheitstage" csv:"Krankheitstage x 8 Stunden"`
Ueberstunden float64 `json:"ueberstunden" csv:"davon Überstunden"`
Tourengeld float64 `json:"tourengeld" csv:"Tourengeld Anzahl"`
TourengeldBonus float64 `json:"tourengeldBonus" csv:"Tourengeld-Bonus (Platz 1-3, EUR)"`
GesundheitsbonusAbzug string `json:"gesundheitsbonusAbzug" csv:"Gesundheitsbonus abzuziehen? (ja/nein)"`
Urlaubsgeld float64 `json:"urlaubsgeld" csv:"Urlaubsgeld (44 EUR pro tag)"`
PScheinZulage float64 `json:"pScheinZulage" csv:"P-schein Zulage"`
Wochenendzuschlag15 float64 `json:"wochenendzuschlag15" csv:"Wochenendzuschlag 15%"`
Sonntagszuschlag50 float64 `json:"sonntagszuschlag50" csv:"Sonntagszuschlag 50%"`
Feiertagszuschlag125 float64 `json:"feiertagszuschlag125" csv:"Feiertagszuschlag 125%"`
Nachtzuschlag25 float64 `json:"nachtzuschlag25" csv:"Nachtzuschlag 25%"`
Nachtzuschlag40 float64 `json:"nachtzuschlag40" csv:"Nachtzuschlag 40%"`
}
type EmployeeOnTime struct {
EmployeeId string `csv:"EmployeeId"`
WorkTime time.Time `csv:"Arbeitszeit"`
PlannedTime time.Time `csv:"Schichtplanung"`
IsOnTime bool `csv:"Pünktlich?"`
}
func NewEmployeeOnTime(employeeId string, worktime, plannedtime time.Time) EmployeeOnTime {
loc, err := time.LoadLocation("Europe/Berlin")
if err != nil {
fmt.Println("Fehler beim Laden der Zeitzone:", err)
}
return EmployeeOnTime{
EmployeeId: employeeId,
WorkTime: worktime.In(loc),
PlannedTime: plannedtime.In(loc),
IsOnTime: worktime.Before(plannedtime) || worktime == plannedtime,
}
}