From 97cc47eeea78f75341d56dfa0af291839b2382cf Mon Sep 17 00:00:00 2001 From: HirschleM Date: Tue, 2 Jun 2026 10:14:31 +0200 Subject: [PATCH] MH: Endart Vorbereitung weitere Kappung --- .../Invoicing/CustomInvoiceCreation.cs | 205 +++++++++--------- 1 file changed, 107 insertions(+), 98 deletions(-) diff --git a/ReportImp/EndartFabrik/Invoicing/CustomInvoiceCreation.cs b/ReportImp/EndartFabrik/Invoicing/CustomInvoiceCreation.cs index 2a0727f9e..6ec960bb8 100644 --- a/ReportImp/EndartFabrik/Invoicing/CustomInvoiceCreation.cs +++ b/ReportImp/EndartFabrik/Invoicing/CustomInvoiceCreation.cs @@ -19,123 +19,132 @@ namespace EndartFabrik.Invoicing { public override void CheckMaxApprovedAmount(ServiceInvoice invoice, ServiceInvoicePeriod sip, SupportConceptApprovalPeriodDC scap, CostBearer2SupportConcept costBearer2SupportConcept, DateTimeSpan invoicePeriod, List serviceRecords) { + // Claude: Kein Kappen nötig wenn keine bewilligten Stunden hinterlegt oder Verschiebung erlaubt ist if (!sip.ApprovedHours.HasValue) return; - - // Claude: Gesamtstunden für den kompletten Bewilligungszeitraum ermitteln. - // ApprovedBETotal direkt nutzen, falls befüllt – sonst über calc für den vollen Span berechnen - // (sip.ApprovedHours enthält nur den wöchentlichen Wert, nicht das Gesamtbudget) - decimal totalApproved = scap.ApprovedBETotal ?? 0; - if (totalApproved == 0) + if (!sip.SupportConceptApprovalPeriod.IsApprovedBEShifting) { - var calc = PluginLoader.FindClass(_SpecificID) ?? Calculations.GetInstance(_SpecificID); - var costRates = costBearer2SupportConcept.CostBearer.CostRatePeriods.MapToNewDCs(); - totalApproved = calc.GetApprovedHoursForPeriod(scap, costRates, scap.Span.StartDateTime, scap.Span.EndDateTime) ?? 0; + } - - if (totalApproved == 0) - return; - - // Claude: Bereits geleistete Stunden aus früheren Abrechnungszeiträumen innerhalb dieser Bewilligung summieren. - // Nur Einträge vor dem aktuellen Zeitraum zählen – diese wurden ggf. bereits in einer früheren Rechnung abgerechnet. - decimal alreadyConsumed = 0; - if (sip.Start.HasValue) + else { - // Claude: Gruppenleistungen nur einmal zählen (wie in der restlichen Abrechnungslogik) - var groupBookingDict = new Dictionary(); - foreach (var sr in costBearer2SupportConcept.ServiceRecords) + // Claude: Gesamtstunden für den kompletten Bewilligungszeitraum ermitteln. + // ApprovedBETotal direkt nutzen, falls befüllt – sonst über calc für den vollen Span berechnen + // (sip.ApprovedHours enthält nur den wöchentlichen Wert, nicht das Gesamtbudget) + decimal totalApproved = scap.ApprovedBETotal ?? 0; + if (totalApproved == 0) { - if (!sr.Start.HasValue) continue; - // Claude: Nur Records innerhalb des Bewilligungszeitraums, aber vor dem aktuellen Abrechnungszeitraum - if (sr.Start.Value < scap.Span.StartDateTime || sr.Start.Value >= sip.Start.Value) continue; - if (!sr.ServiceDescription.ServiceCategory.IsBillable) continue; - if (sr.GroupOid.HasValue && groupBookingDict.ContainsKey(sr.GroupOid.Value)) continue; - - // Claude: Dauer in Stunden umrechnen und ProzentAbrechnung der Leistung berücksichtigen - decimal rd = sr.RoundedDuration; - decimal prozent = sr.ServiceDescription.ServiceCategory.ProzentBudget.Value; - if (sr.ServiceDescription.ProzentBudget.HasValue) - prozent = sr.ServiceDescription.ProzentBudget.Value; - alreadyConsumed += rd * prozent / 100m / 60m; - - if (sr.GroupOid.HasValue) - groupBookingDict.Add(sr.GroupOid.Value, true); + var calc = PluginLoader.FindClass(_SpecificID) ?? Calculations.GetInstance(_SpecificID); + var costRates = costBearer2SupportConcept.CostBearer.CostRatePeriods.MapToNewDCs(); + totalApproved = calc.GetApprovedHoursForPeriod(scap, costRates, scap.Span.StartDateTime, scap.Span.EndDateTime) ?? 0; } - } - // Claude: Verbleibendes Budget = gesamt bewilligt minus bereits in früheren Zeiträumen verbrauchte Stunden - decimal maxApproved = Math.Max(0m, totalApproved - alreadyConsumed); + if (totalApproved == 0) + return; - InvoiceItem gekapptesItem = null; - - foreach (var ii in sip.InvoiceItemList) - { - if (!ii.UnitCount.HasValue) - continue; - - // Claude: RateFactor-Multiplikator nur für die Betragsberechnung vormerken, NICHT für den Budgetvergleich - decimal rf = 1; - if (ii.RateFactor.HasValue && ii.RateFactor.Value != 0) - rf = (100 + ii.RateFactor.Value) / 100; - - // Claude: Rohstunden (ohne Faktor) ermitteln und gegen verbleibendes Budget kappen - var rawCount = Math.Round(ii.UnitCount.Value, 2, MidpointRounding.AwayFromZero); - var cappedRawCount = rawCount; - if (maxApproved < cappedRawCount) - cappedRawCount = maxApproved; - - // Claude: Verbrauchte Rohstunden vom Restbudget abziehen (Faktor hat keinen Einfluss auf das Budget) - maxApproved -= cappedRawCount; - - if (ii.AmountPerUnit.HasValue) + // Claude: Bereits geleistete Stunden aus früheren Abrechnungszeiträumen innerhalb dieser Bewilligung summieren. + // Nur Einträge vor dem aktuellen Zeitraum zählen – diese wurden ggf. bereits in einer früheren Rechnung abgerechnet. + decimal alreadyConsumed = 0; + if (sip.Start.HasValue) { - // Claude: Teilkappung – separates Item für die weggekappten Rohstunden anlegen (ohne Betrag) - if (rawCount - cappedRawCount > 0 && cappedRawCount != 0) + // Claude: Gruppenleistungen nur einmal zählen (wie in der restlichen Abrechnungslogik) + var groupBookingDict = new Dictionary(); + foreach (var sr in costBearer2SupportConcept.ServiceRecords) { - gekapptesItem = new InvoiceItem + if (!sr.Start.HasValue) continue; + // Claude: Nur Records innerhalb des Bewilligungszeitraums, aber vor dem aktuellen Abrechnungszeitraum + if (sr.Start.Value < scap.Span.StartDateTime || sr.Start.Value >= sip.Start.Value) continue; + if (!sr.ServiceDescription.ServiceCategory.IsBillable) continue; + if (sr.GroupOid.HasValue && groupBookingDict.ContainsKey(sr.GroupOid.Value)) continue; + + // Claude: Dauer in Stunden umrechnen und ProzentAbrechnung der Leistung berücksichtigen + decimal rd = sr.RoundedDuration; + decimal prozent = sr.ServiceDescription.ServiceCategory.ProzentBudget.Value; + if (sr.ServiceDescription.ProzentBudget.HasValue) + prozent = sr.ServiceDescription.ProzentBudget.Value; + alreadyConsumed += rd * prozent / 100m / 60m; + + if (sr.GroupOid.HasValue) + groupBookingDict.Add(sr.GroupOid.Value, true); + } + } + + // Claude: Verbleibendes Budget = gesamt bewilligt minus bereits in früheren Zeiträumen verbrauchte Stunden + decimal maxApproved = Math.Max(0m, totalApproved - alreadyConsumed); + + InvoiceItem gekapptesItem = null; + + foreach (var ii in sip.InvoiceItemList) + { + if (!ii.UnitCount.HasValue) + continue; + + // Claude: RateFactor-Multiplikator nur für die Betragsberechnung vormerken, NICHT für den Budgetvergleich + decimal rf = 1; + if (ii.RateFactor.HasValue && ii.RateFactor.Value != 0) + rf = (100 + ii.RateFactor.Value) / 100; + + // Claude: Rohstunden (ohne Faktor) ermitteln und gegen verbleibendes Budget kappen + var rawCount = Math.Round(ii.UnitCount.Value, 2, MidpointRounding.AwayFromZero); + var cappedRawCount = rawCount; + if (maxApproved < cappedRawCount) + cappedRawCount = maxApproved; + + // Claude: Verbrauchte Rohstunden vom Restbudget abziehen (Faktor hat keinen Einfluss auf das Budget) + maxApproved -= cappedRawCount; + + if (ii.AmountPerUnit.HasValue) + { + // Claude: Teilkappung – separates Item für die weggekappten Rohstunden anlegen (ohne Betrag) + if (rawCount - cappedRawCount > 0 && cappedRawCount != 0) { - AccountingInterval = ii.AccountingInterval, - AmountPerUnit = ii.AmountPerUnit, - ItemDescription = ii.ItemDescription + " (zu viel geleistet)", - ItemPeriodEnd = ii.ItemPeriodEnd, - ItemPeriodStart = ii.ItemPeriodStart, - MaxApprovedAmount = ii.MaxApprovedAmount, - MaxApprovedUnitCount = ii.MaxApprovedUnitCount, - RateFactor = ii.RateFactor, - SupportConceptApprovalPeriodOid = ii.SupportConceptApprovalPeriodOid, - UnitCount = rawCount - cappedRawCount, - UnitDescription = ii.UnitDescription - // GrossAmountTotal bleibt null → 0 EUR - }; + gekapptesItem = new InvoiceItem + { + AccountingInterval = ii.AccountingInterval, + AmountPerUnit = ii.AmountPerUnit, + ItemDescription = ii.ItemDescription + " (zu viel geleistet)", + ItemPeriodEnd = ii.ItemPeriodEnd, + ItemPeriodStart = ii.ItemPeriodStart, + MaxApprovedAmount = ii.MaxApprovedAmount, + MaxApprovedUnitCount = ii.MaxApprovedUnitCount, + RateFactor = ii.RateFactor, + SupportConceptApprovalPeriodOid = ii.SupportConceptApprovalPeriodOid, + UnitCount = rawCount - cappedRawCount, + UnitDescription = ii.UnitDescription + // GrossAmountTotal bleibt null → 0 EUR + }; + } + + // Claude: UnitCount auf die abrechenbaren Rohstunden setzen (ohne Faktor) + ii.UnitCount = cappedRawCount; + + if (cappedRawCount != 0) + { + // Claude: Nettobetrag aus Rohstunden × Stundensatz, Faktor erst beim Bruttobetrag anwenden + ii.AmountTotal = ii.UnitCount.Value * ii.AmountPerUnit.Value; + ii.GrossAmountTotal = Math.Round(ii.AmountTotal.Value * rf, 2, MidpointRounding.AwayFromZero); + } + else + { + // Claude: Vollständige Kappung – Rohstunden für Anzeige wiederherstellen, aber 0 EUR abrechnen + ii.UnitCount = rawCount; + ii.AmountTotal = null; + ii.GrossAmountTotal = null; + } } - // Claude: UnitCount auf die abrechenbaren Rohstunden setzen (ohne Faktor) - ii.UnitCount = cappedRawCount; - - if (cappedRawCount != 0) - { - // Claude: Nettobetrag aus Rohstunden × Stundensatz, Faktor erst beim Bruttobetrag anwenden - ii.AmountTotal = ii.UnitCount.Value * ii.AmountPerUnit.Value; - ii.GrossAmountTotal = Math.Round(ii.AmountTotal.Value * rf, 2, MidpointRounding.AwayFromZero); - } - else - { - // Claude: Vollständige Kappung – Rohstunden für Anzeige wiederherstellen, aber 0 EUR abrechnen - ii.UnitCount = rawCount; - ii.AmountTotal = null; - ii.GrossAmountTotal = null; - } + // Claude: MaxApprovedUnitCount aktualisieren, wird für Anzeige auf der Rechnung benötigt + if (!ii.MaxApprovedUnitCount.HasValue || ii.MaxApprovedUnitCount < ii.UnitCount) + ii.MaxApprovedUnitCount = ii.UnitCount; } - // Claude: MaxApprovedUnitCount aktualisieren, wird für Anzeige auf der Rechnung benötigt - if (!ii.MaxApprovedUnitCount.HasValue || ii.MaxApprovedUnitCount < ii.UnitCount) - ii.MaxApprovedUnitCount = ii.UnitCount; + // Claude: Gekappt-Item am Ende einfügen, damit die Reihenfolge der anderen Items nicht verändert wird + if (gekapptesItem != null) + sip.InvoiceItemList.Add(gekapptesItem); } - // Claude: Gekappt-Item am Ende einfügen, damit die Reihenfolge der anderen Items nicht verändert wird - if (gekapptesItem != null) - sip.InvoiceItemList.Add(gekapptesItem); + } public override InvoiceItem CreateInvoiceItem(ServiceRecordDC iServiceRecord, SupportConceptApprovalPeriod scap, BS.Shared.Services.Calculations calc)