Skip to content

Extract NL customization to object extensions (TrMode)#8933

Open
Alexander-Ya wants to merge 10 commits into
mainfrom
bugs/Extract-NL-customization-to-object-extensions-v2
Open

Extract NL customization to object extensions (TrMode)#8933
Alexander-Ya wants to merge 10 commits into
mainfrom
bugs/Extract-NL-customization-to-object-extensions-v2

Conversation

@Alexander-Ya

@Alexander-Ya Alexander-Ya commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What & why

Linked work

Fixes AB#640601

How I validated this

  • I read the full diff and it contains only changes I intended.
  • I built the affected app(s) locally with no new analyzer warnings.
  • I ran the change in Business Central and confirmed it behaves as expected.
  • I added or updated tests for the new behavior, or explained below why none are needed.

What I tested and the outcome (required — be specific: scenarios, commands, screenshots for UI changes)

Risk & compatibility

@Alexander-Ya Alexander-Ya requested review from a team June 30, 2026 13:39
@Alexander-Ya

Copy link
Copy Markdown
Contributor Author

Fixes AB#640601


var
Text1000000: Label '%1 can only be filled in when %2 %3 is equal to Customer or Vendor.';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Label 'Text1000000' has no approved suffix and is missing a Comment for its three placeholders.

The label is passed to Error() so it must end with the 'Err' suffix (AA0074). Its three placeholders (%1 = FieldCaption('Transaction Mode Code'), %2 = FieldCaption('Account Type'), %3 = FieldCaption('Bal. Account Type')) also require a Comment parameter (AA0470). Rename the label to 'Text1000000Err' and add the Comment, then update the Error() call site to match.

Suggested change
}
Text1000000Err: Label '%1 can only be filled in when %2 %3 is equal to Customer or Vendor.', Comment = '%1 = Field Caption (Transaction Mode Code), %2 = Field Caption (Account Type), %3 = Field Caption (Bal. Account Type)';

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

}

var
Text1000001: Label 'It is not allowed to specify %1 when %2 is %3.';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Label 'Text1000001' has no approved suffix and is missing a Comment for its three placeholders.

The label is passed to Error() so it must end with the 'Err' suffix (AA0074). Its three placeholders (%1 = Field Caption 'Currency Euro', %2 = Field Caption 'Local Currency', %3 = the option value) also require a Comment parameter (AA0470) so translators can identify each slot. Rename the label to 'Text1000001Err' and add the Comment, then update the Error() call site to match.

Suggested change
Text1000001: Label 'It is not allowed to specify %1 when %2 is %3.';
Text1000001Err: Label 'It is not allowed to specify %1 when %2 is %3.', Comment = '%1 = Field Caption (Currency Euro), %2 = Field Caption (Local Currency), %3 = Local Currency value';

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

end;

var
Text001: Label 'must be 0 if %1 is %2';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Labels 'MustBe0Lbl', 'MustBeOddLbl', and 'MustBeEvenLbl' carry the 'Lbl' suffix, which is reserved for captions and tooltips, but all three are used as the error-description argument to FieldError() — an error-producing call.

Per the label-suffix guidance, labels consumed in error contexts must use the 'Err' suffix. Rename them to 'MustBe0Err', 'MustBeOddErr', and 'MustBeEvenErr', and update the six FieldError() call sites accordingly.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

TransactionMode: Record "Transaction Mode";
AccountType: Option Customer,Vendor,Employee;
begin
if not TransactionMode.CheckTransactionModePartnerType(AccountType::Vendor, "Transaction Mode Code", "Partner Type") then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Label 'PartnerTypeMismatchMsg' uses the 'Msg' suffix, which is reserved for Message() calls, but the label is passed to Confirm().

Per the label-suffix guidance, labels consumed by Confirm() or StrMenu must use the 'Qst' suffix. Note that the parallel label in CustomerNL.TableExt.al is already correctly named 'PartnerTypeMismatchQst'. Rename this label to 'PartnerTypeMismatchQst' and update its Confirm() call site.

Suggested change
if not TransactionMode.CheckTransactionModePartnerType(AccountType::Vendor, "Transaction Mode Code", "Partner Type") then
PartnerTypeMismatchQst: Label 'The Partner Type does not match the Partner Type defined in Transaction Mode. Do you still want to change the Partner Type?';

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

VendBankAcc: Record "Vendor Bank Account";
IsHandled: Boolean;
begin
IsHandled := false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Confirm() is called with a StrSubstNo-wrapped format string: 'Confirm(StrSubstNo(UpdateBankAccountsQst, UseFieldCaption))'.

The AL platform's Confirm() accepts a format string and variable arguments directly — wrapping with StrSubstNo hides the placeholder from analyzers and breaks the translation link. Replace with 'Confirm(UpdateBankAccountsQst, false, UseFieldCaption)', passing false as the default-button argument and the caption as the format argument.

Suggested change
IsHandled := false;
if not Confirm(UpdateBankAccountsQst, false, UseFieldCaption) then

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code suggestion is off. Addressing issue in #8930.

CustBankAcc: Record "Customer Bank Account";
IsHandled: Boolean;
begin
IsHandled := false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Confirm() is called with a StrSubstNo-wrapped format string: 'Confirm(StrSubstNo(UpdateBankAccountsQst, UseFieldCaption))'.

The AL platform's Confirm() accepts a format string and variable arguments directly — wrapping with StrSubstNo hides the placeholder from analyzers and breaks the translation link. Replace with 'Confirm(UpdateBankAccountsQst, false, UseFieldCaption)', passing false as the default-button argument and the caption as the format argument.

Suggested change
IsHandled := false;
if not Confirm(UpdateBankAccountsQst, false, UseFieldCaption) then

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code suggestion is off. Addressing issue in #8930.

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Copilot PR Review

Iteration 6 · Outcome: completed

Large cross-country refactor (302 files) extracting NL delocalized full-object overrides into table/page extensions plus new integration-event hooks replicated across country layers (APAC, BE, CH, ES, FR, IT, NA, NO, RU, W1). Most of the diff is mechanical deletion of duplicate legacy code or repeated small hook insertions with no independent defects; review focused on the substantive new logic (event subscribers, upgrade codeunits) rather than re-reading every deleted duplicate object.

Knowledge source: https://github.com/microsoft/BCQuality@822cae1b2771ac25f665f73369f69093bd4fd630

Findings by domain

Findings split into Knowledge-backed (cite a BCQuality article) and Agent (the agent's own judgement, no matching BCQuality rule).

Domain Findings Knowledge-backed Agent Inline Fallback
Agent 1 0 1 1 0
Style 1 1 0 1 0
Upgrade 1 1 0 1 0

Totals: 2 knowledge-backed · 1 agent findings.

Orchestrator pre-filter (13 file(s) excluded)

  • layer-disabled (knowledge) : 13 file(s)

Findings produced by the Copilot CLI agent against BCQuality at 822cae1b2771ac25f665f73369f69093bd4fd630. Reply 👎 on any inline comment to flag false positives.

JesperSchulz pushed a commit that referenced this pull request Jun 30, 2026
The model-reported anchor for a single-line suggestion can be off by more
than 8 lines (e.g. PR #8933 VendorNL Confirm() was off by 10, label rename
off by 9). Widen the search window to 40 lines either side so the correct
target within the same procedure is considered, while the 0.5 similarity
floor and 0.1 ambiguity margin keep an unrelated look-alike from winning.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
JesperSchulz pushed a commit that referenced this pull request Jun 30, 2026
Widening the search window exposed a precision problem: a label-rename
suggestion whose added Comment text echoes the field captions at the
Error()/Confirm() call site would re-anchor onto the call site (PR #8933
GenJournalLineNL/GeneralLedgerSetupNL 'Text1000000/1' findings), which is
worse than the original mis-anchor.

Raise the similarity floor to 0.6 and use a bounded window (20). Genuine
edit targets - an edited statement or a renamed declaration - score
~0.75-0.99 and re-anchor confidently (the PR #8933 Confirm() findings and
the PartnerTypeMismatchMsg->Qst rename now land on the right line). Lower-
confidence look-alikes stay below the floor and are suppressed, so the
caller posts a manual snippet instead of a wrong auto-applicable anchor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
VATMod11NotAllowedCharErr: Label 'The VAT registration number must have the format NLdddddddddBdd where d is a digit.';
VATMod97NotAllowedCharErr: Label 'The VAT registration number for a natural person must have the format NLXXXXXXXXXXdd where d is a digit, and x can be a digit, an uppercase letter, ''+'', or ''*''.';
VATMod11Err: Label 'The VAT registration number is not valid according to the Modulus-11 checksum algorithm.';
VATMod97Err: Label 'The VAT registration number is not valid according to the Modulus-97 checksum algorithm.';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 2}}$

'SummaryTwoErr' has Comment = '%1, %2 - error text': both placeholders are described only as 'error text' without naming what they represent.

%1 is always the fixed format-error preamble (VATRegNoFormatErr) and %2 is the specific validation error string (e.g. Mod11 or Mod97 error). Translators working from the label alone cannot determine whether word order can be changed, as required in many languages.

Suggested change
VATMod97Err: Label 'The VAT registration number is not valid according to the Modulus-97 checksum algorithm.';
SummaryTwoErr: Label '%1%2', Comment = '%1 = VAT registration number format error prefix, %2 = specific validation error detail';

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@@ -0,0 +1,22 @@
// ------------------------------------------------------------------------------------------------

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 2}}$

The file is named 'COmpanyInformationNL.Codeunit.al' — the leading 'CO' breaks PascalCase.

The correct name is 'CompanyInformationNL.Codeunit.al', matching the AL object name 'Company Information NL'. Wrong casing breaks grep, symbol search, and the implicit file-to-object map that tooling relies on.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

Caption = 'Open';
DataClassification = CustomerContent;
InitValue = true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Upgrade} \quad \color{gray}{\texttt{\small Iteration\ 2}}$

The new 'Open' Boolean field added to G/L Entry via GLEntryNL.TableExt.al declares InitValue = true.

G/L Entry is a production-scale ledger table: every row that existed before this extension is installed will carry Open = false (the datatype default), not true. GLEntryNL.Codeunit.al branches on GLEntry.Open at runtime, so existing entries silently fall into the wrong code path. The analogous InitValue = true field on GeneralLedgerSetup ('Local SEPA Instr. Priority') is correctly paired with upgrade code in UPGSEPANL.Codeunit.al, but no equivalent upgrade procedure exists for G/L Entry in this PR.

Recommendation:

  • add an upgrade procedure that sets Open = true for existing G/L Entry rows via DataTransfer or ModifyAll, guarded by an upgrade tag registered in UpgradeTagDefCountry.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Accessibility} \quad \color{gray}{\texttt{\small Iteration\ 4}}$

The action "General Business" had its Caption changed from 'Gen.

Business Posting Groups' to 'General Business Posting Groups', but the sibling Tooltip property was left unchanged as 'Open the Gen. Business Posting Groups page.', which now names an action title that no longer exists on screen. This textual drift recurs identically in AT, BE, CH, DACH, DE, ES, FI, FR, GB, and W1 copies of FinanceManagerRoleCenter.Page.al.

Recommendation:

  • update each Tooltip to reference the new caption text.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

                        Tooltip = 'Open the General Business Posting Groups page.';

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

PartnerTypeMismatchQst: Label 'The Partner Type does not match the Partner Type defined in Transaction Mode. Do you still want to change the Partner Type?';

[Scope('OnPrem')]
procedure UpdateCustomerBankAccounts(UseFieldCaption: Text[250])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Agent} \quad \color{gray}{\texttt{\small Iteration\ 4}}$

In the new CustomerNL.TableExt.al, UpdateCustomerBankAccounts was changed from the original local procedure (no scope restriction) to a global procedure decorated with [Scope('OnPrem')].

It is only ever called from OnAfterValidate triggers within the same table extension (Name/Address/City/Country/Post Code), so making it global is unnecessary API-surface exposure, and Scope('OnPrem') was never present on the original code — it appears to have been copy-pasted from the analogous VendorNL.TableExt.al, where [Scope('OnPrem')] legitimately pre-existed on UpdateVendorBankAccounts in the pre-PR Vendor.Table.al. Restricting a procedure invoked from a base-table trigger chain to on-premises-only risks breaking that validation flow for Business Central Online (SaaS) tenants running the NL localization, which is a functional regression, not merely a style nit — the true impact would warrant major, but per BCQuality's agent-finding rules this is capped at minor until a knowledge-backed rule covers this pattern; the concern crosses the security (API-surface) and reliability/availability (SaaS-vs-OnPrem) domains, which is why no single leaf owns it.

Recommendation:

  • revert to local procedure and drop [Scope('OnPrem')] to match the pre-existing (correct) behavior.
Suggested change
procedure UpdateCustomerBankAccounts(UseFieldCaption: Text[250])
local procedure UpdateCustomerBankAccounts(UseFieldCaption: Text[250])

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

field(11000000; "Transaction Mode Code"; Code[20])
{
Caption = 'Transaction Mode Code';
DataClassification = CustomerContent;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Privacy} \quad \color{gray}{\texttt{\small Iteration\ 4}}$

Customer, Vendor, Sales Header, and Employee already declare DataClassification = CustomerContent at the table level (base app).

The new NL telebanking fields added by this PR (e.g. "Transaction Mode Code", "Bank Account Code", "Bank Name", "Bank City") repeat the identical DataClassification = CustomerContent at field level in the new table extensions. Per the referenced guidance this is the 'mirror anti-pattern': redundant field-level classification that duplicates an already-declared table-level default and adds nothing. Occurs in CustomerNL.TableExt.al:23, VendorNL.TableExt.al:16, SalesHeaderNL.TableExt.al:23/45, EmployeeNL.TableExt.al:16/30/35, and PurchaseHeaderNL.TableExt.al:17/36.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@github-actions github-actions Bot added this to the Version 29.0 milestone Jul 1, 2026
@@ -77,6 +77,7 @@ page 8900 "Administrator Main Role Center"
ApplicationArea = All;
Caption = 'System Information';
RunObject = page "Latest Error";
Tooltip = 'Open the System Information page.';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Accessibility} \quad \color{gray}{\texttt{\small Iteration\ 5}}$

Same 'Tooltip' vs.

'ToolTip' casing inconsistency as the al-style-review finding above, rolled up here because it also affects the UI/accessibility surface: pages carry a large number of new tooltip properties whose casing diverges from the codebase's dominant convention and from this article's own examples. No functional/accessibility impact (the tooltip still renders), but it is a widespread, easily-fixed naming inconsistency introduced by this PR across Role Center pages.

Suggested change
Tooltip = 'Open the System Information page.';
ToolTip = 'Open the System Information page.';

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@@ -7581,7 +7582,7 @@ table 36 "Sales Header"
if ReplaceVATDate then
"VAT Reporting Date" := VATDateReq;

if ReplaceDocDate and ("Document Date" <> PostingDateReq) then begin
if ReplacePostingDate and ReplaceDocDate and ("Document Date" <> PostingDateReq) then begin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Agent} \quad \color{gray}{\texttt{\small Iteration\ 6}}$

In BatchConfirmUpdatePostingDate, the condition was changed to if ReplacePostingDate and ReplaceDocDate and ("Document Date" <> PostingDateReq) then begin, but the procedure already exits immediately at its top with if not ReplacePostingDate then exit;.

By the time execution reaches this line, ReplacePostingDate is always true, so the added conjunct is dead weight that changes nothing at runtime. It's harmless today but misleads future readers/maintainers into thinking the flag is still meaningfully guarding this branch, and could mask a future refactor mistake if the early exit is ever removed. Recommend reverting to the original if ReplaceDocDate and ("Document Date" <> PostingDateReq) then begin or, if defensive redundancy is deliberate, adding a short comment noting ReplacePostingDate is guaranteed true here.

Suggested change
if ReplacePostingDate and ReplaceDocDate and ("Document Date" <> PostingDateReq) then begin
if ReplaceDocDate and ("Document Date" <> PostingDateReq) then begin

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

end;

[EventSubscriber(ObjectType::Table, Database::"Purchase Header", 'OnValidatePurchaseHeaderPayToVendorNoOnBeforeCheckDocType', '', false, false)]
local procedure OnValidatePurchaseHeaderPayToVendorNoOnBeforeCheckDocType(Vendor: Record Vendor; var PurchaseHeader: Record "Purchase Header"; xPurchaseHeader: Record "Purchase Header"; SkipPayToContact: Boolean)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 6}}$

The new subscriber OnValidatePurchaseHeaderPayToVendorNoOnBeforeCheckDocType in PurchaseHeaderNL.Codeunit.al declares xPurchaseHeader: Record "Purchase Header" without var, while the publisher (PurchaseHeader.Table.al) declares it as var xPurchaseHeader.

Every other subscriber of this same event in the repo that includes this parameter (CZ PurchaseHeaderHandlerCZL, DK FIKSubscribers, NA DIOTSubscribers) declares it as var xPurchaseHeader, matching the publisher. The referenced guidance requires reproducing the publisher signature verbatim; since the parameter is unused in this subscriber body, the safer and more consistent fix is to omit it entirely (as GSTPurchaseSubscribers does) rather than redeclare it with a mismatched modifier.

Suggested change
local procedure OnValidatePurchaseHeaderPayToVendorNoOnBeforeCheckDocType(Vendor: Record Vendor; var PurchaseHeader: Record "Purchase Header"; xPurchaseHeader: Record "Purchase Header"; SkipPayToContact: Boolean)
local procedure OnValidatePurchaseHeaderPayToVendorNoOnBeforeCheckDocType(Vendor: Record Vendor; var PurchaseHeader: Record "Purchase Header"; SkipPayToContact: Boolean)

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

GeneralLedgerSetup.Validate("Local SEPA Instr. Priority", true);
GeneralLedgerSetup.Modify(true);
GeneralLedgerSetup."Local SEPA Instr. Priority" := true;
GeneralLedgerSetup.Modify(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Upgrade} \quad \color{gray}{\texttt{\small Iteration\ 6}}$

Two NL upgrade codeunits were changed from Validate(...) + Modify(true) to direct field assignment + Modify(false) when writing to pre-existing setup fields (`ElecTaxDeclarationSetup."Tax Decl.

Schema Version"et al., andGeneralLedgerSetup."Local SEPA Instr. Priority"). Neither field currently has an OnValidate/OnModify trigger, but Modify(true)also raises the table's genericOnBeforeModifyEvent/OnAfterModifyEventintegration events;Modify(false)` silently suppresses them. Any current or future subscriber relying on these generic modify events for these setup tables (audit, sync, telemetry) will stop firing for this update path. Per the referenced guidance's principle (applies equally to direct writes as to DataTransfer): this is fine only if intentional and safe — confirm with the field owners or add a comment stating the triggers are intentionally skipped.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@Alexander-Ya Alexander-Ya added Finance GitHub request for Finance area Delocalization Extracting from country layers and converting local functionality to app labels Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Delocalization Extracting from country layers and converting local functionality to app Finance GitHub request for Finance area

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants