From 73d2d31932b90289c0dfb346c0ed7918e48334ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Fri, 21 Aug 2026 18:05:10 +0200 Subject: [PATCH 1/9] E-Documents: support one-to-many PO matching --- .../EDocCreatePurchaseInvoice.Codeunit.al | 54 +- .../Purchase/EDocPurchaseDraftSubform.Page.al | 30 +- .../EDocPOMatchWarning.Enum.al | 3 + .../EDocPOMatching.Codeunit.al | 250 ++++++++-- .../EDocSelectPOLines.Page.al | 20 +- .../EDocPOMatchingUnitTests.Codeunit.al | 472 +++++++++++++++++- 6 files changed, 701 insertions(+), 128 deletions(-) diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchaseInvoice.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchaseInvoice.Codeunit.al index 9972681efb4..1f9aff60a9c 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchaseInvoice.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchaseInvoice.Codeunit.al @@ -35,20 +35,11 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, EDocImpSessionTelemetry: Codeunit "E-Doc. Imp. Session Telemetry"; EmptyRecordId: RecordId; IEDocumentFinishPurchaseDraft: Interface IEDocumentCreatePurchaseInvoice; - YourMatchedLinesAreNotValidErr: Label 'The purchase invoice cannot be created because one or more of its matched lines are not valid matches. Review if your configuration allows for receiving at invoice.'; - SomeLinesNotYetReceivedErr: Label 'Some of the matched purchase order lines have not yet been received, you need to either receive the lines or remove the matches.'; MissingInformationForMatchErr: Label 'Some of the draft lines that were matched to purchase order lines are missing unit of measure information. Please specify the unit of measure for those lines and try again.'; begin EDocumentPurchaseHeader.GetFromEDocument(EDocument); - if not EDocPOMatching.VerifyEDocumentMatchedLinesAreValidMatches(EDocumentPurchaseHeader) then - Error(YourMatchedLinesAreNotValidErr); - - EDocPOMatching.SuggestReceiptsForMatchedOrderLines(EDocumentPurchaseHeader); EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); - TempPOMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::ExceedsInvoiceableQty); - if not TempPOMatchWarnings.IsEmpty() then - Error(SomeLinesNotYetReceivedErr); TempPOMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::MissingInformationForMatch); if not TempPOMatchWarnings.IsEmpty() then Error(MissingInformationForMatchErr); @@ -88,17 +79,12 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, VendorLedgerEntry: Record "Vendor Ledger Entry"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; - PurchaseLine: Record "Purchase Line"; EDocRecordLink: Record "E-Doc. Record Link"; EDocPurchaseDocumentHelper: Codeunit "E-Doc. Purch. Doc. Helper"; PurchCalcDiscByType: Codeunit "Purch - Calc Disc. By Type"; - EDocLineByReceipt: Query "E-Doc. Line by Receipt"; - LastReceiptNo: Code[20]; PurchaseLineNo: Integer; StopCreatingPurchaseInvoice: Boolean; VendorInvoiceNo: Code[35]; - ReceiptNoLbl: Label 'Receipt No. %1:', Comment = '%1 = Receipt No.'; - NullGuid: Guid; begin EDocumentPurchaseHeader.GetFromEDocument(EDocument); if not EDocPurchaseDocumentHelper.AllDraftLinesHaveTypeAndNumber(EDocumentPurchaseHeader) then begin @@ -142,41 +128,13 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, end; EDocRecordLink.InsertEDocumentHeaderLink(EDocumentPurchaseHeader, PurchaseHeader); - PurchaseLineNo := EDocPurchaseDocumentHelper.GetLastPurchaseLineNo("Purchase Document Type"::Invoice, PurchaseHeader."No."); // We get the last line number, even if this is a new document since recurrent lines get inserted on the header's creation - // We create first the lines without any PO matches - EDocLineByReceipt.SetRange(EDocumentEntryNo, EDocument."Entry No"); - EDocLineByReceipt.SetRange(ReceiptNo, ''); - EDocLineByReceipt.SetRange(PurchaseLineSystemId, NullGuid); - EDocLineByReceipt.Open(); - while EDocLineByReceipt.Read() do begin - EDocumentPurchaseLine.GetBySystemId(EDocLineByReceipt.SystemId); - PurchaseLineNo += 10000; - EDocPurchaseDocumentHelper.CreatePurchaseLineFromDraft(PurchaseHeader, EDocumentPurchaseLine, EDocumentPurchaseHeader."Total Discount" > 0, PurchaseLineNo); - end; - EDocLineByReceipt.Close(); - - // Then we create the lines with receipt no., adding comment lines for each receipt no. - LastReceiptNo := ''; - EDocLineByReceipt.SetFilter(ReceiptNo, '<> %1', ''); - EDocLineByReceipt.SetRange(PurchaseLineSystemId); - EDocLineByReceipt.Open(); - while EDocLineByReceipt.Read() do begin - if LastReceiptNo <> EDocLineByReceipt.ReceiptNo then begin // A receipt no. for which we have not created a header comment line yet - Clear(PurchaseLine); - PurchaseLine."Document Type" := PurchaseHeader."Document Type"; - PurchaseLine."Document No." := PurchaseHeader."No."; + PurchaseLineNo := EDocPurchaseDocumentHelper.GetLastPurchaseLineNo("Purchase Document Type"::Invoice, PurchaseHeader."No."); + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); + if EDocumentPurchaseLine.FindSet() then + repeat PurchaseLineNo += 10000; - PurchaseLine."Line No." := PurchaseLineNo; - PurchaseLine.Type := PurchaseLine.Type::" "; - PurchaseLine.Description := StrSubstNo(ReceiptNoLbl, EDocLineByReceipt.ReceiptNo); - PurchaseLine.Insert(); - end; - EDocumentPurchaseLine.GetBySystemId(EDocLineByReceipt.SystemId); - PurchaseLineNo += 10000; - EDocPurchaseDocumentHelper.CreatePurchaseLineFromDraft(PurchaseHeader, EDocumentPurchaseLine, EDocumentPurchaseHeader."Total Discount" > 0, PurchaseLineNo); - LastReceiptNo := EDocLineByReceipt.ReceiptNo; - end; - EDocLineByReceipt.Close(); + EDocPurchaseDocumentHelper.CreatePurchaseLineFromDraft(PurchaseHeader, EDocumentPurchaseLine, EDocumentPurchaseHeader."Total Discount" > 0, PurchaseLineNo); + until EDocumentPurchaseLine.Next() = 0; PurchaseHeader.Modify(); PurchCalcDiscByType.ApplyInvDiscBasedOnAmt(EDocumentPurchaseHeader."Total Discount", PurchaseHeader); EDocPurchaseDocumentHelper.ApplyVATDifferenceToLines(PurchaseHeader, EDocumentPurchaseHeader); diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al index 5447da455c8..d8ac8d90221 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al @@ -56,7 +56,7 @@ page 6183 "E-Doc. Purchase Draft Subform" field(MatchWarnings; MatchWarningsCaption) { ApplicationArea = All; - Caption = 'Order match warnings'; + Caption = 'Warnings'; Editable = false; Visible = HasEDocumentOrderMatchWarnings; StyleExpr = MatchWarningsStyleExpr; @@ -201,9 +201,9 @@ page 6183 "E-Doc. Purchase Draft Subform" action(MatchToOrderLine) { ApplicationArea = All; - Caption = 'Match to order line'; + Caption = 'Match to order lines'; Image = LinkWithExisting; - ToolTip = 'Match this incoming invoice line to a purchase order line.'; + ToolTip = 'Matches this incoming invoice line to purchase order lines.'; Scope = Repeater; trigger OnAction() @@ -224,9 +224,9 @@ page 6183 "E-Doc. Purchase Draft Subform" action(SpecifyReceiptLines) { ApplicationArea = All; - Caption = 'Specify receipt line'; + Caption = 'Specify receipt lines'; Image = ReceiptLines; - ToolTip = 'Specify the corresponding receipt line to the matched order line.'; + ToolTip = 'Specifies the corresponding receipt lines for the matched order lines.'; Scope = Repeater; Enabled = IsLineMatchedToOrderLine; @@ -248,9 +248,9 @@ page 6183 "E-Doc. Purchase Draft Subform" action(OpenMatchedOrder) { ApplicationArea = All; - Caption = 'Open matched order'; + Caption = 'Open matched orders'; Image = ViewOrder; - ToolTip = 'Opens the matched purchase order.'; + ToolTip = 'Opens the matched purchase orders.'; Scope = Repeater; Enabled = IsLineMatchedToOrderLine; @@ -262,9 +262,9 @@ page 6183 "E-Doc. Purchase Draft Subform" action(OpenMatchedReceipt) { ApplicationArea = All; - Caption = 'Open matched receipt'; + Caption = 'Open matched receipts'; Image = PostedReceipt; - ToolTip = 'Opens the matched purchase receipt.'; + ToolTip = 'Opens the matched purchase receipts.'; Scope = Repeater; Enabled = IsLineMatchedToReceiptLine; @@ -583,7 +583,7 @@ page 6183 "E-Doc. Purchase Draft Subform" Page.Run(Page::"Purchase Order", PurchaseOrder); exit; end; - Page.Run(Page::"Purchase Orders", TempPurchaseOrders); + Page.Run(Page::"Purchase Order List", TempPurchaseOrders); end; local procedure UpdatePOMatching() @@ -623,6 +623,7 @@ page 6183 "E-Doc. Purchase Draft Subform" ExceedsInvoiceableQtyLbl: Label 'Exceeds quantity received'; ExceedsRemainingToInvoiceLbl: Label 'Exceeds remaining to invoice'; OverReceiptLbl: Label 'Over-receipt'; + PriceDifferenceLbl: Label 'Price difference'; NoWarningsLbl: Label 'No warnings'; MultipleWarningsLbl: Label 'Multiple warnings'; MostSevereStyle: Text; @@ -663,6 +664,12 @@ page 6183 "E-Doc. Purchase Draft Subform" MatchWarningsCaption := OverReceiptLbl; MostSevereStyle := 'Subordinate'; end; + Enum::"E-Doc PO Match Warning"::AmountMismatch: + begin + CurrentSeverity := 2; + MatchWarningsCaption := PriceDifferenceLbl; + MostSevereStyle := 'Ambiguous'; + end; end; if CurrentSeverity > SeverityLevel then begin SeverityLevel := CurrentSeverity; @@ -689,7 +696,8 @@ page 6183 "E-Doc. Purchase Draft Subform" WarningDetails.AppendLine('• ' + MissingInfoDetailLbl); Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty, Enum::"E-Doc PO Match Warning"::ExceedsRemainingToInvoice, - Enum::"E-Doc PO Match Warning"::OverReceipt: + Enum::"E-Doc PO Match Warning"::OverReceipt, + Enum::"E-Doc PO Match Warning"::AmountMismatch: WarningDetails.AppendLine('• ' + TempEDocumentPOMatchWarnings."Warning Message"); end; until TempEDocumentPOMatchWarnings.Next() = 0; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Enum.al index c763fcde257..ae97b9abfe8 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Enum.al @@ -25,4 +25,7 @@ enum 6111 "E-Doc PO Match Warning" value(5; OverReceipt) { } + value(6; AmountMismatch) + { + } } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al index 1aae56fe467..b8bed9bcdfc 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al @@ -5,9 +5,13 @@ namespace Microsoft.eServices.EDocument.Processing.Import.Purchase; using Microsoft.eServices.EDocument; +using Microsoft.Finance.GeneralLedger.Setup; +using Microsoft.Foundation.UOM; using Microsoft.Inventory.Item; +using Microsoft.Inventory.Tracking; using Microsoft.Purchases.Document; using Microsoft.Purchases.History; +using Microsoft.Purchases.Setup; using Microsoft.Purchases.Vendor; codeunit 6196 "E-Doc. PO Matching" @@ -41,7 +45,7 @@ codeunit 6196 "E-Doc. PO Matching" PurchaseLine.SetRange("Pay-to Vendor No.", Vendor."No."); if EDocumentPurchaseLine."[BC] Unit of Measure" <> '' then PurchaseLine.SetRange("Unit of Measure Code", EDocumentPurchaseLine."[BC] Unit of Measure"); - PurchaseLine.SetLoadFields("Document No.", "Line No.", Description, Quantity, "Qty. Invoiced (Base)", "Qty. Received (Base)", Type, "No.", "Quantity Received", "Quantity Invoiced"); + PurchaseLine.SetLoadFields("Document No.", "Line No.", Description, Quantity, "Qty. Invoiced (Base)", "Qty. Received (Base)", Type, "No.", "Quantity Received", "Quantity Invoiced", "Direct Unit Cost", "Line Discount %", "Currency Code", "Expected Receipt Date"); if PurchaseLine.FindSet() then repeat // We exclude lines that have already been matched unless they were matched to the current line @@ -214,9 +218,11 @@ codeunit 6196 "E-Doc. PO Matching" EDocLineQuantity: Decimal; PurchaseLinesQuantity, PurchaseLinesQuantityInvoiced, PurchaseLinesQuantityReceived : Decimal; RemainingToInvoice, InvoiceableQty : Decimal; + EDocNetUnitCost, ExpectedPONetUnitCost, AmountPctDiff, AmountThreshold : Decimal; ExceedsInvoiceableQtyLbl: Label 'Invoice quantity (%1) exceeds what can be invoiced according to what has been received (%2) by %3. The order line has to be received before invoicing.', Comment = '%1 = Invoice qty, %2 = Invoiceable qty, %3 = Difference'; ExceedsRemainingToInvoiceLbl: Label 'Invoice quantity (%1) exceeds what is missing to invoice from the order (%2) by %3.', Comment = '%1 = Invoice qty, %2 = Remaining to invoice, %3 = Difference'; OverReceiptLbl: Label 'Invoice will close out order but there is an over-receipt of %1 units.', Comment = '%1 = Over-receipt quantity'; + AmountMismatchLbl: Label 'Invoiced unit cost (%1) differs from the order''s unit cost (%2) by %3%, which exceeds the allowed %4%.', Comment = '%1 = Invoiced net unit cost, %2 = Order net unit cost, %3 = Actual % difference, %4 = Allowed % tolerance'; begin LoadPOLinesMatchedToEDocumentLine(EDocumentPurchaseLine, TempPurchaseLine); PurchaseLinesQuantityInvoiced := 0; @@ -267,6 +273,65 @@ codeunit 6196 "E-Doc. PO Matching" POMatchWarnings."Warning Message" := CopyStr(StrSubstNo(OverReceiptLbl, InvoiceableQty - RemainingToInvoice), 1, MaxStrLen(POMatchWarnings."Warning Message")); POMatchWarnings.Insert(); end; + + if ShouldWarnAmountMismatch(EDocumentPurchaseLine, TempPurchaseLine, EDocNetUnitCost, ExpectedPONetUnitCost, AmountPctDiff, AmountThreshold) then begin + POMatchWarnings."E-Doc. Purchase Line SystemId" := EDocumentPurchaseLine.SystemId; + POMatchWarnings."Warning Type" := "E-Doc PO Match Warning"::AmountMismatch; + POMatchWarnings."Warning Message" := CopyStr(StrSubstNo(AmountMismatchLbl, EDocNetUnitCost, ExpectedPONetUnitCost, Round(AmountPctDiff, 0.1), AmountThreshold), 1, MaxStrLen(POMatchWarnings."Warning Message")); + POMatchWarnings.Insert(); + end; + end; + + local procedure ShouldWarnAmountMismatch(EDocumentPurchaseLine: Record "E-Document Purchase Line"; var TempPurchaseLine: Record "Purchase Line" temporary; var EDocNetUnitCost: Decimal; var ExpectedPONetUnitCost: Decimal; var PctDiff: Decimal; var Threshold: Decimal): Boolean + var + PurchasesPayablesSetup: Record "Purchases & Payables Setup"; + GeneralLedgerSetup: Record "General Ledger Setup"; + EDocumentImportHelper: Codeunit "E-Document Import Helper"; + WeightedNetCostNumerator, TotalPOQuantity, AbsDiff, RoundingFloor : Decimal; + EDocCurrencyCode: Code[10]; + begin + Clear(EDocNetUnitCost); + Clear(ExpectedPONetUnitCost); + Clear(PctDiff); + Clear(Threshold); + if EDocumentPurchaseLine.Quantity = 0 then + exit(false); + if not TempPurchaseLine.FindSet() then + exit(false); + + GeneralLedgerSetup.Get(); + EDocCurrencyCode := NormalizeEmptyCurrencyCode(EDocumentPurchaseLine."Currency Code", GeneralLedgerSetup); + repeat + if EDocCurrencyCode <> NormalizeEmptyCurrencyCode(TempPurchaseLine."Currency Code", GeneralLedgerSetup) then + exit(false); + WeightedNetCostNumerator += TempPurchaseLine."Direct Unit Cost" * (1 - TempPurchaseLine."Line Discount %" / 100) * TempPurchaseLine.Quantity; + TotalPOQuantity += TempPurchaseLine.Quantity; + until TempPurchaseLine.Next() = 0; + if TotalPOQuantity = 0 then + exit(false); + + EDocNetUnitCost := (EDocumentPurchaseLine.Quantity * EDocumentPurchaseLine."Unit Price" - EDocumentPurchaseLine."Total Discount") / EDocumentPurchaseLine.Quantity; + ExpectedPONetUnitCost := WeightedNetCostNumerator / TotalPOQuantity; + if EDocNetUnitCost <= 0 then + exit(ExpectedPONetUnitCost > 0); + if ExpectedPONetUnitCost <= 0 then + exit(false); + if PurchasesPayablesSetup.Get() then + Threshold := PurchasesPayablesSetup."E-Document Matching Difference"; + + AbsDiff := Abs(ExpectedPONetUnitCost - EDocNetUnitCost); + RoundingFloor := EDocumentImportHelper.GetCurrencyRoundingPrecision(EDocumentPurchaseLine."Currency Code"); + if AbsDiff <= RoundingFloor then + exit(false); + PctDiff := AbsDiff * 100 / ExpectedPONetUnitCost; + exit(PctDiff > Threshold); + end; + + local procedure NormalizeEmptyCurrencyCode(CurrencyCode: Code[10]; GeneralLedgerSetup: Record "General Ledger Setup"): Code[10] + begin + if CurrencyCode = '' then + exit(GeneralLedgerSetup."LCY Code"); + exit(CurrencyCode); end; /// @@ -431,22 +496,19 @@ codeunit 6196 "E-Doc. PO Matching" PurchaseLine: Record "Purchase Line"; Vendor: Record Vendor; EDocPurchaseLinePOMatch: Record "E-Doc. Purchase Line PO Match"; - TempMatchWarnings: Record "E-Doc PO Match Warning" temporary; - MatchesToMultiplePOLinesNotSupportedErr: Label 'Matching an e-document line to multiple purchase order lines is not currently supported.'; NotLinkedToVendorErr: Label 'The selected purchase order line is not linked to the same vendor as the e-document line.'; AlreadyMatchedErr: Label 'A selected purchase order line is already matched to another e-document line. E-Document: %1, Purchase document: %2 %3.', Comment = '%1 - E-Document No., %2 - Purchase Document Type, %3 - Purchase Document No.'; OrderLineAndEDocFromDifferentVendorsErr: Label 'All selected purchase order lines must belong to orders for the same vendor as the e-document line.'; OrderLinesMustBeOfSameTypeAndNoErr: Label 'All selected purchase order lines must be of the same type and number.'; - NotYetReceivedErr: Label 'The selected purchase order lines are not yet received with the quantity of the invoice. You must first receive them before matching them.'; OrderLinesMustHaveSameUoMErr: Label 'All selected purchase order lines must have the same unit of measure.'; MatchedPOLineType: Enum "Purchase Line Type"; MatchedPOLineVendorNo, MatchedPOLineTypeNo, MatchedUnitOfMeasure : Code[20]; + MatchedShortcutDimension1Code, MatchedShortcutDimension2Code : Code[20]; + MatchedDimensionSetID: Integer; FirstOfLinesBeingMatched: Boolean; begin if SelectedPOLines.IsEmpty() then exit; - if SelectedPOLines.Count() > 1 then - Error(MatchesToMultiplePOLinesNotSupportedErr); RemoveAllMatchesForEDocumentLine(EDocumentPurchaseLine); FirstOfLinesBeingMatched := true; MatchedPOLineVendorNo := ''; @@ -455,7 +517,7 @@ codeunit 6196 "E-Doc. PO Matching" if SelectedPOLines.FindSet() then repeat // Create new matches, if each line being matched is valid - PurchaseLine.SetLoadFields("Document Type", "No.", "Line No.", "Pay-to Vendor No.", Type); + PurchaseLine.SetLoadFields("Document Type", "No.", "Line No.", "Pay-to Vendor No.", Type, "Unit of Measure Code", "Dimension Set ID", "Shortcut Dimension 1 Code", "Shortcut Dimension 2 Code"); PurchaseLine.GetBySystemId(SelectedPOLines.SystemId); PurchaseLine.TestField("Document Type", PurchaseLine."Document Type"::Order); PurchaseLine.TestField("No."); // The line must have been assigned a number for it's purchase type @@ -476,6 +538,9 @@ codeunit 6196 "E-Doc. PO Matching" MatchedPOLineTypeNo := PurchaseLine."No."; MatchedPOLineVendorNo := PurchaseLine."Pay-to Vendor No."; MatchedUnitOfMeasure := PurchaseLine."Unit of Measure Code"; + MatchedShortcutDimension1Code := PurchaseLine."Shortcut Dimension 1 Code"; + MatchedShortcutDimension2Code := PurchaseLine."Shortcut Dimension 2 Code"; + MatchedDimensionSetID := PurchaseLine."Dimension Set ID"; FirstOfLinesBeingMatched := false; end else begin if PurchaseLine.Type <> MatchedPOLineType then @@ -497,11 +562,10 @@ codeunit 6196 "E-Doc. PO Matching" EDocumentPurchaseLine."[BC] Purchase Line Type" := MatchedPOLineType; EDocumentPurchaseLine."[BC] Purchase Type No." := MatchedPOLineTypeNo; EDocumentPurchaseLine."[BC] Unit of Measure" := MatchedUnitOfMeasure; + EDocumentPurchaseLine."[BC] Shortcut Dimension 1 Code" := MatchedShortcutDimension1Code; + EDocumentPurchaseLine."[BC] Shortcut Dimension 2 Code" := MatchedShortcutDimension2Code; + EDocumentPurchaseLine."[BC] Dimension Set ID" := MatchedDimensionSetID; EDocumentPurchaseLine.Modify(); - AppendPOMatchWarnings(EDocumentPurchaseLine, TempMatchWarnings); - TempMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::ExceedsInvoiceableQty); - if (not TempMatchWarnings.IsEmpty) and (not CanMatchInvoiceLineToPOLineWithoutReceipt(EDocumentPurchaseLine, PurchaseLine)) then - Error(NotYetReceivedErr); end; /// @@ -513,19 +577,21 @@ codeunit 6196 "E-Doc. PO Matching" /// /// procedure MatchReceiptLinesToEDocumentLine(var SelectedReceiptLines: Record "Purch. Rcpt. Line" temporary; EDocumentPurchaseLine: Record "E-Document Purchase Line") + begin + MatchReceiptLinesToEDocumentLine(SelectedReceiptLines, EDocumentPurchaseLine, true); + end; + + local procedure MatchReceiptLinesToEDocumentLine(var SelectedReceiptLines: Record "Purch. Rcpt. Line" temporary; EDocumentPurchaseLine: Record "E-Document Purchase Line"; RequireFullCoverage: Boolean) var EDocPurchaseLinePOMatch: Record "E-Doc. Purchase Line PO Match"; TempMatchedPurchaseLines: Record "Purchase Line" temporary; NullGuid: Guid; ReceiptLineNotMatchedErr: Label 'A selected receipt line is not matched to any of the purchase order lines matched to the e-document line.'; ReceiptLinesDontCoverErr: Label 'The selected receipt lines do not cover the full quantity of the e-document line.'; - MatchesToMultipleReceiptLinesNotSupportedErr: Label 'Matching an e-document line to multiple receipt lines is not currently supported.'; QuantityCovered: Decimal; begin if SelectedReceiptLines.IsEmpty() then exit; - if SelectedReceiptLines.Count() > 1 then - Error(MatchesToMultipleReceiptLinesNotSupportedErr); // Remove existing receipt line matches EDocPurchaseLinePOMatch.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); @@ -547,7 +613,7 @@ codeunit 6196 "E-Doc. PO Matching" EDocPurchaseLinePOMatch.Insert(); QuantityCovered += SelectedReceiptLines.Quantity; until SelectedReceiptLines.Next() = 0; - if QuantityCovered < EDocumentPurchaseLine.Quantity then + if RequireFullCoverage and (QuantityCovered < EDocumentPurchaseLine.Quantity) then Error(ReceiptLinesDontCoverErr); end; @@ -687,24 +753,89 @@ codeunit 6196 "E-Doc. PO Matching" /// procedure TransferPOMatchesFromEDocumentToInvoice(EDocument: Record "E-Document") var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; - PurchaseLine: Record "Purchase Line"; - TempPurchaseReceiptLine: Record "Purch. Rcpt. Line" temporary; + EDocPurchaseLinePOMatch: Record "E-Doc. Purchase Line PO Match"; + InvoicePurchaseLine, OrderPurchaseLine : Record "Purchase Line"; + POMatching: Codeunit "PO Matching"; + POMatchingGroup: Codeunit "PO Matching Group"; + UnitOfMeasureMgt: Codeunit "Unit of Measure Management"; + RemainingBaseToDistribute, OrderAvailableBase, QtyToAllocateBase, QtyToAllocate, QtyPerUoM : Decimal; + NullGuid: Guid; + CantDetermineEDocLineBaseQtyErr: Label 'Could not determine the quantity in base unit of measure for line %1.', Comment = '%1 = E-Document Line No.'; begin + EDocumentPurchaseHeader.GetFromEDocument(EDocument); EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); if EDocumentPurchaseLine.FindSet() then repeat - LoadReceiptLinesMatchedToEDocumentLine(EDocumentPurchaseLine, TempPurchaseReceiptLine); - if not TempPurchaseReceiptLine.FindFirst() then // We only support a single receipt line match in BaseApp - continue; - PurchaseLine := EDocumentPurchaseLine.GetLinkedPurchaseLine(); - if IsNullGuid(PurchaseLine.SystemId) then - continue; - PurchaseLine."Receipt No." := TempPurchaseReceiptLine."Document No."; - PurchaseLine."Receipt Line No." := TempPurchaseReceiptLine."Line No."; - PurchaseLine.Modify(); - RemoveAllMatchesForEDocumentLine(EDocumentPurchaseLine); + InvoicePurchaseLine := EDocumentPurchaseLine.GetLinkedPurchaseLine(); + if not IsNullGuid(InvoicePurchaseLine.SystemId) then begin + EDocPurchaseLinePOMatch.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + EDocPurchaseLinePOMatch.SetRange("Receipt Line SystemId", NullGuid); + if EDocPurchaseLinePOMatch.FindSet() then begin + if not GetEDocumentLineQuantityInBaseUoM(EDocumentPurchaseLine, RemainingBaseToDistribute) then + Error(CantDetermineEDocLineBaseQtyErr, EDocumentPurchaseLine."Line No."); + repeat + if OrderPurchaseLine.GetBySystemId(EDocPurchaseLinePOMatch."Purchase Line SystemId") then begin + OrderAvailableBase := OrderPurchaseLine."Quantity (Base)" - OrderPurchaseLine."Qty. Invoiced (Base)"; + QtyToAllocateBase := MinDecimal(RemainingBaseToDistribute, OrderAvailableBase); + if QtyToAllocateBase > 0 then begin + QtyPerUoM := OrderPurchaseLine."Qty. per Unit of Measure"; + if QtyPerUoM = 0 then + QtyPerUoM := 1; + QtyToAllocate := UnitOfMeasureMgt.CalcQtyFromBase(QtyToAllocateBase, QtyPerUoM); + POMatchingGroup.AddMatch(POMatching.InvoiceOrderEdge(InvoicePurchaseLine.SystemId, OrderPurchaseLine.SystemId, QtyToAllocate, QtyToAllocateBase)); + AddExplicitReceiptMatches(EDocumentPurchaseLine, InvoicePurchaseLine, OrderPurchaseLine, QtyToAllocateBase, POMatchingGroup); + RemainingBaseToDistribute -= QtyToAllocateBase; + end; + end; + until (EDocPurchaseLinePOMatch.Next() = 0) or (RemainingBaseToDistribute <= 0); + end; + end; until EDocumentPurchaseLine.Next() = 0; + + POMatching.SuggestCoveringReceipts(POMatchingGroup); + POMatchingGroup.SaveMatchingGroups(); + RemoveAllMatchesForEDocument(EDocumentPurchaseHeader); + end; + + local procedure AddExplicitReceiptMatches(EDocumentPurchaseLine: Record "E-Document Purchase Line"; InvoicePurchaseLine: Record "Purchase Line"; OrderPurchaseLine: Record "Purchase Line"; QtyToAllocateBase: Decimal; var POMatchingGroup: Codeunit "PO Matching Group") + var + ReceiptPOMatch: Record "E-Doc. Purchase Line PO Match"; + PurchaseReceiptLine: Record "Purch. Rcpt. Line"; + POMatching: Codeunit "PO Matching"; + UnitOfMeasureMgt: Codeunit "Unit of Measure Management"; + RemainingBase, ReceiptAvailableBase, ReceiptQtyBase, ReceiptQty, QtyPerUoM : Decimal; + NullGuid: Guid; + begin + RemainingBase := QtyToAllocateBase; + ReceiptPOMatch.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + ReceiptPOMatch.SetRange("Purchase Line SystemId", OrderPurchaseLine.SystemId); + ReceiptPOMatch.SetFilter("Receipt Line SystemId", '<>%1', NullGuid); + if not ReceiptPOMatch.FindSet() then + exit; + + QtyPerUoM := OrderPurchaseLine."Qty. per Unit of Measure"; + if QtyPerUoM = 0 then + QtyPerUoM := 1; + repeat + if PurchaseReceiptLine.GetBySystemId(ReceiptPOMatch."Receipt Line SystemId") then begin + ReceiptAvailableBase := PurchaseReceiptLine."Quantity (Base)" - PurchaseReceiptLine."Qty. Invoiced (Base)"; + ReceiptQtyBase := MinDecimal(RemainingBase, ReceiptAvailableBase); + if ReceiptQtyBase > 0 then begin + ReceiptQty := UnitOfMeasureMgt.CalcQtyFromBase(ReceiptQtyBase, QtyPerUoM); + POMatchingGroup.AddMatch(POMatching.InvoiceOrderReceiptEdge(InvoicePurchaseLine.SystemId, OrderPurchaseLine.SystemId, PurchaseReceiptLine.SystemId, ReceiptQty, ReceiptQtyBase)); + RemainingBase -= ReceiptQtyBase; + end; + end; + until (ReceiptPOMatch.Next() = 0) or (RemainingBase <= 0); + end; + + local procedure MinDecimal(A: Decimal; B: Decimal): Decimal + begin + if A < B then + exit(A); + exit(B); end; /// @@ -713,36 +844,47 @@ codeunit 6196 "E-Doc. PO Matching" /// procedure TransferPOMatchesFromInvoiceToEDocument(PurchaseHeader: Record "Purchase Header") var + MatchedOrderLine: Record "Matched Order Line"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; PurchaseInvoiceLine: Record "Purchase Line"; PurchaseOrderLine: Record "Purchase Line"; PurchaseReceiptLine: Record "Purch. Rcpt. Line"; - TempPOLineToMatch: Record "Purchase Line" temporary; - TempReceiptLineToMatch: Record "Purch. Rcpt. Line" temporary; + TempPOLinesToMatch: Record "Purchase Line" temporary; + TempReceiptLinesToMatch: Record "Purch. Rcpt. Line" temporary; begin PurchaseInvoiceLine.SetRange("Document Type", PurchaseHeader."Document Type"); PurchaseInvoiceLine.SetRange("Document No.", PurchaseHeader."No."); - PurchaseInvoiceLine.SetFilter("Receipt No.", '<>%1', ''); - PurchaseInvoiceLine.SetFilter("Receipt Line No.", '<>%1', 0); - if PurchaseInvoiceLine.IsEmpty() then + if not PurchaseInvoiceLine.FindSet() then exit; - PurchaseInvoiceLine.FindSet(); repeat if not EDocumentPurchaseLine.GetFromLinkedPurchaseLine(PurchaseInvoiceLine) then continue; - if not PurchaseReceiptLine.Get(PurchaseInvoiceLine."Receipt No.", PurchaseInvoiceLine."Receipt Line No.") then - continue; - if not PurchaseOrderLine.Get(Enum::"Purchase Document Type"::Order, PurchaseReceiptLine."Order No.", PurchaseReceiptLine."Order Line No.") then - continue; - TempPOLineToMatch.DeleteAll(); - TempPOLineToMatch.Copy(PurchaseOrderLine); - TempPOLineToMatch.Insert(); - MatchPOLinesToEDocumentLine(TempPOLineToMatch, EDocumentPurchaseLine); - - TempReceiptLineToMatch.DeleteAll(); - TempReceiptLineToMatch.Copy(PurchaseReceiptLine); - TempReceiptLineToMatch.Insert(); - MatchReceiptLinesToEDocumentLine(TempReceiptLineToMatch, EDocumentPurchaseLine); + + TempPOLinesToMatch.Reset(); + TempPOLinesToMatch.DeleteAll(); + TempReceiptLinesToMatch.Reset(); + TempReceiptLinesToMatch.DeleteAll(); + MatchedOrderLine.SetRange("Document Line SystemId", PurchaseInvoiceLine.SystemId); + if MatchedOrderLine.FindSet() then + repeat + if PurchaseOrderLine.GetBySystemId(MatchedOrderLine."Matched Order Line SystemId") then + CollectPOLineToMatch(PurchaseOrderLine, TempPOLinesToMatch); + if not IsNullGuid(MatchedOrderLine."Matched Rcpt./Shpt. Line SysId") then + if PurchaseReceiptLine.GetBySystemId(MatchedOrderLine."Matched Rcpt./Shpt. Line SysId") then + CollectReceiptLineToMatch(PurchaseReceiptLine, TempReceiptLinesToMatch); + until MatchedOrderLine.Next() = 0; + + if (PurchaseInvoiceLine."Receipt No." <> '') and (PurchaseInvoiceLine."Receipt Line No." <> 0) then + if PurchaseReceiptLine.Get(PurchaseInvoiceLine."Receipt No.", PurchaseInvoiceLine."Receipt Line No.") then + if PurchaseOrderLine.Get(PurchaseOrderLine."Document Type"::Order, PurchaseReceiptLine."Order No.", PurchaseReceiptLine."Order Line No.") then begin + CollectPOLineToMatch(PurchaseOrderLine, TempPOLinesToMatch); + CollectReceiptLineToMatch(PurchaseReceiptLine, TempReceiptLinesToMatch); + end; + + if not TempPOLinesToMatch.IsEmpty() then + MatchPOLinesToEDocumentLine(TempPOLinesToMatch, EDocumentPurchaseLine); + if not TempReceiptLinesToMatch.IsEmpty() then + MatchReceiptLinesToEDocumentLine(TempReceiptLinesToMatch, EDocumentPurchaseLine, false); until PurchaseInvoiceLine.Next() = 0; PurchaseInvoiceLine.SetRange("Receipt No."); PurchaseInvoiceLine.SetRange("Receipt Line No."); @@ -750,6 +892,22 @@ codeunit 6196 "E-Doc. PO Matching" PurchaseInvoiceLine.ModifyAll("Receipt Line No.", 0); end; + local procedure CollectPOLineToMatch(PurchaseOrderLine: Record "Purchase Line"; var TempPOLinesToMatch: Record "Purchase Line" temporary) + begin + if TempPOLinesToMatch.Get(PurchaseOrderLine."Document Type", PurchaseOrderLine."Document No.", PurchaseOrderLine."Line No.") then + exit; + TempPOLinesToMatch := PurchaseOrderLine; + TempPOLinesToMatch.Insert(); + end; + + local procedure CollectReceiptLineToMatch(PurchaseReceiptLine: Record "Purch. Rcpt. Line"; var TempReceiptLinesToMatch: Record "Purch. Rcpt. Line" temporary) + begin + if TempReceiptLinesToMatch.Get(PurchaseReceiptLine."Document No.", PurchaseReceiptLine."Line No.") then + exit; + TempReceiptLinesToMatch := PurchaseReceiptLine; + TempReceiptLinesToMatch.Insert(); + end; + /// /// Loads the settings for purchase order matching. /// diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocSelectPOLines.Page.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocSelectPOLines.Page.al index a91b2d447b5..3b55c6a057c 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocSelectPOLines.Page.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocSelectPOLines.Page.al @@ -37,7 +37,12 @@ page 6116 "E-Doc. Select PO Lines" Caption = 'Quantity'; ToolTip = 'Specifies the quantity of the e-document line.'; } - + field(EDocumentPurchaseLineUnitPrice; EDocumentPurchaseLine."Unit Price") + { + ApplicationArea = All; + Caption = 'Unit Price'; + ToolTip = 'Specifies the unit price of the e-document line.'; + } } repeater(Lines) { @@ -84,6 +89,19 @@ page 6116 "E-Doc. Select PO Lines" ToolTip = 'Specifies the quantity that has already been invoiced.'; AutoFormatType = 0; } + field("Direct Unit Cost"; Rec."Direct Unit Cost") + { + ApplicationArea = All; + Caption = 'Unit Price'; + ToolTip = 'Specifies the unit price (direct unit cost) of the purchase order line.'; + AutoFormatType = 2; + AutoFormatExpression = Rec."Currency Code"; + } + field("Expected Receipt Date"; Rec."Expected Receipt Date") + { + ApplicationArea = All; + ToolTip = 'Specifies the expected receipt date.'; + } } } } diff --git a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al index 83f23def22d..fa6694a9830 100644 --- a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al @@ -11,6 +11,7 @@ using Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Import.Purchase; using Microsoft.Foundation.UOM; using Microsoft.Inventory.Item; +using Microsoft.Inventory.Tracking; using Microsoft.Purchases.Document; using Microsoft.Purchases.History; using Microsoft.Purchases.Setup; @@ -796,6 +797,188 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsRemainingToInvoice warning (I > R, I > J)'); end; + [Test] + procedure AmountMismatchWarningRaisedWhenUnitCostExceedsTolerance() + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + Item: Record Item; + TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; + begin + Initialize(); + SetMatchingDifferencePct(5); + CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 10, 110, 0); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); + PurchaseLine := AddOrderLineWithCost(PurchaseHeader, Item."No.", 10, 100, 0); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + + TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected AmountMismatch warning when unit cost exceeds tolerance'); + SetMatchingDifferencePct(0); + end; + + [Test] + procedure NoAmountMismatchWarningWhenWithinTolerance() + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + Item: Record Item; + TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; + begin + Initialize(); + SetMatchingDifferencePct(5); + CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 10, 103, 0); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); + PurchaseLine := AddOrderLineWithCost(PurchaseHeader, Item."No.", 10, 100, 0); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + + TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect AmountMismatch warning within tolerance'); + SetMatchingDifferencePct(0); + end; + + [Test] + procedure AmountMismatchHonorsAbsoluteDiscountOnDraftLine() + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + Item: Record Item; + TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; + begin + Initialize(); + SetMatchingDifferencePct(1); + CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 10, 100, 50); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); + PurchaseLine := AddOrderLineWithCost(PurchaseHeader, Item."No.", 10, 100, 0); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + + TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected AmountMismatch warning after applying the absolute discount'); + SetMatchingDifferencePct(0); + end; + + [Test] + procedure NoAmountWarningWhenThresholdZeroButOnlyRoundingNoise() + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + Item: Record Item; + TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; + begin + Initialize(); + SetMatchingDifferencePct(0); + CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 1, 100.001, 0); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); + PurchaseLine := AddOrderLineWithCost(PurchaseHeader, Item."No.", 1, 100, 0); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + + TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect AmountMismatch warning for sub-rounding-precision noise'); + end; + + [Test] + procedure NoAmountWarningWhenCurrenciesDiffer() + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + Item: Record Item; + TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; + begin + Initialize(); + SetMatchingDifferencePct(5); + CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 10, 200, 0); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); + PurchaseLine := AddOrderLineWithCost(PurchaseHeader, Item."No.", 10, 100, 0); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + EDocumentPurchaseLine."Currency Code" := 'EUR'; + EDocumentPurchaseLine.Modify(); + + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + + TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect AmountMismatch warning when currencies differ'); + SetMatchingDifferencePct(0); + end; + + [Test] + procedure AmountMismatchUsesQuantityWeightedAverageAcrossMultiplePOLines() + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine1, PurchaseLine2 : Record "Purchase Line"; + TempPurchaseLine: Record "Purchase Line" temporary; + Item: Record Item; + TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; + begin + Initialize(); + SetMatchingDifferencePct(5); + CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 1, 125, 0); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); + PurchaseLine1 := AddOrderLineWithCost(PurchaseHeader, Item."No.", 30, 100, 0); + PurchaseLine2 := AddOrderLineWithCost(PurchaseHeader, Item."No.", 10, 200, 0); + TempPurchaseLine := PurchaseLine1; + TempPurchaseLine.Insert(); + TempPurchaseLine := PurchaseLine2; + TempPurchaseLine.Insert(); + EDocPOMatching.MatchPOLinesToEDocumentLine(TempPurchaseLine, EDocumentPurchaseLine); + + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + + TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect AmountMismatch warning when invoiced cost equals the quantity-weighted average'); + SetMatchingDifferencePct(0); + end; + + [Test] + procedure NoAmountWarningWhenDraftQuantityZero() + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + Item: Record Item; + TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; + begin + Initialize(); + SetMatchingDifferencePct(5); + CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 0, 100, 0); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); + PurchaseLine := AddOrderLineWithCost(PurchaseHeader, Item."No.", 10, 50, 0); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + + TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect AmountMismatch warning for a zero-quantity draft line'); + SetMatchingDifferencePct(0); + end; + [Test] procedure IsPOMatchConsistentReturnsTrueWhenAllMatchesAreValid() var @@ -1638,6 +1821,45 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Assert.AreEqual(PurchaseLine."No.", EDocumentPurchaseLine."[BC] Purchase Type No.", 'E-Document line number should be updated'); end; + [Test] + procedure MatchMultiplePOLinesCopiesDimensionsFromFirstOrderLine() + var + EDocument: Record "E-Document"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine1, PurchaseLine2 : Record "Purchase Line"; + TempPurchaseLine: Record "Purchase Line" temporary; + Item: Record Item; + begin + Initialize(); + LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); + EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); + EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; + EDocumentPurchaseHeader.Modify(); + EDocumentPurchaseLine := LibraryEDocument.InsertPurchaseDraftLine(EDocument); + + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); + LibraryEDocument.GetGenericItem(Item); + LibraryPurchase.CreatePurchaseLine(PurchaseLine1, PurchaseHeader, PurchaseLine1.Type::Item, Item."No.", 5); + LibraryPurchase.CreatePurchaseLine(PurchaseLine2, PurchaseHeader, PurchaseLine2.Type::Item, Item."No.", 5); + PurchaseLine1."Dimension Set ID" := LibraryRandom.RandInt(1000); + PurchaseLine1.Modify(); + PurchaseLine2."Dimension Set ID" := LibraryRandom.RandInt(1000) + 1000; + PurchaseLine2.Modify(); + TempPurchaseLine := PurchaseLine1; + TempPurchaseLine.Insert(); + TempPurchaseLine := PurchaseLine2; + TempPurchaseLine.Insert(); + + EDocPOMatching.MatchPOLinesToEDocumentLine(TempPurchaseLine, EDocumentPurchaseLine); + + Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine1, EDocumentPurchaseLine), 'Expected the first order line to be matched'); + Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine2, EDocumentPurchaseLine), 'Expected the second order line to be matched'); + EDocumentPurchaseLine.GetBySystemId(EDocumentPurchaseLine.SystemId); + Assert.AreEqual(PurchaseLine1."Dimension Set ID", EDocumentPurchaseLine."[BC] Dimension Set ID", 'Expected dimensions from the first order line'); + end; + [Test] procedure MatchPOLinesToEDocumentLineRemovesExistingMatchesFirst() var @@ -1806,7 +2028,7 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchaseHeader: Record "Purchase Header"; PurchaseLine: Record "Purchase Line"; PurchaseReceiptHeader: Record "Purch. Rcpt. Header"; - PurchaseReceiptLine: Record "Purch. Rcpt. Line"; + PurchaseReceiptLine1, PurchaseReceiptLine2 : Record "Purch. Rcpt. Line"; TempReceiptLine: Record "Purch. Rcpt. Line" temporary; Item: Record Item; begin @@ -1827,11 +2049,14 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", LibraryRandom.RandDec(10, 2)); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); - // Create receipt line matched to the PO line + // Create receipt lines matched to the PO line CreateMockReceiptHeader(PurchaseReceiptHeader, Vendor."No."); - CreateMockReceiptLine(PurchaseReceiptLine, PurchaseReceiptHeader, Item."No.", 10, PurchaseLine); + CreateMockReceiptLine(PurchaseReceiptLine1, PurchaseReceiptHeader, Item."No.", 4, PurchaseLine); + CreateMockReceiptLine(PurchaseReceiptLine2, PurchaseReceiptHeader, Item."No.", 6, PurchaseLine); - TempReceiptLine := PurchaseReceiptLine; + TempReceiptLine := PurchaseReceiptLine1; + TempReceiptLine.Insert(); + TempReceiptLine := PurchaseReceiptLine2; TempReceiptLine.Insert(); // [WHEN] MatchReceiptLinesToEDocumentLine is called @@ -1839,7 +2064,8 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [THEN] Receipt line matches should be created Assert.IsTrue(EDocPOMatching.IsEDocumentLineMatchedToAnyReceiptLine(EDocumentPurchaseLine), 'Expected E-Document line to be matched to receipt line'); - Assert.IsTrue(EDocPOMatching.IsReceiptLineMatchedToEDocumentLine(PurchaseReceiptLine, EDocumentPurchaseLine), 'Expected receipt line to be matched to E-Document line'); + Assert.IsTrue(EDocPOMatching.IsReceiptLineMatchedToEDocumentLine(PurchaseReceiptLine1, EDocumentPurchaseLine), 'Expected first receipt line to be matched to E-Document line'); + Assert.IsTrue(EDocPOMatching.IsReceiptLineMatchedToEDocumentLine(PurchaseReceiptLine2, EDocumentPurchaseLine), 'Expected second receipt line to be matched to E-Document line'); end; [Test] @@ -2470,6 +2696,83 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Assert.IsTrue(EDocPOMatching.IsReceiptLineMatchedToEDocumentLine(PurchaseReceiptLine2, EDocumentPurchaseLine2), 'Expected second receipt line to be matched to second E-Document line'); end; + [Test] + procedure TransferPOMatchesUsesBaseAppSuggesterForMultiplePartialReceipts() + var + EDocument: Record "E-Document"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + PurchaseOrderHeader: Record "Purchase Header"; + PurchaseOrderLine: Record "Purchase Line"; + PurchaseReceiptHeader: Record "Purch. Rcpt. Header"; + PurchaseReceiptLine1, PurchaseReceiptLine2 : Record "Purch. Rcpt. Line"; + MatchedOrderLine: Record "Matched Order Line"; + Item: Record Item; + begin + Initialize(); + CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); + LibraryInventory.CreateItem(Item); + LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); + + CreateMockReceiptHeader(PurchaseReceiptHeader, Vendor."No."); + CreateMockReceiptLine(PurchaseReceiptLine1, PurchaseReceiptHeader, Item."No.", 5, PurchaseOrderLine); + CreateMockReceiptLine(PurchaseReceiptLine2, PurchaseReceiptHeader, Item."No.", 7, PurchaseOrderLine); + + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); + LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); + + EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); + + Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine.SystemId, PurchaseReceiptLine1.SystemId, MatchedOrderLine), 'Expected the first receipt to be suggested'); + Assert.AreEqual(5, MatchedOrderLine."Qty. to Invoice", 'Expected the full first receipt quantity'); + Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine.SystemId, PurchaseReceiptLine2.SystemId, MatchedOrderLine), 'Expected the second receipt to be suggested'); + Assert.AreEqual(5, MatchedOrderLine."Qty. to Invoice", 'Expected only the remaining quantity from the second receipt'); + end; + + [Test] + procedure TransferDistributesQuantityAcrossMatchedOrderLines() + var + EDocument: Record "E-Document"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + PurchaseOrderHeader: Record "Purchase Header"; + PurchaseOrderLine1, PurchaseOrderLine2 : Record "Purchase Line"; + TempPurchaseLine: Record "Purchase Line" temporary; + MatchedOrderLine: Record "Matched Order Line"; + Item: Record Item; + NullGuid: Guid; + begin + Initialize(); + CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 100); + LibraryInventory.CreateItem(Item); + LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine1, PurchaseOrderHeader, PurchaseOrderLine1.Type::Item, Item."No.", 60); + LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine2, PurchaseOrderHeader, PurchaseOrderLine2.Type::Item, Item."No.", 40); + TempPurchaseLine := PurchaseOrderLine1; + TempPurchaseLine.Insert(); + TempPurchaseLine := PurchaseOrderLine2; + TempPurchaseLine.Insert(); + EDocPOMatching.MatchPOLinesToEDocumentLine(TempPurchaseLine, EDocumentPurchaseLine); + + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); + LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); + + EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); + + Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine1.SystemId, NullGuid, MatchedOrderLine), 'Expected a match for the first order line'); + Assert.AreEqual(60, MatchedOrderLine."Qty. to Invoice", 'Expected the full first order line quantity'); + Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine2.SystemId, NullGuid, MatchedOrderLine), 'Expected a match for the second order line'); + Assert.AreEqual(40, MatchedOrderLine."Qty. to Invoice", 'Expected the full second order line quantity'); + end; + [Test] procedure TransferPOMatchesFromEDocumentToInvoiceTransfersReceiptMatchAndRemovesEDocumentMatches() var @@ -2482,7 +2785,9 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchaseOrderLine: Record "Purchase Line"; PurchaseReceiptHeader: Record "Purch. Rcpt. Header"; PurchaseReceiptLine: Record "Purch. Rcpt. Line"; + MatchedOrderLine: Record "Matched Order Line"; Item: Record Item; + NullGuid: Guid; begin Initialize(); // [SCENARIO] TransferPOMatchesFromEDocumentToInvoice transfers receipt match to linked invoice line and removes E-Document matches @@ -2505,16 +2810,92 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [WHEN] TransferPOMatchesFromEDocumentToInvoice is called EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); - // [THEN] The invoice line's Receipt No. and Receipt Line No. are set - PurchaseLine.Get(PurchaseLine."Document Type", PurchaseLine."Document No.", PurchaseLine."Line No."); - Assert.AreEqual(PurchaseReceiptLine."Document No.", PurchaseLine."Receipt No.", 'Expected invoice line Receipt No. to match receipt'); - Assert.AreEqual(PurchaseReceiptLine."Line No.", PurchaseLine."Receipt Line No.", 'Expected invoice line Receipt Line No. to match receipt line'); + // [THEN] A BaseApp match links the invoice, order, and receipt lines + Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine.SystemId, PurchaseReceiptLine.SystemId, MatchedOrderLine), 'Expected a matched order line for the receipt'); + Assert.AreEqual(10, MatchedOrderLine."Qty. to Invoice", 'Expected the full receipt quantity to be matched'); + Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine.SystemId, NullGuid, MatchedOrderLine), 'Expected the invoice-order allocation budget'); + Assert.AreEqual(10, MatchedOrderLine."Qty. to Invoice", 'Expected the allocation budget to equal the invoice quantity'); // [THEN] The E-Document line no longer has any PO or receipt matches Assert.IsFalse(EDocPOMatching.IsEDocumentLineMatchedToAnyPOLine(EDocumentPurchaseLine), 'Expected E-Document line to have no PO matches'); Assert.IsFalse(EDocPOMatching.IsEDocumentLineMatchedToAnyReceiptLine(EDocumentPurchaseLine), 'Expected E-Document line to have no receipt matches'); end; + [Test] + procedure TransferPOMatchesFromInvoiceToEDocumentReconstructsMatchesFromMatchedOrderLines() + var + EDocument: Record "E-Document"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + PurchaseOrderHeader: Record "Purchase Header"; + PurchaseOrderLine: Record "Purchase Line"; + PurchaseReceiptHeader: Record "Purch. Rcpt. Header"; + PurchaseReceiptLine: Record "Purch. Rcpt. Line"; + Item: Record Item; + begin + Initialize(); + CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); + LibraryInventory.CreateItem(Item); + LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); + CreateMockReceiptHeader(PurchaseReceiptHeader, Vendor."No."); + CreateMockReceiptLine(PurchaseReceiptLine, PurchaseReceiptHeader, Item."No.", 10, PurchaseOrderLine); + MatchEDocumentLineToReceiptLine(EDocumentPurchaseLine, PurchaseReceiptLine); + + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); + LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); + EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); + + EDocPOMatching.TransferPOMatchesFromInvoiceToEDocument(PurchaseHeader); + + Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseOrderLine, EDocumentPurchaseLine), 'Expected the order match to be reconstructed'); + Assert.IsTrue(EDocPOMatching.IsReceiptLineMatchedToEDocumentLine(PurchaseReceiptLine, EDocumentPurchaseLine), 'Expected the receipt match to be reconstructed'); + end; + + [Test] + procedure TransferPOMatchesFromInvoiceReconstructsPartialReceiptCoverage() + var + EDocument: Record "E-Document"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + PurchaseOrderHeader: Record "Purchase Header"; + PurchaseOrderLine1, PurchaseOrderLine2 : Record "Purchase Line"; + PurchaseReceiptHeader: Record "Purch. Rcpt. Header"; + PurchaseReceiptLine: Record "Purch. Rcpt. Line"; + POMatching: Codeunit "PO Matching"; + POMatchingGroup: Codeunit "PO Matching Group"; + Item: Record Item; + begin + Initialize(); + CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); + LibraryInventory.CreateItem(Item); + LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine1, PurchaseOrderHeader, PurchaseOrderLine1.Type::Item, Item."No.", 5); + LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine2, PurchaseOrderHeader, PurchaseOrderLine2.Type::Item, Item."No.", 5); + CreateMockReceiptHeader(PurchaseReceiptHeader, Vendor."No."); + CreateMockReceiptLine(PurchaseReceiptLine, PurchaseReceiptHeader, Item."No.", 5, PurchaseOrderLine1); + + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); + LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); + POMatchingGroup.AddMatch(POMatching.InvoiceOrderEdge(PurchaseLine.SystemId, PurchaseOrderLine1.SystemId, 5, 5)); + POMatchingGroup.AddMatch(POMatching.InvoiceOrderReceiptEdge(PurchaseLine.SystemId, PurchaseOrderLine1.SystemId, PurchaseReceiptLine.SystemId, 5, 5)); + POMatchingGroup.AddMatch(POMatching.InvoiceOrderEdge(PurchaseLine.SystemId, PurchaseOrderLine2.SystemId, 5, 5)); + POMatchingGroup.SaveMatchingGroups(); + + EDocPOMatching.TransferPOMatchesFromInvoiceToEDocument(PurchaseHeader); + + Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseOrderLine1, EDocumentPurchaseLine), 'Expected the first order match to be reconstructed'); + Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseOrderLine2, EDocumentPurchaseLine), 'Expected the second order match to be reconstructed'); + Assert.IsTrue(EDocPOMatching.IsReceiptLineMatchedToEDocumentLine(PurchaseReceiptLine, EDocumentPurchaseLine), 'Expected the partial receipt match to be reconstructed'); + end; + [Test] procedure TransferPOMatchesFromEDocumentToInvoiceProcessesMultipleLinesIndependently() var @@ -2578,18 +2959,10 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [WHEN] TransferPOMatchesFromEDocumentToInvoice is called EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); - // [THEN] Each invoice line has the correct Receipt No. and Receipt Line No. - PurchaseLine1.Get(PurchaseLine1."Document Type", PurchaseLine1."Document No.", PurchaseLine1."Line No."); - Assert.AreEqual(PurchaseReceiptLine1."Document No.", PurchaseLine1."Receipt No.", 'Expected first invoice line Receipt No. to match receipt'); - Assert.AreEqual(PurchaseReceiptLine1."Line No.", PurchaseLine1."Receipt Line No.", 'Expected first invoice line Receipt Line No. to match receipt line'); - - PurchaseLine2.Get(PurchaseLine2."Document Type", PurchaseLine2."Document No.", PurchaseLine2."Line No."); - Assert.AreEqual(PurchaseReceiptLine2."Document No.", PurchaseLine2."Receipt No.", 'Expected second invoice line Receipt No. to match receipt'); - Assert.AreEqual(PurchaseReceiptLine2."Line No.", PurchaseLine2."Receipt Line No.", 'Expected second invoice line Receipt Line No. to match receipt line'); - - PurchaseLine3.Get(PurchaseLine3."Document Type", PurchaseLine3."Document No.", PurchaseLine3."Line No."); - Assert.AreEqual(PurchaseReceiptLine3."Document No.", PurchaseLine3."Receipt No.", 'Expected third invoice line Receipt No. to match receipt'); - Assert.AreEqual(PurchaseReceiptLine3."Line No.", PurchaseLine3."Receipt Line No.", 'Expected third invoice line Receipt Line No. to match receipt line'); + // [THEN] Each invoice line has the expected BaseApp match + AssertMatchedOrderLineExists(PurchaseLine1.SystemId, PurchaseOrderLine1.SystemId, PurchaseReceiptLine1.SystemId, 10, 'first'); + AssertMatchedOrderLineExists(PurchaseLine2.SystemId, PurchaseOrderLine2.SystemId, PurchaseReceiptLine2.SystemId, 15, 'second'); + AssertMatchedOrderLineExists(PurchaseLine3.SystemId, PurchaseOrderLine3.SystemId, PurchaseReceiptLine3.SystemId, 20, 'third'); end; [Test] @@ -2795,6 +3168,43 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" EDocPOMatching.MatchPOLinesToEDocumentLine(TempPurchaseLine, EDocumentLine); end; + local procedure SetMatchingDifferencePct(Pct: Decimal) + var + PurchasesPayablesSetup: Record "Purchases & Payables Setup"; + begin + PurchasesPayablesSetup.Get(); + PurchasesPayablesSetup."E-Document Matching Difference" := Pct; + PurchasesPayablesSetup.Modify(); + end; + + local procedure CreateMatchedAmountDraftLine(var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; var EDocumentPurchaseLine: Record "E-Document Purchase Line"; var Item: Record Item; EDocQuantity: Decimal; EDocUnitPrice: Decimal; EDocTotalDiscount: Decimal) + var + EDocument: Record "E-Document"; + begin + LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); + EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); + EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; + EDocumentPurchaseHeader.Modify(); + + LibraryEDocument.GetGenericItem(Item); + EDocumentPurchaseLine := LibraryEDocument.InsertPurchaseDraftLine(EDocument); + EDocumentPurchaseLine."[BC] Purchase Line Type" := Enum::"Purchase Line Type"::Item; + EDocumentPurchaseLine."[BC] Purchase Type No." := Item."No."; + EDocumentPurchaseLine."[BC] Unit of Measure" := Item."Base Unit of Measure"; + EDocumentPurchaseLine.Quantity := EDocQuantity; + EDocumentPurchaseLine."Unit Price" := EDocUnitPrice; + EDocumentPurchaseLine."Total Discount" := EDocTotalDiscount; + EDocumentPurchaseLine.Modify(); + end; + + local procedure AddOrderLineWithCost(PurchaseHeader: Record "Purchase Header"; ItemNo: Code[20]; Quantity: Decimal; DirectUnitCost: Decimal; LineDiscountPct: Decimal) PurchaseLine: Record "Purchase Line" + begin + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, ItemNo, Quantity); + PurchaseLine."Direct Unit Cost" := DirectUnitCost; + PurchaseLine."Line Discount %" := LineDiscountPct; + PurchaseLine.Modify(); + end; + local procedure MatchEDocumentLineToReceiptLine(EDocumentLine: Record "E-Document Purchase Line"; ReceiptLine: Record "Purch. Rcpt. Line") var TempReceiptLine: Record "Purch. Rcpt. Line" temporary; @@ -2831,6 +3241,8 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchaseReceiptLine.Type := PurchaseReceiptLine.Type::Item; PurchaseReceiptLine."No." := ItemNo; PurchaseReceiptLine.Quantity := Quantity; + PurchaseReceiptLine."Quantity (Base)" := Quantity; + PurchaseReceiptLine."Qty. Rcd. Not Invoiced" := Quantity; PurchaseReceiptLine."Order No." := PurchaseLine."Document No."; PurchaseReceiptLine."Order Line No." := PurchaseLine."Line No."; PurchaseReceiptLine."Buy-from Vendor No." := PurchaseReceiptHeader."Buy-from Vendor No."; @@ -2838,6 +3250,23 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchaseReceiptLine.Insert(); end; + local procedure GetMatchedOrderLine(InvoiceLineSystemId: Guid; OrderLineSystemId: Guid; ReceiptLineSystemId: Guid; var MatchedOrderLine: Record "Matched Order Line"): Boolean + begin + MatchedOrderLine.Reset(); + MatchedOrderLine.SetRange("Document Line SystemId", InvoiceLineSystemId); + MatchedOrderLine.SetRange("Matched Order Line SystemId", OrderLineSystemId); + MatchedOrderLine.SetRange("Matched Rcpt./Shpt. Line SysId", ReceiptLineSystemId); + exit(MatchedOrderLine.FindFirst()); + end; + + local procedure AssertMatchedOrderLineExists(InvoiceLineSystemId: Guid; OrderLineSystemId: Guid; ReceiptLineSystemId: Guid; ExpectedQtyToInvoice: Decimal; LineLabel: Text) + var + MatchedOrderLine: Record "Matched Order Line"; + begin + Assert.IsTrue(GetMatchedOrderLine(InvoiceLineSystemId, OrderLineSystemId, ReceiptLineSystemId, MatchedOrderLine), 'Expected a matched order line for the ' + LineLabel + ' invoice line'); + Assert.AreEqual(ExpectedQtyToInvoice, MatchedOrderLine."Qty. to Invoice", 'Unexpected quantity to invoice on the ' + LineLabel + ' matched order line'); + end; + local procedure LinkEDocumentLineToPurchaseLine(EDocument: Record "E-Document"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; PurchaseLine: Record "Purchase Line") var EDocRecordLink: Record "E-Doc. Record Link"; @@ -2872,4 +3301,3 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" end; } - From dfe1b90e811996cf1d193c0aa64befc572224f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Sun, 23 Aug 2026 18:27:29 +0200 Subject: [PATCH 2/9] Fix E-Document variable declaration ordering --- .../Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al index fa6694a9830..4222e437537 100644 --- a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al @@ -2868,9 +2868,9 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchaseOrderLine1, PurchaseOrderLine2 : Record "Purchase Line"; PurchaseReceiptHeader: Record "Purch. Rcpt. Header"; PurchaseReceiptLine: Record "Purch. Rcpt. Line"; + Item: Record Item; POMatching: Codeunit "PO Matching"; POMatchingGroup: Codeunit "PO Matching Group"; - Item: Record Item; begin Initialize(); CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); From 9eb09621ada2fa0e7df12018a009add0735d8d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Sun, 23 Aug 2026 21:53:15 +0200 Subject: [PATCH 3/9] Initialize vendor number series in PO matching tests --- .../src/Matching/EDocPOMatchingUnitTests.Codeunit.al | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al index 4222e437537..b806a6dd918 100644 --- a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al @@ -3110,6 +3110,15 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchasesPayablesSetup.Modify(); end; + local procedure SetVendorNoSeriesInSetup() + var + PurchasesPayablesSetup: Record "Purchases & Payables Setup"; + begin + PurchasesPayablesSetup.Get(); + PurchasesPayablesSetup.Validate("Vendor Nos.", LibraryERM.CreateNoSeriesCode()); + PurchasesPayablesSetup.Modify(); + end; + local procedure SetupPOMatchingConfiguration(Configuration: Enum "E-Doc. PO M. Configuration"; VendorNo: Code[20]; IncludeVendorInList: Boolean) var EDocPOMatchingSetup: Record "E-Doc. PO Matching Setup"; @@ -3150,6 +3159,7 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" LibraryPurchase.SetOrderNoSeriesInSetup(); LibraryPurchase.SetPostedNoSeriesInSetup(); SetInvoiceNoSeriesInSetup(); + SetVendorNoSeriesInSetup(); ClearPurchaseDocumentsForVendor(); if IsInitialized then From a9c333513b91171a9df1d9e5de88ac86a6e1e0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Mon, 24 Aug 2026 08:06:13 +0200 Subject: [PATCH 4/9] Initialize purchase invoice template in PO matching tests --- .../EDocPOMatchingUnitTests.Codeunit.al | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al index b806a6dd918..0e45ddd50fe 100644 --- a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al @@ -9,6 +9,7 @@ using Microsoft.eServices.EDocument.Integration; using Microsoft.eServices.EDocument.Processing; using Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Foundation.UOM; using Microsoft.Inventory.Item; using Microsoft.Inventory.Tracking; @@ -3119,6 +3120,21 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchasesPayablesSetup.Modify(); end; + local procedure SetPurchaseInvoiceTemplateInSetup() + var + GenJournalTemplate: Record "Gen. Journal Template"; + PurchasesPayablesSetup: Record "Purchases & Payables Setup"; + begin + LibraryERM.CreateGenJournalTemplate(GenJournalTemplate); + GenJournalTemplate.Validate(Type, GenJournalTemplate.Type::Purchases); + GenJournalTemplate.Validate("Posting No. Series", LibraryERM.CreateNoSeriesCode()); + GenJournalTemplate.Modify(true); + + PurchasesPayablesSetup.Get(); + PurchasesPayablesSetup.Validate("P. Invoice Template Name", GenJournalTemplate.Name); + PurchasesPayablesSetup.Modify(); + end; + local procedure SetupPOMatchingConfiguration(Configuration: Enum "E-Doc. PO M. Configuration"; VendorNo: Code[20]; IncludeVendorInList: Boolean) var EDocPOMatchingSetup: Record "E-Doc. PO Matching Setup"; @@ -3160,6 +3176,7 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" LibraryPurchase.SetPostedNoSeriesInSetup(); SetInvoiceNoSeriesInSetup(); SetVendorNoSeriesInSetup(); + SetPurchaseInvoiceTemplateInSetup(); ClearPurchaseDocumentsForVendor(); if IsInitialized then From b6d17f3a0a6e1237cf777b7c4c9e3db5090e7aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Mon, 24 Aug 2026 10:26:41 +0200 Subject: [PATCH 5/9] Use simplified PO matching factories --- .../PurchaseOrderMatching/EDocPOMatching.Codeunit.al | 4 ++-- .../Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al index b8bed9bcdfc..edfceb5a0e6 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al @@ -784,7 +784,7 @@ codeunit 6196 "E-Doc. PO Matching" if QtyPerUoM = 0 then QtyPerUoM := 1; QtyToAllocate := UnitOfMeasureMgt.CalcQtyFromBase(QtyToAllocateBase, QtyPerUoM); - POMatchingGroup.AddMatch(POMatching.InvoiceOrderEdge(InvoicePurchaseLine.SystemId, OrderPurchaseLine.SystemId, QtyToAllocate, QtyToAllocateBase)); + POMatchingGroup.AddMatch(POMatching.InvoiceOrderEdge(InvoicePurchaseLine.SystemId, OrderPurchaseLine.SystemId, QtyToAllocate)); AddExplicitReceiptMatches(EDocumentPurchaseLine, InvoicePurchaseLine, OrderPurchaseLine, QtyToAllocateBase, POMatchingGroup); RemainingBaseToDistribute -= QtyToAllocateBase; end; @@ -824,7 +824,7 @@ codeunit 6196 "E-Doc. PO Matching" ReceiptQtyBase := MinDecimal(RemainingBase, ReceiptAvailableBase); if ReceiptQtyBase > 0 then begin ReceiptQty := UnitOfMeasureMgt.CalcQtyFromBase(ReceiptQtyBase, QtyPerUoM); - POMatchingGroup.AddMatch(POMatching.InvoiceOrderReceiptEdge(InvoicePurchaseLine.SystemId, OrderPurchaseLine.SystemId, PurchaseReceiptLine.SystemId, ReceiptQty, ReceiptQtyBase)); + POMatchingGroup.AddMatch(POMatching.InvoiceOrderReceiptEdge(InvoicePurchaseLine.SystemId, OrderPurchaseLine.SystemId, PurchaseReceiptLine.SystemId, ReceiptQty)); RemainingBase -= ReceiptQtyBase; end; end; diff --git a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al index 0e45ddd50fe..494ef366fe7 100644 --- a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al @@ -2885,9 +2885,9 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); - POMatchingGroup.AddMatch(POMatching.InvoiceOrderEdge(PurchaseLine.SystemId, PurchaseOrderLine1.SystemId, 5, 5)); - POMatchingGroup.AddMatch(POMatching.InvoiceOrderReceiptEdge(PurchaseLine.SystemId, PurchaseOrderLine1.SystemId, PurchaseReceiptLine.SystemId, 5, 5)); - POMatchingGroup.AddMatch(POMatching.InvoiceOrderEdge(PurchaseLine.SystemId, PurchaseOrderLine2.SystemId, 5, 5)); + POMatchingGroup.AddMatch(POMatching.InvoiceOrderEdge(PurchaseLine.SystemId, PurchaseOrderLine1.SystemId, 5)); + POMatchingGroup.AddMatch(POMatching.InvoiceOrderReceiptEdge(PurchaseLine.SystemId, PurchaseOrderLine1.SystemId, PurchaseReceiptLine.SystemId, 5)); + POMatchingGroup.AddMatch(POMatching.InvoiceOrderEdge(PurchaseLine.SystemId, PurchaseOrderLine2.SystemId, 5)); POMatchingGroup.SaveMatchingGroups(); EDocPOMatching.TransferPOMatchesFromInvoiceToEDocument(PurchaseHeader); From a17c4441293135254c8b6712f38966a769178510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Mon, 24 Aug 2026 12:14:53 +0200 Subject: [PATCH 6/9] feedback + removing the autosuggest e-docs side --- .../EDocPOMatching.Codeunit.al | 126 +++------ .../EDocPOMatchingUnitTests.Codeunit.al | 256 +++++++----------- 2 files changed, 132 insertions(+), 250 deletions(-) diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al index edfceb5a0e6..c54ef73c91f 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al @@ -6,7 +6,6 @@ namespace Microsoft.eServices.EDocument.Processing.Import.Purchase; using Microsoft.eServices.EDocument; using Microsoft.Finance.GeneralLedger.Setup; -using Microsoft.Foundation.UOM; using Microsoft.Inventory.Item; using Microsoft.Inventory.Tracking; using Microsoft.Purchases.Document; @@ -45,7 +44,7 @@ codeunit 6196 "E-Doc. PO Matching" PurchaseLine.SetRange("Pay-to Vendor No.", Vendor."No."); if EDocumentPurchaseLine."[BC] Unit of Measure" <> '' then PurchaseLine.SetRange("Unit of Measure Code", EDocumentPurchaseLine."[BC] Unit of Measure"); - PurchaseLine.SetLoadFields("Document No.", "Line No.", Description, Quantity, "Qty. Invoiced (Base)", "Qty. Received (Base)", Type, "No.", "Quantity Received", "Quantity Invoiced", "Direct Unit Cost", "Line Discount %", "Currency Code", "Expected Receipt Date"); + PurchaseLine.SetLoadFields("Document No.", "Line No.", Description, Quantity, Type, "No.", "Quantity Received", "Quantity Invoiced", "Direct Unit Cost", "Line Discount %", "Currency Code", "Expected Receipt Date"); if PurchaseLine.FindSet() then repeat // We exclude lines that have already been matched unless they were matched to the current line @@ -231,17 +230,18 @@ codeunit 6196 "E-Doc. PO Matching" if not TempPurchaseLine.FindSet() then exit; repeat - PurchaseLinesQuantityInvoiced += TempPurchaseLine."Qty. Invoiced (Base)"; - PurchaseLinesQuantityReceived += TempPurchaseLine."Qty. Received (Base)"; + PurchaseLinesQuantityInvoiced += TempPurchaseLine."Quantity Invoiced"; + PurchaseLinesQuantityReceived += TempPurchaseLine."Quantity Received"; PurchaseLinesQuantity += TempPurchaseLine.Quantity; until TempPurchaseLine.Next() = 0; - if not GetEDocumentLineQuantityInBaseUoM(EDocumentPurchaseLine, EDocLineQuantity) then begin + if not HasEDocumentLineQuantityInformation(EDocumentPurchaseLine) then begin POMatchWarnings."E-Doc. Purchase Line SystemId" := EDocumentPurchaseLine.SystemId; POMatchWarnings."Warning Type" := "E-Doc PO Match Warning"::MissingInformationForMatch; POMatchWarnings.Insert(); exit; end; + EDocLineQuantity := EDocumentPurchaseLine.Quantity; // I = Invoice quantity (from the e-document line) // R = Remaining to invoice on the PO (Ordered - Previously Invoiced) @@ -677,74 +677,16 @@ codeunit 6196 "E-Doc. PO Matching" end; end; - /// - /// If the E-Document has been matched to an order line without specifying receipts, we match with receipt lines for that order line that can cover the E-Document line quantity. - /// - /// - procedure SuggestReceiptsForMatchedOrderLines(EDocumentPurchaseHeader: Record "E-Document Purchase Header") - var - EDocumentPurchaseLine: Record "E-Document Purchase Line"; - EDocPurchaseLinePOMatch: Record "E-Doc. Purchase Line PO Match"; - PurchaseOrderLine: Record "Purchase Line"; - PurchaseReceiptLine: Record "Purch. Rcpt. Line"; - TempPurchaseReceiptLine: Record "Purch. Rcpt. Line" temporary; - EDocLineQuantity: Decimal; - NullGuid: Guid; - begin - EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentPurchaseHeader."E-Document Entry No."); - if EDocumentPurchaseLine.FindSet() then - repeat - Clear(EDocPurchaseLinePOMatch); - EDocPurchaseLinePOMatch.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - EDocPurchaseLinePOMatch.SetRange("Receipt Line SystemId", NullGuid); - if not EDocPurchaseLinePOMatch.FindFirst() then - continue; // No PO lines matched, so no receipt can be suggested - if not PurchaseOrderLine.GetBySystemId(EDocPurchaseLinePOMatch."Purchase Line SystemId") then - continue; // Should not happen, but we skip in case it does, this procedure doesn't error out - EDocPurchaseLinePOMatch.SetRange("Purchase Line SystemId", PurchaseOrderLine.SystemId); - EDocPurchaseLinePOMatch.SetFilter("Receipt Line SystemId", '<> %1', NullGuid); - if not EDocPurchaseLinePOMatch.IsEmpty() then - continue; // There's already at least one receipt line matched, so no suggestion is needed - Session.LogMessage('0000QQI', 'Suggesting receipt line for draft line matched to PO line', Verbosity::Verbose, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', 'E-Document'); - PurchaseReceiptLine.SetRange("Order No.", PurchaseOrderLine."Document No."); - PurchaseReceiptLine.SetRange("Order Line No.", PurchaseOrderLine."Line No."); - PurchaseReceiptLine.SetFilter(Quantity, '> 0'); - if PurchaseReceiptLine.FindSet() then - repeat - if GetEDocumentLineQuantityInBaseUoM(EDocumentPurchaseLine, EDocLineQuantity) then - if PurchaseReceiptLine.Quantity >= EDocLineQuantity then begin - // We suggest the first receipt line that can cover the full quantity of the E-Document line - Session.LogMessage('0000QQJ', 'Suggested covering receipt line for draft line matched to PO line', Verbosity::Verbose, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', 'E-Document'); - Clear(TempPurchaseReceiptLine); - TempPurchaseReceiptLine.DeleteAll(); - TempPurchaseReceiptLine.Copy(PurchaseReceiptLine); - TempPurchaseReceiptLine.Insert(); - MatchReceiptLinesToEDocumentLine(TempPurchaseReceiptLine, EDocumentPurchaseLine); - break; // We only suggest a single receipt line - end; - until PurchaseReceiptLine.Next() = 0; - until EDocumentPurchaseLine.Next() = 0; - end; - - local procedure GetEDocumentLineQuantityInBaseUoM(EDocumentPurchaseLine: Record "E-Document Purchase Line"; var Quantity: Decimal): Boolean + local procedure HasEDocumentLineQuantityInformation(EDocumentPurchaseLine: Record "E-Document Purchase Line"): Boolean var Item: Record Item; ItemUnitOfMeasure: Record "Item Unit of Measure"; - ItemFound, ItemUoMFound : Boolean; begin - Clear(Quantity); - if EDocumentPurchaseLine."[BC] Purchase Line Type" <> Enum::"Purchase Line Type"::Item then begin - Quantity := EDocumentPurchaseLine.Quantity; + if EDocumentPurchaseLine."[BC] Purchase Line Type" <> Enum::"Purchase Line Type"::Item then exit(true); - end; - Item.SetLoadFields("No."); - ItemFound := Item.Get(EDocumentPurchaseLine."[BC] Purchase Type No."); - ItemUnitOfMeasure.SetLoadFields("Item No.", Code, "Qty. per Unit of Measure"); - ItemUoMFound := ItemUnitOfMeasure.Get(Item."No.", EDocumentPurchaseLine."[BC] Unit of Measure"); - if not (ItemFound and ItemUoMFound) then + if not Item.Get(EDocumentPurchaseLine."[BC] Purchase Type No.") then exit(false); - Quantity := EDocumentPurchaseLine.Quantity * ItemUnitOfMeasure."Qty. per Unit of Measure"; - exit(true); + exit(ItemUnitOfMeasure.Get(Item."No.", EDocumentPurchaseLine."[BC] Unit of Measure")); end; /// @@ -759,10 +701,9 @@ codeunit 6196 "E-Doc. PO Matching" InvoicePurchaseLine, OrderPurchaseLine : Record "Purchase Line"; POMatching: Codeunit "PO Matching"; POMatchingGroup: Codeunit "PO Matching Group"; - UnitOfMeasureMgt: Codeunit "Unit of Measure Management"; - RemainingBaseToDistribute, OrderAvailableBase, QtyToAllocateBase, QtyToAllocate, QtyPerUoM : Decimal; + RemainingToDistribute, OrderAvailable, QtyToAllocate : Decimal; NullGuid: Guid; - CantDetermineEDocLineBaseQtyErr: Label 'Could not determine the quantity in base unit of measure for line %1.', Comment = '%1 = E-Document Line No.'; + CantDetermineEDocLineUnitOfMeasureErr: Label 'Could not determine the unit of measure for line %1.', Comment = '%1 = E-Document Line No.'; begin EDocumentPurchaseHeader.GetFromEDocument(EDocument); EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -773,23 +714,20 @@ codeunit 6196 "E-Doc. PO Matching" EDocPurchaseLinePOMatch.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); EDocPurchaseLinePOMatch.SetRange("Receipt Line SystemId", NullGuid); if EDocPurchaseLinePOMatch.FindSet() then begin - if not GetEDocumentLineQuantityInBaseUoM(EDocumentPurchaseLine, RemainingBaseToDistribute) then - Error(CantDetermineEDocLineBaseQtyErr, EDocumentPurchaseLine."Line No."); + if not HasEDocumentLineQuantityInformation(EDocumentPurchaseLine) then + Error(CantDetermineEDocLineUnitOfMeasureErr, EDocumentPurchaseLine."Line No."); + RemainingToDistribute := EDocumentPurchaseLine.Quantity; repeat if OrderPurchaseLine.GetBySystemId(EDocPurchaseLinePOMatch."Purchase Line SystemId") then begin - OrderAvailableBase := OrderPurchaseLine."Quantity (Base)" - OrderPurchaseLine."Qty. Invoiced (Base)"; - QtyToAllocateBase := MinDecimal(RemainingBaseToDistribute, OrderAvailableBase); - if QtyToAllocateBase > 0 then begin - QtyPerUoM := OrderPurchaseLine."Qty. per Unit of Measure"; - if QtyPerUoM = 0 then - QtyPerUoM := 1; - QtyToAllocate := UnitOfMeasureMgt.CalcQtyFromBase(QtyToAllocateBase, QtyPerUoM); + OrderAvailable := OrderPurchaseLine.Quantity - OrderPurchaseLine."Quantity Invoiced"; + QtyToAllocate := MinDecimal(RemainingToDistribute, OrderAvailable); + if QtyToAllocate > 0 then begin POMatchingGroup.AddMatch(POMatching.InvoiceOrderEdge(InvoicePurchaseLine.SystemId, OrderPurchaseLine.SystemId, QtyToAllocate)); - AddExplicitReceiptMatches(EDocumentPurchaseLine, InvoicePurchaseLine, OrderPurchaseLine, QtyToAllocateBase, POMatchingGroup); - RemainingBaseToDistribute -= QtyToAllocateBase; + AddExplicitReceiptMatches(EDocumentPurchaseLine, InvoicePurchaseLine, OrderPurchaseLine, QtyToAllocate, POMatchingGroup); + RemainingToDistribute -= QtyToAllocate; end; end; - until (EDocPurchaseLinePOMatch.Next() = 0) or (RemainingBaseToDistribute <= 0); + until (EDocPurchaseLinePOMatch.Next() = 0) or (RemainingToDistribute <= 0); end; end; until EDocumentPurchaseLine.Next() = 0; @@ -799,36 +737,34 @@ codeunit 6196 "E-Doc. PO Matching" RemoveAllMatchesForEDocument(EDocumentPurchaseHeader); end; - local procedure AddExplicitReceiptMatches(EDocumentPurchaseLine: Record "E-Document Purchase Line"; InvoicePurchaseLine: Record "Purchase Line"; OrderPurchaseLine: Record "Purchase Line"; QtyToAllocateBase: Decimal; var POMatchingGroup: Codeunit "PO Matching Group") + local procedure AddExplicitReceiptMatches(EDocumentPurchaseLine: Record "E-Document Purchase Line"; InvoicePurchaseLine: Record "Purchase Line"; OrderPurchaseLine: Record "Purchase Line"; QtyToAllocate: Decimal; var POMatchingGroup: Codeunit "PO Matching Group") var ReceiptPOMatch: Record "E-Doc. Purchase Line PO Match"; PurchaseReceiptLine: Record "Purch. Rcpt. Line"; POMatching: Codeunit "PO Matching"; - UnitOfMeasureMgt: Codeunit "Unit of Measure Management"; - RemainingBase, ReceiptAvailableBase, ReceiptQtyBase, ReceiptQty, QtyPerUoM : Decimal; + Remaining, ReceiptAvailable, ReceiptQty : Decimal; NullGuid: Guid; + ReceiptUnitOfMeasureMismatchErr: Label 'The purchase receipt line and purchase order line must have the same unit of measure.'; begin - RemainingBase := QtyToAllocateBase; + Remaining := QtyToAllocate; ReceiptPOMatch.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); ReceiptPOMatch.SetRange("Purchase Line SystemId", OrderPurchaseLine.SystemId); ReceiptPOMatch.SetFilter("Receipt Line SystemId", '<>%1', NullGuid); if not ReceiptPOMatch.FindSet() then exit; - QtyPerUoM := OrderPurchaseLine."Qty. per Unit of Measure"; - if QtyPerUoM = 0 then - QtyPerUoM := 1; repeat if PurchaseReceiptLine.GetBySystemId(ReceiptPOMatch."Receipt Line SystemId") then begin - ReceiptAvailableBase := PurchaseReceiptLine."Quantity (Base)" - PurchaseReceiptLine."Qty. Invoiced (Base)"; - ReceiptQtyBase := MinDecimal(RemainingBase, ReceiptAvailableBase); - if ReceiptQtyBase > 0 then begin - ReceiptQty := UnitOfMeasureMgt.CalcQtyFromBase(ReceiptQtyBase, QtyPerUoM); + if PurchaseReceiptLine."Unit of Measure Code" <> OrderPurchaseLine."Unit of Measure Code" then + Error(ReceiptUnitOfMeasureMismatchErr); + ReceiptAvailable := PurchaseReceiptLine."Qty. Rcd. Not Invoiced"; + ReceiptQty := MinDecimal(Remaining, ReceiptAvailable); + if ReceiptQty > 0 then begin POMatchingGroup.AddMatch(POMatching.InvoiceOrderReceiptEdge(InvoicePurchaseLine.SystemId, OrderPurchaseLine.SystemId, PurchaseReceiptLine.SystemId, ReceiptQty)); - RemainingBase -= ReceiptQtyBase; + Remaining -= ReceiptQty; end; end; - until (ReceiptPOMatch.Next() = 0) or (RemainingBase <= 0); + until (ReceiptPOMatch.Next() = 0) or (Remaining <= 0); end; local procedure MinDecimal(A: Decimal; B: Decimal): Decimal diff --git a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al index 494ef366fe7..c8231e79c61 100644 --- a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al @@ -596,8 +596,8 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); - PurchaseLine."Qty. Invoiced (Base)" := 0; - PurchaseLine."Qty. Received (Base)" := 50; + PurchaseLine."Quantity Invoiced" := 0; + PurchaseLine."Quantity Received" := 50; PurchaseLine.Modify(); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); @@ -638,8 +638,8 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); - PurchaseLine."Qty. Invoiced (Base)" := 20; - PurchaseLine."Qty. Received (Base)" := 60; + PurchaseLine."Quantity Invoiced" := 20; + PurchaseLine."Quantity Received" := 60; PurchaseLine.Modify(); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); @@ -685,8 +685,8 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); - PurchaseLine."Qty. Invoiced (Base)" := 50; - PurchaseLine."Qty. Received (Base)" := 120; + PurchaseLine."Quantity Invoiced" := 50; + PurchaseLine."Quantity Received" := 120; PurchaseLine.Modify(); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); @@ -734,8 +734,8 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); - PurchaseLine."Qty. Invoiced (Base)" := 50; - PurchaseLine."Qty. Received (Base)" := 120; + PurchaseLine."Quantity Invoiced" := 50; + PurchaseLine."Quantity Received" := 120; PurchaseLine.Modify(); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); @@ -781,8 +781,8 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); - PurchaseLine."Qty. Invoiced (Base)" := 50; - PurchaseLine."Qty. Received (Base)" := 100; + PurchaseLine."Quantity Invoiced" := 50; + PurchaseLine."Quantity Received" := 100; PurchaseLine.Modify(); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); @@ -2544,161 +2544,124 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" end; [Test] - procedure SuggestReceiptsForMatchedOrderLinesDoesNotSuggestWhenEDocLineAlreadyHasReceiptMatch() + procedure TransferPOMatchesUsesBaseAppSuggesterForMultiplePartialReceipts() var EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; PurchaseHeader: Record "Purchase Header"; PurchaseLine: Record "Purchase Line"; + PurchaseOrderHeader: Record "Purchase Header"; + PurchaseOrderLine: Record "Purchase Line"; PurchaseReceiptHeader: Record "Purch. Rcpt. Header"; - PurchaseReceiptLine: Record "Purch. Rcpt. Line"; + PurchaseReceiptLine1, PurchaseReceiptLine2 : Record "Purch. Rcpt. Line"; + MatchedOrderLine: Record "Matched Order Line"; Item: Record Item; begin Initialize(); - // [SCENARIO] SuggestReceiptsForMatchedOrderLines suggests no receipts when E-Document line already has a receipt match - // [GIVEN] An E-Document line matched to a receipt line and an additional receipt line that could be suggested CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); LibraryInventory.CreateItem(Item); - LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); - LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); - - // Match E-Document line to PO line - MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); - // Create receipt with 2 lines that could be suggested, match the last one to an E-Document line CreateMockReceiptHeader(PurchaseReceiptHeader, Vendor."No."); - CreateMockReceiptLine(PurchaseReceiptLine, PurchaseReceiptHeader, Item."No.", 10, PurchaseLine); - CreateMockReceiptLine(PurchaseReceiptLine, PurchaseReceiptHeader, Item."No.", 10, PurchaseLine); - MatchEDocumentLineToReceiptLine(EDocumentPurchaseLine, PurchaseReceiptLine); + CreateMockReceiptLine(PurchaseReceiptLine1, PurchaseReceiptHeader, Item."No.", 5, PurchaseOrderLine); + CreateMockReceiptLine(PurchaseReceiptLine2, PurchaseReceiptHeader, Item."No.", 7, PurchaseOrderLine); - // [WHEN] SuggestReceiptsForMatchedOrderLines is called - EDocPOMatching.SuggestReceiptsForMatchedOrderLines(EDocumentPurchaseHeader); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); + LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); + + EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); - // [THEN] No additional receipt lines are matched (still just the one) - Assert.IsTrue(EDocPOMatching.IsReceiptLineMatchedToEDocumentLine(PurchaseReceiptLine, EDocumentPurchaseLine), 'Expected the original receipt match to remain'); - Assert.AreEqual(1, CountReceiptMatchesForEDocumentLine(EDocumentPurchaseLine), 'Expected exactly one receipt match'); + Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine.SystemId, PurchaseReceiptLine1.SystemId, MatchedOrderLine), 'Expected the first receipt to be suggested'); + Assert.AreEqual(5, MatchedOrderLine."Qty. to Invoice", 'Expected the full first receipt quantity'); + Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine.SystemId, PurchaseReceiptLine2.SystemId, MatchedOrderLine), 'Expected the second receipt to be suggested'); + Assert.AreEqual(5, MatchedOrderLine."Qty. to Invoice", 'Expected only the remaining quantity from the second receipt'); end; [Test] - procedure SuggestReceiptsForMatchedOrderLinesSuggestsReceiptWhenPOLineHasSingleReceiptCoveringFullQuantity() + procedure TransferDistributesQuantityAcrossMatchedOrderLines() var EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; PurchaseHeader: Record "Purchase Header"; PurchaseLine: Record "Purchase Line"; - PurchaseReceiptHeader: Record "Purch. Rcpt. Header"; - PurchaseReceiptLine: Record "Purch. Rcpt. Line"; + PurchaseOrderHeader: Record "Purchase Header"; + PurchaseOrderLine1, PurchaseOrderLine2 : Record "Purchase Line"; + TempPurchaseLine: Record "Purchase Line" temporary; + MatchedOrderLine: Record "Matched Order Line"; Item: Record Item; + NullGuid: Guid; begin Initialize(); - // [SCENARIO] SuggestReceiptsForMatchedOrderLines suggests receipt when PO line has a single receipt that covers full quantity - // [GIVEN] An E-Document line matched to a purchase order line with quantity 10 - CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); - - // [GIVEN] The purchase order line has one receipt line with quantity 10 + CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 100); LibraryInventory.CreateItem(Item); - LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); - LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); - EDocumentPurchaseLine."[BC] Unit of Measure" := PurchaseLine."Unit of Measure Code"; - EDocumentPurchaseLine.Modify(); - MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine1, PurchaseOrderHeader, PurchaseOrderLine1.Type::Item, Item."No.", 60); + LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine2, PurchaseOrderHeader, PurchaseOrderLine2.Type::Item, Item."No.", 40); + TempPurchaseLine := PurchaseOrderLine1; + TempPurchaseLine.Insert(); + TempPurchaseLine := PurchaseOrderLine2; + TempPurchaseLine.Insert(); + EDocPOMatching.MatchPOLinesToEDocumentLine(TempPurchaseLine, EDocumentPurchaseLine); - CreateMockReceiptHeader(PurchaseReceiptHeader, Vendor."No."); - CreateMockReceiptLine(PurchaseReceiptLine, PurchaseReceiptHeader, Item."No.", 10, PurchaseLine); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); + LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); - // [WHEN] SuggestReceiptsForMatchedOrderLines is called - EDocPOMatching.SuggestReceiptsForMatchedOrderLines(EDocumentPurchaseHeader); + EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); - // [THEN] The receipt line is matched to the E-Document line - Assert.IsTrue(EDocPOMatching.IsReceiptLineMatchedToEDocumentLine(PurchaseReceiptLine, EDocumentPurchaseLine), 'Expected receipt line to be matched to E-Document line'); + Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine1.SystemId, NullGuid, MatchedOrderLine), 'Expected a match for the first order line'); + Assert.AreEqual(60, MatchedOrderLine."Qty. to Invoice", 'Expected the full first order line quantity'); + Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine2.SystemId, NullGuid, MatchedOrderLine), 'Expected a match for the second order line'); + Assert.AreEqual(40, MatchedOrderLine."Qty. to Invoice", 'Expected the full second order line quantity'); end; [Test] - procedure SuggestReceiptsForMatchedOrderLinesSuggestsNoReceiptWhenAllReceiptsHaveInsufficientQuantity() + procedure TransferUsesPurchaseOrderUnitOfMeasure() var EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; PurchaseHeader: Record "Purchase Header"; PurchaseLine: Record "Purchase Line"; - PurchaseReceiptHeader: Record "Purch. Rcpt. Header"; - PurchaseReceiptLine1, PurchaseReceiptLine2 : Record "Purch. Rcpt. Line"; + PurchaseOrderHeader: Record "Purchase Header"; + PurchaseOrderLine: Record "Purchase Line"; + MatchedOrderLine: Record "Matched Order Line"; Item: Record Item; + UnitOfMeasure: Record "Unit of Measure"; + ItemUnitOfMeasure: Record "Item Unit of Measure"; + NullGuid: Guid; begin Initialize(); - // [SCENARIO] SuggestReceiptsForMatchedOrderLines suggests no receipt when all receipts have insufficient quantity - // [GIVEN] An E-Document line matched to a purchase order line with quantity 10 CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); - - // [GIVEN] The purchase order line has two receipt lines with quantities 5 and 7 LibraryInventory.CreateItem(Item); - LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); - LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); - MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); - - CreateMockReceiptHeader(PurchaseReceiptHeader, Vendor."No."); - CreateMockReceiptLine(PurchaseReceiptLine1, PurchaseReceiptHeader, Item."No.", 5, PurchaseLine); - CreateMockReceiptLine(PurchaseReceiptLine2, PurchaseReceiptHeader, Item."No.", 7, PurchaseLine); - - // [WHEN] SuggestReceiptsForMatchedOrderLines is called - EDocPOMatching.SuggestReceiptsForMatchedOrderLines(EDocumentPurchaseHeader); - - // [THEN] No receipt lines are matched to the E-Document line - Assert.IsFalse(EDocPOMatching.IsEDocumentLineMatchedToAnyReceiptLine(EDocumentPurchaseLine), 'Expected no receipt lines to be matched'); - end; - - [Test] - procedure SuggestReceiptsForMatchedOrderLinesProcessesMultipleEDocumentLinesIndependently() - var - EDocument: Record "E-Document"; - EDocumentPurchaseHeader: Record "E-Document Purchase Header"; - EDocumentPurchaseLine1, EDocumentPurchaseLine2 : Record "E-Document Purchase Line"; - PurchaseHeader1, PurchaseHeader2 : Record "Purchase Header"; - PurchaseLine1, PurchaseLine2 : Record "Purchase Line"; - PurchaseReceiptHeader1, PurchaseReceiptHeader2 : Record "Purch. Rcpt. Header"; - PurchaseReceiptLine1, PurchaseReceiptLine2 : Record "Purch. Rcpt. Line"; - Item1, Item2 : Record Item; - begin - Initialize(); - // [SCENARIO] SuggestReceiptsForMatchedOrderLines processes multiple E-Document lines independently - // [GIVEN] An E-Document with two lines matched to different purchase order lines - CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine1, 10); - // Second E-Document line - EDocumentPurchaseLine2 := LibraryEDocument.InsertPurchaseDraftLine(EDocument); - EDocumentPurchaseLine2.Quantity := 15; - EDocumentPurchaseLine2.Modify(); - - // [GIVEN] Each purchase order line has its own receipt that covers the full quantity - LibraryInventory.CreateItem(Item1); - LibraryPurchase.CreatePurchHeader(PurchaseHeader1, PurchaseHeader1."Document Type"::Order, Vendor."No."); - LibraryPurchase.CreatePurchaseLine(PurchaseLine1, PurchaseHeader1, PurchaseLine1.Type::Item, Item1."No.", 10); - EDocumentPurchaseLine1."[BC] Unit of Measure" := PurchaseLine1."Unit of Measure Code"; - EDocumentPurchaseLine1.Modify(); - MatchEDocumentLineToPOLine(EDocumentPurchaseLine1, PurchaseLine1); - CreateMockReceiptHeader(PurchaseReceiptHeader1, Vendor."No."); - CreateMockReceiptLine(PurchaseReceiptLine1, PurchaseReceiptHeader1, Item1."No.", 10, PurchaseLine1); + LibraryInventory.CreateUnitOfMeasureCode(UnitOfMeasure); + LibraryInventory.CreateItemUnitOfMeasure(ItemUnitOfMeasure, Item."No.", UnitOfMeasure.Code, 12); + LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); + PurchaseOrderLine.Validate("Unit of Measure Code", UnitOfMeasure.Code); + PurchaseOrderLine.Modify(true); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); - LibraryInventory.CreateItem(Item2); - LibraryPurchase.CreatePurchHeader(PurchaseHeader2, PurchaseHeader2."Document Type"::Order, Vendor."No."); - LibraryPurchase.CreatePurchaseLine(PurchaseLine2, PurchaseHeader2, PurchaseLine2.Type::Item, Item2."No.", 15); - EDocumentPurchaseLine2."[BC] Unit of Measure" := PurchaseLine2."Unit of Measure Code"; - EDocumentPurchaseLine2.Modify(); - MatchEDocumentLineToPOLine(EDocumentPurchaseLine2, PurchaseLine2); - CreateMockReceiptHeader(PurchaseReceiptHeader2, Vendor."No."); - CreateMockReceiptLine(PurchaseReceiptLine2, PurchaseReceiptHeader2, Item2."No.", 15, PurchaseLine2); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); + PurchaseLine.Validate("Unit of Measure Code", UnitOfMeasure.Code); + PurchaseLine.Modify(true); + LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); - // [WHEN] SuggestReceiptsForMatchedOrderLines is called - EDocPOMatching.SuggestReceiptsForMatchedOrderLines(EDocumentPurchaseHeader); + EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); - // [THEN] Each E-Document line is matched to its corresponding receipt line - Assert.IsTrue(EDocPOMatching.IsReceiptLineMatchedToEDocumentLine(PurchaseReceiptLine1, EDocumentPurchaseLine1), 'Expected first receipt line to be matched to first E-Document line'); - Assert.IsTrue(EDocPOMatching.IsReceiptLineMatchedToEDocumentLine(PurchaseReceiptLine2, EDocumentPurchaseLine2), 'Expected second receipt line to be matched to second E-Document line'); + Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine.SystemId, NullGuid, MatchedOrderLine), 'Expected an invoice-order match'); + Assert.AreEqual(10, MatchedOrderLine."Qty. to Invoice", 'Expected quantity in the purchase order unit of measure'); + Assert.AreEqual(120, MatchedOrderLine."Qty. to Invoice (Base)", 'Expected BaseApp to derive the base quantity'); end; [Test] - procedure TransferPOMatchesUsesBaseAppSuggesterForMultiplePartialReceipts() + procedure TransferRejectsMissingUnitOfMeasureInformation() var EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; @@ -2707,9 +2670,6 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchaseLine: Record "Purchase Line"; PurchaseOrderHeader: Record "Purchase Header"; PurchaseOrderLine: Record "Purchase Line"; - PurchaseReceiptHeader: Record "Purch. Rcpt. Header"; - PurchaseReceiptLine1, PurchaseReceiptLine2 : Record "Purch. Rcpt. Line"; - MatchedOrderLine: Record "Matched Order Line"; Item: Record Item; begin Initialize(); @@ -2718,25 +2678,20 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); - - CreateMockReceiptHeader(PurchaseReceiptHeader, Vendor."No."); - CreateMockReceiptLine(PurchaseReceiptLine1, PurchaseReceiptHeader, Item."No.", 5, PurchaseOrderLine); - CreateMockReceiptLine(PurchaseReceiptLine2, PurchaseReceiptHeader, Item."No.", 7, PurchaseOrderLine); + EDocumentPurchaseLine."[BC] Unit of Measure" := ''; + EDocumentPurchaseLine.Modify(); LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); - EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); + asserterror EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); - Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine.SystemId, PurchaseReceiptLine1.SystemId, MatchedOrderLine), 'Expected the first receipt to be suggested'); - Assert.AreEqual(5, MatchedOrderLine."Qty. to Invoice", 'Expected the full first receipt quantity'); - Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine.SystemId, PurchaseReceiptLine2.SystemId, MatchedOrderLine), 'Expected the second receipt to be suggested'); - Assert.AreEqual(5, MatchedOrderLine."Qty. to Invoice", 'Expected only the remaining quantity from the second receipt'); + Assert.ExpectedError('Could not determine the unit of measure'); end; [Test] - procedure TransferDistributesQuantityAcrossMatchedOrderLines() + procedure TransferRejectsExplicitReceiptWithDifferentUnitOfMeasure() var EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; @@ -2744,34 +2699,32 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchaseHeader: Record "Purchase Header"; PurchaseLine: Record "Purchase Line"; PurchaseOrderHeader: Record "Purchase Header"; - PurchaseOrderLine1, PurchaseOrderLine2 : Record "Purchase Line"; - TempPurchaseLine: Record "Purchase Line" temporary; - MatchedOrderLine: Record "Matched Order Line"; + PurchaseOrderLine: Record "Purchase Line"; + PurchaseReceiptHeader: Record "Purch. Rcpt. Header"; + PurchaseReceiptLine: Record "Purch. Rcpt. Line"; Item: Record Item; - NullGuid: Guid; + UnitOfMeasure: Record "Unit of Measure"; begin Initialize(); - CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 100); + CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); LibraryInventory.CreateItem(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); - LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine1, PurchaseOrderHeader, PurchaseOrderLine1.Type::Item, Item."No.", 60); - LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine2, PurchaseOrderHeader, PurchaseOrderLine2.Type::Item, Item."No.", 40); - TempPurchaseLine := PurchaseOrderLine1; - TempPurchaseLine.Insert(); - TempPurchaseLine := PurchaseOrderLine2; - TempPurchaseLine.Insert(); - EDocPOMatching.MatchPOLinesToEDocumentLine(TempPurchaseLine, EDocumentPurchaseLine); + LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); + CreateMockReceiptHeader(PurchaseReceiptHeader, Vendor."No."); + CreateMockReceiptLine(PurchaseReceiptLine, PurchaseReceiptHeader, Item."No.", 10, PurchaseOrderLine); + LibraryInventory.CreateUnitOfMeasureCode(UnitOfMeasure); + PurchaseReceiptLine."Unit of Measure Code" := UnitOfMeasure.Code; + PurchaseReceiptLine.Modify(); + MatchEDocumentLineToReceiptLine(EDocumentPurchaseLine, PurchaseReceiptLine); LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); - LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); - EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); + asserterror EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); - Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine1.SystemId, NullGuid, MatchedOrderLine), 'Expected a match for the first order line'); - Assert.AreEqual(60, MatchedOrderLine."Qty. to Invoice", 'Expected the full first order line quantity'); - Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine2.SystemId, NullGuid, MatchedOrderLine), 'Expected a match for the second order line'); - Assert.AreEqual(40, MatchedOrderLine."Qty. to Invoice", 'Expected the full second order line quantity'); + Assert.ExpectedError('must have the same unit of measure'); end; [Test] @@ -3267,8 +3220,11 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchaseReceiptLine."Line No." := LineNo; PurchaseReceiptLine.Type := PurchaseReceiptLine.Type::Item; PurchaseReceiptLine."No." := ItemNo; + PurchaseReceiptLine."Unit of Measure Code" := PurchaseLine."Unit of Measure Code"; + PurchaseReceiptLine."Qty. per Unit of Measure" := PurchaseLine."Qty. per Unit of Measure"; PurchaseReceiptLine.Quantity := Quantity; - PurchaseReceiptLine."Quantity (Base)" := Quantity; + PurchaseReceiptLine."Quantity (Base)" := + PurchaseLine.CalcBaseQty(Quantity, PurchaseReceiptLine.FieldCaption(Quantity), PurchaseReceiptLine.FieldCaption("Quantity (Base)")); PurchaseReceiptLine."Qty. Rcd. Not Invoiced" := Quantity; PurchaseReceiptLine."Order No." := PurchaseLine."Document No."; PurchaseReceiptLine."Order Line No." := PurchaseLine."Line No."; @@ -3306,16 +3262,6 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" EDocRecordLink.Insert(); end; - local procedure CountReceiptMatchesForEDocumentLine(EDocumentPurchaseLine: Record "E-Document Purchase Line"): Integer - var - EDocPurchaseLinePOMatch: Record "E-Doc. Purchase Line PO Match"; - NullGuid: Guid; - begin - EDocPurchaseLinePOMatch.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - EDocPurchaseLinePOMatch.SetFilter("Receipt Line SystemId", '<>%1', NullGuid); - exit(EDocPurchaseLinePOMatch.Count()); - end; - local procedure CreateMockEDocumentDraftWithLine(var EDocument: Record "E-Document"; var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; var EDocumentPurchaseLine: Record "E-Document Purchase Line"; Quantity: Decimal) begin LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); From 1ac4c0c1505e6da20e7ca74c1031b2996d80176e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Mon, 24 Aug 2026 17:18:54 +0200 Subject: [PATCH 7/9] test updates --- .../EDocPOMatchingUnitTests.Codeunit.al | 172 +++++++++--------- 1 file changed, 90 insertions(+), 82 deletions(-) diff --git a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al index c8231e79c61..2113fdef7c1 100644 --- a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al @@ -808,15 +808,20 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Item: Record Item; TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; begin + // [SCENARIO] An AmountMismatch warning is raised when the invoiced unit cost exceeds the allowed matching difference Initialize(); + + // [GIVEN] A matching difference of 5% and a draft line invoicing 10 units at 110 matched to an order line of 10 units at 100 SetMatchingDifferencePct(5); CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 10, 110, 0); LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); PurchaseLine := AddOrderLineWithCost(PurchaseHeader, Item."No.", 10, 100, 0); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + // [WHEN] CalculatePOMatchWarnings is called EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + // [THEN] An AmountMismatch warning is raised for the draft line TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected AmountMismatch warning when unit cost exceeds tolerance'); @@ -833,15 +838,20 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Item: Record Item; TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; begin + // [SCENARIO] No AmountMismatch warning is raised when the invoiced unit cost stays within the allowed matching difference Initialize(); + + // [GIVEN] A matching difference of 5% and a draft line invoicing 10 units at 103 matched to an order line of 10 units at 100 SetMatchingDifferencePct(5); CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 10, 103, 0); LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); PurchaseLine := AddOrderLineWithCost(PurchaseHeader, Item."No.", 10, 100, 0); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + // [WHEN] CalculatePOMatchWarnings is called EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + // [THEN] No AmountMismatch warning is raised for the draft line TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect AmountMismatch warning within tolerance'); @@ -858,15 +868,20 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Item: Record Item; TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; begin + // [SCENARIO] The AmountMismatch calculation takes the absolute total discount on the draft line into account Initialize(); + + // [GIVEN] A matching difference of 1% and a draft line of 10 units at 100 with a total discount of 50, matched to an order line of 10 units at 100 SetMatchingDifferencePct(1); CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 10, 100, 50); LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); PurchaseLine := AddOrderLineWithCost(PurchaseHeader, Item."No.", 10, 100, 0); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + // [WHEN] CalculatePOMatchWarnings is called EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + // [THEN] An AmountMismatch warning is raised because the discounted unit cost falls outside the tolerance TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected AmountMismatch warning after applying the absolute discount'); @@ -883,15 +898,20 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Item: Record Item; TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; begin + // [SCENARIO] No AmountMismatch warning is raised for a difference below rounding precision even when the tolerance is zero Initialize(); + + // [GIVEN] A matching difference of 0% and a draft line of 1 unit at 100.001 matched to an order line of 1 unit at 100 SetMatchingDifferencePct(0); CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 1, 100.001, 0); LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); PurchaseLine := AddOrderLineWithCost(PurchaseHeader, Item."No.", 1, 100, 0); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + // [WHEN] CalculatePOMatchWarnings is called EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + // [THEN] No AmountMismatch warning is raised for the draft line TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect AmountMismatch warning for sub-rounding-precision noise'); @@ -907,7 +927,10 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Item: Record Item; TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; begin + // [SCENARIO] No AmountMismatch warning is raised when the draft line and the order line use different currencies Initialize(); + + // [GIVEN] A matching difference of 5% and a draft line of 10 units at 200 in "EUR" matched to an order line of 10 units at 100 in local currency SetMatchingDifferencePct(5); CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 10, 200, 0); LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); @@ -916,8 +939,10 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" EDocumentPurchaseLine."Currency Code" := 'EUR'; EDocumentPurchaseLine.Modify(); + // [WHEN] CalculatePOMatchWarnings is called EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + // [THEN] No AmountMismatch warning is raised because amounts are not comparable TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect AmountMismatch warning when currencies differ'); @@ -935,9 +960,14 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Item: Record Item; TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; begin + // [SCENARIO] The AmountMismatch calculation compares against the quantity-weighted average unit cost of all matched order lines Initialize(); + + // [GIVEN] A matching difference of 5% and a draft line of 1 unit at 125 SetMatchingDifferencePct(5); CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 1, 125, 0); + + // [GIVEN] The draft line is matched to order lines of 30 units at 100 and 10 units at 200 (weighted average 125) LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); PurchaseLine1 := AddOrderLineWithCost(PurchaseHeader, Item."No.", 30, 100, 0); PurchaseLine2 := AddOrderLineWithCost(PurchaseHeader, Item."No.", 10, 200, 0); @@ -947,8 +977,10 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" TempPurchaseLine.Insert(); EDocPOMatching.MatchPOLinesToEDocumentLine(TempPurchaseLine, EDocumentPurchaseLine); + // [WHEN] CalculatePOMatchWarnings is called EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + // [THEN] No AmountMismatch warning is raised because the invoiced cost equals the weighted average TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect AmountMismatch warning when invoiced cost equals the quantity-weighted average'); @@ -965,15 +997,20 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Item: Record Item; TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; begin + // [SCENARIO] No AmountMismatch warning is raised when the draft line has zero quantity Initialize(); + + // [GIVEN] A matching difference of 5% and a draft line of 0 units at 100 matched to an order line of 10 units at 50 SetMatchingDifferencePct(5); CreateMatchedAmountDraftLine(EDocumentPurchaseHeader, EDocumentPurchaseLine, Item, 0, 100, 0); LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); PurchaseLine := AddOrderLineWithCost(PurchaseHeader, Item."No.", 10, 50, 0); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + // [WHEN] CalculatePOMatchWarnings is called EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + // [THEN] No AmountMismatch warning is raised because no unit cost can be derived TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::AmountMismatch); Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect AmountMismatch warning for a zero-quantity draft line'); @@ -1833,13 +1870,17 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" TempPurchaseLine: Record "Purchase Line" temporary; Item: Record Item; begin + // [SCENARIO] Matching multiple PO lines to an E-Document line copies the dimensions from the first order line Initialize(); + + // [GIVEN] An E-Document line for a vendor LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; EDocumentPurchaseHeader.Modify(); EDocumentPurchaseLine := LibraryEDocument.InsertPurchaseDraftLine(EDocument); + // [GIVEN] A purchase order with two lines having different dimension set IDs LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); LibraryEDocument.GetGenericItem(Item); LibraryPurchase.CreatePurchaseLine(PurchaseLine1, PurchaseHeader, PurchaseLine1.Type::Item, Item."No.", 5); @@ -1853,8 +1894,10 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" TempPurchaseLine := PurchaseLine2; TempPurchaseLine.Insert(); + // [WHEN] MatchPOLinesToEDocumentLine is called with both order lines EDocPOMatching.MatchPOLinesToEDocumentLine(TempPurchaseLine, EDocumentPurchaseLine); + // [THEN] Both order lines are matched and the E-Document line carries the dimensions of the first order line Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine1, EDocumentPurchaseLine), 'Expected the first order line to be matched'); Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine2, EDocumentPurchaseLine), 'Expected the second order line to be matched'); EDocumentPurchaseLine.GetBySystemId(EDocumentPurchaseLine.SystemId); @@ -2307,47 +2350,6 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no ExceedsInvoiceableQty warning to be generated'); end; - [Test] - procedure POMatchingConfigurationNeverReceiveBlocksMatchingForNotYetReceivedLines() - var - EDocument: Record "E-Document"; - EDocumentPurchaseHeader: Record "E-Document Purchase Header"; - EDocumentPurchaseLine: Record "E-Document Purchase Line"; - PurchaseHeader: Record "Purchase Header"; - PurchaseLine: Record "Purchase Line"; - Item: Record Item; - TempPurchaseLine: Record "Purchase Line" temporary; - begin - Initialize(); - // [SCENARIO] PO matching configuration "Never receive at posting" blocks matching for not yet received PO lines - // [GIVEN] Configuration set to "Never receive at posting" and a PO line not yet received - SetupPOMatchingConfiguration(Enum::"E-Doc. PO M. Configuration"::"Never receive at posting", Vendor."No.", false); - - LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); - EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); - EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; - EDocumentPurchaseHeader.Modify(); - EDocumentPurchaseLine := LibraryEDocument.InsertPurchaseDraftLine(EDocument); - - // Create PO line that is not yet received - LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); - LibraryEDocument.GetGenericItem(Item); - LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); - PurchaseLine.Modify(); - - // Set up E-Document line to match the item - EDocumentPurchaseLine."[BC] Unit of Measure" := Item."Base Unit of Measure"; - EDocumentPurchaseLine.Quantity := 10; - EDocumentPurchaseLine.Modify(); - - TempPurchaseLine := PurchaseLine; - TempPurchaseLine.Insert(); - - // [WHEN] MatchPOLinesToEDocumentLine is called - // [THEN] An error should be raised indicating the lines are not yet received - asserterror EDocPOMatching.MatchPOLinesToEDocumentLine(TempPurchaseLine, EDocumentPurchaseLine); - end; - [Test] procedure POMatchingConfigurationReceiveOnlyForCertainVendorsAllowsMatchingForSpecifiedVendors() var @@ -2450,47 +2452,6 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsInvoiceableQty warning for non-specified vendor'); end; - [Test] - procedure POMatchingConfigurationReceiveExceptForCertainVendorsBlocksMatchingForSpecifiedVendors() - var - EDocument: Record "E-Document"; - EDocumentPurchaseHeader: Record "E-Document Purchase Header"; - EDocumentPurchaseLine: Record "E-Document Purchase Line"; - PurchaseHeader: Record "Purchase Header"; - PurchaseLine: Record "Purchase Line"; - Item: Record Item; - TempPurchaseLine: Record "Purchase Line" temporary; - begin - Initialize(); - // [SCENARIO] PO matching configuration "Receive at posting except for certain vendors" blocks matching for specified vendors - // [GIVEN] Configuration set to "Receive at posting except for certain vendors" with current vendor specified - SetupPOMatchingConfiguration(Enum::"E-Doc. PO M. Configuration"::"Receive at posting except for certain vendors", Vendor."No.", true); - - LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); - EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); - EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; - EDocumentPurchaseHeader.Modify(); - EDocumentPurchaseLine := LibraryEDocument.InsertPurchaseDraftLine(EDocument); - - // Create PO line that is not yet received - LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); - LibraryEDocument.GetGenericItem(Item); - LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); - PurchaseLine.Modify(); - - // Set up E-Document line to match the item - EDocumentPurchaseLine."[BC] Unit of Measure" := Item."Base Unit of Measure"; - EDocumentPurchaseLine.Quantity := 10; - EDocumentPurchaseLine.Modify(); - - TempPurchaseLine := PurchaseLine; - TempPurchaseLine.Insert(); - - // [WHEN] MatchPOLinesToEDocumentLine is called - // [THEN] An error should be raised indicating the lines are not yet received for this vendor - asserterror EDocPOMatching.MatchPOLinesToEDocumentLine(TempPurchaseLine, EDocumentPurchaseLine); - end; - [Test] procedure POMatchingConfigurationReceiveExceptForCertainVendorsAllowsMatchingForNonSpecifiedVendors() var @@ -2558,23 +2519,30 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" MatchedOrderLine: Record "Matched Order Line"; Item: Record Item; begin + // [SCENARIO] Transferring PO matches to the invoice lets the base application suggest quantities across multiple partial receipts Initialize(); + + // [GIVEN] An E-Document draft line of 10 units matched to an order line of 10 units CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); LibraryInventory.CreateItem(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); + // [GIVEN] Two posted receipts of 5 and 7 units for that order line CreateMockReceiptHeader(PurchaseReceiptHeader, Vendor."No."); CreateMockReceiptLine(PurchaseReceiptLine1, PurchaseReceiptHeader, Item."No.", 5, PurchaseOrderLine); CreateMockReceiptLine(PurchaseReceiptLine2, PurchaseReceiptHeader, Item."No.", 7, PurchaseOrderLine); + // [GIVEN] The E-Document line is linked to a purchase invoice line of 10 units LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); + // [WHEN] TransferPOMatchesFromEDocumentToInvoice is called EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); + // [THEN] The invoiced quantity is spread over both receipts, taking 5 from the first and only the remaining 5 from the second Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine.SystemId, PurchaseReceiptLine1.SystemId, MatchedOrderLine), 'Expected the first receipt to be suggested'); Assert.AreEqual(5, MatchedOrderLine."Qty. to Invoice", 'Expected the full first receipt quantity'); Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine.SystemId, PurchaseReceiptLine2.SystemId, MatchedOrderLine), 'Expected the second receipt to be suggested'); @@ -2596,7 +2564,10 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Item: Record Item; NullGuid: Guid; begin + // [SCENARIO] Transferring PO matches to the invoice distributes the invoiced quantity across all matched order lines Initialize(); + + // [GIVEN] An E-Document draft line of 100 units matched to order lines of 60 and 40 units CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 100); LibraryInventory.CreateItem(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); @@ -2608,12 +2579,15 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" TempPurchaseLine.Insert(); EDocPOMatching.MatchPOLinesToEDocumentLine(TempPurchaseLine, EDocumentPurchaseLine); + // [GIVEN] The E-Document line is linked to a purchase invoice line of 100 units LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); + // [WHEN] TransferPOMatchesFromEDocumentToInvoice is called EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); + // [THEN] Each order line is matched with its own full quantity, 60 and 40 Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine1.SystemId, NullGuid, MatchedOrderLine), 'Expected a match for the first order line'); Assert.AreEqual(60, MatchedOrderLine."Qty. to Invoice", 'Expected the full first order line quantity'); Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine2.SystemId, NullGuid, MatchedOrderLine), 'Expected a match for the second order line'); @@ -2636,7 +2610,10 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" ItemUnitOfMeasure: Record "Item Unit of Measure"; NullGuid: Guid; begin + // [SCENARIO] Transferring PO matches to the invoice expresses the matched quantity in the purchase order unit of measure Initialize(); + + // [GIVEN] An E-Document draft line of 10 units matched to an order line of 10 units in unit of measure "UOM" with 12 base units per unit CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); LibraryInventory.CreateItem(Item); LibraryInventory.CreateUnitOfMeasureCode(UnitOfMeasure); @@ -2647,14 +2624,17 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchaseOrderLine.Modify(true); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); + // [GIVEN] The E-Document line is linked to a purchase invoice line of 10 units in the same unit of measure LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); PurchaseLine.Validate("Unit of Measure Code", UnitOfMeasure.Code); PurchaseLine.Modify(true); LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); + // [WHEN] TransferPOMatchesFromEDocumentToInvoice is called EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); + // [THEN] The match holds 10 units in the order unit of measure and 120 base units Assert.IsTrue(GetMatchedOrderLine(PurchaseLine.SystemId, PurchaseOrderLine.SystemId, NullGuid, MatchedOrderLine), 'Expected an invoice-order match'); Assert.AreEqual(10, MatchedOrderLine."Qty. to Invoice", 'Expected quantity in the purchase order unit of measure'); Assert.AreEqual(120, MatchedOrderLine."Qty. to Invoice (Base)", 'Expected BaseApp to derive the base quantity'); @@ -2672,21 +2652,29 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchaseOrderLine: Record "Purchase Line"; Item: Record Item; begin + // [SCENARIO] Transferring PO matches to the invoice fails when the unit of measure cannot be determined Initialize(); + + // [GIVEN] An E-Document draft line of 10 units matched to an order line, with the unit of measure cleared on the draft line CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); LibraryInventory.CreateItem(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); + EDocumentPurchaseLine.SetRecFilter(); + EDocumentPurchaseLine.FindFirst(); EDocumentPurchaseLine."[BC] Unit of Measure" := ''; EDocumentPurchaseLine.Modify(); + // [GIVEN] The E-Document line is linked to a purchase invoice line of 10 units LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); + // [WHEN] TransferPOMatchesFromEDocumentToInvoice is called asserterror EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); + // [THEN] An error about the unit of measure not being determinable is thrown Assert.ExpectedError('Could not determine the unit of measure'); end; @@ -2705,12 +2693,17 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Item: Record Item; UnitOfMeasure: Record "Unit of Measure"; begin + // [SCENARIO] Transferring PO matches to the invoice fails when an explicitly matched receipt uses a different unit of measure Initialize(); + + // [GIVEN] An E-Document draft line of 10 units matched to an order line of 10 units CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); LibraryInventory.CreateItem(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); + + // [GIVEN] The draft line is also matched to a receipt line that uses a different unit of measure CreateMockReceiptHeader(PurchaseReceiptHeader, Vendor."No."); CreateMockReceiptLine(PurchaseReceiptLine, PurchaseReceiptHeader, Item."No.", 10, PurchaseOrderLine); LibraryInventory.CreateUnitOfMeasureCode(UnitOfMeasure); @@ -2718,12 +2711,15 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchaseReceiptLine.Modify(); MatchEDocumentLineToReceiptLine(EDocumentPurchaseLine, PurchaseReceiptLine); + // [GIVEN] The E-Document line is linked to a purchase invoice line of 10 units LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); + // [WHEN] TransferPOMatchesFromEDocumentToInvoice is called asserterror EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); + // [THEN] An error about mismatching units of measure is thrown Assert.ExpectedError('must have the same unit of measure'); end; @@ -2789,7 +2785,10 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" PurchaseReceiptLine: Record "Purch. Rcpt. Line"; Item: Record Item; begin + // [SCENARIO] Transferring PO matches back from the invoice reconstructs the E-Document order and receipt matches Initialize(); + + // [GIVEN] An E-Document draft line of 10 units matched to an order line and to its receipt line CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); LibraryInventory.CreateItem(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); @@ -2799,13 +2798,16 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" CreateMockReceiptLine(PurchaseReceiptLine, PurchaseReceiptHeader, Item."No.", 10, PurchaseOrderLine); MatchEDocumentLineToReceiptLine(EDocumentPurchaseLine, PurchaseReceiptLine); + // [GIVEN] The matches have been transferred to a linked purchase invoice line LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); + // [WHEN] TransferPOMatchesFromInvoiceToEDocument is called EDocPOMatching.TransferPOMatchesFromInvoiceToEDocument(PurchaseHeader); + // [THEN] Both the order match and the receipt match are restored on the E-Document line Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseOrderLine, EDocumentPurchaseLine), 'Expected the order match to be reconstructed'); Assert.IsTrue(EDocPOMatching.IsReceiptLineMatchedToEDocumentLine(PurchaseReceiptLine, EDocumentPurchaseLine), 'Expected the receipt match to be reconstructed'); end; @@ -2826,7 +2828,10 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" POMatching: Codeunit "PO Matching"; POMatchingGroup: Codeunit "PO Matching Group"; begin + // [SCENARIO] Transferring PO matches back from the invoice reconstructs matches when only part of the invoiced quantity is covered by a receipt Initialize(); + + // [GIVEN] An E-Document draft line of 10 units and a purchase order with two lines of 5 units, where only the first line has a receipt CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); LibraryInventory.CreateItem(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); @@ -2835,6 +2840,7 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" CreateMockReceiptHeader(PurchaseReceiptHeader, Vendor."No."); CreateMockReceiptLine(PurchaseReceiptLine, PurchaseReceiptHeader, Item."No.", 5, PurchaseOrderLine1); + // [GIVEN] The linked invoice line carries matches to both order lines and to the receipt of the first order line LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); LinkEDocumentLineToPurchaseLine(EDocument, EDocumentPurchaseLine, PurchaseLine); @@ -2843,8 +2849,10 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" POMatchingGroup.AddMatch(POMatching.InvoiceOrderEdge(PurchaseLine.SystemId, PurchaseOrderLine2.SystemId, 5)); POMatchingGroup.SaveMatchingGroups(); + // [WHEN] TransferPOMatchesFromInvoiceToEDocument is called EDocPOMatching.TransferPOMatchesFromInvoiceToEDocument(PurchaseHeader); + // [THEN] Both order matches and the partial receipt match are restored on the E-Document line Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseOrderLine1, EDocumentPurchaseLine), 'Expected the first order match to be reconstructed'); Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseOrderLine2, EDocumentPurchaseLine), 'Expected the second order match to be reconstructed'); Assert.IsTrue(EDocPOMatching.IsReceiptLineMatchedToEDocumentLine(PurchaseReceiptLine, EDocumentPurchaseLine), 'Expected the partial receipt match to be reconstructed'); From c0e8793400c7ea24233f538580af13dc04993bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Tue, 25 Aug 2026 08:56:29 +0200 Subject: [PATCH 8/9] harden genposting setup in edoc test library --- .../Test/src/LibraryEDocument.Codeunit.al | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Apps/W1/EDocument/Test/src/LibraryEDocument.Codeunit.al b/src/Apps/W1/EDocument/Test/src/LibraryEDocument.Codeunit.al index 81f75dc223e..f81377c1055 100644 --- a/src/Apps/W1/EDocument/Test/src/LibraryEDocument.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/LibraryEDocument.Codeunit.al @@ -183,6 +183,7 @@ codeunit 139629 "Library - E-Document" begin WorkflowSetup.InitWorkflow(); SetupCompanyInfo(); + CleanGeneralPostingSetup(); // Create Customer for sales scenario LibraryPurchase.CreateVendor(Vendor); @@ -217,6 +218,35 @@ codeunit 139629 "Library - E-Document" LibraryItemReference.CreateItemReference(ItemReference, ExtraItem."No.", '', 'PCS', Enum::"Item Reference Type"::Vendor, Vendor."No.", '2000'); end; + local procedure CleanGeneralPostingSetup() + var + GeneralPostingSetup: Record "General Posting Setup"; + GeneralPostingSetupToDelete: Record "General Posting Setup"; + begin + if GeneralPostingSetup.FindSet() then + repeat + if IsGeneralPostingSetupOrphaned(GeneralPostingSetup) then begin + GeneralPostingSetupToDelete := GeneralPostingSetup; + GeneralPostingSetupToDelete.Delete(); + end; + until GeneralPostingSetup.Next() = 0; + end; + + local procedure IsGeneralPostingSetupOrphaned(GeneralPostingSetup: Record "General Posting Setup"): Boolean + var + GenBusinessPostingGroup: Record "Gen. Business Posting Group"; + GenProductPostingGroup: Record "Gen. Product Posting Group"; + begin + if (GeneralPostingSetup."Gen. Bus. Posting Group" <> '') and + (not GenBusinessPostingGroup.Get(GeneralPostingSetup."Gen. Bus. Posting Group")) + then + exit(true); + + exit( + (GeneralPostingSetup."Gen. Prod. Posting Group" <> '') and + (not GenProductPostingGroup.Get(GeneralPostingSetup."Gen. Prod. Posting Group"))); + end; + procedure PostInvoice(var Customer: Record Customer) SalesInvHeader: Record "Sales Invoice Header"; var SalesHeader: Record "Sales Header"; From c194112928845f6441102c66564166ae36513a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Tue, 25 Aug 2026 14:33:27 +0200 Subject: [PATCH 9/9] stale vat product posting group hardening --- .../Test/src/LibraryEDocument.Codeunit.al | 41 +++++++++++-------- .../EDocPOMatchingUnitTests.Codeunit.al | 30 +++++++------- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/Apps/W1/EDocument/Test/src/LibraryEDocument.Codeunit.al b/src/Apps/W1/EDocument/Test/src/LibraryEDocument.Codeunit.al index f81377c1055..9c8340100d6 100644 --- a/src/Apps/W1/EDocument/Test/src/LibraryEDocument.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/LibraryEDocument.Codeunit.al @@ -117,12 +117,7 @@ codeunit 139629 "Library - E-Document" end; // Create Item - if StandardItem."No." = '' then begin - VATPostingSetup.TestField("VAT Prod. Posting Group"); - CreateGenericItem(StandardItem); - StandardItem."VAT Prod. Posting Group" := VATPostingSetup."VAT Prod. Posting Group"; - StandardItem.Modify(); - end; + EnsureStandardItemExists(); SalesSetup.Get(); SalesSetup."Invoice Rounding" := false; @@ -131,10 +126,18 @@ codeunit 139629 "Library - E-Document" procedure GetGenericItem(var Item: Record Item) begin - if (StandardItem."No." = '') or not Item.Get(StandardItem."No.") then begin - CreateGenericItem(StandardItem); - Item.Get(StandardItem."No."); - end; + EnsureStandardItemExists(); + Item.Get(StandardItem."No."); + end; + + local procedure EnsureStandardItemExists() + var + Item: Record Item; + begin + if (StandardItem."No." <> '') and Item.Get(StandardItem."No.") then + exit; + + CreateItemWithStandardVAT(StandardItem); end; #if not CLEAN26 @@ -200,10 +203,7 @@ codeunit 139629 "Library - E-Document" Vendor.Modify(true); // Create Item - if StandardItem."No." = '' then begin - VATPostingSetup.TestField("VAT Prod. Posting Group"); - CreateGenericItem(StandardItem, VATPostingSetup."VAT Prod. Posting Group"); - end; + EnsureStandardItemExists(); UnitOfMeasure.Init(); UnitOfMeasure."International Standard Code" := 'PCS'; @@ -479,6 +479,13 @@ codeunit 139629 "Library - E-Document" Item.Modify(); end; + procedure CreateItemWithStandardVAT(var Item: Record Item) + begin + SetupStandardVAT(); + VATPostingSetup.TestField("VAT Prod. Posting Group"); + CreateGenericItem(Item, VATPostingSetup."VAT Prod. Posting Group"); + end; + procedure CreateGenericItem(var Item: Record Item) begin CreateItemWithPrice(Item, LibraryRandom.RandInt(10)); @@ -521,8 +528,7 @@ codeunit 139629 "Library - E-Document" begin CreateGenericSalesHeader(Customer, SalesHeader, DocumentType); - if StandardItem."No." = '' then - CreateGenericItem(StandardItem); + EnsureStandardItemExists(); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, StandardItem."No.", 1); end; @@ -530,8 +536,7 @@ codeunit 139629 "Library - E-Document" procedure CreatePurchaseOrderWithLine(var Vendor: Record Vendor; var PurchaseHeader: Record "Purchase Header"; var PurchaseLine: Record "Purchase Line"; Quantity: Decimal) begin LibraryPurchase.CreatePurchHeader(PurchaseHeader, Enum::"Purchase Document Type"::Order, Vendor."No."); - if StandardItem."No." = '' then - CreateGenericItem(StandardItem); + EnsureStandardItemExists(); LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, StandardItem."No.", Quantity); end; diff --git a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al index 2113fdef7c1..6a50835f1f5 100644 --- a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al @@ -2524,7 +2524,7 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [GIVEN] An E-Document draft line of 10 units matched to an order line of 10 units CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); - LibraryInventory.CreateItem(Item); + LibraryEDocument.CreateItemWithStandardVAT(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); @@ -2569,7 +2569,7 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [GIVEN] An E-Document draft line of 100 units matched to order lines of 60 and 40 units CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 100); - LibraryInventory.CreateItem(Item); + LibraryEDocument.CreateItemWithStandardVAT(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine1, PurchaseOrderHeader, PurchaseOrderLine1.Type::Item, Item."No.", 60); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine2, PurchaseOrderHeader, PurchaseOrderLine2.Type::Item, Item."No.", 40); @@ -2615,7 +2615,7 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [GIVEN] An E-Document draft line of 10 units matched to an order line of 10 units in unit of measure "UOM" with 12 base units per unit CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); - LibraryInventory.CreateItem(Item); + LibraryEDocument.CreateItemWithStandardVAT(Item); LibraryInventory.CreateUnitOfMeasureCode(UnitOfMeasure); LibraryInventory.CreateItemUnitOfMeasure(ItemUnitOfMeasure, Item."No.", UnitOfMeasure.Code, 12); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); @@ -2657,7 +2657,7 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [GIVEN] An E-Document draft line of 10 units matched to an order line, with the unit of measure cleared on the draft line CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); - LibraryInventory.CreateItem(Item); + LibraryEDocument.CreateItemWithStandardVAT(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); @@ -2698,7 +2698,7 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [GIVEN] An E-Document draft line of 10 units matched to an order line of 10 units CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); - LibraryInventory.CreateItem(Item); + LibraryEDocument.CreateItemWithStandardVAT(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); @@ -2743,7 +2743,7 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [SCENARIO] TransferPOMatchesFromEDocumentToInvoice transfers receipt match to linked invoice line and removes E-Document matches // [GIVEN] An E-Document line matched to a receipt line CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); - LibraryInventory.CreateItem(Item); + LibraryEDocument.CreateItemWithStandardVAT(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); @@ -2790,7 +2790,7 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [GIVEN] An E-Document draft line of 10 units matched to an order line and to its receipt line CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); - LibraryInventory.CreateItem(Item); + LibraryEDocument.CreateItemWithStandardVAT(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseOrderLine); @@ -2833,7 +2833,7 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [GIVEN] An E-Document draft line of 10 units and a purchase order with two lines of 5 units, where only the first line has a receipt CreateMockEDocumentDraftWithLine(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine, 10); - LibraryInventory.CreateItem(Item); + LibraryEDocument.CreateItemWithStandardVAT(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine1, PurchaseOrderHeader, PurchaseOrderLine1.Type::Item, Item."No.", 5); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine2, PurchaseOrderHeader, PurchaseOrderLine2.Type::Item, Item."No.", 5); @@ -2885,11 +2885,11 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // Create purchase order with three lines LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); - LibraryInventory.CreateItem(Item1); + LibraryEDocument.CreateItemWithStandardVAT(Item1); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine1, PurchaseOrderHeader, PurchaseOrderLine1.Type::Item, Item1."No.", 10); - LibraryInventory.CreateItem(Item2); + LibraryEDocument.CreateItemWithStandardVAT(Item2); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine2, PurchaseOrderHeader, PurchaseOrderLine2.Type::Item, Item2."No.", 15); - LibraryInventory.CreateItem(Item3); + LibraryEDocument.CreateItemWithStandardVAT(Item3); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine3, PurchaseOrderHeader, PurchaseOrderLine3.Type::Item, Item3."No.", 20); // Create receipt lines @@ -2944,7 +2944,7 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Initialize(); // [SCENARIO] TransferPOMatchesFromInvoiceToEDocument creates matches from invoice line receipt info and clears invoice receipt info // [GIVEN] A purchase invoice line with Receipt No. and Receipt Line No. - LibraryInventory.CreateItem(Item); + LibraryEDocument.CreateItemWithStandardVAT(Item); LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine, PurchaseOrderHeader, PurchaseOrderLine.Type::Item, Item."No.", 10); @@ -2994,11 +2994,11 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [SCENARIO] TransferPOMatchesFromInvoiceToEDocument processes multiple lines independently // [GIVEN] A purchase invoice with three lines, each with different receipt information LibraryPurchase.CreatePurchHeader(PurchaseOrderHeader, PurchaseOrderHeader."Document Type"::Order, Vendor."No."); - LibraryInventory.CreateItem(Item1); + LibraryEDocument.CreateItemWithStandardVAT(Item1); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine1, PurchaseOrderHeader, PurchaseOrderLine1.Type::Item, Item1."No.", 10); - LibraryInventory.CreateItem(Item2); + LibraryEDocument.CreateItemWithStandardVAT(Item2); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine2, PurchaseOrderHeader, PurchaseOrderLine2.Type::Item, Item2."No.", 15); - LibraryInventory.CreateItem(Item3); + LibraryEDocument.CreateItemWithStandardVAT(Item3); LibraryPurchase.CreatePurchaseLine(PurchaseOrderLine3, PurchaseOrderHeader, PurchaseOrderLine3.Type::Item, Item3."No.", 20); CreateMockReceiptHeader(PurchaseReceiptHeader, Vendor."No.");