diff --git a/src/Apps/W1/Subscription Billing/App/Base/Codeunits/UpgradeSubscriptionBilling.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Base/Codeunits/UpgradeSubscriptionBilling.Codeunit.al index 0b0e5fb1ec..bfdb24d89b 100644 --- a/src/Apps/W1/Subscription Billing/App/Base/Codeunits/UpgradeSubscriptionBilling.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Base/Codeunits/UpgradeSubscriptionBilling.Codeunit.al @@ -3,7 +3,9 @@ namespace Microsoft.SubscriptionBilling; #if not CLEANSCHEMA29 using Microsoft.Finance.GeneralLedger.Setup; #endif +using Microsoft.Purchases.History; using Microsoft.Sales.Document; +using Microsoft.Sales.History; using System.Upgrade; codeunit 8032 "Upgrade Subscription Billing" @@ -28,6 +30,7 @@ codeunit 8032 "Upgrade Subscription Billing" UpdateCreateContractDeferralsFlag(); DeleteSalesSubscriptionLinesConnectedToDeletedQuote(); RemoveDocumentNoFromBillingLines(); + FillPostingGroupsInContractDeferrals(); end; #if not CLEANSCHEMA29 @@ -321,6 +324,83 @@ codeunit 8032 "Upgrade Subscription Billing" exit('MS-XXXXXX-RemoveDocumentNoFromBillingLines-20250819'); end; + local procedure FillPostingGroupsInContractDeferrals() + var + CustContractDeferral: Record "Cust. Sub. Contract Deferral"; + VendContractDeferral: Record "Vend. Sub. Contract Deferral"; + UpgradeTag: Codeunit "Upgrade Tag"; + begin + if UpgradeTag.HasUpgradeTag(FillPostingGroupsInContractDeferralsTag()) then + exit; + + CustContractDeferral.SetRange("Gen. Bus. Posting Group", ''); + CustContractDeferral.SetRange("Gen. Prod. Posting Group", ''); + if CustContractDeferral.FindSet(true) then + repeat + UpdateCustDeferralPostingGroups(CustContractDeferral); + until CustContractDeferral.Next() = 0; + + VendContractDeferral.SetRange("Gen. Bus. Posting Group", ''); + VendContractDeferral.SetRange("Gen. Prod. Posting Group", ''); + if VendContractDeferral.FindSet(true) then + repeat + UpdateVendDeferralPostingGroups(VendContractDeferral); + until VendContractDeferral.Next() = 0; + + UpgradeTag.SetUpgradeTag(FillPostingGroupsInContractDeferralsTag()); + end; + + local procedure UpdateCustDeferralPostingGroups(var CustContractDeferral: Record "Cust. Sub. Contract Deferral") + var + SalesInvoiceLine: Record "Sales Invoice Line"; + SalesCrMemoLine: Record "Sales Cr.Memo Line"; + CustContractDeferralToUpdate: Record "Cust. Sub. Contract Deferral"; + begin + CustContractDeferralToUpdate := CustContractDeferral; + case CustContractDeferral."Document Type" of + "Rec. Billing Document Type"::Invoice: + if SalesInvoiceLine.Get(CustContractDeferral."Document No.", CustContractDeferral."Document Line No.") then begin + CustContractDeferralToUpdate."Gen. Bus. Posting Group" := SalesInvoiceLine."Gen. Bus. Posting Group"; + CustContractDeferralToUpdate."Gen. Prod. Posting Group" := SalesInvoiceLine."Gen. Prod. Posting Group"; + CustContractDeferralToUpdate.Modify(false); + end; + "Rec. Billing Document Type"::"Credit Memo": + if SalesCrMemoLine.Get(CustContractDeferral."Document No.", CustContractDeferral."Document Line No.") then begin + CustContractDeferralToUpdate."Gen. Bus. Posting Group" := SalesCrMemoLine."Gen. Bus. Posting Group"; + CustContractDeferralToUpdate."Gen. Prod. Posting Group" := SalesCrMemoLine."Gen. Prod. Posting Group"; + CustContractDeferralToUpdate.Modify(false); + end; + end; + end; + + local procedure UpdateVendDeferralPostingGroups(var VendContractDeferral: Record "Vend. Sub. Contract Deferral") + var + PurchInvLine: Record "Purch. Inv. Line"; + PurchCrMemoLine: Record "Purch. Cr. Memo Line"; + VendContractDeferralToUpdate: Record "Vend. Sub. Contract Deferral"; + begin + VendContractDeferralToUpdate := VendContractDeferral; + case VendContractDeferral."Document Type" of + "Rec. Billing Document Type"::Invoice: + if PurchInvLine.Get(VendContractDeferral."Document No.", VendContractDeferral."Document Line No.") then begin + VendContractDeferralToUpdate."Gen. Bus. Posting Group" := PurchInvLine."Gen. Bus. Posting Group"; + VendContractDeferralToUpdate."Gen. Prod. Posting Group" := PurchInvLine."Gen. Prod. Posting Group"; + VendContractDeferralToUpdate.Modify(false); + end; + "Rec. Billing Document Type"::"Credit Memo": + if PurchCrMemoLine.Get(VendContractDeferral."Document No.", VendContractDeferral."Document Line No.") then begin + VendContractDeferralToUpdate."Gen. Bus. Posting Group" := PurchCrMemoLine."Gen. Bus. Posting Group"; + VendContractDeferralToUpdate."Gen. Prod. Posting Group" := PurchCrMemoLine."Gen. Prod. Posting Group"; + VendContractDeferralToUpdate.Modify(false); + end; + end; + end; + + local procedure FillPostingGroupsInContractDeferralsTag(): Text[250] + begin + exit('MS-623188-FillPostingGroupsInContractDeferrals-20250209'); + end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", OnGetPerCompanyUpgradeTags, '', false, false)] local procedure RegisterPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]]) begin @@ -335,5 +415,6 @@ codeunit 8032 "Upgrade Subscription Billing" PerCompanyUpgradeTags.Add(GetUpdateCreateContractDeferralsFlag()); PerCompanyUpgradeTags.Add(DeleteSalesSubscriptionLinesConnectedToDeletedQuoteTag()); PerCompanyUpgradeTags.Add(RemoveDocumentNoFromBillingLinesTag()); + PerCompanyUpgradeTags.Add(FillPostingGroupsInContractDeferralsTag()); end; } diff --git a/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/CustomerDeferralsMngmt.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/CustomerDeferralsMngmt.Codeunit.al index bab90908db..ba0d87340b 100644 --- a/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/CustomerDeferralsMngmt.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/CustomerDeferralsMngmt.Codeunit.al @@ -4,7 +4,6 @@ using Microsoft.Finance.Currency; using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Finance.GeneralLedger.Ledger; using Microsoft.Finance.GeneralLedger.Posting; -using Microsoft.Finance.GeneralLedger.Preview; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Foundation.AuditCodes; using Microsoft.Foundation.Navigate; @@ -19,17 +18,9 @@ codeunit 8067 "Customer Deferrals Mngmt." tabledata "Sales Invoice Line" = r; var - TempCustomerContractDeferral: Record "Cust. Sub. Contract Deferral" temporary; GLSetup: Record "General Ledger Setup"; DeferralEntryNo: Integer; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", OnBeforePostSalesDoc, '', false, false)] - local procedure ClearGlobals() - begin - TempCustomerContractDeferral.Reset(); - TempCustomerContractDeferral.DeleteAll(false); - end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales Post Invoice Events", OnPrepareLineOnBeforeSetAccount, '', false, false)] local procedure OnPrepareLineOnBeforeSetAccount(SalesLine: Record "Sales Line"; var SalesAccount: Code[20]) var @@ -63,20 +54,6 @@ codeunit 8067 "Customer Deferrals Mngmt." InsertContractDeferrals(SalesHeader, xSalesLine, SalesInvHeader."No."); end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", OnPostSalesLineOnBeforeInsertCrMemoLine, '', false, false)] - local procedure InsertCustomerDeferralsFromSalesCrMemoOnPostSalesLineOnBeforeInsertCrMemoLine(SalesHeader: Record "Sales Header"; SalesLine: Record "Sales Line"; var IsHandled: Boolean; xSalesLine: Record "Sales Line"; SalesCrMemoHeader: Record "Sales Cr.Memo Header") - var - SalesDocuments: Codeunit "Sales Documents"; - begin - if (xSalesLine.Quantity >= 0) or (xSalesLine."Unit Price" >= 0) then - exit; - - if SalesDocuments.GetAppliesToDocNo(SalesHeader) <> '' then - exit; - - InsertContractDeferrals(SalesHeader, xSalesLine, SalesCrMemoHeader."No."); - end; - local procedure InsertContractDeferrals(SalesHeader: Record "Sales Header"; SalesLine: Record "Sales Line"; DocumentNo: Code[20]) var CustContractHeader: Record "Customer Subscription Contract"; @@ -90,6 +67,8 @@ codeunit 8067 "Customer Deferrals Mngmt." exit; if SalesLine.Quantity = 0 then exit; + if SalesLine.Amount = 0 then + exit; if SalesLine."Recurring Billing from" > SalesLine."Recurring Billing to" then exit; if not (SalesLine."Document Type" in [Enum::"Sales Document Type"::Invoice, Enum::"Sales Document Type"::"Credit Memo"]) then @@ -138,153 +117,136 @@ codeunit 8067 "Customer Deferrals Mngmt." CustomerContractDeferral."Deferral Base Amount" := SalesLine.Amount; SalesLine."Line Discount Amount" := Sign * SalesLine."Line Discount Amount"; - if SalesLine."Recurring Billing from" = CalcDate('<-CM>', SalesLine."Recurring Billing from") then - InsertContractDeferralsWhenStartingOnFirstDayInMonth(CustomerContractDeferral, SalesLine) - else - InsertContractDeferralsWhenNotStartingOnFirstDayInMonth(CustomerContractDeferral, SalesLine); + InsertContractDeferralPeriods(CustomerContractDeferral, SalesLine); end; - local procedure GetDeferralParametersFromSalesLine(SalesLine: Record "Sales Line"; var FirstDayOfBillingPeriod: Date; var LastDayOfBillingPeriod: Date; var TotalLineAmount: Decimal; var TotalLineDiscountAmount: Decimal; var NumberOfPeriods: Integer) + local procedure GetNumberOfDeferralPeriods(FirstDayOfBillingPeriod: Date; LastDayOfBillingPeriod: Date) NumberOfPeriods: Integer var LoopDate: Date; begin - LoopDate := SalesLine."Recurring Billing from"; + LoopDate := FirstDayOfBillingPeriod; repeat NumberOfPeriods += 1; LoopDate := CalcDate('<1M>', LoopDate); - until LoopDate > CalcDate('', SalesLine."Recurring Billing to"); - - FirstDayOfBillingPeriod := SalesLine."Recurring Billing from"; - LastDayOfBillingPeriod := SalesLine."Recurring Billing to"; - TotalLineAmount := SalesLine.Amount; - TotalLineDiscountAmount := SalesLine."Line Discount Amount"; + until LoopDate > CalcDate('', LastDayOfBillingPeriod); end; - local procedure InsertContractDeferralsWhenStartingOnFirstDayInMonth(var CustomerContractDeferral: Record "Cust. Sub. Contract Deferral"; SalesLine: Record "Sales Line") + local procedure InsertContractDeferralPeriods(var CustomerContractDeferral: Record "Cust. Sub. Contract Deferral"; SalesLine: Record "Sales Line") var NumberOfPeriods: Integer; i: Integer; NextPostingDate: Date; - LastDayOfBillingPeriod: Date; - TotalLineAmount: Decimal; - TotalLineDiscountAmount: Decimal; - LineAmountPerPeriod: Decimal; - LineDiscountAmountPerPeriod: Decimal; + FirstDayOfBillingPeriod: Date; + LineAmountPerDay: Decimal; + LineDiscountAmountPerDay: Decimal; + FullMonthLineAmount: Decimal; + FullMonthLineDiscountAmount: Decimal; + PartialFirstMonthAmount: Decimal; + PartialFirstMonthDiscountAmount: Decimal; + PartialLastMonthAmount: Decimal; + PartialLastMonthDiscountAmount: Decimal; + PeriodLineAmount: Decimal; + PeriodLineDiscountAmount: Decimal; RunningLineAmount: Decimal; RunningLineDiscountAmount: Decimal; + NumberOfDaysInSchedule: Integer; + FirstMonthDays: Integer; + LastMonthDays: Integer; + FullMonthCount: Integer; + FirstMonthIsPartial: Boolean; + LastMonthIsPartial: Boolean; begin - RunningLineAmount := 0; - RunningLineDiscountAmount := 0; - GetDeferralParametersFromSalesLine(SalesLine, NextPostingDate, LastDayOfBillingPeriod, TotalLineAmount, TotalLineDiscountAmount, NumberOfPeriods); - LineAmountPerPeriod := Round(TotalLineAmount / NumberOfPeriods, GLSetup."Amount Rounding Precision"); - LineDiscountAmountPerPeriod := Round(TotalLineDiscountAmount / NumberOfPeriods, GLSetup."Amount Rounding Precision"); - - for i := 1 to NumberOfPeriods do begin - CustomerContractDeferral."Posting Date" := NextPostingDate; - NextPostingDate := CalcDate('<1M>', NextPostingDate); - if i = NumberOfPeriods then begin - LineAmountPerPeriod := TotalLineAmount - RunningLineAmount; - LineDiscountAmountPerPeriod := TotalLineDiscountAmount - RunningLineDiscountAmount; + FirstDayOfBillingPeriod := SalesLine."Recurring Billing from"; + NumberOfPeriods := GetNumberOfDeferralPeriods(FirstDayOfBillingPeriod, SalesLine."Recurring Billing to"); + + // Determine which months are partial (not covering the entire calendar month) + FirstMonthIsPartial := FirstDayOfBillingPeriod <> CalcDate('<-CM>', FirstDayOfBillingPeriod); + LastMonthIsPartial := SalesLine."Recurring Billing to" <> CalcDate('', SalesLine."Recurring Billing to"); + + // Calculate daily rate for day-proportioning partial months + NumberOfDaysInSchedule := SalesLine."Recurring Billing to" - FirstDayOfBillingPeriod + 1; + LineAmountPerDay := SalesLine.Amount / NumberOfDaysInSchedule; + LineDiscountAmountPerDay := SalesLine."Line Discount Amount" / NumberOfDaysInSchedule; + FirstMonthDays := CalcDate('', FirstDayOfBillingPeriod) - FirstDayOfBillingPeriod + 1; + LastMonthDays := Date2DMY(SalesLine."Recurring Billing to", 1); + + // Calculate partial month amounts and determine how many full months remain + FullMonthCount := NumberOfPeriods; + if FirstMonthIsPartial then begin + // When first month is partial, day-proportion both first and last months + CalcPartialMonthAmounts(FirstMonthDays, LineAmountPerDay, LineDiscountAmountPerDay, PartialFirstMonthAmount, PartialFirstMonthDiscountAmount); + CalcPartialMonthAmounts(LastMonthDays, LineAmountPerDay, LineDiscountAmountPerDay, PartialLastMonthAmount, PartialLastMonthDiscountAmount); + FullMonthCount -= 2; + end else + if LastMonthIsPartial and (NumberOfPeriods > 1) then begin + // When only last month is partial, day-proportion just that month + CalcPartialMonthAmounts(LastMonthDays, LineAmountPerDay, LineDiscountAmountPerDay, PartialLastMonthAmount, PartialLastMonthDiscountAmount); + FullMonthCount -= 1; end; - RunningLineAmount += LineAmountPerPeriod; - RunningLineDiscountAmount += LineDiscountAmountPerPeriod; - CustomerContractDeferral."Number of Days" := Date2DMY(CalcDate('', CustomerContractDeferral."Posting Date"), 1); - CustomerContractDeferral.Amount := LineAmountPerPeriod; - CustomerContractDeferral."Discount Amount" := LineDiscountAmountPerPeriod; - CustomerContractDeferral."Entry No." := 0; - OnBeforeInsertCustomerContractDeferralWhenStartingOnFirstDayInMonth(CustomerContractDeferral, SalesLine, i, NumberOfPeriods); - CustomerContractDeferral.Insert(false); - TempCustomerContractDeferral := CustomerContractDeferral; - TempCustomerContractDeferral.Insert(false); //Used for Preview Posting + // Equal share for full months from the remaining amount after partial months + if FullMonthCount > 0 then begin + FullMonthLineAmount := Round((SalesLine.Amount - PartialFirstMonthAmount - PartialLastMonthAmount) / FullMonthCount, GLSetup."Amount Rounding Precision"); + FullMonthLineDiscountAmount := Round((SalesLine."Line Discount Amount" - PartialFirstMonthDiscountAmount - PartialLastMonthDiscountAmount) / FullMonthCount, GLSetup."Amount Rounding Precision"); end; - end; - local procedure InsertContractDeferralsWhenNotStartingOnFirstDayInMonth(var CustomerContractDeferral: Record "Cust. Sub. Contract Deferral"; SalesLine: Record "Sales Line") - var - NumberOfPeriods: Integer; - NextPostingDate: Date; - FirstDayOfBillingPeriod: Date; - LastDayOfBillingPeriod: Date; - TotalLineAmount: Decimal; - TotalLineDiscountAmount: Decimal; - LineAmountPerPeriod: Decimal; - LineDiscountAmountPerPeriod: Decimal; - LineAmountPerDay: Decimal; - LineDiscountAmountPerDay: Decimal; - LineAmountPerMonth: Decimal; - LineDiscountAmountPerMonth: Decimal; - FirstMonthDays: Integer; - FirstMonthLineAmount: Decimal; - FirstMonthLineDiscountAmount: Decimal; - LastMonthDays: Integer; - LastMonthLineAmount: Decimal; - LastMonthLineDiscountAmount: Decimal; - RunningLineAmount: Decimal; - RunningLineDiscountTotal: Decimal; - NumberOfDaysInSchedule: Integer; - i: Integer; - begin - RunningLineAmount := 0; - RunningLineDiscountTotal := 0; - GetDeferralParametersFromSalesLine(SalesLine, FirstDayOfBillingPeriod, LastDayOfBillingPeriod, TotalLineAmount, TotalLineDiscountAmount, NumberOfPeriods); + // Insert deferral records for each period NextPostingDate := FirstDayOfBillingPeriod; - NumberOfDaysInSchedule := LastDayOfBillingPeriod - FirstDayOfBillingPeriod + 1; - LineAmountPerDay := TotalLineAmount / NumberOfDaysInSchedule; - LineDiscountAmountPerDay := TotalLineDiscountAmount / NumberOfDaysInSchedule; - FirstMonthDays := CalcDate('', NextPostingDate) - NextPostingDate + 1; - FirstMonthLineAmount := Round(FirstMonthDays * LineAmountPerDay, GLSetup."Amount Rounding Precision"); - FirstMonthLineDiscountAmount := Round(FirstMonthDays * LineDiscountAmountPerDay, GLSetup."Amount Rounding Precision"); - LastMonthDays := Date2DMY(LastDayOfBillingPeriod, 1); - LastMonthLineAmount := Round(LastMonthDays * LineAmountPerDay, GLSetup."Amount Rounding Precision"); - LastMonthLineDiscountAmount := Round(LastMonthDays * LineDiscountAmountPerDay, GLSetup."Amount Rounding Precision"); - if NumberOfPeriods > 2 then begin - LineAmountPerMonth := Round((TotalLineAmount - FirstMonthLineAmount - LastMonthLineAmount) / (NumberOfPeriods - 2), GLSetup."Amount Rounding Precision"); - LineDiscountAmountPerMonth := Round((TotalLineDiscountAmount - FirstMonthLineDiscountAmount - LastMonthLineDiscountAmount) / (NumberOfPeriods - 2), GLSetup."Amount Rounding Precision"); - end; + RunningLineAmount := 0; + RunningLineDiscountAmount := 0; for i := 1 to NumberOfPeriods do begin CustomerContractDeferral."Posting Date" := NextPostingDate; NextPostingDate := CalcDate('<1M-CM>', NextPostingDate); - case i of - 1: - begin - LineAmountPerPeriod := FirstMonthLineAmount; - LineDiscountAmountPerPeriod := FirstMonthLineDiscountAmount; - CustomerContractDeferral."Number of Days" := FirstMonthDays; - end; - NumberOfPeriods: - begin - LineAmountPerPeriod := TotalLineAmount - RunningLineAmount; - LineDiscountAmountPerPeriod := TotalLineDiscountAmount - RunningLineDiscountTotal; - CustomerContractDeferral."Number of Days" := LastMonthDays; - end; - else begin - LineAmountPerPeriod := LineAmountPerMonth; - LineDiscountAmountPerPeriod := LineDiscountAmountPerMonth; - CustomerContractDeferral."Number of Days" := Date2DMY(CalcDate('', CustomerContractDeferral."Posting Date"), 1); + + // Determine period amount: last period absorbs rounding, first partial is day-proportioned, rest are equal + if i = NumberOfPeriods then begin + PeriodLineAmount := SalesLine.Amount - RunningLineAmount; + PeriodLineDiscountAmount := SalesLine."Line Discount Amount" - RunningLineDiscountAmount; + end else + if (i = 1) and FirstMonthIsPartial then begin + PeriodLineAmount := PartialFirstMonthAmount; + PeriodLineDiscountAmount := PartialFirstMonthDiscountAmount; + end else begin + PeriodLineAmount := FullMonthLineAmount; + PeriodLineDiscountAmount := FullMonthLineDiscountAmount; end; - end; - RunningLineAmount += LineAmountPerPeriod; - RunningLineDiscountTotal += LineDiscountAmountPerPeriod; - CustomerContractDeferral.Amount := LineAmountPerPeriod; - CustomerContractDeferral."Discount Amount" := LineDiscountAmountPerPeriod; + // Determine number of days: partial months use actual day count, full months use calendar month days + if (i = NumberOfPeriods) and (NumberOfPeriods > 1) and (FirstMonthIsPartial or LastMonthIsPartial) then + CustomerContractDeferral."Number of Days" := LastMonthDays + else + if (i = 1) and FirstMonthIsPartial then + CustomerContractDeferral."Number of Days" := FirstMonthDays + else + CustomerContractDeferral."Number of Days" := Date2DMY(CalcDate('', CustomerContractDeferral."Posting Date"), 1); + + RunningLineAmount += PeriodLineAmount; + RunningLineDiscountAmount += PeriodLineDiscountAmount; + + CustomerContractDeferral.Amount := PeriodLineAmount; + CustomerContractDeferral."Discount Amount" := PeriodLineDiscountAmount; CustomerContractDeferral."Entry No." := 0; - OnBeforeInsertCustomerContractDeferralWhenNotStartingOnFirstDayInMonth(CustomerContractDeferral, SalesLine, i, NumberOfPeriods); +#if not CLEAN29 +#pragma warning disable AL0432 + if FirstMonthIsPartial then + OnBeforeInsertCustomerContractDeferralWhenNotStartingOnFirstDayInMonth(CustomerContractDeferral, SalesLine, i, NumberOfPeriods) + else + OnBeforeInsertCustomerContractDeferralWhenStartingOnFirstDayInMonth(CustomerContractDeferral, SalesLine, i, NumberOfPeriods); +#pragma warning restore AL0432 +#endif + OnBeforeInsertCustomerContractDeferral(CustomerContractDeferral, SalesLine, i, NumberOfPeriods); CustomerContractDeferral.Insert(false); - TempCustomerContractDeferral := CustomerContractDeferral; - TempCustomerContractDeferral.Insert(false); //Used for Preview Posting end; end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", OnAfterSalesCrMemoLineInsert, '', false, false)] - local procedure InsertCustomerDeferralsFromSalesCrMemo(var SalesCrMemoLine: Record "Sales Cr.Memo Line"; SalesHeader: Record "Sales Header") + local procedure InsertCustomerDeferralsFromSalesCrMemo(var SalesCrMemoLine: Record "Sales Cr.Memo Line"; SalesHeader: Record "Sales Header"; SalesLine: Record "Sales Line") begin - ReleaseAndCreditCustomerContractDeferrals(SalesHeader, SalesCrMemoLine); + ProcessCreditMemoContractDeferrals(SalesHeader, SalesCrMemoLine, SalesLine); end; - local procedure ReleaseAndCreditCustomerContractDeferrals(SalesHeader: Record "Sales Header"; var SalesCrMemoLine: Record "Sales Cr.Memo Line") + local procedure ProcessCreditMemoContractDeferrals(SalesHeader: Record "Sales Header"; var SalesCrMemoLine: Record "Sales Cr.Memo Line"; SalesLine: Record "Sales Line") var InvoiceCustContractDeferral: Record "Cust. Sub. Contract Deferral"; CreditMemoCustContractDeferral: Record "Cust. Sub. Contract Deferral"; @@ -294,6 +256,10 @@ codeunit 8067 "Customer Deferrals Mngmt." AppliesToDocNo: Code[20]; begin AppliesToDocNo := SalesDocuments.GetAppliesToDocNo(SalesHeader); + if AppliesToDocNo = '' then begin + InsertContractDeferrals(SalesHeader, SalesLine, SalesCrMemoLine."Document No."); + exit; + end; if SalesDocuments.IsInvoiceCredited(AppliesToDocNo) then exit; InvoiceCustContractDeferral.FilterOnDocumentTypeAndDocumentNo(Enum::"Rec. Billing Document Type"::Invoice, AppliesToDocNo); @@ -327,28 +293,10 @@ codeunit 8067 "Customer Deferrals Mngmt." ContractDeferralRelease.SetRequestPageParameters(CreditMemoCustContractDeferral."Posting Date", SalesCrMemoLine."Posting Date"); ContractDeferralRelease.ReleaseCustomerContractDeferralAndInsertTempGenJournalLine(CreditMemoCustContractDeferral); ContractDeferralRelease.PostTempGenJnlLineBufferForCustomerDeferrals(); - - TempCustomerContractDeferral := CreditMemoCustContractDeferral; - TempCustomerContractDeferral.Insert(false); //Used for Preview Posting until InvoiceCustContractDeferral.Next() = 0; end; end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Posting Preview Event Handler", OnAfterFillDocumentEntry, '', false, false)] - local procedure OnAfterFillDocumentEntry(var DocumentEntry: Record "Document Entry") - var - PostingPreviewEventHandler: Codeunit "Posting Preview Event Handler"; - begin - PostingPreviewEventHandler.InsertDocumentEntry(TempCustomerContractDeferral, DocumentEntry); - end; - - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Posting Preview Event Handler", OnAfterShowEntries, '', false, false)] - local procedure OnAfterShowEntries(TableNo: Integer) - begin - if TableNo = Database::"Cust. Sub. Contract Deferral" then - Page.Run(Page::"Customer Contract Deferrals", TempCustomerContractDeferral); - end; - [EventSubscriber(ObjectType::Page, Page::Navigate, OnAfterNavigateFindRecords, '', false, false)] local procedure OnAfterFindEntries(var DocumentEntry: Record "Document Entry"; DocNoFilter: Text) var @@ -424,13 +372,35 @@ codeunit 8067 "Customer Deferrals Mngmt." DeferralEntryNo := NewDeferralNo; end; + local procedure CalcPartialMonthAmounts(MonthDays: Integer; LineAmountPerDay: Decimal; LineDiscountAmountPerDay: Decimal; var PartialMonthAmount: Decimal; var PartialMonthDiscountAmount: Decimal) + begin + PartialMonthAmount := CalcDaysAmount(MonthDays, LineAmountPerDay); + PartialMonthDiscountAmount := CalcDaysAmount(MonthDays, LineDiscountAmountPerDay); + end; + + local procedure CalcDaysAmount(MonthDays: Integer; AmountPerDay: Decimal): Decimal + begin + exit(Round(MonthDays * AmountPerDay, GLSetup."Amount Rounding Precision")); + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeInsertCustomerContractDeferral(var CustSubContractDeferral: Record "Cust. Sub. Contract Deferral"; SalesLine: Record "Sales Line"; PeriodNo: Integer; NumberOfPeriods: Integer) + begin + end; + +#if not CLEAN29 + [Obsolete('Replaced by OnBeforeInsertCustomerContractDeferral.', '29.0')] [IntegrationEvent(false, false)] local procedure OnBeforeInsertCustomerContractDeferralWhenStartingOnFirstDayInMonth(var CustSubContractDeferral: Record "Cust. Sub. Contract Deferral"; SalesLine: Record "Sales Line"; PeriodNo: Integer; NumberOfPeriods: Integer) begin end; +#endif +#if not CLEAN29 + [Obsolete('Replaced by OnBeforeInsertCustomerContractDeferral.', '29.0')] [IntegrationEvent(false, false)] local procedure OnBeforeInsertCustomerContractDeferralWhenNotStartingOnFirstDayInMonth(var CustSubContractDeferral: Record "Cust. Sub. Contract Deferral"; SalesLine: Record "Sales Line"; PeriodNo: Integer; NumberOfPeriods: Integer) begin end; +#endif } diff --git a/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/DeferralPostPreviewBinding.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/DeferralPostPreviewBinding.Codeunit.al new file mode 100644 index 0000000000..4f0a0776ee --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/DeferralPostPreviewBinding.Codeunit.al @@ -0,0 +1,34 @@ +namespace Microsoft.SubscriptionBilling; + +using Microsoft.Finance.GeneralLedger.Preview; + +codeunit 8076 "Deferral Post. Preview Binding" +{ + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Preview", OnAfterBindSubscription, '', false, false)] + local procedure BindDeferralPreviewHandlerOnAfterBindSubscription() + begin + TryBindPostingPreviewHandler(); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Preview", OnAfterUnbindSubscription, '', false, false)] + local procedure UnbindDeferralPreviewHandlerOnAfterUnbindSubscription() + begin + TryUnbindPostingPreviewHandler(); + end; + + local procedure TryBindPostingPreviewHandler(): Boolean + var + DeferralPostingPreviewHandler: Codeunit "Deferral Post. Preview Handler"; + begin + DeferralPostingPreviewHandler.DeleteAll(); + exit(BindSubscription(DeferralPostingPreviewHandler)); + end; + + local procedure TryUnbindPostingPreviewHandler(): Boolean + var + DeferralPostingPreviewHandler: Codeunit "Deferral Post. Preview Handler"; + begin + exit(UnbindSubscription(DeferralPostingPreviewHandler)); + end; +} diff --git a/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/DeferralPostPreviewHandler.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/DeferralPostPreviewHandler.Codeunit.al new file mode 100644 index 0000000000..bc3ba09fb1 --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/DeferralPostPreviewHandler.Codeunit.al @@ -0,0 +1,77 @@ +namespace Microsoft.SubscriptionBilling; + +codeunit 8077 "Deferral Post. Preview Handler" +{ + EventSubscriberInstance = Manual; + SingleInstance = true; + + var + TempCustSubContractDeferral: Record "Cust. Sub. Contract Deferral" temporary; + TempVendSubContractDeferral: Record "Vend. Sub. Contract Deferral" temporary; + + [EventSubscriber(ObjectType::Table, Database::"Cust. Sub. Contract Deferral", OnAfterInsertEvent, '', false, false)] + local procedure OnInsertCustContractDeferral(var Rec: Record "Cust. Sub. Contract Deferral"; RunTrigger: Boolean) + begin + if Rec.IsTemporary() then + exit; + + if TempCustSubContractDeferral.Get(Rec."Entry No.") then + exit; + + TempCustSubContractDeferral := Rec; + TempCustSubContractDeferral.Insert(); + end; + + [EventSubscriber(ObjectType::Table, Database::"Cust. Sub. Contract Deferral", OnAfterModifyEvent, '', false, false)] + local procedure OnModifyCustContractDeferral(var Rec: Record "Cust. Sub. Contract Deferral"; var xRec: Record "Cust. Sub. Contract Deferral"; RunTrigger: Boolean) + begin + if Rec.IsTemporary() then + exit; + + TempCustSubContractDeferral := Rec; + if not TempCustSubContractDeferral.Insert() then + TempCustSubContractDeferral.Modify(); + end; + + [EventSubscriber(ObjectType::Table, Database::"Vend. Sub. Contract Deferral", OnAfterInsertEvent, '', false, false)] + local procedure OnInsertVendContractDeferral(var Rec: Record "Vend. Sub. Contract Deferral"; RunTrigger: Boolean) + begin + if Rec.IsTemporary() then + exit; + + if TempVendSubContractDeferral.Get(Rec."Entry No.") then + exit; + + TempVendSubContractDeferral := Rec; + TempVendSubContractDeferral.Insert(); + end; + + [EventSubscriber(ObjectType::Table, Database::"Vend. Sub. Contract Deferral", OnAfterModifyEvent, '', false, false)] + local procedure OnModifyVendContractDeferral(var Rec: Record "Vend. Sub. Contract Deferral"; var xRec: Record "Vend. Sub. Contract Deferral"; RunTrigger: Boolean) + begin + if Rec.IsTemporary() then + exit; + + TempVendSubContractDeferral := Rec; + if not TempVendSubContractDeferral.Insert() then + TempVendSubContractDeferral.Modify(); + end; + + procedure DeleteAll() + begin + TempCustSubContractDeferral.Reset(); + TempCustSubContractDeferral.DeleteAll(); + TempVendSubContractDeferral.Reset(); + TempVendSubContractDeferral.DeleteAll(); + end; + + procedure GetTempCustContractDeferral(var OutTempCustSubContractDeferral: Record "Cust. Sub. Contract Deferral" temporary) + begin + OutTempCustSubContractDeferral.Copy(TempCustSubContractDeferral, true); + end; + + procedure GetTempVendContractDeferral(var OutTempVendSubContractDeferral: Record "Vend. Sub. Contract Deferral" temporary) + begin + OutTempVendSubContractDeferral.Copy(TempVendSubContractDeferral, true); + end; +} diff --git a/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/DeferralPostPreviewSubscr.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/DeferralPostPreviewSubscr.Codeunit.al new file mode 100644 index 0000000000..dc43e5abad --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/DeferralPostPreviewSubscr.Codeunit.al @@ -0,0 +1,52 @@ +namespace Microsoft.SubscriptionBilling; + +using Microsoft.Finance.GeneralLedger.Preview; +using Microsoft.Foundation.Navigate; + +codeunit 8078 "Deferral Post. Preview Subscr." +{ + var + TempCustSubContractDeferral: Record "Cust. Sub. Contract Deferral" temporary; + TempVendSubContractDeferral: Record "Vend. Sub. Contract Deferral" temporary; + PostingPreviewEventHandler: Codeunit "Posting Preview Event Handler"; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Posting Preview Event Handler", OnGetEntries, '', false, false)] + local procedure GetEntriesOnGetEntries(TableNo: Integer; var RecRef: RecordRef) + begin + GetAllTables(); + case TableNo of + Database::"Cust. Sub. Contract Deferral": + RecRef.GetTable(TempCustSubContractDeferral); + Database::"Vend. Sub. Contract Deferral": + RecRef.GetTable(TempVendSubContractDeferral); + end; + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Posting Preview Event Handler", OnAfterShowEntries, '', false, false)] + local procedure ShowEntriesOnAfterShowEntries(TableNo: Integer) + begin + GetAllTables(); + case TableNo of + Database::"Cust. Sub. Contract Deferral": + Page.Run(Page::"Customer Contract Deferrals", TempCustSubContractDeferral); + Database::"Vend. Sub. Contract Deferral": + Page.Run(Page::"Vendor Contract Deferrals", TempVendSubContractDeferral); + end; + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Posting Preview Event Handler", OnAfterFillDocumentEntry, '', false, false)] + local procedure FillDocumentEntryOnAfterFillDocumentEntry(var DocumentEntry: Record "Document Entry") + begin + GetAllTables(); + PostingPreviewEventHandler.InsertDocumentEntry(TempCustSubContractDeferral, DocumentEntry); + PostingPreviewEventHandler.InsertDocumentEntry(TempVendSubContractDeferral, DocumentEntry); + end; + + local procedure GetAllTables() + var + DeferralPostingPreviewHandler: Codeunit "Deferral Post. Preview Handler"; + begin + DeferralPostingPreviewHandler.GetTempCustContractDeferral(TempCustSubContractDeferral); + DeferralPostingPreviewHandler.GetTempVendContractDeferral(TempVendSubContractDeferral); + end; +} diff --git a/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/VendorDeferralsMngmt.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/VendorDeferralsMngmt.Codeunit.al index 99fba60253..b006de302e 100644 --- a/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/VendorDeferralsMngmt.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Deferrals/Codeunits/VendorDeferralsMngmt.Codeunit.al @@ -3,7 +3,6 @@ namespace Microsoft.SubscriptionBilling; using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Finance.GeneralLedger.Ledger; using Microsoft.Finance.GeneralLedger.Posting; -using Microsoft.Finance.GeneralLedger.Preview; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Foundation.AuditCodes; using Microsoft.Foundation.Navigate; @@ -19,19 +18,11 @@ codeunit 8068 "Vendor Deferrals Mngmt." tabledata "Purch. Inv. Line" = r; var - TempVendorContractDeferral: Record "Vend. Sub. Contract Deferral" temporary; GLSetup: Record "General Ledger Setup"; TempPurchaseLine: Record "Purchase Line" temporary; DeferralEntryNo: Integer; VendorContractDeferralLinePosting: Boolean; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Purch.-Post", OnBeforePostPurchaseDoc, '', false, false)] - local procedure ClearGlobals() - begin - TempVendorContractDeferral.Reset(); - TempVendorContractDeferral.DeleteAll(false); - end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Purch. Post Invoice Events", OnPrepareLineOnBeforeSetAccount, '', false, false)] local procedure OnPrepareLineOnBeforeSetAccount(PurchLine: Record "Purchase Line"; var SalesAccount: Code[20]) var @@ -73,18 +64,6 @@ codeunit 8068 "Vendor Deferrals Mngmt." end; end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Purch.-Post", OnPostPurchLineOnBeforeInsertCrMemoLine, '', false, false)] - local procedure InsertVendorDeferralsFromPurchaseInvoiceOnPostPurchLineOnBeforeInsertCrMemoLine(PurchaseHeader: Record "Purchase Header"; PurchaseLine: Record "Purchase Line"; var IsHandled: Boolean; var PurchCrMemoLine: Record "Purch. Cr. Memo Line"; xPurchaseLine: Record "Purchase Line"); - begin - if (PurchaseLine.Quantity >= 0) or (PurchaseLine."Direct Unit Cost" >= 0) then - exit; - - if GetAppliesToDocNo(PurchaseHeader) <> '' then - exit; - - InsertContractDeferrals(PurchaseHeader, PurchaseLine, PurchaseHeader."Posting No."); - end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Purch.-Post", OnBeforePurchInvLineInsert, '', false, false)] local procedure InsertVendorDeferralsFromPurchaseInvoiceOnBeforePurchInvLineInsert(var PurchInvHeader: Record "Purch. Inv. Header"; var PurchaseLine: Record "Purchase Line") begin @@ -103,6 +82,8 @@ codeunit 8068 "Vendor Deferrals Mngmt." exit; if PurchaseLine.Quantity = 0 then exit; + if PurchaseLine.Amount = 0 then + exit; if PurchaseLine."Recurring Billing from" > PurchaseLine."Recurring Billing to" then exit; if not (PurchaseLine."Document Type" in [Enum::"Purchase Document Type"::Invoice, Enum::"Purchase Document Type"::"Credit Memo"]) then @@ -136,151 +117,128 @@ codeunit 8068 "Vendor Deferrals Mngmt." VendorContractDeferral."Deferral Base Amount" := PurchaseLine.Amount; PurchaseLine."Line Discount Amount" := Sign * PurchaseLine."Line Discount Amount"; - if PurchaseLine."Recurring Billing from" = CalcDate('<-CM>', PurchaseLine."Recurring Billing from") then - InsertContractDeferralsWhenStartingOnFirstDayInMonth(VendorContractDeferral, PurchaseLine) - else - InsertContractDeferralsWhenNotStartingOnFirstDayInMonth(VendorContractDeferral, PurchaseLine); + InsertContractDeferralPeriods(VendorContractDeferral, PurchaseLine); end; - local procedure GetDeferralParametersFromPurchaseLine(PurchaseLine: Record "Purchase Line"; var FirstDayOfBillingPeriod: Date; var LastDayOfBillingPeriod: Date; var TotalLineAmount: Decimal; var TotalLineDiscountAmount: Decimal; var NumberOfPeriods: Integer) + local procedure GetNumberOfDeferralPeriods(FirstDayOfBillingPeriod: Date; LastDayOfBillingPeriod: Date) NumberOfPeriods: Integer var LoopDate: Date; begin - LoopDate := PurchaseLine."Recurring Billing from"; + LoopDate := FirstDayOfBillingPeriod; repeat NumberOfPeriods += 1; LoopDate := CalcDate('<1M>', LoopDate); - until LoopDate > CalcDate('', PurchaseLine."Recurring Billing to"); - - FirstDayOfBillingPeriod := PurchaseLine."Recurring Billing from"; - LastDayOfBillingPeriod := PurchaseLine."Recurring Billing to"; - TotalLineAmount := PurchaseLine.Amount; - TotalLineDiscountAmount := PurchaseLine."Line Discount Amount"; + until LoopDate > CalcDate('', LastDayOfBillingPeriod); end; - local procedure InsertContractDeferralsWhenStartingOnFirstDayInMonth(var VendorContractDeferral: Record "Vend. Sub. Contract Deferral"; PurchaseLine: Record "Purchase Line") + local procedure InsertContractDeferralPeriods(var VendorContractDeferral: Record "Vend. Sub. Contract Deferral"; PurchaseLine: Record "Purchase Line") var NumberOfPeriods: Integer; i: Integer; NextPostingDate: Date; - LastDayOfBillingPeriod: Date; - TotalLineAmount: Decimal; - TotalLineDiscountAmount: Decimal; - LineAmountPerPeriod: Decimal; - LineDiscountAmountPerPeriod: Decimal; + FirstDayOfBillingPeriod: Date; + LineAmountPerDay: Decimal; + LineDiscountAmountPerDay: Decimal; + FullMonthLineAmount: Decimal; + FullMonthLineDiscountAmount: Decimal; + PartialFirstMonthAmount: Decimal; + PartialFirstMonthDiscountAmount: Decimal; + PartialLastMonthAmount: Decimal; + PartialLastMonthDiscountAmount: Decimal; + PeriodLineAmount: Decimal; + PeriodLineDiscountAmount: Decimal; RunningLineAmount: Decimal; RunningLineDiscountAmount: Decimal; + NumberOfDaysInSchedule: Integer; + FirstMonthDays: Integer; + LastMonthDays: Integer; + FullMonthCount: Integer; + FirstMonthIsPartial: Boolean; + LastMonthIsPartial: Boolean; begin - RunningLineAmount := 0; - RunningLineDiscountAmount := 0; - GetDeferralParametersFromPurchaseLine(PurchaseLine, NextPostingDate, LastDayOfBillingPeriod, TotalLineAmount, TotalLineDiscountAmount, NumberOfPeriods); - LineAmountPerPeriod := Round(TotalLineAmount / NumberOfPeriods, GLSetup."Amount Rounding Precision"); - LineDiscountAmountPerPeriod := Round(TotalLineDiscountAmount / NumberOfPeriods, GLSetup."Amount Rounding Precision"); - - for i := 1 to NumberOfPeriods do begin - VendorContractDeferral."Posting Date" := NextPostingDate; - NextPostingDate := CalcDate('<1M>', NextPostingDate); - if i = NumberOfPeriods then begin - LineAmountPerPeriod := TotalLineAmount - RunningLineAmount; - LineDiscountAmountPerPeriod := TotalLineDiscountAmount - RunningLineDiscountAmount; + FirstDayOfBillingPeriod := PurchaseLine."Recurring Billing from"; + NumberOfPeriods := GetNumberOfDeferralPeriods(FirstDayOfBillingPeriod, PurchaseLine."Recurring Billing to"); + + // Determine which months are partial (not covering the entire calendar month) + FirstMonthIsPartial := FirstDayOfBillingPeriod <> CalcDate('<-CM>', FirstDayOfBillingPeriod); + LastMonthIsPartial := PurchaseLine."Recurring Billing to" <> CalcDate('', PurchaseLine."Recurring Billing to"); + + // Calculate daily rate for day-proportioning partial months + NumberOfDaysInSchedule := PurchaseLine."Recurring Billing to" - FirstDayOfBillingPeriod + 1; + LineAmountPerDay := PurchaseLine.Amount / NumberOfDaysInSchedule; + LineDiscountAmountPerDay := PurchaseLine."Line Discount Amount" / NumberOfDaysInSchedule; + FirstMonthDays := CalcDate('', FirstDayOfBillingPeriod) - FirstDayOfBillingPeriod + 1; + LastMonthDays := Date2DMY(PurchaseLine."Recurring Billing to", 1); + + // Calculate partial month amounts and determine how many full months remain + FullMonthCount := NumberOfPeriods; + if FirstMonthIsPartial then begin + // When first month is partial, day-proportion both first and last months + CalcPartialMonthAmounts(FirstMonthDays, LineAmountPerDay, LineDiscountAmountPerDay, PartialFirstMonthAmount, PartialFirstMonthDiscountAmount); + CalcPartialMonthAmounts(LastMonthDays, LineAmountPerDay, LineDiscountAmountPerDay, PartialLastMonthAmount, PartialLastMonthDiscountAmount); + FullMonthCount -= 2; + end else + if LastMonthIsPartial and (NumberOfPeriods > 1) then begin + // When only last month is partial, day-proportion just that month + CalcPartialMonthAmounts(LastMonthDays, LineAmountPerDay, LineDiscountAmountPerDay, PartialLastMonthAmount, PartialLastMonthDiscountAmount); + FullMonthCount -= 1; end; - RunningLineAmount += LineAmountPerPeriod; - RunningLineDiscountAmount += LineDiscountAmountPerPeriod; - VendorContractDeferral."Number of Days" := Date2DMY(CalcDate('', VendorContractDeferral."Posting Date"), 1); - VendorContractDeferral.Amount := LineAmountPerPeriod; - VendorContractDeferral."Discount Amount" := LineDiscountAmountPerPeriod; - VendorContractDeferral."Entry No." := 0; - VendorContractDeferral.Insert(false); - TempVendorContractDeferral := VendorContractDeferral; - TempVendorContractDeferral.Insert(false); //Used for Preview Posting + // Equal share for full months from the remaining amount after partial months + if FullMonthCount > 0 then begin + FullMonthLineAmount := Round((PurchaseLine.Amount - PartialFirstMonthAmount - PartialLastMonthAmount) / FullMonthCount, GLSetup."Amount Rounding Precision"); + FullMonthLineDiscountAmount := Round((PurchaseLine."Line Discount Amount" - PartialFirstMonthDiscountAmount - PartialLastMonthDiscountAmount) / FullMonthCount, GLSetup."Amount Rounding Precision"); end; - end; - local procedure InsertContractDeferralsWhenNotStartingOnFirstDayInMonth(var VendorContractDeferral: Record "Vend. Sub. Contract Deferral"; PurchaseLine: Record "Purchase Line") - var - NumberOfPeriods: Integer; - NextPostingDate: Date; - FirstDayOfBillingPeriod: Date; - LastDayOfBillingPeriod: Date; - TotalLineAmount: Decimal; - TotalLineDiscountAmount: Decimal; - LineAmountPerPeriod: Decimal; - LineDiscountAmountPerPeriod: Decimal; - LineAmountPerDay: Decimal; - LineDiscountAmountPerDay: Decimal; - LineAmountPerMonth: Decimal; - LineDiscountAmountPerMonth: Decimal; - FirstMonthDays: Integer; - FirstMonthLineAmount: Decimal; - FirstMonthLineDiscountAmount: Decimal; - LastMonthDays: Integer; - LastMonthLineAmount: Decimal; - LastMonthLineDiscountAmount: Decimal; - RunningLineAmount: Decimal; - RunningLineDiscountTotal: Decimal; - NumberOfDaysInSchedule: Integer; - i: Integer; - begin - RunningLineAmount := 0; - RunningLineDiscountTotal := 0; - GetDeferralParametersFromPurchaseLine(PurchaseLine, FirstDayOfBillingPeriod, LastDayOfBillingPeriod, TotalLineAmount, TotalLineDiscountAmount, NumberOfPeriods); + // Insert deferral records for each period NextPostingDate := FirstDayOfBillingPeriod; - NumberOfDaysInSchedule := (LastDayOfBillingPeriod - FirstDayOfBillingPeriod + 1); - LineAmountPerDay := TotalLineAmount / NumberOfDaysInSchedule; - LineDiscountAmountPerDay := TotalLineDiscountAmount / NumberOfDaysInSchedule; - FirstMonthDays := CalcDate('', NextPostingDate) - NextPostingDate + 1; - FirstMonthLineAmount := Round(FirstMonthDays * LineAmountPerDay, GLSetup."Amount Rounding Precision"); - FirstMonthLineDiscountAmount := Round(FirstMonthDays * LineDiscountAmountPerDay, GLSetup."Amount Rounding Precision"); - LastMonthDays := Date2DMY(LastDayOfBillingPeriod, 1); - LastMonthLineAmount := Round(LastMonthDays * LineAmountPerDay, GLSetup."Amount Rounding Precision"); - LastMonthLineDiscountAmount := Round(LastMonthDays * LineDiscountAmountPerDay, GLSetup."Amount Rounding Precision"); - if NumberOfPeriods > 2 then begin - LineAmountPerMonth := Round((TotalLineAmount - FirstMonthLineAmount - LastMonthLineAmount) / (NumberOfPeriods - 2), GLSetup."Amount Rounding Precision"); - LineDiscountAmountPerMonth := Round((TotalLineDiscountAmount - FirstMonthLineDiscountAmount - LastMonthLineDiscountAmount) / (NumberOfPeriods - 2), GLSetup."Amount Rounding Precision"); - end; + RunningLineAmount := 0; + RunningLineDiscountAmount := 0; for i := 1 to NumberOfPeriods do begin VendorContractDeferral."Posting Date" := NextPostingDate; NextPostingDate := CalcDate('<1M-CM>', NextPostingDate); - case i of - 1: - begin - LineAmountPerPeriod := FirstMonthLineAmount; - LineDiscountAmountPerPeriod := FirstMonthLineDiscountAmount; - VendorContractDeferral."Number of Days" := FirstMonthDays; - end; - NumberOfPeriods: - begin - LineAmountPerPeriod := TotalLineAmount - RunningLineAmount; - LineDiscountAmountPerPeriod := TotalLineDiscountAmount - RunningLineDiscountTotal; - VendorContractDeferral."Number of Days" := LastMonthDays; - end; - else begin - LineAmountPerPeriod := LineAmountPerMonth; - LineDiscountAmountPerPeriod := LineDiscountAmountPerMonth; - VendorContractDeferral."Number of Days" := Date2DMY(CalcDate('', VendorContractDeferral."Posting Date"), 1); + + // Determine period amount: last period absorbs rounding, first partial is day-proportioned, rest are equal + if i = NumberOfPeriods then begin + PeriodLineAmount := PurchaseLine.Amount - RunningLineAmount; + PeriodLineDiscountAmount := PurchaseLine."Line Discount Amount" - RunningLineDiscountAmount; + end else + if (i = 1) and FirstMonthIsPartial then begin + PeriodLineAmount := PartialFirstMonthAmount; + PeriodLineDiscountAmount := PartialFirstMonthDiscountAmount; + end else begin + PeriodLineAmount := FullMonthLineAmount; + PeriodLineDiscountAmount := FullMonthLineDiscountAmount; end; - end; - RunningLineAmount += LineAmountPerPeriod; - RunningLineDiscountTotal += LineDiscountAmountPerPeriod; - VendorContractDeferral.Amount := LineAmountPerPeriod; - VendorContractDeferral."Discount Amount" := LineDiscountAmountPerPeriod; + // Determine number of days: partial months use actual day count, full months use calendar month days + if (i = NumberOfPeriods) and (NumberOfPeriods > 1) and (FirstMonthIsPartial or LastMonthIsPartial) then + VendorContractDeferral."Number of Days" := LastMonthDays + else + if (i = 1) and FirstMonthIsPartial then + VendorContractDeferral."Number of Days" := FirstMonthDays + else + VendorContractDeferral."Number of Days" := Date2DMY(CalcDate('', VendorContractDeferral."Posting Date"), 1); + + RunningLineAmount += PeriodLineAmount; + RunningLineDiscountAmount += PeriodLineDiscountAmount; + + VendorContractDeferral.Amount := PeriodLineAmount; + VendorContractDeferral."Discount Amount" := PeriodLineDiscountAmount; VendorContractDeferral."Entry No." := 0; + OnBeforeInsertVendorContractDeferral(VendorContractDeferral, PurchaseLine, i, NumberOfPeriods); VendorContractDeferral.Insert(false); - TempVendorContractDeferral := VendorContractDeferral; - TempVendorContractDeferral.Insert(false); //Used for Preview Posting end; end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Purch.-Post", OnAfterPurchCrMemoLineInsert, '', false, false)] - local procedure InsertVendorDeferralsFromPurchaseCrMemo(var PurchCrMemoLine: Record "Purch. Cr. Memo Line"; var PurchaseHeader: Record "Purchase Header") + local procedure InsertVendorDeferralsFromPurchaseCrMemo(var PurchCrMemoLine: Record "Purch. Cr. Memo Line"; var PurchaseHeader: Record "Purchase Header"; var PurchLine: Record "Purchase Line") begin - ReleaseAndCreditVendorContractDeferrals(PurchaseHeader, PurchCrMemoLine); + ProcessCreditMemoContractDeferrals(PurchaseHeader, PurchCrMemoLine, PurchLine); end; - local procedure ReleaseAndCreditVendorContractDeferrals(PurchaseHeader: Record "Purchase Header"; var PurchCrMemoLine: Record "Purch. Cr. Memo Line") + local procedure ProcessCreditMemoContractDeferrals(PurchaseHeader: Record "Purchase Header"; var PurchCrMemoLine: Record "Purch. Cr. Memo Line"; var PurchaseLine: Record "Purchase Line") var InvoiceVendorContractDeferral: Record "Vend. Sub. Contract Deferral"; CreditMemoVendorContractDeferral: Record "Vend. Sub. Contract Deferral"; @@ -290,6 +248,10 @@ codeunit 8068 "Vendor Deferrals Mngmt." AppliesToDocNo: Code[20]; begin AppliesToDocNo := GetAppliesToDocNo(PurchaseHeader); + if AppliesToDocNo = '' then begin + InsertContractDeferrals(PurchaseHeader, PurchaseLine, PurchCrMemoLine."Document No."); + exit; + end; if PurchaseDocuments.IsInvoiceCredited(AppliesToDocNo) then exit; InvoiceVendorContractDeferral.FilterOnDocumentTypeAndDocumentNo(Enum::"Rec. Billing Document Type"::Invoice, AppliesToDocNo); @@ -324,28 +286,10 @@ codeunit 8068 "Vendor Deferrals Mngmt." ContractDeferralRelease.SetRequestPageParameters(CreditMemoVendorContractDeferral."Posting Date", PurchCrMemoLine."Posting Date"); ContractDeferralRelease.ReleaseVendorContractDeferralsAndInsertTempGenJournalLines(CreditMemoVendorContractDeferral); ContractDeferralRelease.PostTempGenJnlLineBufferForVendorDeferrals(); - - TempVendorContractDeferral := CreditMemoVendorContractDeferral; - TempVendorContractDeferral.Insert(false); //Used for Preview Posting until InvoiceVendorContractDeferral.Next() = 0; end; end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Posting Preview Event Handler", OnAfterFillDocumentEntry, '', false, false)] - local procedure OnAfterFillDocumentEntry(var DocumentEntry: Record "Document Entry") - var - PostingPreviewEventHandler: Codeunit "Posting Preview Event Handler"; - begin - PostingPreviewEventHandler.InsertDocumentEntry(TempVendorContractDeferral, DocumentEntry); - end; - - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Posting Preview Event Handler", OnAfterShowEntries, '', false, false)] - local procedure OnAfterShowEntries(TableNo: Integer) - begin - if TableNo = Database::"Vend. Sub. Contract Deferral" then - Page.Run(Page::"Vendor Contract Deferrals", TempVendorContractDeferral); - end; - [EventSubscriber(ObjectType::Page, Page::Navigate, OnAfterNavigateFindRecords, '', false, false)] local procedure OnAfterFindEntries(var DocumentEntry: Record "Document Entry"; DocNoFilter: Text) var @@ -420,4 +364,20 @@ codeunit 8068 "Vendor Deferrals Mngmt." exit(PurchHeader."Applies-to Doc. No."); exit(BillingLine.GetCorrectionDocumentNo("Service Partner"::Vendor, PurchHeader."No.")); end; + + local procedure CalcPartialMonthAmounts(MonthDays: Integer; LineAmountPerDay: Decimal; LineDiscountAmountPerDay: Decimal; var PartialMonthAmount: Decimal; var PartialMonthDiscountAmount: Decimal) + begin + PartialMonthAmount := CalcDaysAmount(MonthDays, LineAmountPerDay); + PartialMonthDiscountAmount := CalcDaysAmount(MonthDays, LineDiscountAmountPerDay); + end; + + local procedure CalcDaysAmount(MonthDays: Integer; AmountPerDay: Decimal): Decimal + begin + exit(Round(MonthDays * AmountPerDay, GLSetup."Amount Rounding Precision")); + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeInsertVendorContractDeferral(var VendSubContractDeferral: Record "Vend. Sub. Contract Deferral"; PurchaseLine: Record "Purchase Line"; PeriodNo: Integer; NumberOfPeriods: Integer) + begin + end; } diff --git a/src/Apps/W1/Subscription Billing/App/Deferrals/Pages/CustomerContractDeferrals.Page.al b/src/Apps/W1/Subscription Billing/App/Deferrals/Pages/CustomerContractDeferrals.Page.al index cf67a15fca..73c640f242 100644 --- a/src/Apps/W1/Subscription Billing/App/Deferrals/Pages/CustomerContractDeferrals.Page.al +++ b/src/Apps/W1/Subscription Billing/App/Deferrals/Pages/CustomerContractDeferrals.Page.al @@ -95,6 +95,12 @@ page 8079 "Customer Contract Deferrals" { ToolTip = 'Specifies whether the Subscription Line is used as a basis for periodic invoicing or discounts.'; } + field("Gen. Bus. Posting Group"; Rec."Gen. Bus. Posting Group") + { + } + field("Gen. Prod. Posting Group"; Rec."Gen. Prod. Posting Group") + { + } field("G/L Entry No."; Rec."G/L Entry No.") { ToolTip = 'Specifies the number of the G/L item with which the deferral was released.'; diff --git a/src/Apps/W1/Subscription Billing/App/Deferrals/Pages/VendorContractDeferrals.Page.al b/src/Apps/W1/Subscription Billing/App/Deferrals/Pages/VendorContractDeferrals.Page.al index 8e78c4a011..1eeb308695 100644 --- a/src/Apps/W1/Subscription Billing/App/Deferrals/Pages/VendorContractDeferrals.Page.al +++ b/src/Apps/W1/Subscription Billing/App/Deferrals/Pages/VendorContractDeferrals.Page.al @@ -95,6 +95,12 @@ page 8081 "Vendor Contract Deferrals" { ToolTip = 'Specifies whether the Subscription Line is used as a basis for periodic invoicing or discounts.'; } + field("Gen. Bus. Posting Group"; Rec."Gen. Bus. Posting Group") + { + } + field("Gen. Prod. Posting Group"; Rec."Gen. Prod. Posting Group") + { + } field("G/L Entry No."; Rec."G/L Entry No.") { ToolTip = 'Specifies the number of the G/L item with which the deferral was released.'; diff --git a/src/Apps/W1/Subscription Billing/App/Deferrals/Reports/ContractDeferralsRelease.Report.al b/src/Apps/W1/Subscription Billing/App/Deferrals/Reports/ContractDeferralsRelease.Report.al index 80aa0d4f9c..e14fd1c541 100644 --- a/src/Apps/W1/Subscription Billing/App/Deferrals/Reports/ContractDeferralsRelease.Report.al +++ b/src/Apps/W1/Subscription Billing/App/Deferrals/Reports/ContractDeferralsRelease.Report.al @@ -138,25 +138,38 @@ report 8051 "Contract Deferrals Release" end; end; - internal procedure ReleaseCustomerContractDeferralAndInsertTempGenJournalLine(var CustomerContractDeferral: Record "Cust. Sub. Contract Deferral") + internal procedure ReleaseCustomerContractDeferralAndInsertTempGenJournalLine(CustomerContractDeferral: Record "Cust. Sub. Contract Deferral") var - GenBusPostingGroup: Code[20]; - GenProdPostingGroup: Code[20]; + ShouldRelease: Boolean; + PostingAmount: Decimal; begin - if not CustomerContractDeferral.GetDocumentPostingGroups(GenBusPostingGroup, GenProdPostingGroup) then + ShouldRelease := true; + OnBeforeReleaseCustomerContractDeferral(CustomerContractDeferral, ShouldRelease); + if not ShouldRelease then begin + UpdateWindow(CustomerContractDeferral."Subscription Contract No."); exit; - CheckGenPostingSetup(GenBusPostingGroup, GenProdPostingGroup, Enum::"Service Partner"::Customer); - ReleaseContractDeferral(Enum::"Service Partner"::Customer, CustomerContractDeferral."Entry No."); - InsertTempGenJournalLine( - CustomerContractDeferral."Document No.", - CustomerContractDeferral."Subscription Contract No.", - CustomerContractDeferral."Entry No.", - CustomerContractDeferral."Dimension Set ID", - GenPostingSetup."Cust. Sub. Contract Account", - GenPostingSetup."Cust. Sub. Contr. Def Account", - GenBusPostingGroup, - GenProdPostingGroup, - GetPostingAmount(CustomerContractDeferral.Amount, CustomerContractDeferral."Discount Amount")); + end; + + if (CustomerContractDeferral.Amount <> 0) or (CustomerContractDeferral."Discount Amount" <> 0) then + CheckGenPostingSetup(CustomerContractDeferral."Gen. Bus. Posting Group", CustomerContractDeferral."Gen. Prod. Posting Group", Enum::"Service Partner"::Customer); + + CustomerContractDeferral.Released := true; + CustomerContractDeferral."Release Posting Date" := PostingDate; + CustomerContractDeferral.Modify(false); + + PostingAmount := GetPostingAmount(CustomerContractDeferral.Amount, CustomerContractDeferral."Discount Amount"); + if PostingAmount <> 0 then + InsertTempGenJournalLine( + CustomerContractDeferral."Document No.", + CustomerContractDeferral."Subscription Contract No.", + CustomerContractDeferral."Entry No.", + CustomerContractDeferral."Dimension Set ID", + GenPostingSetup."Cust. Sub. Contract Account", + GenPostingSetup."Cust. Sub. Contr. Def Account", + CustomerContractDeferral."Gen. Bus. Posting Group", + CustomerContractDeferral."Gen. Prod. Posting Group", + PostingAmount); + if LineDiscountPosting and (CustomerContractDeferral."Discount Amount" <> 0) then InsertTempGenJournalLine( CustomerContractDeferral."Document No.", @@ -165,8 +178,8 @@ report 8051 "Contract Deferrals Release" CustomerContractDeferral."Dimension Set ID", GenPostingSetup."Sales Line Disc. Account", GenPostingSetup."Cust. Sub. Contr. Def Account", - GenBusPostingGroup, - GenProdPostingGroup, + CustomerContractDeferral."Gen. Bus. Posting Group", + CustomerContractDeferral."Gen. Prod. Posting Group", -CustomerContractDeferral."Discount Amount"); end; @@ -180,25 +193,38 @@ report 8051 "Contract Deferrals Release" end; end; - internal procedure ReleaseVendorContractDeferralsAndInsertTempGenJournalLines(var VendorContractDeferral: Record "Vend. Sub. Contract Deferral") + internal procedure ReleaseVendorContractDeferralsAndInsertTempGenJournalLines(VendorContractDeferral: Record "Vend. Sub. Contract Deferral") var - GenBusPostingGroup: Code[20]; - GenProdPostingGroup: Code[20]; + ShouldRelease: Boolean; + PostingAmount: Decimal; begin - if not VendorContractDeferral.GetDocumentPostingGroups(GenBusPostingGroup, GenProdPostingGroup) then + ShouldRelease := true; + OnBeforeReleaseVendorContractDeferral(VendorContractDeferral, ShouldRelease); + if not ShouldRelease then begin + UpdateWindow(VendorContractDeferral."Subscription Contract No."); exit; - CheckGenPostingSetup(GenBusPostingGroup, GenProdPostingGroup, Enum::"Service Partner"::Vendor); - ReleaseContractDeferral(Enum::"Service Partner"::Vendor, VendorContractDeferral."Entry No."); - InsertTempGenJournalLine( - VendorContractDeferral."Document No.", - VendorContractDeferral."Subscription Contract No.", - VendorContractDeferral."Entry No.", - VendorContractDeferral."Dimension Set ID", - GenPostingSetup."Vend. Sub. Contract Account", - GenPostingSetup."Vend. Sub. Contr. Def. Account", - GenBusPostingGroup, - GenProdPostingGroup, - GetPostingAmount(VendorContractDeferral.Amount, VendorContractDeferral."Discount Amount")); + end; + + if (VendorContractDeferral.Amount <> 0) or (VendorContractDeferral."Discount Amount" <> 0) then + CheckGenPostingSetup(VendorContractDeferral."Gen. Bus. Posting Group", VendorContractDeferral."Gen. Prod. Posting Group", Enum::"Service Partner"::Vendor); + + VendorContractDeferral.Released := true; + VendorContractDeferral."Release Posting Date" := PostingDate; + VendorContractDeferral.Modify(false); + + PostingAmount := GetPostingAmount(VendorContractDeferral.Amount, VendorContractDeferral."Discount Amount"); + if PostingAmount <> 0 then + InsertTempGenJournalLine( + VendorContractDeferral."Document No.", + VendorContractDeferral."Subscription Contract No.", + VendorContractDeferral."Entry No.", + VendorContractDeferral."Dimension Set ID", + GenPostingSetup."Vend. Sub. Contract Account", + GenPostingSetup."Vend. Sub. Contr. Def. Account", + VendorContractDeferral."Gen. Bus. Posting Group", + VendorContractDeferral."Gen. Prod. Posting Group", + PostingAmount); + if LineDiscountPosting and (VendorContractDeferral."Discount Amount" <> 0) then InsertTempGenJournalLine( VendorContractDeferral."Document No.", @@ -207,8 +233,8 @@ report 8051 "Contract Deferrals Release" VendorContractDeferral."Dimension Set ID", GenPostingSetup."Purch. Line Disc. Account", GenPostingSetup."Vend. Sub. Contr. Def. Account", - GenBusPostingGroup, - GenProdPostingGroup, + VendorContractDeferral."Gen. Bus. Posting Group", + VendorContractDeferral."Gen. Prod. Posting Group", -VendorContractDeferral."Discount Amount"); end; @@ -249,27 +275,6 @@ report 8051 "Contract Deferrals Release" end; end; - local procedure ReleaseContractDeferral(Partner: Enum "Service Partner"; DeferralEntryNo: Integer) - var - CustContractDeferralsToUpdate: Record "Cust. Sub. Contract Deferral"; - VendContractDeferralsToUpdate: Record "Vend. Sub. Contract Deferral"; - begin - case Partner of - Enum::"Service Partner"::Customer: - if CustContractDeferralsToUpdate.Get(DeferralEntryNo) then begin - CustContractDeferralsToUpdate.Released := true; - CustContractDeferralsToUpdate."Release Posting Date" := PostingDate; - CustContractDeferralsToUpdate.Modify(false); - end; - Enum::"Service Partner"::Vendor: - if VendContractDeferralsToUpdate.Get(DeferralEntryNo) then begin - VendContractDeferralsToUpdate.Released := true; - VendContractDeferralsToUpdate."Release Posting Date" := PostingDate; - VendContractDeferralsToUpdate.Modify(false); - end; - end; - end; - local procedure GetPostingAmount(Amount: Decimal; DiscountAmount: Decimal): Decimal begin if LineDiscountPosting then @@ -406,4 +411,14 @@ report 8051 "Contract Deferrals Release" begin AllowGUI := NewAllowGUI; end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeReleaseCustomerContractDeferral(var CustomerContractDeferral: Record "Cust. Sub. Contract Deferral"; var ShouldReleaseDeferral: Boolean) + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeReleaseVendorContractDeferral(var VendorContractDeferral: Record "Vend. Sub. Contract Deferral"; var ShouldReleaseDeferral: Boolean) + begin + end; } diff --git a/src/Apps/W1/Subscription Billing/App/Deferrals/Reports/CustContrDefAnalysis.Report.al b/src/Apps/W1/Subscription Billing/App/Deferrals/Reports/CustContrDefAnalysis.Report.al index b4ea806dc0..17ee3d9fc8 100644 --- a/src/Apps/W1/Subscription Billing/App/Deferrals/Reports/CustContrDefAnalysis.Report.al +++ b/src/Apps/W1/Subscription Billing/App/Deferrals/Reports/CustContrDefAnalysis.Report.al @@ -221,6 +221,7 @@ report 8052 "Cust. Contr. Def. Analysis" CustomerContractDeferral.SetRange("Subscription Contract No.", SourceContractNo); CustomerContractDeferral.SetFilter("Document Type", Format(DocumentTypeFilter)); CustomerContractDeferral.SetFilter("Document No.", DocumentNoFilter); + OnAfterSetContractDeferralFilter(CustomerContractDeferral, SourceContractNo); end; local procedure SetPeriodFilter() @@ -314,4 +315,9 @@ report 8052 "Cust. Contr. Def. Analysis" if CustomerContractDeferral.FindLast() then DateLastRelease := CustomerContractDeferral."Posting Date"; end; + + [IntegrationEvent(false, false)] + local procedure OnAfterSetContractDeferralFilter(var CustomerContractDeferral: Record "Cust. Sub. Contract Deferral"; SourceContractNo: Code[20]) + begin + end; } diff --git a/src/Apps/W1/Subscription Billing/App/Deferrals/Reports/VendContrDefAnalysis.Report.al b/src/Apps/W1/Subscription Billing/App/Deferrals/Reports/VendContrDefAnalysis.Report.al index 94abadaf5f..420c522975 100644 --- a/src/Apps/W1/Subscription Billing/App/Deferrals/Reports/VendContrDefAnalysis.Report.al +++ b/src/Apps/W1/Subscription Billing/App/Deferrals/Reports/VendContrDefAnalysis.Report.al @@ -221,6 +221,7 @@ report 8053 "Vend Contr. Def. Analysis" VendorContractDeferral.SetRange("Subscription Contract No.", SourceContractNo); VendorContractDeferral.SetFilter("Document Type", Format(DocumentTypeFilter)); VendorContractDeferral.SetFilter("Document No.", DocumentNoFilter); + OnAfterSetContractDeferralFilter(VendorContractDeferral, SourceContractNo); end; local procedure SetPeriodFilter() @@ -314,4 +315,9 @@ report 8053 "Vend Contr. Def. Analysis" if VendorContractDeferral.FindLast() then DateLastRelease := VendorContractDeferral."Posting Date"; end; + + [IntegrationEvent(false, false)] + local procedure OnAfterSetContractDeferralFilter(var VendorContractDeferral: Record "Vend. Sub. Contract Deferral"; SourceContractNo: Code[20]) + begin + end; } diff --git a/src/Apps/W1/Subscription Billing/App/Deferrals/Tables/CustSubContractDeferral.Table.al b/src/Apps/W1/Subscription Billing/App/Deferrals/Tables/CustSubContractDeferral.Table.al index 5c8535cef1..613788f4a7 100644 --- a/src/Apps/W1/Subscription Billing/App/Deferrals/Tables/CustSubContractDeferral.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Deferrals/Tables/CustSubContractDeferral.Table.al @@ -2,6 +2,7 @@ namespace Microsoft.SubscriptionBilling; using Microsoft.Finance.Dimension; using Microsoft.Finance.GeneralLedger.Ledger; +using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Sales.Customer; using Microsoft.Sales.Document; using Microsoft.Sales.History; @@ -134,6 +135,18 @@ table 8066 "Cust. Sub. Contract Deferral" { Caption = 'Currency Code'; } + field(74; "Gen. Bus. Posting Group"; Code[20]) + { + Caption = 'Gen. Bus. Posting Group'; + ToolTip = 'Specifies the general business posting group.'; + TableRelation = "Gen. Business Posting Group"; + } + field(75; "Gen. Prod. Posting Group"; Code[20]) + { + Caption = 'Gen. Prod. Posting Group'; + ToolTip = 'Specifies the general product posting group.'; + TableRelation = "Gen. Product Posting Group"; + } field(480; "Dimension Set ID"; Integer) { Caption = 'Dimension Set ID'; @@ -180,6 +193,9 @@ table 8066 "Cust. Sub. Contract Deferral" Rec."Document Posting Date" := SalesLine."Posting Date"; Rec.Discount := SalesLine."Discount"; Rec."Currency Code" := SalesLine."Currency Code"; + Rec."Gen. Bus. Posting Group" := SalesLine."Gen. Bus. Posting Group"; + Rec."Gen. Prod. Posting Group" := SalesLine."Gen. Prod. Posting Group"; + OnAfterInitFromSalesLine(Rec, SalesLine, Sign); end; internal procedure ShowDimensions() @@ -217,4 +233,9 @@ table 8066 "Cust. Sub. Contract Deferral" end; exit(false); end; + + [IntegrationEvent(false, false)] + local procedure OnAfterInitFromSalesLine(var CustSubContractDeferral: Record "Cust. Sub. Contract Deferral"; SalesLine: Record "Sales Line"; var Sign: Integer) + begin + end; } diff --git a/src/Apps/W1/Subscription Billing/App/Deferrals/Tables/VendSubContractDeferral.Table.al b/src/Apps/W1/Subscription Billing/App/Deferrals/Tables/VendSubContractDeferral.Table.al index 7423680bb5..533001fc49 100644 --- a/src/Apps/W1/Subscription Billing/App/Deferrals/Tables/VendSubContractDeferral.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Deferrals/Tables/VendSubContractDeferral.Table.al @@ -2,6 +2,7 @@ namespace Microsoft.SubscriptionBilling; using Microsoft.Finance.Dimension; using Microsoft.Finance.GeneralLedger.Ledger; +using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Purchases.Document; using Microsoft.Purchases.History; using Microsoft.Purchases.Vendor; @@ -135,6 +136,18 @@ table 8072 "Vend. Sub. Contract Deferral" { Caption = 'Currency Code'; } + field(74; "Gen. Bus. Posting Group"; Code[20]) + { + Caption = 'Gen. Bus. Posting Group'; + ToolTip = 'Specifies the general business posting group.'; + TableRelation = "Gen. Business Posting Group"; + } + field(75; "Gen. Prod. Posting Group"; Code[20]) + { + Caption = 'Gen. Prod. Posting Group'; + ToolTip = 'Specifies the general product posting group.'; + TableRelation = "Gen. Product Posting Group"; + } field(480; "Dimension Set ID"; Integer) { Caption = 'Dimension Set ID'; @@ -180,6 +193,9 @@ table 8072 "Vend. Sub. Contract Deferral" Rec."Pay-to Vendor No." := PurchaseLine."Pay-to Vendor No."; Rec.Discount := PurchaseLine."Discount"; Rec."Currency Code" := PurchaseLine."Currency Code"; + Rec."Gen. Bus. Posting Group" := PurchaseLine."Gen. Bus. Posting Group"; + Rec."Gen. Prod. Posting Group" := PurchaseLine."Gen. Prod. Posting Group"; + OnAfterInitFromPurchaseLine(Rec, PurchaseLine, Sign); end; internal procedure ShowDimensions() @@ -217,4 +233,9 @@ table 8072 "Vend. Sub. Contract Deferral" end; exit(false); end; + + [IntegrationEvent(false, false)] + local procedure OnAfterInitFromPurchaseLine(var VendSubContractDeferral: Record "Vend. Sub. Contract Deferral"; PurchaseLine: Record "Purchase Line"; var Sign: Integer) + begin + end; } diff --git a/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingObjects.PermissionSet.al b/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingObjects.PermissionSet.al index f607846052..9810690500 100644 --- a/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingObjects.PermissionSet.al +++ b/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingObjects.PermissionSet.al @@ -23,6 +23,9 @@ permissionset 8001 "Sub. Billing Objects" codeunit "Customer Management" = X, codeunit "Date Formula Management" = X, codeunit "Date Time Management" = X, + codeunit "Deferral Post. Preview Binding" = X, + codeunit "Deferral Post. Preview Handler" = X, + codeunit "Deferral Post. Preview Subscr." = X, codeunit "Dimension Mgt." = X, codeunit "Document Change Management" = X, codeunit "Extend Sub. Contract Mgt." = X, diff --git a/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/PurchaseLine.TableExt.al b/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/PurchaseLine.TableExt.al index 4c623085bf..e7d79f1bfb 100644 --- a/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/PurchaseLine.TableExt.al +++ b/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/PurchaseLine.TableExt.al @@ -31,10 +31,21 @@ tableextension 8065 "Purchase Line" extends "Purchase Line" FieldClass = FlowField; CalcFormula = exist("Billing Line" where("Document Type" = filter(Invoice), "Document No." = field("Document No."), "Document Line No." = field("Line No."))); } + modify("Deferral Code") + { + trigger OnAfterValidate() + begin + if Rec."Deferral Code" <> '' then + if Rec.IsLineAttachedToBillingLine() then + if Rec.CreateContractDeferrals() then + Error(DeferralCodeCannotBeUsedWithContractDeferralsErr); + end; + } } var DimMgt: Codeunit DimensionManagement; + DeferralCodeCannotBeUsedWithContractDeferralsErr: Label 'A Deferral Code cannot be used on a line where Subscription Contract Deferrals are active. Either remove the Deferral Code or disable Contract Deferrals on the subscription line or contract.'; internal procedure GetCombinedDimensionSetID(DimSetID1: Integer; DimSetID2: Integer) var diff --git a/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/SalesLine.TableExt.al b/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/SalesLine.TableExt.al index 651f3d97d8..c1047609e4 100644 --- a/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/SalesLine.TableExt.al +++ b/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/SalesLine.TableExt.al @@ -143,6 +143,16 @@ tableextension 8054 "Sales Line" extends "Sales Line" Error(Item.GetDoNotAllowInvoiceDiscountForServiceCommitmentItemErrorText()); end; } + modify("Deferral Code") + { + trigger OnAfterValidate() + begin + if Rec."Deferral Code" <> '' then + if Rec.IsLineAttachedToBillingLine() then + if Rec.CreateContractDeferrals() then + Error(DeferralCodeCannotBeUsedWithContractDeferralsErr); + end; + } } var BillingLineExist, IsBillingLineCached : Boolean; @@ -168,6 +178,7 @@ tableextension 8054 "Sales Line" extends "Sales Line" var DimMgt: Codeunit DimensionManagement; TypeCannotBeSelectedManuallyErr: Label 'Type "%1" cannot be selected manually.', Comment = '%1 = Sales Line Type'; + DeferralCodeCannotBeUsedWithContractDeferralsErr: Label 'A Deferral Code cannot be used on a line where Subscription Contract Deferrals are active. Either remove the Deferral Code or disable Contract Deferrals on the subscription line or contract.'; procedure InitFromSalesHeader(SourceSalesHeader: Record "Sales Header") begin diff --git a/src/Apps/W1/Subscription Billing/Test/Deferrals/CustomerDeferralsTest.Codeunit.al b/src/Apps/W1/Subscription Billing/Test/Deferrals/CustomerDeferralsTest.Codeunit.al index e4ca028654..a63091c360 100644 --- a/src/Apps/W1/Subscription Billing/Test/Deferrals/CustomerDeferralsTest.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/Test/Deferrals/CustomerDeferralsTest.Codeunit.al @@ -1,6 +1,7 @@ namespace Microsoft.SubscriptionBilling; using Microsoft.Finance.Currency; +using Microsoft.Finance.Deferral; using Microsoft.Finance.GeneralLedger.Ledger; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Inventory.Item; @@ -47,6 +48,7 @@ codeunit 139912 "Customer Deferrals Test" LibraryTestInitialize: Codeunit "Library - Test Initialize"; LibraryRandom: Codeunit "Library - Random"; LibrarySales: Codeunit "Library - Sales"; + LibraryERM: Codeunit "Library - ERM"; CorrectedDocumentNo: Code[20]; PostedDocumentNo: Code[20]; PostingDate: Date; @@ -234,6 +236,55 @@ codeunit 139912 "Customer Deferrals Test" until CustomerContractDeferral.Next() = 0; end; + [Test] + [HandlerFunctions('CreateCustomerBillingDocsContractPageHandler,MessageHandler')] + procedure CheckContractDeferralsWhenStartDateIsOnFirstDayInMonthAndEndDateIsMidMonthLCY() + var + DeferralCount: Integer; + FullMonthAmount: Decimal; + TotalDeferralBaseAmount: Decimal; + i: Integer; + LastDayOfBillingPeriod: Date; + begin + // [SCENARIO] When billing starts on 1st of month and ends mid-month (partial last month), + // full months get equal deferral amounts and the partial last month gets a day-proportioned amount. + Initialize(); + + // [GIVEN] A customer contract with deferrals starting on Jan 1 + CreateCustomerContractWithDeferrals('<-CY>', true); + + // [WHEN] Billing from Jan 1 to Jul 23 (partial last month) and document is posted + CreateBillingProposalAndCreateBillingDocuments('<-CY>', '<-CY+6M+22D>'); + PostSalesDocumentAndFetchDeferrals(); + + DeferralCount := CustomerContractDeferral.Count; + TotalDeferralBaseAmount := CustomerContractDeferral."Deferral Base Amount"; + LastDayOfBillingPeriod := CalcDate('<-CY+6M+22D>', WorkDate()); + + // [THEN] 7 deferral periods are created (Jan through Jul) + Assert.AreEqual(7, DeferralCount, 'Expected 7 deferral periods for Jan to Jul billing.'); + + // Use the first full-month deferral amount as reference; verify all other full months match it + FullMonthAmount := CustomerContractDeferral.Amount; + + // [THEN] The first 6 full-month periods have equal amounts and full month Number of Days + for i := 1 to DeferralCount - 1 do begin + Assert.AreEqual(FullMonthAmount, CustomerContractDeferral.Amount, 'Full month deferrals should have equal amounts.'); + CustomerContractDeferral.TestField("Number of Days", Date2DMY(CalcDate('', CustomerContractDeferral."Posting Date"), 1)); + CustomerContractDeferral.Next(); + end; + + // [THEN] Last partial month has a different (day-proportioned) amount and correct Number of Days + Assert.AreNotEqual(FullMonthAmount, CustomerContractDeferral.Amount, 'Partial last month should have a different amount than full months.'); + CustomerContractDeferral.TestField("Number of Days", Date2DMY(LastDayOfBillingPeriod, 1)); + + // [THEN] Sum of all deferral amounts equals the total deferral base amount + CustomerContractDeferral.Reset(); + CustomerContractDeferral.SetRange("Document No.", PostedDocumentNo); + CustomerContractDeferral.CalcSums(Amount); + Assert.AreEqual(TotalDeferralBaseAmount, CustomerContractDeferral.Amount, 'Sum of deferral amounts must equal the deferral base amount.'); + end; + [Test] [HandlerFunctions('CreateCustomerBillingDocsContractPageHandler,MessageHandler')] procedure DeferralsAreCorrectAfterPostingPartialSalesCreditMemo() @@ -545,16 +596,20 @@ codeunit 139912 "Customer Deferrals Test" procedure TestIfDeferralsExistOnAfterPostSalesCreditMemoWithoutAppliesToDocNo() begin Initialize(); + // [GIVEN] Contract has been created and the billing proposal with a posted contract invoice CreateCustomerContractWithDeferrals('<2M-CM>', true); CreateBillingProposalAndCreateBillingDocuments('<2M-CM>', '<8M+CM>'); PostSalesDocumentAndGetSalesInvoice(); + // [WHEN] A credit memo is created from the posted invoice but without any link to the original invoice CorrectPostedSalesInvoice.CreateCreditMemoCopyDocument(SalesInvoiceHeader, SalesCrMemoHeader); - // Force Applies to Doc No. and Doc Type to be empty - SalesCrMemoHeader."Applies-to Doc. Type" := SalesCrMemoHeader."Applies-to Doc. Type"::Invoice; + SalesCrMemoHeader."Applies-to Doc. Type" := SalesCrMemoHeader."Applies-to Doc. Type"::" "; SalesCrMemoHeader."Applies-to Doc. No." := ''; SalesCrMemoHeader.Modify(false); + ClearCorrectionDocumentNoFromBillingLines(SalesCrMemoHeader."No."); CorrectedDocumentNo := LibrarySales.PostSalesDocument(SalesCrMemoHeader, true, true); + + // [THEN] Deferral entries are created for the standalone credit memo FetchCustomerContractDeferrals(CorrectedDocumentNo); end; @@ -747,6 +802,96 @@ codeunit 139912 "Customer Deferrals Test" SubscriptionHeader.Modify(true); end; + [Test] + [HandlerFunctions('CreateCustomerBillingDocsContractPageHandler,MessageHandler')] + procedure TestPostingGroupsAreFilledOnCustomerContractDeferrals() + var + TotalDeferralCount: Integer; + begin + // [SCENARIO] When posting a sales invoice with contract deferrals, the Gen. Bus. Posting Group and Gen. Prod. Posting Group fields are populated on the deferral entries. + Initialize(); + + // [GIVEN] A customer contract with deferrals + CreateCustomerContractWithDeferrals('<2M-CM>', true); + CreateBillingProposalAndCreateBillingDocuments('<2M-CM>', '<8M+CM>'); + + // [WHEN] The sales document is posted + PostSalesDocumentAndFetchDeferrals(); + + // [THEN] Gen. Bus. Posting Group and Gen. Prod. Posting Group are filled on each deferral entry + SalesInvoiceLine.SetRange("Document No.", PostedDocumentNo); + SalesInvoiceLine.SetFilter("No.", '<>%1', ''); + SalesInvoiceLine.FindFirst(); + CustomerContractDeferral.SetRange("Document No.", PostedDocumentNo); + TotalDeferralCount := CustomerContractDeferral.Count; + CustomerContractDeferral.SetRange("Gen. Bus. Posting Group", SalesInvoiceLine."Gen. Bus. Posting Group"); + CustomerContractDeferral.SetRange("Gen. Prod. Posting Group", SalesInvoiceLine."Gen. Prod. Posting Group"); + Assert.RecordIsNotEmpty(CustomerContractDeferral); + Assert.RecordCount(CustomerContractDeferral, TotalDeferralCount); + end; + + [Test] + [HandlerFunctions('CreateCustomerBillingDocsContractPageHandler,ContractDeferralsReleaseRequestPageHandler,MessageHandler')] + procedure TestZeroAmountDeferralsReleasedWithoutGLEntries() + var + ContractDeferralsRelease: Report "Contract Deferrals Release"; + begin + // [SCENARIO] Zero-amount customer contract deferrals should be marked as released without creating GL entries. + Initialize(); + SetPostingAllowTo(0D); + + // [GIVEN] A customer contract with deferrals that have been posted + CreateCustomerContractWithDeferrals('<2M-CM>', true); + CreateBillingProposalAndCreateBillingDocuments('<2M-CM>', '<8M+CM>'); + PostSalesDocumentAndFetchDeferrals(); + + // [GIVEN] All deferral amounts are set to 0 (simulating zero-amount service commitments) + CustomerContractDeferral.Reset(); + CustomerContractDeferral.SetRange("Document No.", PostedDocumentNo); + CustomerContractDeferral.ModifyAll(Amount, 0, false); + CustomerContractDeferral.ModifyAll("Discount Amount", 0, false); + CustomerContractDeferral.FindFirst(); + + // [WHEN] Release deferrals is run + PostingDate := CustomerContractDeferral."Posting Date"; + Commit(); + ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler + + // [THEN] The first deferral is released without creating a GL entry + CustomerContractDeferral.Get(CustomerContractDeferral."Entry No."); + CustomerContractDeferral.TestField(Released, true); + CustomerContractDeferral.TestField("G/L Entry No.", 0); + end; + + [Test] + [HandlerFunctions('CreateCustomerBillingDocsContractPageHandler,MessageHandler')] + procedure DeferralCodeNotAllowedWithContractDeferralsOnSalesLine() + var + DeferralTemplate: Record "Deferral Template"; + DeferralCodeCannotBeUsedWithContractDeferralsErr: Label 'A Deferral Code cannot be used on a line where Subscription Contract Deferrals are active. Either remove the Deferral Code or disable Contract Deferrals on the subscription line or contract.', Locked = true; + begin + // [SCENARIO] A standard Deferral Code must not be assigned to a sales invoice line + // that already has subscription contract deferrals enabled. + Initialize(); + + // [GIVEN] A customer contract with deferrals enabled and a billing document + CreateCustomerContractWithDeferrals('<2M-CM>', true); + CreateBillingProposalAndCreateBillingDocuments('<2M-CM>', '<8M+CM>'); + + // [GIVEN] A standard BC deferral template + LibraryERM.CreateDeferralTemplate(DeferralTemplate, Enum::"Deferral Calculation Method"::"Straight-Line", + Enum::"Deferral Calculation Start Date"::"Posting Date", 3); + + // [WHEN] Assigning the Deferral Code to the sales line that has contract deferrals + SalesLine.SetRange("Document No.", SalesHeader."No."); + SalesLine.SetFilter("No.", '<>%1', ''); + SalesLine.FindFirst(); + + // [THEN] An error is raised preventing double deferrals + asserterror SalesLine.Validate("Deferral Code", DeferralTemplate."Deferral Code"); + Assert.ExpectedError(DeferralCodeCannotBeUsedWithContractDeferralsErr); + end; + #endregion Tests #region Procedures @@ -865,6 +1010,16 @@ codeunit 139912 "Customer Deferrals Test" CustomerContractDeferral.FindFirst(); end; + local procedure ClearCorrectionDocumentNoFromBillingLines(CreditMemoDocumentNo: Code[20]) + var + CrMemoBillingLine: Record "Billing Line"; + begin + CrMemoBillingLine.SetRange(Partner, CrMemoBillingLine.Partner::Customer); + CrMemoBillingLine.SetRange("Document Type", CrMemoBillingLine."Document Type"::"Credit Memo"); + CrMemoBillingLine.SetRange("Document No.", CreditMemoDocumentNo); + CrMemoBillingLine.ModifyAll("Correction Document No.", '', false); + end; + local procedure GetCalculatedMonthAmountsForDeferrals(SourceDeferralBaseAmount: Decimal; NumberOfPeriods: Integer; FirstDayOfBillingPeriod: Date; LastDayOfBillingPeriod: Date; CalculateInLCY: Boolean) var DailyDefBaseAmount: Decimal; diff --git a/src/Apps/W1/Subscription Billing/Test/Deferrals/VendorDeferralsTest.Codeunit.al b/src/Apps/W1/Subscription Billing/Test/Deferrals/VendorDeferralsTest.Codeunit.al index ac5df83204..e88b16bcc9 100644 --- a/src/Apps/W1/Subscription Billing/Test/Deferrals/VendorDeferralsTest.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/Test/Deferrals/VendorDeferralsTest.Codeunit.al @@ -1,6 +1,7 @@ namespace Microsoft.SubscriptionBilling; using Microsoft.Finance.Currency; +using Microsoft.Finance.Deferral; using Microsoft.Finance.GeneralLedger.Account; using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Finance.GeneralLedger.Ledger; @@ -49,6 +50,7 @@ codeunit 139913 "Vendor Deferrals Test" LibraryTestInitialize: Codeunit "Library - Test Initialize"; LibraryPurchase: Codeunit "Library - Purchase"; LibraryUtility: Codeunit "Library - Utility"; + LibraryERM: Codeunit "Library - ERM"; CorrectedDocumentNo: Code[20]; PostedDocumentNo: Code[20]; PostingDate: Date; @@ -241,6 +243,55 @@ codeunit 139913 "Vendor Deferrals Test" until VendorContractDeferral.Next() = 0; end; + [Test] + [HandlerFunctions('CreateVendorBillingDocsContractPageHandler,MessageHandler')] + procedure CheckContractDeferralsWhenStartDateIsOnFirstDayInMonthAndEndDateIsMidMonthLCY() + var + DeferralCount: Integer; + FullMonthAmount: Decimal; + TotalDeferralBaseAmount: Decimal; + i: Integer; + LastDayOfBillingPeriod: Date; + begin + // [SCENARIO] When billing starts on 1st of month and ends mid-month (partial last month), + // full months get equal deferral amounts and the partial last month gets a day-proportioned amount. + Initialize(); + + // [GIVEN] A vendor contract with deferrals starting on Jan 1 + CreateVendorContractWithDeferrals('<-CY>', true); + + // [WHEN] Billing from Jan 1 to Jul 23 (partial last month) and document is posted + CreateBillingProposalAndCreateBillingDocuments('<-CY>', '<-CY+6M+22D>'); + PostPurchDocumentAndFetchDeferrals(); + + DeferralCount := VendorContractDeferral.Count; + TotalDeferralBaseAmount := VendorContractDeferral."Deferral Base Amount"; + LastDayOfBillingPeriod := CalcDate('<-CY+6M+22D>', WorkDate()); + + // [THEN] 7 deferral periods are created (Jan through Jul) + Assert.AreEqual(7, DeferralCount, 'Expected 7 deferral periods for Jan to Jul billing.'); + + // Use the first full-month deferral amount as reference; verify all other full months match it + FullMonthAmount := VendorContractDeferral.Amount; + + // [THEN] The first 6 full-month periods have equal amounts and full month Number of Days + for i := 1 to DeferralCount - 1 do begin + Assert.AreEqual(FullMonthAmount, VendorContractDeferral.Amount, 'Full month deferrals should have equal amounts.'); + VendorContractDeferral.TestField("Number of Days", Date2DMY(CalcDate('', VendorContractDeferral."Posting Date"), 1)); + VendorContractDeferral.Next(); + end; + + // [THEN] Last partial month has a different (day-proportioned) amount and correct Number of Days + Assert.AreNotEqual(FullMonthAmount, VendorContractDeferral.Amount, 'Partial last month should have a different amount than full months.'); + VendorContractDeferral.TestField("Number of Days", Date2DMY(LastDayOfBillingPeriod, 1)); + + // [THEN] Sum of all deferral amounts equals the total deferral base amount + VendorContractDeferral.Reset(); + VendorContractDeferral.SetRange("Document No.", PostedDocumentNo); + VendorContractDeferral.CalcSums(Amount); + Assert.AreEqual(TotalDeferralBaseAmount, VendorContractDeferral.Amount, 'Sum of deferral amounts must equal the deferral base amount.'); + end; + [Test] [HandlerFunctions('CreateVendorBillingDocsContractPageHandler,MessageHandler')] procedure DeferralsAreCorrectAfterPostingPartialPurchCreditMemo() @@ -584,15 +635,21 @@ codeunit 139913 "Vendor Deferrals Test" procedure TestIfDeferralsExistOnAfterPostPurchCreditMemoWithoutAppliesToDocNo() begin Initialize(); + // [GIVEN] Contract has been created and the billing proposal with a posted contract invoice CreateVendorContractWithDeferrals('<2M-CM>', true); CreateBillingProposalAndCreateBillingDocuments('<2M-CM>', '<8M+CM>'); PostPurchDocumentAndGetPurchInvoice(); + + // [WHEN] A credit memo is created from the posted invoice but without any link to the original invoice CorrectPostedPurchaseInvoice.CreateCreditMemoCopyDocument(PurchaseInvoiceHeader, PurchaseCrMemoHeader); PurchaseCrMemoHeader.Validate("Vendor Cr. Memo No.", LibraryUtility.GenerateGUID()); PurchaseCrMemoHeader."Applies-to Doc. Type" := PurchaseCrMemoHeader."Applies-to Doc. Type"::" "; PurchaseCrMemoHeader."Applies-to Doc. No." := ''; PurchaseCrMemoHeader.Modify(false); + ClearCorrectionDocumentNoFromBillingLines(PurchaseCrMemoHeader."No."); CorrectedDocumentNo := LibraryPurchase.PostPurchaseDocument(PurchaseCrMemoHeader, true, true); + + // [THEN] Deferral entries are created for the standalone credit memo FetchVendorContractDeferrals(CorrectedDocumentNo); end; @@ -756,6 +813,98 @@ codeunit 139913 "Vendor Deferrals Test" Assert.IsTrue(PurchaseLine2.CreateContractDeferrals(), FunctionReturnedWrongResultErr); end; + [Test] + [HandlerFunctions('CreateVendorBillingDocsContractPageHandler,MessageHandler')] + procedure TestPostingGroupsAreFilledOnVendorContractDeferrals() + var + TotalDeferralCount: Integer; + begin + // [SCENARIO] When posting a purchase invoice with contract deferrals, the Gen. Bus. Posting Group and Gen. Prod. Posting Group fields are populated on the deferral entries. + Initialize(); + + // [GIVEN] A vendor contract with deferrals + CreateVendorContractWithDeferrals('<2M-CM>', true); + CreateBillingProposalAndCreateBillingDocuments('<2M-CM>', '<8M+CM>'); + + // [WHEN] The purchase document is posted + BillingLine.FindLast(); + PostPurchDocumentAndFetchDeferrals(); + + // [THEN] Gen. Bus. Posting Group and Gen. Prod. Posting Group are filled on each deferral entry + PurchInvLine.SetRange("Document No.", PostedDocumentNo); + PurchInvLine.SetFilter("No.", '<>%1', ''); + PurchInvLine.FindFirst(); + VendorContractDeferral.SetRange("Document No.", PostedDocumentNo); + TotalDeferralCount := VendorContractDeferral.Count; + VendorContractDeferral.SetRange("Gen. Bus. Posting Group", PurchInvLine."Gen. Bus. Posting Group"); + VendorContractDeferral.SetRange("Gen. Prod. Posting Group", PurchInvLine."Gen. Prod. Posting Group"); + Assert.RecordIsNotEmpty(VendorContractDeferral); + Assert.RecordCount(VendorContractDeferral, TotalDeferralCount); + end; + + [Test] + [HandlerFunctions('CreateVendorBillingDocsContractPageHandler,ContractDeferralsReleaseRequestPageHandler,MessageHandler')] + procedure TestZeroAmountVendorDeferralsReleasedWithoutGLEntries() + var + ContractDeferralsRelease: Report "Contract Deferrals Release"; + begin + // [SCENARIO] Zero-amount vendor contract deferrals should be marked as released without creating GL entries. + Initialize(); + SetPostingAllowTo(0D); + + // [GIVEN] A vendor contract with deferrals that have been posted + CreateVendorContractWithDeferrals('<2M-CM>', true); + CreateBillingProposalAndCreateBillingDocuments('<2M-CM>', '<8M+CM>'); + BillingLine.FindLast(); + PostPurchDocumentAndFetchDeferrals(); + + // [GIVEN] All deferral amounts are set to 0 (simulating zero-amount service commitments) + VendorContractDeferral.Reset(); + VendorContractDeferral.SetRange("Document No.", PostedDocumentNo); + VendorContractDeferral.ModifyAll(Amount, 0, false); + VendorContractDeferral.ModifyAll("Discount Amount", 0, false); + VendorContractDeferral.FindFirst(); + + // [WHEN] Release deferrals is run + PostingDate := VendorContractDeferral."Posting Date"; + Commit(); + ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler + + // [THEN] The first deferral is released without creating a GL entry + VendorContractDeferral.Get(VendorContractDeferral."Entry No."); + VendorContractDeferral.TestField(Released, true); + VendorContractDeferral.TestField("G/L Entry No.", 0); + end; + + [Test] + [HandlerFunctions('CreateVendorBillingDocsContractPageHandler,MessageHandler')] + procedure DeferralCodeNotAllowedWithContractDeferralsOnPurchaseLine() + var + DeferralTemplate: Record "Deferral Template"; + DeferralCodeCannotBeUsedWithContractDeferralsErr: Label 'A Deferral Code cannot be used on a line where Subscription Contract Deferrals are active. Either remove the Deferral Code or disable Contract Deferrals on the subscription line or contract.', Locked = true; + begin + // [SCENARIO] A standard Deferral Code must not be assigned to a purchase invoice line + // that already has subscription contract deferrals enabled. + Initialize(); + + // [GIVEN] A vendor contract with deferrals enabled and a billing document + CreateVendorContractWithDeferrals('<2M-CM>', true); + CreateBillingProposalAndCreateBillingDocuments('<2M-CM>', '<8M+CM>'); + + // [GIVEN] A standard BC deferral template + LibraryERM.CreateDeferralTemplate(DeferralTemplate, Enum::"Deferral Calculation Method"::"Straight-Line", + Enum::"Deferral Calculation Start Date"::"Posting Date", 3); + + // [WHEN] Assigning the Deferral Code to the purchase line that has contract deferrals + PurchaseLine.SetRange("Document No.", PurchaseHeader."No."); + PurchaseLine.SetFilter("No.", '<>%1', ''); + PurchaseLine.FindFirst(); + + // [THEN] An error is raised preventing double deferrals + asserterror PurchaseLine.Validate("Deferral Code", DeferralTemplate."Deferral Code"); + Assert.ExpectedError(DeferralCodeCannotBeUsedWithContractDeferralsErr); + end; + #endregion Tests #region Procedures @@ -781,7 +930,6 @@ codeunit 139913 "Vendor Deferrals Test" GLAccount: Record "G/L Account"; GenJournalBatch: Record "Gen. Journal Batch"; GenJournalLine: Record "Gen. Journal Line"; - LibraryERM: Codeunit "Library - ERM"; begin CreateGeneralJournalBatch(GenJournalBatch); LibraryERM.CreateGLAccount(GLAccount); @@ -852,7 +1000,6 @@ codeunit 139913 "Vendor Deferrals Test" local procedure CreateGeneralJournalBatch(var GenJournalBatch: Record "Gen. Journal Batch") var GenJournalTemplate: Record "Gen. Journal Template"; - LibraryERM: Codeunit "Library - ERM"; begin GenJournalTemplate.SetRange(Recurring, false); GenJournalTemplate.SetRange(Type, GenJournalTemplate.Type::General); @@ -913,6 +1060,16 @@ codeunit 139913 "Vendor Deferrals Test" VendorContractDeferral.FindFirst(); end; + local procedure ClearCorrectionDocumentNoFromBillingLines(CreditMemoDocumentNo: Code[20]) + var + CrMemoBillingLine: Record "Billing Line"; + begin + CrMemoBillingLine.SetRange(Partner, CrMemoBillingLine.Partner::Vendor); + CrMemoBillingLine.SetRange("Document Type", CrMemoBillingLine."Document Type"::"Credit Memo"); + CrMemoBillingLine.SetRange("Document No.", CreditMemoDocumentNo); + CrMemoBillingLine.ModifyAll("Correction Document No.", '', false); + end; + local procedure GetCalculatedMonthAmountsForDeferrals(SourceDeferralBaseAmount: Decimal; NumberOfPeriods: Integer; FirstDayOfBillingPeriod: Date; LastDayOfBillingPeriod: Date; CalculateInLCY: Boolean) var DailyDefBaseAmount: Decimal;