Files
BeWoPlaner/ReportImp/TeamPfeil/Invoicing/CustomInvoiceCreation.cs
2026-07-07 16:13:35 +02:00

356 lines
19 KiB
C#

using BeWo.Data.Access;
using BeWo.Data.Entities;
using BeWo.Service.DCEntityMapper;
using BeWo.Service.Invoicing;
using BeWo.Service.Plugins;
using BS.Shared;
using BS.Shared.Core;
using BS.Shared.DataContracts;
using BS.Shared.Extensions;
using BS.Shared.Services;
using DevExpress.Data.Helpers;
using DevExpress.Utils.About;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Windows.Markup.Localizer;
using System.Windows.Media.Media3D;
using TeamPfeil.Korridorberechnung;
namespace TeamPfeil.Invoicing
{
public class CustomInvoiceCreation : InvoiceCreation
{
// Noch die Prüfung auf Übererbringung wieder einfügen
public override void SetInvoiceId(ServiceInvoice invoice)
{
invoice.InvoiceBase.InvoiceId = "Qualifikation";
invoice.InvoiceBase.InvoiceTypeText = "RechnungQualifikation";
}
public override void SetInvoiceItems(ServiceInvoice invoice,
ServiceInvoicePeriod serviceInvoicePeriod,
SupportConceptApprovalPeriodDC supportConceptApprovalPeriod,
CostBearer2SupportConcept cb2sc,
DateTimeSpan invoicePeriod,
List<ServiceRecordDC> serviceRecords)
{
Calculations calc = PluginLoader.FindClass<Calculations>(_SpecificID) ?? Calculations.GetInstance(_SpecificID);
List<ServiceRecordDC> serviceRecordsInPeriod = serviceRecords.Where(
i =>
{
bool valid = i.Start.Value.InBetween(serviceInvoicePeriod.Start.Value,
serviceInvoicePeriod.End.Value, true);
//if (valid && serviceInvoicePeriod.SupportConceptApprovalPeriod != null && serviceInvoicePeriod.SupportConceptApprovalPeriod.ServiceCategory != null)
// valid = serviceInvoicePeriod.SupportConceptApprovalPeriod.ServiceCategory.Oid ==
// i.ServiceDescription.Category.ServiceCategoryOid;
return valid;
}).ToList();
if (serviceRecordsInPeriod.Count > 0)
{
var absenceTimes = MapperFactory.AbsenceTimeDC_AbsenceTime.MapToNewDCs(cb2sc.SupportConcept.Customer.AbsenceTimes);
Dictionary<DateTimeSpan, decimal> span2Hours = calc.GetAbsencesTimesNotBillable(absenceTimes);
serviceInvoicePeriod.HoursNotBillableAbsence +=
calc.GetHoursNotBillableDueToAbsenceTimes(supportConceptApprovalPeriod,
cb2sc.CostBearer.CostRatePeriods.MapToNewDCs(),
serviceRecordsInPeriod,
span2Hours,
true, true);
var itemList = CreateInvoiceItems(serviceRecordsInPeriod, invoicePeriod, serviceInvoicePeriod.SupportConceptApprovalPeriod, calc);
serviceInvoicePeriod.InvoiceItemList.AddRange(itemList);
}
}
public override IList<InvoiceItem> CreateInvoiceItems(List<ServiceRecordDC> serviceRecordsInPeriod, DateTimeSpan invoicePeriod, SupportConceptApprovalPeriod scap, Calculations calc)
{
var list = new List<InvoiceItem>();
foreach (ServiceRecordDC iServiceRecord in serviceRecordsInPeriod)
{
if (iServiceRecord.ServiceDescription.Category.IsBillable)
{
if (!iServiceRecord.AlreadyAccounted)
{
InvoiceItem invoiceItem = CreateInvoiceItem(iServiceRecord, scap, calc);
if (invoiceItem != null)
{
// Wenn das Item ganz normal abgerechnet wird, einfach normal zur Liste
if (invoiceItem.CustomString5 != "Mischitem")
list.Add(invoiceItem);
else
{
// Ist es ein Mischitem, müssen zwei Items erstellt werden. Eins mit dem Mitarbeiterstundensatz und eins mit dem Leistungsstundensatz
// invoiceItem.CustomNumber1: Stunden für den MA-Stundensatz
// invoiceItem.CustomNumber2: MA-Stundensatz
// invoiceItem.CustomString2: Leistungsname für MA-Stundensatz
// invoiceItem.CustomNumber3: Stunden für den Leistungs-Stundensatz
// invoiceItem.CustomNumber4: Leistungs-Stundensatz
// invoiceItem.CustomString4: Leistungsname für Leistungsstundensatz
var item1 = invoiceItem;
var item2 = CopyInvoiceItem(invoiceItem);
item1.UnitCount = invoiceItem.CustomNumber1;
item1.AmountPerUnit = invoiceItem.CustomNumber2;
item1.AmountTotal = item1.UnitCount * invoiceItem.AmountPerUnit;
item1.GrossAmountTotal = item1.AmountTotal;
item1.ItemDescription = item1.CustomString2;
list.Add(item1);
item2.UnitCount = invoiceItem.CustomNumber3;
item2.AmountPerUnit = invoiceItem.CustomNumber4;
item2.AmountTotal = item2.UnitCount * invoiceItem.AmountPerUnit;
item2.GrossAmountTotal = item2.AmountTotal;
item2.ItemDescription = item1.CustomString4;
list.Add(item2);
}
}
iServiceRecord.AlreadyAccounted = true;
}
}
}
return CreateSummaryInvoiceItems(list, invoicePeriod, scap, calc);
}
public override InvoiceItem CreateInvoiceItem(ServiceRecordDC iServiceRecord, SupportConceptApprovalPeriod scap, Calculations calc)
{
var invoiceItem = new InvoiceItem();
//Dictionary<long, string> empQuali = new Dictionary<long, string>();
BillingData bd = calc.GetBillingData(iServiceRecord);
decimal stundensatz = 0;
// Bis Ende 2025 richtet sich die erbrachte Kategorie nach dem, was dokumentiert wurde.
long qualiOid;
if (iServiceRecord.Start.HasValue && iServiceRecord.Start.Value.Date < new DateTime(2026, 01, 01))
{
if (iServiceRecord.ServiceDescription.CategoryName.Contains("1"))
{
qualiOid = 2118;
stundensatz = GetStundensatz(scap.CostBearer2SupportConcept, qualiOid, iServiceRecord.Start);
invoiceItem.ItemDescription = "Qualifikationsniveau 1: ";
}
else if (iServiceRecord.ServiceDescription.CategoryName.Contains("2"))
{
qualiOid = 2119;
stundensatz = GetStundensatz(scap.CostBearer2SupportConcept, qualiOid, iServiceRecord.Start);
invoiceItem.ItemDescription = "Qualifikationsniveau 2: ";
}
else if (iServiceRecord.ServiceDescription.CategoryName.Contains("3"))
{
qualiOid = 2120;
stundensatz = GetStundensatz(scap.CostBearer2SupportConcept, qualiOid, iServiceRecord.Start);
invoiceItem.ItemDescription = "Qualifikationsniveau 3: ";
}
else if (iServiceRecord.ServiceDescription.CategoryName.Contains("4"))
{
qualiOid = 2121;
stundensatz = GetStundensatz(scap.CostBearer2SupportConcept, qualiOid, iServiceRecord.Start);
invoiceItem.ItemDescription = "Qualifikationsniveau 4: ";
}
}
// Ab 2026 wird das abgerechnet, was dokumentiert wird.
// Sonderfall: Korridor
// Korridor wird Thema, wenn die bewilligte Leistungsform sich von der Qualifikation der dokumentierenden Person unterscheidet.
// Es wird dann erstmal der Stundensatz der dokumentierten Leistung genommen, solange ein Schwellwert an Stunden nicht überschritten wird.
// Wenn qual.MA unterstützend dokumentiert, dürfen es 30 % der bew.Stunden sein
// Wenn unt.MA qualifiziert dokumentiert, dürfen es 20 % der bew.Stunden sein
// Wenn diese %-Werte überschritten sind, darf in der Abrechnung nur noch der Stundensatz genommen werden,
// den der Korridormitarbeiter eigentlich hat (also qualfi.abhäng. Stundensatz im Kostenträger, der der Qualifikation des Mitarbeiters entspricht)
else
{
bool korridorAufgebraucht = false; // Wenn true, wird statt dem Leistungsstundensatz der MA-Stundensatz genommen
// Mischitem = ServiceRecord kann nur teilweise mit Leistungsstundensatz abgerechnet werden, der Rest muss aber mit MA-Qual--Std.satz abgerechnet werden
// In dem Fall werden im Item beide Teilstunden und beide jeweiligem Stundensätze gesetzt und im Nachgang wird ein weiteres Item erzeugt (in CreateInvoiceItems)
bool mischitem = false;
// KORRIDOR ERMITTELN
var scapDC = MapperFactory.SupportConceptApprovalPeriodDC_SupportConceptApprovalPeriod.MapToNewDC(scap);
var gesamtBewilligt = KorridorberechnungHelper.BerechneStundenBewilligtGesamt(scapDC);
double korridorbudget = 0;
double genutztesKorridorbudget = 0;
double rest = 0;
if (scap.ServiceCategory.Name.Contains("Qualifizierte Assistenz"))
{
korridorbudget = KorridorberechnungHelper.BerechneBudgetFuerQualAss(gesamtBewilligt);
genutztesKorridorbudget = KorridorberechnungHelper.BerechneGenutzteKorridorstunden(iServiceRecord.SupportConcept.SupportConceptOid, scapDC.ServiceCategory,
korridorbudget, iServiceRecord.Start.Value.AddTicks(-1), true);
rest = korridorbudget - genutztesKorridorbudget;
}
else if (scap.ServiceCategory.Name.Contains("Unterstützende Assistenz"))
{
korridorbudget = KorridorberechnungHelper.BerechneBudgetFuerUnterstAss(gesamtBewilligt);
genutztesKorridorbudget = KorridorberechnungHelper.BerechneGenutzteKorridorstunden(iServiceRecord.SupportConcept.SupportConceptOid, scapDC.ServiceCategory,
korridorbudget, iServiceRecord.Start.Value.AddTicks(-1), false);
rest = korridorbudget - genutztesKorridorbudget;
}
// Wenn das Item noch etwas Restbudget hat, aber es nicht ganz ausreicht, um alles abzudecken, muss ein Mischitem erzeugt werden.
// Es muss also der Teil, der noch verfügbar ist, mit dem Leistungsstundensatz verrechnet werden und der Rest mit der Qualifikation
// Dazu werden der jeweilige UnitCount und der jeweilige Stundensatz in CustomNumbers gespeichert, um in CreateInvoiceItems ein zweites Item zu machen
if (rest > 0 && rest - Convert.ToDouble(iServiceRecord.RoundedDuration) / 60 < 0)
{
decimal zeitNormalAbrechenbar = Convert.ToDecimal(rest);
decimal zeitMitMitarbeiterStundensatz = iServiceRecord.RoundedDuration / 60 - Convert.ToDecimal(rest);
// Wenn Mischitem, dann UnitCounts zwischenspeichern, um später aufzuteilen
invoiceItem.CustomNumber1 = zeitMitMitarbeiterStundensatz;
invoiceItem.CustomNumber3 = zeitNormalAbrechenbar;
mischitem = true;
invoiceItem.CustomString5 = "Mischitem";
}
// Wenn der Rest eh schon unter 0 ist, wird einfach mit dem MA-Stundensatz abgerechnet
else if (rest < 0)
korridorAufgebraucht = true;
if (korridorAufgebraucht || mischitem) // Stundensatz der Mitarbeitendenqualifikation
{
var qualifikation = iServiceRecord.Employee.ValueListEntries.FirstOrDefault(v => v.Value == ValueListEntryType.StaffQualificationsType);
if (qualifikation.Key != null)
{
qualiOid = qualifikation.Key;
if (qualiOid == 2118) // QA 1
{
stundensatz = GetStundensatz(scap.CostBearer2SupportConcept, qualiOid, iServiceRecord.Start);
invoiceItem.ItemDescription = "QA 1: ";
}
else if (qualiOid == 2119) // QA 2
{
stundensatz = GetStundensatz(scap.CostBearer2SupportConcept, qualiOid, iServiceRecord.Start);
invoiceItem.ItemDescription = "QA 2: ";
}
else if (qualiOid == 2120) // UA 3
{
stundensatz = GetStundensatz(scap.CostBearer2SupportConcept, qualiOid, iServiceRecord.Start);
invoiceItem.ItemDescription = "UA 3: ";
}
else if (qualiOid == 2121) // UA 4
{
stundensatz = GetStundensatz(scap.CostBearer2SupportConcept, qualiOid, iServiceRecord.Start);
invoiceItem.ItemDescription = "UA 4: ";
}
if (mischitem) // Wenn Mischitem, dann Stundensatz der Person + dessen Qualifikation zwischenspeichern, um später aufzuteilen
{
invoiceItem.CustomNumber2 = stundensatz;
invoiceItem.CustomString2 = invoiceItem.ItemDescription;
}
}
}
if (!korridorAufgebraucht || mischitem) // Stundensatz, der der dokumentierten Leistung entspricht
{
var leistung = iServiceRecord.ServiceDescription.Name;
switch (leistung)
{
case "QA 1": stundensatz = GetStundensatz(scap.CostBearer2SupportConcept, 2118, iServiceRecord.Start); invoiceItem.ItemDescription = "QA 1: "; break;
case "QA 2": stundensatz = GetStundensatz(scap.CostBearer2SupportConcept, 2119, iServiceRecord.Start); invoiceItem.ItemDescription = "QA 2: "; break;
case "UA 3": stundensatz = GetStundensatz(scap.CostBearer2SupportConcept, 2120, iServiceRecord.Start); invoiceItem.ItemDescription = "UA 3: "; break;
case "UA 4": stundensatz = GetStundensatz(scap.CostBearer2SupportConcept, 2121, iServiceRecord.Start); invoiceItem.ItemDescription = "UA 4: "; break;
}
if (mischitem) // Wenn Mischitem, dann Stundensatz der Leistung + Leistungsbezeichnung zwischenspeichern, um später aufzuteilen
{
invoiceItem.CustomNumber4 = stundensatz;
invoiceItem.CustomString4 = invoiceItem.ItemDescription;
}
}
}
if (stundensatz != 0)
{
invoiceItem.AmountPerUnit = stundensatz;
invoiceItem.AmountTotal = stundensatz * bd.UnitCount;
invoiceItem.GrossAmountTotal = stundensatz * bd.UnitCount;
}
else
{
invoiceItem.AmountPerUnit = bd.AmountPerUnit;
invoiceItem.AmountTotal = bd.AmountTotal;
invoiceItem.GrossAmountTotal = bd.GrossAmountTotal;
invoiceItem.ItemDescription = iServiceRecord.ServiceDescription.CategoryName;
}
invoiceItem.UnitCount = bd.UnitCount;
invoiceItem.ItemPeriodStart = bd.BillingPeriodStart;
invoiceItem.ItemPeriodEnd = bd.BillingPeriodEnd;
invoiceItem.RateFactor = bd.RateFactor;
invoiceItem.AccountingInterval = bd.AccountingInterval;
invoiceItem.ServiceRecord = DAOFactory.GenericDAO.LoadByID<ServiceRecord>(iServiceRecord.ServiceRecordOid.Value);
return invoiceItem;
}
private decimal GetStundensatz(CostBearer2SupportConcept c2s, long vleOid, DateTime? date)
{
if (c2s.CostBearer.CostRatePeriods != null)
{
//Valuelistentryoid:
//2118 Q 1 Sozialpädagoge
//2119 Q 2 Fachkraft
//2120 Qualifizierte Hilfskraft
//2121 Unqualifiziert Hilfskraft
var crpList = MapperFactory.CostRatePeriodDC_CostRatePeriod.MapToNewDCs(c2s.CostBearer.CostRatePeriods);
var cp = crpList.GetCostRatePeriodForDate(CostRatePeriodType.HourlyRate, (DateTime)date, vleOid);
if (cp != null && cp.CostRateValue.HasValue)
{
return cp.CostRateValue.Value;
}
}
return 0;
}
private InvoiceItem CopyInvoiceItem(InvoiceItem invoiceItem)
{
return new InvoiceItem()
{
AccountingInterval = invoiceItem.AccountingInterval,
AmountPerUnit = invoiceItem.AmountPerUnit,
AmountTotal = invoiceItem.AmountTotal,
ApprovalUnit = invoiceItem.ApprovalUnit,
ApprovedPerUnit = invoiceItem.ApprovedPerUnit,
GrossAmountTotal = invoiceItem.GrossAmountTotal,
ItemDescription = invoiceItem.ItemDescription,
IsActive = invoiceItem.IsActive,
ItemPeriodEnd = invoiceItem.ItemPeriodEnd,
ItemPeriodStart = invoiceItem.ItemPeriodStart,
MaxApprovedAmount = invoiceItem.MaxApprovedAmount,
MaxApprovedUnitCount = invoiceItem.MaxApprovedAmount,
RateFactor = invoiceItem.RateFactor,
ServiceRecord = invoiceItem.ServiceRecord,
SupportConceptApprovalPeriodOid = invoiceItem.SupportConceptApprovalPeriodOid,
UnitCount = invoiceItem.UnitCount,
UnitDescription = invoiceItem.UnitDescription,
};
}
public override string GetSummaryItemKey(SupportConceptApprovalPeriod scap, InvoiceItem iitem, bool summaryPerMonth)
{
String key = String.Format("{0}_{1}", iitem.AmountPerUnit, iitem.ItemDescription);
if (summaryPerMonth)
{
key = String.Format("{0}_{1}_{2:yyyyMM}", iitem.AmountPerUnit, iitem.ItemDescription, iitem.ItemPeriodStart);
}
return key;
}
}
}