diff --git a/src/Apps/W1/ExpenseAgent/app/src/APIs/ExpenseVATSpecAPI.Page.al b/src/Apps/W1/ExpenseAgent/app/src/APIs/ExpenseVATSpecAPI.Page.al index 9edfddd58aa..928ca56dde3 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/APIs/ExpenseVATSpecAPI.Page.al +++ b/src/Apps/W1/ExpenseAgent/app/src/APIs/ExpenseVATSpecAPI.Page.al @@ -105,4 +105,28 @@ page 7085 "Expense VAT Spec. API" begin ExpenseAgentAPIValidation.VerifyAgentAccess(); end; + + trigger OnNewRecord(BelowxRec: Boolean) + begin + VerifyExpenseAgentCaller(); + Rec.Source := Rec.Source::Agent; + end; + + trigger OnInsertRecord(BelowxRec: Boolean): Boolean + begin + VerifyExpenseAgentCaller(); + Rec.Source := Rec.Source::Agent; + exit(true); + end; + + var + ExpenseAgentCallerRequiredErr: Label 'Only the Expense Agent application can create agent-authored VAT specifications.'; + + local procedure VerifyExpenseAgentCaller() + var + ExpenseAgentAPIValidation: Codeunit "Expense Agent API Validation"; + begin + if not ExpenseAgentAPIValidation.IsCurrentUserExpenseAgent() then + Error(ExpenseAgentCallerRequiredErr); + end; } diff --git a/src/Apps/W1/ExpenseAgent/app/src/Expense/Pages/ExpenseVATSpecification.Page.al b/src/Apps/W1/ExpenseAgent/app/src/Expense/Pages/ExpenseVATSpecification.Page.al index c0eab5ebcb4..43a54638637 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Expense/Pages/ExpenseVATSpecification.Page.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Expense/Pages/ExpenseVATSpecification.Page.al @@ -66,4 +66,10 @@ page 7084 "Expense VAT Specification" } } } + + trigger OnInsertRecord(BelowxRec: Boolean): Boolean + begin + Rec.Source := Rec.Source::"Manual"; + exit(true); + end; } diff --git a/src/Apps/W1/ExpenseAgent/app/src/Expense/Tables/Expense.Table.al b/src/Apps/W1/ExpenseAgent/app/src/Expense/Tables/Expense.Table.al index 0661c3eb9dd..dc099b9a911 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Expense/Tables/Expense.Table.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Expense/Tables/Expense.Table.al @@ -788,6 +788,7 @@ table 6900 Expense NonRefundableAmountCannotBeNegativeErr: Label '%1 cannot be in negative on Expense No. %2.', Comment = '%1 = Field Caption, %2 = Expense No.'; ExpenseUserMustBeLinkedToAnEmployeeErr: Label 'Expense User %1 must be linked to an Employee No.', Comment = '%1 = Expense User No.'; BillableCustomerAndProjectErr: Label 'You cannot use both %1 and %2 at the same time.', Comment = '%1 = Billable to Customer field caption, %2 = Project No. field caption'; + ModifyOrDeleteErr: Label 'Modifications and delete are not allowed for records created by the Expense Agent API.'; procedure AssistEdit() Result: Boolean begin @@ -1309,6 +1310,7 @@ table 6900 Expense internal procedure UpdateVATSpecification(ExpenseNo: Code[20]) var + ExpenseCategory: Record "Expense Category"; ExpenseItemization: Record "Expense Itemization"; ExpenseVATSpec: Record "Expense VAT Specification"; TempExpenseVATSpec: Record "Expense VAT Specification" temporary; @@ -1318,33 +1320,59 @@ table 6900 Expense if ExpenseAgentSetup."Default VAT Bus. Posting Group" = '' then exit; - ExpenseItemization.SetRange("Expense No.", ExpenseNo); - if ExpenseItemization.IsEmpty() then - exit; - ExpenseVATSpec.SetRange("Expense No.", ExpenseNo); - ExpenseVATSpec.DeleteAll(); - - LineNo := 0; - ExpenseItemization.FindSet(); - repeat - TempExpenseVATSpec.SetRange("Expense Category", ExpenseItemization."Expense Category Code"); - TempExpenseVATSpec.SetRange("Expense Subcategory", ExpenseItemization."Expense Subcategory Code"); - if TempExpenseVATSpec.FindFirst() then begin - TempExpenseVATSpec."Amount" += ExpenseItemization."Amount"; - TempExpenseVATSpec.Modify(); - end else begin - TempExpenseVATSpec.Init(); - TempExpenseVATSpec."Expense No." := ExpenseNo; - LineNo += 1; - TempExpenseVATSpec."Line No." := LineNo; - TempExpenseVATSpec.Validate("Expense Category", ExpenseItemization."Expense Category Code"); - TempExpenseVATSpec.Validate("Expense Subcategory", ExpenseItemization."Expense Subcategory Code"); - TempExpenseVATSpec.Validate("VAT Bus. Posting Group", ExpenseAgentSetup."Default VAT Bus. Posting Group"); - TempExpenseVATSpec.Validate("Amount", ExpenseItemization.Amount); - TempExpenseVATSpec.Insert(); - end; - until ExpenseItemization.Next() = 0; + ExpenseVATSpec.SetRange(Source, ExpenseVATSpec.Source::Agent); + if not ExpenseVATSpec.IsEmpty() then + error(ModifyOrDeleteErr); + + ExpenseCategory.Get("Expense Category"); + case ExpenseCategory."Expense Detail Required" of + ExpenseCategory."Expense Detail Required"::Itemize: + begin + ExpenseItemization.SetRange("Expense No.", ExpenseNo); + if not ExpenseItemization.IsEmpty() then begin + ExpenseVATSpec.SetRange(Source); + ExpenseVATSpec.DeleteAll(); + + LineNo := 0; + ExpenseItemization.FindSet(); + repeat + TempExpenseVATSpec.SetRange("Expense Category", ExpenseItemization."Expense Category Code"); + TempExpenseVATSpec.SetRange("Expense Subcategory", ExpenseItemization."Expense Subcategory Code"); + if TempExpenseVATSpec.FindFirst() then begin + TempExpenseVATSpec.Validate(Amount, TempExpenseVATSpec.Amount + ExpenseItemization.Amount); + TempExpenseVATSpec.Modify(); + end else begin + TempExpenseVATSpec.Init(); + TempExpenseVATSpec.Source := TempExpenseVATSpec.Source::Manual; + TempExpenseVATSpec."Expense No." := ExpenseNo; + LineNo += 1; + TempExpenseVATSpec."Line No." := LineNo; + TempExpenseVATSpec.Validate("Expense Category", ExpenseItemization."Expense Category Code"); + TempExpenseVATSpec.Validate("Expense Subcategory", ExpenseItemization."Expense Subcategory Code"); + TempExpenseVATSpec.Validate("VAT Bus. Posting Group", ExpenseAgentSetup."Default VAT Bus. Posting Group"); + TempExpenseVATSpec.Validate("Amount", ExpenseItemization.Amount); + TempExpenseVATSpec.Insert(); + end; + until ExpenseItemization.Next() = 0; + end; + end; + else + if ExpenseCategory."VAT Prod. Posting Group" <> '' then begin + ExpenseVATSpec.SetRange(Source); + ExpenseVATSpec.DeleteAll(); + + TempExpenseVATSpec.Init(); + TempExpenseVATSpec.Source := TempExpenseVATSpec.Source::Manual; + TempExpenseVATSpec."Expense No." := ExpenseNo; + LineNo += 1; + TempExpenseVATSpec."Line No." := LineNo; + TempExpenseVATSpec.Validate("Expense Category", "Expense Category"); + TempExpenseVATSpec.Validate("VAT Bus. Posting Group", ExpenseAgentSetup."Default VAT Bus. Posting Group"); + TempExpenseVATSpec.Validate("Amount", Amount); + TempExpenseVATSpec.Insert(); + end; + end; TempExpenseVATSpec.Reset(); if TempExpenseVATSpec.FindSet() then diff --git a/src/Apps/W1/ExpenseAgent/app/src/Expense/Tables/ExpenseVATSpecification.Table.al b/src/Apps/W1/ExpenseAgent/app/src/Expense/Tables/ExpenseVATSpecification.Table.al index 7f8c2aec658..6157d275f2f 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Expense/Tables/ExpenseVATSpecification.Table.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Expense/Tables/ExpenseVATSpecification.Table.al @@ -34,10 +34,12 @@ table 6918 "Expense VAT Specification" trigger OnValidate() begin GetExpense(); - if Expense."Expense Category" <> '' then - Validate("Expense Category", "Expense Category"); - if Expense."Expense Subcategory" <> '' then - Validate("Expense Subcategory", "Expense Subcategory"); + if Source <> Source::Agent then begin + if Expense."Expense Category" <> '' then + Validate("Expense Category", "Expense Category"); + if Expense."Expense Subcategory" <> '' then + Validate("Expense Subcategory", "Expense Subcategory"); + end; end; } field(2; "Line No."; Integer) @@ -57,12 +59,14 @@ table 6918 "Expense VAT Specification" trigger OnValidate() begin - InitializeCurrency(); - "VAT Amount" := Round(Amount * "VAT %" / (100 + "VAT %"), Currency."Amount Rounding Precision", Currency.VATRoundingDirection()); - "VAT Base Amount" := Round(Amount - "VAT Amount", Currency."Amount Rounding Precision"); - "VAT Difference" := 0; - "VAT Amount (LCY)" := CalcVATAmountLCY(); - "VAT Base Amount (LCY)" := "Amount (LCY)" - "VAT Amount (LCY)"; + if Source <> Source::Agent then begin + InitializeCurrency(); + "VAT Amount" := Round(Amount * "VAT %" / (100 + "VAT %"), Currency."Amount Rounding Precision", Currency.VATRoundingDirection()); + "VAT Base Amount" := Round(Amount - "VAT Amount", Currency."Amount Rounding Precision"); + "VAT Difference" := 0; + "VAT Amount (LCY)" := CalcVATAmountLCY(); + "VAT Base Amount (LCY)" := "Amount (LCY)" - "VAT Amount (LCY)"; + end; end; } field(11; "VAT Base Amount"; Decimal) @@ -91,7 +95,8 @@ table 6918 "Expense VAT Specification" trigger OnValidate() begin - ValidateAmount(); + if Source <> Source::Agent then + ValidateAmount(); end; } /// @@ -151,17 +156,17 @@ table 6918 "Expense VAT Specification" trigger OnValidate() begin - GetExpense(); - if Expense."Currency Code" = '' then begin - Amount := "Amount (LCY)"; - Validate(Amount); - end else begin - TestField("Amount (LCY)"); - TestField(Amount); - Expense."Currency Factor" := Amount / "Amount (LCY)"; + if Source <> Source::Agent then begin + GetExpense(); + if Expense."Currency Code" = '' then begin + Amount := "Amount (LCY)"; + Validate(Amount); + end else begin + TestField("Amount (LCY)"); + TestField(Amount); + end; + Validate("VAT %"); end; - - Validate("VAT %"); end; } field(27; "Expense Category"; Code[20]) @@ -174,14 +179,16 @@ table 6918 "Expense VAT Specification" var ExpenseCategory: Record "Expense Category"; begin - if "Expense Category" = '' then - exit; - if "Expense Subcategory" <> '' then - exit; - if ExpenseCategory.Get("Expense Category") then begin - "VAT Prod. Posting Group" := ExpenseCategory."VAT Prod. Posting Group"; - "VAT %" := ExpenseCategory."Default VAT %"; - Validate("VAT %"); + if Source <> Source::Agent then begin + if "Expense Category" = '' then + exit; + if "Expense Subcategory" <> '' then + exit; + if ExpenseCategory.Get("Expense Category") then begin + "VAT Prod. Posting Group" := ExpenseCategory."VAT Prod. Posting Group"; + "VAT %" := ExpenseCategory."Default VAT %"; + Validate("VAT %"); + end; end; end; } @@ -196,6 +203,9 @@ table 6918 "Expense VAT Specification" ExpenseSubcategory: Record "Expense Subcategory"; ExpenseCategory: Record "Expense Category"; begin + if Source = Source::Agent then + exit; + if "Expense Subcategory" <> '' then begin if ExpenseSubcategory.Get("Expense Category", "Expense Subcategory") then begin "VAT Prod. Posting Group" := ExpenseSubcategory."VAT Prod. Posting Group"; @@ -214,6 +224,8 @@ table 6918 "Expense VAT Specification" field(40; Source; Enum "Expense VAT Spec Source") { Caption = 'Source'; + Editable = false; + DataClassification = SystemMetadata; ToolTip = 'Specifies how this VAT specification line was created, for example whether it was entered manually or extracted automatically from a receipt.'; } field(41; Confidence; Decimal) @@ -249,11 +261,24 @@ table 6918 "Expense VAT Specification" end; end; + trigger OnModify() + begin + if Source = Source::Agent then + error(ModifyOrDeleteErr); + end; + + trigger OnDelete() + begin + if Source = Source::Agent then + error(ModifyOrDeleteErr); + end; + var Currency: Record Currency; CurrExchRate: Record "Currency Exchange Rate"; Expense: Record Expense; ExpenseAgentSetup: Record "Expense Agent Setup"; + ModifyOrDeleteErr: Label 'Modifications and delete are not allowed for records created by the Expense Agent API.'; local procedure CalcVATAmountLCY(): Decimal var diff --git a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Codeunits/CreateExpenseReport.Codeunit.al b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Codeunits/CreateExpenseReport.Codeunit.al index 32f7a184923..8ee647bd12b 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Codeunits/CreateExpenseReport.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Codeunits/CreateExpenseReport.Codeunit.al @@ -177,7 +177,7 @@ codeunit 6983 "Create Expense Report" ExpenseReportLine.Validate("Expense User No.", Expense."Expense User No."); ExpenseReportLine.Validate("Expense Category", Expense."Expense Category"); ExpenseReportLine.Validate("Expense Subcategory Code", Expense."Expense Subcategory"); - ExpenseReportLine.Validate("Expense Location", Expense."Expense Location"); + ExpenseReportLine.Validate("Expense Location", CopyStr(Expense."Expense Location", 1, 20)); if Expense.Description <> '' then ExpenseReportLine.Validate(Description, Expense.Description); if ExpenseReportLine."Expense Subcategory Code" <> '' then @@ -327,6 +327,8 @@ codeunit 6983 "Create Expense Report" ExpenseReportLineVATSpec."VAT Amount" := ExpenseVATSpec."VAT Amount"; ExpenseReportLineVATSpec.Amount := ExpenseVATSpec.Amount; ExpenseReportLineVATSpec."VAT Difference" := ExpenseVATSpec."VAT Difference"; + ExpenseReportLineVATSpec."Currency Code" := Expense."Currency Code"; + ExpenseReportLineVATSpec."Currency Factor" := Expense."Currency Factor"; ExpenseReportLineVATSpec."VAT Bus. Posting Group" := ExpenseVATSpec."VAT Bus. Posting Group"; ExpenseReportLineVATSpec."VAT Prod. Posting Group" := ExpenseVATSpec."VAT Prod. Posting Group"; ExpenseReportLineVATSpec."VAT Amount (LCY)" := ExpenseVATSpec."VAT Amount (LCY)"; @@ -340,6 +342,7 @@ codeunit 6983 "Create Expense Report" else if ExpenseCategory.Get(ExpenseVATSpec."Expense Category") then ExpenseReportLineVATSpec.Validate("Reclaim %", ExpenseCategory."Default VAT Reclaim %"); + ExpenseReportLineVATSpec.UpdateReimbursementAmounts(); ExpenseReportLineVATSpec.Insert(); until ExpenseVATSpec.Next() = 0; diff --git a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Codeunits/ExpenseReportPost.Codeunit.al b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Codeunits/ExpenseReportPost.Codeunit.al index af7abe1c8bf..258d726541c 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Codeunits/ExpenseReportPost.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Codeunits/ExpenseReportPost.Codeunit.al @@ -71,7 +71,9 @@ codeunit 6987 "Expense Report-Post" AgentNotEnabledErr: Label 'Please make sure the Expense Agent is active.'; CommunicationDisabledErr: Label 'Sending emails to users is turned off. Turn on Communication for the Expense Agent before sending reimbursement notifications.'; NoNoreplyAccountErr: Label 'No account is set for sending emails. Set the send mail account for the Expense Agent before sending reimbursement notifications.'; + NotApprovedForVATReclaimCategoryErr: Label 'VAT Reclaim Status is not set for Line with Expense Category %1.', Comment = '%1 = Expense Category'; NotApprovedForVATReclaimErr: Label 'VAT Reclaim Status is not set for Line with Expense Category %1 and Expense Subcategory %2.', Comment = '%1 = Expense Category, %2 = Expense Subcategory'; + ShowItLbl: Label 'Show it'; internal procedure RunWithCheck(var ExpenseReportHeader: Record "Expense Report Header") var @@ -244,10 +246,25 @@ codeunit 6987 "Expense Report-Post" if ExpenseReportLineVATSpec.FindSet() then repeat if ExpenseReportLineVATSpec."Reclaim Status" = ExpenseReportLineVATSpec."Reclaim Status"::"Pending" then - Error(NotApprovedForVATReclaimErr, ExpenseReportLineVATSpec."Expense Category", ExpenseReportLineVATSpec."Expense Subcategory"); + Error(GetPendingVATSpecErrorInfo(ExpenseReportLineVATSpec)); until ExpenseReportLineVATSpec.Next() = 0; end; + local procedure GetPendingVATSpecErrorInfo(ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."): ErrorInfo + var + PendingVATSpecErrorInfo: ErrorInfo; + begin + if ExpenseReportLineVATSpec."Expense Subcategory" = '' then + PendingVATSpecErrorInfo.Message := StrSubstNo(NotApprovedForVATReclaimCategoryErr, ExpenseReportLineVATSpec."Expense Category") + else + PendingVATSpecErrorInfo.Message := StrSubstNo(NotApprovedForVATReclaimErr, ExpenseReportLineVATSpec."Expense Category", ExpenseReportLineVATSpec."Expense Subcategory"); + PendingVATSpecErrorInfo.RecordId := ExpenseReportLineVATSpec.RecordId; + PendingVATSpecErrorInfo.FieldNo := ExpenseReportLineVATSpec.FieldNo("Reclaim Status"); + PendingVATSpecErrorInfo.PageNo := Page::"Expense Report Line VAT Spec."; + PendingVATSpecErrorInfo.AddNavigationAction(ShowItLbl); + exit(PendingVATSpecErrorInfo); + end; + local procedure CheckMandatoryFields(var ExpenseReportLine: Record "Expense Report Line") var ExpenseCategory: Record "Expense Category"; @@ -428,7 +445,7 @@ codeunit 6987 "Expense Report-Post" exit; if ExpenseReportLine.Refundable then - PostCompanyPaidExpenseJournal(ExpenseReportHeader, ExpenseReportLine, PostedExpReportLine, RefundableAmountLCY); + PostCompanyPaidExpenseJournal(ExpenseReportHeader, ExpenseReportLine, PostedExpReportLine, RefundableAmount, RefundableAmountLCY); if ExpenseReportLine."Reimbursable Amount (LCY)" < 0 then PostNonRefundableJnlLine(ExpenseReportHeader, ExpenseReportLine, PostedExpReportLine); @@ -471,10 +488,11 @@ codeunit 6987 "Expense Report-Post" RefundableAmount := GenJournalLine.Amount + GenJournalLine."VAT Amount"; RefundableAmountLCY := GenJournalLine."Amount (LCY)" + GenJournalLine."VAT Amount (LCY)"; - if ShouldConsiderPostingRoundingDifference(PostedExpenseReportHeader."Reimbursement Currency Code", ExpenseReportLine."Expense Currency Code") then - if ExpenseReportLine."Reimbursement Type" = ExpenseReportLine."Reimbursement Type"::"Employee Paid" then - if Abs(RefundableAmountLCY) <> Abs(ExpenseReportLine."Reimbursable Amount (LCY)") then - PostRoundingDifferenceOnCurrency(GenJournalLine, ExpenseReportLine, ExpenseReportLine."Reimbursable Amount (LCY)" - RefundableAmountLCY); + if ExpenseReportLine."Reimbursement Type" = ExpenseReportLine."Reimbursement Type"::"Employee Paid" then + PostRoundingDifferenceOnCurrency( + ExpenseReportHeader, ExpenseReportLine, + ExpenseReportLine."Reimbursable Amount" - RefundableAmount, + ExpenseReportLine."Reimbursable Amount (LCY)" - RefundableAmountLCY); end; local procedure PostRefundableJnlLineFromSpecs(ExpenseReportHeader: Record "Expense Report Header"; ExpenseReportLine: Record "Expense Report Line"; PostedExpReportLine: Record "Posted Expense Report Line"; var ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; var RefundableAmount: Decimal; var RefundableAmountLCY: Decimal) @@ -507,7 +525,7 @@ codeunit 6987 "Expense Report-Post" GenJnlPostLine.RunWithCheck(GenJournalLine); // Accumulate gross (base + VAT) from the spec row for rounding/balance checks. - RefundableAmount += ExpenseReportLineVATSpec."VAT Base Amount" + ExpenseReportLineVATSpec."VAT Amount"; + RefundableAmount += ExpenseReportLineVATSpec."VAT Base Amount (RCY)" + ExpenseReportLineVATSpec."VAT Amount (RCY)"; RefundableAmountLCY += ExpenseReportLineVATSpec."VAT Base Amount (LCY)" + ExpenseReportLineVATSpec."VAT Amount (LCY)"; until ExpenseReportLineVATSpec.Next() = 0; @@ -517,13 +535,14 @@ codeunit 6987 "Expense Report-Post" AmountToEmployeeLCY += ExpenseReportLine."Reimbursable Amount (LCY)"; end; - if ShouldConsiderPostingRoundingDifference(PostedExpenseReportHeader."Reimbursement Currency Code", ExpenseReportLine."Expense Currency Code") then - if ExpenseReportLine."Reimbursement Type" = ExpenseReportLine."Reimbursement Type"::"Employee Paid" then - if Abs(RefundableAmountLCY) <> Abs(ExpenseReportLine."Reimbursable Amount (LCY)") then - PostRoundingDifferenceOnCurrency(GenJournalLine, ExpenseReportLine, ExpenseReportLine."Reimbursable Amount (LCY)" - RefundableAmountLCY); + if ExpenseReportLine."Reimbursement Type" = ExpenseReportLine."Reimbursement Type"::"Employee Paid" then + PostRoundingDifferenceOnCurrency( + ExpenseReportHeader, ExpenseReportLine, + ExpenseReportLine."Reimbursable Amount" - RefundableAmount, + ExpenseReportLine."Reimbursable Amount (LCY)" - RefundableAmountLCY); end; - local procedure PostCompanyPaidExpenseJournal(ExpenseReportHeader: Record "Expense Report Header"; ExpenseReportLine: Record "Expense Report Line"; PostedExpReportLine: Record "Posted Expense Report Line"; RefundableAmountLCY: Decimal) + local procedure PostCompanyPaidExpenseJournal(ExpenseReportHeader: Record "Expense Report Header"; ExpenseReportLine: Record "Expense Report Line"; PostedExpReportLine: Record "Posted Expense Report Line"; RefundableAmount: Decimal; RefundableAmountLCY: Decimal) var GenJournalLine: Record "Gen. Journal Line"; begin @@ -533,15 +552,17 @@ codeunit 6987 "Expense Report-Post" GenJnlPostLine.RunWithCheck(GenJournalLine); - if ShouldConsiderPostingRoundingDifference(PostedExpenseReportHeader."Reimbursement Currency Code", ExpenseReportLine."Expense Currency Code") then - if Abs(RefundableAmountLCY) <> Abs(GenJournalLine."Amount (LCY)") then - PostRoundingDifferenceOnCurrency(GenJournalLine, ExpenseReportLine, Abs(GenJournalLine."Amount (LCY)") - RefundableAmountLCY); + PostRoundingDifferenceOnCurrency( + ExpenseReportHeader, ExpenseReportLine, + Abs(GenJournalLine.Amount) - RefundableAmount, + Abs(GenJournalLine."Amount (LCY)") - RefundableAmountLCY); end; local procedure PostNonRefundableJnlLine(ExpenseReportHeader: Record "Expense Report Header"; ExpenseReportLine: Record "Expense Report Line"; PostedExpReportLine: Record "Posted Expense Report Line") var GenJournalLine: Record "Gen. Journal Line"; ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + RefundableAmount: Decimal; RefundableLCY: Decimal; begin ExpenseReportLineVATSpec.SetRange("Document No.", ExpenseReportLine."Document No."); @@ -560,16 +581,20 @@ codeunit 6987 "Expense Report-Post" GenJnlPostLine.RunWithCheck(GenJournalLine); + RefundableAmount := GenJournalLine.Amount + GenJournalLine."VAT Amount"; RefundableLCY := GenJournalLine."Amount (LCY)" + GenJournalLine."VAT Amount (LCY)"; - if ShouldConsiderPostingRoundingDifference(PostedExpenseReportHeader."Reimbursement Currency Code", ExpenseReportLine."Expense Currency Code") then - if Abs(ExpenseReportLine."Reimbursable Amount (LCY)") <> Abs(RefundableLCY) then - PostRoundingDifferenceOnCurrency(GenJournalLine, ExpenseReportLine, ExpenseReportLine."Reimbursable Amount (LCY)" - RefundableLCY); + PostRoundingDifferenceOnCurrency( + ExpenseReportHeader, ExpenseReportLine, + ExpenseReportLine."Reimbursable Amount" - RefundableAmount, + ExpenseReportLine."Reimbursable Amount (LCY)" - RefundableLCY); end; local procedure PostNonRefundableJnlLineFromSpecs(ExpenseReportHeader: Record "Expense Report Header"; ExpenseReportLine: Record "Expense Report Line"; var ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec.") var GenJournalLine: Record "Gen. Journal Line"; GlobalEntryCopied: Boolean; + PostedAmount: Decimal; + PostedAmountLCY: Decimal; begin // Each spec row produces its own Gen. Journal Line and VAT Entry. // Amounts are taken directly from the spec — no proportional splitting. @@ -586,11 +611,17 @@ codeunit 6987 "Expense Report-Post" end; GenJnlPostLine.RunWithCheck(GenJournalLine); + PostedAmount += ExpenseReportLineVATSpec."VAT Base Amount (RCY)" + ExpenseReportLineVATSpec."VAT Amount (RCY)"; + PostedAmountLCY += ExpenseReportLineVATSpec."VAT Base Amount (LCY)" + ExpenseReportLineVATSpec."VAT Amount (LCY)"; until ExpenseReportLineVATSpec.Next() = 0; // Accumulate employee reimbursement amount once per line. AmountToEmployee += ExpenseReportLine."Reimbursable Amount"; AmountToEmployeeLCY += ExpenseReportLine."Reimbursable Amount (LCY)"; + PostRoundingDifferenceOnCurrency( + ExpenseReportHeader, ExpenseReportLine, + ExpenseReportLine."Reimbursable Amount" - PostedAmount, + ExpenseReportLine."Reimbursable Amount (LCY)" - PostedAmountLCY); end; local procedure SetupNonRefundableAccountForSpec(var GenJournalLine: Record "Gen. Journal Line"; ExpenseReportLine: Record "Expense Report Line"; ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec.") @@ -609,11 +640,6 @@ codeunit 6987 "Expense Report-Post" GenJournalLine."Account No." := ExpensePostingGroup."Non-Refundable Debit Account"; end; - local procedure ShouldConsiderPostingRoundingDifference(ReimbursementCurrencyCode: Code[10]; ExpenseCurrencyCode: Code[10]): Boolean - begin - exit((ReimbursementCurrencyCode = '') and (ExpenseCurrencyCode <> '')); - end; - local procedure PostProjectJnlLine(PostedExpReportLine: Record "Posted Expense Report Line"; PostedExpReportHeader: Record "Posted Expense Report Header"; GenJournalLine: Record "Gen. Journal Line"): Integer var ProjectJournalLine: Record "Job Journal Line"; @@ -700,19 +726,6 @@ codeunit 6987 "Expense Report-Post" GenJournalLine.Validate("Gen. Posting Type", GenJournalLine."Gen. Posting Type"::Purchase); end; - local procedure ClearVATInformationOnGenJnlLine(var GenJournalLine: Record "Gen. Journal Line") - begin - GenJournalLine."Bill-to/Pay-to No." := ''; - GenJournalLine."VAT Registration No." := ''; - GenJournalLine."VAT Posting" := GenJournalLine."VAT Posting"::"Automatic VAT Entry"; - GenJournalLine."VAT %" := 0; - GenJournalLine."VAT Amount" := 0; - GenJournalLine."VAT Amount (LCY)" := 0; - GenJournalLine.Validate("VAT Prod. Posting Group", ''); - GenJournalLine.Validate("VAT Bus. Posting Group", ''); - GenJournalLine."Gen. Posting Type" := GenJournalLine."Gen. Posting Type"::" "; - end; - local procedure CreateGenJournalLine(var GenJournalLine: Record "Gen. Journal Line"; ExpenseReportHeader: Record "Expense Report Header"; PostedExpReportLine: Record "Posted Expense Report Line") begin GenJournalLine.Init(); @@ -740,7 +753,6 @@ codeunit 6987 "Expense Report-Post" GenJournalLine.Validate("Document Type", GenJournalLine."Document Type"::Invoice); GenJournalLine.Validate("Document No.", PostedExpReportLine."Document No."); GenJournalLine.Validate("Expense User No.", ExpenseReportHeader."Expense User No."); - GenJournalLine.Validate(Description, ExpenseReportLine.UpdatePostingDescription()); GenJournalLine.Validate("Keep Description", true); // Use spec-level expense category/subcategory when present; fall back to parent line. @@ -751,36 +763,44 @@ codeunit 6987 "Expense Report-Post" GenJournalLine.Validate("Expense Category", ExpenseReportLine."Expense Category"); GenJournalLine.Validate("Expense Subcategory Code", ExpenseReportLine."Expense Subcategory Code"); end; + GenJournalLine.Validate( + Description, + ExpenseReportLine.UpdatePostingDescription(GenJournalLine."Expense Category", GenJournalLine."Expense Subcategory Code")); + + GenJournalLine.Validate("Currency Code", ExpenseReportHeader."Reimbursement Currency Code"); + GenJournalLine.Validate("Source Currency Code", ExpenseReportHeader."Reimbursement Currency Code"); + GenJournalLine."Currency Factor" := ExpenseReportHeader."Reimbursement Currency Factor"; - // Amounts come directly from the spec row — base (net) + VAT as captured on the receipt. + // Amounts come directly from the spec row in reimbursement currency and LCY. GenJournalLine."Gen. Posting Type" := GenJournalLine."Gen. Posting Type"::Purchase; - GenJournalLine."Currency Code" := ExpenseReportLine."Expense Currency Code"; - GenJournalLine."Currency Factor" := ExpenseReportLine."Expense Currency Factor"; - GenJournalLine.Amount := ExpenseReportLineVATSpec."VAT Base Amount"; + GenJournalLine.Amount := ExpenseReportLineVATSpec."VAT Base Amount (RCY)"; GenJournalLine."Amount (LCY)" := ExpenseReportLineVATSpec."VAT Base Amount (LCY)"; + GenJournalLine."Source Currency Amount" := GenJournalLine.Amount; if ExpenseReportLineVATSpec."Reclaim Status" = ExpenseReportLineVATSpec."Reclaim Status"::"Approved" then begin GenJournalLine."VAT Bus. Posting Group" := ExpenseReportLineVATSpec."VAT Bus. Posting Group"; GenJournalLine."VAT Prod. Posting Group" := ExpenseReportLineVATSpec."VAT Prod. Posting Group"; GenJournalLine."VAT Posting" := GenJournalLine."VAT Posting"::"Manual VAT Entry"; GenJournalLine."VAT Calculation Type" := ExpenseReportLine."VAT Calculation Type"; - GenJournalLine."VAT Base Amount" := ExpenseReportLineVATSpec."VAT Base Amount"; + GenJournalLine."VAT %" := ExpenseReportLineVATSpec."VAT %"; + GenJournalLine."VAT Base Amount" := ExpenseReportLineVATSpec."VAT Base Amount (RCY)"; GenJournalLine."VAT Base Amount (LCY)" := ExpenseReportLineVATSpec."VAT Base Amount (LCY)"; - GenJournalLine."VAT Amount" := ExpenseReportLineVATSpec."VAT Amount"; + GenJournalLine."VAT Amount" := ExpenseReportLineVATSpec."VAT Amount (RCY)"; GenJournalLine."VAT Amount (LCY)" := ExpenseReportLineVATSpec."VAT Amount (LCY)"; - GenJournalLine."VAT %" := ExpenseReportLineVATSpec."VAT %"; + GenJournalLine."Source Curr. VAT Base Amount" := ExpenseReportLineVATSpec."VAT Base Amount (RCY)"; + GenJournalLine."Source Curr. VAT Amount" := ExpenseReportLineVATSpec."VAT Amount (RCY)"; // VAT Reclaim % is used to route the non-deductible portion of VAT to the correct accounts via VAT Posting Setup. - if (ExpenseReportLineVATSpec."Reclaim %" <> 100) and (ExpenseReportLineVATSpec."VAT Amount" <> 0) then begin + if (ExpenseReportLineVATSpec."Reclaim %" <> 100) and (ExpenseReportLineVATSpec."VAT Amount (LCY)" <> 0) then begin VATSetup.Get(); VATSetup.TestField("Non-Deductible VAT Is Enabled"); GenJournalLine.Validate("Non-Deductible VAT %", 100 - ExpenseReportLineVATSpec."Reclaim %"); end; end else begin // VAT is not reclaimable: include VAT in the expense amount (gross) and do not create a VAT entry. - GenJournalLine.Amount := ExpenseReportLineVATSpec."VAT Base Amount" + ExpenseReportLineVATSpec."VAT Amount"; + GenJournalLine.Amount := ExpenseReportLineVATSpec."VAT Base Amount (RCY)" + ExpenseReportLineVATSpec."VAT Amount (RCY)"; GenJournalLine."Amount (LCY)" := ExpenseReportLineVATSpec."VAT Base Amount (LCY)" + ExpenseReportLineVATSpec."VAT Amount (LCY)"; - + GenJournalLine."Source Currency Amount" := GenJournalLine.Amount; GenJournalLine."VAT Posting" := GenJournalLine."VAT Posting"::"Automatic VAT Entry"; GenJournalLine."VAT %" := 0; GenJournalLine."VAT Amount" := 0; @@ -991,15 +1011,23 @@ codeunit 6987 "Expense Report-Post" ReimbursementCurrency."Amount Rounding Precision"); end; - local procedure PostRoundingDifferenceOnCurrency(GenJournalLine: Record "Gen. Journal Line"; ExpenseReportLine: Record "Expense Report Line"; AmountToPost: Decimal) + local procedure PostRoundingDifferenceOnCurrency(ExpenseReportHeader: Record "Expense Report Header"; ExpenseReportLine: Record "Expense Report Line"; AmountToPost: Decimal; AmountToPostLCY: Decimal) var ExpenseCategory: Record "Expense Category"; ExpensePostingGroup: Record "Expense Posting Group"; + GenJournalLine: Record "Gen. Journal Line"; AccNo: Code[20]; + AmountForAccountSelection: Decimal; begin + if (AmountToPost = 0) and (AmountToPostLCY = 0) then + exit; + ExpenseCategory.Get(ExpenseReportLine."Expense Category"); ExpensePostingGroup.Get(ExpenseCategory."Posting Group"); - if AmountToPost > 0 then begin + AmountForAccountSelection := AmountToPostLCY; + if AmountForAccountSelection = 0 then + AmountForAccountSelection := AmountToPost; + if AmountForAccountSelection > 0 then begin ExpensePostingGroup.TestField("Debit Rounding Account"); AccNo := ExpensePostingGroup."Debit Rounding Account"; end else begin @@ -1007,13 +1035,25 @@ codeunit 6987 "Expense Report-Post" AccNo := ExpensePostingGroup."Credit Rounding Account"; end; - GenJournalLine."Account Type" := GenJournalLine."Account Type"::"G/L Account"; + GenJournalLine.Init(); + GenJournalLine.Validate("Posting Date", ExpenseReportHeader."Posting Date"); + GenJournalLine.Validate("Document Type", GenJournalLine."Document Type"::Invoice); + GenJournalLine.Validate("Document No.", PostedExpenseReportHeader."No."); + GenJournalLine.Validate("Expense User No.", ExpenseReportHeader."Expense User No."); + GenJournalLine.Validate("Expense Category", ExpenseReportLine."Expense Category"); + GenJournalLine.Validate("Expense Subcategory Code", ExpenseReportLine."Expense Subcategory Code"); + GenJournalLine.Validate(Description, ExpenseReportLine.UpdatePostingDescription()); + GenJournalLine.Validate("Keep Description", true); + GenJournalLine.Validate("Currency Code", ExpenseReportHeader."Reimbursement Currency Code"); + GenJournalLine.Validate("Source Currency Code", ExpenseReportHeader."Reimbursement Currency Code"); + GenJournalLine."Currency Factor" := ExpenseReportHeader."Reimbursement Currency Factor"; + GenJournalLine.Validate("Account Type", GenJournalLine."Account Type"::"G/L Account"); GenJournalLine."Account No." := AccNo; GenJournalLine.Amount := AmountToPost; - GenJournalLine."Amount (LCY)" := AmountToPost; + GenJournalLine."Amount (LCY)" := AmountToPostLCY; GenJournalLine."Source Currency Amount" := AmountToPost; + SetupSourceCodeAndDimensions(GenJournalLine, ExpenseReportLine."Dimension Set ID"); GenJournalLine."System-Created Entry" := true; - ClearVATInformationOnGenJnlLine(GenJournalLine); GenJnlPostLine.RunWithCheck(GenJournalLine); end; diff --git a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Codeunits/UpgradeExpReportVATSpec.Codeunit.al b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Codeunits/UpgradeExpReportVATSpec.Codeunit.al new file mode 100644 index 00000000000..b24ea6e4ca4 --- /dev/null +++ b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Codeunits/UpgradeExpReportVATSpec.Codeunit.al @@ -0,0 +1,119 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.ExpenseAgent; + +using Microsoft.Finance.Currency; +using Microsoft.Finance.GeneralLedger.Setup; +using System.Upgrade; + +codeunit 7105 "Upgrade Exp. Report VAT Spec" +{ + Access = Internal; + Subtype = Upgrade; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata Currency = r, + tabledata "Currency Exchange Rate" = r, + tabledata "Expense Report Header" = r, + tabledata "Expense Report Line VAT Spec." = rm, + tabledata "General Ledger Setup" = r, + tabledata "Posted Expense Report Header" = r, + tabledata "Posted Exp. Rep. Line VAT Spec" = rm; + + trigger OnUpgradePerCompany() + begin + BackfillReimbursementAmounts(); + end; + + local procedure BackfillReimbursementAmounts() + var + UpgradeTag: Codeunit "Upgrade Tag"; + begin + if UpgradeTag.HasUpgradeTag(GetBackfillReimbursementAmountsUpgradeTag()) then + exit; + + BackfillExpenseReportLineVATSpecs(); + BackfillPostedExpenseReportLineVATSpecs(); + + UpgradeTag.SetUpgradeTag(GetBackfillReimbursementAmountsUpgradeTag()); + end; + + local procedure BackfillExpenseReportLineVATSpecs() + var + ExpenseReportHeader: Record "Expense Report Header"; + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + begin + if not ExpenseReportLineVATSpec.FindSet(true) then + exit; + + repeat + if ExpenseReportHeader.Get(ExpenseReportLineVATSpec."Document No.") then begin + ExpenseReportLineVATSpec.UpdateReimbursementAmounts(ExpenseReportHeader); + ExpenseReportLineVATSpec.Modify(false); + end; + until ExpenseReportLineVATSpec.Next() = 0; + end; + + local procedure BackfillPostedExpenseReportLineVATSpecs() + var + PostedExpenseReportHeader: Record "Posted Expense Report Header"; + PostedExpenseReportLineVATSpec: Record "Posted Exp. Rep. Line VAT Spec"; + begin + if not PostedExpenseReportLineVATSpec.FindSet(true) then + exit; + + repeat + if PostedExpenseReportHeader.Get(PostedExpenseReportLineVATSpec."Expense Report No.") then begin + UpdatePostedReimbursementAmounts(PostedExpenseReportLineVATSpec, PostedExpenseReportHeader); + PostedExpenseReportLineVATSpec.Modify(false); + end; + until PostedExpenseReportLineVATSpec.Next() = 0; + end; + + local procedure UpdatePostedReimbursementAmounts(var PostedExpenseReportLineVATSpec: Record "Posted Exp. Rep. Line VAT Spec"; PostedExpenseReportHeader: Record "Posted Expense Report Header") + var + CurrencyExchangeRate: Record "Currency Exchange Rate"; + ReimbursementCurrency: Record Currency; + begin + ReimbursementCurrency.Initialize(PostedExpenseReportHeader."Reimbursement Currency Code"); + + if PostedExpenseReportHeader."Reimbursement Currency Code" = '' then begin + PostedExpenseReportLineVATSpec."VAT Base Amount (RCY)" := PostedExpenseReportLineVATSpec."VAT Base Amount (LCY)"; + PostedExpenseReportLineVATSpec."VAT Amount (RCY)" := PostedExpenseReportLineVATSpec."VAT Amount (LCY)"; + PostedExpenseReportLineVATSpec."Amount (RCY)" := PostedExpenseReportLineVATSpec."Amount (LCY)"; + end else begin + PostedExpenseReportLineVATSpec."VAT Base Amount (RCY)" := + Round( + CurrencyExchangeRate.ExchangeAmtLCYToFCY( + PostedExpenseReportHeader."Posting Date", PostedExpenseReportHeader."Reimbursement Currency Code", + PostedExpenseReportLineVATSpec."VAT Base Amount (LCY)", PostedExpenseReportHeader."Reimbursement Currency Factor"), + ReimbursementCurrency."Amount Rounding Precision"); + PostedExpenseReportLineVATSpec."VAT Amount (RCY)" := + Round( + CurrencyExchangeRate.ExchangeAmtLCYToFCY( + PostedExpenseReportHeader."Posting Date", PostedExpenseReportHeader."Reimbursement Currency Code", + PostedExpenseReportLineVATSpec."VAT Amount (LCY)", PostedExpenseReportHeader."Reimbursement Currency Factor"), + ReimbursementCurrency."Amount Rounding Precision"); + PostedExpenseReportLineVATSpec."Amount (RCY)" := + PostedExpenseReportLineVATSpec."VAT Base Amount (RCY)" + PostedExpenseReportLineVATSpec."VAT Amount (RCY)"; + end; + + PostedExpenseReportLineVATSpec."Reclaim VAT Amount (RCY)" := + Round( + PostedExpenseReportLineVATSpec."VAT Amount (RCY)" * PostedExpenseReportLineVATSpec."Reclaim %" / 100, + ReimbursementCurrency."Amount Rounding Precision"); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", OnGetPerCompanyUpgradeTags, '', false, false)] + local procedure RegisterPerCompanyUpgradeTags(var PerCompanyUpgradeTags: List of [Code[250]]) + begin + PerCompanyUpgradeTags.Add(GetBackfillReimbursementAmountsUpgradeTag()); + end; + + local procedure GetBackfillReimbursementAmountsUpgradeTag(): Code[250] + begin + exit('MS-ExpenseAgent-BackfillVATSpecReimbursementAmounts-20260818'); + end; +} diff --git a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/ExpenseReport.Page.al b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/ExpenseReport.Page.al index a12d79258f7..9d0e260e8da 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/ExpenseReport.Page.al +++ b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/ExpenseReport.Page.al @@ -468,6 +468,7 @@ page 6910 "Expense Report" CurrPage.SaveRecord(); end; } +#if not CLEAN29 action(VATSpecification) { ApplicationArea = Basic, Suite; @@ -476,8 +477,12 @@ page 6910 "Expense Report" RunObject = Page "Expense Report Line VAT Spec."; RunPageLink = "Document No." = field("No."), "Document Line No." = const(0); ToolTip = 'View the VAT details for the record.'; - Visible = (Rec."No." <> '') and AllowVATReclaim; + Visible = false; + ObsoleteReason = 'Replaced by Expense Report Statistics'; + ObsoleteState = Pending; + ObsoleteTag = '29.0'; } +#endif action("Spend Request") { ApplicationArea = Basic, Suite; @@ -627,9 +632,14 @@ page 6910 "Expense Report" actionref(dimension_Promoted; Dimensions) { } +#if not CLEAN29 actionref(VATSpecification_Promoted; VATSpecification) { + ObsoleteReason = 'Replaced by Expense Report Statistics'; + ObsoleteState = Pending; + ObsoleteTag = '29.0'; } +#endif actionref("Spend Request_Promoted"; "Spend Request") { } @@ -691,7 +701,6 @@ page 6910 "Expense Report" DocNoVisible: Boolean; ExpenseUserNo: Code[20]; ApproverComment: Text; - AllowVATReclaim: Boolean; ApprovalActionsEnabled: Boolean; protected var @@ -708,9 +717,6 @@ page 6910 "Expense Report" ReopenSubmittedEnabled := ExpenseReportApprovalMgt.CanPerformApprovalAction(Rec, RefActionType::"Reopen Submitted"); ApproveEnabled := ExpenseReportApprovalMgt.CanPerformApprovalAction(Rec, RefActionType::Approve); ReopenApprovedEnabled := ExpenseReportApprovalMgt.CanPerformApprovalAction(Rec, RefActionType::"Reopen Approved"); - - ExpenseAgentSetup.GetRecordOnce(); - AllowVATReclaim := ExpenseAgentSetup."Allow VAT Reclaim"; ApprovalActionsEnabled := ExpenseAgentSetup."Enable Agent" and ApproveEnabled and (Rec."Approver Expense User ID" = UserId()); end; diff --git a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/ExpenseReportLineVATSpec.Page.al b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/ExpenseReportLineVATSpec.Page.al index 5a2b365e1bf..36f38cb312f 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/ExpenseReportLineVATSpec.Page.al +++ b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/ExpenseReportLineVATSpec.Page.al @@ -45,17 +45,22 @@ page 7081 "Expense Report Line VAT Spec." field("VAT %"; Rec."VAT %") { Caption = 'VAT %'; - Editable = false; + } + field(Amount; Rec.Amount) + { + Caption = 'Amount'; } field("VAT Base Amount"; Rec."VAT Base Amount") { Caption = 'VAT Base Amount'; - Editable = false; } field("VAT Amount"; Rec."VAT Amount") { Caption = 'VAT Amount'; - Editable = false; + } + field("Amount (LCY)"; Rec."Amount (LCY)") + { + Caption = 'Amount (LCY)'; } field("VAT Base Amount (LCY)"; Rec."VAT Base Amount (LCY)") { @@ -67,6 +72,21 @@ page 7081 "Expense Report Line VAT Spec." Caption = 'VAT Amount (LCY)'; Editable = false; } + field("Amount (RCY)"; Rec."Amount (RCY)") + { + Caption = 'Amount (RCY)'; + Editable = false; + } + field("VAT Base Amount (RCY)"; Rec."VAT Base Amount (RCY)") + { + Caption = 'VAT Base Amount (RCY)'; + Editable = false; + } + field("VAT Amount (RCY)"; Rec."VAT Amount (RCY)") + { + Caption = 'VAT Amount (RCY)'; + Editable = false; + } field(Reclaimable; Rec.Reclaimable) { Caption = 'Reclaimable'; @@ -85,6 +105,11 @@ page 7081 "Expense Report Line VAT Spec." Caption = 'Reclaim VAT Amount (LCY)'; Editable = false; } + field("Reclaim VAT Amount (RCY)"; Rec."Reclaim VAT Amount (RCY)") + { + Caption = 'Reclaim VAT Amount (RCY)'; + Editable = false; + } field("Reclaim Reason"; Rec."Reclaim Reason") { Caption = 'Reclaim Reason'; @@ -154,6 +179,28 @@ page 7081 "Expense Report Line VAT Spec." CurrPage.Update(false); end; } + action(ApproveAllReclaims) + { + ApplicationArea = Basic, Suite; + Caption = 'Approve All Reclaims'; + Image = Approve; + ToolTip = 'Approve all VAT reclaims for the selected expense report line.'; + + trigger OnAction() + var + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + begin + ExpenseReportLineVATSpec.SetRange("Document No.", Rec."Document No."); + ExpenseReportLineVATSpec.SetRange("Document Line No.", Rec."Document Line No."); + ExpenseReportLineVATSpec.SetRange("Reclaim Status", ExpenseReportLineVATSpec."Reclaim Status"::Pending); + if ExpenseReportLineVATSpec.FindSet() then + repeat + ExpenseReportLineVATSpec.Validate("Reclaim Status", Rec."Reclaim Status"::Approved); + ExpenseReportLineVATSpec.Modify(true); + until ExpenseReportLineVATSpec.Next() = 0; + CurrPage.Update(false); + end; + } action(RejectReclaim) { ApplicationArea = Basic, Suite; @@ -178,6 +225,9 @@ page 7081 "Expense Report Line VAT Spec." actionref(ApproveReclaim_Promoted; ApproveReclaim) { } + actionref(ApproveAllReclaims_Promoted; ApproveAllReclaims) + { + } actionref(RejectReclaim_Promoted; RejectReclaim) { } diff --git a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/ExpenseReportVATSpec.Page.al b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/ExpenseReportVATSpec.Page.al index ef957a5658e..3b8cd3b6820 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/ExpenseReportVATSpec.Page.al +++ b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/ExpenseReportVATSpec.Page.al @@ -40,6 +40,24 @@ page 7096 "Expense Report VAT Spec." Caption = 'VAT Amount'; ToolTip = 'Specifies the VAT amount for this rate.'; } + field("Amount (RCY)"; Rec."Amount (RCY)") + { + Caption = 'Amount (RCY)'; + ToolTip = 'Specifies the total amount for this rate in reimbursement currency.'; + Visible = ShowRCYFields; + } + field("VAT Base Amount (RCY)"; Rec."VAT Base Amount (RCY)") + { + Caption = 'VAT Base Amount (RCY)'; + ToolTip = 'Specifies the net amount this VAT rate applies to in reimbursement currency.'; + Visible = ShowRCYFields; + } + field("VAT Amount (RCY)"; Rec."VAT Amount (RCY)") + { + Caption = 'VAT Amount (RCY)'; + ToolTip = 'Specifies the VAT amount for this rate in reimbursement currency.'; + Visible = ShowRCYFields; + } field(Reclaimable; Rec.Reclaimable) { Caption = 'Reclaimable'; @@ -60,6 +78,12 @@ page 7096 "Expense Report VAT Spec." Caption = 'Reclaim VAT Amount (LCY)'; ToolTip = 'Specifies the reclaimable VAT amount for this row.'; } + field("Reclaim VAT Amount (RCY)"; Rec."Reclaim VAT Amount (RCY)") + { + Caption = 'Reclaim VAT Amount (RCY)'; + ToolTip = 'Specifies the reclaimable VAT amount for this row in reimbursement currency.'; + Visible = ShowRCYFields; + } field("Reclaim Status"; Rec."Reclaim Status") { Caption = 'Reclaim Status'; @@ -68,4 +92,18 @@ page 7096 "Expense Report VAT Spec." } } } + + trigger OnOpenPage() + var + ExpenseReportHeader: Record "Expense Report Header"; + begin + if Rec.GetFilter("Document No.") = '' then + exit; + + if ExpenseReportHeader.Get(Rec.GetRangeMin("Document No.")) then + ShowRCYFields := ExpenseReportHeader."Reimbursement Currency Code" <> ''; + end; + + var + ShowRCYFields: Boolean; } diff --git a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/PostedExpenseReportVATSpec.Page.al b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/PostedExpenseReportVATSpec.Page.al index 0d264ec3943..328c3bf2fc9 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/PostedExpenseReportVATSpec.Page.al +++ b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Pages/PostedExpenseReportVATSpec.Page.al @@ -50,6 +50,24 @@ page 7093 "Posted Expense Report VAT Spec" Caption = 'VAT Amount'; ToolTip = 'Specifies the VAT amount for this rate.'; } + field("Amount (RCY)"; Rec."Amount (RCY)") + { + Caption = 'Amount (RCY)'; + ToolTip = 'Specifies the total amount for this rate in reimbursement currency.'; + Visible = ShowRCYFields; + } + field("VAT Base Amount (RCY)"; Rec."VAT Base Amount (RCY)") + { + Caption = 'VAT Base Amount (RCY)'; + ToolTip = 'Specifies the net amount this VAT rate applies to in reimbursement currency.'; + Visible = ShowRCYFields; + } + field("VAT Amount (RCY)"; Rec."VAT Amount (RCY)") + { + Caption = 'VAT Amount (RCY)'; + ToolTip = 'Specifies the VAT amount for this rate in reimbursement currency.'; + Visible = ShowRCYFields; + } field(Reclaimable; Rec.Reclaimable) { Caption = 'Reclaimable'; @@ -68,6 +86,12 @@ page 7093 "Posted Expense Report VAT Spec" { Caption = 'Reclaim VAT Amount (LCY)'; } + field("Reclaim VAT Amount (RCY)"; Rec."Reclaim VAT Amount (RCY)") + { + Caption = 'Reclaim VAT Amount (RCY)'; + ToolTip = 'Specifies the reclaimable VAT amount for this row in reimbursement currency.'; + Visible = ShowRCYFields; + } field("Reclaim Status"; Rec."Reclaim Status") { Caption = 'Reclaim Status'; @@ -76,4 +100,18 @@ page 7093 "Posted Expense Report VAT Spec" } } } + + trigger OnOpenPage() + var + PostedExpenseReportHeader: Record "Posted Expense Report Header"; + begin + if Rec.GetFilter("Expense Report No.") = '' then + exit; + + if PostedExpenseReportHeader.Get(Rec.GetRangeMin("Expense Report No.")) then + ShowRCYFields := PostedExpenseReportHeader."Reimbursement Currency Code" <> ''; + end; + + var + ShowRCYFields: Boolean; } diff --git a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/ExpenseReportHeader.Table.al b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/ExpenseReportHeader.Table.al index 023e1e8810c..ce9958113fe 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/ExpenseReportHeader.Table.al +++ b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/ExpenseReportHeader.Table.al @@ -687,6 +687,23 @@ table 6906 "Expense Report Header" UpdateSpendRequestOnReportLine(ExpenseReportLine); end; until ExpenseReportLine.Next() = 0; + + if CalledFromFieldName in [Rec.FieldName("Reimbursement Currency Code"), Rec.FieldName("Reimbursement Currency Factor"), Rec.FieldName("Posting Date")] then + UpdateVATSpecReimbursementAmounts(); + end; + + local procedure UpdateVATSpecReimbursementAmounts() + var + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + begin + ExpenseReportLineVATSpec.SetRange("Document No.", "No."); + if ExpenseReportLineVATSpec.FindSet() then + repeat + ExpenseReportLineVATSpec.UpdateReimbursementAmounts(Rec); +#pragma warning disable AA0214 + ExpenseReportLineVATSpec.Modify(); +#pragma warning restore AA0214 + until ExpenseReportLineVATSpec.Next() = 0; end; local procedure UpdateCurrFactorOnReportLine(var ExpenseReportLine: Record "Expense Report Line") diff --git a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/ExpenseReportLine.Table.al b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/ExpenseReportLine.Table.al index 62bacc1252e..29378b22fda 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/ExpenseReportLine.Table.al +++ b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/ExpenseReportLine.Table.al @@ -1389,14 +1389,35 @@ table 6907 "Expense Report Line" end; procedure UpdatePostingDescription(): Text[100] + begin + exit(UpdatePostingDescription("Expense Category", "Expense Subcategory Code")); + end; + + internal procedure UpdatePostingDescription(ExpenseCategoryCode: Code[20]; ExpenseSubcategoryCode: Code[20]): Text[100] var ExpenseSubcategory: Record "Expense Subcategory"; - begin - if "Expense Subcategory Code" <> '' then begin - ExpenseSubcategory.Get("Expense Category", "Expense Subcategory Code"); - exit(CopyStr(Description + ' - ' + ExpenseSubcategory."Posting Description", 1, 100)); + BaseDescription: Text[100]; + PostingDescriptionSuffix: Text; + begin + BaseDescription := Description; + if ("Expense Subcategory Code" <> '') and + ExpenseSubcategory.Get("Expense Category", "Expense Subcategory Code") and + (ExpenseSubcategory."Posting Description" <> '') + then begin + PostingDescriptionSuffix := ' / ' + ExpenseSubcategory."Posting Description"; +#pragma warning disable AA0139 + if BaseDescription.EndsWith(PostingDescriptionSuffix) then + BaseDescription := CopyStr(BaseDescription, 1, StrLen(BaseDescription) - StrLen(PostingDescriptionSuffix)); end; - exit(CopyStr(Description, 1, 100)); +#pragma warning restore AA0139 + + if (ExpenseSubcategoryCode = '') or + (not ExpenseSubcategory.Get(ExpenseCategoryCode, ExpenseSubcategoryCode)) or + (ExpenseSubcategory."Posting Description" = '') + then + exit(BaseDescription); + + exit(CopyStr(BaseDescription + ' / ' + ExpenseSubcategory."Posting Description", 1, 100)); end; local procedure InitDefaultDimensionSources(var DefaultDimSource: List of [Dictionary of [Integer, Code[20]]]; FieldNo: Integer) diff --git a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/ExpenseReportLineVATSpec.Table.al b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/ExpenseReportLineVATSpec.Table.al index 5fb1d603f8f..ef02d2f2d7c 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/ExpenseReportLineVATSpec.Table.al +++ b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/ExpenseReportLineVATSpec.Table.al @@ -52,6 +52,17 @@ table 6922 "Expense Report Line VAT Spec." MinValue = 0; MaxValue = 100; ToolTip = 'Specifies the VAT rate.'; + + trigger OnValidate() + begin + InitializeCurrency(); + "VAT Amount" := Round(Amount * "VAT %" / (100 + "VAT %"), Currency."Amount Rounding Precision", Currency.VATRoundingDirection()); + "VAT Base Amount" := Round(Amount - "VAT Amount", Currency."Amount Rounding Precision"); + "VAT Difference" := 0; + "VAT Amount (LCY)" := CalcVATAmountLCY(); + "VAT Base Amount (LCY)" := "Amount (LCY)" - "VAT Amount (LCY)"; + UpdateReimbursementAmounts(); + end; } field(11; "VAT Base Amount"; Decimal) { @@ -73,6 +84,11 @@ table 6922 "Expense Report Line VAT Spec." AutoFormatType = 1; Caption = 'Amount'; ToolTip = 'Specifies the total amount (including VAT) that the expense line consists of.'; + + trigger OnValidate() + begin + ValidateAmount(); + end; } /// /// Calculated difference between expected and actual VAT amount allowing for VAT tolerance variations. @@ -162,12 +178,47 @@ table 6922 "Expense Report Line VAT Spec." Caption = 'Expense Category'; TableRelation = "Expense Category".Code; ToolTip = 'Specifies the expense category associated with this VAT specification line, used to identify the type of expense for reporting and VAT reclaim purposes.'; + + trigger OnValidate() + var + ExpenseCategory: Record "Expense Category"; + begin + if "Expense Category" = '' then + exit; + if "Expense Subcategory" <> '' then + exit; + if ExpenseCategory.Get("Expense Category") then begin + "VAT Prod. Posting Group" := ExpenseCategory."VAT Prod. Posting Group"; + "VAT %" := ExpenseCategory."Default VAT %"; + Validate("VAT %"); + end; + end; } field(28; "Expense Subcategory"; Code[20]) { Caption = 'Expense Subcategory'; TableRelation = "Expense Subcategory".Code where("Expense Category Code" = field("Expense Category")); ToolTip = 'Specifies the expense subcategory associated with this VAT specification line, providing a more detailed classification within the expense category.'; + + trigger OnValidate() + var + ExpenseCategory: Record "Expense Category"; + ExpenseSubcategory: Record "Expense Subcategory"; + begin + if "Expense Subcategory" <> '' then begin + if ExpenseSubcategory.Get("Expense Category", "Expense Subcategory") then begin + "VAT Prod. Posting Group" := ExpenseSubcategory."VAT Prod. Posting Group"; + "VAT %" := ExpenseSubcategory."Default VAT %"; + Validate("VAT %"); + end; + end else + if "Expense Category" <> '' then + if ExpenseCategory.Get("Expense Category") then begin + "VAT Prod. Posting Group" := ExpenseCategory."VAT Prod. Posting Group"; + "VAT %" := ExpenseCategory."Default VAT %"; + Validate("VAT %"); + end; + end; } field(30; Reclaimable; Boolean) { @@ -192,22 +243,11 @@ table 6922 "Expense Report Line VAT Spec." ToolTip = 'Specifies the reclaim percentage for partial deductibility.'; trigger OnValidate() - var - Currency: Record Currency; - GLSetup: Record "General Ledger Setup"; - CurrAmountRoundingPrecision: Decimal; - LCYAmountRoundingPrecision: Decimal; begin if "Reclaim %" <> xRec."Reclaim %" then "Reclaim Status" := "Reclaim Status"::"Pending"; - GLSetup.Get(); - LCYAmountRoundingPrecision := GLSetup."Amount Rounding Precision"; - CurrAmountRoundingPrecision := LCYAmountRoundingPrecision; - if ("Currency Code" <> '') and Currency.Get("Currency Code") then - CurrAmountRoundingPrecision := Currency."Amount Rounding Precision"; - "Reclaim VAT Amount" := Round("VAT Amount" * "Reclaim %" / 100, CurrAmountRoundingPrecision); - "Reclaim VAT Amount (LCY)" := Round("VAT Amount (LCY)" * "Reclaim %" / 100, LCYAmountRoundingPrecision); + UpdateReclaimAmounts(); end; } field(32; "Reclaim Reason"; Text[250]) @@ -304,6 +344,45 @@ table 6922 "Expense Report Line VAT Spec." Rec."Reclaim Approved At" := CurrentDateTime(); end; } + /// + /// VAT amount converted to local currency for accounting and reporting purposes. + /// + field(50; "VAT Base Amount (RCY)"; Decimal) + { + AutoFormatType = 1; + AutoFormatExpression = GetReimbursementCurrencyCode(); + Caption = 'VAT Base Amount (RCY)'; + DataClassification = CustomerContent; + Editable = false; + ToolTip = 'Specifies the VAT base amount in reimbursement currency for this rate.'; + } + field(51; "VAT Amount (RCY)"; Decimal) + { + AutoFormatType = 1; + AutoFormatExpression = GetReimbursementCurrencyCode(); + Caption = 'VAT Amount (RCY)'; + DataClassification = CustomerContent; + Editable = false; + ToolTip = 'Specifies the VAT amount in reimbursement currency for this rate.'; + } + field(52; "Amount (RCY)"; Decimal) + { + AutoFormatType = 1; + AutoFormatExpression = GetReimbursementCurrencyCode(); + Caption = 'Amount (RCY)'; + DataClassification = CustomerContent; + Editable = false; + ToolTip = 'Specifies the total amount in reimbursement currency for this rate.'; + } + field(53; "Reclaim VAT Amount (RCY)"; Decimal) + { + AutoFormatType = 1; + AutoFormatExpression = GetReimbursementCurrencyCode(); + Caption = 'Reclaim VAT Amount (RCY)'; + DataClassification = CustomerContent; + Editable = false; + ToolTip = 'Specifies the reclaim VAT amount in reimbursement currency for this rate.'; + } } keys @@ -315,6 +394,148 @@ table 6922 "Expense Report Line VAT Spec." key(Reclaim; "Document No.", Reclaimable, "Reclaim Status") { } } + var + Currency: Record Currency; + CurrencyExchangeRate: Record "Currency Exchange Rate"; + ExpenseReportHeader: Record "Expense Report Header"; + ExpenseReportLine: Record "Expense Report Line"; + + local procedure CalcVATAmountLCY(): Decimal + var + LCYCurrency: Record Currency; + VATAmountLCY: Decimal; + begin + if "Currency Code" = '' then + exit("VAT Amount"); + + LCYCurrency.InitRoundingPrecision(); + InitializeCurrency(); + + "VAT Difference" := + "VAT Amount" - + Round(Amount * "VAT %" / (100 + "VAT %"), Currency."Amount Rounding Precision", Currency.VATRoundingDirection()); + + if "VAT Difference" = 0 then + VATAmountLCY := Round("Amount (LCY)" * "VAT %" / (100 + "VAT %"), LCYCurrency."Amount Rounding Precision", LCYCurrency.VATRoundingDirection()) + else begin + GetExpenseReportLine(); + VATAmountLCY := + Round( + CurrencyExchangeRate.ExchangeAmtFCYToLCY(ExpenseReportLine."Expense Date", "Currency Code", "VAT Amount", "Currency Factor"), + LCYCurrency."Amount Rounding Precision", LCYCurrency.VATRoundingDirection()); + end; + + exit(VATAmountLCY); + end; + + local procedure GetExpenseReportHeader() + begin + if "Document No." <> ExpenseReportHeader."No." then + ExpenseReportHeader.Get("Document No."); + end; + + local procedure GetExpenseReportLine() + begin + if ("Document No." <> ExpenseReportLine."Document No.") or ("Document Line No." <> ExpenseReportLine."Line No.") then + ExpenseReportLine.Get("Document No.", "Document Line No."); + end; + + local procedure GetReimbursementCurrencyCode(): Code[20] + begin + GetExpenseReportHeader(); + exit(ExpenseReportHeader."Reimbursement Currency Code"); + end; + + local procedure InitializeCurrency() + begin + if "Currency Code" = '' then begin + Clear(Currency); + Currency.InitRoundingPrecision(); + end else + if "Currency Code" <> Currency.Code then begin + Currency.Get("Currency Code"); + Currency.TestField("Amount Rounding Precision"); + end; + end; + + local procedure UpdateReclaimAmounts() + begin + GetExpenseReportHeader(); + UpdateReclaimAmounts(ExpenseReportHeader."Reimbursement Currency Code"); + end; + + local procedure UpdateReclaimAmounts(ReimbursementCurrencyCode: Code[10]) + var + GLSetup: Record "General Ledger Setup"; + ReimbursementCurrency: Record Currency; + CurrencyAmountRoundingPrecision: Decimal; + LCYAmountRoundingPrecision: Decimal; + ReimbursementAmountRoundingPrecision: Decimal; + begin + GLSetup.Get(); + LCYAmountRoundingPrecision := GLSetup."Amount Rounding Precision"; + CurrencyAmountRoundingPrecision := LCYAmountRoundingPrecision; + if ("Currency Code" <> '') and Currency.Get("Currency Code") then + CurrencyAmountRoundingPrecision := Currency."Amount Rounding Precision"; + + ReimbursementCurrency.Initialize(ReimbursementCurrencyCode); + ReimbursementAmountRoundingPrecision := ReimbursementCurrency."Amount Rounding Precision"; + + "Reclaim VAT Amount" := Round("VAT Amount" * "Reclaim %" / 100, CurrencyAmountRoundingPrecision); + "Reclaim VAT Amount (LCY)" := Round("VAT Amount (LCY)" * "Reclaim %" / 100, LCYAmountRoundingPrecision); + "Reclaim VAT Amount (RCY)" := Round("VAT Amount (RCY)" * "Reclaim %" / 100, ReimbursementAmountRoundingPrecision); + end; + + internal procedure UpdateReimbursementAmounts() + begin + GetExpenseReportHeader(); + UpdateReimbursementAmounts(ExpenseReportHeader); + end; + + internal procedure UpdateReimbursementAmounts(NewExpenseReportHeader: Record "Expense Report Header") + var + ReimbursementCurrency: Record Currency; + begin + ReimbursementCurrency.Initialize(NewExpenseReportHeader."Reimbursement Currency Code"); + + if NewExpenseReportHeader."Reimbursement Currency Code" = '' then begin + "VAT Base Amount (RCY)" := "VAT Base Amount (LCY)"; + "VAT Amount (RCY)" := "VAT Amount (LCY)"; + "Amount (RCY)" := "Amount (LCY)"; + end else begin + "VAT Base Amount (RCY)" := + Round( + CurrencyExchangeRate.ExchangeAmtLCYToFCY( + NewExpenseReportHeader."Posting Date", NewExpenseReportHeader."Reimbursement Currency Code", + "VAT Base Amount (LCY)", NewExpenseReportHeader."Reimbursement Currency Factor"), + ReimbursementCurrency."Amount Rounding Precision"); + "VAT Amount (RCY)" := + Round( + CurrencyExchangeRate.ExchangeAmtLCYToFCY( + NewExpenseReportHeader."Posting Date", NewExpenseReportHeader."Reimbursement Currency Code", + "VAT Amount (LCY)", NewExpenseReportHeader."Reimbursement Currency Factor"), + ReimbursementCurrency."Amount Rounding Precision"); + "Amount (RCY)" := "VAT Base Amount (RCY)" + "VAT Amount (RCY)"; + end; + + UpdateReclaimAmounts(NewExpenseReportHeader."Reimbursement Currency Code"); + end; + + local procedure ValidateAmount() + begin + InitializeCurrency(); + if "Currency Code" = '' then + "Amount (LCY)" := Amount + else begin + GetExpenseReportLine(); + "Amount (LCY)" := Round( + CurrencyExchangeRate.ExchangeAmtFCYToLCY(ExpenseReportLine."Expense Date", "Currency Code", Amount, "Currency Factor")); + end; + + Amount := Round(Amount, Currency."Amount Rounding Precision"); + Validate("VAT %"); + end; + /// Stores a UTF-8 reclaim justification text into the blob. procedure SetJustification(NewText: Text) var diff --git a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/PostedExpRepLineVATSpec.Table.al b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/PostedExpRepLineVATSpec.Table.al index bc49985be51..55105475af0 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/PostedExpRepLineVATSpec.Table.al +++ b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Tables/PostedExpRepLineVATSpec.Table.al @@ -235,6 +235,42 @@ table 6934 "Posted Exp. Rep. Line VAT Spec" Editable = false; ToolTip = 'Specifies whether the VAT reclaim for this row is pending, approved, or rejected.'; } + field(50; "VAT Base Amount (RCY)"; Decimal) + { + AutoFormatType = 1; + AutoFormatExpression = GetReimbursementCurrencyCode(); + Caption = 'VAT Base Amount (RCY)'; + DataClassification = CustomerContent; + Editable = false; + ToolTip = 'Specifies the VAT base amount in reimbursement currency for this rate.'; + } + field(51; "VAT Amount (RCY)"; Decimal) + { + AutoFormatType = 1; + AutoFormatExpression = GetReimbursementCurrencyCode(); + Caption = 'VAT Amount (RCY)'; + DataClassification = CustomerContent; + Editable = false; + ToolTip = 'Specifies the VAT amount in reimbursement currency for this rate.'; + } + field(52; "Amount (RCY)"; Decimal) + { + AutoFormatType = 1; + AutoFormatExpression = GetReimbursementCurrencyCode(); + Caption = 'Amount (RCY)'; + DataClassification = CustomerContent; + Editable = false; + ToolTip = 'Specifies the total amount in reimbursement currency for this rate.'; + } + field(53; "Reclaim VAT Amount (RCY)"; Decimal) + { + AutoFormatType = 1; + AutoFormatExpression = GetReimbursementCurrencyCode(); + Caption = 'Reclaim VAT Amount (RCY)'; + DataClassification = CustomerContent; + Editable = false; + ToolTip = 'Specifies the reclaim VAT amount in reimbursement currency for this rate.'; + } } keys @@ -248,6 +284,14 @@ table 6934 "Posted Exp. Rep. Line VAT Spec" } } + local procedure GetReimbursementCurrencyCode(): Code[20] + var + PostedExpenseReportHeader: Record "Posted Expense Report Header"; + begin + if PostedExpenseReportHeader.Get("Expense Report No.") then + exit(PostedExpenseReportHeader."Reimbursement Currency Code"); + end; + /// Stores a UTF-8 reclaim justification text into the blob. procedure SetJustification(NewText: Text) var diff --git a/src/Apps/W1/ExpenseAgent/test/src/ExpenseNoRuleTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/ExpenseNoRuleTest.Codeunit.al index 49c49f1d273..c6fc26b8400 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/ExpenseNoRuleTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/ExpenseNoRuleTest.Codeunit.al @@ -370,7 +370,7 @@ codeunit 148311 "Expense No Rule Test" FindExpenseReportLine(ExpenseReportLine, CopyStr(ExpenseReportPage."No.".Value(), 1, 20)); // [GIVEN] Update "Merchant Name" in Expense Report Line. - ExpenseReportLine.Validate("Merchant Name", LibraryRandom.RandText(20)); + ExpenseReportLine.Validate("Merchant Name", CopyStr(LibraryRandom.RandText(20), 1, 100)); ExpenseReportLine.Modify(); // [WHEN] Release Expense Report. diff --git a/src/Apps/W1/ExpenseAgent/test/src/ExpensePostingVATTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/ExpensePostingVATTest.Codeunit.al index 4f28fd7ca42..64b8a0eee4a 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/ExpensePostingVATTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/ExpensePostingVATTest.Codeunit.al @@ -5,6 +5,7 @@ namespace Microsoft.Test.ExpenseAgent; using Microsoft.ExpenseAgent; +using Microsoft.Finance.Currency; using Microsoft.Finance.GeneralLedger.Ledger; using Microsoft.Finance.VAT.Ledger; using Microsoft.Finance.VAT.Setup; @@ -25,7 +26,12 @@ codeunit 148330 "Expense Posting VAT Test" LibraryTestInitialize: Codeunit "Library - Test Initialize"; LibraryUtility: Codeunit "Library - Utility"; IsInitialized: Boolean; + ExpectedExpenseNo: Code[20]; + ExpectedExpenseReportNo: Code[20]; + PostExpenseReportQst: Label 'Do you want to post Expense Report %1?', Comment = '%1 = Expense Report No.'; + NotApprovedForVATReclaimCategoryErr: Label 'VAT Reclaim Status is not set for Line with Expense Category %1.', Comment = '%1 = Expense Category'; NotApprovedForVATReclaimErr: Label 'VAT Reclaim Status is not set for Line with Expense Category %1 and Expense Subcategory %2.', Comment = '%1 = Expense Category, %2 = Expense Subcategory'; + ModifyOrDeleteAgentVATSpecErr: Label 'Modifications and delete are not allowed for records created by the Expense Agent API.'; [Test] [HandlerFunctions('ExpensesModalPageHandler,ConfirmHandler')] @@ -327,13 +333,13 @@ codeunit 148330 "Expense Posting VAT Test" // [GIVEN] Expense report is created with expenses added LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", '', Expense."VAT Bus. Posting Group"); - CreateExpenseReport.AddExpensesToReport(ExpenseReportHeader); + AddExpensesToReport(CreateExpenseReport, ExpenseReportHeader, Expense."No."); UpdateExpenseReportLinesWithVendor(ExpenseReportHeader); UpdateExpenseReportVATSpecLineStatuses(ExpenseReportHeader); ExpenseReportHeader.PerformManualRelease(); // [WHEN] Expense report is posted - ExpenseReportPost.PostExpenseReport(ExpenseReportHeader); + PostExpenseReportWithConfirmation(ExpenseReportPost, ExpenseReportHeader); // [THEN] Posted expense report is created FindPostedExpenseReport(PostedExpenseReportHeader, Expense); @@ -389,12 +395,12 @@ codeunit 148330 "Expense Posting VAT Test" CreateExpenseWithHotelItemizations(Expense, ExpenseUser, ExpenseCategory, ExpenseSubCategory, VATPostingSetup, 100, 50, 0, 0); LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", '', Expense."VAT Bus. Posting Group"); - CreateExpenseReport.AddExpensesToReport(ExpenseReportHeader); + AddExpensesToReport(CreateExpenseReport, ExpenseReportHeader, Expense."No."); UpdateExpenseReportLinesWithVendorKeepingPending(ExpenseReportHeader); ExpenseReportHeader.PerformManualRelease(); // [WHEN] Posting is attempted - asserterror ExpenseReportPost.PostExpenseReport(ExpenseReportHeader); + asserterror PostExpenseReportWithConfirmation(ExpenseReportPost, ExpenseReportHeader); // [THEN] A reclaim-status error is raised and posting does not create a posted report. Assert.ExpectedError(StrSubstNo(NotApprovedForVATReclaimErr, ExpenseCategory.Code, ExpenseSubCategory[1].Code)); @@ -402,6 +408,426 @@ codeunit 148330 "Expense Posting VAT Test" Assert.RecordCount(PostedExpenseReportHeader, 0); end; + [Test] + [HandlerFunctions('ExpensesModalPageHandler,ConfirmHandler')] + procedure VATSpecWithoutSubcategoryPendingStatusBlocksPosting() + var + Expense: Record Expense; + ExpenseCategory: Record "Expense Category"; + ExpenseReportHeader: Record "Expense Report Header"; + ExpenseSubCategory: Record "Expense Subcategory"; + ExpenseUser: Record "Expense User"; + PostedExpenseReportHeader: Record "Posted Expense Report Header"; + VATPostingSetup: Record "VAT Posting Setup"; + CreateExpenseReport: Codeunit "Create Expense Report"; + ExpenseReportPost: Codeunit "Expense Report-Post"; + begin + // [SCENARIO] Posting is blocked when a category-only VAT specification remains pending. + Initialize(); + + // [GIVEN] A non-itemized expense with a category-level VAT specification. + LibraryExpense.CreateExpenseUser(ExpenseUser); + LibraryExpense.CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); + CreateSubcategoryWithVATRate(ExpenseSubCategory, ExpenseCategory.Code, 20, VATPostingSetup); + ExpenseCategory.Validate("VAT Prod. Posting Group", VATPostingSetup."VAT Prod. Posting Group"); + ExpenseCategory.Validate("Default VAT %", VATPostingSetup."VAT %"); + ExpenseCategory.Modify(true); + LibraryExpense.CreateExpense(Expense, ExpenseUser."No.", ExpenseCategory.Code, '', '', true, '', 120); + Expense.UpdateVATSpecification(Expense."No."); + ReleaseExpenseAndUpdateAccounts(Expense, ExpenseUser); + + LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", '', Expense."VAT Bus. Posting Group"); + AddExpensesToReport(CreateExpenseReport, ExpenseReportHeader, Expense."No."); + UpdateExpenseReportLinesWithVendorKeepingPending(ExpenseReportHeader); + ExpenseReportHeader.PerformManualRelease(); + + // [WHEN] Posting is attempted. + asserterror PostExpenseReportWithConfirmation(ExpenseReportPost, ExpenseReportHeader); + + // [THEN] The error identifies the expense category without requiring a subcategory. + Assert.ExpectedError(StrSubstNo(NotApprovedForVATReclaimCategoryErr, ExpenseCategory.Code)); + PostedExpenseReportHeader.SetRange("Expense User No.", ExpenseUser."No."); + Assert.RecordCount(PostedExpenseReportHeader, 0); + end; + + [Test] + [HandlerFunctions('ExpensesModalPageHandler,ConfirmHandler')] + procedure ModifiedVATSpecIsRecalculatedAndPostedInReimbursementCurrency() + var + Currency: Record Currency; + CurrencyExchangeRate: Record "Currency Exchange Rate"; + Expense: Record Expense; + ExpenseUser: Record "Expense User"; + ExpenseCategory: Record "Expense Category"; + ExpenseSubCategory: array[4] of Record "Expense Subcategory"; + ExpenseReportHeader: Record "Expense Report Header"; + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + PostedExpenseReportHeader: Record "Posted Expense Report Header"; + PostedExpenseReportLineVATSpec: Record "Posted Exp. Rep. Line VAT Spec"; + VATPostingSetup: array[3] of Record "VAT Posting Setup"; + GLEntry: Record "G/L Entry"; + CreateExpenseReport: Codeunit "Create Expense Report"; + ExpenseReportPost: Codeunit "Expense Report-Post"; + CurrencyCode: Code[10]; + ExpectedVATBaseAmountRCY: Decimal; + ExpectedVATAmountRCY: Decimal; + begin + // [SCENARIO] A copied VAT specification can be changed and is posted in reimbursement currency. + Initialize(); + + // [GIVEN] An LCY expense for 110 with 10% VAT and an expense report in a foreign reimbursement currency. + CreateExpenseUserAndCategory(ExpenseUser, ExpenseCategory); + CreateSubcategoryWithVATRate(ExpenseSubCategory[1], ExpenseCategory.Code, 10, VATPostingSetup[1]); + CreateExpenseWithHotelItemizations(Expense, ExpenseUser, ExpenseCategory, ExpenseSubCategory, VATPostingSetup, 110, 0, 0, 0); + CurrencyCode := LibraryERM.CreateCurrencyWithExchangeRate(WorkDate(), 1, 2); + Currency.Get(CurrencyCode); + + LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", CurrencyCode, Expense."VAT Bus. Posting Group"); + AddExpensesToReport(CreateExpenseReport, ExpenseReportHeader, Expense."No."); + + // [WHEN] The VAT rate is changed to 20% on the copied specification. + ExpenseReportLineVATSpec.SetRange("Document No.", ExpenseReportHeader."No."); + ExpenseReportLineVATSpec.FindFirst(); + ExpenseReportLineVATSpec.Validate("VAT %", 20); + ExpenseReportLineVATSpec.Modify(true); + + // [THEN] Transaction, LCY, and reimbursement-currency amounts are recalculated from the gross amount. + ExpectedVATBaseAmountRCY := + Round( + CurrencyExchangeRate.ExchangeAmtLCYToFCY( + ExpenseReportHeader."Posting Date", CurrencyCode, ExpenseReportLineVATSpec."VAT Base Amount (LCY)", + ExpenseReportHeader."Reimbursement Currency Factor"), + Currency."Amount Rounding Precision"); + ExpectedVATAmountRCY := + Round( + CurrencyExchangeRate.ExchangeAmtLCYToFCY( + ExpenseReportHeader."Posting Date", CurrencyCode, ExpenseReportLineVATSpec."VAT Amount (LCY)", + ExpenseReportHeader."Reimbursement Currency Factor"), + Currency."Amount Rounding Precision"); + Assert.AreNearlyEqual(91.67, ExpenseReportLineVATSpec."VAT Base Amount", 0.01, 'VAT base amount must be recalculated.'); + Assert.AreNearlyEqual(18.33, ExpenseReportLineVATSpec."VAT Amount", 0.01, 'VAT amount must be recalculated.'); + Assert.AreNearlyEqual(ExpectedVATBaseAmountRCY, ExpenseReportLineVATSpec."VAT Base Amount (RCY)", 0.01, 'VAT base amount in reimbursement currency must be calculated from LCY.'); + Assert.AreNearlyEqual(ExpectedVATAmountRCY, ExpenseReportLineVATSpec."VAT Amount (RCY)", 0.01, 'VAT amount in reimbursement currency must be calculated from LCY.'); + Assert.AreNearlyEqual(ExpectedVATBaseAmountRCY + ExpectedVATAmountRCY, ExpenseReportLineVATSpec."Amount (RCY)", 0.01, 'Gross amount in reimbursement currency must equal base plus VAT.'); + + // [WHEN] The report is approved for reclaim and posted. + UpdateExpenseReportLinesWithVendor(ExpenseReportHeader); + ExpenseReportHeader.PerformManualRelease(); + PostExpenseReportWithConfirmation(ExpenseReportPost, ExpenseReportHeader); + + // [THEN] The VAT specification journal line is posted with RCY as source currency and LCY as G/L amount. + FindPostedExpenseReport(PostedExpenseReportHeader, Expense); + PostedExpenseReportLineVATSpec.SetRange("Expense Report No.", PostedExpenseReportHeader."No."); + PostedExpenseReportLineVATSpec.SetFilter("Expense Report Line No.", '<>%1', 0); + PostedExpenseReportLineVATSpec.FindFirst(); + Assert.AreNearlyEqual(ExpectedVATBaseAmountRCY, PostedExpenseReportLineVATSpec."VAT Base Amount (RCY)", 0.01, 'Posted VAT base amount in reimbursement currency must be retained.'); + Assert.AreNearlyEqual(ExpectedVATAmountRCY, PostedExpenseReportLineVATSpec."VAT Amount (RCY)", 0.01, 'Posted VAT amount in reimbursement currency must be retained.'); + Assert.AreNearlyEqual(ExpectedVATBaseAmountRCY + ExpectedVATAmountRCY, PostedExpenseReportLineVATSpec."Amount (RCY)", 0.01, 'Posted gross amount in reimbursement currency must be retained.'); + Assert.AreNearlyEqual(ExpectedVATAmountRCY, PostedExpenseReportLineVATSpec."Reclaim VAT Amount (RCY)", 0.01, 'Posted reclaim VAT amount in reimbursement currency must be retained.'); + GLEntry.SetRange("Document No.", PostedExpenseReportHeader."No."); + GLEntry.SetRange("G/L Account No.", GetRefundableDebitAccount(ExpenseCategory.Code)); + GLEntry.SetRange("Source Currency Code", CurrencyCode); + GLEntry.SetRange(Description, CopyStr(Expense.Description + ' / ' + ExpenseSubCategory[1]."Posting Description", 1, MaxStrLen(GLEntry.Description))); + GLEntry.CalcSums(Amount, "Source Currency Amount", "Source Currency VAT Amount"); + Assert.AreNearlyEqual(ExpenseReportLineVATSpec."VAT Base Amount (LCY)", GLEntry.Amount, 0.01, 'Posted G/L amount must use the VAT specification LCY base.'); + Assert.AreNearlyEqual(ExpectedVATBaseAmountRCY, GLEntry."Source Currency Amount", 0.01, 'Posted source currency amount must use the VAT specification reimbursement amount.'); + Assert.AreNearlyEqual(ExpectedVATAmountRCY, GLEntry."Source Currency VAT Amount", 0.01, 'Posted source currency VAT amount must use the VAT specification reimbursement amount.'); + end; + + [Test] + [HandlerFunctions('ExpensesModalPageHandler,ConfirmHandler')] + procedure PartialVATReclaimPostsReclaimAmountsAndNonDeductibleVAT() + begin + // [SCENARIO] A partially reclaimable VAT specification posts deductible and non-deductible VAT separately. + VerifyPartialVATReclaimPosting(50, 50, 10, 50, 10, 10, 20); + end; + + [Test] + [HandlerFunctions('ExpensesModalPageHandler,ConfirmHandler')] + procedure AsymmetricPartialVATReclaimPostsDifferentDeductibleAmounts() + begin + // [SCENARIO] An asymmetric partial reclaim keeps deductible and non-deductible VAT amounts distinct. + VerifyPartialVATReclaimPosting(70, 70, 14, 30, 6, 14, 28); + end; + + [Test] + [HandlerFunctions('ExpensesModalPageHandler,ConfirmHandler')] + procedure VATSpecPostingBalancesForeignReimbursementCurrency() + var + Expense: Record Expense; + ExpenseUser: Record "Expense User"; + ExpenseCategory: Record "Expense Category"; + ExpenseSubCategory: array[4] of Record "Expense Subcategory"; + ExpenseReportHeader: Record "Expense Report Header"; + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + PostedExpenseReportHeader: Record "Posted Expense Report Header"; + VATPostingSetup: array[3] of Record "VAT Posting Setup"; + GLEntry: Record "G/L Entry"; + CreateExpenseReport: Codeunit "Create Expense Report"; + ExpenseReportPost: Codeunit "Expense Report-Post"; + CurrencyCode: Code[10]; + begin + // [SCENARIO] Component rounding in a foreign reimbursement currency does not leave the G/L transaction out of balance. + Initialize(); + + // [GIVEN] A 200.02 LCY expense split into two VAT specs and an 8.56 reimbursement-currency exchange rate. + CreateExpenseUserAndCategory(ExpenseUser, ExpenseCategory); + CreateSubcategoryWithVATRate(ExpenseSubCategory[1], ExpenseCategory.Code, 20, VATPostingSetup[1]); + CreateSubcategoryWithVATRate(ExpenseSubCategory[2], ExpenseCategory.Code, 20, VATPostingSetup[1]); + CreateExpenseWithHotelItemizations(Expense, ExpenseUser, ExpenseCategory, ExpenseSubCategory, VATPostingSetup, 100.01, 100.01, 0, 0); + CurrencyCode := LibraryERM.CreateCurrencyWithExchangeRate(WorkDate(), 1, 8.56); + LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", CurrencyCode, Expense."VAT Bus. Posting Group"); + AddExpensesToReport(CreateExpenseReport, ExpenseReportHeader, Expense."No."); + ExpenseReportLineVATSpec.SetRange("Document No.", ExpenseReportHeader."No."); + ExpenseReportLineVATSpec.FindFirst(); + ExpenseReportLineVATSpec."VAT Base Amount (LCY)" += 0.01; + ExpenseReportLineVATSpec."VAT Base Amount (RCY)" += 0.08; + ExpenseReportLineVATSpec."Amount (RCY)" += 0.08; + ExpenseReportLineVATSpec.Modify(); + + // [WHEN] The report is approved and posted. + UpdateExpenseReportLinesWithVendor(ExpenseReportHeader); + ExpenseReportHeader.PerformManualRelease(); + PostExpenseReportWithConfirmation(ExpenseReportPost, ExpenseReportHeader); + + // [THEN] The document balances in LCY and reimbursement currency. + FindPostedExpenseReport(PostedExpenseReportHeader, Expense); + GLEntry.SetRange("Document No.", PostedExpenseReportHeader."No."); + GLEntry.CalcSums(Amount, "Source Currency Amount"); + Assert.AreEqual(0, GLEntry.Amount, 'The posted G/L entries must balance in LCY.'); + Assert.AreEqual(0, GLEntry."Source Currency Amount", 'The posted G/L entries must balance in reimbursement currency.'); + end; + + [Test] + procedure UpdateVATSpecificationCreatesSpecForNonItemizedExpense() + var + Expense: Record Expense; + ExpenseCategory: Record "Expense Category"; + ExpenseSubCategory: Record "Expense Subcategory"; + ExpenseUser: Record "Expense User"; + ExpenseVATSpecification: Record "Expense VAT Specification"; + VATPostingSetup: Record "VAT Posting Setup"; + begin + // [SCENARIO] Updating the VAT specification for a non-itemized expense creates one row from the expense category. + Initialize(); + + // [GIVEN] A non-itemized expense category with 20% VAT and an expense for 120 LCY. + LibraryExpense.CreateExpenseUser(ExpenseUser); + LibraryExpense.CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); + CreateSubcategoryWithVATRate(ExpenseSubCategory, ExpenseCategory.Code, 20, VATPostingSetup); + ExpenseCategory.Validate("VAT Prod. Posting Group", VATPostingSetup."VAT Prod. Posting Group"); + ExpenseCategory.Validate("Default VAT %", VATPostingSetup."VAT %"); + ExpenseCategory.Modify(true); + LibraryExpense.CreateExpense(Expense, ExpenseUser."No.", ExpenseCategory.Code, '', '', true, '', 120); + + // [WHEN] The VAT specification is updated. + Expense.UpdateVATSpecification(Expense."No."); + + // [THEN] One manual VAT specification contains the category defaults and calculated VAT amounts. + ExpenseVATSpecification.SetRange("Expense No.", Expense."No."); + Assert.RecordCount(ExpenseVATSpecification, 1); + ExpenseVATSpecification.FindFirst(); + Assert.AreEqual(ExpenseVATSpecification.Source::Manual, ExpenseVATSpecification.Source, 'The generated VAT specification must be manual.'); + Assert.AreEqual(ExpenseCategory.Code, ExpenseVATSpecification."Expense Category", 'The expense category must be copied.'); + Assert.AreEqual('', ExpenseVATSpecification."Expense Subcategory", 'A non-itemized VAT specification must not have a subcategory.'); + Assert.AreEqual(VATPostingSetup."VAT Bus. Posting Group", ExpenseVATSpecification."VAT Bus. Posting Group", 'The default VAT business posting group must be used.'); + Assert.AreEqual(VATPostingSetup."VAT Prod. Posting Group", ExpenseVATSpecification."VAT Prod. Posting Group", 'The category VAT product posting group must be used.'); + Assert.AreNearlyEqual(20, ExpenseVATSpecification."VAT %", 0.01, 'The category VAT percentage must be used.'); + Assert.AreNearlyEqual(120, ExpenseVATSpecification.Amount, 0.01, 'The expense amount must be used.'); + Assert.AreNearlyEqual(100, ExpenseVATSpecification."VAT Base Amount", 0.01, 'The VAT base amount must be calculated.'); + Assert.AreNearlyEqual(20, ExpenseVATSpecification."VAT Amount", 0.01, 'The VAT amount must be calculated.'); + end; + + [Test] + procedure UpdateVATSpecificationCreatesSpecForZeroRatedNonItemizedExpense() + var + Expense: Record Expense; + ExpenseCategory: Record "Expense Category"; + ExpenseSubCategory: Record "Expense Subcategory"; + ExpenseUser: Record "Expense User"; + ExpenseVATSpecification: Record "Expense VAT Specification"; + VATPostingSetup: Record "VAT Posting Setup"; + begin + // [SCENARIO] Updating the VAT specification preserves the 0% bucket for a non-itemized expense. + Initialize(); + + // [GIVEN] A zero-rated non-itemized category with a VAT product posting group and an expense for 120 LCY. + LibraryExpense.CreateExpenseUser(ExpenseUser); + LibraryExpense.CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); + CreateSubcategoryWithVATRate(ExpenseSubCategory, ExpenseCategory.Code, 0, VATPostingSetup); + ExpenseCategory.Validate("VAT Prod. Posting Group", VATPostingSetup."VAT Prod. Posting Group"); + ExpenseCategory.Validate("Default VAT %", VATPostingSetup."VAT %"); + ExpenseCategory.Modify(true); + LibraryExpense.CreateExpense(Expense, ExpenseUser."No.", ExpenseCategory.Code, '', '', true, '', 120); + + // [WHEN] The VAT specification is updated. + Expense.UpdateVATSpecification(Expense."No."); + + // [THEN] One zero-rated VAT specification retains the category VAT product posting group. + ExpenseVATSpecification.SetRange("Expense No.", Expense."No."); + Assert.RecordCount(ExpenseVATSpecification, 1); + ExpenseVATSpecification.FindFirst(); + Assert.AreEqual(ExpenseVATSpecification.Source::Manual, ExpenseVATSpecification.Source, 'The generated VAT specification must be manual.'); + Assert.AreEqual(VATPostingSetup."VAT Prod. Posting Group", ExpenseVATSpecification."VAT Prod. Posting Group", 'The zero-rated VAT product posting group must be retained.'); + Assert.AreEqual(0, ExpenseVATSpecification."VAT %", 'The VAT specification must remain zero-rated.'); + Assert.AreNearlyEqual(120, ExpenseVATSpecification."VAT Base Amount", 0.01, 'The full expense amount must be retained as the VAT base.'); + Assert.AreEqual(0, ExpenseVATSpecification."VAT Amount", 'The zero-rated VAT amount must be zero.'); + end; + + [Test] + procedure AgentVATSpecificationCannotBeUpdatedModifiedOrDeleted() + var + Expense: Record Expense; + ExpenseCategory: Record "Expense Category"; + ExpenseSubCategory: Record "Expense Subcategory"; + ExpenseUser: Record "Expense User"; + ExpenseVATSpecification: Record "Expense VAT Specification"; + VATPostingSetup: Record "VAT Posting Setup"; + begin + // [SCENARIO] Agent-authored VAT specifications cannot be regenerated, modified, or deleted. + Initialize(); + + // [GIVEN] An expense with an Agent-authored VAT specification. + LibraryExpense.CreateExpenseUser(ExpenseUser); + LibraryExpense.CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); + CreateSubcategoryWithVATRate(ExpenseSubCategory, ExpenseCategory.Code, 20, VATPostingSetup); + LibraryExpense.CreateExpense(Expense, ExpenseUser."No.", ExpenseCategory.Code, '', '', true, '', 120); + + ExpenseVATSpecification.Init(); + ExpenseVATSpecification."Expense No." := Expense."No."; + ExpenseVATSpecification."Line No." := 1; + ExpenseVATSpecification.Source := ExpenseVATSpecification.Source::Agent; + ExpenseVATSpecification.Amount := 120; + ExpenseVATSpecification.Insert(true); + Commit(); + + // [WHEN] The VAT specification is regenerated, modified, or deleted. + asserterror Expense.UpdateVATSpecification(Expense."No."); + Assert.ExpectedError(ModifyOrDeleteAgentVATSpecErr); + + ExpenseVATSpecification.Amount := 121; + asserterror ExpenseVATSpecification.Modify(true); + Assert.ExpectedError(ModifyOrDeleteAgentVATSpecErr); + + asserterror ExpenseVATSpecification.Delete(true); + Assert.ExpectedError(ModifyOrDeleteAgentVATSpecErr); + + // [THEN] The Agent-authored VAT specification remains unchanged. + ExpenseVATSpecification.Get(Expense."No.", 1); + Assert.AreEqual(ExpenseVATSpecification.Source::Agent, ExpenseVATSpecification.Source, 'The VAT specification source must remain Agent.'); + Assert.AreEqual(120, ExpenseVATSpecification.Amount, 'The Agent-authored VAT specification must remain unchanged.'); + end; + + [Test] + procedure UpdateVATSpecificationAggregatesItemizationsBySubcategory() + var + Expense: Record Expense; + ExpenseCategory: Record "Expense Category"; + ExpenseItemization: array[2] of Record "Expense Itemization"; + ExpenseSubCategory: Record "Expense Subcategory"; + ExpenseUser: Record "Expense User"; + ExpenseVATSpecification: Record "Expense VAT Specification"; + VATPostingSetup: Record "VAT Posting Setup"; + begin + // [SCENARIO] Updating the VAT specification for an itemized expense aggregates equal category and subcategory rows. + Initialize(); + + // [GIVEN] An itemized expense with two amounts in the same 20% VAT subcategory. + CreateExpenseUserAndCategory(ExpenseUser, ExpenseCategory); + CreateSubcategoryWithVATRate(ExpenseSubCategory, ExpenseCategory.Code, 20, VATPostingSetup); + LibraryExpense.CreateExpense(Expense, ExpenseUser."No.", ExpenseCategory.Code, ExpenseSubCategory.Code, '', true, '', 120); + LibraryExpense.CreateExpenseItemization(ExpenseItemization[1], Expense, ExpenseCategory.Code, ExpenseSubCategory.Code, WorkDate(), 50, 1); + LibraryExpense.CreateExpenseItemization(ExpenseItemization[2], Expense, ExpenseCategory.Code, ExpenseSubCategory.Code, WorkDate(), 70, 1); + + // [WHEN] The VAT specification is updated. + Expense.UpdateVATSpecification(Expense."No."); + + // [THEN] One VAT specification contains the aggregated amount and calculated VAT. + ExpenseVATSpecification.SetRange("Expense No.", Expense."No."); + Assert.RecordCount(ExpenseVATSpecification, 1); + ExpenseVATSpecification.FindFirst(); + Assert.AreEqual(ExpenseCategory.Code, ExpenseVATSpecification."Expense Category", 'The itemization category must be copied.'); + Assert.AreEqual(ExpenseSubCategory.Code, ExpenseVATSpecification."Expense Subcategory", 'The itemization subcategory must be copied.'); + Assert.AreNearlyEqual(120, ExpenseVATSpecification.Amount, 0.01, 'Itemization amounts must be aggregated.'); + Assert.AreNearlyEqual(100, ExpenseVATSpecification."VAT Base Amount", 0.01, 'VAT base must be calculated from the aggregated amount.'); + Assert.AreNearlyEqual(20, ExpenseVATSpecification."VAT Amount", 0.01, 'VAT must be calculated from the aggregated amount.'); + end; + + local procedure VerifyPartialVATReclaimPosting(ReclaimPct: Decimal; ExpectedDeductibleBase: Decimal; ExpectedDeductibleVAT: Decimal; ExpectedNonDeductibleBase: Decimal; ExpectedNonDeductibleVAT: Decimal; ExpectedReclaimVATLCY: Decimal; ExpectedReclaimVATRCY: Decimal) + var + Expense: Record Expense; + ExpenseUser: Record "Expense User"; + ExpenseCategory: Record "Expense Category"; + ExpenseSubCategory: array[4] of Record "Expense Subcategory"; + ExpenseReportHeader: Record "Expense Report Header"; + ExpenseReportLine: Record "Expense Report Line"; + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + PostedExpenseReportHeader: Record "Posted Expense Report Header"; + PostedExpenseReportLineVATSpec: Record "Posted Exp. Rep. Line VAT Spec"; + VATEntry: Record "VAT Entry"; + VATPostingSetup: array[3] of Record "VAT Posting Setup"; + GLEntry: Record "G/L Entry"; + Vendor: Record Vendor; + CreateExpenseReport: Codeunit "Create Expense Report"; + ExpenseReportPost: Codeunit "Expense Report-Post"; + CurrencyCode: Code[10]; + begin + Initialize(); + + // [GIVEN] A 120 LCY expense with 20% VAT and a report reimbursed at 2 RCY per LCY. + CreateExpenseUserAndCategory(ExpenseUser, ExpenseCategory); + CreateSubcategoryWithVATRate(ExpenseSubCategory[1], ExpenseCategory.Code, 20, VATPostingSetup[1]); + VATPostingSetup[1].Validate("Allow Non-Deductible VAT", VATPostingSetup[1]."Allow Non-Deductible VAT"::Allow); + VATPostingSetup[1].Validate("Non-Ded. Purchase VAT Account", LibraryERM.CreateGLAccountNo()); + VATPostingSetup[1].Modify(true); + CreateExpenseWithHotelItemizations(Expense, ExpenseUser, ExpenseCategory, ExpenseSubCategory, VATPostingSetup, 120, 0, 0, 0); + CurrencyCode := LibraryERM.CreateCurrencyWithExchangeRate(WorkDate(), 2, 2); + LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", CurrencyCode, Expense."VAT Bus. Posting Group"); + AddExpensesToReport(CreateExpenseReport, ExpenseReportHeader, Expense."No."); + + // [GIVEN] The VAT specification is approved with partial reclaim. + LibraryPurchase.CreateVendor(Vendor); + ExpenseReportLine.SetRange("Document No.", ExpenseReportHeader."No."); + ExpenseReportLine.FindFirst(); + ExpenseReportLine.Validate("Vendor No.", Vendor."No."); + ExpenseReportLine.Modify(true); + ExpenseReportLineVATSpec.SetRange("Document No.", ExpenseReportHeader."No."); + ExpenseReportLineVATSpec.SetRange("Document Line No.", ExpenseReportLine."Line No."); + ExpenseReportLineVATSpec.FindFirst(); + ExpenseReportLineVATSpec.Validate("Reclaim %", ReclaimPct); + ExpenseReportLineVATSpec.Validate("Reclaim Status", ExpenseReportLineVATSpec."Reclaim Status"::Approved); + ExpenseReportLineVATSpec.Modify(true); + + // [WHEN] The expense report is posted. + ExpenseReportHeader.PerformManualRelease(); + PostExpenseReportWithConfirmation(ExpenseReportPost, ExpenseReportHeader); + + // [THEN] The posted specification retains the partial reclaim amounts in LCY and reimbursement currency. + FindPostedExpenseReport(PostedExpenseReportHeader, Expense); + PostedExpenseReportLineVATSpec.SetRange("Expense Report No.", PostedExpenseReportHeader."No."); + PostedExpenseReportLineVATSpec.SetFilter("Expense Report Line No.", '<>%1', 0); + PostedExpenseReportLineVATSpec.FindFirst(); + Assert.AreNearlyEqual(ReclaimPct, PostedExpenseReportLineVATSpec."Reclaim %", 0.01, 'The posted VAT specification must retain the partial reclaim percentage.'); + Assert.AreNearlyEqual(ExpectedReclaimVATLCY, PostedExpenseReportLineVATSpec."Reclaim VAT Amount (LCY)", 0.01, 'The posted reclaim VAT amount in LCY must match the partial reclaim percentage.'); + Assert.AreNearlyEqual(ExpectedReclaimVATRCY, PostedExpenseReportLineVATSpec."Reclaim VAT Amount (RCY)", 0.01, 'The posted reclaim VAT amount in reimbursement currency must match the partial reclaim percentage.'); + + // [THEN] The VAT entry contains distinct deductible and non-deductible portions. + VATEntry.SetRange("Document No.", PostedExpenseReportHeader."No."); + VATEntry.SetRange("VAT Bus. Posting Group", VATPostingSetup[1]."VAT Bus. Posting Group"); + VATEntry.SetRange("VAT Prod. Posting Group", VATPostingSetup[1]."VAT Prod. Posting Group"); + VATEntry.CalcSums(Base, Amount, "Non-Deductible VAT Base", "Non-Deductible VAT Amount"); + Assert.AreNearlyEqual(ExpectedDeductibleBase, VATEntry.Base, 0.01, 'The VAT entry must contain the deductible VAT base.'); + Assert.AreNearlyEqual(ExpectedDeductibleVAT, VATEntry.Amount, 0.01, 'The VAT entry must contain the deductible VAT amount.'); + Assert.AreNearlyEqual(ExpectedNonDeductibleBase, VATEntry."Non-Deductible VAT Base", 0.01, 'The VAT entry must contain the non-deductible VAT base.'); + Assert.AreNearlyEqual(ExpectedNonDeductibleVAT, VATEntry."Non-Deductible VAT Amount", 0.01, 'The VAT entry must contain the non-deductible VAT amount.'); + + // [THEN] The non-deductible VAT amount is routed to its configured purchase VAT account. + GLEntry.SetRange("Document No.", PostedExpenseReportHeader."No."); + GLEntry.SetRange("G/L Account No.", VATPostingSetup[1]."Non-Ded. Purchase VAT Account"); + GLEntry.CalcSums(Amount); + Assert.AreNearlyEqual(ExpectedNonDeductibleVAT, GLEntry.Amount, 0.01, 'The non-deductible purchase VAT account must receive the non-reclaimable VAT amount.'); + end; + local procedure Initialize() var ExpenseAgentSetup: Record "Expense Agent Setup"; @@ -409,6 +835,8 @@ codeunit 148330 "Expense Posting VAT Test" VATSetup: Record "VAT Setup"; LibraryERMCountryData: Codeunit "Library - ERM Country Data"; begin + Clear(ExpectedExpenseNo); + Clear(ExpectedExpenseReportNo); LibraryTestInitialize.OnTestInitialize(Codeunit::"Expense Posting VAT Test"); LibraryExpense.CleanUpBeforeTesting(); LibraryExpense.CleanTransactionalData(); @@ -549,10 +977,10 @@ codeunit 148330 "Expense Posting VAT Test" ExpenseReportPost: Codeunit "Expense Report-Post"; begin LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", '', Expense."VAT Bus. Posting Group"); - CreateExpenseReport.AddExpensesToReport(ExpenseReportHeader); + AddExpensesToReport(CreateExpenseReport, ExpenseReportHeader, Expense."No."); UpdateExpenseReportLinesWithVendor(ExpenseReportHeader); ExpenseReportHeader.PerformManualRelease(); - ExpenseReportPost.PostExpenseReport(ExpenseReportHeader); + PostExpenseReportWithConfirmation(ExpenseReportPost, ExpenseReportHeader); end; local procedure FindPostedExpenseReport(var PostedExpenseReportHeader: Record "Posted Expense Report Header"; Expense: Record Expense) @@ -561,6 +989,20 @@ codeunit 148330 "Expense Posting VAT Test" PostedExpenseReportHeader.FindFirst(); end; + local procedure AddExpensesToReport(var CreateExpenseReport: Codeunit "Create Expense Report"; ExpenseReportHeader: Record "Expense Report Header"; ExpenseNo: Code[20]) + begin + ExpectedExpenseNo := ExpenseNo; + CreateExpenseReport.AddExpensesToReport(ExpenseReportHeader); + Assert.AreEqual('', ExpectedExpenseNo, 'The expected Expenses modal page was not handled.'); + end; + + local procedure PostExpenseReportWithConfirmation(var ExpenseReportPost: Codeunit "Expense Report-Post"; var ExpenseReportHeader: Record "Expense Report Header") + begin + ExpectedExpenseReportNo := ExpenseReportHeader."No."; + ExpenseReportPost.PostExpenseReport(ExpenseReportHeader); + Assert.AreEqual('', ExpectedExpenseReportNo, 'The expected expense report posting confirmation was not handled.'); + end; + local procedure GetRefundableDebitAccount(ExpenseCategoryCode: Code[20]): Code[20] var ExpenseCategory: Record "Expense Category"; @@ -650,6 +1092,7 @@ codeunit 148330 "Expense Posting VAT Test" RecordRef: RecordRef; begin ExpenseVATSpecification.Init(); + ExpenseVATSpecification.Source := ExpenseVATSpecification.Source::Manual; ExpenseVATSpecification.Validate("Expense No.", Expense."No."); RecordRef.GetTable(ExpenseVATSpecification); ExpenseVATSpecification.Validate("Line No.", LibraryUtility.GetNewLineNo(RecordRef, ExpenseVATSpecification.FieldNo("Line No."))); @@ -747,7 +1190,7 @@ codeunit 148330 "Expense Posting VAT Test" // [GIVEN] Expense report is created with expenses added LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", '', Expense."VAT Bus. Posting Group"); - CreateExpenseReport.AddExpensesToReport(ExpenseReportHeader); + AddExpensesToReport(CreateExpenseReport, ExpenseReportHeader, Expense."No."); UpdateExpenseReportLinesWithVendor(ExpenseReportHeader); ExpenseReportHeader.PerformManualRelease(); @@ -835,12 +1278,18 @@ codeunit 148330 "Expense Posting VAT Test" [ModalPageHandler] procedure ExpensesModalPageHandler(var Expenses: TestPage Expenses) begin + Assert.AreNotEqual('', ExpectedExpenseNo, 'An unexpected Expenses modal page was shown.'); + Expenses."No.".AssertEquals(ExpectedExpenseNo); + Clear(ExpectedExpenseNo); Expenses.OK().Invoke(); end; [ConfirmHandler] procedure ConfirmHandler(Question: Text[1024]; var Reply: Boolean) begin + Assert.AreNotEqual('', ExpectedExpenseReportNo, 'An unexpected confirmation dialog was shown.'); + Assert.AreEqual(StrSubstNo(PostExpenseReportQst, ExpectedExpenseReportNo), Question, 'The expense report posting confirmation is incorrect.'); + Clear(ExpectedExpenseReportNo); Reply := true; end; } diff --git a/src/Apps/W1/ExpenseAgent/test/src/ExpenseReportTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/ExpenseReportTest.Codeunit.al index 4d3439a7c03..acc9a32eae1 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/ExpenseReportTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/ExpenseReportTest.Codeunit.al @@ -3251,11 +3251,49 @@ codeunit 148306 "Expense Report Test" FindExpenseReportLine(ExpenseReportLine, Expense); ExpenseSubcategory.Get(Expense."Expense Category", Expense."Expense Subcategory"); Assert.AreEqual( - Format(Amount) + ' - ' + ExpenseSubcategory."Posting Description", + Format(Amount) + ' / ' + ExpenseSubcategory."Posting Description", ExpenseReportLine.Description, StrSubstNo(ValueMustBeEqualErr, ExpenseReportLine.FieldCaption("Description"), Format(Amount), ExpenseReportLine.TableCaption())); end; + [Test] + procedure PostingDescriptionUsesAvailableSubcategoryDescription() + var + ExpenseCategory: Record "Expense Category"; + ExpenseSubcategory: array[2] of Record "Expense Subcategory"; + ExpenseReportLine: Record "Expense Report Line"; + BaseDescription: Text[100]; + begin + // [SCENARIO] The posting description uses the selected subcategory description when it is available. + Initialize(); + + // [GIVEN] A report line whose description already includes its original subcategory description. + LibraryExpense.CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); + LibraryExpense.CreateExpenseSubCategory(ExpenseSubcategory[1], ExpenseCategory.Code, true); + LibraryExpense.CreateExpenseSubCategory(ExpenseSubcategory[2], ExpenseCategory.Code, true); + BaseDescription := ExpenseCategory."Posting Description"; + ExpenseReportLine."Expense Category" := ExpenseCategory.Code; + ExpenseReportLine."Expense Subcategory Code" := ExpenseSubcategory[1].Code; + ExpenseReportLine.Description := CopyStr(BaseDescription + ' / ' + ExpenseSubcategory[1]."Posting Description", 1, MaxStrLen(ExpenseReportLine.Description)); + + // [WHEN] A different subcategory is used for posting. + // [THEN] Its posting description replaces the original subcategory suffix. + Assert.AreEqual( + BaseDescription + ' / ' + ExpenseSubcategory[2]."Posting Description", + ExpenseReportLine.UpdatePostingDescription(ExpenseCategory.Code, ExpenseSubcategory[2].Code), + 'The posting description must use the selected subcategory description.'); + + // [WHEN] The selected subcategory has no posting description. + ExpenseSubcategory[2]."Posting Description" := ''; + ExpenseSubcategory[2].Modify(); + + // [THEN] The base description is retained without a separator. + Assert.AreEqual( + BaseDescription, + ExpenseReportLine.UpdatePostingDescription(ExpenseCategory.Code, ExpenseSubcategory[2].Code), + 'The base posting description must be retained when the subcategory posting description is unavailable.'); + end; + [Test] procedure DescriptionMustBeFlowFromExpenseCategoryInExpenseReport() var @@ -4485,7 +4523,7 @@ codeunit 148306 "Expense Report Test" FindExpenseReportLine(ExpenseReportLine, CopyStr(ExpenseReportPage."No.".Value, 1, 20)); // [GIVEN] Update "Merchant Name" in Expense Report Line. - ExpenseReportLine.Validate("Merchant Name", LibraryRandom.RandText(20)); + ExpenseReportLine.Validate("Merchant Name", CopyStr(LibraryRandom.RandText(20), 1, 100)); ExpenseReportLine.Modify(); // [GIVEN] Enqueue Expense Sub Category Code, Quantity and Amount for Expense Itemization. @@ -4702,7 +4740,7 @@ codeunit 148306 "Expense Report Test" FindExpenseReportLine(ExpenseReportLine, CopyStr(ExpenseReportPage."No.".Value, 1, 20)); // [GIVEN] Update "Merchant Name" in Expense Report Line. - ExpenseReportLine.Validate("Merchant Name", LibraryRandom.RandText(20)); + ExpenseReportLine.Validate("Merchant Name", CopyStr(LibraryRandom.RandText(20), 1, 100)); ExpenseReportLine.Modify(); // [GIVEN] Create Expense Report Line Itemization with Refundable and Non-Refundable Expense Sub Category. @@ -5127,7 +5165,7 @@ codeunit 148306 "Expense Report Test" ReleaseExpenseDocument: Codeunit "Release Expense Document"; Amount: Decimal; CurrencyCode: Code[10]; - NewStartingPoint: Text; + NewStartingPoint: Text[50]; begin // [SCENARIO 617013] Verify Starting Point can be updated in Expense Report Line When Expense Report is created from Expense and Starting Point is blank in Expense. Initialize(); @@ -5154,7 +5192,7 @@ codeunit 148306 "Expense Report Test" FindExpenseReportLine(ExpenseReportLine, Expense); // [GIVEN] Generate New Starting Point. - NewStartingPoint := LibraryRandom.RandText(50); + NewStartingPoint := CopyStr(LibraryRandom.RandText(50), 1, 50); // [WHEN] Update "Starting Point" in Expense Report Line. ExpenseReportLine.Validate("Starting Point", NewStartingPoint); @@ -5177,7 +5215,7 @@ codeunit 148306 "Expense Report Test" ReleaseExpenseDocument: Codeunit "Release Expense Document"; Amount: Decimal; CurrencyCode: Code[10]; - NewEndingPoint: Text; + NewEndingPoint: Text[50]; begin // [SCENARIO 617013] Verify Ending Point can be updated in Expense Report Line When Expense Report is created from Expense and Ending Point is blank in Expense. Initialize(); @@ -5204,7 +5242,7 @@ codeunit 148306 "Expense Report Test" FindExpenseReportLine(ExpenseReportLine, Expense); // [GIVEN] Generate New Ending Point. - NewEndingPoint := LibraryRandom.RandText(50); + NewEndingPoint := CopyStr(LibraryRandom.RandText(50), 1, 50); // [WHEN] Update "Ending Point" in Expense Report Line. ExpenseReportLine.Validate("Ending Point", NewEndingPoint); @@ -6169,8 +6207,8 @@ codeunit 148306 "Expense Report Test" ReleaseExpenseDocument: Codeunit "Release Expense Document"; Amount: Decimal; CurrencyCode: Code[10]; - MerchantName: Text; - ExpenseExtDocNo: Text; + MerchantName: Text[100]; + ExpenseExtDocNo: Text[30]; OriginalMileage: Decimal; begin // [SCENARIO 616955] Verify that Merchant Name and Ext. Doc. No. must flow from Expense to Expense Report. @@ -6185,8 +6223,8 @@ codeunit 148306 "Expense Report Test" // [GIVEN] Generate Random Amount. Amount := LibraryRandom.RandInt(100); OriginalMileage := LibraryRandom.RandDecInRange(50, 500, 2); - MerchantName := LibraryRandom.RandText(20); - ExpenseExtDocNo := LibraryRandom.RandText(20); + MerchantName := CopyStr(LibraryRandom.RandText(20), 1, 100); + ExpenseExtDocNo := CopyStr(LibraryRandom.RandText(20), 1, 30); // [GIVEN] Create Expense with Mileage. CreateExpense(Expense, true, CurrencyCode, Amount); @@ -6337,9 +6375,9 @@ codeunit 148306 "Expense Report Test" CurrencyCode: Code[10]; JobNo: Code[20]; Amount: array[3] of Decimal; - LineDescription: Text; - AdditionalInformation: Text; - LineJustification: Text; + LineDescription: Text[100]; + AdditionalInformation: Text[100]; + LineJustification: Text[100]; i: Integer; begin // [SCENARIO 580731] Verify the data of "Expense Report Details". @@ -6741,10 +6779,10 @@ codeunit 148306 "Expense Report Test" ExpenseUserNo: Code[20]; CurrencyCode: Code[10]; Amount: Decimal; - MerchantName: Text; - LineDescription: Text; - AdditionalInformation: Text; - LineJustification: Text; + MerchantName: Text[100]; + LineDescription: Text[100]; + AdditionalInformation: Text[100]; + LineJustification: Text[100]; IsBillable: Boolean; var JobNo: Code[20]) var @@ -6863,19 +6901,19 @@ codeunit 148306 "Expense Report Test" ExpenseUserNo: Code[20]; CurrencyCode: Code[10]; var Amount: array[3] of Decimal; - LineDescription: Text; - AdditionalInformation: Text; - LineJustification: Text; + LineDescription: Text[100]; + AdditionalInformation: Text[100]; + LineJustification: Text[100]; Refundable: Boolean; var JobNo: Code[20]; NumberOfLines: Integer) var i: Integer; - MerchantName: Text; + MerchantName: Text[100]; begin for i := 1 to NumberOfLines do begin Amount[i] := LibraryRandom.RandIntInRange(100, 200); - MerchantName := LibraryRandom.RandText(20); + MerchantName := CopyStr(LibraryRandom.RandText(20), 1, 100); CreateExpenseReportLine(ExpenseReportLine[i], ExpenseReportHeader, ExpenseUserNo, CurrencyCode, Amount[i], MerchantName, LineDescription, AdditionalInformation, LineJustification, Refundable, JobNo); end; end; diff --git a/src/Apps/W1/ExpenseAgent/test/src/ExpenseRuleTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/ExpenseRuleTest.Codeunit.al index 0bcb7cb1d67..23af114f889 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/ExpenseRuleTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/ExpenseRuleTest.Codeunit.al @@ -3073,8 +3073,8 @@ codeunit 148301 "Expense Rule Test" ExpenseReportPage.Close(); // [WHEN] Update "Expense Ext. Doc. No.", "Merchant Name" in Expense Report Line. - ExpenseReportLine.Validate("Merchant Name", LibraryRandom.RandText(30)); - ExpenseReportLine.Validate("Expense Ext. Doc. No.", LibraryRandom.RandText(30)); + ExpenseReportLine.Validate("Merchant Name", CopyStr(LibraryRandom.RandText(30), 1, 100)); + ExpenseReportLine.Validate("Expense Ext. Doc. No.", CopyStr(LibraryRandom.RandText(30), 1, 30)); ExpenseReportLine.Modify(); // [THEN] Verify that expense Rule Violation is true before adding Itemization. @@ -3801,13 +3801,13 @@ codeunit 148301 "Expense Rule Test" Commit(); // [WHEN] Update Participant Name. - asserterror ExpenseReportLineParticipant.validate("Participant Name", LibraryRandom.RandText(10)); + asserterror ExpenseReportLineParticipant.validate("Participant Name", CopyStr(LibraryRandom.RandText(10), 1, 100)); // [THEN] Verify that Expense "Participant Employee No." is required when some fields are updating. Assert.ExpectedTestFieldError(ExpenseReportLineParticipant.FieldCaption("Participant Employee No."), ''); // [WHEN] Update Participant Organization. - asserterror ExpenseReportLineParticipant.validate("Participant Organization", LibraryRandom.RandText(10)); + asserterror ExpenseReportLineParticipant.validate("Participant Organization", CopyStr(LibraryRandom.RandText(10), 1, 100)); // [THEN] Verify that Expense "Participant Employee No." is required when some fields are updating. Assert.ExpectedTestFieldError(ExpenseReportLineParticipant.FieldCaption("Participant Employee No."), ''); @@ -3819,13 +3819,13 @@ codeunit 148301 "Expense Rule Test" Assert.ExpectedTestFieldError(ExpenseReportLineParticipant.FieldCaption("Participant Employee No."), ''); // [WHEN] Update Participant Title. - asserterror ExpenseReportLineParticipant.validate("Participant Title", LibraryRandom.RandText(10)); + asserterror ExpenseReportLineParticipant.validate("Participant Title", CopyStr(LibraryRandom.RandText(10), 1, 30)); // [THEN] Verify that Expense "Participant Employee No." is required when some fields are updating. Assert.ExpectedTestFieldError(ExpenseReportLineParticipant.FieldCaption("Participant Employee No."), ''); // [WHEN] Update Participant Email. - asserterror ExpenseReportLineParticipant.validate("Participant Email", LibraryRandom.RandText(10)); + asserterror ExpenseReportLineParticipant.validate("Participant Email", CopyStr(LibraryRandom.RandText(10), 1, 80)); // [THEN] Verify that Expense "Participant Employee No." is required when some fields are updating. Assert.ExpectedTestFieldError(ExpenseReportLineParticipant.FieldCaption("Participant Employee No."), ''); @@ -6991,8 +6991,8 @@ codeunit 148301 "Expense Rule Test" ExpenseReportPage.Close(); // [WHEN] Update "Expense Ext. Doc. No.", "Merchant Name" in Expense Report Line. - ExpenseReportLine.Validate("Merchant Name", LibraryRandom.RandText(30)); - ExpenseReportLine.Validate("Expense Ext. Doc. No.", LibraryRandom.RandText(30)); + ExpenseReportLine.Validate("Merchant Name", CopyStr(LibraryRandom.RandText(30), 1, 100)); + ExpenseReportLine.Validate("Expense Ext. Doc. No.", CopyStr(LibraryRandom.RandText(30), 1, 30)); ExpenseReportLine.Modify(); // [THEN] Verify that expense Rule Violation is true before adding Participant. diff --git a/src/Apps/W1/ExpenseAgent/test/src/ExpenseTestII.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/ExpenseTestII.Codeunit.al index 790a75e0a96..722114c2da5 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/ExpenseTestII.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/ExpenseTestII.Codeunit.al @@ -1067,7 +1067,7 @@ codeunit 148309 "Expense Test II" // [GIVEN] Create Expense with Expense Category. LibraryExpense.CreateExpense(Expense, ExpenseUser."No.", ExpenseCategory.Code, '', '', true, '', Amount); - Expense.Validate("Merchant Name", LibraryRandom.RandText(10)); + Expense.Validate("Merchant Name", CopyStr(LibraryRandom.RandText(10), 1, 100)); Expense.Modify(); // [WHEN] Update Amount Reduction in Expense to negative value. @@ -1730,14 +1730,14 @@ codeunit 148309 "Expense Test II" // [GIVEN] Create Expense User. LibraryExpense.CreateExpenseUser(ExpenseUser[1]); - ExpenseUser[1].Validate("E-mail", EmailId); + ExpenseUser[1].Validate("E-mail", CopyStr(EmailId, 1, 80)); ExpenseUser[1].Modify(); // [GIVEN] Create another Expense User. LibraryExpense.CreateExpenseUser(ExpenseUser[2]); // [WHEN] Update the same Email ID in another Expense User. - asserterror ExpenseUser[2].Validate("E-mail", EmailId); + asserterror ExpenseUser[2].Validate("E-mail", CopyStr(EmailId, 1, 80)); // [THEN] Verify that the error is thrown for duplicate Email ID in Expense User. Assert.ExpectedError(StrSubstNo(DuplicateEmailErr, ExpenseUser[2].FieldCaption("E-mail"), EmailId, ExpenseUser[2].TableCaption())); @@ -1792,7 +1792,7 @@ codeunit 148309 "Expense Test II" StrSubstNo(ValueMustBeEqualErr, ExpenseUser.FieldCaption("E-mail"), Employee."Company E-Mail", ExpenseUser.TableCaption())); // [WHEN] Update Job Title in Employee. - Employee.Validate("Job Title", LibraryRandom.RandText(20)); + Employee.Validate("Job Title", CopyStr(LibraryRandom.RandText(20), 1, 30)); // [THEN] Verify that the Job Title is updated in Expense User. ExpenseUser.Get(ExpenseUser."No."); @@ -1802,7 +1802,7 @@ codeunit 148309 "Expense Test II" StrSubstNo(ValueMustBeEqualErr, ExpenseUser.FieldCaption("Job Title"), Employee."Job Title", ExpenseUser.TableCaption())); // [WHEN] Update First Name in Employee. - Employee.Validate("First Name", LibraryRandom.RandText(20)); + Employee.Validate("First Name", CopyStr(LibraryRandom.RandText(20), 1, 30)); // [THEN] Verify that the Name is updated in Expense User. ExpenseUser.Get(ExpenseUser."No.");