MH: Endart Vorbereitung weitere Kappung
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user