MH: Endart Vorbereitung weitere Kappung

This commit is contained in:
2026-06-02 10:14:31 +02:00
parent 52da8cf1fb
commit 97cc47eeea

View File

@@ -19,123 +19,132 @@ namespace EndartFabrik.Invoicing
{
public override void CheckMaxApprovedAmount(ServiceInvoice invoice, ServiceInvoicePeriod sip, SupportConceptApprovalPeriodDC scap, CostBearer2SupportConcept costBearer2SupportConcept, DateTimeSpan invoicePeriod, List<ServiceRecordDC> 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<Calculations>(_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<long, bool>();
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<Calculations>(_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<long, bool>();
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)