mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 14:46:55 +01:00
Merge upstream/main; adopt articles[] eval override, add sample-link READ convention
- Resolve conflicts in al-data-modeling-review.md by keeping both sides' additions (folder-path support, InitRecord/Round cues from upstream; the 9 document-distribution/pricing/barcode cues from this branch). - Switch the data-modeling evaluation override from an ad-hoc additionalArticles field to upstream's now-established articles[] convention (used elsewhere for finance/scm/query/reporting/style), removing the redundant parallel code path from Test-ReviewFixtures.ps1. - Fix all 9 new articles' sample references to the markdown-link READ convention required by Knowledge-Retrieval.ps1's Assert-SampleLink (plain backticks satisfy validate_frontmatter.py's regex alone but not this stricter check - both validators must pass). Validators: frontmatter 0/0, review-fixtures 126 cases/20 domains PASSED, knowledge-index 342 articles/575 samples PASSED, skill-index 19 review leaves PASSED. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
e22352b248
504 changed files with 9501 additions and 1969 deletions
|
|
@ -40,7 +40,7 @@ relevant `Method` (e.g. `"Lowest Price"`), `Type` (`Sale`/`Purchase`), and
|
|||
`Asset Type` — with `Default := true`, since `FindSetup` only considers
|
||||
rows where `Default` is set when resolving a handler for a line.
|
||||
|
||||
See sample: `activate-new-price-calculation-handler-via-onfindsupportedsetup.good.al`.
|
||||
See sample: [`activate-new-price-calculation-handler-via-onfindsupportedsetup.good.al`](activate-new-price-calculation-handler-via-onfindsupportedsetup.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ selected manually if a user creates their own `Price Calculation Setup`
|
|||
row through the UI — but ships with no default row, so it's never active
|
||||
for anyone until someone notices it's missing and configures it by hand.
|
||||
|
||||
See sample: `activate-new-price-calculation-handler-via-onfindsupportedsetup.bad.al`.
|
||||
See sample: [`activate-new-price-calculation-handler-via-onfindsupportedsetup.bad.al`](activate-new-price-calculation-handler-via-onfindsupportedsetup.bad.al).
|
||||
|
||||
## Source
|
||||
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ Putting the block check inside the master's own `OnInsert`/`OnModify` does nothi
|
|||
|
||||
The referencing line validates `Master.TestField(Blocked, false)` in `OnValidate` of the reference field and re-checks before posting. The master table stays logic-free on `Blocked`.
|
||||
|
||||
See sample: `check-blocked-in-referencing-code-not-in-master.good.al`.
|
||||
See sample: [`check-blocked-in-referencing-code-not-in-master.good.al`](check-blocked-in-referencing-code-not-in-master.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
The block check sits in the master's own `OnModify`/`OnInsert` (so referencing and posting proceed unchecked), or there is no check at all on the referencing side.
|
||||
|
||||
See sample: `check-blocked-in-referencing-code-not-in-master.bad.al`.
|
||||
See sample: [`check-blocked-in-referencing-code-not-in-master.bad.al`](check-blocked-in-referencing-code-not-in-master.bad.al).
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ what validation must pass before sending — is genuinely specific to the
|
|||
document. Custom logic belongs around the call to `Report Selections`,
|
||||
not instead of it.
|
||||
|
||||
See sample: `custom-document-dispatch-must-not-bypass-report-selections.good.al`.
|
||||
See sample: [`custom-document-dispatch-must-not-bypass-report-selections.good.al`](custom-document-dispatch-must-not-bypass-report-selections.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ without a code change and a new release, and the document is invisible to
|
|||
"Document Layouts" — the standard place every other document's
|
||||
distribution is configured.
|
||||
|
||||
See sample: `custom-document-dispatch-must-not-bypass-report-selections.bad.al`.
|
||||
See sample: [`custom-document-dispatch-must-not-bypass-report-selections.bad.al`](custom-document-dispatch-must-not-bypass-report-selections.bad.al).
|
||||
|
||||
## Source
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ equally correct; neither reads the counterparty's assigned profile.
|
|||
Reserve a genuine `Get`/`GetDefaultForCustomer`/`GetDefaultForVendor`
|
||||
lookup and `Send`/`SendVendor` for Post-and-Send.
|
||||
|
||||
See sample: `document-print-and-email-actions-call-report-selections-directly.good.al`.
|
||||
See sample: [`document-print-and-email-actions-call-report-selections-directly.good.al`](document-print-and-email-actions-call-report-selections-directly.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ clicking "Email" does nothing observable. A second version of the same
|
|||
mistake: an email action on a document that only receives from its
|
||||
counterparty and was never meant to send anything back.
|
||||
|
||||
See sample: `document-print-and-email-actions-call-report-selections-directly.bad.al`.
|
||||
See sample: [`document-print-and-email-actions-call-report-selections-directly.bad.al`](document-print-and-email-actions-call-report-selections-directly.bad.al).
|
||||
|
||||
## Source
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ custom table that should be searchable by document number:
|
|||
uncombined keys, with no compound key between them, because `"No."`
|
||||
alone is already sufficient.
|
||||
|
||||
See sample: `extend-find-entries-navigate-for-new-document-types.good.al`.
|
||||
See sample: [`extend-find-entries-navigate-for-new-document-types.good.al`](extend-find-entries-navigate-for-new-document-types.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ and a correct record count both show up — but leads nowhere when
|
|||
selected, with no error and no indication to the user that anything is
|
||||
wrong.
|
||||
|
||||
See sample: `extend-find-entries-navigate-for-new-document-types.bad.al`.
|
||||
See sample: [`extend-find-entries-navigate-for-new-document-types.bad.al`](extend-find-entries-navigate-for-new-document-types.bad.al).
|
||||
|
||||
## Source
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ price list, extend `Price Source Type` and the matching document subset
|
|||
enum (`Sales Price Source Type`, `Purchase Price Source Type`, `Job Price
|
||||
Source Type`) together, using the identical numeric ID in both.
|
||||
|
||||
See sample: `extend-price-source-type-must-sync-document-subset-enum.good.al`.
|
||||
See sample: [`extend-price-source-type-must-sync-document-subset-enum.good.al`](extend-price-source-type-must-sync-document-subset-enum.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ absent from the "Applies-to Type" options on an actual sales price list,
|
|||
with no error anywhere: the base enum extension compiles and installs
|
||||
cleanly on its own.
|
||||
|
||||
See sample: `extend-price-source-type-must-sync-document-subset-enum.bad.al`.
|
||||
See sample: [`extend-price-source-type-must-sync-document-subset-enum.bad.al`](extend-price-source-type-must-sync-document-subset-enum.bad.al).
|
||||
|
||||
## Source
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ legitimately, since that document posts to both ledgers, not by default.
|
|||
triad and the value is unreachable in Document Layouts; wire both sides
|
||||
needlessly and the picker is cluttered with a value that never applies.
|
||||
|
||||
See sample: `extend-report-selection-usage-for-new-document-types.good.al`
|
||||
See sample: [`extend-report-selection-usage-for-new-document-types.good.al`](extend-report-selection-usage-for-new-document-types.good.al)
|
||||
(customer-only document — only the customer-side enum and triad added).
|
||||
|
||||
## Anti Pattern
|
||||
|
|
@ -60,7 +60,7 @@ See sample: `extend-report-selection-usage-for-new-document-types.good.al`
|
|||
subscribers. Works via the tenant-wide default, so it's invisible in
|
||||
testing — but Document Layouts shows the value's rows blank, can't offer
|
||||
it in the Usage dropdown, and "Copy from Report Selection" never lists
|
||||
it. See sample: `extend-report-selection-usage-for-new-document-types.bad.al`.
|
||||
it. See sample: [`extend-report-selection-usage-for-new-document-types.bad.al`](extend-report-selection-usage-for-new-document-types.bad.al).
|
||||
2. Subscribe both counterparties' triads for a one-sided document. This is
|
||||
the overbroad default Jesper Schulz-Wedde's review caught: it
|
||||
contradicts how `ReportSelectionHandlerCZZ` actually partitions its
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -19,10 +19,10 @@ This is not an `Integer` `AutoIncrement` key, a GUID, or the `SystemId`. Those a
|
|||
|
||||
`No.` `Code[20]` is the sole primary key; a non-editable `No. Series` `Code[20]` field records the source series. `OnInsert` checks `if "No." = ''`, reads the setup table, `TestField`s the configured series, stores it in `No. Series`, and assigns `No.` from the series.
|
||||
|
||||
See sample: `master-table-no-from-number-series-in-oninsert.good.al`.
|
||||
See sample: [`master-table-no-from-number-series-in-oninsert.good.al`](master-table-no-from-number-series-in-oninsert.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
An `Integer` `AutoIncrement` (or GUID / `SystemId`) primary key used as the business key, with no `OnInsert` number assignment. Records get an opaque identifier no user can reference, and the master no longer participates in the standard numbering and manual-entry behavior every other BC master follows.
|
||||
|
||||
See sample: `master-table-no-from-number-series-in-oninsert.bad.al`.
|
||||
See sample: [`master-table-no-from-number-series-in-oninsert.bad.al`](master-table-no-from-number-series-in-oninsert.bad.al).
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ a trigger on the field itself (its own `OnValidate`, or a matching
|
|||
`OnAfterValidate` integration event) that calls
|
||||
`SalesLine.UpdateUnitPriceByField(SalesLine.FieldNo(<TheField>))`.
|
||||
|
||||
See sample: `new-price-source-must-add-candidate-and-trigger-recalculation.good.al`.
|
||||
See sample: [`new-price-source-must-add-candidate-and-trigger-recalculation.good.al`](new-price-source-must-add-candidate-and-trigger-recalculation.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ validation. The field is a genuine, working calculation candidate — new
|
|||
lines price correctly — but editing the field on an existing line leaves
|
||||
the unit price stale, with nothing to indicate why.
|
||||
|
||||
See sample: `new-price-source-must-add-candidate-and-trigger-recalculation.bad.al`.
|
||||
See sample: [`new-price-source-must-add-candidate-and-trigger-recalculation.bad.al`](new-price-source-must-add-candidate-and-trigger-recalculation.bad.al).
|
||||
|
||||
## Source
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ See also `validate-table-relation-false-suppresses-rename-propagation.md` for th
|
|||
|
||||
The owning table implements `OnDelete` and deletes its dependents there, filtered on the foreign key. Declare `Permissions = tabledata <dependent> = rd` on the owning table — granting delete rights only on the parent is a common miss that makes the trigger fail for a non-`SUPER` user. This mirrors the base application, where every header table deletes its own lines.
|
||||
|
||||
See sample: `owning-table-must-delete-dependents-in-ondelete.good.al`.
|
||||
See sample: [`owning-table-must-delete-dependents-in-ondelete.good.al`](owning-table-must-delete-dependents-in-ondelete.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -33,4 +33,4 @@ A parent table with dependent rows and no `OnDelete` trigger, where the dependen
|
|||
|
||||
Detection signal: a table declares `TableRelation` to table X, and table X has no `OnDelete` trigger. Whether a delete path currently exists in the UI is irrelevant to the finding.
|
||||
|
||||
See sample: `owning-table-must-delete-dependents-in-ondelete.bad.al`.
|
||||
See sample: [`owning-table-must-delete-dependents-in-ondelete.bad.al`](owning-table-must-delete-dependents-in-ondelete.bad.al).
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ font name to specify is literally `IDAutomation2D` (Maxicode itself uses
|
|||
purchased version name for that specific font (e.g. `IDAutomationHC39M`
|
||||
for Code 39), never a name containing `Demo`.
|
||||
|
||||
See sample: `report-barcodes-must-use-barcode-module-and-production-font-name.good.al`.
|
||||
See sample: [`report-barcodes-must-use-barcode-module-and-production-font-name.good.al`](report-barcodes-must-use-barcode-module-and-production-font-name.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ in review and testing and fails silently — the first because the encoded
|
|||
data was never a real barcode, the second because Business Central
|
||||
online refuses to render it at all.
|
||||
|
||||
See sample: `report-barcodes-must-use-barcode-module-and-production-font-name.bad.al`.
|
||||
See sample: [`report-barcodes-must-use-barcode-module-and-production-font-name.bad.al`](report-barcodes-must-use-barcode-module-and-production-font-name.bad.al).
|
||||
|
||||
## Source
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -19,10 +19,10 @@ The reason is a BC-specific trap: renaming a record changes its primary key and
|
|||
|
||||
Both `OnModify` and `OnRename` set `"Last Date Modified" := Today();`, and the field is declared `Editable = false` so only the triggers maintain it.
|
||||
|
||||
See sample: `set-last-date-modified-in-onmodify-and-onrename.good.al`.
|
||||
See sample: [`set-last-date-modified-in-onmodify-and-onrename.good.al`](set-last-date-modified-in-onmodify-and-onrename.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Only `OnModify` assigns `Last Date Modified`. After a rename the value is stale, and any process that trusts it to detect changes misses the record.
|
||||
|
||||
See sample: `set-last-date-modified-in-onmodify-and-onrename.bad.al`.
|
||||
See sample: [`set-last-date-modified-in-onmodify-and-onrename.bad.al`](set-last-date-modified-in-onmodify-and-onrename.bad.al).
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ The setup **card** page enforces the singleton: `InsertAllowed = false` and `Del
|
|||
|
||||
`Primary Key` `Code[10]` is the sole key; the setup is surfaced through a Card page with `InsertAllowed = false`, `DeleteAllowed = false`, and an open-time guard that inserts the blank row if it is missing.
|
||||
|
||||
See sample: `setup-table-is-a-singleton.good.al`.
|
||||
See sample: [`setup-table-is-a-singleton.good.al`](setup-table-is-a-singleton.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
An `Integer` / `AutoIncrement` key, a page that allows insert or delete, or a List page over the setup table. Any of these lets the table hold zero or many rows, so "the setup" becomes ambiguous and `Get()` may fail or read the wrong record.
|
||||
|
||||
See sample: `setup-table-is-a-singleton.bad.al`.
|
||||
See sample: [`setup-table-is-a-singleton.bad.al`](setup-table-is-a-singleton.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ application-area: [all]
|
|||
|
||||
When sharing media between different tables, iterate the source `MediaSet` and call `Target.MediaSetField.Insert(Source.MediaSetField.Item(Index))`, then modify the target record. Direct field assignment is safe only when source and target are the same record subtype and use the same field ID. This concern is about reference/delete integrity, not the separate performance cost of `ModifyAll` on tables with media fields.
|
||||
|
||||
See sample: `share-mediaset-items-with-insert-not-field-assignment.good.al`.
|
||||
See sample: [`share-mediaset-items-with-insert-not-field-assignment.good.al`](share-mediaset-items-with-insert-not-field-assignment.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`Target.Picture := Source.Picture;` where the two variables refer to different table types or different media-field IDs. The code copies an opaque ID, but the platform does not know that two independent fields now share the media object.
|
||||
|
||||
See sample: `share-mediaset-items-with-insert-not-field-assignment.bad.al`.
|
||||
See sample: [`share-mediaset-items-with-insert-not-field-assignment.bad.al`](share-mediaset-items-with-insert-not-field-assignment.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ A `tableextension` can add to an existing `TableRelation`, but the combined rela
|
|||
|
||||
When a relation is designed to follow an extensible enum, express the base cases as conditional branches and leave no unconditional catch-all ahead of future extension branches. An enum extension can then append a condition for its new value. When extending a field you do not own, inspect the original `TableRelation`; do not claim that an appended condition overrides an unconditional relation.
|
||||
|
||||
See sample: `table-relation-extensions-are-additive-and-top-down.good.al`.
|
||||
See sample: [`table-relation-extensions-are-additive-and-top-down.good.al`](table-relation-extensions-are-additive-and-top-down.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A base field has an unconditional `TableRelation = Customer;` and a `tableextension` adds `if (Type = const(Resource)) Resource`. The original unconditional branch always wins, so the new enum value still validates and looks up against Customer. The concern is evaluation order, not `ValidateTableRelation`; free-form input is covered separately by security guidance.
|
||||
|
||||
See sample: `table-relation-extensions-are-additive-and-top-down.bad.al`.
|
||||
See sample: [`table-relation-extensions-are-additive-and-top-down.bad.al`](table-relation-extensions-are-additive-and-top-down.bad.al).
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ consistency requirement between definitions that are already meant to be
|
|||
linked, not a mandate to check every field against every table on the
|
||||
cascade.
|
||||
|
||||
See sample: `transferfields-mirrored-fields-must-match-type-and-length.good.al`.
|
||||
See sample: [`transferfields-mirrored-fields-must-match-type-and-length.good.al`](transferfields-mirrored-fields-must-match-type-and-length.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ incompatible data type — on one side. Both definitions compile without
|
|||
error; nothing fails until an actual value exceeds the shorter one, which
|
||||
typical test data never does.
|
||||
|
||||
See sample: `transferfields-mirrored-fields-must-match-type-and-length.bad.al`.
|
||||
See sample: [`transferfields-mirrored-fields-must-match-type-and-length.bad.al`](transferfields-mirrored-fields-must-match-type-and-length.bad.al).
|
||||
|
||||
## Source
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ application-area: [all]
|
|||
|
||||
Use `TransferFields(Source)` only when every field the destination requires, including primary key fields, is guaranteed to share a matching field number and type with the source; this form defaults `InitPrimaryKeyFields` to `true`. Fields with no matching field number, and fields whose types differ across extensions, are skipped regardless of `SkipFieldsNotMatchingType` — that parameter only governs same-extension type mismatches. If the destination depends on a field that falls into either case, map and validate it explicitly in code rather than relying on `TransferFields` to catch the gap. Use `SkipFieldsNotMatchingType = true` only when skipping same-extension type mismatches is an intentional, documented part of the transfer contract.
|
||||
|
||||
See sample: `transferfields-skip-type-mismatch-can-drop-data.good.al`.
|
||||
See sample: [`transferfields-skip-type-mismatch-can-drop-data.good.al`](transferfields-skip-type-mismatch-can-drop-data.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Using `TransferFields(Source, InitPrimaryKeyFields, true)` as a generic way to make two evolving table schemas transfer without errors, when the destination depends on every required source field being copied. A type change on either table can turn a previously transferred field into a silently skipped one without making the transfer itself fail.
|
||||
|
||||
See sample: `transferfields-skip-type-mismatch-can-drop-data.bad.al`.
|
||||
See sample: [`transferfields-skip-type-mismatch-can-drop-data.bad.al`](transferfields-skip-type-mismatch-can-drop-data.bad.al).
|
||||
|
|
@ -19,10 +19,10 @@ LLMs reproduce the legacy `NoSeriesManagement` pattern because it dominates pre-
|
|||
|
||||
`OnInsert` assigns the number with `NoSeries.GetNextNo("No. Series")` where `NoSeries` is `Codeunit "No. Series"`. The `No.` field's `OnValidate` guards manual entry by calling `NoSeries.IsManual(...)` (or `TestManual`) before clearing `No. Series`.
|
||||
|
||||
See sample: `use-no-series-codeunit-not-noseriesmanagement.good.al`.
|
||||
See sample: [`use-no-series-codeunit-not-noseriesmanagement.good.al`](use-no-series-codeunit-not-noseriesmanagement.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`NoSeriesMgt.InitSeries(...)` for assignment and `NoSeriesMgt.TestManual(...)` for the manual check, where `NoSeriesMgt` is `Codeunit NoSeriesManagement`. Both are obsolete-pending and emit compiler warnings.
|
||||
|
||||
See sample: `use-no-series-codeunit-not-noseriesmanagement.bad.al`.
|
||||
See sample: [`use-no-series-codeunit-not-noseriesmanagement.bad.al`](use-no-series-codeunit-not-noseriesmanagement.bad.al).
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ See also `owning-table-must-delete-dependents-in-ondelete.md` for the delete hal
|
|||
|
||||
Leave `ValidateTableRelation` at its default wherever the stored value must stay correct across a rename. When it must be disabled, or when the relationship cannot be expressed as a `TableRelation` at all, the table owning the referenced key carries an explicit `OnRename` that repoints the dependents itself.
|
||||
|
||||
See sample: `validate-table-relation-false-suppresses-rename-propagation.good.al`.
|
||||
See sample: [`validate-table-relation-false-suppresses-rename-propagation.good.al`](validate-table-relation-false-suppresses-rename-propagation.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -35,4 +35,4 @@ See sample: `validate-table-relation-false-suppresses-rename-propagation.good.al
|
|||
|
||||
Detection signal: any `ValidateTableRelation = false` on a field that also declares a `TableRelation`. Ask what repoints the value when the target is renamed; if the answer is "the platform", the finding stands.
|
||||
|
||||
See sample: `validate-table-relation-false-suppresses-rename-propagation.bad.al`.
|
||||
See sample: [`validate-table-relation-false-suppresses-rename-propagation.bad.al`](validate-table-relation-false-suppresses-rename-propagation.bad.al).
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ See also `validate-table-relation-false-suppresses-rename-propagation.md`, which
|
|||
|
||||
Use `xRec` for the previous key in `OnRename`, and for the record being removed in `OnDelete`. In `OnModify`, obtain the before-image by re-reading the stored row rather than trusting `xRec`, so the logic behaves identically whether a page, a job queue or an API drove the write.
|
||||
|
||||
See sample: `xrec-is-a-before-image-only-in-some-triggers.good.al`.
|
||||
See sample: [`xrec-is-a-before-image-only-in-some-triggers.good.al`](xrec-is-a-before-image-only-in-some-triggers.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -33,4 +33,4 @@ Comparing `Rec` against `xRec` inside `OnModify` (or `OnInsert`) to detect a cha
|
|||
|
||||
Detection signal: any read of `xRec` inside `OnModify` or `OnInsert`. Treat "but it works when I test it on the page" as confirmation of the defect rather than a refutation.
|
||||
|
||||
See sample: `xrec-is-a-before-image-only-in-some-triggers.bad.al`.
|
||||
See sample: [`xrec-is-a-before-image-only-in-some-triggers.bad.al`](xrec-is-a-before-image-only-in-some-triggers.bad.al).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue