diff --git a/.github/scripts/Test-SkillIndex.ps1 b/.github/scripts/Test-SkillIndex.ps1 index ed07204d..b694a5b1 100644 --- a/.github/scripts/Test-SkillIndex.ps1 +++ b/.github/scripts/Test-SkillIndex.ps1 @@ -93,14 +93,16 @@ try { 'microsoft/skills/review/al-query-review.md', 'microsoft/skills/review/al-reporting-review.md', 'microsoft/skills/review/al-appsource-review.md', - 'microsoft/skills/review/al-telemetry-review.md' + 'microsoft/skills/review/al-telemetry-review.md', + 'microsoft/skills/review/al-scm-review.md', + 'microsoft/skills/review/al-finance-review.md' ) $review = @($skills | Where-Object id -eq 'al-code-review') if ($review.Count -ne 1) { throw "Expected exactly one al-code-review record, found $($review.Count)." } if ((@($review[0].subSkills) -join "`n") -cne ($expectedLeaves -join "`n")) { - throw 'al-code-review subSkills did not preserve the declared 17-leaf order.' + throw "al-code-review subSkills did not preserve the declared $($expectedLeaves.Count)-leaf order." } foreach ($leafPath in $expectedLeaves) { $leaf = @($skills | Where-Object path -ceq $leafPath) @@ -238,4 +240,4 @@ finally { Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue } -Write-Output 'Skill-index check PASSED: deterministic, schema-valid, and all 17 review leaves preserved in order.' +Write-Output "Skill-index check PASSED: deterministic, schema-valid, and all $($expectedLeaves.Count) review leaves preserved in order." diff --git a/README.md b/README.md index daea4050..9aaea9a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -

- BCQuality logo -

+# BC Quality - Don’t teach one agent. Teach the ecosystem. 🀝 Quality skills and knowledge that help AI tools make better Business Central development decisions: catch BC-specific defects, avoid misleading advice, @@ -76,7 +74,8 @@ the review can still discover knowledge by reading the folders. ## Scope -Today's curated content focuses on **technical AL code review**. It augments +Today's curated content covers **technical AL code review** and a focused +**Supply Chain Management (SCM)** functional domain. It augments the agent's judgment; it is not an exhaustive BC manual or a substitute for compilation, analyzers, tests, or human review. See [coverage and limits](docs/using-bcquality.md#coverage-and-limits) for the @@ -84,9 +83,11 @@ available domains and the difference between a folder review and a comparison. Mechanical issues already enforced by the AL compiler or standard analyzers are intentionally left to those deterministic tools rather than duplicated here. -Functional areas such as Finance, Supply Chain Management, Manufacturing, Jobs, -Warehousing, and Service, and technologies such as PowerShell, pipelines, and -Power Platform, remain valid future scope, **not current coverage claims**. +The [SCM domain](microsoft/knowledge/scm/) covers selected inventory, costing, +reservation, tracking, and warehouse/posting workflows, not exhaustive supply +chain validation. Broader functional coverage such as Finance, Manufacturing, +Jobs, and Service, and technologies such as PowerShell, pipelines, and Power +Platform, remain valid future scope, **not current coverage claims**. ## What's in this repo diff --git a/docs/README.md b/docs/README.md index 7210cb7b..d9ca27b0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,3 @@ -

- BCQuality mark -

- # BCQuality documentation **New to BCQuality? Start with the [quick start](../README.md#quick-start).** @@ -36,4 +32,3 @@ prerequisites for using the plugin. | [DO](../skills/do.md) | Action-skill format and structured output contract. | | [WRITE](../skills/write.md) | Knowledge-authoring rules. | | [Review evaluation](../evaluation/README.md) | Sample conventions, fixture preparation, and scoring. | -| [Brand assets](brand-assets.md) | BCQ logo files and usage guidance. | diff --git a/docs/assets/bcq-logo.svg b/docs/assets/bcq-logo.svg deleted file mode 100644 index 0c7aae55..00000000 --- a/docs/assets/bcq-logo.svg +++ /dev/null @@ -1,26 +0,0 @@ - - BCQuality banner logo - A blue letter Q crossed by a teal quality check beside the BCQuality wordmark. - - - - - - - - - - - - - - - - - - - - - - BCQuality - diff --git a/docs/assets/bcq-mark.svg b/docs/assets/bcq-mark.svg deleted file mode 100644 index 0b6c00e0..00000000 --- a/docs/assets/bcq-mark.svg +++ /dev/null @@ -1,22 +0,0 @@ - - BCQuality mark - A blue letter Q crossed by a teal quality check. - - - - - - - - - - - - - - - - - - - diff --git a/docs/brand-assets.md b/docs/brand-assets.md deleted file mode 100644 index e578fcd4..00000000 --- a/docs/brand-assets.md +++ /dev/null @@ -1,43 +0,0 @@ -# BCQ brand assets - -

- BCQuality logo -

- -The BCQuality mark combines a **Q** with a check to represent review, -confidence, and quality. The vector artwork follows the supplied blue-to-teal -concept and connects the mark to the split-color BCQuality wordmark. A true -circular ring and square-ended 45-degree bars keep the check and Q tail aligned -at exact right angles; the check's vertical cut and the tail's horizontal cut -match the source concept. - -## Available artwork - -| Asset | Best use | -| --- | --- | -| [`bcq-logo.svg`](assets/bcq-logo.svg) | Horizontal banner with the mark and wordmark. | -| [`bcq-mark.svg`](assets/bcq-mark.svg) | Symbol without the wordmark. | - -The SVG files can be scaled without losing quality. - -## Color palette - -The artwork uses the dominant colors sampled from the supplied Business Central -logo reference. - -| Color | Hex | -| --- | --- | -| Deep blue | `#086194` | -| Blue | `#138EB7` | -| Cyan | `#17ABCB` | -| Aqua | `#2CD2CD` | -| Mint | `#57E6CB` | -| Light mint | `#9BF2C8` | - -## Usage - -- Prefer the full logo when at least 320 pixels of horizontal space is - available; use the mark at smaller sizes. -- Preserve the artwork's proportions, colors, and orientation. -- Use `BCQuality logo` as alternative text unless nearby text already names the - project, in which case the image can be decorative. diff --git a/docs/using-bcquality.md b/docs/using-bcquality.md index e55751f9..b65fcb09 100644 --- a/docs/using-bcquality.md +++ b/docs/using-bcquality.md @@ -85,6 +85,7 @@ branch names with ones in your project. | Uncommitted changes | Use the installed al-code-review skill to review my staged and unstaged tracked changes against HEAD, without changing files. Identify any untracked AL files not included in that diff. | | Branch changes | Use the installed al-code-review skill to review changes on this branch since its merge base with `origin/main`. Exclude uncommitted changes and do not edit files. | | Focused review | Use the installed al-code-review skill to review performance in the app in this folder, without changing files. Return the complete performance findings report. | +| Supply chain code | Use the installed al-code-review skill to review SCM posting, inventory, reservations, item tracking, and warehouse workflows in this app folder, without changing files. Return the complete Supply Chain Management findings report. | | Agent SDK code | Use the installed al-code-review skill to review Agent SDK implementation and usage in this app folder, without changing files. Return the complete Agents findings report. | For Git comparisons, the named base ref must exist locally. If it is missing, @@ -183,7 +184,7 @@ using your normal compilation, analyzer, test, and human-review workflow. ## Coverage and limits -The Microsoft broad review composes the 17 Microsoft domains listed below. +The Microsoft broad review composes the Microsoft domains listed below. The Community Agents review is a separate skill selected by the request, not a nested part of that coordinator. All current review leaves accept app folders, files, and diffs; request an Agent SDK review explicitly when that @@ -193,8 +194,32 @@ Available knowledge is **not** a promise that every rule will run. Selection depends on the task, target context, enabled layers, and source evidence. A whole-folder review is a current-state snapshot: detecting a published API removal or another comparison-only regression requires an actual baseline. -The corpus is technical AL guidance, not exhaustive functional validation or -AppSource certification. +The corpus combines technical AL guidance with targeted functional-domain +invariants, not exhaustive functional validation or AppSource certification. + +The SCM leaf owns selected inventory/value, application, reservation, tracking, +and warehouse/posting invariants. It prunes unrelated AL using the actual +tables, codeunits, fields, and operations in scope; an item caption or a broad +`ApplicationArea` alone is not an SCM review signal. Missing workflow context +must not be replaced with an assumed posting defect. Manufacturing, assembly, +planning, and other supply-chain areas are covered only where an article +explicitly names the shared interface or invariant. + +SCM owns Item/Value/Capacity/Warehouse and inventory-application posting +records. Pure G/L, customer/vendor/detailed/VAT and financial-only posting +mutations belong to Finance, even when that domain is not enabled. Equivalent +findings for one inventory-originated posting bypass have one SCM primary +owner; distinct independent financial defects remain separate. + +The Finance leaf reviews journal posting, financial ledger changes, +applications, and posting-linked dimension handling. It prunes unrelated code +at the leaf rather than changing broad-review orchestration. Finance articles +use `application-area: [all]` so missing application-area context does not +weaken applicable findings; resolved records and operations supply the +narrowing. Finance owns financial ledgers, not Item, Value, Capacity, Warehouse, +or inventory-application records owned by SCM. It also does not own generic +custom-table or master Default Dimension wiring. Request a focused "Finance +posting review" when only this domain is needed. BCQuality intentionally does not duplicate mechanical diagnostics already enforced by the AL compiler or standard analyzers. Run the consuming app's @@ -215,6 +240,7 @@ Each article describes one concern. Where samples exist, use its linked | Data modeling | [Data modeling](../microsoft/knowledge/data-modeling/) | | Error handling | [Error handling](../microsoft/knowledge/error-handling/) | | Events | [Events](../microsoft/knowledge/events/) | +| Finance | [Finance](../microsoft/knowledge/finance/) | | Interfaces | [Interfaces](../microsoft/knowledge/interfaces/) | | Performance | [Performance](../microsoft/knowledge/performance/) | | Privacy | [Privacy](../microsoft/knowledge/privacy/) | @@ -222,6 +248,7 @@ Each article describes one concern. Where samples exist, use its linked | Reporting | [Reporting](../microsoft/knowledge/reporting/) | | Security | [Security](../microsoft/knowledge/security/) | | Style | [Style](../microsoft/knowledge/style/) | +| Supply Chain Management | [SCM](../microsoft/knowledge/scm/) | | Telemetry | [Telemetry](../microsoft/knowledge/telemetry/) | | Testing | [Testing](../microsoft/knowledge/testing/) | | User interface | [UI](../microsoft/knowledge/ui/) | diff --git a/evaluation/README.md b/evaluation/README.md index 2125ce39..bce3208f 100644 --- a/evaluation/README.md +++ b/evaluation/README.md @@ -6,6 +6,28 @@ The evaluation is convention-driven. The harness discovers every `/skills Model-facing preparation hashes case IDs, neutralizes `Good`/`Bad` object-name tokens, and removes full-line sample comments so neither the article slug, domain, nor expected outcome reveals the answer. +The SCM `articles` override deliberately selects every rule in the initial +functional domain, producing nine positive cases and nine clean controls. +Business context is executable: document/status `TestField` guards, +calculated-revaluation fields, source-transfer base quantities, warehouse +reconciliation steps, and additional-demand promising parameters survive +neutralization. Do not move those preconditions into comments or generic +"posting" helper names; removing them can turn a real defect into a valid +alternative workflow. The clean pairs exercise the supported APIs selected by +the same routing cues, not merely unrelated code that contains no SCM tokens. + +The Finance override deliberately covers every paired Finance article, not +only the first filename. Its shared context supplies the target version and +localization but deliberately omits application area, as production callers +often do. Finance applicability must come from its source-surface gate, not +an artificial evaluation-only area hint. Scenario prerequisites live in +executable AL: the document-balance cases check the template setting, and the +VAT cases encode the imported net/VAT/gross totals and applicable VAT mode. +Do not move these prerequisites into comments that preparation removes. +Clean samples also retain supported operational edits, temporary ledger/set +buffers, legitimate entry-number APIs, and reads of individual shortcut +dimensions so these exceptions are exercised rather than blanket-excluded. + ## Validate the corpus ```powershell diff --git a/evaluation/review-fixtures.json b/evaluation/review-fixtures.json index 90c42c62..3f131a55 100644 --- a/evaluation/review-fixtures.json +++ b/evaluation/review-fixtures.json @@ -13,6 +13,20 @@ "events": { "article": "reset-ishandled-only-when-the-value-can-carry-over" }, + "finance": { + "articles": [ + "apply-ledger-entries-through-application-codeunits", + "change-ledger-due-dates-through-entry-edit", + "do-not-edit-shared-dimension-sets", + "do-not-modify-or-delete-posted-ledger-entries", + "normal-vat-journal-amount-includes-vat", + "post-ledger-entries-through-posting-codeunits", + "preserve-journal-batch-document-balance", + "reverse-transactions-by-transaction-number", + "write-dimensions-as-dimension-set-entries" + ], + "context": "Target Business Central 28, worldwide standard application (w1). Review each file as an independent source input." + }, "interfaces": { "article": "set-defaultimplementation-on-enum" }, @@ -51,8 +65,24 @@ "stop-when-runrequestpage-returns-empty-parameters" ] }, + "scm": { + "articles": [ + "post-item-ledger-changes-through-item-journals", + "post-revaluation-through-the-item-journal-batch", + "change-item-applications-through-posting-routines", + "cancel-reservations-through-reservation-management", + "transfer-item-tracking-through-source-reservation-codeunits", + "reconcile-warehouse-adjustments-with-the-item-ledger", + "post-transfers-through-shipment-and-receipt-codeunits", + "use-date-aware-availability-for-promising", + "carry-out-requisition-actions-through-the-standard-workflow" + ] + }, "style": { - "article": "label-comment-explains-placeholders" + "articles": [ + "label-comment-explains-placeholders", + "dateformula-evaluate-needs-language-independent-literals" + ] }, "telemetry": { "article": "telemetry-event-id-stable-unique" diff --git a/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.bad.al b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.bad.al new file mode 100644 index 00000000..0c87bb2a --- /dev/null +++ b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.bad.al @@ -0,0 +1,28 @@ +table 50603 "Sample Order Header Bad" +{ + fields + { + field(1; "No."; Code[20]) + { + DataClassification = CustomerContent; + } + field(2; "Document Date"; Date) + { + DataClassification = CustomerContent; + } + } + + trigger OnInsert() + var + SalesSetup: Record "Sales & Receivables Setup"; + NoSeries: Codeunit "No. Series"; + begin + "Document Date" := WorkDate(); + + if "No." = '' then begin + SalesSetup.Get(); + SalesSetup.TestField("Order Nos."); + "No." := NoSeries.GetNextNo(SalesSetup."Order Nos."); + end; + end; +} diff --git a/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.good.al b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.good.al new file mode 100644 index 00000000..355d3d54 --- /dev/null +++ b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.good.al @@ -0,0 +1,45 @@ +table 50602 "Sample Order Header Good" +{ + fields + { + field(1; "No."; Code[20]) + { + DataClassification = CustomerContent; + } + field(2; "Document Date"; Date) + { + DataClassification = CustomerContent; + } + } + + trigger OnInsert() + var + SalesSetup: Record "Sales & Receivables Setup"; + NoSeries: Codeunit "No. Series"; + begin + if "No." = '' then begin + SalesSetup.Get(); + SalesSetup.TestField("Order Nos."); + "No." := NoSeries.GetNextNo(SalesSetup."Order Nos."); + end; + + InitRecord(); + end; + + procedure InitRecord() + begin + OnBeforeInitRecord(Rec); + "Document Date" := WorkDate(); + OnAfterInitRecord(Rec); + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeInitRecord(var SampleOrderHeader: Record "Sample Order Header Good") + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnAfterInitRecord(var SampleOrderHeader: Record "Sample Order Header Good") + begin + end; +} diff --git a/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md new file mode 100644 index 00000000..e83a6d34 --- /dev/null +++ b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md @@ -0,0 +1,30 @@ +--- +bc-version: [all] +domain: data-modeling +keywords: [document-header, initrecord, number-series, default-values, oninsert, initialization] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Initialize document defaults in `InitRecord` after assigning the number + +## Description + +Business Central document headers assign their number series first and then call an `InitRecord` procedure that owns the remaining business defaults, such as posting and document dates. Keeping that sequence and extensibility point makes initialization consistent for every creation path and lets extensions subscribe around one documented operation. Defaults scattered across page triggers or unrelated helpers can differ between UI, API, test, and background creation. + +## Best Practice + +In the document table's insert path, assign the document number and then call `InitRecord`. Keep the default assignments in that procedure and expose narrow before/after events when other extensions must participate. + +See sample: [`initialize-document-defaults-in-initrecord.good.al`](initialize-document-defaults-in-initrecord.good.al). + +## Anti Pattern + +Assigning document defaults in a page trigger, or scattering them directly through `OnInsert` with no `InitRecord` boundary. Non-page creation paths can then miss the defaults, and extensions have no stable initialization hook. + +See sample: [`initialize-document-defaults-in-initrecord.bad.al`](initialize-document-defaults-in-initrecord.bad.al). + +## Reference + +[Use the InitRecord function](https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/3-use-initrecord-function) diff --git a/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.bad.al b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.bad.al new file mode 100644 index 00000000..f8e11cca --- /dev/null +++ b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.bad.al @@ -0,0 +1,8 @@ +codeunit 50601 "Directed Rounding Bad" +{ + procedure FloorAmount(Value: Decimal; Precision: Decimal): Decimal + begin + // For negative values, '<' rounds toward zero rather than toward negative infinity. + exit(Round(Value, Precision, '<')); + end; +} diff --git a/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.good.al b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.good.al new file mode 100644 index 00000000..cd8a80fc --- /dev/null +++ b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.good.al @@ -0,0 +1,10 @@ +codeunit 50600 "Directed Rounding Good" +{ + procedure RoundAmount(Value: Decimal; Precision: Decimal; IncreaseMagnitude: Boolean): Decimal + begin + if IncreaseMagnitude then + exit(Round(Value, Precision, '>')); + + exit(Round(Value, Precision, '<')); + end; +} diff --git a/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.md b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.md new file mode 100644 index 00000000..260905db --- /dev/null +++ b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.md @@ -0,0 +1,30 @@ +--- +bc-version: [all] +domain: data-modeling +keywords: [round, rounding, direction, precision, negative-decimal, amount] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# `Round` direction symbols follow magnitude, not mathematical ordering + +## Description + +AL's `Round(Number, Precision, Direction)` uses `'>'` to round away from zero and `'<'` to round toward zero. For a negative value this reverses mathematical ordering: `Round(-1234.56789, 0.001, '<')` returns `-1234.567`, while direction `'>'` returns `-1234.568`. Code that treats the symbols as mathematical ceiling and floor produces sign-dependent amount errors, commonly on credit documents and negative adjustments. + +## Best Practice + +Choose the direction from the business meaning: `'>'` increases absolute magnitude and `'<'` decreases absolute magnitude for both positive and negative values. Include positive and negative cases whenever a directed rounding rule is tested. + +See sample: [`round-direction-symbols-use-magnitude.good.al`](round-direction-symbols-use-magnitude.good.al). + +## Anti Pattern + +Using `'<'` as a mathematical floor or `'>'` as a mathematical ceiling. The result looks correct for positive amounts but moves in the opposite mathematical direction for negative amounts. + +See sample: [`round-direction-symbols-use-magnitude.bad.al`](round-direction-symbols-use-magnitude.bad.al). + +## Reference + +[Use the Round function](https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/4a-use-round-function) diff --git a/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.bad.al b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.bad.al new file mode 100644 index 00000000..1e605d37 --- /dev/null +++ b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.bad.al @@ -0,0 +1,33 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50104 "Customer Settlement Actions" +{ + procedure RequestApplication(CustomerEntryNo: Integer) + var + CustomerEntry: Record "Cust. Ledger Entry"; + begin + RequireInteractiveSession(); + CustomerEntry.Get(CustomerEntryNo); + CustomerEntry.TestField(Open, true); + CustomerEntry.Open := false; + CustomerEntry.Modify(true); + end; + + procedure RequestUnapplication(CustomerEntryNo: Integer) + var + DetailedCustomerEntry: Record "Detailed Cust. Ledg. Entry"; + begin + RequireInteractiveSession(); + DetailedCustomerEntry.SetRange("Cust. Ledger Entry No.", CustomerEntryNo); + DetailedCustomerEntry.SetRange("Entry Type", DetailedCustomerEntry."Entry Type"::Application); + DetailedCustomerEntry.ModifyAll(Unapplied, true); + end; + + local procedure RequireInteractiveSession() + begin + if not GuiAllowed() then + Error(InteractiveSessionErr); + end; + + var + InteractiveSessionErr: Label 'Request settlement from an interactive session.'; +} diff --git a/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.good.al b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.good.al new file mode 100644 index 00000000..b33e2d26 --- /dev/null +++ b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.good.al @@ -0,0 +1,31 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50104 "Customer Settlement Actions" +{ + procedure RequestApplication(CustomerEntryNo: Integer) + var + CustomerEntry: Record "Cust. Ledger Entry"; + CustomerApplication: Codeunit "CustEntry-Apply Posted Entries"; + begin + RequireInteractiveSession(); + CustomerEntry.Get(CustomerEntryNo); + CustomerEntry.TestField(Open, true); + CustomerApplication.ApplyCustEntryFormEntry(CustomerEntry); + end; + + procedure RequestUnapplication(CustomerEntryNo: Integer) + var + CustomerApplication: Codeunit "CustEntry-Apply Posted Entries"; + begin + RequireInteractiveSession(); + CustomerApplication.UnApplyCustLedgEntry(CustomerEntryNo); + end; + + local procedure RequireInteractiveSession() + begin + if not GuiAllowed() then + Error(InteractiveSessionErr); + end; + + var + InteractiveSessionErr: Label 'Request settlement from an interactive session.'; +} diff --git a/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.md b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.md new file mode 100644 index 00000000..11f78cd7 --- /dev/null +++ b/microsoft/knowledge/finance/apply-ledger-entries-through-application-codeunits.md @@ -0,0 +1,37 @@ +--- +bc-version: [all] +domain: finance +keywords: [cust-ledger-entry, vendor-ledger-entry, detailed-ledger-entry, remaining-amount, application, unapplication, open, closed-by-entry-no] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Apply and unapply entries through the application workflow, not status flags + +## Description + +Customer/vendor settlement is a posting operation involving detailed ledger entries, not just a change to `Open` on the main entry. Remaining amounts are calculated from detailed entries. Unapplication posts correcting entries and handles application-derived effects such as discounts and currency gains/losses; deleting details or changing `Unapplied` cannot reproduce that history. + +## Best Practice + +Use `"CustEntry-Apply Posted Entries"` / `"VendEntry-Apply Posted Entries"` and the supported application or unapplication workflow. Let it check application dates, entry state, and application ordering. The `ApplyCustEntryFormEntry` / `ApplyVendEntryFormEntry` and `UnApply...LedgEntry` methods are **interactive**: users select and confirm the operation. They are not unattended "mark paid" APIs. + +For programmatic posting, use the target version's public `Apply` / `PostUnApply...` APIs with properly prepared selection and `Apply Unapply Parameters`; handle cancellation and the workflow's transaction/commit behavior. `"Applying Entry"`, `"Applies-to ID"`, and `"Amount to Apply"` are legitimate application-preparation fields. Do not report their writes alone, temporary buffers, supported posting/compression internals, or unrelated operational/extension fields. + +See sample: [`apply-ledger-entries-through-application-codeunits.good.al`](apply-ledger-entries-through-application-codeunits.good.al). + +## Anti Pattern + +Implement customer/vendor payment matching, settlement, or reopening by directly persisting `Open`, `"Closed by Entry No."`, closure amounts/dates, or detailed-entry unapplication flags, or by deleting/rewriting detailed application amounts. Require confirmed writes to existing non-temporary customer/vendor or detailed customer/vendor entries and settlement intent. Item/inventory application records belong to SCM, not this rule. Do not suggest assigning a `Remaining Amount` FlowField as a fix. + +This article owns fabricated application state. Use the [posted-financial-content rule](do-not-modify-or-delete-posted-ledger-entries.md) for original accounting-value corrections, not a second finding prescribing the same application fix. + +See sample: [`apply-ledger-entries-through-application-codeunits.bad.al`](apply-ledger-entries-through-application-codeunits.bad.al). + +## References + +- [Apply and unapply customer transactions](https://learn.microsoft.com/en-us/dynamics365/business-central/receivables-how-apply-sales-transactions-manually). +- [Apply and unapply vendor transactions](https://learn.microsoft.com/en-us/dynamics365/business-central/payables-how-apply-purchase-transactions-manually). +- [BCApps: customer application workflow](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Sales/Receivables/CustEntryApplyPostedEntries.Codeunit.al). +- [BCApps: vendor application workflow](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Purchases/Payables/VendEntryApplyPostedEntries.Codeunit.al). diff --git a/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.bad.al b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.bad.al new file mode 100644 index 00000000..7a7bec68 --- /dev/null +++ b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.bad.al @@ -0,0 +1,21 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50105 "Update Ledger Due Dates" +{ + procedure UpdateCustomerDueDate(EntryNo: Integer; NewDueDate: Date) + var + CustomerEntry: Record "Cust. Ledger Entry"; + begin + CustomerEntry.Get(EntryNo); + CustomerEntry.Validate("Due Date", NewDueDate); + CustomerEntry.Modify(true); + end; + + procedure UpdateVendorDueDate(EntryNo: Integer; NewDueDate: Date) + var + VendorEntry: Record "Vendor Ledger Entry"; + begin + VendorEntry.Get(EntryNo); + VendorEntry.Validate("Due Date", NewDueDate); + VendorEntry.Modify(true); + end; +} diff --git a/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.good.al b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.good.al new file mode 100644 index 00000000..540548e7 --- /dev/null +++ b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.good.al @@ -0,0 +1,21 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50105 "Update Ledger Due Dates" +{ + procedure UpdateCustomerDueDate(EntryNo: Integer; NewDueDate: Date) + var + CustomerEntry: Record "Cust. Ledger Entry"; + begin + CustomerEntry.Get(EntryNo); + CustomerEntry.Validate("Due Date", NewDueDate); + Codeunit.Run(Codeunit::"Cust. Entry-Edit", CustomerEntry); + end; + + procedure UpdateVendorDueDate(EntryNo: Integer; NewDueDate: Date) + var + VendorEntry: Record "Vendor Ledger Entry"; + begin + VendorEntry.Get(EntryNo); + VendorEntry.Validate("Due Date", NewDueDate); + Codeunit.Run(Codeunit::"Vend. Entry-Edit", VendorEntry); + end; +} diff --git a/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.md b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.md new file mode 100644 index 00000000..aca7b769 --- /dev/null +++ b/microsoft/knowledge/finance/change-ledger-due-dates-through-entry-edit.md @@ -0,0 +1,35 @@ +--- +bc-version: [all] +domain: finance +keywords: [due-date, initial-entry-due-date, cust-entry-edit, vend-entry-edit, detailed-ledger-entry, aging] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Change posted customer/vendor due dates through the entry-edit workflow + +## Description + +A posted customer or vendor due date is supported editable operational data, but it is also represented by `Initial Entry Due Date` on related detailed ledger entries. Validating `Due Date` and calling `Modify(true)` on the main entry does not perform all synchronization done by `"Cust. Entry-Edit"` or `"Vend. Entry-Edit"`. The main entry and due-date-based analysis can otherwise disagree. + +## Best Practice + +Fetch the existing entry, validate the proposed `Due Date`, and pass the changed record to the corresponding entry-edit codeunit, as the standard ledger pages do. Field validation enforces entry-state rules; the editor persists the supported change and synchronizes related detailed entries. Preserve both steps rather than treating table triggers as equivalent to the edit workflow. + +This is a due-date synchronization rule, not a prohibition on all operational edits after posting. Exclude temporary buffers, extension-only fields, supported editor internals, and code that demonstrably performs the equivalent synchronization under the supported workflow. Check the actual table/routine instead of assuming every `*Entry-Edit` accepts the same fields. + +See sample: [`change-ledger-due-dates-through-entry-edit.good.al`](change-ledger-due-dates-through-entry-edit.good.al). + +## Anti Pattern + +Change `Due Date` on an existing non-temporary `Cust. Ledger Entry` or `Vendor Ledger Entry` and persist it with `Modify`, `Modify(true)`, or `ModifyAll` without the edit workflow or equivalent related-entry update. A preceding `Validate("Due Date", ...)` is not sufficient evidence of synchronization. + +See sample: [`change-ledger-due-dates-through-entry-edit.bad.al`](change-ledger-due-dates-through-entry-edit.bad.al). + +## References + +- [Cust. Entry-Edit API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/codeunit/microsoft.sales.receivables.cust.-entry-edit). +- [Vend. Entry-Edit API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/codeunit/microsoft.purchases.payables.vend.-entry-edit). +- [BCApps: customer due-date synchronization](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Sales/Receivables/CustEntryEdit.Codeunit.al). +- [BCApps: vendor due-date synchronization](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Purchases/Payables/VendEntryEdit.Codeunit.al). diff --git a/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.bad.al b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.bad.al new file mode 100644 index 00000000..2c5761c9 --- /dev/null +++ b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.bad.al @@ -0,0 +1,14 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50103 "Change Journal Dimension" +{ + procedure ChangeExistingDimension(TemplateName: Code[10]; BatchName: Code[10]; LineNo: Integer; DimensionCode: Code[20]; NewValue: Code[20]) + var + JournalLine: Record "Gen. Journal Line"; + DimensionSetEntry: Record "Dimension Set Entry"; + begin + JournalLine.Get(TemplateName, BatchName, LineNo); + DimensionSetEntry.Get(JournalLine."Dimension Set ID", DimensionCode); + DimensionSetEntry.Validate("Dimension Value Code", NewValue); + DimensionSetEntry.Modify(); + end; +} diff --git a/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.good.al b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.good.al new file mode 100644 index 00000000..72d09b3f --- /dev/null +++ b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.good.al @@ -0,0 +1,18 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50103 "Change Journal Dimension" +{ + procedure ChangeExistingDimension(TemplateName: Code[10]; BatchName: Code[10]; LineNo: Integer; DimensionCode: Code[20]; NewValue: Code[20]) + var + JournalLine: Record "Gen. Journal Line"; + TempDimensionSetEntry: Record "Dimension Set Entry" temporary; + DimensionManagement: Codeunit DimensionManagement; + begin + JournalLine.Get(TemplateName, BatchName, LineNo); + DimensionManagement.GetDimensionSet(TempDimensionSetEntry, JournalLine."Dimension Set ID"); + TempDimensionSetEntry.Get(JournalLine."Dimension Set ID", DimensionCode); + TempDimensionSetEntry.Validate("Dimension Value Code", NewValue); + TempDimensionSetEntry.Modify(); + JournalLine.Validate("Dimension Set ID", DimensionManagement.GetDimensionSetID(TempDimensionSetEntry)); + JournalLine.Modify(true); + end; +} diff --git a/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.md b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.md new file mode 100644 index 00000000..97b29f23 --- /dev/null +++ b/microsoft/knowledge/finance/do-not-edit-shared-dimension-sets.md @@ -0,0 +1,35 @@ +--- +bc-version: [all] +domain: finance +keywords: [dimension-set-entry, dimension-value-id, getdimensionset, getdimensionsetid, posted-dimensions, temporary] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Change a transaction's dimension set reference, not a shared set's membership + +## Description + +The same `Dimension Set ID` can be referenced by an unposted journal line and by many already-posted entries. Editing or deleting the persisted set's dimension/value rows therefore changes the meaning of unrelated transactions, including posting history. The dimension-set search tree also relies on those combinations remaining stable; a set is not a mutable child collection owned by one journal line. + +## Best Practice + +To change an unposted transaction's dimensions, load its set into a **temporary** `Dimension Set Entry` buffer with `DimensionManagement.GetDimensionSet`, change the buffer, and obtain a reusable ID with `GetDimensionSetID`. Validate dimension values in the buffer so `Dimension Value ID` matches the chosen value. Store the resulting ID on the transaction and synchronize its projections through that record's supported dimension validation. + +For already-posted G/L dimensions, use the supported dimension-correction workflow rather than changing shared rows. Read-only access, temporary buffers, and standard maintenance of projection metadata such as `Global Dimension No.` are not membership changes. This rule protects dimension sets reached from general-journal, financial-document, or Finance-ledger flows. It does not own Item, Value, Capacity, Warehouse, or inventory-application record writes, or prescribe custom-table/default-dimension wiring. + +See sample: [`do-not-edit-shared-dimension-sets.good.al`](do-not-edit-shared-dimension-sets.good.al). + +## Anti Pattern + +Follow a general-journal, financial-document, or Finance-ledger `Dimension Set ID` to a **persistent** `Dimension Set Entry` and modify, rename, or delete its dimension/value membership in order to change that one transaction. Inspect `IsTemporary` guards, aliases, and the fields written before reporting: the same operations on a temporary working copy are expected. + +See sample: [`do-not-edit-shared-dimension-sets.bad.al`](do-not-edit-shared-dimension-sets.bad.al). + +## References + +- [Dimension set entries overview](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-dimension-set-entries-overview). +- [Supported G/L dimension correction](https://learn.microsoft.com/en-us/dynamics365/business-central/finance-troubleshooting-correcting-dimensions). +- [DimensionManagement API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/codeunit/microsoft.finance.dimension.dimensionmanagement). +- [BCApps: Dimension Set Entry and its set-ID resolver](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/Dimension/DimensionSetEntry.Table.al). diff --git a/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.bad.al b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.bad.al new file mode 100644 index 00000000..71607857 --- /dev/null +++ b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.bad.al @@ -0,0 +1,32 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50101 "Correct Posted Transaction" +{ + procedure RequestTransactionReversal(EntryNo: Integer) + var + GLEntry: Record "G/L Entry"; + begin + if not GuiAllowed() then + Error(InteractiveSessionErr); + GLEntry.Get(EntryNo); + GLEntry.TestField("Transaction No."); + GLEntry.Amount := -GLEntry.Amount; + GLEntry.Modify(true); + end; + + procedure UpdateDescription(EntryNo: Integer; NewDescription: Text[100]) + var + GLEntry: Record "G/L Entry"; + begin + GLEntry.Get(EntryNo); + GLEntry.Description := NewDescription; + Codeunit.Run(Codeunit::"G/L Entry-Edit", GLEntry); + end; + + procedure ClearSimulation(var TempGLEntry: Record "G/L Entry" temporary) + begin + TempGLEntry.DeleteAll(); + end; + + var + InteractiveSessionErr: Label 'Request the reversal from an interactive session.'; +} diff --git a/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.good.al b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.good.al new file mode 100644 index 00000000..1aa2c8ff --- /dev/null +++ b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.good.al @@ -0,0 +1,32 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50101 "Correct Posted Transaction" +{ + procedure RequestTransactionReversal(EntryNo: Integer) + var + GLEntry: Record "G/L Entry"; + ReversalEntry: Record "Reversal Entry"; + begin + if not GuiAllowed() then + Error(InteractiveSessionErr); + GLEntry.Get(EntryNo); + GLEntry.TestField("Transaction No."); + ReversalEntry.ReverseTransaction(GLEntry."Transaction No."); + end; + + procedure UpdateDescription(EntryNo: Integer; NewDescription: Text[100]) + var + GLEntry: Record "G/L Entry"; + begin + GLEntry.Get(EntryNo); + GLEntry.Description := NewDescription; + Codeunit.Run(Codeunit::"G/L Entry-Edit", GLEntry); + end; + + procedure ClearSimulation(var TempGLEntry: Record "G/L Entry" temporary) + begin + TempGLEntry.DeleteAll(); + end; + + var + InteractiveSessionErr: Label 'Request the reversal from an interactive session.'; +} diff --git a/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md new file mode 100644 index 00000000..33a8029c --- /dev/null +++ b/microsoft/knowledge/finance/do-not-modify-or-delete-posted-ledger-entries.md @@ -0,0 +1,37 @@ +--- +bc-version: [all] +domain: finance +keywords: [g-l-entry, ledger-entry, reversal, audit-trail, correction, financial-content, entry-edit] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Correct posted financial content through posting workflows, not row surgery + +## Description + +Changing a posted entry's original amount, account, posting date, or tax amounts in place does not correct the related ledger, register, or source document. Deleting one erroneous row has the same problem. Business Central provides reversing and correcting posting workflows that retain the relationship between the original transaction and its correction; this is not a blanket prohibition on every write to a posted table. + +## Best Practice + +Use a supported transaction/register reversal, credit memo, or correcting journal appropriate to the original posting and its current state. Request the standard reversal workflow rather than negating a single row or setting `Reversed` yourself. Let that workflow enforce eligibility; do not change source or application fields to make a rejected reversal pass. + +Supported operational edits are deliberate exceptions: for example, `"G/L Entry-Edit"` supports description changes, and `"Cust. Entry-Edit"` / `"Vend. Entry-Edit"` handle their table-specific editable fields. [Due-date synchronization](change-ledger-due-dates-through-entry-edit.md), [application/unapplication](apply-ledger-entries-through-application-codeunits.md), G/L dimension correction, and supported date compression have their own workflows. Do not flag their standard implementations, temporary simulation buffers, or extension-only metadata updates as financial row surgery. A subscriber is not exempt merely because it runs inside a supported workflow: inspect the fields it actually changes. + +This rule covers G/L, customer/vendor/detailed, VAT, and financial-posting/register records. Item, Value, Capacity, Warehouse, inventory-application, and other inventory-posting records are SCM concerns. The financial-row leg of one inventory-posting bypass is outside this rule when the same inventory correction resolves it; an independently actionable financial defect remains in scope regardless of the containing module's name. + +See sample: [`do-not-modify-or-delete-posted-ledger-entries.good.al`](do-not-modify-or-delete-posted-ledger-entries.good.al). + +## Anti Pattern + +Persist a change to original financial content, delete posted rows, or fabricate reversal flags/links to repair or undo a transaction outside the supported correction/maintenance workflow. Require an existing, non-temporary Finance-owned record and evidence of the fields or rows affected; a `Modify` token or `*Ledger Entry` name alone is insufficient. Settlement-state writes belong to the application article rather than a duplicate finding here. + +See sample: [`do-not-modify-or-delete-posted-ledger-entries.bad.al`](do-not-modify-or-delete-posted-ledger-entries.bad.al). + +## References + +- [Reverse journal postings](https://learn.microsoft.com/en-us/dynamics365/business-central/finance-how-reverse-journal-posting). +- [Correct G/L dimensions](https://learn.microsoft.com/en-us/dynamics365/business-central/finance-troubleshooting-correcting-dimensions). +- [BCApps: G/L Entry-Edit](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Ledger/GLEntryEdit.Codeunit.al). +- [BCApps: supported customer-ledger date compression](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Sales/Receivables/DateCompressCustomerLedger.Report.al). diff --git a/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.bad.al b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.bad.al new file mode 100644 index 00000000..50ac8ffa --- /dev/null +++ b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.bad.al @@ -0,0 +1,54 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50108 "Import Purchase Journal Total" +{ + procedure ImportExampleTotal(TemplateName: Code[10]; BatchName: Code[10]; LineNo: Integer) + var + JournalLine: Record "Gen. Journal Line"; + VATPostingSetup: Record "VAT Posting Setup"; + GeneralLedgerSetup: Record "General Ledger Setup"; + SourceInvoice: JsonObject; + NetToken: JsonToken; + VATToken: JsonToken; + GrossToken: JsonToken; + ImportedNet: Decimal; + ImportedVAT: Decimal; + ImportedGross: Decimal; + begin + SourceInvoice.ReadFrom('{"netAmount":100,"vatAmount":25,"grossAmount":125}'); + SourceInvoice.Get('netAmount', NetToken); + SourceInvoice.Get('vatAmount', VATToken); + SourceInvoice.Get('grossAmount', GrossToken); + ImportedNet := NetToken.AsValue().AsDecimal(); + ImportedVAT := VATToken.AsValue().AsDecimal(); + ImportedGross := GrossToken.AsValue().AsDecimal(); + if ImportedGross <> ImportedNet + ImportedVAT then + Error(TotalsErr); + + JournalLine.Get(TemplateName, BatchName, LineNo); + JournalLine.TestField("Account Type", JournalLine."Account Type"::"G/L Account"); + JournalLine.TestField("Bal. Account Type", JournalLine."Bal. Account Type"::"G/L Account"); + JournalLine.TestField("Account No."); + JournalLine.TestField("Bal. Account No."); + JournalLine.TestField("Currency Code", ''); + JournalLine.TestField("Gen. Posting Type", JournalLine."Gen. Posting Type"::Purchase); + JournalLine.TestField("VAT Posting", JournalLine."VAT Posting"::"Automatic VAT Entry"); + JournalLine.TestField("VAT Calculation Type", JournalLine."VAT Calculation Type"::"Normal VAT"); + JournalLine.TestField("VAT %", 25); + JournalLine.TestField("VAT Difference", 0); + JournalLine.TestField("Bal. Gen. Posting Type", JournalLine."Bal. Gen. Posting Type"::" "); + JournalLine.TestField("Bal. VAT %", 0); + VATPostingSetup.Get(JournalLine."VAT Bus. Posting Group", JournalLine."VAT Prod. Posting Group"); + VATPostingSetup.TestField("VAT Calculation Type", VATPostingSetup."VAT Calculation Type"::"Normal VAT"); + VATPostingSetup.TestField("VAT %", 25); + VATPostingSetup.TestField("Unrealized VAT Type", VATPostingSetup."Unrealized VAT Type"::" "); + GeneralLedgerSetup.Get(); + GeneralLedgerSetup.TestField("Additional Reporting Currency", ''); + GeneralLedgerSetup.TestField("Amount Rounding Precision", 0.01); + + JournalLine.Validate(Amount, ImportedNet); + JournalLine.Modify(true); + end; + + var + TotalsErr: Label 'The invoice total must equal its net amount plus VAT.'; +} diff --git a/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.good.al b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.good.al new file mode 100644 index 00000000..961e234c --- /dev/null +++ b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.good.al @@ -0,0 +1,54 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50108 "Import Purchase Journal Total" +{ + procedure ImportExampleTotal(TemplateName: Code[10]; BatchName: Code[10]; LineNo: Integer) + var + JournalLine: Record "Gen. Journal Line"; + VATPostingSetup: Record "VAT Posting Setup"; + GeneralLedgerSetup: Record "General Ledger Setup"; + SourceInvoice: JsonObject; + NetToken: JsonToken; + VATToken: JsonToken; + GrossToken: JsonToken; + ImportedNet: Decimal; + ImportedVAT: Decimal; + ImportedGross: Decimal; + begin + SourceInvoice.ReadFrom('{"netAmount":100,"vatAmount":25,"grossAmount":125}'); + SourceInvoice.Get('netAmount', NetToken); + SourceInvoice.Get('vatAmount', VATToken); + SourceInvoice.Get('grossAmount', GrossToken); + ImportedNet := NetToken.AsValue().AsDecimal(); + ImportedVAT := VATToken.AsValue().AsDecimal(); + ImportedGross := GrossToken.AsValue().AsDecimal(); + if ImportedGross <> ImportedNet + ImportedVAT then + Error(TotalsErr); + + JournalLine.Get(TemplateName, BatchName, LineNo); + JournalLine.TestField("Account Type", JournalLine."Account Type"::"G/L Account"); + JournalLine.TestField("Bal. Account Type", JournalLine."Bal. Account Type"::"G/L Account"); + JournalLine.TestField("Account No."); + JournalLine.TestField("Bal. Account No."); + JournalLine.TestField("Currency Code", ''); + JournalLine.TestField("Gen. Posting Type", JournalLine."Gen. Posting Type"::Purchase); + JournalLine.TestField("VAT Posting", JournalLine."VAT Posting"::"Automatic VAT Entry"); + JournalLine.TestField("VAT Calculation Type", JournalLine."VAT Calculation Type"::"Normal VAT"); + JournalLine.TestField("VAT %", 25); + JournalLine.TestField("VAT Difference", 0); + JournalLine.TestField("Bal. Gen. Posting Type", JournalLine."Bal. Gen. Posting Type"::" "); + JournalLine.TestField("Bal. VAT %", 0); + VATPostingSetup.Get(JournalLine."VAT Bus. Posting Group", JournalLine."VAT Prod. Posting Group"); + VATPostingSetup.TestField("VAT Calculation Type", VATPostingSetup."VAT Calculation Type"::"Normal VAT"); + VATPostingSetup.TestField("VAT %", 25); + VATPostingSetup.TestField("Unrealized VAT Type", VATPostingSetup."Unrealized VAT Type"::" "); + GeneralLedgerSetup.Get(); + GeneralLedgerSetup.TestField("Additional Reporting Currency", ''); + GeneralLedgerSetup.TestField("Amount Rounding Precision", 0.01); + + JournalLine.Validate(Amount, ImportedGross); + JournalLine.Modify(true); + end; + + var + TotalsErr: Label 'The invoice total must equal its net amount plus VAT.'; +} diff --git a/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.md b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.md new file mode 100644 index 00000000..6dda9efc --- /dev/null +++ b/microsoft/knowledge/finance/normal-vat-journal-amount-includes-vat.md @@ -0,0 +1,37 @@ +--- +bc-version: [all] +domain: finance +keywords: [normal-vat, automatic-vat-entry, gross-amount, net-amount, vat-posting-setup, gen-journal-line, purchase] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Supply a VAT-inclusive journal Amount for automatic Normal VAT + +## Description + +For a general-journal line using **Automatic VAT Entry** and **Normal VAT**, `Amount` includes VAT. The posting engine extracts tax from that total; it does not add tax to a VAT-exclusive expense imported into `Amount`. An LCY invoice with net 100 and VAT 25 therefore needs a journal total of 125, not 100. + +## Best Practice + +Map the source's VAT-inclusive total to journal `Amount` in this posting mode. Establish the intended account and posting-group combination before validating the final amount. Account-derived VAT defaults depend on `Copy VAT Setup to Jnl. Lines`; do not assume account selection always supplies the intended configuration. + +Require the actual input contract and calculation mode, not just a variable named `NetAmount`. The examples encode source net, VAT, and gross values plus a 25% Normal-VAT setup check. They target an LCY G/L purchase with 0.01 amount rounding and without balancing-side VAT, additional reporting currency, VAT differences, or unrealized VAT. Other calculation types, Manual VAT Entry, reverse charge, Full VAT, sales/use tax, unrealized tax, and other currency/rounding contexts need their own analysis; this is not a universal gross-up formula or country-specific tax advice. + +The concern is the supplied transaction total, not its deductible/non-deductible allocation. Non-deductible VAT features can change the allocation of that total, not turn the source's net amount into its gross amount. Do not infer a particular expense or deductible-VAT split from this rule. The samples therefore do not depend on later-version non-deductible-VAT fields. + +See sample: [`normal-vat-journal-amount-includes-vat.good.al`](normal-vat-journal-amount-includes-vat.good.al). + +## Anti Pattern + +In the demonstrated automatic Normal-VAT configuration, put a provably VAT-exclusive source amount into journal `Amount` while expecting posting to add tax. An amount assignment alone, unknown setup, or a suggestive variable name is insufficient. Do not report the correctly supplied gross amount or automatically rewrite tax calculations outside this scope. + +See sample: [`normal-vat-journal-amount-includes-vat.bad.al`](normal-vat-journal-amount-includes-vat.bad.al). + +## References + +- [VAT posting setup combinations](https://learn.microsoft.com/en-us/dynamics365/business-central/finance-setup-vat#combine-vat-posting-groups-in-vat-posting-setups). +- [BCApps: journal amount validation](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Journal/GenJournalLine.Table.al). +- [BCApps: Normal VAT extraction during posting](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Posting/GenJnlPostLine.Codeunit.al). +- [BCApps: journal VAT amount regression cases](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/Tests/VAT/ERMVATOnGenJournalLine.Codeunit.al). diff --git a/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al new file mode 100644 index 00000000..948976b0 --- /dev/null +++ b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.bad.al @@ -0,0 +1,60 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50100 "Post Transfer Journal" +{ + procedure PostTransferBatch(TemplateName: Code[10]; BatchName: Code[10]) + var + JournalLine: Record "Gen. Journal Line"; + LastGLEntry: Record "G/L Entry"; + NextEntryNo: Integer; + begin + JournalLine.SetRange("Journal Template Name", TemplateName); + JournalLine.SetRange("Journal Batch Name", BatchName); + if JournalLine.Count() <> 1 then + Error(SingleTransferErr); + JournalLine.FindFirst(); + JournalLine.TestField("Account Type", JournalLine."Account Type"::"G/L Account"); + JournalLine.TestField("Bal. Account Type", JournalLine."Bal. Account Type"::"G/L Account"); + JournalLine.TestField("Account No."); + JournalLine.TestField("Bal. Account No."); + JournalLine.TestField("Posting Date"); + JournalLine.TestField("Document No."); + JournalLine.TestField(Amount); + JournalLine.TestField("Currency Code", ''); + JournalLine.TestField("Gen. Posting Type", JournalLine."Gen. Posting Type"::" "); + JournalLine.TestField("Bal. Gen. Posting Type", JournalLine."Bal. Gen. Posting Type"::" "); + + LastGLEntry.LockTable(); + if LastGLEntry.FindLast() then + NextEntryNo := LastGLEntry."Entry No." + 1 + else + NextEntryNo := 1; + InsertLedgerRow(JournalLine, NextEntryNo, JournalLine."Account No.", JournalLine.Amount); + InsertLedgerRow(JournalLine, NextEntryNo + 1, JournalLine."Bal. Account No.", -JournalLine.Amount); + end; + + local procedure InsertLedgerRow(JournalLine: Record "Gen. Journal Line"; EntryNo: Integer; AccountNo: Code[20]; Amount: Decimal) + var + GLEntry: Record "G/L Entry"; + begin + GLEntry.Init(); + GLEntry."Entry No." := EntryNo; + GLEntry."G/L Account No." := AccountNo; + GLEntry."Posting Date" := JournalLine."Posting Date"; + GLEntry."Document Type" := JournalLine."Document Type"; + GLEntry."Document No." := JournalLine."Document No."; + GLEntry."Source Code" := JournalLine."Source Code"; + GLEntry."Journal Batch Name" := JournalLine."Journal Batch Name"; + GLEntry."Dimension Set ID" := JournalLine."Dimension Set ID"; + GLEntry."Global Dimension 1 Code" := JournalLine."Shortcut Dimension 1 Code"; + GLEntry."Global Dimension 2 Code" := JournalLine."Shortcut Dimension 2 Code"; + GLEntry.Amount := Amount; + if Amount > 0 then + GLEntry."Debit Amount" := Amount + else + GLEntry."Credit Amount" := -Amount; + GLEntry.Insert(true); + end; + + var + SingleTransferErr: Label 'Use a journal batch containing exactly one self-balancing G/L transfer.'; +} diff --git a/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al new file mode 100644 index 00000000..4d3bee3c --- /dev/null +++ b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.good.al @@ -0,0 +1,29 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50100 "Post Transfer Journal" +{ + procedure PostTransferBatch(TemplateName: Code[10]; BatchName: Code[10]) + var + JournalLine: Record "Gen. Journal Line"; + PostBatch: Codeunit "Gen. Jnl.-Post Batch"; + begin + JournalLine.SetRange("Journal Template Name", TemplateName); + JournalLine.SetRange("Journal Batch Name", BatchName); + if JournalLine.Count() <> 1 then + Error(SingleTransferErr); + JournalLine.FindFirst(); + JournalLine.TestField("Account Type", JournalLine."Account Type"::"G/L Account"); + JournalLine.TestField("Bal. Account Type", JournalLine."Bal. Account Type"::"G/L Account"); + JournalLine.TestField("Account No."); + JournalLine.TestField("Bal. Account No."); + JournalLine.TestField("Posting Date"); + JournalLine.TestField("Document No."); + JournalLine.TestField(Amount); + JournalLine.TestField("Currency Code", ''); + JournalLine.TestField("Gen. Posting Type", JournalLine."Gen. Posting Type"::" "); + JournalLine.TestField("Bal. Gen. Posting Type", JournalLine."Bal. Gen. Posting Type"::" "); + PostBatch.Run(JournalLine); + end; + + var + SingleTransferErr: Label 'Use a journal batch containing exactly one self-balancing G/L transfer.'; +} diff --git a/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.md b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.md new file mode 100644 index 00000000..aefb6bb1 --- /dev/null +++ b/microsoft/knowledge/finance/post-ledger-entries-through-posting-codeunits.md @@ -0,0 +1,38 @@ +--- +bc-version: [all] +domain: finance +keywords: [g-l-entry, ledger-entry, gen-jnl-post-line, gen-jnl-post-batch, journal-line, register, insert] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Create financial ledger entries through the owning posting engine + +## Description + +Standard financial ledger entries are outputs of posting, not independent rows an extension manufactures. Even two manually inserted G/L rows with balanced amounts bypass posting checks, register bookkeeping, and transaction/source relationships. `G/L Entry.Insert(true)` runs the table trigger; it does not invoke the posting engine. + +## Best Practice + +Use the owning document or journal posting workflow. For a normal persisted general-journal batch, use `"Gen. Jnl.-Post Batch"`; the example posts an existing batch containing one self-balancing, non-VAT G/L transfer. Let posting allocate entries and maintain the register rather than reconstructing its tables. + +`"Gen. Jnl.-Post Line".RunWithCheck` is appropriate for a complete journal line inside a correctly owned posting lifecycle, but it does not invent a balancing account or document number, allocate numbering merely from `Posting No. Series`, or replace [batch document-balancing policy](preserve-journal-batch-document-balance.md). The line codeunit is stateful; its checked wrapper owns its start/continue/finish work. Normal batch posting owns its numbering and commits by default; do not imply these entry points are transaction-neutral. + +Exclude temporary buffers and the standard engine's own insertion points. A checked parent may legitimately use `RunWithoutCheck`; do not replace it without inspecting that parent. This rule owns `G/L Entry`, `Cust. Ledger Entry`, `Vendor Ledger Entry`, their detailed customer/vendor entries, `VAT Entry`, and financial-posting/register records, not a custom table merely named `Ledger Entry` or a supported, specifically reviewed migration/repair workflow. + +`Item Ledger Entry`, `Value Entry`, Capacity/Warehouse entries, `Item Application Entry`, and other inventory-posting records are SCM concerns, not this rule's financial-ledger scope. That exclusion includes the financial-row leg of a single inventory-posting bypass when restoring the inventory workflow corrects the whole operation. Distinct, independently actionable financial defects remain in scope. + +See sample: [`post-ledger-entries-through-posting-codeunits.good.al`](post-ledger-entries-through-posting-codeunits.good.al). + +## Anti Pattern + +Create posted financial effects by directly inserting the Finance-owned records named above outside their owning posting workflow. Resolve the actual record type, operation, and lifecycle; do not match `*Ledger Entry` as a wildcard. Balanced debit/credit values, copied dimensions, `Insert(true)`, and a lock around entry-number allocation do not turn raw inserts into a complete posting. + +See sample: [`post-ledger-entries-through-posting-codeunits.bad.al`](post-ledger-entries-through-posting-codeunits.bad.al). + +## References + +- [Posting engine structure](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-posting-engine-structure). +- [Gen. Jnl.-Post Line API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/codeunit/microsoft.finance.generalledger.posting.gen.-jnl.-post-line). +- [BCApps: posting lifecycle and register maintenance](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Posting/GenJnlPostLine.Codeunit.al). diff --git a/microsoft/knowledge/finance/preserve-journal-batch-document-balance.bad.al b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.bad.al new file mode 100644 index 00000000..1d5b5b3a --- /dev/null +++ b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.bad.al @@ -0,0 +1,53 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50107 "Post Journal Allocation" +{ + procedure PostAllocation(TemplateName: Code[10]; BatchName: Code[10]; DebitAccount: Code[20]; CreditAccount: Code[20]; PostingDate: Date) + var + JournalTemplate: Record "Gen. Journal Template"; + JournalBatch: Record "Gen. Journal Batch"; + JournalLine: Record "Gen. Journal Line"; + LineToPost: Record "Gen. Journal Line"; + PostLine: Codeunit "Gen. Jnl.-Post Line"; + begin + JournalTemplate.Get(TemplateName); + JournalTemplate.TestField(Recurring, false); + JournalTemplate.TestField("Force Doc. Balance", true); + JournalTemplate.TestField("Source Code"); + JournalBatch.Get(TemplateName, BatchName); + JournalBatch.TestField("No. Series", ''); + JournalBatch.TestField("Posting No. Series", ''); + JournalLine.SetRange("Journal Template Name", TemplateName); + JournalLine.SetRange("Journal Batch Name", BatchName); + if not JournalLine.IsEmpty() then + Error(EmptyBatchErr); + + AddAllocationLine(JournalTemplate, BatchName, 10000, DebitAccount, PostingDate, 'ALLOC-A', 90); + AddAllocationLine(JournalTemplate, BatchName, 20000, CreditAccount, PostingDate, 'ALLOC-B', -90); + JournalLine.FindSet(); + repeat + LineToPost := JournalLine; + PostLine.RunWithCheck(LineToPost); + until JournalLine.Next() = 0; + end; + + local procedure AddAllocationLine(JournalTemplate: Record "Gen. Journal Template"; BatchName: Code[10]; LineNo: Integer; AccountNo: Code[20]; PostingDate: Date; DocumentNo: Code[20]; LineAmount: Decimal) + var + JournalLine: Record "Gen. Journal Line"; + begin + JournalLine.Init(); + JournalLine."Journal Template Name" := JournalTemplate.Name; + JournalLine."Journal Batch Name" := BatchName; + JournalLine."Line No." := LineNo; + JournalLine."Source Code" := JournalTemplate."Source Code"; + JournalLine.Validate("Posting Date", PostingDate); + JournalLine.Validate("Document No.", DocumentNo); + JournalLine.Validate("Account Type", JournalLine."Account Type"::"G/L Account"); + JournalLine.Validate("Account No.", AccountNo); + JournalLine.Validate("Gen. Posting Type", JournalLine."Gen. Posting Type"::" "); + JournalLine.Validate(Amount, LineAmount); + JournalLine.Insert(true); + end; + + var + EmptyBatchErr: Label 'Use an empty journal batch for this allocation.'; +} diff --git a/microsoft/knowledge/finance/preserve-journal-batch-document-balance.good.al b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.good.al new file mode 100644 index 00000000..beff7f43 --- /dev/null +++ b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.good.al @@ -0,0 +1,49 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50107 "Post Journal Allocation" +{ + procedure PostAllocation(TemplateName: Code[10]; BatchName: Code[10]; DebitAccount: Code[20]; CreditAccount: Code[20]; PostingDate: Date) + var + JournalTemplate: Record "Gen. Journal Template"; + JournalBatch: Record "Gen. Journal Batch"; + JournalLine: Record "Gen. Journal Line"; + PostBatch: Codeunit "Gen. Jnl.-Post Batch"; + begin + JournalTemplate.Get(TemplateName); + JournalTemplate.TestField(Recurring, false); + JournalTemplate.TestField("Force Doc. Balance", true); + JournalTemplate.TestField("Source Code"); + JournalBatch.Get(TemplateName, BatchName); + JournalBatch.TestField("No. Series", ''); + JournalBatch.TestField("Posting No. Series", ''); + JournalLine.SetRange("Journal Template Name", TemplateName); + JournalLine.SetRange("Journal Batch Name", BatchName); + if not JournalLine.IsEmpty() then + Error(EmptyBatchErr); + + AddAllocationLine(JournalTemplate, BatchName, 10000, DebitAccount, PostingDate, 'ALLOC-A', 90); + AddAllocationLine(JournalTemplate, BatchName, 20000, CreditAccount, PostingDate, 'ALLOC-A', -90); + JournalLine.FindFirst(); + PostBatch.Run(JournalLine); + end; + + local procedure AddAllocationLine(JournalTemplate: Record "Gen. Journal Template"; BatchName: Code[10]; LineNo: Integer; AccountNo: Code[20]; PostingDate: Date; DocumentNo: Code[20]; LineAmount: Decimal) + var + JournalLine: Record "Gen. Journal Line"; + begin + JournalLine.Init(); + JournalLine."Journal Template Name" := JournalTemplate.Name; + JournalLine."Journal Batch Name" := BatchName; + JournalLine."Line No." := LineNo; + JournalLine."Source Code" := JournalTemplate."Source Code"; + JournalLine.Validate("Posting Date", PostingDate); + JournalLine.Validate("Document No.", DocumentNo); + JournalLine.Validate("Account Type", JournalLine."Account Type"::"G/L Account"); + JournalLine.Validate("Account No.", AccountNo); + JournalLine.Validate("Gen. Posting Type", JournalLine."Gen. Posting Type"::" "); + JournalLine.Validate(Amount, LineAmount); + JournalLine.Insert(true); + end; + + var + EmptyBatchErr: Label 'Use an empty journal batch for this allocation.'; +} diff --git a/microsoft/knowledge/finance/preserve-journal-batch-document-balance.md b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.md new file mode 100644 index 00000000..b3079ac2 --- /dev/null +++ b/microsoft/knowledge/finance/preserve-journal-batch-document-balance.md @@ -0,0 +1,35 @@ +--- +bc-version: [all] +domain: finance +keywords: [force-doc-balance, gen-journal-template, gen-jnl-post-batch, runwithcheck, document-no, posting-date, balancing] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Preserve the journal batch's document-balancing policy + +## Description + +A balanced G/L total does not prove that a general-journal batch satisfies its template's document-balancing policy. `"Gen. Jnl.-Post Batch"` checks balances at posting-date boundaries and, when the **journal template's** `Force Doc. Balance` is enabled, document-type/document-number boundaries. A loop over `"Gen. Jnl.-Post Line".RunWithCheck` does not reproduce these batch-level checks. + +## Best Practice + +Post normal persisted general-journal batches through their owning batch workflow. Keep balancing lines in the appropriate document/date group when the template requires it. The examples use two LCY G/L lines: opposite amounts under different document numbers are not a document-balanced transfer when `Force Doc. Balance` is true; the good example groups them under one document and retains the batch checks. + +Do not claim every document must always balance: when that template option is false, the supported workflow can allow document imbalance while still checking the required aggregate balances. Standalone self-balancing line posting and purpose-built posting engines that demonstrably own equivalent aggregate policies are not prohibited. A `RunWithCheck` call or loop alone is not sufficient evidence of a defect. + +See sample: [`preserve-journal-batch-document-balance.good.al`](preserve-journal-batch-document-balance.good.al). + +## Anti Pattern + +Replace a normal persisted journal batch's posting path with per-line posting or only an aggregate-total check, bypassing a demonstrated template/document/date policy. For the document-imbalance finding, require evidence that `Force Doc. Balance` applies and that separate document groups can be unbalanced; do not infer the setting from its name or a comment alone. + +See sample: [`preserve-journal-batch-document-balance.bad.al`](preserve-journal-batch-document-balance.bad.al). + +## References + +- [Work with general journals](https://learn.microsoft.com/en-us/dynamics365/business-central/ui-work-general-journals). +- [Gen. Jnl.-Post Batch API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/codeunit/microsoft.finance.generalledger.posting.gen.-jnl.-post-batch). +- [BCApps: batch balance checks](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Posting/GenJnlPostBatch.Codeunit.al). +- [BCApps: document-balance option regression cases](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/Tests/General%20Journal/ERMTestMultipleGenJnlLines.Codeunit.al). diff --git a/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.bad.al b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.bad.al new file mode 100644 index 00000000..9f5d3116 --- /dev/null +++ b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.bad.al @@ -0,0 +1,18 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50106 "Reverse Selected Posting" +{ + procedure RequestReversal(SelectedEntryNo: Integer) + var + GLEntry: Record "G/L Entry"; + ReversalEntry: Record "Reversal Entry"; + begin + if not GuiAllowed() then + Error(InteractiveSessionErr); + GLEntry.Get(SelectedEntryNo); + GLEntry.TestField("Transaction No."); + ReversalEntry.ReverseTransaction(GLEntry."Entry No."); + end; + + var + InteractiveSessionErr: Label 'Request the reversal from an interactive session.'; +} diff --git a/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.good.al b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.good.al new file mode 100644 index 00000000..392d3cd4 --- /dev/null +++ b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.good.al @@ -0,0 +1,18 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50106 "Reverse Selected Posting" +{ + procedure RequestReversal(SelectedEntryNo: Integer) + var + GLEntry: Record "G/L Entry"; + ReversalEntry: Record "Reversal Entry"; + begin + if not GuiAllowed() then + Error(InteractiveSessionErr); + GLEntry.Get(SelectedEntryNo); + GLEntry.TestField("Transaction No."); + ReversalEntry.ReverseTransaction(GLEntry."Transaction No."); + end; + + var + InteractiveSessionErr: Label 'Request the reversal from an interactive session.'; +} diff --git a/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.md b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.md new file mode 100644 index 00000000..296d9023 --- /dev/null +++ b/microsoft/knowledge/finance/reverse-transactions-by-transaction-number.md @@ -0,0 +1,34 @@ +--- +bc-version: [all] +domain: finance +keywords: [reversetransaction, reverseregister, transaction-no, entry-no, reversal-entry, g-l-register] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Pass the transaction number, not a ledger-entry number, to ReverseTransaction + +## Description + +`Reversal Entry.ReverseTransaction` expects a **transaction number**, while `ReverseRegister` expects a **G/L register number**. Neither parameter means a ledger `Entry No.`. All are integers, so the compiler accepts the wrong identity; a coincidentally matching integer can select another transaction instead of the posting the user intended to reverse. + +## Best Practice + +When starting from a `G/L Entry`, fetch that entry and pass **its** `Transaction No.` to `Reversal Entry.ReverseTransaction`, as the sample does. The same identity distinction applies to customer/vendor ledger entries when using their supported transaction-reversal path. If the starting point is a G/L register, use `Reversal Entry.ReverseRegister` with that register's number. The interactive workflow collects the participating entries and validates reversal eligibility before the user posts the reversal. + +Do not infer eligibility from `Open` alone or bypass a rejection by changing origin, application, or reversal fields. The supported path depends on source and state; some postings require unapplication or a correcting document first. A request to reverse is not a guarantee that reversal will be permitted. This rule concerns the two named `Reversal Entry` APIs, not routines such as `UnApplyCustLedgEntry` that legitimately accept a ledger entry number. + +See sample: [`reverse-transactions-by-transaction-number.good.al`](reverse-transactions-by-transaction-number.good.al). + +## Anti Pattern + +Pass a ledger entry's `Entry No.` or a register number into `ReverseTransaction`, or pass a ledger-entry/transaction number into `ReverseRegister`. Require visible value provenance, not merely a suspicious variable name or an arbitrary integer. A correctly sourced transaction number is valid even when the variable is poorly named. + +See sample: [`reverse-transactions-by-transaction-number.bad.al`](reverse-transactions-by-transaction-number.bad.al). + +## References + +- [Reversal Entry API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/table/microsoft.finance.generalledger.reversal.reversal-entry). +- [Reverse journal postings](https://learn.microsoft.com/en-us/dynamics365/business-central/finance-how-reverse-journal-posting). +- [BCApps: reversal entry selection](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Reversal/ReversalEntry.Table.al). diff --git a/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.bad.al b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.bad.al new file mode 100644 index 00000000..f54d42be --- /dev/null +++ b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.bad.al @@ -0,0 +1,20 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50102 "Copy Journal Dimensions" +{ + procedure CopyAllLineDimensions(TemplateName: Code[10]; BatchName: Code[10]; SourceLineNo: Integer; TargetLineNo: Integer) + var + SourceLine: Record "Gen. Journal Line"; + TargetLine: Record "Gen. Journal Line"; + begin + SourceLine.Get(TemplateName, BatchName, SourceLineNo); + TargetLine.Get(TemplateName, BatchName, TargetLineNo); + TargetLine."Shortcut Dimension 1 Code" := SourceLine."Shortcut Dimension 1 Code"; + TargetLine."Shortcut Dimension 2 Code" := SourceLine."Shortcut Dimension 2 Code"; + TargetLine.Modify(true); + end; + + procedure HasShortcutDimension1(JournalLine: Record "Gen. Journal Line"; DimensionValue: Code[20]): Boolean + begin + exit(JournalLine."Shortcut Dimension 1 Code" = DimensionValue); + end; +} diff --git a/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.good.al b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.good.al new file mode 100644 index 00000000..330c0de1 --- /dev/null +++ b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.good.al @@ -0,0 +1,19 @@ +// Demonstration only; independently authored, not copied from BaseApp. +codeunit 50102 "Copy Journal Dimensions" +{ + procedure CopyAllLineDimensions(TemplateName: Code[10]; BatchName: Code[10]; SourceLineNo: Integer; TargetLineNo: Integer) + var + SourceLine: Record "Gen. Journal Line"; + TargetLine: Record "Gen. Journal Line"; + begin + SourceLine.Get(TemplateName, BatchName, SourceLineNo); + TargetLine.Get(TemplateName, BatchName, TargetLineNo); + TargetLine.Validate("Dimension Set ID", SourceLine."Dimension Set ID"); + TargetLine.Modify(true); + end; + + procedure HasShortcutDimension1(JournalLine: Record "Gen. Journal Line"; DimensionValue: Code[20]): Boolean + begin + exit(JournalLine."Shortcut Dimension 1 Code" = DimensionValue); + end; +} diff --git a/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.md b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.md new file mode 100644 index 00000000..0764f201 --- /dev/null +++ b/microsoft/knowledge/finance/write-dimensions-as-dimension-set-entries.md @@ -0,0 +1,34 @@ +--- +bc-version: [all] +domain: finance +keywords: [dimension-set-id, shortcut-dimension, global-dimension, journal-line, posting, copy-dimensions] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Copy complete posting dimension sets, not only shortcut projections + +## Description + +When a journal line or posting document inherits dimensions, its `Dimension Set ID` identifies the complete combination of `Dimension Set Entry` rows. Global and shortcut dimensions expose selected dimensions, not the complete set; the eight shortcut dimensions are not a limit on set membership. Copying only these projections can leave the destination's posting dimensions unchanged or silently lose dimensions outside the shortcuts. + +## Best Practice + +For an intentional **complete** dimension transfer, copy the source set ID and synchronize the destination's projections through its supported validation or dimension-management routine. On `Gen. Journal Line`, `Validate("Dimension Set ID", SourceSetID)` updates the two shortcut fields. Do not assume another table has the same validation trigger. + +When line-specific dimensions must survive a header change, use the appropriate set-combination or delta routine instead of blindly replacing the line's entire set. Reading or filtering a known global dimension is legitimate; it is not a claim to enumerate every dimension. This rule owns transfers through general-journal and financial-document posting records, not writes to Item, Value, Capacity, Warehouse, or inventory-application records owned by SCM. Generic custom-table or master `Default Dimension` wiring belongs to data modeling. + +See sample: [`write-dimensions-as-dimension-set-entries.good.al`](write-dimensions-as-dimension-set-entries.good.al). + +## Anti Pattern + +A routine intended to copy **all** posting dimensions copies only global/shortcut codes, or assigns a set ID without synchronizing the destination's stored projections. Require evidence of a complete-transfer intent and inspect surrounding validation; an explicit change to one selected dimension or a read-only filter is not this defect. + +See sample: [`write-dimensions-as-dimension-set-entries.bad.al`](write-dimensions-as-dimension-set-entries.bad.al). + +## References + +- [Dimension set entries overview](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-dimension-set-entries-overview). +- [DimensionManagement API](https://learn.microsoft.com/en-us/dynamics365/business-central/application/base-application/codeunit/microsoft.finance.dimension.dimensionmanagement). +- [BCApps: Gen. Journal Line dimension validation](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Finance/GeneralLedger/Journal/GenJournalLine.Table.al). diff --git a/microsoft/knowledge/interfaces/guard-interface-casts-with-is.bad.al b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.bad.al new file mode 100644 index 00000000..4fd98be0 --- /dev/null +++ b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.bad.al @@ -0,0 +1,20 @@ +interface "I Quote Amount Bad" +{ + procedure GetAmount(): Decimal; +} + +interface "I Quote Date Bad" +{ + procedure GetDate(): Date; +} + +codeunit 50611 "Quote Reader Bad" +{ + procedure GetDate(Quote: Interface "I Quote Amount Bad"): Date + var + DatedQuote: Interface "I Quote Date Bad"; + begin + DatedQuote := Quote as "I Quote Date Bad"; + exit(DatedQuote.GetDate()); + end; +} diff --git a/microsoft/knowledge/interfaces/guard-interface-casts-with-is.good.al b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.good.al new file mode 100644 index 00000000..f58a19d8 --- /dev/null +++ b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.good.al @@ -0,0 +1,24 @@ +interface "I Quote Amount Good" +{ + procedure GetAmount(): Decimal; +} + +interface "I Quote Date Good" +{ + procedure GetDate(): Date; +} + +codeunit 50610 "Quote Reader Good" +{ + procedure TryGetDate(Quote: Interface "I Quote Amount Good"; var QuoteDate: Date): Boolean + var + DatedQuote: Interface "I Quote Date Good"; + begin + if not (Quote is "I Quote Date Good") then + exit(false); + + DatedQuote := Quote as "I Quote Date Good"; + QuoteDate := DatedQuote.GetDate(); + exit(true); + end; +} diff --git a/microsoft/knowledge/interfaces/guard-interface-casts-with-is.md b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.md new file mode 100644 index 00000000..ad9c38e2 --- /dev/null +++ b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.md @@ -0,0 +1,30 @@ +--- +bc-version: [25..] +domain: interfaces +keywords: [interface, is-operator, as-operator, type-test, cast, variant, runtime-error] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Guard optional interface casts with `is` + +## Description + +From runtime 15.0, AL can type-test an interface or `Variant` with `is` and cast it to another interface with `as`. The test is non-throwing, but `as` raises a runtime error when the underlying codeunit does not implement the target interface. This matters when an extended capability is optional or implementations can come from other extensions. + +## Best Practice + +Use `is` to establish that the value supports the target interface before using `as`. Cast directly only where the target implementation is an invariant guaranteed by the surrounding contract. + +See sample: [`guard-interface-casts-with-is.good.al`](guard-interface-casts-with-is.good.al). + +## Anti Pattern + +Using `as` unconditionally for an optional extended interface. An otherwise valid implementation of the base interface then fails at runtime merely because it does not implement the additional contract. + +See sample: [`guard-interface-casts-with-is.bad.al`](guard-interface-casts-with-is.bad.al). + +## Reference + +[Understand type testing and casting operators for interfaces](https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/type-testing) diff --git a/microsoft/knowledge/scm/cancel-reservations-through-reservation-management.bad.al b/microsoft/knowledge/scm/cancel-reservations-through-reservation-management.bad.al new file mode 100644 index 00000000..248f3a40 --- /dev/null +++ b/microsoft/knowledge/scm/cancel-reservations-through-reservation-management.bad.al @@ -0,0 +1,13 @@ +codeunit 50106 "SCM Cancel Reservation Bad" +{ + procedure CancelSalesReservation(ReservationEntryNo: Integer) + var + ReservationEntry: Record "Reservation Entry"; + begin + ReservationEntry.Get(ReservationEntryNo, false); + ReservationEntry.TestField("Source Type", Database::"Sales Line"); + ReservationEntry.TestField("Reservation Status", ReservationEntry."Reservation Status"::Reservation); + + ReservationEntry.Delete(true); + end; +} diff --git a/microsoft/knowledge/scm/cancel-reservations-through-reservation-management.good.al b/microsoft/knowledge/scm/cancel-reservations-through-reservation-management.good.al new file mode 100644 index 00000000..3d5849dd --- /dev/null +++ b/microsoft/knowledge/scm/cancel-reservations-through-reservation-management.good.al @@ -0,0 +1,14 @@ +codeunit 50107 "SCM Cancel Reservation Good" +{ + procedure CancelSalesReservation(ReservationEntryNo: Integer) + var + ReservationEntry: Record "Reservation Entry"; + ReservationEngineMgt: Codeunit "Reservation Engine Mgt."; + begin + ReservationEntry.Get(ReservationEntryNo, false); + ReservationEntry.TestField("Source Type", Database::"Sales Line"); + ReservationEntry.TestField("Reservation Status", ReservationEntry."Reservation Status"::Reservation); + + ReservationEngineMgt.CancelReservation(ReservationEntry); + end; +} diff --git a/microsoft/knowledge/scm/cancel-reservations-through-reservation-management.md b/microsoft/knowledge/scm/cancel-reservations-through-reservation-management.md new file mode 100644 index 00000000..1df3b125 --- /dev/null +++ b/microsoft/knowledge/scm/cancel-reservations-through-reservation-management.md @@ -0,0 +1,38 @@ +--- +bc-version: [all] +domain: scm +keywords: [reservation-entry, cancelreservation, reservation-engine-mgt, reservation-status, order-tracking, disallow-cancellation] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Cancel reservations through reservation management + +## Description + +Persistent `"Reservation Entry"` rows are not disposable allocation markers. Reservation and Tracking links use an entry-number pair with opposite `Positive` values, while Surplus and Prospect entries can legitimately be unpaired. Cancelling a binding reservation must handle its counterpart and any remaining item tracking or order tracking, not just remove a row. + +## Best Practice + +Explicit cancellation of an existing binding reservation uses `"Reservation Engine Mgt.".CancelReservation`. It checks the reservation status and `"Disallow Cancellation"`, handles the counterpart, and preserves or retracks the remaining source quantities as appropriate. Source-line quantity changes have their own source-specific reservation management path. + +Not every Reservation Entry has a partner or identical lot/serial values on both sides: Surplus/Prospect entries and supported late-binding scenarios have different relationships. Temporary buffers, engine-owned updates, and supported publisher metadata are not independent cancellation. Cancelling a reservation is also different from intentionally removing an item-tracking assignment. + +The samples retrieve the negative side of a persistent sales-line reservation and cancel only the binding. They do not delete the sales line or remove its tracking specifications. + +See sample: [`cancel-reservations-through-reservation-management.good.al`](cancel-reservations-through-reservation-management.good.al). + +## Anti Pattern + +`Delete(true)`, `DeleteAll`, or a status/source rewrite on persistent `"Reservation Entry"` records does not perform binding-reservation cancellation. Even deleting both sides can discard tracking that should survive and omit retracking. + +Normal processing of temporary Prospect/Surplus buffers is outside that cancellation workflow, and an unpaired row is not intrinsically an orphan. + +See sample: [`cancel-reservations-through-reservation-management.bad.al`](cancel-reservations-through-reservation-management.bad.al). + +## References + +- [Reservation, order tracking, and action messaging](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-reservation-order-tracking-and-action-messaging) +- [Item tracking and reservations](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-item-tracking-and-reservations) +- [BaseApp reservation cancellation](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Tracking/ReservationEngineMgt.Codeunit.al#L51-L90) diff --git a/microsoft/knowledge/scm/carry-out-requisition-actions-through-the-standard-workflow.bad.al b/microsoft/knowledge/scm/carry-out-requisition-actions-through-the-standard-workflow.bad.al new file mode 100644 index 00000000..7bc4df82 --- /dev/null +++ b/microsoft/knowledge/scm/carry-out-requisition-actions-through-the-standard-workflow.bad.al @@ -0,0 +1,47 @@ +codeunit 50116 "SCM Requisition Action Bad" +{ + procedure CarryOutAcceptedNewPurchase(TemplateName: Code[10]; BatchName: Code[10]; LineNo: Integer; OrderDate: Date; PostingDate: Date; ReceiptDate: Date; CutoffDate: Date) + var + RequisitionLine: Record "Requisition Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + begin + if (OrderDate = 0D) or (PostingDate = 0D) or (ReceiptDate = 0D) or (CutoffDate = 0D) then + Error(PlanningDatesErr); + RequisitionLine.Get(TemplateName, BatchName, LineNo); + RequisitionLine.TestField(Type, RequisitionLine.Type::Item); + RequisitionLine.TestField("Replenishment System", RequisitionLine."Replenishment System"::Purchase); + RequisitionLine.TestField("Action Message", RequisitionLine."Action Message"::New); + RequisitionLine.TestField("Accept Action Message", true); + RequisitionLine.TestField("Demand Type", Database::"Sales Line"); + RequisitionLine.TestField("Demand Order No."); + RequisitionLine.TestField("Vendor No."); + RequisitionLine.SetRecFilter(); + + PurchaseHeader.Init(); + PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::Order; + PurchaseHeader.Insert(true); + PurchaseHeader.Validate("Buy-from Vendor No.", RequisitionLine."Vendor No."); + PurchaseHeader.Validate("Order Date", OrderDate); + PurchaseHeader.Validate("Posting Date", PostingDate); + PurchaseHeader.Validate("Expected Receipt Date", ReceiptDate); + PurchaseHeader.Modify(true); + + PurchaseLine.Init(); + PurchaseLine."Document Type" := PurchaseHeader."Document Type"; + PurchaseLine."Document No." := PurchaseHeader."No."; + PurchaseLine."Line No." := 10000; + PurchaseLine.Validate(Type, PurchaseLine.Type::Item); + PurchaseLine.Validate("No.", RequisitionLine."No."); + PurchaseLine.Validate("Location Code", RequisitionLine."Location Code"); + PurchaseLine.Validate("Variant Code", RequisitionLine."Variant Code"); + PurchaseLine.Validate("Unit of Measure Code", RequisitionLine."Unit of Measure Code"); + PurchaseLine.Validate(Quantity, RequisitionLine.Quantity); + PurchaseLine.Insert(true); + + RequisitionLine.Delete(true); + end; + + var + PlanningDatesErr: Label 'Supply explicit order, posting, receipt, and cutoff dates.'; +} diff --git a/microsoft/knowledge/scm/carry-out-requisition-actions-through-the-standard-workflow.good.al b/microsoft/knowledge/scm/carry-out-requisition-actions-through-the-standard-workflow.good.al new file mode 100644 index 00000000..c09ea65a --- /dev/null +++ b/microsoft/knowledge/scm/carry-out-requisition-actions-through-the-standard-workflow.good.al @@ -0,0 +1,31 @@ +codeunit 50117 "SCM Requisition Action Good" +{ + procedure CarryOutAcceptedNewPurchase(TemplateName: Code[10]; BatchName: Code[10]; LineNo: Integer; OrderDate: Date; PostingDate: Date; ReceiptDate: Date; CutoffDate: Date) + var + RequisitionLine: Record "Requisition Line"; + PurchaseHeaderDefaults: Record "Purchase Header"; + ReqWkshMakeOrder: Codeunit "Req. Wksh.-Make Order"; + begin + if (OrderDate = 0D) or (PostingDate = 0D) or (ReceiptDate = 0D) or (CutoffDate = 0D) then + Error(PlanningDatesErr); + RequisitionLine.Get(TemplateName, BatchName, LineNo); + RequisitionLine.TestField(Type, RequisitionLine.Type::Item); + RequisitionLine.TestField("Replenishment System", RequisitionLine."Replenishment System"::Purchase); + RequisitionLine.TestField("Action Message", RequisitionLine."Action Message"::New); + RequisitionLine.TestField("Accept Action Message", true); + RequisitionLine.TestField("Demand Type", Database::"Sales Line"); + RequisitionLine.TestField("Demand Order No."); + RequisitionLine.TestField("Vendor No."); + RequisitionLine.SetRecFilter(); + + PurchaseHeaderDefaults."Order Date" := OrderDate; + PurchaseHeaderDefaults."Posting Date" := PostingDate; + PurchaseHeaderDefaults."Expected Receipt Date" := ReceiptDate; + ReqWkshMakeOrder.Set(PurchaseHeaderDefaults, CutoffDate, false); + ReqWkshMakeOrder.SetSuppressCommit(true); + ReqWkshMakeOrder.CarryOutBatchAction(RequisitionLine); + end; + + var + PlanningDatesErr: Label 'Supply explicit order, posting, receipt, and cutoff dates.'; +} diff --git a/microsoft/knowledge/scm/carry-out-requisition-actions-through-the-standard-workflow.md b/microsoft/knowledge/scm/carry-out-requisition-actions-through-the-standard-workflow.md new file mode 100644 index 00000000..88f68bdf --- /dev/null +++ b/microsoft/knowledge/scm/carry-out-requisition-actions-through-the-standard-workflow.md @@ -0,0 +1,43 @@ +--- +bc-version: [all] +domain: scm +keywords: [requisition-line, action-message, accept-action-message, req-wksh-make-order, carryoutbatchaction, demand-order-no, planning-flexibility] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Carry out requisition actions through the standard workflow + +## Description + +A requisition/planning line is a pending change to a supply/demand network, not just a template for a purchase line. Carry-out interprets New, change-quantity, reschedule, and cancel actions, preserves referenced supply and planning flexibility, and moves reservation/tracking ownership before finalizing the proposal. Creating a plausible purchase order and deleting the requisition line can leave new supply unrelated to the demand that caused it. + +## Best Practice + +Requisition batch carry-out initializes `"Req. Wksh.-Make Order"` with `Set` and invokes `CarryOutBatchAction` on the intended accepted lines. Order/posting/receipt defaults are separate from the ending-order-date cutoff, and worksheet/batch/line filters define the selection. A plain `Run` or a single order-line insertion helper is not a replacement for this batch initialization and finalization. + +The standard `"Carry Out Action"` dispatch handles broader planning output and its configured purchase, transfer, assembly, or manufacturing choices. Each action's supply change and source-specific reservation transfer precede proposal finalization; not every action creates a new purchase order. + +Ordinary manual purchase creation that does not consume planning output is outside this rule. Users may reject or delete unwanted proposals without creating supply; temporary planning simulations, pre-carry-out enrichment, and engine-owned cleanup are also legitimate. `Delete(true)` on a requisition line is not intrinsically a defect. + +The samples select an existing accepted New/Purchase item proposal with sales-demand context. Dates are explicit, the source selection remains bounded, and the clean sample leaves order creation and reservation handoff to the standard workflow; it is not a complete planning-run generator. + +See sample: [`carry-out-requisition-actions-through-the-standard-workflow.good.al`](carry-out-requisition-actions-through-the-standard-workflow.good.al). + +## Anti Pattern + +Manually creating or changing supply from a subset of an accepted persistent `"Requisition Line"`, then deleting or marking that proposal handled, skips the standard carry-out/source-reservation handoff. Purchase-field validation and the requisition delete trigger do not first move the proposal's demand links to the new purchase line. + +Deleting an unwanted suggestion or creating an ordinary purchase order without consuming planning output is a separate operation. The standard carry-out engine's own insert/delete sequence participates in the source handoff rather than replacing it. + +See sample: [`carry-out-requisition-actions-through-the-standard-workflow.bad.al`](carry-out-requisition-actions-through-the-standard-workflow.bad.al). + +## References + +- [Perform planning action messages](https://learn.microsoft.com/en-us/dynamics365/business-central/production-how-to-run-mps-and-mrp#to-perform-action-messages) +- [Planning functionality](https://learn.microsoft.com/en-us/dynamics365/business-central/production-about-planning-functionality) +- [Reservation, order tracking, and action messaging](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-reservation-order-tracking-and-action-messaging) +- [BaseApp carry-out caller and date defaults](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Requisition/CarryOutActionMsgReq.Report.al#L116-L133) +- [Batch initialization and selection](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Requisition/ReqWkshMakeOrder.Codeunit.al#L116-L215) +- [Reservation handoff before supply finalization](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Requisition/ReqWkshMakeOrder.Codeunit.al#L663-L743) diff --git a/microsoft/knowledge/scm/change-item-applications-through-posting-routines.bad.al b/microsoft/knowledge/scm/change-item-applications-through-posting-routines.bad.al new file mode 100644 index 00000000..f0239dba --- /dev/null +++ b/microsoft/knowledge/scm/change-item-applications-through-posting-routines.bad.al @@ -0,0 +1,33 @@ +codeunit 50104 "SCM Item Application Bad" +{ + procedure ChangeSalesQuantityApplication(ApplicationEntryNo: Integer; NewInboundEntryNo: Integer) + var + ItemApplicationEntry: Record "Item Application Entry"; + OutboundItemLedgerEntry: Record "Item Ledger Entry"; + InboundItemLedgerEntry: Record "Item Ledger Entry"; + begin + ItemApplicationEntry.Get(ApplicationEntryNo); + ItemApplicationEntry.TestField(Quantity); + ItemApplicationEntry.TestField("Inbound Item Entry No."); + ItemApplicationEntry.TestField("Outbound Item Entry No."); + ItemApplicationEntry.TestField("Transferred-from Entry No.", 0); + if ItemApplicationEntry.CostApplication() then + Error(QuantityApplicationErr); + OutboundItemLedgerEntry.Get(ItemApplicationEntry."Outbound Item Entry No."); + OutboundItemLedgerEntry.TestField("Entry Type", OutboundItemLedgerEntry."Entry Type"::Sale); + OutboundItemLedgerEntry.TestField(Positive, false); + OutboundItemLedgerEntry.TestField("Drop Shipment", false); + OutboundItemLedgerEntry.TestField(Correction, false); + InboundItemLedgerEntry.Get(NewInboundEntryNo); + InboundItemLedgerEntry.TestField(Positive, true); + InboundItemLedgerEntry.TestField("Item No.", OutboundItemLedgerEntry."Item No."); + InboundItemLedgerEntry.TestField("Variant Code", OutboundItemLedgerEntry."Variant Code"); + InboundItemLedgerEntry.TestField("Location Code", OutboundItemLedgerEntry."Location Code"); + + ItemApplicationEntry."Inbound Item Entry No." := NewInboundEntryNo; + ItemApplicationEntry.Modify(true); + end; + + var + QuantityApplicationErr: Label 'Select an ordinary quantity application, not a cost application.'; +} diff --git a/microsoft/knowledge/scm/change-item-applications-through-posting-routines.good.al b/microsoft/knowledge/scm/change-item-applications-through-posting-routines.good.al new file mode 100644 index 00000000..e176aadd --- /dev/null +++ b/microsoft/knowledge/scm/change-item-applications-through-posting-routines.good.al @@ -0,0 +1,40 @@ +codeunit 50105 "SCM Item Application Good" +{ + procedure ChangeSalesQuantityApplication(ApplicationEntryNo: Integer; NewInboundEntryNo: Integer) + var + ItemApplicationEntry: Record "Item Application Entry"; + OutboundItemLedgerEntry: Record "Item Ledger Entry"; + InboundItemLedgerEntry: Record "Item Ledger Entry"; + ItemJnlPostLine: Codeunit "Item Jnl.-Post Line"; + OutboundEntryNo: Integer; + begin + ItemApplicationEntry.Get(ApplicationEntryNo); + ItemApplicationEntry.TestField(Quantity); + ItemApplicationEntry.TestField("Inbound Item Entry No."); + ItemApplicationEntry.TestField("Outbound Item Entry No."); + ItemApplicationEntry.TestField("Transferred-from Entry No.", 0); + if ItemApplicationEntry.CostApplication() then + Error(QuantityApplicationErr); + OutboundEntryNo := ItemApplicationEntry."Outbound Item Entry No."; + OutboundItemLedgerEntry.Get(OutboundEntryNo); + OutboundItemLedgerEntry.TestField("Entry Type", OutboundItemLedgerEntry."Entry Type"::Sale); + OutboundItemLedgerEntry.TestField(Positive, false); + OutboundItemLedgerEntry.TestField("Drop Shipment", false); + OutboundItemLedgerEntry.TestField(Correction, false); + InboundItemLedgerEntry.Get(NewInboundEntryNo); + InboundItemLedgerEntry.TestField(Positive, true); + InboundItemLedgerEntry.TestField("Item No.", OutboundItemLedgerEntry."Item No."); + InboundItemLedgerEntry.TestField("Variant Code", OutboundItemLedgerEntry."Variant Code"); + InboundItemLedgerEntry.TestField("Location Code", OutboundItemLedgerEntry."Location Code"); + + ItemJnlPostLine.UnApply(ItemApplicationEntry); + OutboundItemLedgerEntry.Get(OutboundEntryNo); + ItemJnlPostLine.ReApply(OutboundItemLedgerEntry, NewInboundEntryNo); + ItemJnlPostLine.RedoApplications(); + ItemJnlPostLine.CostAdjust(); + ItemJnlPostLine.ClearApplicationLog(); + end; + + var + QuantityApplicationErr: Label 'Select an ordinary quantity application, not a cost application.'; +} diff --git a/microsoft/knowledge/scm/change-item-applications-through-posting-routines.md b/microsoft/knowledge/scm/change-item-applications-through-posting-routines.md new file mode 100644 index 00000000..5b5443e5 --- /dev/null +++ b/microsoft/knowledge/scm/change-item-applications-through-posting-routines.md @@ -0,0 +1,39 @@ +--- +bc-version: [all] +domain: scm +keywords: [item-application-entry, inbound-item-entry-no, unapply, reapply, redoapplications, costadjust, application-worksheet] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Change item applications through posting routines + +## Description + +An `"Item Application Entry"` connects quantity application to cost flow; changing its inbound entry number is not merely fixing a foreign key. Unapplication/reapplication also affects ledger remaining quantities, open states, valuation, and entries needing cost adjustment. Direct edits can leave a plausible application row attached to inconsistent inventory and costs. + +## Best Practice + +The Application Worksheet provides the interactive correction workflow. A narrowly controlled programmatic correction of an ordinary quantity application uses the same `"Item Jnl.-Post Line"` instance for `UnApply`, a reload of the affected outbound item entry, and `ReApply` to the compatible inbound entry. Its finalization lifecycle includes `RedoApplications`, `CostAdjust`, and `ClearApplicationLog`. + +The posting routines enforce inventory-period, correction, transfer, and drop-shipment restrictions. Entries with `"Transferred-from Entry No."`, outbound transfers, and special application types are outside the ordinary-sales sample's scope. Application-check bypasses remove those protections, and the worksheet's multi-step recovery flags belong to its UI lifecycle rather than a standalone transaction. + +`CostAdjust` honors automatic-cost-adjustment setup; calling it does not mean all costs are settled when adjustment is disabled or deferred. Scheduled/manual adjustment remains necessary in those configurations. Temporary application projections, extension metadata, and source-document reservation/order-tracking changes are not edits to the persistent item-application graph. + +See sample: [`change-item-applications-through-posting-routines.good.al`](change-item-applications-through-posting-routines.good.al). + +## Anti Pattern + +Independent `Modify`, `Delete`, or replacement `Insert` operations on persistent `"Item Application Entry"` rows can repoint a receipt/shipment application without updating remaining quantities or cost propagation. Valid item numbers, matching quantities, and running table triggers do not complete reapplication. + +Omitting finalization or committing between unapply and reapply exposes an incomplete replacement. The standard posting/application workflow's internal table writes differ because they participate in that lifecycle. + +See sample: [`change-item-applications-through-posting-routines.bad.al`](change-item-applications-through-posting-routines.bad.al). + +## References + +- [Item application design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-item-application) +- [Cost adjustment design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-cost-adjustment) +- [BaseApp application finalization sequence](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Ledger/ApplicationWorksheet.Page.al#L495-L503) +- [BaseApp reapplication and cost-adjustment lifecycle](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Posting/ItemJnlPostLine.Codeunit.al#L5464-L5538) diff --git a/microsoft/knowledge/scm/post-item-ledger-changes-through-item-journals.bad.al b/microsoft/knowledge/scm/post-item-ledger-changes-through-item-journals.bad.al new file mode 100644 index 00000000..1187f96d --- /dev/null +++ b/microsoft/knowledge/scm/post-item-ledger-changes-through-item-journals.bad.al @@ -0,0 +1,32 @@ +codeunit 50100 "SCM Stock Adjustment Bad" +{ + procedure PostPreparedPositiveAdjustment(ItemJournalLine: Record "Item Journal Line"; NewEntryNo: Integer) + var + ItemLedgerEntry: Record "Item Ledger Entry"; + begin + ItemJournalLine.TestField("Entry Type", ItemJournalLine."Entry Type"::"Positive Adjmt."); + ItemJournalLine.TestField("Value Entry Type", ItemJournalLine."Value Entry Type"::"Direct Cost"); + ItemJournalLine.TestField("Item No."); + ItemJournalLine.TestField("Posting Date"); + ItemJournalLine.TestField("Quantity (Base)"); + if ItemJournalLine."Quantity (Base)" < 0 then + Error(PositiveQuantityErr); + + ItemLedgerEntry.Init(); + ItemLedgerEntry."Entry No." := NewEntryNo; + ItemLedgerEntry."Item No." := ItemJournalLine."Item No."; + ItemLedgerEntry."Entry Type" := ItemJournalLine."Entry Type"; + ItemLedgerEntry."Posting Date" := ItemJournalLine."Posting Date"; + ItemLedgerEntry."Document No." := ItemJournalLine."Document No."; + ItemLedgerEntry."Location Code" := ItemJournalLine."Location Code"; + ItemLedgerEntry."Variant Code" := ItemJournalLine."Variant Code"; + ItemLedgerEntry.Quantity := ItemJournalLine."Quantity (Base)"; + ItemLedgerEntry."Remaining Quantity" := ItemLedgerEntry.Quantity; + ItemLedgerEntry.Positive := true; + ItemLedgerEntry.Open := true; + ItemLedgerEntry.Insert(true); + end; + + var + PositiveQuantityErr: Label 'The prepared adjustment must increase inventory.'; +} diff --git a/microsoft/knowledge/scm/post-item-ledger-changes-through-item-journals.good.al b/microsoft/knowledge/scm/post-item-ledger-changes-through-item-journals.good.al new file mode 100644 index 00000000..3e7866a1 --- /dev/null +++ b/microsoft/knowledge/scm/post-item-ledger-changes-through-item-journals.good.al @@ -0,0 +1,20 @@ +codeunit 50101 "SCM Stock Adjustment Good" +{ + procedure PostPreparedPositiveAdjustment(var ItemJournalLine: Record "Item Journal Line") + var + ItemJnlPostLine: Codeunit "Item Jnl.-Post Line"; + begin + ItemJournalLine.TestField("Entry Type", ItemJournalLine."Entry Type"::"Positive Adjmt."); + ItemJournalLine.TestField("Value Entry Type", ItemJournalLine."Value Entry Type"::"Direct Cost"); + ItemJournalLine.TestField("Item No."); + ItemJournalLine.TestField("Posting Date"); + ItemJournalLine.TestField("Quantity (Base)"); + if ItemJournalLine."Quantity (Base)" < 0 then + Error(PositiveQuantityErr); + + ItemJnlPostLine.RunWithCheck(ItemJournalLine); + end; + + var + PositiveQuantityErr: Label 'The prepared adjustment must increase inventory.'; +} diff --git a/microsoft/knowledge/scm/post-item-ledger-changes-through-item-journals.md b/microsoft/knowledge/scm/post-item-ledger-changes-through-item-journals.md new file mode 100644 index 00000000..0df6dac2 --- /dev/null +++ b/microsoft/knowledge/scm/post-item-ledger-changes-through-item-journals.md @@ -0,0 +1,42 @@ +--- +bc-version: [all] +domain: scm +keywords: [item-ledger-entry, value-entry, item-journal-line, item-jnl-post-line, runwithcheck, inventory-posting] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Post item ledger changes through item journals + +## Description + +An item ledger entry is not an independently insertable stock balance. Posting connects its quantity to item applications, reservations, tracking, and one or more value entries; expected cost, invoicing, revaluation, and later cost adjustment can produce different value entries for the same item entry. Running a table's insert trigger does not run this posting workflow or its configured inventory-to-G/L integration. + +## Best Practice + +The posting entry point for a prepared standalone item-journal movement is `"Item Jnl.-Post Line".RunWithCheck`. Persisted journal batches use `"Item Jnl.-Post Batch"`; sales, purchase, transfer, assembly, and production transactions retain their owning document/posting orchestration. Those workflows create the ledger, value, and application records together with their required checks. + +Supported posting events enrich validated journal/source data within the owning workflow. Read-only ledger queries, temporary ledger previews, extension-owned metadata fields, and publisher parameters consumed by the poster are not independent ledger posting. A temporary item-journal buffer can still produce persistent entries when passed to a posting codeunit. A publisher's `var` parameter or `IsHandled` flag alone does not supply the missing quantity/cost coordination; the normal engine's own inserts operate within that coordination. + +Not every value entry points to an item ledger entry: capacity and production WIP have their own supported posting relationships. Assembly and manufacturing posting retain order/component/routing and capacity context; one bare output/consumption call is not full order completion. + +The clean sample takes an already prepared positive-adjustment journal line. It is not a substitute for journal preparation, batch revaluation, warehouse reconciliation, or source-document posting. + +See sample: [`post-item-ledger-changes-through-item-journals.good.al`](post-item-ledger-changes-through-item-journals.good.al). + +## Anti Pattern + +Independent inserts/deletes of persistent `"Item Ledger Entry"` or `"Value Entry"` transaction rows, or overwrites of posted quantity, remaining quantity, application identity, or cost amounts, bypass the coordinated receipt, shipment, adjustment, or cost-correction workflow. `Insert(true)`, `Modify(true)`, and balanced-looking quantities do not supply that orchestration: stock can change without the corresponding application/value graph, or downstream cost flow can remain stale. + +A table declaration or custom annotation-field update does not change inventory quantities or costs and is outside this transaction-state concern. + +See sample: [`post-item-ledger-changes-through-item-journals.bad.al`](post-item-ledger-changes-through-item-journals.bad.al). + +## References + +- [Inventory posting design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-inventory-posting) +- [Item application design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-item-application) +- [BaseApp item-journal posting entry point](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Posting/ItemJnlPostLine.Codeunit.al#L162-L179) +- [Assembly-order posting context](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-assembly-order-posting) +- [Production-order posting context](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-production-order-posting) diff --git a/microsoft/knowledge/scm/post-revaluation-through-the-item-journal-batch.bad.al b/microsoft/knowledge/scm/post-revaluation-through-the-item-journal-batch.bad.al new file mode 100644 index 00000000..df450cc8 --- /dev/null +++ b/microsoft/knowledge/scm/post-revaluation-through-the-item-journal-batch.bad.al @@ -0,0 +1,17 @@ +codeunit 50102 "SCM Revaluation Batch Bad" +{ + procedure PostCalculatedRevaluationBatch(TemplateName: Code[10]; BatchName: Code[10]) + var + ItemJournalLine: Record "Item Journal Line"; + ItemJnlPostLine: Codeunit "Item Jnl.-Post Line"; + begin + ItemJournalLine.SetRange("Journal Template Name", TemplateName); + ItemJournalLine.SetRange("Journal Batch Name", BatchName); + ItemJournalLine.FindSet(); + repeat + ItemJournalLine.TestField("Value Entry Type", ItemJournalLine."Value Entry Type"::Revaluation); + ItemJournalLine.TestField("Inventory Value Per"); + ItemJnlPostLine.RunWithCheck(ItemJournalLine); + until ItemJournalLine.Next() = 0; + end; +} diff --git a/microsoft/knowledge/scm/post-revaluation-through-the-item-journal-batch.good.al b/microsoft/knowledge/scm/post-revaluation-through-the-item-journal-batch.good.al new file mode 100644 index 00000000..810fad5d --- /dev/null +++ b/microsoft/knowledge/scm/post-revaluation-through-the-item-journal-batch.good.al @@ -0,0 +1,16 @@ +codeunit 50103 "SCM Revaluation Batch Good" +{ + procedure PostCalculatedRevaluationBatch(TemplateName: Code[10]; BatchName: Code[10]) + var + ItemJournalLine: Record "Item Journal Line"; + ItemJnlPostBatch: Codeunit "Item Jnl.-Post Batch"; + begin + ItemJournalLine.SetRange("Journal Template Name", TemplateName); + ItemJournalLine.SetRange("Journal Batch Name", BatchName); + ItemJournalLine.FindFirst(); + ItemJournalLine.TestField("Value Entry Type", ItemJournalLine."Value Entry Type"::Revaluation); + ItemJournalLine.TestField("Inventory Value Per"); + + ItemJnlPostBatch.Run(ItemJournalLine); + end; +} diff --git a/microsoft/knowledge/scm/post-revaluation-through-the-item-journal-batch.md b/microsoft/knowledge/scm/post-revaluation-through-the-item-journal-batch.md new file mode 100644 index 00000000..9730c2dc --- /dev/null +++ b/microsoft/knowledge/scm/post-revaluation-through-the-item-journal-batch.md @@ -0,0 +1,38 @@ +--- +bc-version: [all] +domain: scm +keywords: [revaluation, inventory-value-per, partial-revaluation, item-jnl-post-batch, item-journal-line, runwithcheck, standard-cost] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Post calculated revaluation through the item journal batch + +## Description + +A calculated revaluation line with nonblank `"Inventory Value Per"` represents an aggregate, not a finalized posting against one item ledger entry. Codeunit `"Item Jnl.-Post Batch"` distributes that value over eligible entries, handles rounding, and coordinates Item/SKU standard-cost updates. Calling the line poster directly skips that batch work even though the input is a valid `"Item Journal Line"`. + +## Best Practice + +Posting a prepared revaluation batch through `"Item Jnl.-Post Batch"` preserves the calculated line's valuation date, aggregation scope, location/variant filters, and revaluation fields. The batch expands summarized values into per-entry postings and checks that the eligible inventory has not changed; for partial revaluation it also rechecks remaining quantity before posting. + +The public `"Item Jnl.-Post Line".RunWithCheck` API does not replace that orchestration. It remains legitimate for finalized individual-entry revaluation lines within a workflow that already supplies the necessary checks; the batch itself uses the line poster. Ordinary quantity journals and finalized per-entry revaluations are distinct from this summarized/partial-revaluation case. + +The samples explicitly require `"Value Entry Type" = Revaluation` and a nonblank `"Inventory Value Per"` in an existing calculated journal batch. They demonstrate posting, not how to calculate a new valuation or choose a standard cost. + +See sample: [`post-revaluation-through-the-item-journal-batch.good.al`](post-revaluation-through-the-item-journal-batch.good.al). + +## Anti Pattern + +A loop that sends calculated aggregate revaluation lines straight to `"Item Jnl.-Post Line"` skips distribution over the underlying item entries. A partial-revaluation workflow without the remaining-quantity recheck can post a valuation against inventory that no longer matches the calculation. + +Directly editing existing `"Value Entry"` cost amounts or the Item's unit cost does not repair those allocation and adjustment relationships. Their correction belongs to the revaluation/cost-adjustment workflow. + +See sample: [`post-revaluation-through-the-item-journal-batch.bad.al`](post-revaluation-through-the-item-journal-batch.bad.al). + +## References + +- [Revaluation design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-revaluation) +- [Inventory posting design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-inventory-posting) +- [BaseApp summarized revaluation and remaining-quantity checks](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Posting/ItemJnlPostBatch.Codeunit.al#L510-L714) diff --git a/microsoft/knowledge/scm/post-transfers-through-shipment-and-receipt-codeunits.bad.al b/microsoft/knowledge/scm/post-transfers-through-shipment-and-receipt-codeunits.bad.al new file mode 100644 index 00000000..2e149200 --- /dev/null +++ b/microsoft/knowledge/scm/post-transfers-through-shipment-and-receipt-codeunits.bad.al @@ -0,0 +1,22 @@ +codeunit 50112 "SCM Transfer Posting Bad" +{ + procedure ShipTransferOrder(TransferOrderNo: Code[20]; var ItemJournalLine: Record "Item Journal Line") + var + TransferHeader: Record "Transfer Header"; + Location: Record Location; + ItemJnlPostLine: Codeunit "Item Jnl.-Post Line"; + begin + TransferHeader.Get(TransferOrderNo); + TransferHeader.TestField("Direct Transfer", false); + TransferHeader.TestField("In-Transit Code"); + Location.Get(TransferHeader."Transfer-from Code"); + Location.TestField("Require Shipment", false); + ItemJournalLine.TestField("Entry Type", ItemJournalLine."Entry Type"::Transfer); + ItemJournalLine.TestField("Location Code", TransferHeader."Transfer-from Code"); + ItemJournalLine.TestField("New Location Code", TransferHeader."In-Transit Code"); + + ItemJnlPostLine.RunWithCheck(ItemJournalLine); + TransferHeader."Last Shipment No." := ItemJournalLine."Document No."; + TransferHeader.Modify(true); + end; +} diff --git a/microsoft/knowledge/scm/post-transfers-through-shipment-and-receipt-codeunits.good.al b/microsoft/knowledge/scm/post-transfers-through-shipment-and-receipt-codeunits.good.al new file mode 100644 index 00000000..8507c99b --- /dev/null +++ b/microsoft/knowledge/scm/post-transfers-through-shipment-and-receipt-codeunits.good.al @@ -0,0 +1,32 @@ +codeunit 50113 "SCM Transfer Posting Good" +{ + procedure ShipTransferOrder(TransferOrderNo: Code[20]) + var + TransferHeader: Record "Transfer Header"; + Location: Record Location; + TransferOrderPostShipment: Codeunit "TransferOrder-Post Shipment"; + begin + TransferHeader.Get(TransferOrderNo); + TransferHeader.TestField("Direct Transfer", false); + TransferHeader.TestField("In-Transit Code"); + Location.Get(TransferHeader."Transfer-from Code"); + Location.TestField("Require Shipment", false); + + TransferOrderPostShipment.Run(TransferHeader); + end; + + procedure ReceiveTransferOrder(TransferOrderNo: Code[20]) + var + TransferHeader: Record "Transfer Header"; + Location: Record Location; + TransferOrderPostReceipt: Codeunit "TransferOrder-Post Receipt"; + begin + TransferHeader.Get(TransferOrderNo); + TransferHeader.TestField("Direct Transfer", false); + TransferHeader.TestField("In-Transit Code"); + Location.Get(TransferHeader."Transfer-to Code"); + Location.TestField("Require Receive", false); + + TransferOrderPostReceipt.Run(TransferHeader); + end; +} diff --git a/microsoft/knowledge/scm/post-transfers-through-shipment-and-receipt-codeunits.md b/microsoft/knowledge/scm/post-transfers-through-shipment-and-receipt-codeunits.md new file mode 100644 index 00000000..b541dbab --- /dev/null +++ b/microsoft/knowledge/scm/post-transfers-through-shipment-and-receipt-codeunits.md @@ -0,0 +1,41 @@ +--- +bc-version: [all] +domain: scm +keywords: [transfer-header, transfer-line, transferorder-post-shipment, transferorder-post-receipt, in-transit-code, last-shipment-no, item-application-entry] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Post transfers through shipment and receipt codeunits + +## Description + +A two-step transfer order preserves a continuous quantity, reservation, and cost/application lineage from the source through in-transit to the destination. Its shipment and receipt workflows also own posted documents and derived lines for partial receipt. Posting an item-journal movement and manually marking a transfer header or line as shipped is not equivalent, even if the total quantities balance. + +## Best Practice + +A prepared non-direct transfer order without required warehouse documents uses `"TransferOrder-Post Shipment".Run` at shipment and `"TransferOrder-Post Receipt".Run` at receipt, with the actual `"Transfer Header"`. Validated source quantities to ship/receive prepare the operation; posted quantity counters are results of posting. + +Required warehouse shipment or receipt enters the warehouse document posting workflow, which invokes the transfer poster with its real source context. Direct transfers have their configured standard workflow; the two-step sample's in-transit guard is not a universal requirement. + +Standalone item reclassification journals and bin movements are legitimate separate operations, not completion of an existing transfer order. Tracking/application splits and average-cost handling mean transfers do not have one fixed item-entry count or a nonzero `"Transferred-from Entry No."` on every application. + +See sample: [`post-transfers-through-shipment-and-receipt-codeunits.good.al`](post-transfers-through-shipment-and-receipt-codeunits.good.al). + +## Anti Pattern + +Ad-hoc item postings, independent positive/negative adjustments, manually created posted-transfer rows, and direct shipment/receipt-counter changes cannot substitute for transfer-order posting. Updating `"Last Shipment No."` after a bare item-journal call does not create the posted shipment, source-line progress, or transfer application lineage. + +Changing an existing item ledger entry's location or inventing application links does not repair that missing workflow. Metadata enrichment within the normal shipment/receipt or direct-transfer workflow is distinct from replacing the posting operation. + +See sample: [`post-transfers-through-shipment-and-receipt-codeunits.bad.al`](post-transfers-through-shipment-and-receipt-codeunits.bad.al). + +## References + +- [Transfer inventory between locations](https://learn.microsoft.com/en-us/dynamics365/business-central/inventory-how-transfer-between-locations) +- [Item application design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-item-application) +- [Cost adjustment design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-cost-adjustment) +- [BaseApp shipment journal/source linkage](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Transfer/TransferOrderPostShipment.Codeunit.al#L292-L336) +- [Partial-receipt derived-line handling](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Transfer/TransferOrderPostReceipt.Codeunit.al#L457-L529) +- [Transfer application and average-cost branches](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Posting/ItemJnlPostLine.Codeunit.al#L1911-L1976) diff --git a/microsoft/knowledge/scm/reconcile-warehouse-adjustments-with-the-item-ledger.bad.al b/microsoft/knowledge/scm/reconcile-warehouse-adjustments-with-the-item-ledger.bad.al new file mode 100644 index 00000000..97c7cb4a --- /dev/null +++ b/microsoft/knowledge/scm/reconcile-warehouse-adjustments-with-the-item-ledger.bad.al @@ -0,0 +1,37 @@ +codeunit 50110 "SCM Warehouse Adjustment Bad" +{ + procedure ReconcileRegisteredWarehouseAdjustment(ItemNo: Code[20]; LocationCode: Code[10]; TemplateName: Code[10]; BatchName: Code[10]; PostingDate: Date; DocumentNo: Code[20]): Boolean + var + Item: Record Item; + ItemJournalBatch: Record "Item Journal Batch"; + ItemJournalLine: Record "Item Journal Line"; + Location: Record Location; + CalculateWhseAdjustment: Report "Calculate Whse. Adjustment"; + begin + Location.Get(LocationCode); + Location.TestField("Directed Put-away and Pick", true); + Location.TestField("Adjustment Bin Code"); + ItemJournalBatch.Get(TemplateName, BatchName); + ItemJournalLine.SetRange("Journal Template Name", TemplateName); + ItemJournalLine.SetRange("Journal Batch Name", BatchName); + if not ItemJournalLine.IsEmpty() then + Error(EmptyBatchErr); + + Item.Get(ItemNo); + Item.SetRecFilter(); + Item.SetRange("Location Filter", LocationCode); + ItemJournalLine."Journal Template Name" := TemplateName; + ItemJournalLine."Journal Batch Name" := BatchName; + CalculateWhseAdjustment.SetItemJnlLine(ItemJournalLine); + CalculateWhseAdjustment.SetTableView(Item); + CalculateWhseAdjustment.InitializeRequest(PostingDate, DocumentNo); + CalculateWhseAdjustment.SetHideValidationDialog(true); + CalculateWhseAdjustment.UseRequestPage(false); + CalculateWhseAdjustment.RunModal(); + + exit(true); + end; + + var + EmptyBatchErr: Label 'Use an empty, dedicated item journal batch for warehouse reconciliation.'; +} diff --git a/microsoft/knowledge/scm/reconcile-warehouse-adjustments-with-the-item-ledger.good.al b/microsoft/knowledge/scm/reconcile-warehouse-adjustments-with-the-item-ledger.good.al new file mode 100644 index 00000000..469ed2d1 --- /dev/null +++ b/microsoft/knowledge/scm/reconcile-warehouse-adjustments-with-the-item-ledger.good.al @@ -0,0 +1,42 @@ +codeunit 50111 "SCM Warehouse Adjustment Good" +{ + procedure ReconcileRegisteredWarehouseAdjustment(ItemNo: Code[20]; LocationCode: Code[10]; TemplateName: Code[10]; BatchName: Code[10]; PostingDate: Date; DocumentNo: Code[20]): Boolean + var + Item: Record Item; + ItemJournalBatch: Record "Item Journal Batch"; + ItemJournalLine: Record "Item Journal Line"; + Location: Record Location; + CalculateWhseAdjustment: Report "Calculate Whse. Adjustment"; + ItemJnlPostBatch: Codeunit "Item Jnl.-Post Batch"; + begin + Location.Get(LocationCode); + Location.TestField("Directed Put-away and Pick", true); + Location.TestField("Adjustment Bin Code"); + ItemJournalBatch.Get(TemplateName, BatchName); + ItemJournalLine.SetRange("Journal Template Name", TemplateName); + ItemJournalLine.SetRange("Journal Batch Name", BatchName); + if not ItemJournalLine.IsEmpty() then + Error(EmptyBatchErr); + + Item.Get(ItemNo); + Item.SetRecFilter(); + Item.SetRange("Location Filter", LocationCode); + ItemJournalLine."Journal Template Name" := TemplateName; + ItemJournalLine."Journal Batch Name" := BatchName; + CalculateWhseAdjustment.SetItemJnlLine(ItemJournalLine); + CalculateWhseAdjustment.SetTableView(Item); + CalculateWhseAdjustment.InitializeRequest(PostingDate, DocumentNo); + CalculateWhseAdjustment.SetHideValidationDialog(true); + CalculateWhseAdjustment.UseRequestPage(false); + CalculateWhseAdjustment.RunModal(); + + if ItemJournalLine.FindFirst() then begin + ItemJournalLine.TestField("Warehouse Adjustment", true); + ItemJnlPostBatch.Run(ItemJournalLine); + end; + exit(true); + end; + + var + EmptyBatchErr: Label 'Use an empty, dedicated item journal batch for warehouse reconciliation.'; +} diff --git a/microsoft/knowledge/scm/reconcile-warehouse-adjustments-with-the-item-ledger.md b/microsoft/knowledge/scm/reconcile-warehouse-adjustments-with-the-item-ledger.md new file mode 100644 index 00000000..06230e75 --- /dev/null +++ b/microsoft/knowledge/scm/reconcile-warehouse-adjustments-with-the-item-ledger.md @@ -0,0 +1,40 @@ +--- +bc-version: [all] +domain: scm +keywords: [warehouse-adjustment, calculate-whse-adjustment, adjustment-bin-code, directed-put-away-and-pick, item-journal-line, warehouse-entry] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Reconcile warehouse adjustments with the item ledger + +## Description + +At a Directed Put-away and Pick location, registering an ordinary warehouse quantity or physical-inventory adjustment and synchronizing it to inventory are distinct steps. The warehouse registration balances quantity through the adjustment bin. An additional ordinary item-journal increase/decrease is not the same as consuming that pending warehouse adjustment. + +## Best Practice + +After warehouse registration, `"Calculate Whse. Adjustment"` prepares item-journal lines for the intended item/location and batch; `"Item Jnl.-Post Batch"` posts those lines. The calculation derives the reconciliation by location, variant, units of measure, and tracking, marks the lines `"Warehouse Adjustment"`, and accounts for already prepared unposted adjustments. + +Reconciliation is separate from source-document posting: warehouse receipts/shipments use their document workflows. Intentional warehouse-only staging is valid when a separately owned reconciliation step completes the process; registration need not perform both phases in one call. + +Bin movements need not change total inventory, and warehouse tracking/expiration reclassification has a standard batch path that can also post item-journal entries. Standard reclassification and basic-location item adjustments are not this ordinary advanced-warehouse quantity-adjustment case. + +The samples start after warehouse quantity registration and report whether inventory reconciliation completed. The clean sample calculates and posts into an empty dedicated batch; it is not a complete warehouse physical-count workflow. + +See sample: [`reconcile-warehouse-adjustments-with-the-item-ledger.good.al`](reconcile-warehouse-adjustments-with-the-item-ledger.good.al). + +## Anti Pattern + +A manually mirrored ordinary item-journal line does not reconcile a registered advanced-warehouse quantity adjustment. Likewise, a reconciliation function that returns completion after only registration or adjustment calculation leaves any generated adjustment lines unposted. Calculation prepares journal lines; it does not post them. + +Invented positive/negative quantities or flipping `"Warehouse Adjustment"` on an arbitrary line does not establish the required relationship to the adjustment-bin balance and tracked quantities. That relationship comes from the calculation step. + +See sample: [`reconcile-warehouse-adjustments-with-the-item-ledger.bad.al`](reconcile-warehouse-adjustments-with-the-item-ledger.bad.al). + +## References + +- [Synchronize adjusted warehouse entries with item ledger entries](https://learn.microsoft.com/en-us/dynamics365/business-central/inventory-how-count-adjust-reclassify#to-synchronize-the-adjusted-warehouse-entries-with-the-related-item-ledger-entries) +- [BaseApp warehouse-adjustment calculation](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Warehouse/Journal/CalculateWhseAdjustment.Report.al#L298-L384) +- [Warehouse reclassification exception](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Warehouse/Journal/WhseJnlRegisterBatch.Codeunit.al#L196-L210) diff --git a/microsoft/knowledge/scm/transfer-item-tracking-through-source-reservation-codeunits.bad.al b/microsoft/knowledge/scm/transfer-item-tracking-through-source-reservation-codeunits.bad.al new file mode 100644 index 00000000..89368cfc --- /dev/null +++ b/microsoft/knowledge/scm/transfer-item-tracking-through-source-reservation-codeunits.bad.al @@ -0,0 +1,30 @@ +codeunit 50108 "SCM Tracking Transfer Bad" +{ + procedure TransferBlanketOrderTracking(var SourceBlanketOrderLine: Record "Sales Line"; var DestinationSalesOrderLine: Record "Sales Line"; QuantityBaseToTransfer: Decimal) + var + ReservationEntry: Record "Reservation Entry"; + begin + SourceBlanketOrderLine.TestField("Document Type", SourceBlanketOrderLine."Document Type"::"Blanket Order"); + SourceBlanketOrderLine.TestField(Type, SourceBlanketOrderLine.Type::Item); + DestinationSalesOrderLine.TestField("Document Type", DestinationSalesOrderLine."Document Type"::Order); + DestinationSalesOrderLine.TestField(Type, DestinationSalesOrderLine.Type::Item); + DestinationSalesOrderLine.TestField("No.", SourceBlanketOrderLine."No."); + if QuantityBaseToTransfer <= 0 then + Error(PositiveQuantityErr); + + ReservationEntry.SetRange("Source Type", Database::"Sales Line"); + ReservationEntry.SetRange("Source Subtype", SourceBlanketOrderLine."Document Type".AsInteger()); + ReservationEntry.SetRange("Source ID", SourceBlanketOrderLine."Document No."); + ReservationEntry.SetRange("Source Ref. No.", SourceBlanketOrderLine."Line No."); + ReservationEntry.SetRange(Positive, false); + ReservationEntry.FindFirst(); + ReservationEntry."Source Subtype" := DestinationSalesOrderLine."Document Type".AsInteger(); + ReservationEntry."Source ID" := DestinationSalesOrderLine."Document No."; + ReservationEntry."Source Ref. No." := DestinationSalesOrderLine."Line No."; + ReservationEntry.Validate("Quantity (Base)", -QuantityBaseToTransfer); + ReservationEntry.Modify(true); + end; + + var + PositiveQuantityErr: Label 'The base quantity to transfer must be positive.'; +} diff --git a/microsoft/knowledge/scm/transfer-item-tracking-through-source-reservation-codeunits.good.al b/microsoft/knowledge/scm/transfer-item-tracking-through-source-reservation-codeunits.good.al new file mode 100644 index 00000000..a655b349 --- /dev/null +++ b/microsoft/knowledge/scm/transfer-item-tracking-through-source-reservation-codeunits.good.al @@ -0,0 +1,20 @@ +codeunit 50109 "SCM Tracking Transfer Good" +{ + procedure TransferBlanketOrderTracking(var SourceBlanketOrderLine: Record "Sales Line"; var DestinationSalesOrderLine: Record "Sales Line"; QuantityBaseToTransfer: Decimal) + var + SalesLineReserve: Codeunit "Sales Line-Reserve"; + begin + SourceBlanketOrderLine.TestField("Document Type", SourceBlanketOrderLine."Document Type"::"Blanket Order"); + SourceBlanketOrderLine.TestField(Type, SourceBlanketOrderLine.Type::Item); + DestinationSalesOrderLine.TestField("Document Type", DestinationSalesOrderLine."Document Type"::Order); + DestinationSalesOrderLine.TestField(Type, DestinationSalesOrderLine.Type::Item); + DestinationSalesOrderLine.TestField("No.", SourceBlanketOrderLine."No."); + if QuantityBaseToTransfer <= 0 then + Error(PositiveQuantityErr); + + SalesLineReserve.TransferSaleLineToSalesLine(SourceBlanketOrderLine, DestinationSalesOrderLine, QuantityBaseToTransfer); + end; + + var + PositiveQuantityErr: Label 'The base quantity to transfer must be positive.'; +} diff --git a/microsoft/knowledge/scm/transfer-item-tracking-through-source-reservation-codeunits.md b/microsoft/knowledge/scm/transfer-item-tracking-through-source-reservation-codeunits.md new file mode 100644 index 00000000..8bc42ccd --- /dev/null +++ b/microsoft/knowledge/scm/transfer-item-tracking-through-source-reservation-codeunits.md @@ -0,0 +1,41 @@ +--- +bc-version: [all] +domain: scm +keywords: [reservation-entry, tracking-specification, sales-line-reserve, transfersalelinetosalesline, transferreserventry, copyitemtracking, quantity-base] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Transfer item tracking through source reservation codeunits + +## Description + +Moving lot/serial tracking between document lines is a source-ownership operation, not a copy of visible tracking fields. Partial movement must retain the old source's remainder, destination units of measure, quantities to handle/invoice, status, sign, and any reservation counterpart. Repointing a `"Reservation Entry"` loses this coordination; inserting a `"Tracking Specification"` row alone does not book the destination's source tracking. + +## Best Practice + +Reservation codeunits provide source-specific tracking workflows. For the tracking portion of blanket-sales-order or quote conversion to a sales order, `"Sales Line-Reserve".TransferSaleLineToSalesLine` takes the existing source line, prepared destination line, and quantity to transfer in **base units**. It delegates the source/status and quantity movement to `"Create Reserv. Entry".TransferReservEntry`. + +The caller still owns document conversion and destination-line preparation; this method does not create a sales order. The source and destination must have compatible item, variant, location, and source identity. Purchases, transfers, assembly, and production have their own source-specific wrappers rather than sharing the sales conversion contract. + +`"Item Tracking Management".CopyItemTracking` serves a different purpose: it creates Prospect copies, not a transfer of reservation ownership. That is valid for its intended copy workflow. Temporary Tracking Specification processing and persisted historical tracking specifications are also normal; the working/historic representation differs from the current source booking. + +See sample: [`transfer-item-tracking-through-source-reservation-codeunits.good.al`](transfer-item-tracking-through-source-reservation-codeunits.good.al). + +## Anti Pattern + +Direct rewrites of persistent `"Reservation Entry"` source type/subtype, ID, reference number, or quantities do not perform the source-line conversion or partial tracking-transfer workflow. Changing only `"Quantity (Base)"` and source keys can drop the remainder or leave the other tracking/reservation quantities attached to the wrong source. + +A tracking copy cannot replace movement of an existing binding reservation. Legitimate Prospect copying, temporary tracking buffers, historical tracking reads, and source-specific engine calls serve distinct purposes and are not independent reservation transfers. + +See sample: [`transfer-item-tracking-through-source-reservation-codeunits.bad.al`](transfer-item-tracking-through-source-reservation-codeunits.bad.al). + +## References + +- [Item Tracking Lines window design](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-item-tracking-lines-window) +- [Active versus historic item-tracking entries](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-active-versus-historic-item-tracking-entries) +- [Item tracking and reservations](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-item-tracking-and-reservations) +- [BaseApp sales tracking transfer](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Sales/Document/SalesLineReserve.Codeunit.al#L532-L578) +- [Base-unit conversion caller](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Sales/Document/BlanketSalesOrdertoOrder.Codeunit.al#L195-L198) +- [Prospect-copy API](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Tracking/ItemTrackingManagement.Codeunit.al#L575-L657) diff --git a/microsoft/knowledge/scm/use-date-aware-availability-for-promising.bad.al b/microsoft/knowledge/scm/use-date-aware-availability-for-promising.bad.al new file mode 100644 index 00000000..b90f9321 --- /dev/null +++ b/microsoft/knowledge/scm/use-date-aware-availability-for-promising.bad.al @@ -0,0 +1,21 @@ +codeunit 50114 "SCM Additional Promise Bad" +{ + procedure CanPromiseAdditionalDemand(ItemNo: Code[20]; LocationCode: Code[10]; VariantCode: Code[10]; ShipmentDate: Date; RequestedAdditionalQuantityBase: Decimal; LookaheadDateFormula: DateFormula): Boolean + var + Item: Record Item; + begin + if (ShipmentDate = 0D) or (RequestedAdditionalQuantityBase <= 0) then + Error(DemandInputErr); + Item.Get(ItemNo); + Item.TestField(Type, Item.Type::Inventory); + Item.SetRange("Location Filter", LocationCode); + Item.SetRange("Variant Filter", VariantCode); + Item.SetRange("Date Filter", 0D, ShipmentDate); + + Item.CalcFields(Inventory); + exit(Item.Inventory >= RequestedAdditionalQuantityBase); + end; + + var + DemandInputErr: Label 'Enter a shipment date and a positive additional base quantity.'; +} diff --git a/microsoft/knowledge/scm/use-date-aware-availability-for-promising.good.al b/microsoft/knowledge/scm/use-date-aware-availability-for-promising.good.al new file mode 100644 index 00000000..b33ee79e --- /dev/null +++ b/microsoft/knowledge/scm/use-date-aware-availability-for-promising.good.al @@ -0,0 +1,27 @@ +codeunit 50115 "SCM Additional Promise Good" +{ + procedure CanPromiseAdditionalDemand(ItemNo: Code[20]; LocationCode: Code[10]; VariantCode: Code[10]; ShipmentDate: Date; RequestedAdditionalQuantityBase: Decimal; LookaheadDateFormula: DateFormula): Boolean + var + Item: Record Item; + AvailableToPromise: Codeunit "Available to Promise"; + GrossRequirement: Decimal; + ScheduledReceipt: Decimal; + PromisableQuantityBase: Decimal; + begin + if (ShipmentDate = 0D) or (RequestedAdditionalQuantityBase <= 0) then + Error(DemandInputErr); + Item.Get(ItemNo); + Item.TestField(Type, Item.Type::Inventory); + Item.SetRange("Location Filter", LocationCode); + Item.SetRange("Variant Filter", VariantCode); + Item.SetRange("Date Filter", 0D, ShipmentDate); + + PromisableQuantityBase := AvailableToPromise.CalcQtyAvailableToPromise( + Item, GrossRequirement, ScheduledReceipt, ShipmentDate, + Enum::"Analysis Period Type"::Day, LookaheadDateFormula); + exit(PromisableQuantityBase >= RequestedAdditionalQuantityBase); + end; + + var + DemandInputErr: Label 'Enter a shipment date and a positive additional base quantity.'; +} diff --git a/microsoft/knowledge/scm/use-date-aware-availability-for-promising.md b/microsoft/knowledge/scm/use-date-aware-availability-for-promising.md new file mode 100644 index 00000000..a182a94c --- /dev/null +++ b/microsoft/knowledge/scm/use-date-aware-availability-for-promising.md @@ -0,0 +1,39 @@ +--- +bc-version: [all] +domain: scm +keywords: [available-to-promise, calcqtyavailabletopromise, inventory, shipment-date, gross-requirement, scheduled-receipt, location-filter, variant-filter] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Use date-aware availability for promising + +## Description + +`Item.Inventory` is an on-hand quantity, not an available-to-promise answer. Promising additional demand must account for the requested date, location and variant, reservations, scheduled receipts, existing requirements, and demand within the configured lookahead. An on-hand comparison can promise inventory already committed elsewhere and miss incoming supply. + +## Best Practice + +For additional demand not already recorded on a source line, `"Available to Promise".CalcQtyAvailableToPromise` uses the Item's location/variant filters, date range ending on the shipment date, and configured period/lookahead horizon. Its result and the additional quantity are compared in base units. A fresh calculation context or the codeunit's recalculation support avoids carrying cached quantities between unrelated items or requests. + +The source-aware order-promising/availability workflow accounts for an existing sales line's own quantity or delta; applying an additional-demand calculation to that line can double-count it. Assembly and production requirements/supply likewise participate in the standard availability context, not just a sales-only stock subtraction. + +An ATP result is not a reservation or a guarantee of warehouse pickability. Lot/serial constraints, bins, warehouse activity, and later concurrent changes still need their own checks. Conversely, an on-hand display, valuation report, or deliberately immediate-stock-only check can use `Item.Inventory`: it answers a different business question from ATP. + +See sample: [`use-date-aware-availability-for-promising.good.al`](use-date-aware-availability-for-promising.good.al). + +## Anti Pattern + +`CalcFields(Inventory)` or an equivalent item-ledger quantity sum used as the complete decision for a dated additional-demand promise ignores existing demand and incoming supply, even with location/variant filters. An Inventory FlowField read for an on-hand display has no such promising contract. + +Losing location, variant, date, or source-line context changes the calculation's business meaning. Another supported workflow that preserves the same availability semantics does not have to call this exact API. + +See sample: [`use-date-aware-availability-for-promising.bad.al`](use-date-aware-availability-for-promising.bad.al). + +## References + +- [Calculate order-promising dates](https://learn.microsoft.com/en-us/dynamics365/business-central/sales-how-to-calculate-order-promising-dates) +- [Availability in the warehouse](https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-availability-in-the-warehouse) +- [BaseApp ATP calculation](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Availability/AvailabletoPromise.Codeunit.al#L52-L184) +- [Forward-demand lookahead](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/Inventory/Availability/AvailabletoPromise.Codeunit.al#L295-L356) diff --git a/microsoft/knowledge/style/dateformula-evaluate-needs-language-independent-literals.bad.al b/microsoft/knowledge/style/dateformula-evaluate-needs-language-independent-literals.bad.al new file mode 100644 index 00000000..c520ee00 --- /dev/null +++ b/microsoft/knowledge/style/dateformula-evaluate-needs-language-independent-literals.bad.al @@ -0,0 +1,18 @@ +codeunit 50102 "Bad Review Scheduling" +{ + procedure NextReviewDate(Interval: DateFormula; ReferenceDate: Date): Date + begin + if Format(Interval) = '' then + Evaluate(Interval, '1W'); + + exit(CalcDate(Interval, ReferenceDate)); + end; + + procedure NextReviewFromUserInput(UserFormulaText: Text; ReferenceDate: Date): Date + var + Interval: DateFormula; + begin + Evaluate(Interval, UserFormulaText); + exit(NextReviewDate(Interval, ReferenceDate)); + end; +} diff --git a/microsoft/knowledge/style/dateformula-evaluate-needs-language-independent-literals.good.al b/microsoft/knowledge/style/dateformula-evaluate-needs-language-independent-literals.good.al new file mode 100644 index 00000000..ee07dbe0 --- /dev/null +++ b/microsoft/knowledge/style/dateformula-evaluate-needs-language-independent-literals.good.al @@ -0,0 +1,18 @@ +codeunit 50101 "Good Review Scheduling" +{ + procedure NextReviewDate(Interval: DateFormula; ReferenceDate: Date): Date + begin + if Format(Interval) = '' then + Evaluate(Interval, '<1W>'); + + exit(CalcDate(Interval, ReferenceDate)); + end; + + procedure NextReviewFromUserInput(UserFormulaText: Text; ReferenceDate: Date): Date + var + Interval: DateFormula; + begin + Evaluate(Interval, UserFormulaText); + exit(NextReviewDate(Interval, ReferenceDate)); + end; +} diff --git a/microsoft/knowledge/style/dateformula-evaluate-needs-language-independent-literals.md b/microsoft/knowledge/style/dateformula-evaluate-needs-language-independent-literals.md new file mode 100644 index 00000000..91fcfe51 --- /dev/null +++ b/microsoft/knowledge/style/dateformula-evaluate-needs-language-independent-literals.md @@ -0,0 +1,38 @@ +--- +bc-version: [all] +domain: style +keywords: [dateformula, evaluate, calcdate, date-expression, language-independent, multilanguage, global-language] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Parse DateFormula constants with language-independent input + +## Description + +A `DateFormula` stores a formula in a language-independent representation, but `Evaluate` must first interpret its text input. Declaring the destination as `DateFormula` does not make an English literal such as `1W` independent of the session language: French uses `S` for weeks. Passing the resulting typed variable to `CalcDate` satisfies that call's CodeCop AA0462 argument requirement, but cannot repair a parsing failure that already happened in `Evaluate`. + +## Best Practice + +For an application-defined formula in a normal two-argument `Evaluate` call, use the generic units inside angle brackets, such as `<1W>`. Apply this at the text-to-`DateFormula` boundary, including a visible constant passed through a helper. A label's `Locked = true` prevents translation of its text; it does not make unbracketed English units language independent. + +Preserve genuinely localized input: text entered by the user, or already formatted for the same session language, should be parsed in that language. Do not blindly wrap that text in angle brackets. Already invariant `<...>` literals, explicit import-format conversions, a typed formula passed to `CalcDate`, and `Format(Interval) = ''` checks are not findings without an unsafe constant at the parsing boundary. + +See sample: [`dateformula-evaluate-needs-language-independent-literals.good.al`](dateformula-evaluate-needs-language-independent-literals.good.al). + +## Anti Pattern + +A hard-coded, language-fixed formula such as `1W` flows into a normal two-argument `Evaluate` whose destination is known to be `DateFormula`, and the application expects that default to work across session languages. Require the destination type and constant provenance; an arbitrary `Evaluate` call or dynamic text parameter is not enough. The resulting code can compile and work in English while failing when the same default is first needed in another language. + +Do not report direct `CalcDate` text arguments under this article: CodeCop AA0462 already owns the requirement for a typed formula or angle-bracketed text there. Its typed-argument check does not establish that an earlier `Evaluate` parsed language-independent input. + +See sample: [`dateformula-evaluate-needs-language-independent-literals.bad.al`](dateformula-evaluate-needs-language-independent-literals.bad.al). + +## References + +[DateFormula data type](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods-auto/dateformula/dateformula-data-type) and [CalcDate language behavior](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods-auto/system/system-calcdate-dateformula-date-method). + +[CodeCop AA0462](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/analyzers/codecop-aa0462) defines the separate direct-`CalcDate` check. In a CodeCop compilation probe against BC28.5 symbols, the direct text control produced AA0462; `Evaluate(Interval, '1W')` followed by typed `CalcDate` did not. + +[BaseApp retention scheduling](https://github.com/microsoft/BCApps/blob/8f7a04cb0db8aa96cb97e055c45c61aead49e280/src/Layers/W1/BaseApp/System/RetentionPolicy/RetentionPolicyScheduler.Codeunit.al#L73-L97) initializes a typed formula with an invariant literal. diff --git a/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.bad.al b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.bad.al new file mode 100644 index 00000000..78602b4d --- /dev/null +++ b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.bad.al @@ -0,0 +1,9 @@ +tableextension 50622 "Ship-to Dropdown Bad" extends "Ship-to Address" +{ + fieldgroups + { + addlast(DropDown; "Address 2") + { + } + } +} diff --git a/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.good.al b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.good.al new file mode 100644 index 00000000..c8a8c28c --- /dev/null +++ b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.good.al @@ -0,0 +1,20 @@ +tableextension 50620 "Ship-to Dropdown Good" extends "Ship-to Address" +{ + fieldgroups + { + addlast(DropDown; "Address 2") + { + } + } +} + +pageextension 50621 "Ship-to Lookup Good" extends "Ship-to Address List" +{ + layout + { + modify("Address 2") + { + Visible = true; + } + } +} diff --git a/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.md b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.md new file mode 100644 index 00000000..afa2c34d --- /dev/null +++ b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.md @@ -0,0 +1,30 @@ +--- +bc-version: [all] +domain: ui +keywords: [fieldgroup, dropdown, addlast, lookup-page, visible, tableextension, pageextension] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# A `DropDown` field remains hidden when its lookup-page control is hidden + +## Description + +A tableextension can append a field to the `DropDown` field group with `addlast`, but the client still omits that field when its control on the underlying lookup page has `Visible = false`. Changing only the table field group therefore compiles while producing no visible UI change. The field-group name is case-sensitive and must be written as `DropDown`. + +## Best Practice + +When adding a hidden field to a `DropDown` field group, also extend the page used for the lookup and make that field control visible. Verify the actual lookup page rather than assuming the table definition alone controls the drop-down. + +See sample: [`dropdown-fieldgroup-respects-lookup-page-visibility.good.al`](dropdown-fieldgroup-respects-lookup-page-visibility.good.al). + +## Anti Pattern + +Adding the field with `addlast(DropDown; ...)` while leaving its lookup-page control hidden, then expecting the field to appear in the drop-down. + +See sample: [`dropdown-fieldgroup-respects-lookup-page-visibility.bad.al`](dropdown-fieldgroup-respects-lookup-page-visibility.bad.al). + +## Reference + +[Add a new FieldGroup to an existing table](https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/add-field-group) diff --git a/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.bad.al b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.bad.al new file mode 100644 index 00000000..f5d2b2be --- /dev/null +++ b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.bad.al @@ -0,0 +1,26 @@ +page 50631 "Sample Order Bad" +{ + PageType = Document; + SourceTable = "Sales Header"; + + layout + { + area(Content) + { + group(General) + { + field(Amount; Rec.Amount) + { + ApplicationArea = All; + ToolTip = 'Specifies the total amount of the order.'; + } + } + part(Lines; "Sales Order Subform") + { + ApplicationArea = All; + SubPageLink = "Document Type" = field("Document Type"), + "Document No." = field("No."); + } + } + } +} diff --git a/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.good.al b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.good.al new file mode 100644 index 00000000..34620981 --- /dev/null +++ b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.good.al @@ -0,0 +1,27 @@ +page 50630 "Sample Order Good" +{ + PageType = Document; + SourceTable = "Sales Header"; + + layout + { + area(Content) + { + group(General) + { + field(Amount; Rec.Amount) + { + ApplicationArea = All; + ToolTip = 'Specifies the total amount of the order.'; + } + } + part(Lines; "Sales Order Subform") + { + ApplicationArea = All; + SubPageLink = "Document Type" = field("Document Type"), + "Document No." = field("No."); + UpdatePropagation = Both; + } + } + } +} diff --git a/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md new file mode 100644 index 00000000..3ce676b5 --- /dev/null +++ b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md @@ -0,0 +1,30 @@ +--- +bc-version: [all] +domain: ui +keywords: [updatepropagation, page-part, subpage, main-page, refresh, flowfield, document-lines] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Use `UpdatePropagation = Both` when line edits must refresh the main page + +## Description + +A page part does not automatically refresh its parent page when the subpage changes. `UpdatePropagation = Subpage` updates only the part; `Both` also refreshes the main page. Without `Both`, header totals, FlowFields, and FactBoxes that depend on edited lines can remain stale until another user action refreshes the page. + +## Best Practice + +Set `UpdatePropagation = Both` on a part when edits in that subpage must immediately update values rendered by the main page. Leave propagation at `Subpage` when the parent has no dependent presentation to avoid unnecessary refreshes. + +See sample: [`updatepropagation-both-refreshes-main-page.good.al`](updatepropagation-both-refreshes-main-page.good.al). + +## Anti Pattern + +Displaying a line-dependent total on the main page while the editable lines part updates only itself. The persisted values can be correct while the parent page continues to show an old total. + +See sample: [`updatepropagation-both-refreshes-main-page.bad.al`](updatepropagation-both-refreshes-main-page.bad.al). + +## Reference + +[Set different control properties](https://learn.microsoft.com/en-us/training/modules/work-with-pages/8-controls) diff --git a/microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md b/microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md new file mode 100644 index 00000000..61e74226 --- /dev/null +++ b/microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md @@ -0,0 +1,26 @@ +--- +bc-version: [all] +domain: upgrade +keywords: [appversion, dataversion, moduleinfo, install-codeunit, upgrade-codeunit, version-context] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# `ModuleInfo.AppVersion` changes meaning with execution context + +## Description + +`ModuleInfo.AppVersion()` is the installed version during normal operation, the version being installed inside install code, and the target version inside upgrade code. It is therefore not the source data version during an upgrade. In upgrade code, `DataVersion()` describes the version of the existing data, whether from the currently installed app or the version most recently uninstalled. + +## Best Practice + +Interpret `AppVersion()` as the code package entering the context and `DataVersion()` as the existing data state. Prefer upgrade tags for controlling individual migration steps; when version information is needed for diagnostics or preconditions, name variables so target app version and source data version cannot be confused. + +## Anti Pattern + +Reading `AppVersion()` from an upgrade codeunit and treating it as the version being upgraded from. The comparison actually observes the target package and can skip or misroute migration logic. + +## Reference + +[Create proper installation and upgrade codeunits](https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/3-installation-upgrade-codeunits) diff --git a/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.bad.al b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.bad.al new file mode 100644 index 00000000..2a92d112 --- /dev/null +++ b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.bad.al @@ -0,0 +1,28 @@ +codeunit 50641 "Sample Upgrade Part One" +{ + Subtype = Upgrade; + + trigger OnUpgradePerCompany() + begin + CreateUpgradeState(); + end; + + local procedure CreateUpgradeState() + begin + end; +} + +codeunit 50642 "Sample Upgrade Part Two" +{ + Subtype = Upgrade; + + trigger OnUpgradePerCompany() + begin + // This can run before Part One; object IDs do not sequence upgrade codeunits. + MigrateDataThatRequiresUpgradeState(); + end; + + local procedure MigrateDataThatRequiresUpgradeState() + begin + end; +} diff --git a/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.good.al b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.good.al new file mode 100644 index 00000000..42346a88 --- /dev/null +++ b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.good.al @@ -0,0 +1,18 @@ +codeunit 50640 "Sample Upgrade Good" +{ + Subtype = Upgrade; + + trigger OnUpgradePerCompany() + begin + CreateUpgradeState(); + MigrateDependentData(); + end; + + local procedure CreateUpgradeState() + begin + end; + + local procedure MigrateDependentData() + begin + end; +} diff --git a/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md new file mode 100644 index 00000000..b11a8425 --- /dev/null +++ b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md @@ -0,0 +1,30 @@ +--- +bc-version: [all] +domain: upgrade +keywords: [install-codeunit, upgrade-codeunit, execution-order, subtype-install, subtype-upgrade, sequencing] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Separate install or upgrade codeunits have no execution order + +## Description + +An extension can contain multiple `Install` or `Upgrade` codeunits, but Business Central does not guarantee the order in which codeunits of the same subtype execute. Upgrade trigger phases are ordered globally, yet one codeunit's `OnUpgradePerCompany` must not assume another codeunit's same-phase trigger already ran. Object ID and source-file order do not provide sequencing. + +## Best Practice + +Keep separate install or upgrade codeunits independent. When two steps have a real dependency, coordinate them from one owning trigger in the required order; use upgrade tags to make each completed step idempotent. + +See sample: [`install-and-upgrade-codeunits-have-no-order.good.al`](install-and-upgrade-codeunits-have-no-order.good.al). + +## Anti Pattern + +Splitting dependent steps into separate codeunits and relying on names, object IDs, or declaration order. The dependent codeunit can run first and fail or observe partially migrated data. + +See sample: [`install-and-upgrade-codeunits-have-no-order.bad.al`](install-and-upgrade-codeunits-have-no-order.bad.al). + +## Reference + +[Create proper installation and upgrade codeunits](https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/3-installation-upgrade-codeunits) diff --git a/microsoft/skills/review/al-code-review.md b/microsoft/skills/review/al-code-review.md index dc1f1db1..df476f3d 100644 --- a/microsoft/skills/review/al-code-review.md +++ b/microsoft/skills/review/al-code-review.md @@ -28,6 +28,8 @@ sub-skills: - microsoft/skills/review/al-reporting-review.md - microsoft/skills/review/al-appsource-review.md - microsoft/skills/review/al-telemetry-review.md + - microsoft/skills/review/al-scm-review.md + - microsoft/skills/review/al-finance-review.md --- # AL code review diff --git a/microsoft/skills/review/al-data-modeling-review.md b/microsoft/skills/review/al-data-modeling-review.md index 05dcd0ba..3fca8ecb 100644 --- a/microsoft/skills/review/al-data-modeling-review.md +++ b/microsoft/skills/review/al-data-modeling-review.md @@ -39,7 +39,7 @@ Narrow the relevant files to the subset that applies to the changes under review - The changed AL object names and types β€” especially `* Setup` singleton tables and Card pages, custom master tables, tableextensions that add master-data fields, and document or journal lines that reference a master. - The changed fields, keys, triggers, and procedures, weighted toward `Primary Key`, `No.`, `No. Series`, `Blocked`, `Last Date Modified`, `OnInsert`, `OnModify`, `OnRename`, reference-field `OnValidate`, and posting validation. -- Tokens extracted from the diff that relate to data modeling (`setup`, `master`, `Primary Key`, `Code[10]`, `Code[20]`, `AutoIncrement`, `SystemId`, `No.`, `No. Series`, `NoSeriesManagement`, `Codeunit "No. Series"`, `GetNextNo`, `IsManual`, `TestManual`, `Blocked`, `TestField`, `Last Date Modified`, `Today`, `WorkDate`, `InsertAllowed`, `DeleteAllowed`, `PageType = Card`, `OnOpenPage`, `GetRecordOnce`, `OnInsert`, `OnModify`, `OnRename`, `TableRelation`, `tableextension`, `enumextension`, `Media`, `MediaSet`, `Item`, `Count`). +- Tokens extracted from the diff that relate to data modeling (`setup`, `master`, `Primary Key`, `Code[10]`, `Code[20]`, `AutoIncrement`, `SystemId`, `No.`, `No. Series`, `NoSeriesManagement`, `Codeunit "No. Series"`, `GetNextNo`, `IsManual`, `TestManual`, `Blocked`, `TestField`, `Last Date Modified`, `Today`, `WorkDate`, `InsertAllowed`, `DeleteAllowed`, `PageType = Card`, `OnOpenPage`, `GetRecordOnce`, `OnInsert`, `OnModify`, `OnRename`, `InitRecord`, `Round`, `Precision`, `Direction`, `TableRelation`, `tableextension`, `enumextension`, `Media`, `MediaSet`, `Item`, `Count`). A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file β€” its `## Best Practice` / `## Anti Pattern` bodies β€” only after it makes the worklist; candidate selection uses the index alone. When the diff contains no data-modeling changes by any of the above signals, return `outcome: "not-applicable"` without evaluating files. @@ -52,6 +52,8 @@ The following targeted checks cover every current `data-modeling` article. Treat - A master table adds or changes `Last Date Modified`, `OnModify`, or `OnRename`, but the non-editable field is not assigned `Today()` in both triggers β€” `set-last-date-modified-in-onmodify-and-onrename`. - A `tableextension` appends a conditional `TableRelation` as if it overrides an earlier unconditional relation, or relation branches are otherwise designed without accounting for additive top-down evaluation β€” `table-relation-extensions-are-additive-and-top-down`. - A `Media` or `MediaSet` field is assigned directly between different table types or different field IDs instead of registering each shared item with `MediaSet.Insert` β€” `share-mediaset-items-with-insert-not-field-assignment`. +- A custom document header assigns defaults outside an `InitRecord` boundary, calls `InitRecord` before assigning its number, or places UI-independent defaults only in a page trigger β€” `initialize-document-defaults-in-initrecord`. +- Directed `Round` calls use `'<'` as mathematical floor or `'>'` as mathematical ceiling, especially where negative amounts are possible β€” `round-direction-symbols-use-magnitude`. Once the candidate worklist is known, resolve layer-precedence conflicts per READ. Drop lower-precedence files whose normative guidance (`## Best Practice` or `## Anti Pattern`) directly contradicts a higher-precedence candidate, and record each dropped file in `suppressed` with `reason: "layer-precedence"`. Files that would have been candidates but are hidden because their layer is disabled in consumer configuration are recorded with `reason: "configuration"`. Files that never became candidates are NOT recorded in `suppressed`. diff --git a/microsoft/skills/review/al-finance-review.md b/microsoft/skills/review/al-finance-review.md new file mode 100644 index 00000000..06090b12 --- /dev/null +++ b/microsoft/skills/review/al-finance-review.md @@ -0,0 +1,146 @@ +--- +kind: action-skill +id: al-finance-review +version: 1 +title: AL Finance review +description: Reviews financial journal posting, ledger corrections, applications, VAT handling, and posting-linked dimensions against BCQuality Finance guidance. +inputs: [pr-diff, file-path, folder-path] +outputs: [findings-report] +bc-version: [all] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# AL Finance review + +Reviews the `finance` knowledge domain. This is a leaf action skill composed by +`al-code-review`; it invokes no other skills. Application-area metadata does +not gate Finance coverage; resolved source records and operations do. + +## Source + +Apply the source-surface gate in Relevance before retrieving knowledge. +If it passes, use READ's **Bounded retrieval for review skills** workflow with +`-Domain finance`. Consume every catalog page across enabled layers, preserving +each exact path and applicability metadata. Do not select a top-k catalog or +deduplicate by basename. Open complete bodies only for exact Worklist paths, +in stable chunks of at most eight, consuming every continuation. If the helper +or prepared index is unavailable or invalid, use READ's explicit path-discovery +and bounded native-read fallback; a retrieval error is not an empty corpus. + +## Relevance + +Apply READ's frontmatter matching rules using only known task dimensions. +Use the target application version from `app.json` when available; do not invent +a country or application area from a filename or UI `ApplicationArea` token. +Retain conditional articles only when configured, cap resulting confidence at +`medium`, and name every unknown dimension in the finding. + +Inspect the supplied AL scope and its enclosing declarations. Admit only +changed executable behavior involving at least one of these surfaces: + +- General-journal construction or posting, journal-batch processing, or an + event subscriber whose resolved publisher is in the financial posting path. +- Writes or correction/application/reversal calls involving `G/L Entry`, + `Cust. Ledger Entry`, `Vendor Ledger Entry`, `Detailed Cust. Ledg. Entry`, + `Detailed Vendor Ledg. Entry`, `VAT Entry`, or financial-posting/G/L-register + records. +- Dimension transfer or dimension-set mutation connected by visible data flow + to an existing general journal, financial posting document, or Finance-owned + ledger record. + +Exclude `Item Ledger Entry`, `Value Entry`, Capacity/Warehouse entries, +`Item Application Entry`, and other inventory-posting records owned by SCM. +Do not adopt their findings when the SCM skill is absent or disabled. For one +inventory-originated posting bypass, equivalent findings have one SCM primary +owner; distinct independent financial defects remain Finance. Classify the +operation and actual record, not an inventory/finance word in a module name. + +Return `not-applicable` when none is present. Imports, object names, comments, +read-only ledger displays, generic `Amount`/`Date`/`Open` fields, and calls to +`DimensionManagement` without posting-linked context do not establish relevance. +For a diff, retain surrounding variable types, field provenance, event +attributes, and reachable helpers; do not review isolated added lines without +the context needed to classify their record or call. + +## Worklist + +Match the complete relevant catalog's keywords, titles, and descriptions to +the admitted source surfaces. Add an exact catalog path only when its concern +maps to the changed behavior; generic financial vocabulary is not enough. +The following deterministic cues must select their named articles even if +keyword ranking would otherwise omit them: + +- Persistent Finance-owned ledger inserts reached from extension posting + code β€” `post-ledger-entries-through-posting-codeunits`. +- Persisted general-journal lines posted through a line-codeunit loop or custom + aggregate check, with visible template/document/date balancing context β€” + `preserve-journal-batch-document-balance`. +- Imported net/tax/gross values mapped into `Gen. Journal Line.Amount`, with + evidence of the VAT posting mode and posting-setup combination β€” + `normal-vat-journal-amount-includes-vat`. +- Persisted original Finance accounting-value changes, deletion of Finance rows, or + fabricated reversal flags/links β€” `do-not-modify-or-delete-posted-ledger-entries`. +- Customer/vendor settlement/reopening code writing `Open`, closure fields, + detailed customer/vendor application amounts, or unapplication flags β€” + `apply-ledger-entries-through-application-codeunits`. +- A due-date change persisted on an existing customer/vendor ledger entry β€” + `change-ledger-due-dates-through-entry-edit`. +- A `Reversal Entry.ReverseTransaction` or `ReverseRegister` argument with + visible ledger-entry, transaction, or register provenance β€” + `reverse-transactions-by-transaction-number`. +- A complete Finance posting-dimension transfer represented by shortcut/global + fields or a `Dimension Set ID` assignment β€” + `write-dimensions-as-dimension-set-entries`. +- A dimension/value membership change on `Dimension Set Entry`, reached from + a general-journal/financial-document/Finance-ledger set ID β€” + `do-not-edit-shared-dimension-sets`. + +These are retrieval cues, not findings. Use the selected articles' normative +exceptions and ownership boundaries to classify standard workflows, temporary +records, operational edits, and extension fields. Do not select a Finance +ledger rule from `*Ledger Entry` or `Insert`/`Modify` alone. Finance does not +own SCM records, generic custom-table/master dimension wiring, number-series +API migration, or general AL validation, locking, transaction, and event-style +advice. Do not add those concerns as Finance agent findings. + +Resolve actual normative conflicts across layers per READ and record suppressed +candidates per DO. Keep every remaining exact path in a stable worklist. +Return `no-knowledge` if no applicable Finance knowledge survives filtering or +configuration; return `completed` with no findings when applicable knowledge +exists but no article matches the admitted changes. + +## Action + +Evaluate every worklist article in full against the changed behavior and its +surrounding control flow. Establish record type, existing versus newly prepared +state, temporariness, fields actually persisted, argument provenance, and the +posting/edit API boundary before emitting a finding. Do not infer a financial +defect from a method name, missing external setup, or unsupported speculation +about callers. + +Use the most specific article for the correction: application-state, due-date, +and shared-dimension findings must not also become generic posted-row findings +for the same change. Do not emit an equivalent Finance finding for the +financial-row leg of one SCM-owned inventory posting bypass; evaluate a +distinct financial defect only when its corrective action is independent. +Emit `major` for a demonstrated financial-correctness +violation and reserve `blocker` for directly evidenced destructive corruption +under DO's severity rules. Applicability alone produces no finding. + +Set `high` confidence only for established source evidence and known matching +context. Domain-scoped agent findings follow DO's precision bar and remain +capped at `minor`/`medium`; do not broaden this pass into other AL domains. +Provide literal `suggested-code` for complete, local, unambiguous fixes. +Otherwise give `suggested-code-omission-reason`, particularly when selecting +the correct posting workflow requires business context. + +Follow DO's acceptance gate and outcome rules. Report `partial` rather than +silently dropping worklist items when a budget is reached, and `failed` for an +unrecoverable retrieval or evaluation error. + +## Output + +Output conforms to the DO findings-report contract. Every finding this skill +emits MUST set `findings[].domain` to `"Finance"`. diff --git a/microsoft/skills/review/al-interfaces-review.md b/microsoft/skills/review/al-interfaces-review.md index f5d65cd0..1c38be76 100644 --- a/microsoft/skills/review/al-interfaces-review.md +++ b/microsoft/skills/review/al-interfaces-review.md @@ -39,7 +39,7 @@ Narrow the relevant files to the subset that applies to the changes under review - The changed AL object names and types β€” especially `interface` objects, codeunits and enums declared with the `implements` keyword, and consumers that declare or assign an `Interface` variable. - The changed procedures and triggers, weighted toward factory or dispatch routines that resolve a variant to behaviour, setter-injection procedures that take an `Interface` parameter, and `case`-over-enum blocks that select between strategies. -- Tokens extracted from the diff that relate to interfaces and enum-backed implementation (`interface`, `extends`, `implements`, `Implementation`, `DefaultImplementation`, `UnknownValueImplementation`, `enum`, `Extensible`, `Interface`, `case`, and the `case of` anti-pattern signal β€” a `case` over an enum value whose branches choose between variant computations). +- Tokens extracted from the diff that relate to interfaces and enum-backed implementation (`interface`, `extends`, `implements`, `Implementation`, `DefaultImplementation`, `UnknownValueImplementation`, `enum`, `Extensible`, `Interface`, `Variant`, `is`, `as`, `case`, and the `case of` anti-pattern signal β€” a `case` over an enum value whose branches choose between variant computations). A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file β€” its `## Best Practice` / `## Anti Pattern` bodies β€” only after it makes the worklist; candidate selection uses the index alone. @@ -54,6 +54,7 @@ The following targeted checks map diff signals to specific `interfaces` articles - `DefaultImplementation` used as the only fallback where a persisted ordinal may no longer match any declared enum value, or a persisted enum lacks `UnknownValueImplementation` on BC18 or later β€” `handle-unknown-enum-ordinals-with-unknownvalueimplementation`. - A method added directly to an interface that exists in the baseline, instead of adding a BC25+ interface that `extends` it or a versioned sibling for older targets β€” `extend-published-interfaces-dont-edit-them`. - A declared enum value with no `Implementation` and no enum-level `DefaultImplementation` β€” `set-defaultimplementation-on-enum`. +- An `Interface` or `Variant` is cast with `as` to an optional extended interface without first establishing support with `is` β€” `guard-interface-casts-with-is`. For `set-defaultimplementation-on-enum`, inspect the complete containing enum before emitting. An enum-level `DefaultImplementation = = ;` conclusively covers every declared value that omits its own `Implementation`; do not flag such a value and do not replace the intentional fallback with a per-value mapping. diff --git a/microsoft/skills/review/al-scm-review.md b/microsoft/skills/review/al-scm-review.md new file mode 100644 index 00000000..a4345292 --- /dev/null +++ b/microsoft/skills/review/al-scm-review.md @@ -0,0 +1,121 @@ +--- +kind: action-skill +id: al-scm-review +version: 1 +title: AL Supply Chain Management review +description: Reviews SCM inventory costing, item application, reservations, order tracking, item tracking, warehouse, transfer, and planning workflows in AL. +inputs: [pr-diff, file-path, folder-path] +outputs: [findings-report] +bc-version: [all] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# AL Supply Chain Management review + +Reviews AL source against the `scm` knowledge domain. This leaf invokes no +sub-skills and is composed by `al-code-review`. For a folder, inspect every +relevant AL file; a folder supplies no historical baseline. + +## Source + +Apply Relevance's source gate before retrieval. For a relevant scope, use READ's +**Bounded retrieval for review skills** with `-Domain scm` and +`-Technologies @('al')`. Consume every catalog page across enabled layers, +preserving exact paths and applicability. Select from metadata, then read only +worklisted complete articles. Entry owns index preparation; the leaf does not +rebuild. Unavailable/invalid helpers or indexes use READ's bounded native-read +fallback, never a success-shaped empty result. + +## Relevance + +Resolve changed record/codeunit types, source tables, publishers and calls. +Gate on code that mutates or posts inventory, application, reservation, +tracking, warehouse, transfer or planning state, or makes a supply/demand +availability decision. Stock displays and other read-only queries without +that decision do not pass the gate. Names, comments, captions, an `Item` +reference or a broad `ApplicationArea` alone are not signals. +If no SCM surface remains, return `not-applicable` with zero coverage +and no article-body retrieval; in mixed diffs, retain only relevant procedures +and their visible supporting context. + +SCM owns `"Item Ledger Entry"`, `"Value Entry"`, `"Capacity Ledger Entry"`, +`"Warehouse Entry"` and inventory posting/application records. Pure `"G/L Entry"`, +`"Cust. Ledger Entry"`, `"Vendor Ledger Entry"`, `"Detailed Cust. Ledg. Entry"`, +`"Detailed Vendor Ledg. Entry"`, `"VAT Entry"` and financial-only posting +mutations belong to Finance. They remain outside SCM even if Finance is absent +or disabled; do not reclaim them as SCM agent findings. Ownership is not a +claim that every owned surface already has a dedicated article. + +Apply READ's frontmatter filters using the target BC major version from +application dependency/host context (not the extension version), AL, known +localization and actual task/object application areas. Omitted context stays +unknown, not `[all]`; unknown areas alone do not exclude codeunits/subscribers. +Retain conditional articles only when configured, cap their findings at +`medium`, and name every unknown dimension. + +## Worklist + +Extract resolved object/type names, quoted fields, methods, enum members and +publishers. Normalize these and catalog keywords by lowercasing invariantly, +replacing punctuation/whitespace runs with one hyphen and trimming hyphens: +`"Item Ledger Entry"` becomes `item-ledger-entry`; `RunWithCheck` becomes +`runwithcheck`. Match whole tokens/phrases, not identifier substrings. + +Select matching keywords or catalog topics only for the same source surface +**and operation**. The following cues resolve slugs to actual enabled catalog +paths; they select articles, not findings. Facts and exceptions stay in articles. + +| Changed source surface and operation | Article slug | +| --- | --- | +| `"Item Ledger Entry"`/`"Value Entry"` transaction writes, or a standalone item-journal quantity/value posting entry point | `post-item-ledger-changes-through-item-journals` | +| Revaluation `"Item Journal Line"` with `"Inventory Value Per"` or `"Partial Revaluation"`, and its line/batch posting calls | `post-revaluation-through-the-item-journal-batch` | +| `"Item Application Entry"` relationship/quantity mutation, or `UnApply`, `ReApply`, `RedoApplications`, `CostAdjust` in an application-correction flow | `change-item-applications-through-posting-routines` | +| Binding-reservation cancellation: `"Reservation Entry"` status, delete/quantity/source edits, `CancelReservation`, or source reservation-lifecycle calls | `cancel-reservations-through-reservation-management` | +| Tracking source conversion/partial movement: `"Sales Line-Reserve"`, `TransferSaleLineToSalesLine`, `TransferReservEntry`, `CopyItemTracking`, or `"Reservation Entry"`/`"Tracking Specification"` source/quantity writes | `transfer-item-tracking-through-source-reservation-codeunits` | +| Registered warehouse quantity/physical-adjustment synchronization, `"Directed Put-away and Pick"`, `"Adjustment Bin Code"`, `"Warehouse Adjustment"`, or `"Calculate Whse. Adjustment"` and the resulting item-journal posting | `reconcile-warehouse-adjustments-with-the-item-ledger` | +| `"Transfer Header"`/`"Transfer Line"` shipment/receipt completion, transfer posting publishers, in-transit/document-link changes, or item-journal posting presented as transfer-order completion | `post-transfers-through-shipment-and-receipt-codeunits` | +| `Inventory`, `CalcQtyAvailableToPromise`, or stock sums used in a dated supply/demand promise, including changed location/variant/date filters and source-demand context | `use-date-aware-availability-for-promising` | +| `"Requisition Line"` action-message execution, accepted planning suggestions, `"Req. Wksh.-Make Order"`, `CarryOutBatchAction`, or linked supply creation/change plus requisition-line deletion | `carry-out-requisition-actions-through-the-standard-workflow` | + +Route clean supported calls through the same cues, not just suspicious writes. +Resolve actual normative conflicts per READ, preserving additive layers and +recording `layer-precedence`/`configuration` suppressions, not noncandidates. +Retrieve exact paths in ordinal chunks of at most eight, consume every +continuation, and never impose a top-eight cutoff. Samples use exact READ links. + +## Action + +Evaluate every opened article's normative facts, scope and exclusions against +visible persistence, caller contract, document state and operation. Emit only +concrete violations with business consequences and supported remediation; a +declaration, valid alternative or unseen caller is not evidence of a defect. + +- Use `major` for material SCM defects, `minor` for narrower best-practice + conflicts, and `blocker` only for an article-established platform guarantee. + Applicability alone produces no finding. High confidence requires unambiguous + evidence and known applicability; inference/conditional applicability caps it + at `medium`. +- Apply DO's single-owner deduplication. Equivalent findings for the same + inventory-originated posting bypass and correction have one SCM primary + owner, even when financial records are downstream. Prefer the most specific + SCM article and retain other applicable references as supporting evidence. + Distinct independent financial defects remain Finance; do not duplicate them. +- Agent findings stay strictly SCM-scoped under DO's precision bar, with + `references: []`, an `agent:` id and `minor`/`medium` ceilings. Generic AL and + other domains' concerns remain outside this leaf. +- Supply literal `suggested-code` only for a complete, local, unambiguous fix, + not a sample call that omits workflow setup/source identity. Explain omitted + mechanical-looking fixes with `suggested-code-omission-reason`. + +Outcome selection follows DO, including accurate coverage and reasons for +`partial`/`failed`. No surviving applicable corpus is `no-knowledge`; an existing +corpus with no matching operation is `completed` with an empty worklist. + +## Output + +Output conforms to the DO findings-report contract and shared schema. Every +finding MUST set `domain` to `"Supply Chain Management"`. Knowledge-backed ids +equal the primary opened article's exact catalog path. The coordinator, not +this leaf, sets `from-sub-skill`. diff --git a/microsoft/skills/review/al-style-review.md b/microsoft/skills/review/al-style-review.md index 2703c879..5b0b6b6c 100644 --- a/microsoft/skills/review/al-style-review.md +++ b/microsoft/skills/review/al-style-review.md @@ -3,7 +3,7 @@ kind: action-skill id: al-style-review version: 1 title: AL style review -description: Reviews AL source changes against naming, labelling, and code-convention guidance from BCQuality. +description: Reviews AL source changes against naming, labelling, localization, and code-convention guidance from BCQuality. inputs: [pr-diff, file-path, folder-path] outputs: [findings-report] bc-version: [all] @@ -16,7 +16,7 @@ application-area: [all] Reviews AL source changes against the `style` knowledge domain in BCQuality and emits a findings report. This is a leaf action skill: it invokes no sub-skills. It is one of the skills composed by `al-code-review`. -Style findings cover AL conventions that require contextual judgment β€” API page naming, temporary-variable prefixes, label semantics, named invocations, `FieldCaption`/`TableCaption` in user messages, error-parameter handling, and file naming. Mechanical compiler and analyzer rules are intentionally outside this skill; run the consuming app's configured analyzers separately. +Style findings cover AL conventions that require contextual judgment β€” API page naming, temporary-variable prefixes, label semantics, date-formula localization, named invocations, `FieldCaption`/`TableCaption` in user messages, error-parameter handling, and file naming. Mechanical compiler and analyzer rules are intentionally outside this skill; run the consuming app's configured analyzers separately. An orchestrator invokes this skill with a `pr-diff`, `file-path`, or `folder-path`. The skill produces a single JSON document conforming to the DO output contract. @@ -40,8 +40,8 @@ Discard files that are not applicable. Retain conditionally applicable files onl Narrow the relevant files to the subset that applies to the changes under review. For each relevant file, compute overlap against: - Changed AL objects β€” especially API pages (`PageType = API`), tables and pages declaring Labels/TextConsts, codeunits issuing `Error`/`Message`/`Confirm`, and any file whose name violates the `..al` convention. -- Changed declarations, weighted toward `: Label '...'`, `: TextConst '...'`, temporary record variables, error-handling call sites, and API declarations. -- Tokens extracted from the diff (`Label`, `TextConst`, `Locked`, `Comment`, `MaxLength`, `temporary`, `APIPublisher`, `APIGroup`, `APIVersion`, `EntityName`, `EntitySetName`, `DelayedInsert`, `FieldCaption`, `TableCaption`, `FieldName`, `TableName`, `Page.RunModal`, `Report.Run`, `StrSubstNo`). +- Changed declarations, weighted toward `: Label '...'`, `: TextConst '...'`, temporary record variables, `DateFormula` declarations and their `Evaluate` call sites, error-handling call sites, and API declarations. +- Tokens extracted from the diff (`Label`, `TextConst`, `Locked`, `Comment`, `MaxLength`, `temporary`, `DateFormula`, `Evaluate`, `CalcDate`, `APIPublisher`, `APIGroup`, `APIVersion`, `EntityName`, `EntitySetName`, `DelayedInsert`, `FieldCaption`, `TableCaption`, `FieldName`, `TableName`, `Page.RunModal`, `Report.Run`, `StrSubstNo`). A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object or declaration. Read an article's full file β€” its `## Best Practice` / `## Anti Pattern` bodies β€” only after it makes the worklist; candidate selection uses the index alone. @@ -50,6 +50,8 @@ Do not worklist `temporary-variable-temp-prefix.md` for an event publisher param Apply these high-signal mappings before fuzzy topic ranking: - A `Label` or `TextConst` contains multiple or ambiguous placeholders but has no `Comment`, or its Comment does not explain every placeholder β€” `label-comment-explains-placeholders.md`. A single placeholder whose meaning is explicit in the text, such as `Customer %1`, is allowed without a Comment and must not be flagged. +- A normal two-argument `Evaluate` has a resolved `DateFormula` destination and a hard-coded non-angle-bracket date-formula literal, directly or through a visible constant β€” `dateformula-evaluate-needs-language-independent-literals.md`. Do not use this cue for dynamic/localized external input, already invariant `<...>` input, or direct `CalcDate(Text, ...)` calls. + Once the candidate worklist is known, resolve layer-precedence conflicts per READ and record suppressions. When the post-conflict worklist is empty because no applicable style knowledge exists, or because configuration suppressed every candidate, emit `outcome: "no-knowledge"`. When the worklist is empty because no applicable style knowledge matched the changes, emit `outcome: "completed"` with an empty `findings` array. diff --git a/microsoft/skills/review/al-ui-review.md b/microsoft/skills/review/al-ui-review.md index 8ffd7319..8c35f498 100644 --- a/microsoft/skills/review/al-ui-review.md +++ b/microsoft/skills/review/al-ui-review.md @@ -41,10 +41,15 @@ Narrow the relevant files to the subset that applies to the changes under review - **UI-file filter.** UI review applies to files declaring `page`, `pageextension`, or `pagecustomization`, and to JavaScript/CSS/HTML that implements a control add-in's rendering or Business Central communication. When the diff contains no such files, return `outcome: "not-applicable"` without evaluating knowledge files. - For each relevant knowledge file, compute overlap against changed page declarations and control add-in files, weighted toward `Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `OptionCaption`, `ShowCaption`, `InstructionalText`, `GridLayout`, `Style`, `StyleExpr`, promoted action definitions, field importance, page background tasks, DOM creation, ARIA attributes, keyboard/focus handlers, packaged-resource AJAX, and calls from JavaScript into AL. -- Tokens extracted from the diff (`Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `PageType`, `ShowCaption`, `InstructionalText`, `grid`, `fixed`, `GridLayout`, `Style`, `StyleExpr`, `Importance`, `Promoted`, `Additional`, `area(Promoted)`, `actionref`, `PromotedCategory`, `PromotedOnly`, `PromotedIsBig`, `ShowAs`, `SplitButton`, `EnqueueBackgroundTask`, `OnAfterGetCurrRecord`, `OnAfterGetRecord`, `OnPageBackgroundTaskCompleted`, `OnPageBackgroundTaskError`, `RunPageBackgroundTask`, `Favorable`, `Unfavorable`, `Ambiguous`, `cuegroup`, `controladdin`, `control-add-in`, `usercontrol`, `aria-`, `tabindex`, `keydown`, `focus`, `innerHTML`, `createElement`, `packaged-resource`, `ajax`, `$.get`, `$.ajax`, `XMLHttpRequest`, `xhrFields`, `withCredentials`, `withcredentials`, `InvokeExtensibilityMethod`, `invokeextensibilitymethod`, `skipIfBusy`, `successCallback`, `success-callback`, `errorCallback`, `setInterval`, `JSON.stringify`, `payload`, `throttling`, `reduced-functionality`, `ClientServicesMaxUploadSize`, `&`, `Specifies`, `Message(`, `Confirm(`, `Error(` in a page context, `Disabled`, `Invalid`, `Whitelist`, `Blacklist`, trailing punctuation patterns on captions). +- Tokens extracted from the diff (`Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `PageType`, `ShowCaption`, `InstructionalText`, `grid`, `fixed`, `GridLayout`, `Style`, `StyleExpr`, `Importance`, `Promoted`, `Additional`, `area(Promoted)`, `actionref`, `PromotedCategory`, `PromotedOnly`, `PromotedIsBig`, `ShowAs`, `SplitButton`, `fieldgroups`, `DropDown`, `UpdatePropagation`, `EnqueueBackgroundTask`, `OnAfterGetCurrRecord`, `OnAfterGetRecord`, `OnPageBackgroundTaskCompleted`, `OnPageBackgroundTaskError`, `RunPageBackgroundTask`, `Favorable`, `Unfavorable`, `Ambiguous`, `cuegroup`, `controladdin`, `control-add-in`, `usercontrol`, `aria-`, `tabindex`, `keydown`, `focus`, `innerHTML`, `createElement`, `packaged-resource`, `ajax`, `$.get`, `$.ajax`, `XMLHttpRequest`, `xhrFields`, `withCredentials`, `withcredentials`, `InvokeExtensibilityMethod`, `invokeextensibilitymethod`, `skipIfBusy`, `successCallback`, `success-callback`, `errorCallback`, `setInterval`, `JSON.stringify`, `payload`, `throttling`, `reduced-functionality`, `ClientServicesMaxUploadSize`, `&`, `Specifies`, `Message(`, `Confirm(`, `Error(` in a page context, `Disabled`, `Invalid`, `Whitelist`, `Blacklist`, trailing punctuation patterns on captions). A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed page element. Read an article's full file β€” its `## Best Practice` / `## Anti Pattern` bodies β€” only after it makes the worklist; candidate selection uses the index alone. +Apply these high-signal mappings before fuzzy topic ranking: + +- A tableextension adds a field to `DropDown` while the corresponding lookup-page control remains `Visible = false` β€” `dropdown-fieldgroup-respects-lookup-page-visibility`. +- An editable page part affects a total, FlowField, or FactBox on the parent but does not set `UpdatePropagation = Both` β€” `updatepropagation-both-refreshes-main-page`. + Once the candidate worklist is known, resolve layer-precedence conflicts per READ and record suppressions. When the post-conflict worklist is empty because no applicable UI knowledge exists, or because configuration suppressed every candidate, emit `outcome: "no-knowledge"`. When the worklist is empty because no applicable UI knowledge matched the page changes, emit `outcome: "completed"` with an empty `findings` array. diff --git a/microsoft/skills/review/al-upgrade-review.md b/microsoft/skills/review/al-upgrade-review.md index 94851a36..9ae61f8f 100644 --- a/microsoft/skills/review/al-upgrade-review.md +++ b/microsoft/skills/review/al-upgrade-review.md @@ -39,10 +39,12 @@ Narrow the relevant files to the subset that applies to the changes under review - The changed AL object names and types β€” especially codeunits with `Subtype = Upgrade` or `Subtype = Install`, tables and tableextensions adding or changing fields, enums and enumextensions, and objects under `Hybrid*`/`Migration`/`Upgrade` namespaces. - The changed triggers and procedures, weighted toward `OnCheckPreconditionsPerCompany`/`PerDatabase`, `OnUpgradePerCompany`/`PerDatabase`, `OnValidateUpgradePerCompany`/`PerDatabase`, `OnInstallAppPerCompany`/`PerDatabase`, the `OnGetPerCompanyUpgradeTags`/`OnGetPerDatabaseUpgradeTags` subscribers, and helper procedures transitively reachable from those entry points. -- Tokens extracted from the diff that relate to upgrade concerns (`Subtype = Upgrade`, `Subtype = Install`, `Upgrade Tag`, `HasUpgradeTag`, `SetUpgradeTag`, `OnCheckPreconditions`, `OnUpgrade`, `OnValidateUpgrade`, `OnInstallApp`, `DataTransfer`, `CopyFields`, `Insert`, `Modify`, `Delete`, `Rename`, `InitValue`, `ObsoleteState`, `ObsoleteReason`, `ObsoleteTag`, `DataVersion`, `ExecutionContext`, `PrimaryKey`, `key(`, `field(`, `value(`, `enum`, `enumextension`, `HybridSL`, `HybridGP`, `HybridBC`, `HybridBaseDeployment`). +- Tokens extracted from the diff that relate to upgrade concerns (`Subtype = Upgrade`, `Subtype = Install`, `Upgrade Tag`, `HasUpgradeTag`, `SetUpgradeTag`, `OnCheckPreconditions`, `OnUpgrade`, `OnValidateUpgrade`, `OnInstallApp`, `DataTransfer`, `CopyFields`, `Insert`, `Modify`, `Delete`, `Rename`, `InitValue`, `ObsoleteState`, `ObsoleteReason`, `ObsoleteTag`, `ModuleInfo`, `AppVersion`, `DataVersion`, `NavApp.GetCurrentModuleInfo`, `ExecutionContext`, `PrimaryKey`, `key(`, `field(`, `value(`, `enum`, `enumextension`, `HybridSL`, `HybridGP`, `HybridBC`, `HybridBaseDeployment`). - For each `OnCheckPreconditions...` and `OnValidateUpgrade...` trigger, build the best available call graph from surrounding unchanged source as well as changed hunks, tracing resolved calls through reachable local or internal helpers. Worklist the check-only rule when a database write occurs either directly in the trigger or in any helper procedure reachable from it. Writes include `Insert`, `Modify`, `ModifyAll`, `Delete`, `DeleteAll`, `Rename`, and `DataTransfer`. Also perform the reverse check when a PR changes a writing helper body: worklist the rule when that helper is invoked directly or transitively by an unchanged check or validation trigger. - Treat a direct write or a fully resolved call chain as high-confidence evidence. When cross-object dispatch, unavailable declarations, or an incomplete call graph prevents proving the complete chain, cap confidence at `medium`, name the unresolved edge in the finding, and do not claim a violation without a resolved path from a check or validation trigger to a write. - Worklist the install-versus-upgrade rule when migration helpers are reachable only from an install codeunit. +- Worklist `install-and-upgrade-codeunits-have-no-order.md` when a change adds multiple install or upgrade codeunits whose same-phase triggers share state or depend on one another. +- Worklist `appversion-meaning-depends-on-execution-context.md` when install or upgrade code branches on `ModuleInfo.AppVersion()` or confuses it with `DataVersion()`. A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file β€” its `## Best Practice` / `## Anti Pattern` bodies β€” only after it makes the worklist; candidate selection uses the index alone. When the diff contains no upgrade-related changes by any of the above signals, return `outcome: "not-applicable"` without evaluating files.