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 e26ff50213..061e16efe4 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 @@ -42,6 +42,7 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, 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); @@ -50,9 +51,12 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, EDocPOMatching.SuggestReceiptsForMatchedOrderLines(EDocumentPurchaseHeader); EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); - TempPOMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::NotYetReceived); + 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); IEDocumentFinishPurchaseDraft := EDocImportParameters."Processing Customizations"; if EDocImportParameters."Existing Doc. RecordId" <> EmptyRecordId then begin 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 581ed8ed08..210ad9e8a9 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 @@ -55,6 +55,11 @@ page 6183 "E-Doc. Purchase Draft Subform" Visible = HasEDocumentOrderMatchWarnings; StyleExpr = MatchWarningsStyleExpr; ToolTip = 'Specifies any warnings related to matching this line to a purchase order line.'; + + trigger OnDrillDown() + begin + ShowMatchWarningDetails(); + end; } field("Line Type"; Rec."[BC] Purchase Line Type") { @@ -348,11 +353,6 @@ page 6183 "E-Doc. Purchase Draft Subform" end; trigger OnAfterGetRecord() - var - MissingInfoLbl: Label 'Missing information for match'; - NotYetReceivedLbl: Label 'Not yet received'; - QuantityMismatchLbl: Label 'Quantity mismatch'; - NoWarningsLbl: Label 'No warnings'; begin if EDocumentPurchaseLine.Get(Rec."E-Document Entry No.", Rec."Line No.") then; AdditionalColumns := Rec.AdditionalColumnsDisplayText(); @@ -361,21 +361,7 @@ page 6183 "E-Doc. Purchase Draft Subform" IsLineMatchedToOrderLine := EDocPOMatching.IsEDocumentLineMatchedToAnyPOLine(EDocumentPurchaseLine); IsLineMatchedToReceiptLine := EDocPOMatching.IsEDocumentLineMatchedToAnyReceiptLine(EDocumentPurchaseLine); OrderMatchedCaption := IsLineMatchedToOrderLine ? GetSummaryOfMatchedOrders() : ''; - MatchWarningsStyleExpr := 'None'; - EDocumentPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", Rec.SystemId); - if EDocumentPOMatchWarnings.FindFirst() then begin - case EDocumentPOMatchWarnings."Warning Type" of - Enum::"E-Doc PO Match Warning"::MissingInformationForMatch: - MatchWarningsCaption := MissingInfoLbl; - Enum::"E-Doc PO Match Warning"::NotYetReceived: - MatchWarningsCaption := NotYetReceivedLbl; - Enum::"E-Doc PO Match Warning"::QuantityMismatch: - MatchWarningsCaption := QuantityMismatchLbl; - end; - MatchWarningsStyleExpr := 'Ambiguous'; - end - else - MatchWarningsCaption := NoWarningsLbl; + UpdateMatchWarnings(); end; internal procedure SetEDocumentPurchaseHeader(EDocPurchHeader: Record "E-Document Purchase Header") @@ -498,4 +484,85 @@ page 6183 "E-Doc. Purchase Draft Subform" exit(StrSubstNo(MatchedToSingleOrderMultipleLinesLbl, MatchedPO)); end; + local procedure UpdateMatchWarnings() + var + MissingInfoLbl: Label 'Unit of measure information is missing'; + ExceedsInvoiceableQtyLbl: Label 'Exceeds quantity received'; + ExceedsRemainingToInvoiceLbl: Label 'Exceeds remaining to invoice'; + OverReceiptLbl: Label 'Over-receipt'; + NoWarningsLbl: Label 'No warnings'; + MultipleWarningsLbl: Label 'Multiple warnings'; + MostSevereStyle: Text; + SeverityLevel: Integer; + CurrentSeverity: Integer; + begin + MatchWarningsCaption := NoWarningsLbl; + MatchWarningsStyleExpr := 'None'; + + EDocumentPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", Rec.SystemId); + + // Severity: Unfavorable (critical) > Ambiguous (warning) > Subordinate (info) + SeverityLevel := 0; + if EDocumentPOMatchWarnings.FindSet() then + repeat + case EDocumentPOMatchWarnings."Warning Type" of + Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty: + begin + CurrentSeverity := 3; + MatchWarningsCaption := ExceedsInvoiceableQtyLbl; + MostSevereStyle := 'Unfavorable'; + end; + Enum::"E-Doc PO Match Warning"::MissingInformationForMatch: + begin + CurrentSeverity := 3; + MatchWarningsCaption := MissingInfoLbl; + MostSevereStyle := 'Unfavorable'; + end; + Enum::"E-Doc PO Match Warning"::ExceedsRemainingToInvoice: + begin + CurrentSeverity := 2; + MatchWarningsCaption := ExceedsRemainingToInvoiceLbl; + MostSevereStyle := 'Ambiguous'; + end; + Enum::"E-Doc PO Match Warning"::OverReceipt: + begin + CurrentSeverity := 1; + MatchWarningsCaption := OverReceiptLbl; + MostSevereStyle := 'Subordinate'; + end; + end; + if CurrentSeverity > SeverityLevel then begin + SeverityLevel := CurrentSeverity; + MatchWarningsStyleExpr := MostSevereStyle; + end; + until EDocumentPOMatchWarnings.Next() = 0; + + if EDocumentPOMatchWarnings.Count() > 1 then + MatchWarningsCaption := MultipleWarningsLbl; + end; + + local procedure ShowMatchWarningDetails() + var + WarningDetails: TextBuilder; + MissingInfoDetailLbl: Label 'Quantity information for this line is missing to complete the match. Verify that the draft line has a unit of measure assigned for this item.'; + begin + EDocumentPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", Rec.SystemId); + if not EDocumentPOMatchWarnings.FindSet() then + exit; + + repeat + case EDocumentPOMatchWarnings."Warning Type" of + Enum::"E-Doc PO Match Warning"::MissingInformationForMatch: + WarningDetails.AppendLine('• ' + MissingInfoDetailLbl); + Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty, + Enum::"E-Doc PO Match Warning"::ExceedsRemainingToInvoice, + Enum::"E-Doc PO Match Warning"::OverReceipt: + WarningDetails.AppendLine('• ' + EDocumentPOMatchWarnings."Warning Message"); + end; + until EDocumentPOMatchWarnings.Next() = 0; + + if WarningDetails.Length() > 0 then + Message(WarningDetails.ToText()); + end; + } 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 813c0e7e8e..c763fcde25 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 @@ -16,4 +16,13 @@ enum 6111 "E-Doc PO Match Warning" value(2; MissingInformationForMatch) { } + value(3; ExceedsInvoiceableQty) + { + } + value(4; ExceedsRemainingToInvoice) + { + } + value(5; OverReceipt) + { + } } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Table.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Table.al index 8a62481cd4..36bf07065d 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Table.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Table.al @@ -25,6 +25,12 @@ table 6115 "E-Doc PO Match Warning" Caption = 'Warning Type'; Editable = false; } + field(3; "Warning Message"; Text[250]) + { + DataClassification = SystemMetadata; + Caption = 'Warning Message'; + Editable = false; + } } keys { 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 250f44236d..1aae56fe46 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 @@ -213,6 +213,10 @@ codeunit 6196 "E-Doc. PO Matching" TempPurchaseLine: Record "Purchase Line" temporary; EDocLineQuantity: Decimal; PurchaseLinesQuantity, PurchaseLinesQuantityInvoiced, PurchaseLinesQuantityReceived : Decimal; + RemainingToInvoice, InvoiceableQty : 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'; begin LoadPOLinesMatchedToEDocumentLine(EDocumentPurchaseLine, TempPurchaseLine); PurchaseLinesQuantityInvoiced := 0; @@ -232,17 +236,37 @@ codeunit 6196 "E-Doc. PO Matching" POMatchWarnings.Insert(); exit; end; - if EDocLineQuantity <> PurchaseLinesQuantity - PurchaseLinesQuantityInvoiced then begin - POMatchWarnings."E-Doc. Purchase Line SystemId" := EDocumentPurchaseLine.SystemId; - POMatchWarnings."Warning Type" := "E-Doc PO Match Warning"::QuantityMismatch; - POMatchWarnings.Insert(); - end; - if (EDocLineQuantity + PurchaseLinesQuantityInvoiced) > PurchaseLinesQuantityReceived then + + // I = Invoice quantity (from the e-document line) + // R = Remaining to invoice on the PO (Ordered - Previously Invoiced) + // J = Invoiceable quantity (Received - Previously Invoiced) + RemainingToInvoice := PurchaseLinesQuantity - PurchaseLinesQuantityInvoiced; + InvoiceableQty := PurchaseLinesQuantityReceived - PurchaseLinesQuantityInvoiced; + + // I > J: Invoice exceeds what has been received and not yet invoiced + if EDocLineQuantity > InvoiceableQty then if ShouldWarnIfNotYetReceived(EDocumentPurchaseLine.GetBCVendor()."No.") then begin POMatchWarnings."E-Doc. Purchase Line SystemId" := EDocumentPurchaseLine.SystemId; - POMatchWarnings."Warning Type" := "E-Doc PO Match Warning"::NotYetReceived; + POMatchWarnings."Warning Type" := "E-Doc PO Match Warning"::ExceedsInvoiceableQty; + POMatchWarnings."Warning Message" := CopyStr(StrSubstNo(ExceedsInvoiceableQtyLbl, EDocLineQuantity, InvoiceableQty, EDocLineQuantity - InvoiceableQty), 1, MaxStrLen(POMatchWarnings."Warning Message")); POMatchWarnings.Insert(); end; + + // I > R: Invoice exceeds what remains on the order + if EDocLineQuantity > RemainingToInvoice then begin + POMatchWarnings."E-Doc. Purchase Line SystemId" := EDocumentPurchaseLine.SystemId; + POMatchWarnings."Warning Type" := "E-Doc PO Match Warning"::ExceedsRemainingToInvoice; + POMatchWarnings."Warning Message" := CopyStr(StrSubstNo(ExceedsRemainingToInvoiceLbl, EDocLineQuantity, RemainingToInvoice, EDocLineQuantity - RemainingToInvoice), 1, MaxStrLen(POMatchWarnings."Warning Message")); + POMatchWarnings.Insert(); + end; + + // I = R and I < J: Order will be closed but there is an over-receipt + if (EDocLineQuantity = RemainingToInvoice) and (EDocLineQuantity < InvoiceableQty) then begin + POMatchWarnings."E-Doc. Purchase Line SystemId" := EDocumentPurchaseLine.SystemId; + POMatchWarnings."Warning Type" := "E-Doc PO Match Warning"::OverReceipt; + POMatchWarnings."Warning Message" := CopyStr(StrSubstNo(OverReceiptLbl, InvoiceableQty - RemainingToInvoice), 1, MaxStrLen(POMatchWarnings."Warning Message")); + POMatchWarnings.Insert(); + end; end; /// @@ -475,7 +499,7 @@ codeunit 6196 "E-Doc. PO Matching" EDocumentPurchaseLine."[BC] Unit of Measure" := MatchedUnitOfMeasure; EDocumentPurchaseLine.Modify(); AppendPOMatchWarnings(EDocumentPurchaseLine, TempMatchWarnings); - TempMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::NotYetReceived); + TempMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::ExceedsInvoiceableQty); if (not TempMatchWarnings.IsEmpty) and (not CanMatchInvoiceLineToPOLineWithoutReceipt(EDocumentPurchaseLine, PurchaseLine)) then Error(NotYetReceivedErr); end; @@ -763,8 +787,8 @@ codeunit 6196 "E-Doc. PO Matching" TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; begin CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); - TempPOMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::NotYetReceived); - // For each line that has a Not Yet Received warning, we check if it can be matched without receipt + TempPOMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::ExceedsInvoiceableQty); + // For each line that exceeds invoiceable qty, we check if it can be matched without receipt if TempPOMatchWarnings.FindSet() then repeat EDocumentPurchaseLine.GetBySystemId(TempPOMatchWarnings."E-Doc. Purchase Line SystemId"); 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 4133a2fb53..83f23def22 100644 --- a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al @@ -561,8 +561,12 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected MissingInformationForMatch warning for line with non-existent unit of measure'); end; + // Quantity warning tests use the following notation: + // I = Invoice quantity (from the e-document line) + // R = Remaining to invoice on the PO (Ordered - Previously Invoiced) + // J = Invoiceable quantity (Received - Previously Invoiced) [Test] - procedure CalculatePOMatchWarningsGeneratesQuantityMismatchWarning() + procedure CalculatePOMatchWarningsNoWarningsWhenInvoiceWithinOrderAndReceipts() var EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; @@ -573,40 +577,85 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; begin Initialize(); - // [SCENARIO] Calculating PO match warnings generates quantity mismatch warning when quantities don't match - // [GIVEN] An E-Document with lines where calculated quantity differs from original quantity + // [SCENARIO] I < R and I < J — partial invoice within order and receipts generates no warnings + // [GIVEN] PO=100, I=30, Rcv=50, PrevInv=0 → R=100, J=50 LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); + EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); + EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; + EDocumentPurchaseHeader.Modify(); - // Create a purchase order line with 10 units 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 := 30; + EDocumentPurchaseLine.Modify(); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); - LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); + PurchaseLine."Qty. Invoiced (Base)" := 0; + PurchaseLine."Qty. Received (Base)" := 50; + PurchaseLine.Modify(); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); - // Create E-Document Purchase Header + // [WHEN] CalculatePOMatchWarnings is called + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + + // [THEN] No warnings should be generated + TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no warnings for (I < R, I < J)'); + end; + + [Test] + procedure CalculatePOMatchWarningsGeneratesExceedsInvoiceableQtyWarning() + 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; + TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; + begin + Initialize(); + // [SCENARIO] I < R but I > J — invoice within order but exceeds uninvoiced receipts + // [GIVEN] PO=100, I=50, Rcv=60, PrevInv=20 → R=80, J=40 + LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; EDocumentPurchaseHeader.Modify(); - // Set up E-Document line to create quantity mismatch + 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.Quantity := 100; + EDocumentPurchaseLine."[BC] Unit of Measure" := Item."Base Unit of Measure"; + EDocumentPurchaseLine.Quantity := 50; EDocumentPurchaseLine.Modify(); + 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.Modify(); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); // [WHEN] CalculatePOMatchWarnings is called EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); - // [THEN] QuantityMismatch warnings should be generated + // [THEN] ExceedsInvoiceableQty warning should be generated TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::QuantityMismatch); - Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected QuantityMismatch warning to be generated'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsInvoiceableQty warning for (I < R, I > J)'); + + // [THEN] ExceedsRemainingToInvoice warning should NOT be generated + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsRemainingToInvoice); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect ExceedsRemainingToInvoice warning'); end; [Test] - procedure CalculatePOMatchWarningsGeneratesNotYetReceivedWarning() + procedure CalculatePOMatchWarningsGeneratesOverReceiptWarning() var EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; @@ -617,39 +666,134 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; begin Initialize(); - // [SCENARIO] Calculating PO match warnings generates not yet received warning when trying to invoice more than received - // [GIVEN] An E-Document with lines where E-Doc quantity plus already invoiced quantity exceeds received quantity + // [SCENARIO] I = R and I < J — invoice closes out order but there is an over-receipt + // [GIVEN] PO=100, I=50, Rcv=120, PrevInv=50 → R=50, J=70 LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); + EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); + EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; + EDocumentPurchaseHeader.Modify(); - // Create E-Document Purchase Header and Line + 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 := 50; + EDocumentPurchaseLine.Modify(); + + 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.Modify(); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + + // [WHEN] CalculatePOMatchWarnings is called + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + + // [THEN] OverReceipt warning should be generated + TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::OverReceipt); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected OverReceipt warning for (I = R, I < J)'); + + // [THEN] No other quantity warnings should be generated + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect ExceedsInvoiceableQty warning'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsRemainingToInvoice); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect ExceedsRemainingToInvoice warning'); + end; + + [Test] + procedure CalculatePOMatchWarningsGeneratesExceedsRemainingToInvoiceWarning() + 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; + TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; + begin + Initialize(); + // [SCENARIO] I > R but I < J — invoice exceeds order but within receipts (over-receipt charged) + // [GIVEN] PO=100, I=60, Rcv=120, PrevInv=50 → R=50, J=70 + 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 := 60; + EDocumentPurchaseLine.Modify(); + + 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.Modify(); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + + // [WHEN] CalculatePOMatchWarnings is called + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + + // [THEN] ExceedsRemainingToInvoice warning should be generated + TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsRemainingToInvoice); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsRemainingToInvoice warning (I > R, I < J)'); + + // [THEN] ExceedsInvoiceableQty warning should NOT be generated + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect ExceedsInvoiceableQty warning'); + end; + + [Test] + procedure CalculatePOMatchWarningsGeneratesBothWarningsWhenExceedingBothRemainingAndInvoiceable() + 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; + TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; + begin + Initialize(); + // [SCENARIO] I > R and I > J — invoice exceeds both order and receipts + // [GIVEN] PO=100, I=80, Rcv=100, PrevInv=50 → R=50, J=50 + LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); + EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); + EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; + EDocumentPurchaseHeader.Modify(); - // Set up E-Document line 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 := 15; // More than what's received (10) + EDocumentPurchaseLine.Quantity := 80; EDocumentPurchaseLine.Modify(); - // Create purchase order line with some invoiced and received quantities LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); - LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 20); - PurchaseLine."Qty. Invoiced (Base)" := 5; // Already invoiced 5 - PurchaseLine."Qty. Received (Base)" := 10; // Only received 10, so trying to invoice 15 + 5 = 20 > 10 received + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); + PurchaseLine."Qty. Invoiced (Base)" := 50; + PurchaseLine."Qty. Received (Base)" := 100; PurchaseLine.Modify(); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); // [WHEN] CalculatePOMatchWarnings is called EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); - // [THEN] NotYetReceived warnings should be generated + // [THEN] Both ExceedsInvoiceableQty and ExceedsRemainingToInvoice warnings should be generated TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::NotYetReceived); - Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected NotYetReceived warning to be generated'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsInvoiceableQty warning (I > R, I > J)'); + + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsRemainingToInvoice); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsRemainingToInvoice warning (I > R, I > J)'); end; [Test] @@ -1879,11 +2023,11 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [THEN] Matching should succeed Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine, EDocumentPurchaseLine), 'PO line should be matched to E-Document line'); - // [THEN] NotYetReceived warning should be generated + // [THEN] ExceedsInvoiceableQty warning should be generated EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::NotYetReceived); - Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected NotYetReceived warning to be generated'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsInvoiceableQty warning to be generated'); end; [Test] @@ -1929,11 +2073,11 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [THEN] Matching should succeed Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine, EDocumentPurchaseLine), 'PO line should be matched to E-Document line'); - // [THEN] NotYetReceived warning should NOT be generated + // [THEN] ExceedsInvoiceableQty warning should NOT be generated EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::NotYetReceived); - Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no NotYetReceived warning to be generated'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no ExceedsInvoiceableQty warning to be generated'); end; [Test] @@ -2020,11 +2164,11 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [THEN] Matching should succeed Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine, EDocumentPurchaseLine), 'PO line should be matched to E-Document line'); - // [THEN] NotYetReceived warning should NOT be generated for specified vendor + // [THEN] ExceedsInvoiceableQty warning should NOT be generated for specified vendor EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::NotYetReceived); - Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no NotYetReceived warning for specified vendor'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no ExceedsInvoiceableQty warning for specified vendor'); end; [Test] @@ -2072,11 +2216,11 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [THEN] Matching should succeed Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine, EDocumentPurchaseLine), 'PO line should be matched to E-Document line'); - // [THEN] NotYetReceived warning should be generated for non-specified vendor (default behavior is "Always ask") + // [THEN] ExceedsInvoiceableQty warning should be generated for non-specified vendor (default behavior is "Always ask") EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::NotYetReceived); - Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected NotYetReceived warning for non-specified vendor'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsInvoiceableQty warning for non-specified vendor'); end; [Test] @@ -2165,11 +2309,11 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [THEN] Matching should succeed Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine, EDocumentPurchaseLine), 'PO line should be matched to E-Document line'); - // [THEN] NotYetReceived warning should NOT be generated for non-specified vendor (default behavior is "Always receive at posting") + // [THEN] ExceedsInvoiceableQty warning should NOT be generated for non-specified vendor (default behavior is "Always receive at posting") EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::NotYetReceived); - Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no NotYetReceived warning for non-specified vendor'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no ExceedsInvoiceableQty warning for non-specified vendor'); end; [Test]