MH: Endart Kappung in Spitzabrechnung Zwischenstand

This commit is contained in:
2026-05-27 16:49:31 +02:00
parent 671cd2de67
commit 2e14cdc670
2 changed files with 132 additions and 5 deletions

View File

@@ -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<InvoiceItemDC>();
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)"
};
}
}
}

View File

@@ -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())