Track receipt on invoice per purchase order line - #10528
Conversation
f6c1294 to
9fba93f
Compare
| PurchaseHeader.SetLoadFields("No."); | ||
| if PurchaseHeader.FindSet() then | ||
| repeat | ||
| Clear(ReceiptOnInvoiceDataTransfer); |
There was a problem hiding this comment.
UpgradePurchaseLineReceiptOnInvoice() backfills Purchase Line."Receipt on Invoice" with DataTransfer.CopyFields(), but the new field's OnValidate is load-bearing: it calls InitQtyToReceive() for orders and updates existing matched rows through ApplyPurchaseLineReceiptSettingToMatches(). Because DataTransfer skips validation, migrated open orders that already had header-level Receipt on Invoice enabled will get the new line flag without the required derived-state updates (Qty. to Receive not reinitialized, Matched Order Line rows not refreshed). The same pattern is duplicated in the AT/BE/CH/IT/NL/NO/RU/W1 upgrade codeunits.
Knowledge:
- microsoft/knowledge/upgrade/datatransfer-skips-triggers-and-subscribers.md
- microsoft/knowledge/upgrade/datatransfer-for-bulk-init.md
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
9fba93f to
0a54477
Compare
0a54477 to
6ac2c96
Compare
| GetDefaultBin(); | ||
| CheckWMS(); | ||
| MatchedOrderLineMgmt.CheckReceiptOnInvoiceAllowedForLocation("Location Code", GetPurchHeader()); | ||
| if Rec."Receipt on Invoice" and not MatchedOrderLineMgmt.IsReceiptOnInvoiceAllowedForLocation("Location Code") then |
There was a problem hiding this comment.
This PR changes Purchase Line validation from raising an error to silently clearing "Receipt on Invoice" when "Location Code" or "No." is changed to a value that is no longer eligible (directed put-away/pick location, or item with serial/lot/package tracking). The rejection reason built by IsLineReceiptOnInvoiceAllowed is computed and then discarded instead of surfaced, so a previously blocking validation now silently mutates document state. Extensions, tests, or automations that relied on Validate(...) failing will now continue with mutated state instead of an error. The same pattern is duplicated across the localized PurchaseLine.Table.al copies. Consider surfacing the reason (e.g. via a non-blocking notification) or gating the auto-reset behind an explicit, documented opt-in.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| exit; | ||
|
|
||
| PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::Order); | ||
| PurchaseHeader.SetRange("Receipt on Invoice", true); |
There was a problem hiding this comment.
The upgrade routine copies header-level "Receipt on Invoice" = true onto every purchase order line via DataTransfer, bypassing the new line-level validation that rejects directed put-away locations, specific-tracked items, or lines that already have posted receipts. This can persist line state immediately after upgrade that the runtime would now refuse if set through PurchaseLine.Validate. Consider migrating only eligible lines, or reusing the same per-line eligibility checks as ApplyReceiptOnInvoiceToEligibleLines so the new field is never backfilled with a value the current validation logic considers illegal.
Agent judgement — not directly backed by a BCQuality knowledge article.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| end; | ||
| if PurchaseHeader."Receipt on Invoice" = NewReceiptOnInvoice then | ||
| exit; | ||
| if PurchaseHeader."Receipt on Invoice" and not NewReceiptOnInvoice then |
There was a problem hiding this comment.
ApplyVendorsReceiptOnInvoicePolicy prompts for confirmation before disabling an already-enabled "Receipt on Invoice" in interactive sessions, but when GuiAllowed() is false (API/job-queue contexts) it skips the confirmation entirely and silently resets the header and its eligible lines. This makes non-interactive behavior diverge from the UI path and hides a material document change from the caller. If confirmation is required in the UI, the non-interactive path should fail with a clear error instead of mutating the document silently.
Agent judgement — not directly backed by a BCQuality knowledge article.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| end else | ||
| SelectDefaultRemitAddress(Rec); | ||
|
|
||
| MatchedOrderLineMgmt.ApplyVendorsReceiptOnInvoicePolicy(Rec); |
There was a problem hiding this comment.
"Buy-from Vendor No." validation now calls ApplyVendorsReceiptOnInvoicePolicy(Rec) after the trigger's existing publisher calls, so it overwrites "Receipt on Invoice" after any subscriber to the existing OnAfter events has already run, with no new post-policy event to hook. Subscribers that used to set or preserve this field via the existing seams can no longer reliably do so. Consider moving the policy application earlier, or adding a new thin publisher after the policy is applied so dependent extensions retain a stable final-state hook.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| NewReceiptOnInvoice: Boolean; | ||
| ResetReceiptOnInvoiceQst: Label 'The vendor''s receipt on invoice policy disables %1, which is currently enabled on this document. Do you want to reset it on the document and its lines?', Comment = '%1 = Receipt on Invoice field caption'; | ||
| begin | ||
| if not Vendor.Get(PurchaseHeader."Buy-from Vendor No.") then |
There was a problem hiding this comment.
ApplyVendorsReceiptOnInvoicePolicy does a full Vendor.Get even though it only reads "Receipt on Invoice Policy". Vendor is a wide table, so this hot-path lookup (invoked on every Buy-from Vendor No. validation) should call SetLoadFields before Get to avoid loading unused columns.
Suggested fix (apply manually — could not be anchored as a one-click suggestion):
Vendor.SetLoadFields("Receipt on Invoice Policy");
if not Vendor.Get(PurchaseHeader."Buy-from Vendor No.") then
exit;Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| var | ||
| PurchaseLine: Record "Purchase Line"; | ||
| begin | ||
| PurchaseLine.SetRange("Document Type", PurchaseHeader."Document Type"); |
There was a problem hiding this comment.
RefreshMatchedOrderLineReceipt regresses the old batched-update strategy into one ModifyAll call per purchase line. On orders with many lines, this turns a handful of set-based updates into N separate updates against Matched Order Line. Consider restoring a batched-update strategy (e.g. grouping line SystemIds and updating matches per batch) instead of issuing one ModifyAll per line.
Agent judgement — not directly backed by a BCQuality knowledge article.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::Order); | ||
| PurchaseHeader.SetRange("Receipt on Invoice", true); | ||
| PurchaseHeader.SetLoadFields("No."); | ||
| if PurchaseHeader.FindSet() then |
There was a problem hiding this comment.
UpgradePurchaseLineReceiptOnInvoice loops every qualifying Purchase Header and runs DataTransfer.CopyFields once per order, defeating the set-based benefit of DataTransfer on tenants with many eligible orders. This pattern is duplicated in the AT/BE/CH/IT/NL/NO/RU/W1 upgrade codeunits. Consider reworking into a single set-based pass (or far fewer batches) over Purchase Line.
Agent judgement — not directly backed by a BCQuality knowledge article.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| ToolTip = 'Specifies the policy that will be used for the vendor if more items than ordered are received.'; | ||
| TableRelation = "Over-Receipt Code"; | ||
| } | ||
| field(8551; "Receipt on Invoice Policy"; Enum "Receipt on Invoice Policy") |
There was a problem hiding this comment.
The new Vendor field "Receipt on Invoice Policy" is added without its own DataClassification in all 14 changed Vendor.Table.al copies (APAC, BE, CH, DACH, ES, FI, FR, GB, IT, NA, NL, NO, RU, W1). Table-scope DataClassification does not satisfy field-level classification for a new field, so this shipped field remains effectively unclassified unless an explicit value (likely CustomerContent, matching the table) is added.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| end; | ||
| if PurchaseHeader."Receipt on Invoice" = NewReceiptOnInvoice then | ||
| exit; | ||
| if PurchaseHeader."Receipt on Invoice" and not NewReceiptOnInvoice then |
There was a problem hiding this comment.
Tests cover the Automatic vendor policy, but there is no test for the Manual downgrade branch in ApplyVendorsReceiptOnInvoicePolicy, which resets an already-enabled order and its lines after a Confirm() (or leaves them unchanged if the user declines). A regression in either confirm outcome would not be caught. Add handler-backed tests for both outcomes.
Agent judgement — not directly backed by a BCQuality knowledge article.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| UpgradeTag.SetUpgradeTag(UpgradeTagDefinitions.GetDepreciationBooksGLIntegrationUpgradeTag()); | ||
| end; | ||
|
|
||
| local procedure UpgradePurchaseLineReceiptOnInvoice() |
There was a problem hiding this comment.
UpgradePurchaseLineReceiptOnInvoice backfills the new line field with DataTransfer for existing purchase orders, but no upgrade test exercises this path. Because it only runs during upgrade and is guarded by an upgrade tag, a broken filter or missed case would silently ship to upgraded tenants unnoticed. Add an upgrade test that seeds legacy orders, runs the upgrade, and verifies both correct backfill and idempotence.
Agent judgement — not directly backed by a BCQuality knowledge article.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| Caption = 'Receipt on Invoice'; | ||
| ToolTip = 'Specifies whether the receipt is posted automatically with the invoice.'; | ||
|
|
||
| trigger OnValidate() |
There was a problem hiding this comment.
The new Purchase Line field has its own OnValidate eligibility guard, but added tests only cover the already-posted-receipt rejection case, not the specific-tracked-item or directed put-away/pick rejection branches of PurchaseLine.Validate("Receipt on Invoice", true). Regressions in those two disallow paths could slip through undetected. Add explicit line-level tests for both.
Agent judgement — not directly backed by a BCQuality knowledge article.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
6ac2c96 to
a6e8226
Compare
|
The same PurchaseHeader/PurchaseLine "Receipt on Invoice" field additions and the new Vendor "Receipt on Invoice Policy" field/enum are duplicated verbatim across ~14 country layers (APAC, AT, BE, CH, DACH, ES, FI, FR, GB, IS, IT, NA, NL, NO, RU, SE, W1). This is consistent with BCApps' layered-country architecture, but it multiplies the surface that must stay in sync for future changes (e.g. a fix or field-number shift in one layer must be manually replicated to all others). Consider whether the common Vendor/Purchase fields could be consolidated in W1 with country layers only adding true localization deltas, to reduce long-term maintenance risk. Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4 |
| begin | ||
| if not Vendor.Get(PurchaseHeader."Buy-from Vendor No.") then | ||
| exit; | ||
| case Vendor."Receipt on Invoice Policy" of |
There was a problem hiding this comment.
The new extensible enum "Receipt on Invoice Policy" is consumed through a case statement in ApplyVendorsReceiptOnInvoicePolicy. This hard-codes the policy variants into the consumer, so adding another policy requires editing this codeunit, and extension-defined values silently fall through the else branch with no behavior applied. Consider modeling the policy as enum-backed interface dispatch instead of branching on the enum in the consumer.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| end; | ||
| if PurchaseHeader."Receipt on Invoice" = NewReceiptOnInvoice then | ||
| exit; | ||
| if PurchaseHeader."Receipt on Invoice" and not NewReceiptOnInvoice then |
There was a problem hiding this comment.
ApplyVendorsReceiptOnInvoicePolicy can raise a Confirm dialog while the caller's flow is mid-update of the purchase document; if the user leaves the dialog open, the transaction/locks are held for the wait duration. Compute the policy choice and confirm before entering the write/line-recreation path, or move the confirm ahead of any document/line modification.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| ApplyReceiptOnInvoiceToEligibleLines(PurchaseHeader); | ||
| end; | ||
|
|
||
| internal procedure ApplyReceiptOnInvoiceToLines(PurchaseHeader: Record "Purchase Header") |
There was a problem hiding this comment.
Both new Purchase Line update loops (ApplyReceiptOnInvoiceToLines and ApplyReceiptOnInvoiceToEligibleLines) call FindSet() and then Validate/Modify each iterated row. That takes a read lock first and upgrades it on every Modify, the deadlock-prone shape this rule warns against. Use FindSet(true) for these write loops so the read acquires the update lock up front.
Suggested fix (apply manually — could not be anchored as a one-click suggestion):
internal procedure ApplyReceiptOnInvoiceToLines(PurchaseHeader: Record "Purchase Header")
var
PurchaseLine: Record "Purchase Line";
begin
PurchaseLine.SetRange("Document Type", PurchaseHeader."Document Type");
PurchaseLine.SetRange("Document No.", PurchaseHeader."No.");
if PurchaseLine.FindSet(true) then
repeat
PurchaseLine.Validate("Receipt on Invoice", PurchaseHeader."Receipt on Invoice");
PurchaseLine.Modify();
until PurchaseLine.Next() = 0;
end;
local procedure ApplyReceiptOnInvoiceToEligibleLines(PurchaseHeader: Record "Purchase Header")
var
PurchaseLine: Record "Purchase Line";
LineReceiptOnInvoice: Boolean;
ErrorMessage: Text;
begin
PurchaseLine.SetRange("Document Type", PurchaseHeader."Document Type");
PurchaseLine.SetRange("Document No.", PurchaseHeader."No.");
if PurchaseLine.FindSet(true) then
repeat
LineReceiptOnInvoice := PurchaseHeader."Receipt on Invoice";
if LineReceiptOnInvoice and not IsLineReceiptOnInvoiceAllowed(PurchaseLine, ErrorMessage) then
LineReceiptOnInvoice := false;
PurchaseLine.Validate("Receipt on Invoice", LineReceiptOnInvoice);
PurchaseLine.Modify();
until PurchaseLine.Next() = 0;
end;Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| OrderLine.GetBySystemId(OrderLineSystemId); | ||
| OrderHeader.Get(OrderLine."Document Type", OrderLine."Document No."); | ||
| exit(OrderHeader."Receipt on Invoice"); | ||
| exit(OrderLine."Receipt on Invoice"); |
There was a problem hiding this comment.
ReceiptOnInvoiceForMatch re-fetches a full Purchase Line row even though it only reads the new Receipt on Invoice flag. This helper sits on the save path for matching groups, so loading the whole wide line record for every match adds avoidable payload. Call SetLoadFields("Receipt on Invoice") before GetBySystemId().
Suggested fix (apply manually — could not be anchored as a one-click suggestion):
begin
OrderLine.SetLoadFields("Receipt on Invoice");
OrderLine.GetBySystemId(OrderLineSystemId);
exit(OrderLine."Receipt on Invoice");
end;Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
|
|
||
| if PurchaseLineSystemIDFilter <> '' then | ||
| RefreshMatchedOrderLinesBatch(PurchaseLineSystemIDFilter, PurchaseHeader."Receipt on Invoice"); | ||
| if not IsLineReceiptOnInvoiceAllowed(PurchaseLine, ErrorMessage) then |
There was a problem hiding this comment.
CheckLineReceiptOnInvoiceAllowed calls Error(ErrorMessage) after IsLineReceiptOnInvoiceAllowed builds ErrorMessage with StrSubstNo(...) from line-specific values such as PurchaseLine."Location Code", PurchaseLine."No.", and PurchaseLine."Line No.". That makes the first Error argument dynamic instead of a Label/TextConst, losing the static classified template needed for error telemetry.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
|
The PR adds new Normal-classified fields without an explicit DataClassification in every touched copy of Purchase Line and Vendor: "Receipt on Invoice" in 14 src/Layers//BaseApp/Purchases/Document/PurchaseLine.Table.al files, and "Receipt on Invoice Policy" in 14 src/Layers//BaseApp/Purchases/Vendor/Vendor.Table.al files. Each new field should declare its own non-ToBeClassified DataClassification value rather than relying on table-level defaults. Knowledge: Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4 |
| Permissions = TableData "Posted Matched Order Line" = RIMD; | ||
|
|
||
| internal procedure ApplyVendorsReceiptOnInvoicePolicy(var PurchaseHeader: Record "Purchase Header") | ||
| var |
There was a problem hiding this comment.
ResetReceiptOnInvoiceQst is declared inside ApplyVendorsReceiptOnInvoicePolicy's local var block. BCQuality recommends moving Labels to the codeunit's top-level var block because procedure-scoped Labels are fragile in XLIFF extraction and translation review.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
a6e8226 to
8e8c1ae
Compare
| end; | ||
|
|
||
| internal procedure RefreshMatchedOrderLineReceipt(PurchaseHeader: Record "Purchase Header") | ||
| internal procedure CheckLineReceiptOnInvoiceAllowed(PurchaseLine: Record "Purchase Line") |
There was a problem hiding this comment.
CheckLineReceiptOnInvoiceAllowed / IsLineReceiptOnInvoiceAllowed prebuild the error text with StrSubstNo across three branches and then raise Error(ErrorMessage) from the new "Receipt on Invoice" field OnValidate. This is flagged from three angles: (1) error-handling guidance prefers FieldError for field-validation failures so the platform supplies field/record context automatically instead of a hand-built Error; (2) style guidance says Error() should receive the label and its parameters directly rather than a pre-formatted Text, so translation/analyzer tooling can still see the original label identity; (3) privacy/telemetry guidance notes that prebuilding the message before raising Error loses the static label identity needed for RT0030 error classification. All three point at the same code shape and the same
Recommendation:
- raise structured errors (FieldError or Error(Label, Params...)) directly at the point of failure instead of composing Text in a helper and raising it later.
Knowledge:
- microsoft/knowledge/error-handling/fielderror-vs-testfield.md
- microsoft/knowledge/error-handling/fielderror-default-message-logic.md
- microsoft/knowledge/style/error-passes-parameters-directly-not-strsubstno.md
- microsoft/knowledge/privacy/avoid-strsubstno-prebuild-before-error.md
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
Stacked on #10526 (stack #10527).
AB#625392