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..3671ca6406c 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/APIs/ExpenseVATSpecAPI.Page.al +++ b/src/Apps/W1/ExpenseAgent/app/src/APIs/ExpenseVATSpecAPI.Page.al @@ -18,6 +18,8 @@ page 7085 "Expense VAT Spec. API" ODataKeyFields = SystemId; SourceTable = "Expense VAT Specification"; AboutText = 'Provides access to data from the Expense VAT Specification table'; + ModifyAllowed = false; + DeleteAllowed = false; AutoSplitKey = true; layout @@ -90,6 +92,7 @@ page 7085 "Expense VAT Spec. API" field(source; Rec.Source) { Caption = 'Source'; + Editable = false; } field(confidence; Rec.Confidence) { @@ -105,4 +108,16 @@ page 7085 "Expense VAT Spec. API" begin ExpenseAgentAPIValidation.VerifyAgentAccess(); end; + + trigger OnInsertRecord(BelowxRec: Boolean): Boolean + begin + InsertVATSpecification(Rec); + exit(false); + end; + + internal procedure InsertVATSpecification(var ExpenseVATSpecification: Record "Expense VAT Specification") + begin + ExpenseVATSpecification.Source := ExpenseVATSpecification.Source::Agent; + ExpenseVATSpecification.Insert(true); + 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..3bda2bdcd29 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,5 @@ page 7084 "Expense VAT Specification" } } } + } 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 e590ee77992..dc1a84e4730 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 @@ -800,6 +800,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 @@ -1321,6 +1322,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; @@ -1330,33 +1332,61 @@ 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"); + if "Expense Subcategory" <> '' then + TempExpenseVATSpec.Validate("Expense Subcategory", "Expense Subcategory"); + 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..122061e957a 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,18 @@ 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); + Expense."Currency Factor" := Amount / "Amount (LCY)"; + end; + Validate("VAT %"); end; - - Validate("VAT %"); end; } field(27; "Expense Category"; Code[20]) @@ -174,14 +180,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 +204,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 +225,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 +262,26 @@ table 6918 "Expense VAT Specification" end; end; + trigger OnModify() + begin + TestStatusOpenOfExpense(); + if Source = Source::Agent then + error(ModifyOrDeleteErr); + end; + + trigger OnDelete() + begin + TestStatusOpenOfExpense(); + 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 @@ -356,4 +384,13 @@ table 6918 "Expense VAT Specification" Rec.Reasoning.CreateInStream(InStream, TextEncoding::UTF8); InStream.ReadText(Result); end; + + local procedure TestStatusOpenOfExpense() + var + ExpenseRecord: Record Expense; + begin + ExpenseRecord.SetLoadFields(Status); + ExpenseRecord.Get("Expense No."); + ExpenseRecord.TestStatusOpen(); + end; } 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 36be0a09673..4f3f141e9c6 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 @@ -328,6 +328,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)"; @@ -341,6 +343,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 f1647d29411..cd9c083fde6 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 @@ -17,6 +17,7 @@ using Microsoft.Projects.Project.Journal; using Microsoft.Projects.Project.Posting; using Microsoft.Purchases.Vendor; using Microsoft.Sales.Document; +using System.Telemetry; using System.Utilities; codeunit 6987 "Expense Report-Post" @@ -62,7 +63,9 @@ codeunit 6987 "Expense Report-Post" VATSetup: Record "VAT Setup"; SourceCodeSetup: Record "Source Code Setup"; GenJnlPostLine: Codeunit "Gen. Jnl.-Post Line"; + FeatureTelemetry: Codeunit "Feature Telemetry"; PreviewMode: Boolean; + DisableNonDeductibleVATAfterPost: Boolean; AmountToEmployee: Decimal; AmountToEmployeeLCY: Decimal; CanPostExpenseReportQst: Label 'Do you want to post Expense Report %1?', Comment = '%1 = Expense Report No.'; @@ -73,12 +76,19 @@ 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'; + PendingVATSpecTitleErr: Label 'VAT reclaim status is pending'; + PendingVATSpecDetailedErr: Label 'Open the VAT specification line and approve or reject the reclaim, and then retry posting the expense report.'; + RoundingDifferenceTooLargeErr: Label 'The difference between expense report line %1 and its posted amounts exceeds the currency rounding precision. The difference is %2 in reimbursement currency and %3 in local currency.', Comment = '%1 = Expense report line number, %2 = Difference in reimbursement currency, %3 = Difference in local currency'; + AgentVATSpecificationsPostedLbl: Label 'Agent-authored VAT specifications posted.', Locked = true; + ShowItLbl: Label 'Show it'; internal procedure RunWithCheck(var ExpenseReportHeader: Record "Expense Report Header") var GenJnlPostPreview: Codeunit "Gen. Jnl.-Post Preview"; ReleaseExpenseReport: Codeunit "Release Exp. Report Document"; + AgentVATSpecificationCount: Integer; begin SourceCodeSetup.Get(); SourceCodeSetup.TestField(Expense); @@ -95,18 +105,46 @@ codeunit 6987 "Expense Report-Post" ReleaseExpenseReport.Run(ExpenseReportHeader); ExpenseReportHeader.Get(ExpenseReportHeader."No."); // Refresh after release + AgentVATSpecificationCount := GetAgentVATSpecificationCount(ExpenseReportHeader."No."); CheckAndCreatePostedDocument(ExpenseReportHeader); if not PreviewMode then begin if TrySendReimbursementNotification(PostedExpenseReportHeader) then; ExpenseReportHeader.Delete(true); + if DisableNonDeductibleVATAfterPost then begin + VATSetup.Get(); + VATSetup."Enable Non-Deductible VAT" := false; + VATSetup.Modify(); + DisableNonDeductibleVATAfterPost := false; + end; + LogAgentVATSpecificationUsage(AgentVATSpecificationCount); end; if PreviewMode then GenJnlPostPreview.ThrowError(); end; + local procedure GetAgentVATSpecificationCount(ExpenseReportNo: Code[20]): Integer + var + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + begin + ExpenseReportLineVATSpec.SetRange("Document No.", ExpenseReportNo); + ExpenseReportLineVATSpec.SetRange(Source, ExpenseReportLineVATSpec.Source::Agent); + exit(ExpenseReportLineVATSpec.Count()); + end; + + local procedure LogAgentVATSpecificationUsage(AgentVATSpecificationCount: Integer) + var + TelemetryDimensions: Dictionary of [Text, Text]; + begin + if AgentVATSpecificationCount = 0 then + exit; + + TelemetryDimensions.Add('VATSpecificationCount', Format(AgentVATSpecificationCount)); + FeatureTelemetry.LogUsage('0000UZ8', ExpenseAgentSetup.GetFeatureName(), AgentVATSpecificationsPostedLbl, TelemetryDimensions); + end; + internal procedure PostExpenseReport(var ExpenseReportHeader: Record "Expense Report Header") var ExpenseReportHeaderCopy: Record "Expense Report Header"; @@ -249,11 +287,48 @@ codeunit 6987 "Expense Report-Post" ExpenseReportLineVATSpec.SetRange("Document Line No.", ExpenseReportLine."Line No."); if ExpenseReportLineVATSpec.FindSet() then repeat + ValidateVATSpecMasterRecords(ExpenseReportLineVATSpec); 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 ValidateVATSpecMasterRecords(ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec.") + var + ExpenseCategory: Record "Expense Category"; + ExpenseSubcategory: Record "Expense Subcategory"; + begin + if ExpenseReportLineVATSpec."Expense Category" = '' then + exit; + + ExpenseCategory.Get(ExpenseReportLineVATSpec."Expense Category"); + ExpenseCategory.TestField(Inactive, false); + + if ExpenseReportLineVATSpec."Expense Subcategory" <> '' then begin + ExpenseSubcategory.Get(ExpenseReportLineVATSpec."Expense Category", ExpenseReportLineVATSpec."Expense Subcategory"); + ExpenseSubcategory.TestField(Inactive, false); + end; + 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.Title := PendingVATSpecTitleErr; + PendingVATSpecErrorInfo.DetailedMessage := PendingVATSpecDetailedErr; + PendingVATSpecErrorInfo.DataClassification := DataClassification::CustomerContent; + PendingVATSpecErrorInfo.ErrorType := ErrorType::Client; + 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"; @@ -455,7 +530,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); @@ -498,16 +573,19 @@ 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) var GenJournalLine: Record "Gen. Journal Line"; - JobLedgEntryPosted: Boolean; + ProjectGenJournalLine: Record "Gen. Journal Line"; + ProjectAmountLCY: Decimal; + ProjectJournalLineInitialized: Boolean; GlobalEntryCopied: Boolean; begin // Each spec row produces its own Gen. Journal Line and VAT Entry. @@ -516,12 +594,15 @@ codeunit 6987 "Expense Report-Post" Clear(GenJournalLine); CreateGenJournalLineFromSpec(GenJournalLine, ExpenseReportHeader, ExpenseReportLine, ExpenseReportLineVATSpec, PostedExpReportLine); SetupRefundableAccountForSpec(GenJournalLine, ExpenseReportLine, ExpenseReportLineVATSpec); + SetupNonDeductibleVATForSpec(GenJournalLine, ExpenseReportLineVATSpec); SetupSourceCodeAndDimensions(GenJournalLine, ExpenseReportLine."Dimension Set ID"); - if (not JobLedgEntryPosted) and (ExpenseReportLine."Job No." <> '') then begin - PostedExpReportLine."Job Ledger Entry No." := PostProjectJnlLine(PostedExpReportLine, PostedExpenseReportHeader, GenJournalLine); - PostedExpReportLine.Modify(); - JobLedgEntryPosted := true; + if ExpenseReportLine."Job No." <> '' then begin + if not ProjectJournalLineInitialized then begin + ProjectGenJournalLine := GenJournalLine; + ProjectJournalLineInitialized := true; + end; + ProjectAmountLCY += GenJournalLine."Amount (LCY)"; end; if not GlobalEntryCopied then begin @@ -534,23 +615,30 @@ 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; + if ProjectJournalLineInitialized then begin + ProjectGenJournalLine."Amount (LCY)" := ProjectAmountLCY; + PostedExpReportLine."Job Ledger Entry No." := PostProjectJnlLine(PostedExpReportLine, PostedExpenseReportHeader, ProjectGenJournalLine); + PostedExpReportLine.Modify(); + end; + // Employee paid accumulator: the employee paid the gross reimbursable amount for the whole line. if ExpenseReportLine."Reimbursement Type" = ExpenseReportLine."Reimbursement Type"::"Employee Paid" then begin AmountToEmployee += ExpenseReportLine."Reimbursable Amount"; 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 @@ -560,15 +648,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."); @@ -587,16 +677,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. @@ -613,11 +707,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.") @@ -636,11 +736,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"; @@ -727,19 +822,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(); @@ -767,7 +849,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. @@ -778,36 +859,38 @@ 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 - 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; @@ -831,6 +914,27 @@ codeunit 6987 "Expense Report-Post" GenJournalLine."System-Created Entry" := true; end; + local procedure SetupNonDeductibleVATForSpec(var GenJournalLine: Record "Gen. Journal Line"; ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec.") + begin + if ExpenseReportLineVATSpec."Reclaim Status" <> ExpenseReportLineVATSpec."Reclaim Status"::Approved then + exit; + if (ExpenseReportLineVATSpec."Reclaim %" = 100) or (ExpenseReportLineVATSpec."VAT Amount (LCY)" = 0) then + exit; + + VATSetup.Get(); + if not VATSetup."Enable Non-Deductible VAT" then begin + VATSetup."Enable Non-Deductible VAT" := true; + VATSetup.Modify(); + DisableNonDeductibleVATAfterPost := true; + end; + GenJournalLine.Validate("Non-Deductible VAT %", 100 - ExpenseReportLineVATSpec."Reclaim %"); + GenJournalLine.Validate("Non-Deductible VAT Base", ExpenseReportLineVATSpec."VAT Base Amount (RCY)" * (100 - ExpenseReportLineVATSpec."Reclaim %") / 100); + GenJournalLine.Validate("Non-Deductible VAT Amount", ExpenseReportLineVATSpec."VAT Amount (RCY)" - ExpenseReportLineVATSpec."Reclaim VAT Amount (RCY)"); + GenJournalLine.Validate("Non-Deductible VAT Base LCY", ExpenseReportLineVATSpec."VAT Base Amount (LCY)" * (100 - ExpenseReportLineVATSpec."Reclaim %") / 100); + GenJournalLine.Validate("Non-Deductible VAT Amount LCY", ExpenseReportLineVATSpec."VAT Amount (LCY)" - ExpenseReportLineVATSpec."Reclaim VAT Amount (LCY)"); + GenJournalLine."Source Curr. VAT Amount" := ExpenseReportLineVATSpec."Reclaim VAT Amount (RCY)"; + end; + local procedure SetupRefundableAccount(var GenJournalLine: Record "Gen. Journal Line"; ExpenseReportHeader: Record "Expense Report Header"; ExpenseReportLine: Record "Expense Report Line") var ExpenseCategory: Record "Expense Category"; @@ -1018,15 +1122,37 @@ 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 + LCYCurrency: Record Currency; + ReimbursementCurrency: Record Currency; ExpenseCategory: Record "Expense Category"; ExpensePostingGroup: Record "Expense Posting Group"; + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + GenJournalLine: Record "Gen. Journal Line"; AccNo: Code[20]; + AmountForAccountSelection: Decimal; + AmountRoundingPrecision: Decimal; begin + LCYCurrency.Initialize(''); + ReimbursementCurrency.Initialize(ExpenseReportHeader."Reimbursement Currency Code"); + 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; + AmountRoundingPrecision := LCYCurrency."Amount Rounding Precision"; + if AmountForAccountSelection = 0 then begin + AmountForAccountSelection := AmountToPost; + AmountRoundingPrecision := ReimbursementCurrency."Amount Rounding Precision"; + end; + ExpenseReportLineVATSpec.SetRange("Document No.", ExpenseReportLine."Document No."); + ExpenseReportLineVATSpec.SetRange("Document Line No.", ExpenseReportLine."Line No."); + if not ExpenseReportLineVATSpec.IsEmpty() then + if Abs(AmountForAccountSelection) > AmountRoundingPrecision then + Error(RoundingDifferenceTooLargeErr, ExpenseReportLine."Line No.", AmountToPost, AmountToPostLCY); + if AmountForAccountSelection > 0 then begin ExpensePostingGroup.TestField("Debit Rounding Account"); AccNo := ExpensePostingGroup."Debit Rounding Account"; end else begin @@ -1034,13 +1160,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; @@ -1296,16 +1434,19 @@ codeunit 6987 "Expense Report-Post" ExpenseReportLineVATSpec.DeleteAll(); end; + [CommitBehavior(CommitBehavior::Ignore)] [IntegrationEvent(false, false)] local procedure OnAfterProcessExpenseReportLine(ExpenseReportHeader: Record "Expense Report Header"; ExpenseReportLine: Record "Expense Report Line"; PostedExpenseReportLine: Record "Posted Expense Report Line"; PostedExpenseReportHeader: Record "Posted Expense Report Header") begin end; + [CommitBehavior(CommitBehavior::Ignore)] [IntegrationEvent(false, false)] local procedure OnBeforePostEmployeeEntry(var GenJournalLine: Record "Gen. Journal Line"; ExpenseReportHeader: Record "Expense Report Header"; PostedExpenseReportHeader: Record "Posted Expense Report Header") begin end; + [CommitBehavior(CommitBehavior::Ignore)] [IntegrationEvent(false, false)] local procedure OnAfterPostEmployeeEntry(GenJournalLine: Record "Gen. Journal Line"; ExpenseReportHeader: Record "Expense Report Header"; PostedExpenseReportHeader: Record "Posted Expense Report Header") begin 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..5169b9411e6 --- /dev/null +++ b/src/Apps/W1/ExpenseAgent/app/src/ExpenseReport/Codeunits/UpgradeExpReportVATSpec.Codeunit.al @@ -0,0 +1,263 @@ +// ------------------------------------------------------------------------------------------------ +// 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" = 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; + + var + BackfillCompletedTelemetryMsg: Label 'Expense VAT specification reimbursement amount backfill completed.', Locked = true; + + trigger OnUpgradePerCompany() + begin + BackfillReimbursementAmounts(); + end; + + local procedure BackfillExpenseReportLineVATSpecCurrencyMetadata() + var + ExpenseReportLine: Record "Expense Report Line"; + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + CachedDocumentNo: Code[20]; + CachedDocumentLineNo: Integer; + HasCachedDocumentLine: Boolean; + ParentLineFound: Boolean; + begin + ExpenseReportLine.SetLoadFields("Expense Currency Code", "Expense Currency Factor"); + ExpenseReportLineVATSpec.SetCurrentKey("Document No.", "Document Line No.", "Line No."); + ExpenseReportLineVATSpec.SetLoadFields("Document No.", "Document Line No.", "Currency Code", "Currency Factor"); + CachedDocumentNo := ''; + CachedDocumentLineNo := 0; + if ExpenseReportLineVATSpec.FindSet(true) then + repeat + if (not HasCachedDocumentLine) or + (CachedDocumentNo <> ExpenseReportLineVATSpec."Document No.") or + (CachedDocumentLineNo <> ExpenseReportLineVATSpec."Document Line No.") + then begin + CachedDocumentNo := ExpenseReportLineVATSpec."Document No."; + CachedDocumentLineNo := ExpenseReportLineVATSpec."Document Line No."; + HasCachedDocumentLine := true; + ParentLineFound := ExpenseReportLine.Get(CachedDocumentNo, CachedDocumentLineNo); + end; + + if ParentLineFound then + if (ExpenseReportLineVATSpec."Currency Code" <> ExpenseReportLine."Expense Currency Code") or + (ExpenseReportLineVATSpec."Currency Factor" <> ExpenseReportLine."Expense Currency Factor") + then begin + ExpenseReportLineVATSpec."Currency Code" := ExpenseReportLine."Expense Currency Code"; + ExpenseReportLineVATSpec."Currency Factor" := ExpenseReportLine."Expense Currency Factor"; + ExpenseReportLineVATSpec.Modify(false); + end; + until ExpenseReportLineVATSpec.Next() = 0; + end; + + local procedure BackfillReimbursementAmounts() + var + UpgradeTag: Codeunit "Upgrade Tag"; + begin + if UpgradeTag.HasUpgradeTag(GetBackfillReimbursementAmountsUpgradeTag()) then + exit; + + BackfillExpenseReportLineVATSpecCurrencyMetadata(); + CopyReimbursementAmountsFromLCY(); + BackfillExpenseReportLineVATSpecs(); + BackfillPostedExpenseReportLineVATSpecs(); + + LogBackfillCompleted(); + SetBackfillReimbursementAmountsUpgradeTag(); + end; + + local procedure LogBackfillCompleted() + var + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + PostedExpenseReportLineVATSpec: Record "Posted Exp. Rep. Line VAT Spec"; + ExpenseAuditSubscribers: Codeunit "Expense Audit Subscribers"; + TelemetryDimensions: Dictionary of [Text, Text]; + begin + TelemetryDimensions.Add('Category', ExpenseAuditSubscribers.TelemetryCategory()); + TelemetryDimensions.Add('ExpenseReportVATSpecificationCount', Format(ExpenseReportLineVATSpec.Count())); + TelemetryDimensions.Add('PostedExpenseReportVATSpecificationCount', Format(PostedExpenseReportLineVATSpec.Count())); + Session.LogMessage( + '0000V16', BackfillCompletedTelemetryMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, + TelemetryDimensions); + end; + + internal procedure SetBackfillReimbursementAmountsUpgradeTag() + var + UpgradeTag: Codeunit "Upgrade Tag"; + begin + if not UpgradeTag.HasUpgradeTag(GetBackfillReimbursementAmountsUpgradeTag()) then + UpgradeTag.SetUpgradeTag(GetBackfillReimbursementAmountsUpgradeTag()); + end; + + local procedure CopyReimbursementAmountsFromLCY() + var + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + PostedExpenseReportLineVATSpec: Record "Posted Exp. Rep. Line VAT Spec"; + ExpenseReportLineVATSpecDataTransfer: DataTransfer; + PostedExpenseReportLineVATSpecDataTransfer: DataTransfer; + begin + ExpenseReportLineVATSpecDataTransfer.SetTables(Database::"Expense Report Line VAT Spec.", Database::"Expense Report Line VAT Spec."); + ExpenseReportLineVATSpecDataTransfer.AddFieldValue(ExpenseReportLineVATSpec.FieldNo("VAT Base Amount (LCY)"), ExpenseReportLineVATSpec.FieldNo("VAT Base Amount (RCY)")); + ExpenseReportLineVATSpecDataTransfer.AddFieldValue(ExpenseReportLineVATSpec.FieldNo("VAT Amount (LCY)"), ExpenseReportLineVATSpec.FieldNo("VAT Amount (RCY)")); + ExpenseReportLineVATSpecDataTransfer.AddFieldValue(ExpenseReportLineVATSpec.FieldNo("Amount (LCY)"), ExpenseReportLineVATSpec.FieldNo("Amount (RCY)")); + ExpenseReportLineVATSpecDataTransfer.CopyFields(); + + PostedExpenseReportLineVATSpecDataTransfer.SetTables(Database::"Posted Exp. Rep. Line VAT Spec", Database::"Posted Exp. Rep. Line VAT Spec"); + PostedExpenseReportLineVATSpecDataTransfer.AddFieldValue(PostedExpenseReportLineVATSpec.FieldNo("VAT Base Amount (LCY)"), PostedExpenseReportLineVATSpec.FieldNo("VAT Base Amount (RCY)")); + PostedExpenseReportLineVATSpecDataTransfer.AddFieldValue(PostedExpenseReportLineVATSpec.FieldNo("VAT Amount (LCY)"), PostedExpenseReportLineVATSpec.FieldNo("VAT Amount (RCY)")); + PostedExpenseReportLineVATSpecDataTransfer.AddFieldValue(PostedExpenseReportLineVATSpec.FieldNo("Amount (LCY)"), PostedExpenseReportLineVATSpec.FieldNo("Amount (RCY)")); + PostedExpenseReportLineVATSpecDataTransfer.CopyFields(); + end; + + local procedure BackfillExpenseReportLineVATSpecs() + var + ExpenseReportHeader: Record "Expense Report Header"; + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + CachedDocumentNo: Code[20]; + HasCachedDocumentNo: Boolean; + HeaderFound: Boolean; + begin + ExpenseReportLineVATSpec.SetCurrentKey("Document No.", "Document Line No.", "Line No."); + ExpenseReportLineVATSpec.SetLoadFields( + "Document No.", "Currency Code", "VAT Base Amount (LCY)", "VAT Amount", "VAT Amount (LCY)", "Reclaim %", + "VAT Base Amount (RCY)", "VAT Amount (RCY)", "Amount (RCY)", "Reclaim VAT Amount", + "Reclaim VAT Amount (LCY)", "Reclaim VAT Amount (RCY)"); + if not ExpenseReportLineVATSpec.FindSet(true) then + exit; + + CachedDocumentNo := ''; + repeat + if (not HasCachedDocumentNo) or (CachedDocumentNo <> ExpenseReportLineVATSpec."Document No.") then begin + CachedDocumentNo := ExpenseReportLineVATSpec."Document No."; + HasCachedDocumentNo := true; + ExpenseReportHeader.SetLoadFields("Reimbursement Currency Code", "Posting Date", "Reimbursement Currency Factor"); + HeaderFound := ExpenseReportHeader.Get(CachedDocumentNo); + end; + + if HeaderFound then + if ExpenseReportHeader."Reimbursement Currency Code" <> '' then begin + ExpenseReportLineVATSpec.UpdateReimbursementAmounts(ExpenseReportHeader); + ExpenseReportLineVATSpec.Modify(false); + end else + if ExpenseReportLineVATSpec."Reclaim %" <> 0 then begin + UpdateExpenseReportLineReclaimAmount(ExpenseReportLineVATSpec); + ExpenseReportLineVATSpec.Modify(false); + end; + until ExpenseReportLineVATSpec.Next() = 0; + end; + + local procedure UpdateExpenseReportLineReclaimAmount(var ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec.") + var + ReimbursementCurrency: Record Currency; + begin + ReimbursementCurrency.Initialize(''); + ExpenseReportLineVATSpec."Reclaim VAT Amount (RCY)" := + Round( + ExpenseReportLineVATSpec."VAT Amount (RCY)" * ExpenseReportLineVATSpec."Reclaim %" / 100, + ReimbursementCurrency."Amount Rounding Precision"); + end; + + local procedure BackfillPostedExpenseReportLineVATSpecs() + var + PostedExpenseReportHeader: Record "Posted Expense Report Header"; + PostedExpenseReportLineVATSpec: Record "Posted Exp. Rep. Line VAT Spec"; + CachedExpenseReportNo: Code[20]; + HasCachedExpenseReportNo: Boolean; + HeaderFound: Boolean; + begin + PostedExpenseReportLineVATSpec.SetCurrentKey("Expense Report No.", "Expense Report Line No.", "Line No."); + PostedExpenseReportLineVATSpec.SetLoadFields( + "Expense Report No.", "VAT Base Amount (LCY)", "VAT Amount (LCY)", "Reclaim %", + "VAT Base Amount (RCY)", "VAT Amount (RCY)", "Amount (RCY)", "Reclaim VAT Amount (RCY)"); + if not PostedExpenseReportLineVATSpec.FindSet(true) then + exit; + + CachedExpenseReportNo := ''; + repeat + if (not HasCachedExpenseReportNo) or (CachedExpenseReportNo <> PostedExpenseReportLineVATSpec."Expense Report No.") then begin + CachedExpenseReportNo := PostedExpenseReportLineVATSpec."Expense Report No."; + HasCachedExpenseReportNo := true; + PostedExpenseReportHeader.SetLoadFields("Reimbursement Currency Code", "Posting Date", "Reimbursement Currency Factor"); + HeaderFound := PostedExpenseReportHeader.Get(CachedExpenseReportNo); + end; + + if HeaderFound then + if PostedExpenseReportHeader."Reimbursement Currency Code" <> '' then begin + UpdatePostedReimbursementAmounts(PostedExpenseReportLineVATSpec, PostedExpenseReportHeader); + PostedExpenseReportLineVATSpec.Modify(false); + end else + if PostedExpenseReportLineVATSpec."Reclaim %" <> 0 then begin + UpdatePostedReclaimAmount(PostedExpenseReportLineVATSpec); + PostedExpenseReportLineVATSpec.Modify(false); + end; + until PostedExpenseReportLineVATSpec.Next() = 0; + end; + + local procedure UpdatePostedReclaimAmount(var PostedExpenseReportLineVATSpec: Record "Posted Exp. Rep. Line VAT Spec") + var + ReimbursementCurrency: Record Currency; + begin + ReimbursementCurrency.Initialize(''); + PostedExpenseReportLineVATSpec."Reclaim VAT Amount (RCY)" := + Round( + PostedExpenseReportLineVATSpec."VAT Amount (RCY)" * PostedExpenseReportLineVATSpec."Reclaim %" / 100, + ReimbursementCurrency."Amount Rounding Precision"); + 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"); + + 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)"; + + 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 cfd5e807b89..22b4e4f2fa6 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 @@ -498,6 +498,7 @@ page 6910 "Expense Report" CurrPage.SaveRecord(); end; } +#if not CLEAN30 action(VATSpecification) { ApplicationArea = Basic, Suite; @@ -506,8 +507,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 = '30.0'; } +#endif action("Spend Request") { ApplicationArea = Basic, Suite; @@ -660,9 +665,14 @@ page 6910 "Expense Report" actionref(dimension_Promoted; Dimensions) { } +#if not CLEAN30 actionref(VATSpecification_Promoted; VATSpecification) { + ObsoleteReason = 'Replaced by Expense Report Statistics'; + ObsoleteState = Pending; + ObsoleteTag = '30.0'; } +#endif actionref("Spend Request_Promoted"; "Spend Request") { } @@ -724,7 +734,6 @@ page 6910 "Expense Report" DocNoVisible: Boolean; ExpenseUserNo: Code[20]; ApproverComment: Text; - AllowVATReclaim: Boolean; ApprovalActionsEnabled: Boolean; AgentEnabled: Boolean; @@ -744,7 +753,6 @@ page 6910 "Expense Report" ReopenApprovedEnabled := ExpenseReportApprovalMgt.CanPerformApprovalAction(Rec, RefActionType::"Reopen Approved"); ExpenseAgentSetup.GetRecordOnce(); - AllowVATReclaim := ExpenseAgentSetup."Allow VAT Reclaim"; AgentEnabled := ExpenseAgentSetup."Enable Agent"; 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..ec751fa48fc 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,7 +45,10 @@ 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") { @@ -57,6 +60,10 @@ page 7081 "Expense Report Line VAT Spec." Caption = 'VAT Amount'; Editable = false; } + field("Amount (LCY)"; Rec."Amount (LCY)") + { + Caption = 'Amount (LCY)'; + } field("VAT Base Amount (LCY)"; Rec."VAT Base Amount (LCY)") { Caption = 'VAT Base Amount (LCY)'; @@ -67,6 +74,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 +107,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 +181,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(true) 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 +227,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..2515245e9fa 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,19 @@ page 7096 "Expense Report VAT Spec." } } } + + trigger OnOpenPage() + var + ExpenseReportHeader: Record "Expense Report Header"; + begin + if Rec.GetFilter("Document No.") = '' then + exit; + + ExpenseReportHeader.SetLoadFields("Reimbursement Currency Code"); + 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..cf233a717f7 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,19 @@ 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; + + PostedExpenseReportHeader.SetLoadFields("Reimbursement Currency Code"); + 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 b82a1bc91a5..1b5b31fb326 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 @@ -105,7 +105,7 @@ table 6906 "Expense Report Header" UpdateCurrencyFactor(); if xRec."Posting Date" <> Rec."Posting Date" then - UpdateReportLines(Rec.FieldName("Posting Date")); + UpdateReportLines(Rec.FieldCaption("Posting Date")); end; } field(6; "Description"; Text[100]) @@ -359,7 +359,7 @@ table 6906 "Expense Report Header" begin TestStatusOpen(); if xRec."VAT Bus. Posting Group" <> Rec."VAT Bus. Posting Group" then - UpdateReportLines(Rec.FieldName("VAT Bus. Posting Group")); + UpdateReportLines(Rec.FieldCaption("VAT Bus. Posting Group")); end; } field(42; "Submission DateTime"; DateTime) @@ -440,7 +440,7 @@ table 6906 "Expense Report Header" begin if "Reimbursement Currency Factor" <> xRec."Reimbursement Currency Factor" then begin TestStatusOpen(); - UpdateReportLines(Rec.FieldName("Reimbursement Currency Factor")); + UpdateReportLines(Rec.FieldCaption("Reimbursement Currency Factor")); end; end; } @@ -561,7 +561,7 @@ table 6906 "Expense Report Header" Rec."Spend Request Close" := false; if xRec."Spend Request No." <> Rec."Spend Request No." then - UpdateReportLines(Rec.FieldName("Spend Request No.")); + UpdateReportLines(Rec.FieldCaption("Spend Request No.")); end; } field(101; "Spend Request Close"; Boolean) @@ -695,7 +695,7 @@ table 6906 "Expense Report Header" end; end; - local procedure UpdateReportLines(CalledFromFieldName: Text) + local procedure UpdateReportLines(CalledFromFieldCaption: Text) var ExpenseReportLine: Record "Expense Report Line"; ConfirmManagement: Codeunit "Confirm Management"; @@ -703,7 +703,7 @@ table 6906 "Expense Report Header" if not ExpenseLinesExist() then exit; - if not ConfirmManagement.GetResponseOrDefault(StrSubstNo(CanModifyLinesQst, CalledFromFieldName), true) then + if not ConfirmManagement.GetResponseOrDefault(StrSubstNo(CanModifyLinesQst, CalledFromFieldCaption), true) then Error(''); ExpenseReportLine.SetRange("Document No.", "No."); @@ -711,17 +711,38 @@ table 6906 "Expense Report Header" repeat ExpenseReportLine.Initialize(Rec); - case CalledFromFieldName of - Rec.FieldName("Reimbursement Currency Code"), Rec.FieldName("Reimbursement Currency Factor"): + case CalledFromFieldCaption of + Rec.FieldCaption("Reimbursement Currency Code"), Rec.FieldCaption("Reimbursement Currency Factor"): UpdateCurrFactorOnReportLine(ExpenseReportLine); - Rec.FieldName("VAT Bus. Posting Group"): + Rec.FieldCaption("VAT Bus. Posting Group"): UpdateVATBusPostingGroupOnReportLine(ExpenseReportLine); - Rec.FieldName("Posting Date"): + Rec.FieldCaption("Posting Date"): UpdatePostingDateOnReportLine(ExpenseReportLine); - Rec.FieldName("Spend Request No."): + Rec.FieldCaption("Spend Request No."): UpdateSpendRequestOnReportLine(ExpenseReportLine); end; until ExpenseReportLine.Next() = 0; + + if CalledFromFieldCaption in [Rec.FieldCaption("Reimbursement Currency Code"), Rec.FieldCaption("Reimbursement Currency Factor"), Rec.FieldCaption("Posting Date")] then + UpdateVATSpecReimbursementAmounts(); + end; + + local procedure UpdateVATSpecReimbursementAmounts() + var + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + begin + ExpenseReportLineVATSpec.SetRange("Document No.", "No."); + ExpenseReportLineVATSpec.SetLoadFields( + "Currency Code", "VAT Base Amount (LCY)", "VAT Amount", "VAT Amount (LCY)", "Amount (LCY)", "Reclaim %", + "VAT Base Amount (RCY)", "VAT Amount (RCY)", "Amount (RCY)", "Reclaim VAT Amount", + "Reclaim VAT Amount (LCY)", "Reclaim VAT Amount (RCY)"); + if ExpenseReportLineVATSpec.FindSet(true) 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") @@ -1115,13 +1136,13 @@ table 6906 "Expense Report Header" if UpdateCurrencyExchangeRates.ExchangeRatesForCurrencyExist(CurrencyDate, Rec."Reimbursement Currency Code") then begin Rec."Reimbursement Currency Factor" := CurrExchRate.ExchangeRate(CurrencyDate, Rec."Reimbursement Currency Code"); if (Rec."Reimbursement Currency Code" <> xRec."Reimbursement Currency Code") and (xRec."No." <> '') then - UpdateReportLines(Rec.FieldName("Reimbursement Currency Code")); + UpdateReportLines(Rec.FieldCaption("Reimbursement Currency Code")); end else UpdateCurrencyExchangeRates.ShowMissingExchangeRatesNotification("Reimbursement Currency Code"); end else begin Rec."Reimbursement Currency Factor" := 0; if "Reimbursement Currency Code" <> xRec."Reimbursement Currency Code" then - UpdateReportLines(Rec.FieldName("Reimbursement Currency Code")); + UpdateReportLines(Rec.FieldCaption("Reimbursement Currency Code")); end; end; 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 ad0d228a328..e26abf4ce40 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 @@ -1584,14 +1584,44 @@ 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; + StoredSuffixLength: Integer; + 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"; + StoredSuffixLength := StrLen(PostingDescriptionSuffix); + if StoredSuffixLength > MaxStrLen(BaseDescription) then + StoredSuffixLength := MaxStrLen(BaseDescription); + while (StoredSuffixLength >= StrLen(' / ')) and + (not BaseDescription.EndsWith(CopyStr(PostingDescriptionSuffix, 1, StoredSuffixLength))) + do + StoredSuffixLength -= 1; + if (StoredSuffixLength = StrLen(PostingDescriptionSuffix)) or + ((StrLen(BaseDescription) = MaxStrLen(BaseDescription)) and (StoredSuffixLength >= StrLen(' / '))) + then + BaseDescription := CopyStr( + DelStr(BaseDescription, StrLen(BaseDescription) - StoredSuffixLength + 1), 1, MaxStrLen(BaseDescription)); end; - exit(CopyStr(Description, 1, 100)); + + 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..227f1601aac 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,50 @@ 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 ExpenseCategory.Get("Expense Category") then begin + ExpenseCategory.TestField(Inactive, false); + if "Expense Subcategory" <> '' then + exit; + "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 + ExpenseSubcategory.TestField(Inactive, false); + "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 + ExpenseCategory.TestField(Inactive, false); + "VAT Prod. Posting Group" := ExpenseCategory."VAT Prod. Posting Group"; + "VAT %" := ExpenseCategory."Default VAT %"; + Validate("VAT %"); + end; + end; } field(30; Reclaimable; Boolean) { @@ -192,22 +246,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 +347,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 +397,152 @@ 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 begin + ExpenseReportHeader.SetLoadFields("Reimbursement Currency Code"); + ExpenseReportHeader.Get("Document No."); + end; + end; + + local procedure GetExpenseReportLine() + begin + if ("Document No." <> ExpenseReportLine."Document No.") or ("Document Line No." <> ExpenseReportLine."Line No.") then begin + ExpenseReportLine.SetLoadFields("Expense Date"); + ExpenseReportLine.Get("Document No.", "Document Line No."); + end; + 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..53e405dc3fd 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,20 @@ table 6934 "Posted Exp. Rep. Line VAT Spec" } } + var + PostedExpenseReportHeader: Record "Posted Expense Report Header"; + + local procedure GetReimbursementCurrencyCode(): Code[20] + begin + if "Expense Report No." <> PostedExpenseReportHeader."No." then begin + PostedExpenseReportHeader.SetLoadFields("Reimbursement Currency Code"); + if not PostedExpenseReportHeader.Get("Expense Report No.") then + Clear(PostedExpenseReportHeader); + end; + + 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/app/src/Setup/Codeunits/CreateExpenseVATRates.Codeunit.al b/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/CreateExpenseVATRates.Codeunit.al index ab86217346f..f5fad4c1aeb 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/CreateExpenseVATRates.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/CreateExpenseVATRates.Codeunit.al @@ -38,12 +38,6 @@ codeunit 6975 "Create Expense VAT Rates" CompanyInfo: Record "Company Information"; GLAccount: Record "G/L Account"; begin - ExpenseAgentSetup.GetRecordOnce(); - if ExpenseAgentSetup."Default VAT Bus. Posting Group" = '' then begin - ExpenseAgentSetup."Default VAT Bus. Posting Group" := XDOMESTICTxt; - ExpenseAgentSetup.Modify(); - end; - if not GLAccount.Get(XEXPENSEVATTok) then begin GLAccount.Init(); GLAccount."No." := XEXPENSEVATTok; @@ -140,6 +134,7 @@ codeunit 6975 "Create Expense VAT Rates" VATProductPostingGroup.Insert(); end; + ExpenseAgentSetup.GetRecordOnce(); if not VATPostingSetup.Get(ExpenseAgentSetup."Default VAT Bus. Posting Group", VATProdPostingGroup) then begin VATPostingSetup.Init(); VATPostingSetup.Validate("VAT Bus. Posting Group", ExpenseAgentSetup."Default VAT Bus. Posting Group"); @@ -170,7 +165,6 @@ codeunit 6975 "Create Expense VAT Rates" CreateExpenseCategories: Codeunit "Create Expense Categories"; XEXPENSEVATTok: Label 'EXPENSE VAT', Locked = true; // Virtual G/L account used for VAT on expenses - XDOMESTICTxt: Label 'DOMESTIC'; // DOMESTIC VAT Business Posting Group used as default for all rates created by this codeunit local procedure CreateVATRatesAT() begin diff --git a/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/InstallExpenseAgentSetup.Codeunit.al b/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/InstallExpenseAgentSetup.Codeunit.al index 18143576fa2..0c10040296a 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/InstallExpenseAgentSetup.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/InstallExpenseAgentSetup.Codeunit.al @@ -19,8 +19,13 @@ codeunit 6992 "Install Expense Agent Setup" trigger OnInstallAppPerCompany() var ExpenseAgentSetup: Record "Expense Agent Setup"; + UpgradeExpReportVATSpec: Codeunit "Upgrade Exp. Report VAT Spec"; + AppInfo: ModuleInfo; begin ExpenseAgentSetup.InitRecord(); + NavApp.GetCurrentModuleInfo(AppInfo); + if AppInfo.DataVersion() = Version.Create('0.0.0.0') then + UpgradeExpReportVATSpec.SetBackfillReimbursementAmountsUpgradeTag(); end; trigger OnInstallAppPerDatabase() diff --git a/src/Apps/W1/ExpenseAgent/app/src/Setup/Tables/ExpenseAgentSetup.Table.al b/src/Apps/W1/ExpenseAgent/app/src/Setup/Tables/ExpenseAgentSetup.Table.al index 4e2f602c6f6..7721f65ac7f 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Setup/Tables/ExpenseAgentSetup.Table.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Setup/Tables/ExpenseAgentSetup.Table.al @@ -666,14 +666,9 @@ table 6930 "Expense Agent Setup" ToolTip = 'Specifies whether VAT reclaim is allowed on expenses.'; trigger OnValidate() - var - VATSetup: Record "VAT Setup"; begin - if "Allow VAT Reclaim" then begin + if "Allow VAT Reclaim" then TestField("Default VAT Bus. Posting Group"); - VATSetup.Get(); - VATSetup.TestField("Non-Deductible VAT Is Enabled"); - end; end; } field(105; "VAT Rates Applied"; Boolean) @@ -799,7 +794,7 @@ table 6930 "Expense Agent Setup" CreateEmployeesForExpenseUsersQst: Label 'Turning on this setting will enable automatic creation of records in the Employee table. This may impact your HR setup in Business Central.\\Are you sure you want to enable this feature?'; UpdateDefaultsApproverQst: Label 'You have changed the default approver.\\Do you also want to change approver from %1 to %2 for all expense users who currently have %1 as approver?', Comment = '%1 and %2 are both person names.'; UpdatingDefaultApproversLbl: Label 'Updating approvers...'; - + XDOMESTICTxt: Label 'DOMESTIC'; // DOMESTIC VAT Business Posting Group used as default for all rates created by this codeunit internal procedure AssistEditNoreplyMailbox() @@ -1180,9 +1175,14 @@ table 6930 "Expense Agent Setup" var CreateExpenseCountryVATRates: Codeunit "Create Expense VAT Rates"; begin + Rec.GetRecordOnce(); + if Rec."Default VAT Bus. Posting Group" = '' then begin + Rec."Default VAT Bus. Posting Group" := XDOMESTICTxt; + Rec.Modify(); + end; + CreateExpenseCountryVATRates.InsertDefaultRates(); - Rec.GetRecordOnce(); if not Rec."VAT Rates Applied" then begin Rec."VAT Rates Applied" := true; Rec.Modify(); diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseVATSpecAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseVATSpecAPITest.Codeunit.al new file mode 100644 index 00000000000..d2fb06bdfa5 --- /dev/null +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseVATSpecAPITest.Codeunit.al @@ -0,0 +1,92 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Test.ExpenseAgent; + +using Microsoft.ExpenseAgent; + +codeunit 148348 "Expense VAT Spec. API Test" +{ + Subtype = Test; + TestType = IntegrationTest; + TestPermissions = Disabled; + + var + Assert: Codeunit Assert; + LibraryExpense: Codeunit "Library - Expense"; + LibraryGraphMgt: Codeunit "Library - Graph Mgt"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + APITestAuthHelper: Codeunit "Expense API Test Auth Helper"; + IsInitialized: Boolean; + ServiceNameTok: Label 'expenseVATSpecifications', Locked = true; + + [Test] + procedure AgentVATSpecificationIsInsertedThroughAPI() + var + Expense: Record Expense; + ExpenseCategory: Record "Expense Category"; + ExpenseUser: Record "Expense User"; + ExpenseVATSpecification: Record "Expense VAT Specification"; + RequestBody: JsonObject; + ResponseText: Text; + RequestText: Text; + TargetURL: Text; + begin + // [SCENARIO] An authorized Expense Agent caller creates a VAT specification through the OData API. + Initialize(); + + // [GIVEN] A persisted expense that can own the agent-authored VAT specification. + LibraryExpense.CreateExpenseUser(ExpenseUser); + LibraryExpense.CreateExpenseCategory( + ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); + LibraryExpense.CreateExpense(Expense, ExpenseUser."No.", ExpenseCategory.Code, '', '', true, '', 120); + Commit(); + + // [WHEN] The VAT specification is posted through the published API endpoint. + RequestBody.Add('expenseNo', Expense."No."); + RequestBody.Add('vatPercent', 20); + RequestBody.Add('vatBaseAmount', 100); + RequestBody.Add('vatAmount', 20); + RequestBody.Add('amount', 120); + RequestBody.Add('amountLCY', 120); + RequestBody.Add('vatBaseAmountLCY', 100); + RequestBody.Add('vatAmountLCY', 20); + RequestBody.Add('confidence', 0.95); + RequestBody.WriteTo(RequestText); + TargetURL := LibraryGraphMgt.CreateTargetURL('', Page::"Expense VAT Spec. API", ServiceNameTok); + LibraryGraphMgt.PostToWebServiceAndCheckResponseCode(TargetURL, RequestText, ResponseText, 201); + + // [THEN] OnInsertRecord inserted one row and exit(false) prevented a duplicate platform insert. + ExpenseVATSpecification.SetRange("Expense No.", Expense."No."); + Assert.RecordCount(ExpenseVATSpecification, 1); + ExpenseVATSpecification.FindFirst(); + + // [THEN] The endpoint persisted the caller payload as an immutable agent-authored source. + Assert.AreEqual(ExpenseVATSpecification.Source::Agent, ExpenseVATSpecification.Source, 'The persisted source must be Agent.'); + Assert.AreEqual(20, ExpenseVATSpecification."VAT %", 'The VAT percent must match the API payload.'); + Assert.AreEqual(100, ExpenseVATSpecification."VAT Base Amount", 'The VAT base amount must match the API payload.'); + Assert.AreEqual(20, ExpenseVATSpecification."VAT Amount", 'The VAT amount must match the API payload.'); + Assert.AreEqual(120, ExpenseVATSpecification.Amount, 'The amount must match the API payload.'); + Assert.AreEqual(120, ExpenseVATSpecification."Amount (LCY)", 'The LCY amount must match the API payload.'); + Assert.AreEqual(0.95, ExpenseVATSpecification.Confidence, 'The confidence must match the API payload.'); + Assert.AreNotEqual( + 0, + StrPos(LowerCase(ResponseText), LowerCase(LibraryGraphMgt.StripBrackets(Format(ExpenseVATSpecification.SystemId)))), + 'The response must identify the row inserted by OnInsertRecord.'); + end; + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Expense VAT Spec. API Test"); + if IsInitialized then + exit; + + BindSubscription(APITestAuthHelper); + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Expense VAT Spec. API Test"); + LibraryExpense.SetupNumberSeriesInExpenseMgmt(); + IsInitialized := true; + Commit(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Expense VAT Spec. API Test"); + end; +} \ No newline at end of file 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..323ede7d9fc 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/ExpensePostingVATTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/ExpensePostingVATTest.Codeunit.al @@ -5,11 +5,16 @@ 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; using Microsoft.HumanResources.Employee; +using Microsoft.Projects.Project.Job; +using Microsoft.Projects.Project.Ledger; using Microsoft.Purchases.Vendor; +using System.Environment; +using System.TestLibraries.Utilities; codeunit 148330 "Expense Posting VAT Test" { @@ -21,11 +26,17 @@ codeunit 148330 "Expense Posting VAT Test" Assert: Codeunit "Assert"; LibraryERM: Codeunit "Library - ERM"; LibraryExpense: Codeunit "Library - Expense"; + LibraryJob: Codeunit "Library - Job"; LibraryPurchase: Codeunit "Library - Purchase"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; LibraryUtility: Codeunit "Library - Utility"; + LibraryVariableStorage: Codeunit "Library - Variable Storage"; IsInitialized: Boolean; + CanModifyLinesQst: Label 'You have modified %1 which will also update the lines.\\Do you want to continue?', Comment = '%1 = Field Caption'; + 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.'; + RoundingDifferenceTooLargeErr: Label 'exceeds the currency rounding precision'; [Test] [HandlerFunctions('ExpensesModalPageHandler,ConfirmHandler')] @@ -78,6 +89,56 @@ codeunit 148330 "Expense Posting VAT Test" VerifyGLEntry(PostedExpenseReportHeader."No.", GetRefundableDebitAccount(ExpenseCategory.Code), 144.09); end; + [Test] + [HandlerFunctions('ExpensesModalPageHandler,ConfirmHandler')] + procedure MultipleVATSpecificationsPostCompleteAmountToProject() + var + Expense: Record Expense; + ExpenseUser: Record "Expense User"; + ExpenseCategory: Record "Expense Category"; + ExpenseSubCategory: array[4] of Record "Expense Subcategory"; + ExpenseReportHeader: Record "Expense Report Header"; + PostedExpenseReportHeader: Record "Posted Expense Report Header"; + PostedExpenseReportLine: Record "Posted Expense Report Line"; + Job: Record Job; + JobTask: Record "Job Task"; + JobLedgerEntry: Record "Job Ledger Entry"; + VATPostingSetup: array[3] of Record "VAT Posting Setup"; + begin + // [SCENARIO] A project receives the complete expense-line amount when the line has multiple VAT specifications. + Initialize(); + + // [GIVEN] An expense assigned to a project with two VAT specifications whose LCY bases total 200. + CreateExpenseUserAndCategory(ExpenseUser, ExpenseCategory); + CreateSubcategoryWithVATRate(ExpenseSubCategory[1], ExpenseCategory.Code, 10, VATPostingSetup[1]); + CreateSubcategoryWithVATRate(ExpenseSubCategory[2], ExpenseCategory.Code, 20, VATPostingSetup[2]); + CreateExpenseWithHotelItemizations(Expense, ExpenseUser, ExpenseCategory, ExpenseSubCategory, VATPostingSetup, 110, 120, 0, 0); + LibraryJob.CreateJob(Job); + LibraryJob.CreateJobTask(Job, JobTask); + Expense.PerformManualReopen(Expense); + Expense.Get(Expense."No."); + Expense.Validate("Job No.", JobTask."Job No."); + Expense.Validate("Job Task No.", JobTask."Job Task No."); + Expense.Modify(true); + ReleaseExpenseAndUpdateAccounts(Expense, ExpenseUser); + + // [WHEN] The expense report is posted. + PostExpenseReport(ExpenseReportHeader, Expense, ExpenseUser); + + // [THEN] One project ledger entry contains the sum of both specification journal amounts. + FindPostedExpenseReport(PostedExpenseReportHeader, Expense); + PostedExpenseReportLine.SetRange("Document No.", PostedExpenseReportHeader."No."); + PostedExpenseReportLine.FindFirst(); +#pragma warning disable AA0210 + JobLedgerEntry.SetRange("Expense Report No.", PostedExpenseReportHeader."No."); + JobLedgerEntry.SetRange("Expense Report Line No.", PostedExpenseReportLine."Line No."); +#pragma warning restore AA0210 + JobLedgerEntry.FindFirst(); + Assert.RecordCount(JobLedgerEntry, 1); + Assert.AreNearlyEqual(200, JobLedgerEntry."Unit Cost (LCY)", 0.01, 'Project unit cost must include all VAT specification journal amounts.'); + Assert.AreNearlyEqual(200, JobLedgerEntry."Unit Price (LCY)", 0.01, 'Project unit price must include all VAT specification journal amounts.'); + end; + [Test] [HandlerFunctions('ExpensesModalPageHandler,ConfirmHandler')] procedure VATEntriesPerHotelSubcat() @@ -327,13 +388,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,26 +450,785 @@ 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)); + LibraryVariableStorage.AssertEmpty(); PostedExpenseReportHeader.SetRange("Expense User No.", ExpenseUser."No."); Assert.RecordCount(PostedExpenseReportHeader, 0); end; + [Test] + [HandlerFunctions('ExpensesModalPageHandler,ConfirmHandler')] + procedure MaterialVATSpecificationDifferenceBlocksPosting() + 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"; + VATPostingSetup: array[3] of Record "VAT Posting Setup"; + CreateExpenseReport: Codeunit "Create Expense Report"; + ExpenseReportPost: Codeunit "Expense Report-Post"; + begin + // [SCENARIO] A material difference between an expense report line and its VAT specifications blocks posting. + Initialize(); + + // [GIVEN] An LCY expense report whose VAT specification total is lower than the line amount by 1.00. + CreateExpenseUserAndCategory(ExpenseUser, ExpenseCategory); + CreateSubcategoryWithVATRate(ExpenseSubCategory[1], ExpenseCategory.Code, 10, VATPostingSetup[1]); + CreateExpenseWithHotelItemizations(Expense, ExpenseUser, ExpenseCategory, ExpenseSubCategory, VATPostingSetup, 110, 0, 0, 0); + LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", '', Expense."VAT Bus. Posting Group"); + AddExpensesToReport(CreateExpenseReport, ExpenseReportHeader, Expense."No."); + UpdateExpenseReportLinesWithVendor(ExpenseReportHeader); + ExpenseReportHeader.PerformManualRelease(); + ExpenseReportLineVATSpec.SetRange("Document No.", ExpenseReportHeader."No."); + ExpenseReportLineVATSpec.SetFilter("Document Line No.", '<>%1', 0); + ExpenseReportLineVATSpec.FindFirst(); + ExpenseReportLineVATSpec."VAT Base Amount (RCY)" -= 1; + ExpenseReportLineVATSpec."VAT Base Amount (LCY)" -= 1; + ExpenseReportLineVATSpec.Modify(false); + ExpenseReportLine.SetRange("Document No.", ExpenseReportHeader."No."); + ExpenseReportLine.FindFirst(); + ExpenseReportLineVATSpec.CalcSums("VAT Base Amount (RCY)", "VAT Amount (RCY)", "VAT Base Amount (LCY)", "VAT Amount (LCY)"); + Assert.AreNearlyEqual( + 1, ExpenseReportLine."Reimbursable Amount" - ExpenseReportLineVATSpec."VAT Base Amount (RCY)" - ExpenseReportLineVATSpec."VAT Amount (RCY)", 0.01, + 'The test must create a material reimbursement-currency difference.'); + Assert.AreNearlyEqual( + 1, ExpenseReportLine."Reimbursable Amount (LCY)" - ExpenseReportLineVATSpec."VAT Base Amount (LCY)" - ExpenseReportLineVATSpec."VAT Amount (LCY)", 0.01, + 'The test must create a material local-currency difference.'); + + // [WHEN] Posting is attempted. + asserterror PostExpenseReportWithConfirmation(ExpenseReportPost, ExpenseReportHeader); + + // [THEN] The material difference is not reclassified to an expense rounding account. + Assert.ExpectedError(RoundingDifferenceTooLargeErr); + LibraryVariableStorage.AssertEmpty(); + PostedExpenseReportHeader.SetRange("Expense User No.", ExpenseUser."No."); + 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)); + LibraryVariableStorage.AssertEmpty(); + PostedExpenseReportHeader.SetRange("Expense User No.", ExpenseUser."No."); + Assert.RecordCount(PostedExpenseReportHeader, 0); + end; + + [Test] + [HandlerFunctions('ExpensesModalPageHandler,ConfirmHandler')] + procedure InactiveVATSpecExpenseCategoryBlocksPosting() + var + Expense: Record Expense; + ExpenseCategory: Record "Expense Category"; + VATSpecExpenseCategory: Record "Expense Category"; + ExpenseItemization: Record "Expense Itemization"; + ExpenseReportHeader: Record "Expense Report Header"; + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + ExpenseSubcategory: Record "Expense Subcategory"; + ExpenseUser: Record "Expense User"; + VATPostingSetup: Record "VAT Posting Setup"; + CreateExpenseReport: Codeunit "Create Expense Report"; + ExpenseReportPost: Codeunit "Expense Report-Post"; + begin + // [SCENARIO] Posting is blocked when a VAT specification references an inactive expense category. + Initialize(); + + // [GIVEN] An expense report whose parent line references active expense masters. + CreateExpenseUserAndCategory(ExpenseUser, ExpenseCategory); + CreateSubcategoryWithVATRate(ExpenseSubcategory, ExpenseCategory.Code, 20, VATPostingSetup); + CreateExpenseForHotel(Expense, ExpenseUser, ExpenseCategory, VATPostingSetup, 120); + LibraryExpense.CreateExpenseItemization(ExpenseItemization, Expense, ExpenseCategory.Code, ExpenseSubcategory.Code, WorkDate(), 120, 1); + CreateExpenseVATSpecification(Expense, ExpenseCategory.Code, ExpenseSubcategory.Code, 120); + ReleaseExpenseAndUpdateAccounts(Expense, ExpenseUser); + + LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", '', Expense."VAT Bus. Posting Group"); + AddExpensesToReport(CreateExpenseReport, ExpenseReportHeader, Expense."No."); + UpdateExpenseReportLinesWithVendor(ExpenseReportHeader); + + // [GIVEN] The VAT specification references a different category that becomes inactive after release. + LibraryExpense.CreateExpenseCategory(VATSpecExpenseCategory, VATSpecExpenseCategory."Reimbursement Type"::"Employee Paid", VATSpecExpenseCategory."Expense Detail Required"::" "); + ExpenseReportLineVATSpec.SetRange("Document No.", ExpenseReportHeader."No."); + ExpenseReportLineVATSpec.FindFirst(); + ExpenseReportLineVATSpec."Expense Category" := VATSpecExpenseCategory.Code; + ExpenseReportLineVATSpec."Expense Subcategory" := ''; + ExpenseReportLineVATSpec.Modify(); + ExpenseReportHeader.PerformManualRelease(); + VATSpecExpenseCategory.Validate(Inactive, true); + VATSpecExpenseCategory.Modify(true); + + // [WHEN] Posting is attempted. + asserterror PostExpenseReportWithConfirmation(ExpenseReportPost, ExpenseReportHeader); + + // [THEN] The inactive VAT-spec category blocks posting. + Assert.ExpectedTestFieldError(VATSpecExpenseCategory.FieldCaption(Inactive), Format(false)); + LibraryVariableStorage.AssertEmpty(); + end; + + [Test] + [HandlerFunctions('ExpensesModalPageHandler,ConfirmHandler')] + procedure InactiveVATSpecExpenseSubcategoryBlocksPosting() + var + Expense: Record Expense; + ExpenseCategory: Record "Expense Category"; + ExpenseItemization: Record "Expense Itemization"; + ExpenseReportHeader: Record "Expense Report Header"; + ExpenseSubcategory: array[2] of Record "Expense Subcategory"; + ExpenseUser: Record "Expense User"; + VATPostingSetup: Record "VAT Posting Setup"; + CreateExpenseReport: Codeunit "Create Expense Report"; + ExpenseReportPost: Codeunit "Expense Report-Post"; + begin + // [SCENARIO] Posting is blocked when a VAT specification references an inactive expense subcategory. + Initialize(); + + // [GIVEN] An expense report whose parent line references an active subcategory. + CreateExpenseUserAndCategory(ExpenseUser, ExpenseCategory); + CreateSubcategoryWithVATRate(ExpenseSubcategory[1], ExpenseCategory.Code, 20, VATPostingSetup); + CreateSubcategoryWithVATRate(ExpenseSubcategory[2], ExpenseCategory.Code, 20, VATPostingSetup); + CreateExpenseForHotel(Expense, ExpenseUser, ExpenseCategory, VATPostingSetup, 120); + LibraryExpense.CreateExpenseItemization(ExpenseItemization, Expense, ExpenseCategory.Code, ExpenseSubcategory[2].Code, WorkDate(), 120, 1); + CreateExpenseVATSpecification(Expense, ExpenseCategory.Code, ExpenseSubcategory[2].Code, 120); + ReleaseExpenseAndUpdateAccounts(Expense, ExpenseUser); + + LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", '', Expense."VAT Bus. Posting Group"); + AddExpensesToReport(CreateExpenseReport, ExpenseReportHeader, Expense."No."); + UpdateExpenseReportLinesWithVendor(ExpenseReportHeader); + ExpenseReportHeader.PerformManualRelease(); + + // [GIVEN] The subcategory referenced only by the VAT specification becomes inactive. + ExpenseSubcategory[2].Validate(Inactive, true); + ExpenseSubcategory[2].Modify(true); + + // [WHEN] Posting is attempted. + asserterror PostExpenseReportWithConfirmation(ExpenseReportPost, ExpenseReportHeader); + + // [THEN] The inactive VAT-spec subcategory blocks posting. + Assert.ExpectedTestFieldError(ExpenseSubcategory[2].FieldCaption(Inactive), Format(false)); + LibraryVariableStorage.AssertEmpty(); + end; + + [Test] + procedure InactiveExpenseCategoryCannotBeValidatedOnVATSpec() + var + ExpenseCategory: Record "Expense Category"; + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + begin + // [SCENARIO] An inactive expense category cannot be selected on a VAT specification. + Initialize(); + + // [GIVEN] An inactive expense category. + LibraryExpense.CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); + ExpenseCategory.Validate(Inactive, true); + ExpenseCategory.Modify(true); + + // [WHEN] The category is validated on a VAT specification. + asserterror ExpenseReportLineVATSpec.Validate("Expense Category", ExpenseCategory.Code); + + // [THEN] The inactive category is rejected immediately. + Assert.ExpectedTestFieldError(ExpenseCategory.FieldCaption(Inactive), Format(false)); + end; + + [Test] + procedure InactiveExpenseSubcategoryCannotBeValidatedOnVATSpec() + var + ExpenseCategory: Record "Expense Category"; + ExpenseReportLineVATSpec: Record "Expense Report Line VAT Spec."; + ExpenseSubcategory: Record "Expense Subcategory"; + begin + // [SCENARIO] An inactive expense subcategory cannot be selected on a VAT specification. + Initialize(); + + // [GIVEN] An active category with an inactive subcategory. + LibraryExpense.CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); + LibraryExpense.CreateExpenseSubCategory(ExpenseSubcategory, ExpenseCategory.Code, true); + ExpenseSubcategory.Validate(Inactive, true); + ExpenseSubcategory.Modify(true); + ExpenseReportLineVATSpec."Expense Category" := ExpenseCategory.Code; + + // [WHEN] The subcategory is validated on a VAT specification. + asserterror ExpenseReportLineVATSpec.Validate("Expense Subcategory", ExpenseSubcategory.Code); + + // [THEN] The inactive subcategory is rejected immediately. + Assert.ExpectedTestFieldError(ExpenseSubcategory.FieldCaption(Inactive), Format(false)); + 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 ReopenedReportRecalculatesVATSpecWhenReimbursementCurrencyFactorChanges() + 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."; + VATPostingSetup: array[3] of Record "VAT Posting Setup"; + CreateExpenseReport: Codeunit "Create Expense Report"; + CurrencyCode: Code[10]; + InitialVATBaseAmountRCY: Decimal; + InitialVATAmountRCY: Decimal; + begin + // [SCENARIO] Changing the reimbursement currency factor on a reopened report recalculates existing VAT specifications. + Initialize(); + + // [GIVEN] A released report with an existing 20% VAT specification calculated at a reimbursement factor of 1. + CreateExpenseUserAndCategory(ExpenseUser, ExpenseCategory); + CreateSubcategoryWithVATRate(ExpenseSubCategory[1], ExpenseCategory.Code, 20, VATPostingSetup[1]); + CreateExpenseWithHotelItemizations(Expense, ExpenseUser, ExpenseCategory, ExpenseSubCategory, VATPostingSetup, 120, 0, 0, 0); + CurrencyCode := LibraryERM.CreateCurrencyWithExchangeRate(WorkDate(), 1, 1); + LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", CurrencyCode, Expense."VAT Bus. Posting Group"); + AddExpensesToReport(CreateExpenseReport, ExpenseReportHeader, Expense."No."); + UpdateExpenseReportLinesWithVendor(ExpenseReportHeader); + + ExpenseReportLineVATSpec.SetRange("Document No.", ExpenseReportHeader."No."); + ExpenseReportLineVATSpec.FindFirst(); + InitialVATBaseAmountRCY := ExpenseReportLineVATSpec."VAT Base Amount (RCY)"; + InitialVATAmountRCY := ExpenseReportLineVATSpec."VAT Amount (RCY)"; + + ExpenseReportHeader.PerformManualRelease(); + ExpenseReportHeader.PerformManualReopen(ExpenseReportHeader); + ExpenseReportHeader.Get(ExpenseReportHeader."No."); + + // [WHEN] The reimbursement currency factor is changed to 2 after reopening the report. + LibraryVariableStorage.Enqueue(StrSubstNo(CanModifyLinesQst, ExpenseReportHeader.FieldCaption("Reimbursement Currency Factor"))); + LibraryVariableStorage.Enqueue(true); + ExpenseReportHeader.Validate("Reimbursement Currency Factor", 2); + ExpenseReportHeader.Modify(true); + LibraryVariableStorage.AssertEmpty(); + + // [THEN] The persisted VAT specification reimbursement amounts are recalculated using the new factor. + ExpenseReportLineVATSpec.Get( + ExpenseReportLineVATSpec."Document No.", ExpenseReportLineVATSpec."Document Line No.", ExpenseReportLineVATSpec."Line No."); + Assert.AreNearlyEqual(InitialVATBaseAmountRCY * 2, ExpenseReportLineVATSpec."VAT Base Amount (RCY)", 0.01, 'VAT base amount in reimbursement currency must be recalculated.'); + Assert.AreNearlyEqual(InitialVATAmountRCY * 2, ExpenseReportLineVATSpec."VAT Amount (RCY)", 0.01, 'VAT amount in reimbursement currency must be recalculated.'); + Assert.AreNearlyEqual((InitialVATBaseAmountRCY + InitialVATAmountRCY) * 2, ExpenseReportLineVATSpec."Amount (RCY)", 0.01, 'Gross amount in reimbursement currency must be recalculated.'); + Assert.AreNearlyEqual(InitialVATAmountRCY * 2, ExpenseReportLineVATSpec."Reclaim VAT Amount (RCY)", 0.01, 'Reclaim VAT amount in reimbursement currency must be recalculated.'); + 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 UpdateVATSpecificationPreservesSubcategoryForNonItemizedExpense() + var + Expense: Record Expense; + ExpenseCategory: Record "Expense Category"; + ExpenseSubCategory: array[2] of Record "Expense Subcategory"; + ExpenseUser: Record "Expense User"; + ExpenseVATSpecification: Record "Expense VAT Specification"; + VATPostingSetup: array[2] of Record "VAT Posting Setup"; + begin + // [SCENARIO] Updating the VAT specification for a non-itemized expense preserves its subcategory VAT defaults. + Initialize(); + + // [GIVEN] A non-itemized category with 10% VAT and a selected subcategory with 20% VAT. + LibraryExpense.CreateExpenseUser(ExpenseUser); + LibraryExpense.CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); + CreateSubcategoryWithVATRate(ExpenseSubCategory[1], ExpenseCategory.Code, 10, VATPostingSetup[1]); + CreateSubcategoryWithVATRate(ExpenseSubCategory[2], ExpenseCategory.Code, 20, VATPostingSetup[2]); + ExpenseCategory.Validate("VAT Prod. Posting Group", VATPostingSetup[1]."VAT Prod. Posting Group"); + ExpenseCategory.Validate("Default VAT %", VATPostingSetup[1]."VAT %"); + ExpenseCategory.Modify(true); + LibraryExpense.CreateExpense(Expense, ExpenseUser."No.", ExpenseCategory.Code, ExpenseSubCategory[2].Code, '', true, '', 120); + + // [WHEN] The VAT specification is updated. + Expense.UpdateVATSpecification(Expense."No."); + + // [THEN] The generated row retains the selected subcategory and uses its VAT defaults for the amounts. + ExpenseVATSpecification.SetRange("Expense No.", Expense."No."); + Assert.RecordCount(ExpenseVATSpecification, 1); + ExpenseVATSpecification.FindFirst(); + Assert.AreEqual(ExpenseCategory.Code, ExpenseVATSpecification."Expense Category", 'The expense category must be copied.'); + Assert.AreEqual(ExpenseSubCategory[2].Code, ExpenseVATSpecification."Expense Subcategory", 'The expense subcategory must be copied.'); + Assert.AreEqual(VATPostingSetup[2]."VAT Prod. Posting Group", ExpenseVATSpecification."VAT Prod. Posting Group", 'The subcategory VAT product posting group must be used.'); + Assert.AreNearlyEqual(20, ExpenseVATSpecification."VAT %", 0.01, 'The subcategory VAT percentage must be used.'); + Assert.AreNearlyEqual(100, ExpenseVATSpecification."VAT Base Amount", 0.01, 'The VAT base amount must use the subcategory VAT percentage.'); + Assert.AreNearlyEqual(20, ExpenseVATSpecification."VAT Amount", 0.01, 'The VAT amount must use the subcategory VAT percentage.'); + 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 AgentVATSpecificationAPIAcceptsNonSaaSCaller() + var + Expense: Record Expense; + ExpenseVATSpecification: Record "Expense VAT Specification"; + EnvironmentInfo: Codeunit "Environment Information"; + ExpenseVATSpecAPI: Page "Expense VAT Spec. API"; + begin + // [SCENARIO] The Expense VAT Specification API accepts callers outside SaaS for local agent development. + if EnvironmentInfo.IsSaaSInfrastructure() then + exit; + + // [GIVEN] A VAT specification whose relational fields have been validated against a persisted expense. + Initialize(); + CreateAgentVATSpecificationFixture(ExpenseVATSpecification, Expense); + + // [WHEN] A caller attempts to insert an agent-authored VAT specification through the API. + ExpenseVATSpecAPI.InsertVATSpecification(ExpenseVATSpecification); + + // [THEN] The agent-authored VAT specification is inserted. + Assert.AreEqual(ExpenseVATSpecification.Source::Agent, ExpenseVATSpecification.Source, 'The VAT specification source must be Agent.'); + Assert.IsTrue(ExpenseVATSpecification.Get(Expense."No.", ExpenseVATSpecification."Line No."), 'The VAT specification must be inserted for the parent expense.'); + end; + + [Test] + procedure AgentVATSpecificationInsertAcceptsNonSaaSCaller() + var + Expense: Record Expense; + ExpenseVATSpecification: Record "Expense VAT Specification"; + EnvironmentInfo: Codeunit "Environment Information"; + begin + // [SCENARIO] The table authorization subscriber accepts agent-authored VAT specifications outside SaaS. + if EnvironmentInfo.IsSaaSInfrastructure() then + exit; + + // [GIVEN] A VAT specification whose relational fields have been validated against a persisted expense. + Initialize(); + CreateAgentVATSpecificationFixture(ExpenseVATSpecification, Expense); + ExpenseVATSpecification.Source := ExpenseVATSpecification.Source::Agent; + + // [WHEN] A caller attempts to insert an agent-authored VAT specification with table triggers. + ExpenseVATSpecification.Insert(true); + + // [THEN] The agent-authored VAT specification is inserted. + Assert.IsTrue(ExpenseVATSpecification.Get(Expense."No.", ExpenseVATSpecification."Line No."), 'The VAT specification must be inserted for the parent expense.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoCommit)] + 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.SetRange("Expense No.", Expense."No."); + ExpenseVATSpecification.DeleteAll(false); + ExpenseVATSpecification.Init(); + ExpenseVATSpecification."Expense No." := Expense."No."; + ExpenseVATSpecification."Line No." := 1; + ExpenseVATSpecification.Source := ExpenseVATSpecification.Source::Manual; + ExpenseVATSpecification."VAT Bus. Posting Group" := VATPostingSetup."VAT Bus. Posting Group"; + ExpenseVATSpecification."VAT Prod. Posting Group" := VATPostingSetup."VAT Prod. Posting Group"; + ExpenseVATSpecification.Amount := 120; + ExpenseVATSpecification.Insert(true); + ExpenseVATSpecification.Source := ExpenseVATSpecification.Source::Agent; + ExpenseVATSpecification.Modify(false); + 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.'); + ExpenseVATSpecification.Delete(false); + 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"; VATBusinessPostingGroup: Record "VAT Business Posting Group"; - VATSetup: Record "VAT Setup"; LibraryERMCountryData: Codeunit "Library - ERM Country Data"; begin + LibraryVariableStorage.Clear(); LibraryTestInitialize.OnTestInitialize(Codeunit::"Expense Posting VAT Test"); LibraryExpense.CleanUpBeforeTesting(); LibraryExpense.CleanTransactionalData(); @@ -422,10 +1242,6 @@ codeunit 148330 "Expense Posting VAT Test" ExpenseAgentSetup.Validate("Default VAT Bus. Posting Group", VATBusinessPostingGroup.Code); ExpenseAgentSetup.Modify(true); - VATSetup.Get(); - VATSetup.Validate("Non-Deductible VAT Is Enabled", true); - VATSetup.Modify(true); - LibraryERMCountryData.UpdateJournalTemplMandatory(false); LibraryERMCountryData.UpdateLocalData(); LibraryExpense.SetupNumberSeriesInExpenseMgmt(); @@ -549,10 +1365,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 +1377,21 @@ 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 + LibraryVariableStorage.Enqueue(ExpenseNo); + CreateExpenseReport.AddExpensesToReport(ExpenseReportHeader); + LibraryVariableStorage.AssertEmpty(); + end; + + local procedure PostExpenseReportWithConfirmation(var ExpenseReportPost: Codeunit "Expense Report-Post"; var ExpenseReportHeader: Record "Expense Report Header") + begin + LibraryVariableStorage.Enqueue(ExpenseReportHeader."No."); + LibraryVariableStorage.Enqueue(true); + ExpenseReportPost.PostExpenseReport(ExpenseReportHeader); + LibraryVariableStorage.AssertEmpty(); + end; + local procedure GetRefundableDebitAccount(ExpenseCategoryCode: Code[20]): Code[20] var ExpenseCategory: Record "Expense Category"; @@ -647,19 +1478,49 @@ codeunit 148330 "Expense Posting VAT Test" local procedure CreateExpenseVATSpecification(Expense: Record Expense; ExpenseCategoryCode: Code[20]; ExpenseSubcategoryCode: Code[20]; Amount: Decimal) var ExpenseVATSpecification: Record "Expense VAT Specification"; + ExpenseSubcategory: Record "Expense Subcategory"; 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."))); ExpenseVATSpecification.Validate("VAT Bus. Posting Group", Expense."VAT Bus. Posting Group"); ExpenseVATSpecification.Validate("Expense Category", ExpenseCategoryCode); ExpenseVATSpecification.Validate("Expense Subcategory", ExpenseSubcategoryCode); + if ExpenseSubcategoryCode <> '' then begin + ExpenseSubcategory.Get(ExpenseCategoryCode, ExpenseSubcategoryCode); + ExpenseVATSpecification.Validate("VAT Prod. Posting Group", ExpenseSubcategory."VAT Prod. Posting Group"); + end; ExpenseVATSpecification.Validate(Amount, Amount); ExpenseVATSpecification.Insert(true); end; + local procedure CreateAgentVATSpecificationFixture(var ExpenseVATSpecification: Record "Expense VAT Specification"; var Expense: Record Expense) + var + ExpenseCategory: Record "Expense Category"; + ExpenseSubcategory: Record "Expense Subcategory"; + ExpenseUser: Record "Expense User"; + VATPostingSetup: Record "VAT Posting Setup"; + begin + 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, ExpenseSubcategory.Code, '', true, '', 120); + Expense.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); + Expense.Validate("VAT Prod. Posting Group", VATPostingSetup."VAT Prod. Posting Group"); + Expense.Modify(true); + + ExpenseVATSpecification.Init(); + ExpenseVATSpecification.Validate("Expense No.", Expense."No."); + ExpenseVATSpecification.Validate("Expense Category", ExpenseCategory.Code); + ExpenseVATSpecification.Validate("Expense Subcategory", ExpenseSubcategory.Code); + ExpenseVATSpecification.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); + ExpenseVATSpecification.Validate("VAT Prod. Posting Group", VATPostingSetup."VAT Prod. Posting Group"); + ExpenseVATSpecification.Validate(Amount, Expense.Amount); + end; + local procedure UpdateExpenseReportLinesWithVendor(ExpenseReportHeader: Record "Expense Report Header") var ExpenseReportLine: Record "Expense Report Line"; @@ -747,7 +1608,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 +1696,14 @@ codeunit 148330 "Expense Posting VAT Test" [ModalPageHandler] procedure ExpensesModalPageHandler(var Expenses: TestPage Expenses) begin + Expenses."No.".AssertEquals(LibraryVariableStorage.DequeueText()); Expenses.OK().Invoke(); end; [ConfirmHandler] procedure ConfirmHandler(Question: Text[1024]; var Reply: Boolean) begin - Reply := true; + Assert.ExpectedConfirm(LibraryVariableStorage.DequeueText(), Question); + Reply := LibraryVariableStorage.DequeueBoolean(); end; } diff --git a/src/Apps/W1/ExpenseAgent/test/src/ExpenseReportTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/ExpenseReportTest.Codeunit.al index 99aa4094cd8..5d5c92f96a0 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/ExpenseReportTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/ExpenseReportTest.Codeunit.al @@ -3301,11 +3301,63 @@ 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 original subcategory suffix was truncated by the description length limit. + BaseDescription := PadStr('', 95, 'B'); + ExpenseSubcategory[1]."Posting Description" := 'OLD SUFFIX'; + ExpenseSubcategory[1].Modify(); + ExpenseSubcategory[2]."Posting Description" := 'NEW SUFFIX'; + ExpenseSubcategory[2].Modify(); + ExpenseReportLine.Description := CopyStr(BaseDescription + ' / ' + ExpenseSubcategory[1]."Posting Description", 1, MaxStrLen(ExpenseReportLine.Description)); + + // [THEN] The stored part of the old suffix is removed before the new suffix is applied. + Assert.AreEqual( + CopyStr(BaseDescription + ' / ' + ExpenseSubcategory[2]."Posting Description", 1, MaxStrLen(ExpenseReportLine.Description)), + ExpenseReportLine.UpdatePostingDescription(ExpenseCategory.Code, ExpenseSubcategory[2].Code), + 'The truncated posting-description suffix must be replaced.'); + + // [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 @@ -4535,7 +4587,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. @@ -4752,7 +4804,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. @@ -5177,7 +5229,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(); @@ -5204,7 +5256,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); @@ -5227,7 +5279,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(); @@ -5254,7 +5306,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); @@ -6219,8 +6271,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. @@ -6235,8 +6287,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); @@ -6387,9 +6439,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". @@ -6791,10 +6843,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 @@ -6913,19 +6965,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..8e3bd4e6932 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. @@ -8442,11 +8442,16 @@ codeunit 148301 "Expense Rule Test" local procedure Initialize() var + ExpenseAgentSetup: Record "Expense Agent Setup"; Workflow: Record Workflow; LibraryERMCountryData: Codeunit "Library - ERM Country Data"; begin LibraryTestInitialize.OnTestInitialize(Codeunit::"Expense Rule Test"); + LibraryExpense.CleanTransactionalData(); LibraryExpense.CleanUpBeforeTesting(); + ExpenseAgentSetup.Get(); + ExpenseAgentSetup."Default VAT Bus. Posting Group" := ''; + ExpenseAgentSetup.Modify(); LibraryVariableStorage.Clear(); if IsInitialized then exit; 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."); diff --git a/src/Apps/W1/ExpenseAgent/test/src/LibraryExpense.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/LibraryExpense.Codeunit.al index cad0e6300ce..924233820da 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/LibraryExpense.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/LibraryExpense.Codeunit.al @@ -638,6 +638,7 @@ codeunit 148300 "Library - Expense" ExpenseItemization: Record "Expense Itemization"; ExpenseParticipant: Record "Expense Participant"; ExpensePerDiem: Record "Expense Per Diem"; + ExpenseVATSpecification: Record "Expense VAT Specification"; ExpenseReportHeader: Record "Expense Report Header"; ExpenseReportLine: Record "Expense Report Line"; ExpenseReportLineItem: Record "Expense Report Line Item"; @@ -663,6 +664,7 @@ codeunit 148300 "Library - Expense" ExpenseParticipant.DeleteAll(); ExpenseItemization.DeleteAll(); ExpensePerDiem.DeleteAll(); + ExpenseVATSpecification.DeleteAll(false); Expense.DeleteAll(); ExpenseRuleViolation.DeleteAll(); diff --git a/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json b/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json index 44be58a7731..27cd512e9a1 100644 --- a/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json +++ b/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json @@ -203,5 +203,10 @@ "codeunitId": 148319, "codeunitName": "Cancel Posted Exp. Report Test", "method": "CancelMultiLineReportGeneratesNewTransactionNoForReversedLines" + }, + { + "codeunitId": 148348, + "codeunitName": "Expense VAT Spec. API Test", + "method": "AgentVATSpecificationIsInsertedThroughAPI" } ]