From 2e14cdc670f6825fd080bd95835cb5f503a8d0d8 Mon Sep 17 00:00:00 2001 From: HirschleM Date: Wed, 27 May 2026 16:49:31 +0200 Subject: [PATCH] MH: Endart Kappung in Spitzabrechnung Zwischenstand --- .../Invoicing/SettlementInvoiceCreation.cs | 113 ++++++++++++++++++ ReportImp/EndartFabrik/SettlementReport2.cs | 24 +++- 2 files changed, 132 insertions(+), 5 deletions(-) diff --git a/ReportImp/EndartFabrik/Invoicing/SettlementInvoiceCreation.cs b/ReportImp/EndartFabrik/Invoicing/SettlementInvoiceCreation.cs index e77201f67..5f7337f40 100644 --- a/ReportImp/EndartFabrik/Invoicing/SettlementInvoiceCreation.cs +++ b/ReportImp/EndartFabrik/Invoicing/SettlementInvoiceCreation.cs @@ -14,5 +14,118 @@ namespace EndartFabrik.Invoicing { return false; } + + public override void CalculateInvoiceItemAmounts(Settlement2DC si) + { + // Claude: Gesamtbudget aus bewilligten Stunden – kein Limit wenn CheckMaxApprovedHours() false zurückgibt + decimal flsRestBudget = si.ApprovedFLS ?? 0; + if (!CheckMaxApprovedHours()) + flsRestBudget = decimal.MaxValue; + + // Claude: Gekappt-Items sammeln und erst nach der Schleife einfügen, um die Item-Reihenfolge nicht zu stören + var gekapptItems = new List(); + + foreach (var ii in si.InvoiceItems) + { + if (!ii.UnitCount.HasValue) + continue; + + if (ii.UnitCount > 0) + { + // Claude: Rohstunden (UnitCount) und Budgetstunden (UnitCountBudget) trennen – + // ProzentBudget der Leistung geht über die Relation in den Budgetverbrauch ein, RateFactor nicht + var rawCount = Math.Round(ii.UnitCount.Value, 2, MidpointRounding.AwayFromZero); + var unitCountBudget = Math.Round(ii.UnitCountBudget ?? rawCount, 2, MidpointRounding.AwayFromZero); + var relation = unitCountBudget / rawCount; + + // Claude: Gewichtete Stunden für den Budgetvergleich berechnen + var gewichteteStunden = rawCount * relation; + var cappedRawCount = rawCount; + + if (gewichteteStunden > flsRestBudget) + { + // Claude: Budget reicht nicht → Rohstunden auf verbleibendes Budget herunterrechnen + gewichteteStunden = flsRestBudget; + cappedRawCount = Math.Round(gewichteteStunden / relation, 2, MidpointRounding.AwayFromZero); + } + + // Claude: Verbrauchte gewichtete Stunden vom Restbudget abziehen (RateFactor hat keinen Einfluss) + flsRestBudget -= gewichteteStunden; + + if (ii.AmountPerUnit.HasValue) + { + if (!string.IsNullOrEmpty(ii.ItemDescription) && ii.ItemDescription.ToLower().Contains("fehlkontakt") && ii.ProzentAbrechenbar.HasValue) + { + // Claude: Fehlkontakte: Stundensatz anteilig reduzieren statt aufzuteilen + ii.AmountPerUnit = ii.AmountPerUnit * ii.ProzentAbrechenbar / 100; + } + else if (rawCount - cappedRawCount > 0 && cappedRawCount != 0) + { + // Claude: Teilkappung – separates Item für überschüssige Stunden anlegen (ohne Betrag / 0 EUR) + gekapptItems.Add(CopyItemWithoutAmount(ii, rawCount - cappedRawCount)); + } + + // 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 = ii.RateFactor.HasValue && ii.RateFactor.Value != 0 + ? Math.Round(ii.AmountTotal.Value * ((100m + ii.RateFactor.Value) / 100m), 2, MidpointRounding.AwayFromZero) + : ii.AmountTotal; + } + else + { + // Claude: Vollständige Kappung – Rohstunden für Anzeige wiederherstellen, aber 0 EUR abrechnen + ii.UnitCount = rawCount; + ii.AmountTotal = null; + ii.GrossAmountTotal = null; + } + } + } + else if (ii.AmountPerUnit.HasValue) + { + // Claude: UnitCount = 0: kein Kappen nötig, Fehlkontakt-Reduktion ggf. anwenden und Betrag auf 0 setzen + if (!string.IsNullOrEmpty(ii.ItemDescription) && ii.ItemDescription.ToLower().Contains("fehlkontakt") && ii.ProzentAbrechenbar.HasValue) + ii.AmountPerUnit = ii.AmountPerUnit * ii.ProzentAbrechenbar / 100; + ii.AmountTotal = 0; + ii.GrossAmountTotal = 0; + } + + // 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-Items am Ende einfügen, damit die Reihenfolge der regulären Items erhalten bleibt + si.InvoiceItems.AddRange(gekapptItems); + } + + private InvoiceItemDC CopyItemWithoutAmount(InvoiceItemDC ii, decimal unitCount) + { + // Claude: Kopie eines Items für die weggekappten Stunden – identische Metadaten, aber kein Betrag (GrossAmountTotal = null → 0 EUR) + return new InvoiceItemDC + { + AccountingInterval = ii.AccountingInterval, + AmountPerUnit = ii.AmountPerUnit, + ApprovalUnit = ii.ApprovalUnit, + ApprovedPerUnit = ii.ApprovedPerUnit, + GrossAmountTotal = 0, + ItemDescription = ii.ItemDescription + " (zu viel geleistet)", + ItemPeriodEnd = ii.ItemPeriodEnd, + ItemPeriodStart = ii.ItemPeriodStart, + MaxApprovedAmount = ii.MaxApprovedAmount, + MaxApprovedUnitCount = ii.MaxApprovedUnitCount, + Notice = ii.Notice, + ProzentAbrechenbar = ii.ProzentAbrechenbar, + RateFactor = ii.RateFactor, + SupportConceptApprovalPeriodOid = ii.SupportConceptApprovalPeriodOid, + UnitCount = unitCount, + UnitCountBudget = unitCount, + UnitDescription = ii.UnitDescription + " (zu viel geleistet)" + }; + } } } diff --git a/ReportImp/EndartFabrik/SettlementReport2.cs b/ReportImp/EndartFabrik/SettlementReport2.cs index d6af3ad2b..32e51ad74 100644 --- a/ReportImp/EndartFabrik/SettlementReport2.cs +++ b/ReportImp/EndartFabrik/SettlementReport2.cs @@ -1,12 +1,13 @@ +using BeWo.Report; +using BeWo.Report.ReportObjects; +using BS.Shared.Extensions; +using DevExpress.XtraReports.UI; using System; -using System.Drawing; using System.Collections; using System.ComponentModel; -using DevExpress.XtraReports.UI; -using BeWo.Report.ReportObjects; -using BeWo.Report; +using System.Drawing; using System.Text.RegularExpressions; -using BS.Shared.Extensions; +using System.Windows.Forms; namespace EndartFabrik { @@ -47,7 +48,20 @@ namespace EndartFabrik { ii.ItemDescription = "Fachleistungsstunden"; } + else if (ii.ItemDescription.Contains("(zu viel geleistet)")) + { + ii.AbrechnungsText = "(zu viel geleistet) " + ii.AbrechnungsText; + + } } + // Claude: Items mit "(zu viel geleistet)" ans Ende sortieren, damit sie nach den regulären Positionen erscheinen + pRO.InvoiceItems.Sort((a, b) => + { + bool aGekapptes = a.ItemDescription != null && a.ItemDescription.Contains("(zu viel geleistet)"); + bool bGekapptes = b.ItemDescription != null && b.ItemDescription.Contains("(zu viel geleistet)"); + return aGekapptes.CompareTo(bGekapptes); + }); + pRO.RateFactor = (double)rateFactor; if (pRO.Salutation1.IsNullOrEmpty() && pRO.Salutation2.IsNullOrEmpty())