KetteReports/Export/Personio Exporte für Arbeitszeit und Abwesenheiten neu
SupportOID=167814
This commit is contained in:
@@ -24,7 +24,15 @@ namespace KetteReports.Export
|
||||
public override QueryDC CreateQuery(QueryDC query)
|
||||
{
|
||||
QueryDC result = null;
|
||||
if (query.Oid == 1004)
|
||||
if (query.Title != null && query.Title.Contains("Personio") && query.Title.Contains("Abwesenheit"))
|
||||
{
|
||||
result = PersonioAbwesenheitExporter.CreateQuery(query);
|
||||
}
|
||||
else if (query.Title != null && query.Title.Contains("Personio") && query.Title.Contains("Anwesenheit"))
|
||||
{
|
||||
result = PersonioAnwesenheitExporter.CreateQuery(query);
|
||||
}
|
||||
else if (query.Oid == 1004)
|
||||
{
|
||||
result = DatevExporter.CreateDifferenzQuery(query);
|
||||
}
|
||||
|
||||
99
ReportImp/KetteReports/Export/PersonioAbwesenheitExporter.cs
Normal file
99
ReportImp/KetteReports/Export/PersonioAbwesenheitExporter.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using BeWo.Data.Access;
|
||||
using BS.Shared.DataContracts;
|
||||
|
||||
namespace KetteReports.Export
|
||||
{
|
||||
/// Exportiert die Mitarbeiter-Abwesenheiten im Personio-Importformat "Abwesenheitsperioden" als CSV (Trennzeichen ";").
|
||||
public class PersonioAbwesenheitExporter
|
||||
{
|
||||
private const string Header =
|
||||
"E-Mail;Art der Abwesenheit;Startdatum;Enddatum;Startzeit;Endzeit;Halber Tag erster Tag;Halber Tag letzter Tag";
|
||||
|
||||
public static QueryDC CreateQuery(QueryDC query)
|
||||
{
|
||||
var monatStart = PersonioExportUtils.GetMonthStart(query);
|
||||
var monatEnde = monatStart.AddMonths(1).AddDays(-1);
|
||||
|
||||
query.FileName = string.Format("Personio_Abwesenheitsperioden_{0:yyyyMM}.csv", monatStart);
|
||||
query.QueryResult = BuildCsv(monatStart, monatEnde);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
private static string BuildCsv(DateTime monatStart, DateTime monatEnde)
|
||||
{
|
||||
var identifierByEmployeeOid = PersonioExportUtils.BuildEmployeeIdentifierByOid();
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(Header);
|
||||
|
||||
// FindAbsenceTimes liefert auch Abwesenheiten ohne Enddatum (End IS NULL) ohne untere
|
||||
// Datumsgrenze -> zusätzlich auf echte Datumsüberlappung mit dem Monat filtern.
|
||||
// Erst alle Einträge eines Mitarbeiters, innerhalb chronologisch nach Startzeitpunkt.
|
||||
var absenceTimes = DAOFactory.SearchDAO.FindAbsenceTimes(monatStart, monatEnde)
|
||||
.Where(a => a.EmployeeOid.HasValue && a.Start.HasValue
|
||||
&& a.Start.Value.Date <= monatEnde.Date
|
||||
&& (a.End ?? a.Start.Value).Date >= monatStart.Date)
|
||||
.OrderBy(a => a.EmployeeOid.Value).ThenBy(a => a.Start.Value).ToList();
|
||||
foreach (var absence in absenceTimes)
|
||||
{
|
||||
// Nur Mitarbeiter-Abwesenheiten (mit EmployeeOid), keine Klienten-Abwesenheiten.
|
||||
if (!absence.EmployeeOid.HasValue || !absence.Start.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string mitarbeiter;
|
||||
identifierByEmployeeOid.TryGetValue(absence.EmployeeOid.Value, out mitarbeiter);
|
||||
|
||||
var start = absence.Start.Value;
|
||||
var hatEnde = absence.End.HasValue;
|
||||
var end = absence.End ?? start;
|
||||
|
||||
var artDerAbwesenheit = absence.AbsenceReason != null ? absence.AbsenceReason.Description : string.Empty;
|
||||
|
||||
// Eine gesetzte Uhrzeit (Zeitanteil != 00:00) kennzeichnet einen halben Tag:
|
||||
// Startzeit -> erster Tag halb, Endzeit -> letzter Tag halb. Ohne Uhrzeit ganztägig.
|
||||
var hatStartzeit = start.TimeOfDay != TimeSpan.Zero;
|
||||
var hatEndzeit = hatEnde && end.TimeOfDay != TimeSpan.Zero;
|
||||
|
||||
var startzeit = hatStartzeit ? start.ToString("HH:mm") : string.Empty;
|
||||
var endzeit = hatEndzeit ? end.ToString("HH:mm") : string.Empty;
|
||||
|
||||
// Halbe Tage nicht bei "Fortbildung" ausweisen. Bei eintägiger Abwesenheit nur "Halber Tag letzter Tag" setzen.
|
||||
var istFortbildung = (artDerAbwesenheit ?? string.Empty).IndexOf("Fortbildung", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
var istEintaegig = start.Date == end.Date;
|
||||
|
||||
var halberTagErster = string.Empty;
|
||||
var halberTagLetzter = string.Empty;
|
||||
if (!istFortbildung)
|
||||
{
|
||||
if (istEintaegig)
|
||||
{
|
||||
halberTagLetzter = (hatStartzeit || hatEndzeit) ? "1" : string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
halberTagErster = hatStartzeit ? "1" : string.Empty;
|
||||
halberTagLetzter = hatEndzeit ? "1" : string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.Append(PersonioExportUtils.BuildCsvLine(
|
||||
mitarbeiter ?? string.Empty,
|
||||
artDerAbwesenheit ?? string.Empty,
|
||||
start.ToString("yyyy-MM-dd"),
|
||||
end.ToString("yyyy-MM-dd"),
|
||||
startzeit,
|
||||
endzeit,
|
||||
halberTagErster, // Halber Tag erster Tag
|
||||
halberTagLetzter)); // Halber Tag letzter Tag
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
86
ReportImp/KetteReports/Export/PersonioAnwesenheitExporter.cs
Normal file
86
ReportImp/KetteReports/Export/PersonioAnwesenheitExporter.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using BeWo.Data.Access;
|
||||
using BS.Shared.Core;
|
||||
using BS.Shared.DataContracts;
|
||||
|
||||
namespace KetteReports.Export
|
||||
{
|
||||
/// Exportiert die geleisteten Arbeitszeiten im Personio-Importformat "Anwesenheitsperioden" als CSV (Trennzeichen ";").
|
||||
/// Quelle: ServiceRecords der Leistungskategorie "Arbeitszeit". Innerhalb dieser Kategorie gibt es die Leistungen "Arbeitszeit" und "Pause";
|
||||
/// jede Buchung wird als eigene Anwesenheitsperiode exportiert.
|
||||
|
||||
public class PersonioAnwesenheitExporter
|
||||
{
|
||||
private const string Kategorie = "Arbeitszeit";
|
||||
private const string Header = "E-Mail;Start (z. B. 2024-04-01 09:00);Ende (z. B. 2024-04-01 18:00);Art der Periode;Kommentar;Projekt;Unterprojekt";
|
||||
|
||||
public static QueryDC CreateQuery(QueryDC query)
|
||||
{
|
||||
var monatStart = PersonioExportUtils.GetMonthStart(query);
|
||||
query.FileName = string.Format("Personio_Anwesenheitsperioden_{0:yyyyMM}.csv", monatStart);
|
||||
query.QueryResult = BuildCsv(monatStart);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
private static string BuildCsv(DateTime monatStart)
|
||||
{
|
||||
// Oberes Ende exklusiv: der 2-arg-DateTimeSpan-Konstruktor lässt die Uhrzeit auf 00:00,
|
||||
// FindServiceRecordsInSpan verwendet Between(Start, StartDateTime, EndDateTime).
|
||||
var monatEndeExklusiv = monatStart.AddMonths(1);
|
||||
var identifierByEmployeeOid = PersonioExportUtils.BuildEmployeeIdentifierByOid();
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(Header);
|
||||
|
||||
// Erst alle Einträge eines Mitarbeiters, innerhalb chronologisch nach Startzeitpunkt.
|
||||
var serviceRecords = DAOFactory.SearchDAO.FindServiceRecordsInSpan(new DateTimeSpan(monatStart, monatEndeExklusiv), null)
|
||||
.Where(sr => sr.EmployeeOid.HasValue && sr.Start.HasValue && sr.End.HasValue)
|
||||
.OrderBy(sr => sr.EmployeeOid.Value).ThenBy(sr => sr.Start.Value).ToList();
|
||||
|
||||
foreach (var serviceRecord in serviceRecords)
|
||||
{
|
||||
if (!serviceRecord.EmployeeOid.HasValue || !serviceRecord.Start.HasValue || !serviceRecord.End.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var serviceDescription = serviceRecord.ServiceDescription;
|
||||
if (serviceDescription == null || serviceDescription.ServiceCategory == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Nur Leistungen der Kategorie "Arbeitszeit" berücksichtigen.
|
||||
var kategorieName = (serviceDescription.ServiceCategory.Name ?? string.Empty).Trim();
|
||||
if (!kategorieName.Equals(Kategorie, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string mitarbeiter;
|
||||
identifierByEmployeeOid.TryGetValue(serviceRecord.EmployeeOid.Value, out mitarbeiter);
|
||||
|
||||
// Leistung "Pause" -> Art der Periode "Pause", sonst "Arbeit".
|
||||
var leistung = (serviceDescription.Name ?? string.Empty).Trim();
|
||||
var artDerPeriode = leistung.Equals("Pause", StringComparison.OrdinalIgnoreCase) ? "Pause" : "Arbeit";
|
||||
|
||||
var start = serviceRecord.Start.Value;
|
||||
var end = serviceRecord.End.Value;
|
||||
|
||||
sb.AppendLine();
|
||||
sb.Append(PersonioExportUtils.BuildCsvLine(
|
||||
mitarbeiter ?? string.Empty,
|
||||
start.ToString("yyyy-MM-dd HH:mm"),
|
||||
end.ToString("yyyy-MM-dd HH:mm"),
|
||||
artDerPeriode,
|
||||
serviceRecord.Notice ?? string.Empty, // Kommentar (ServiceRecord.Notice)
|
||||
string.Empty, // Projekt - nicht benötigt
|
||||
string.Empty)); // Unterprojekt - nicht benötigt
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
107
ReportImp/KetteReports/Export/PersonioExportUtils.cs
Normal file
107
ReportImp/KetteReports/Export/PersonioExportUtils.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using BeWo.Data.Access;
|
||||
using BeWo.Data.Entities;
|
||||
using BS.Shared;
|
||||
using BS.Shared.DataContracts;
|
||||
|
||||
namespace KetteReports.Export
|
||||
{
|
||||
|
||||
// Gemeinsame Hilfsfunktionen für die beiden Personio-Exporte (Abwesenheits- und Anwesenheitsperioden).
|
||||
internal static class PersonioExportUtils
|
||||
{
|
||||
|
||||
public static string GetEmail(Employee employee)
|
||||
{
|
||||
var contacts = employee?.Person?.Contacts;
|
||||
if (contacts == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var mail = contacts.FirstOrDefault(c => c.Type == ContactType.business_Mail && !string.IsNullOrWhiteSpace(c.Value))
|
||||
?? contacts.FirstOrDefault(c => c.Type == ContactType.private_Mail && !string.IsNullOrWhiteSpace(c.Value));
|
||||
|
||||
return mail?.Value ?? string.Empty;
|
||||
}
|
||||
|
||||
// Baut ein Nachschlagewerk EmployeeOid -> Anzeige-Kennung: E-Mail, sonst "Vorname Nachname".
|
||||
// GetAll (nicht GetAllActive), damit auch inaktive/ausgeschiedene Mitarbeiter mit Datensaetzen im Monat aufgeloest werden.
|
||||
public static Dictionary<long, string> BuildEmployeeIdentifierByOid()
|
||||
{
|
||||
var result = new Dictionary<long, string>();
|
||||
var employees = DAOFactory.GenericDAO.GetAll<Employee>();
|
||||
foreach (var employee in employees)
|
||||
{
|
||||
if (!employee.Oid.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var email = GetEmail(employee);
|
||||
result[employee.Oid.Value] = !string.IsNullOrWhiteSpace(email) ? email : GetFullName(employee);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Liefert "Vorname Nachname" des Mitarbeiters (leer, falls keine Person hinterlegt).
|
||||
public static string GetFullName(Employee employee)
|
||||
{
|
||||
var person = employee?.Person;
|
||||
if (person == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return $"{person.FirstName} {person.LastName}".Trim();
|
||||
}
|
||||
// Liest den Abrechnungsmonat aus dem ersten Query-Parameter (wie im DatevExporter) und liefert den ersten Tag dieses Monats zurück.
|
||||
public static DateTime GetMonthStart(QueryDC query)
|
||||
{
|
||||
var dt = DateTime.Now;
|
||||
if (query?.Parameter != null && query.Parameter.Count > 0 && query.Parameter[0].Value != null)
|
||||
{
|
||||
DateTime.TryParse(query.Parameter[0].Value.ToString(), out dt);
|
||||
}
|
||||
|
||||
return new DateTime(dt.Year, dt.Month, 1);
|
||||
}
|
||||
|
||||
// Maskiert ein einzelnes CSV-Feld (Trennzeichen ";") gemäß RFC 4180.
|
||||
public static string CsvField(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (value.Contains(";") || value.Contains("\"") || value.Contains("\n") || value.Contains("\r"))
|
||||
{
|
||||
return "\"" + value.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Setzt eine CSV-Zeile aus den übergebenen Feldern zusammen (Trennzeichen ";").
|
||||
public static string BuildCsvLine(params string[] fields)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
for (int i = 0; i < fields.Length; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(";");
|
||||
}
|
||||
|
||||
sb.Append(CsvField(fields[i]));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,6 +136,9 @@
|
||||
</Compile>
|
||||
<Compile Include="Export\CustomDataExporter.cs" />
|
||||
<Compile Include="Export\DatevExporter.cs" />
|
||||
<Compile Include="Export\PersonioExportUtils.cs" />
|
||||
<Compile Include="Export\PersonioAbwesenheitExporter.cs" />
|
||||
<Compile Include="Export\PersonioAnwesenheitExporter.cs" />
|
||||
<Compile Include="Finanzauswertung.cs">
|
||||
<SubType>Component</SubType>
|
||||
</Compile>
|
||||
|
||||
Reference in New Issue
Block a user