Skip to content

[E-Document] Data Exchange v2 import handler (bridge pattern)#7565

Open
Groenbech96 wants to merge 34 commits into
mainfrom
magnushar/edoc-import-v2-data-exchange
Open

[E-Document] Data Exchange v2 import handler (bridge pattern)#7565
Groenbech96 wants to merge 34 commits into
mainfrom
magnushar/edoc-import-v2-data-exchange

Conversation

@Groenbech96
Copy link
Copy Markdown
Contributor

@Groenbech96 Groenbech96 commented Apr 7, 2026

Summary

  • Add IStructuredFormatReader implementation for Data Exchange format (codeunit 6407)
  • Bridge existing Data Exchange Definition infrastructure to v2 import pipeline staging tables
  • Register as enum value 5 "Data Exchange" on E-Doc. Read into Draft
  • Namespace-based definition matching (replaces v1 trial-and-error auto-detection)
  • Run Data Handling Codeunit only (skip Pre-Mapping codeunit 6156 — vendor/GL resolution deferred to Prepare Draft)
  • Map Intermediate Data Import records to E-Document Purchase Header / E-Document Purchase Line via field-ID bridge
  • Process base64 attachments from Document Attachment intermediate records
  • XPath supplement for Company Information fields not in intermediate data
  • Integration events for partner extensibility

New codeunit

ID Name Purpose
6407 E-Document Data Exch. Handler IStructuredFormatReader for Data Exchange format

Key design decisions

  • No Commit() — ReadIntoDraft runs inside pipeline try-function context
  • No EDocument.Modify() — record passed by value; local variables used
  • Namespace matching — matches XML root namespace against DataExchLineDef.Namespace instead of v1's trial-and-error (which requires Commit())
  • Data Handling only — runs codeunit 1214 to populate Intermediate Data Import, skips Pre-Mapping (6156) since v2 Prepare Draft handles vendor/item resolution

Test plan

  • Invoice ReadIntoDraft: header fields mapped (vendor name, invoice no, dates, amounts)
  • Invoice ReadIntoDraft: line fields mapped (description, quantity, unit price, line numbers)
  • Invoice returns "Purchase Invoice" process draft type
  • CreditNote returns "Purchase Credit Memo" process draft type
  • Total VAT computed correctly
  • Attachments decoded from base64 and stored
  • Currency code blank when matching LCY

AB#630822
AB#599123

Magnus Hartvig Grønbech and others added 15 commits March 31, 2026 08:51
…mport

Add full credit memo pipeline: PEPPOL CreditNote XML parsing, per-type
Process Draft enum dispatch, shared PrepareDraft helper, FinishDraft
credit memo creation with ISV extension interface.

- Parse CreditNote XML with BillingReference extraction (warning on empty)
- Add enum values "Purchase Invoice" (1) and "Purchase Credit Memo" (2),
  obsolete "Purchase Document" (0) with Pending tag 29.0
- Extract shared PrepareDraft logic into E-Doc. Prepare Draft Helper (6406)
- Create Prepare Purch. Cr. Memo Draft (6403) returning correct E-Document Type
- Create E-Doc. Create Purch. Cr. Memo (6404) with IEDocumentCreatePurchaseCreditMemo
  interface (6405) for ISV customization
- Wire E-Document Type value 10 to new FinishDraft implementation
- Add field 39 "Applies-to Doc. No." to staging header table
- Add PEPPOL CreditNote test XML and 5 new tests covering parsing,
  enum routing, E2E pipeline, FinishDraft undo, and invoice regression

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… naming and tags

- Extract shared ApplyDraftToBC logic (amounts, E-Document Link, attachments,
  totals validation) into FinalizeCreatedDocument/RevertCreatedDocument on
  E-Doc. Purch. Doc. Helper — both invoice and credit memo codeunits now
  delegate to it, keeping only type-specific dispatch
- Rename "E-Doc. Prepare Draft Helper" to "Prepare Purchase Draft"
- Add #if not CLEAN29 tags around obsoleted enum value 0 "Purchase Document"
- Fix telemetry tag to empty string per convention

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…achments and charges

- Fix CreditNote DueDate XPath: use PaymentMeans/PaymentDueDate per
  PEPPOL BIS 3.0 spec (Invoice uses top-level cbc:DueDate, CreditNote
  does not)
- Add document attachment extraction from AdditionalDocumentReference
  with embedded base64 binary objects
- Add document-level AllowanceCharge line creation for charges
  (ChargeIndicator=true), matching V1 behavior
- Fix Customer EndpointID: only set GLN when schemeID=0088, store
  full schemeID:value in Customer Company Id
- Fix Description priority: use mandatory Item Name as primary,
  fallback to Description only if Name absent
- Rename XML Utility to PEPPOL Utility (codeunit 6401)
- Add PEPPOL BIS 3.0 spec reference comments throughout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 10 new test cases covering all completeness document items:
- Document-level charges/allowances (charge creates line, allowance does not)
- Embedded document attachments (base64 extraction, external URI skip)
- CreditNote without DueDate (PaymentMeans/PaymentDueDate absent)
- Description cascade (Name priority, Description fallback)
- PayeeParty override (vendor name and VAT ID)
- StandardItemIdentification priority over SellersItemIdentification
- Customer endpoint schemeID logic (GLN only for 0088)
- Multiple VAT rates and zero-rated VAT category Z

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…data, valid attachment content

- Add RegistrationName fallback for vendor and customer name when
  PartyName/Name is absent (optional in PEPPOL BIS 3.0)
- Fix test GLN to valid 13-digit value matching Text[13] field
- Use valid PDF and PNG content in attachment test XML

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r PEPPOL BIS 3.0

CreditNote has no top-level cbc:DueDate. Per spec, the due date is
at cac:PaymentMeans/cbc:PaymentDueDate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lingReference

Populate the Applies-to Doc. No. field on the BC Purchase Credit Memo
from the PEPPOL CreditNote BillingReference. Uses direct assignment
instead of Validate to avoid triggering Vendor Ledger Entry lookups,
since the BillingReference is the vendor's invoice ID, not a BC
document number. Consolidates Modify calls in CreatePurchaseCreditMemo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split responsibilities between handler and utility:
- Handler (224 lines): orchestration only — what to parse, in what order,
  document-type-specific dispatch (Invoice vs CreditNote doc info)
- Utility (339 lines): reusable PEPPOL extraction — party info, amounts,
  dates, currency, line fields, attachment decoding, MIME mapping

Moved to utility: PopulateSupplierInfo, PopulateCustomerInfo,
PopulateAmountsAndDates, PopulateCurrency, SetCurrencyIfForeign,
PopulatePurchaseLine, ExtractAttachment, MimeToFileExtension.

Methods taking internal table types use 'internal' access to satisfy
AL0749 (public codeunit exposing internal parameter types).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… mapping, attachments, XPath

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… Data Handling codeunit, test assertions

- Replace trial-and-error auto-detection (incompatible with try-function context)
  with namespace-based definition matching against DataExchLineDef.Namespace
- Run Data Handling Codeunit (1214) after ImportToDataExch to populate
  Intermediate Data Import records (skip Pre-Mapping codeunit 6156 only)
- Use local variables instead of EDocument.Modify() (record passed by value)
- Fix test Sub Total assertions to match actual XML TaxExclusiveAmount
@github-actions github-actions Bot added the AL: Apps (W1) Add-on apps for W1 label Apr 7, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

AL Documentation Audit

Documentation gaps were detected in the following apps:

  • E-Document-Core-Tests: 0% documentation coverage
  • E-Document-Core: 45% documentation coverage

To generate documentation, run /al-docs init or /al-docs update using GitHub Copilot CLI or Claude Code.
This review is for awareness to help keep documentation in sync with code changes. It is okay to dismiss this request.

Magnus Hartvig Grønbech and others added 9 commits April 9, 2026 08:51
…verflow

- AA0228: Remove unused CreateDataExch local method
- AA0137: Remove unused EDocumentPurchaseHeader param from MapIntermediateLineFields
- AA0005: Remove unnecessary begin..end around single if-else in SupplementWithXPath
- AA0139: Change ExtractXPathField to return Text, callers use CopyStr to prevent overflow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…Next

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CodeCop analyzer requires FindSet() to be used with a
repeat...until Next() = 0 loop. Refactored the line assertions
to use a loop with a case statement instead of standalone
FindSet() followed by Next().
Include the actual error message from the Error Message table
when the processing step fails, so build logs reveal the root
cause instead of a generic assertion failure.
The Error Message table is not available in Clean builds. Use
E-Document Log fields for diagnostics instead.
Clean build requires text constants/labels for StrSubstNo format
strings. Use concatenation instead for diagnostic output.
In CI environments, the shipped EDOCPEPPOLINVIMP and
EDOCPEPPOLCRMEMOIMP definitions may not exist if the install
codeunit hasn't run. Explicitly create them in Initialize()
using the E-Document Install codeunit.
@github-actions github-actions Bot added this to the Version 29.0 milestone Apr 13, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

AL Documentation Audit

Documentation gaps were detected in the following apps:

  • E-Document-Core-Tests: 0% documentation coverage
  • E-Document-Core: 59% documentation coverage

To generate documentation, run /al-docs init or /al-docs update using GitHub Copilot CLI or Claude Code.
This review is for awareness to help keep documentation in sync with code changes. It is okay to dismiss this request.

Magnus Hartvig Grønbech and others added 2 commits April 14, 2026 11:02
Creates new PEPPOL import definitions (EDOCPEPPOLINVIMPV2,
EDOCPEPPOLCRMEMOIMPV2) stored as XML resource files and loaded via
NavApp.GetResource. These v2 definitions have no pre-mapping codeunit,
making them safe for the v2 import pipeline where Prepare Draft handles
vendor/GL resolution separately.

The DX handler now calls ProcessDataExchange conformantly instead of
manually running individual pipeline steps. FindBestDataExchDef matches
the document namespace against known PEPPOL BIS 3.0 namespaces directly.

Renames the enum caption from "Data Exchange" to
"PEPPOL BIS 3 - Data Exchange" for clarity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ridge

V2 definitions now target E-Document Purchase Header (6100) and
E-Document Purchase Line (6101) directly instead of BC standard tables.
This makes the field mapping fully configurable through the product UI.

Added 6 new XML columns per definition to match PEPPOL handler 1:1:
- SupplierRegistrationName, SupplierContactName, SupplierTaxSchemeCompanyID
- PayeeLegalEntityCompanyID, CustomerRegistrationName, CustLegalEntityCompanyID

Replaced the hardcoded MapPurchaseHeaderField/MapPurchaseLineField case
statements with a generic RecordRef-based bridge that reads intermediate
data by staging table field IDs. Post-processing handles Total VAT
calculation, Amount Due, and Currency LCY-blank convention.

Removed XPath supplement fallback — no longer needed since the Data
Exchange definitions now map directly to staging fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

AL Documentation Audit

Documentation gaps were detected in the following apps:

  • E-Document-Core-Tests: 0% documentation coverage

To generate documentation, run /al-docs init or /al-docs update using GitHub Copilot CLI or Claude Code.
This review is for awareness to help keep documentation in sync with code changes. It is okay to dismiss this request.

VolodySyn
VolodySyn previously approved these changes Apr 14, 2026
Shorten definition codes from EDOCPEPPOLINVIMPV2 (21 chars, over limit)
to EDOCPEPINVIMPV2 (15) and EDOCPEPCRMEMOIMPV2 (18).

Rename resource files to remove spaces:
  eDocPEPPOLInvoiceImportV2.xml, eDocPEPPOLCrMemoImportV2.xml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

AL Documentation Audit

Documentation gaps were detected in the following apps:

  • E-Document-Core-Tests: 0% documentation coverage

To generate documentation, run /al-docs init or /al-docs update using GitHub Copilot CLI or Claude Code.
This review is for awareness to help keep documentation in sync with code changes. It is okay to dismiss this request.

Magnus Hartvig Grønbech added 2 commits May 18, 2026 14:57
@Groenbech96 Groenbech96 requested a review from a team as a code owner May 19, 2026 15:49
FieldValue := CopyStr(IntermediateDataImport.GetValue(), 1, GetFieldMaxLength(FieldRef));
if FieldValue <> '' then
AssignFieldValue(FieldRef, FieldValue);
RecordRef.SetTable(EDocumentPurchaseLine);
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\ —\ Performance} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Redundant RecordRef SetTable/GetTable calls per field

Inside the inner loop of MapIntermediateToLines, lines 223–224 call RecordRef.SetTable(EDocumentPurchaseLine) followed immediately by RecordRef.GetTable(EDocumentPurchaseLine) for every single intermediate field. This double round-trip per field is unnecessary and adds overhead for documents with many lines and fields.

Recommendation:

  • Move the RecordRef.SetTable call to only occur once per record group boundary (after the loop or at the start of a new record), rather than per-field. The intermediate GetTable after each field assignment can be replaced by working entirely with FieldRef until the record needs to be committed.
Suggested change
RecordRef.SetTable(EDocumentPurchaseLine);
until IntermediateDataImport.Next() = 0;
// Insert last line - only set table once at the end
RecordRef.SetTable(EDocumentPurchaseLine);
PostProcessLine(EDocumentPurchaseLine);
EDocumentPurchaseLine.Insert();

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

DocumentAttachment: Record "Document Attachment";
IntermediateDataImport: Record "Intermediate Data Import";
EDocAttachmentProcessor: Codeunit "E-Doc. Attachment Processor";
AttachmentTempBlob: Codeunit "Temp Blob";
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\ —\ Performance} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

AttachmentTempBlob not cleared between attachment records

AttachmentTempBlob is a single local codeunit instance reused across all attachment records in the loop. When CreateOutStream is called for a second attachment, the first attachment's data may not be fully flushed before the blob is reused, risking data leakage between attachment records.

Recommendation:

  • Call Clear(AttachmentTempBlob) at the start of each new attachment record iteration, before calling CreateOutStream, to ensure each attachment starts with a clean blob.
Suggested change
AttachmentTempBlob: Codeunit "Temp Blob";
if CurrRecordNo <> IntermediateDataImport."Record No." then begin
if CurrRecordNo <> -1 then
if FileName <> '' then begin
AttachmentTempBlob.CreateInStream(InStream);
EDocAttachmentProcessor.Insert(EDocument, InStream, FileName);
FileName := '';
Clear(AttachmentTempBlob);
end;
CurrRecordNo := IntermediateDataImport."Record No.";
end;

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

var
PurchInvHeader: Record "Purch. Inv. Header";
begin
PurchInvHeader.SetRange("Vendor Invoice No.", ExtInvoiceNo);
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\ —\ Security} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Invoice lookup missing vendor filter in ResolveAppliesToFromExtInvoiceNo

PurchInvHeader.SetRange("Vendor Invoice No.", ExtInvoiceNo) searches all posted purchase invoices without filtering by vendor. Because vendor invoice numbers are only unique per vendor, this can match an invoice from a different vendor, causing the credit memo to incorrectly apply against another vendor's invoice.

Recommendation:

  • Add a filter on Buy-from Vendor No. (taken from the PurchaseHeader) before calling FindFirst() to restrict the lookup to the correct vendor.
Suggested change
PurchInvHeader.SetRange("Vendor Invoice No.", ExtInvoiceNo);
local procedure ResolveAppliesToFromExtInvoiceNo(ExtInvoiceNo: Text[100]; var PurchaseHeader: Record "Purchase Header")
var
PurchInvHeader: Record "Purch. Inv. Header";
begin
PurchInvHeader.SetRange("Buy-from Vendor No.", PurchaseHeader."Buy-from Vendor No.");
PurchInvHeader.SetRange("Vendor Invoice No.", ExtInvoiceNo);
if PurchInvHeader.FindFirst() then begin
PurchaseHeader."Applies-to Doc. Type" := PurchaseHeader."Applies-to Doc. Type"::Invoice;
PurchaseHeader."Applies-to Doc. No." := PurchInvHeader."No.";
end;
end;

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


procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob")
begin
Error('A view is not implemented for this handler.');
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}}$

Hard-coded string literal instead of Label in View

The View procedure calls Error('A view is not implemented for this handler.') using a raw string literal. This string is not localizable and violates the AL convention of using Label for all user-visible text.

Recommendation:

  • Declare a Label variable for the error message and reference it in the Error call.
Suggested change
Error('A view is not implemented for this handler.');
var
ViewNotImplementedErr: Label 'A view is not implemented for this handler.';
procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob")
begin
Error(ViewNotImplementedErr);
end;

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

Caption = 'Applies-to Doc. No.';
DataClassification = CustomerContent;
}
field(40; "Vendor Invoice No."; Text[100])
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\ 1}}$

Table field rename breaks existing extensions and data

Field 40 is renamed from "Vendor Invoice No." to "Applies-to Ext. Invoice No.". Renaming a published table field is a breaking change: any existing extension or report that references "Vendor Invoice No." on this table will fail to compile, and any data exchange definitions targeting the old field name will stop working.

Recommendation:

  • For published tables, prefer adding a new field rather than renaming. If rename is required, verify all callers across all dependent apps/extensions are updated and consider an upgrade codeunit to migrate existing data.
Suggested change
field(40; "Vendor Invoice No."; Text[100])
// Option: Keep old field as obsolete and add new field
field(40; "Vendor Invoice No."; Text[100])
{
Caption = 'Vendor Invoice No.';
DataClassification = CustomerContent;
ObsoleteReason = 'Use Applies-to Ext. Invoice No. instead.';
ObsoleteState = Pending;
ObsoleteTag = '26.0';
}
field(41; "Applies-to Ext. Invoice No."; Text[100])
{
Caption = 'Applies-to Ext. Invoice No.';
DataClassification = CustomerContent;
}

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

DataExchDef.ProcessDataExchange(DataExch);

BridgeToStagingTables(EDocument, DataExch);
DeleteIntermediateData(DataExch);
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\ —\ Performance} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

DataExch record leaks on pipeline error

In RunPipelineAndBridge, a DataExch record is inserted and modified but there is no error handling around the subsequent ImportToDataExch, ProcessDataExchange, BridgeToStagingTables, or DeleteIntermediateData calls. If any of those steps throws an error, the DataExch record and its intermediate data remain permanently in the database (orphaned rows in "Data Exch." and "Intermediate Data Import").

Recommendation:

  • Wrap the pipeline in a try-function or add a commit-free cleanup on failure. At minimum, call DataExch.Delete(true) in an error handler so intermediate data is rolled back or cleaned up.
Suggested change
DeleteIntermediateData(DataExch);
local procedure RunPipelineAndBridge(EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob"; DataExchDefCode: Code[20])
begin
// ... setup DataExch record ...
if not TryRunPipeline(EDocument, DataExch, DataExchDef) then begin
DataExch.Delete(true);
Error(GetLastErrorText());
end;
end;
[TryFunction]
local procedure TryRunPipeline(...)
begin
DataExch.ImportToDataExch(DataExchDef);
DataExchDef.ProcessDataExchange(DataExch);
BridgeToStagingTables(EDocument, DataExch);
DeleteIntermediateData(DataExch);
end;

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

@github-actions
Copy link
Copy Markdown
Contributor

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

FindFirst without index on Vendor Invoice No.

ResolveAppliesToFromExtInvoiceNo calls PurchInvHeader.SetRange("Vendor Invoice No.", ExtInvoiceNo) then FindFirst() without calling SetCurrentKey. If "Vendor Invoice No." is not the primary key or part of a sorted index, this triggers a full-table scan on Purch. Inv. Header, which can be very slow for companies with large invoice history.

Recommendation:

  • Add PurchInvHeader.SetCurrentKey("Vendor Invoice No.") before the SetRange, and consider also filtering by "Buy-from Vendor No." to avoid applying the wrong vendor's invoice.
local procedure ResolveAppliesToFromExtInvoiceNo(ExtInvoiceNo: Text[100]; var PurchaseHeader: Record "Purchase Header")
var
    PurchInvHeader: Record "Purch. Inv. Header";
begin
    PurchInvHeader.SetCurrentKey("Vendor Invoice No.");
    PurchInvHeader.SetRange("Vendor Invoice No.", ExtInvoiceNo);
    PurchInvHeader.SetRange("Buy-from Vendor No.", PurchaseHeader."Buy-from Vendor No.");
    if PurchInvHeader.FindFirst() then begin
        PurchaseHeader."Applies-to Doc. Type" := PurchaseHeader."Applies-to Doc. Type"::Invoice;
        PurchaseHeader."Applies-to Doc. No." := PurchInvHeader."No.";
    end;
end;

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

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

RootElement: XmlElement;
Stream: InStream;
begin
TempBlob.CreateInStream(Stream);
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\ —\ Performance} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

GetDocumentRootNamespace creates new InStream on each call

GetDocumentRootNamespace calls TempBlob.CreateInStream(Stream) and reads the entire XML document to extract the root namespace. FindBestDataExchDef then calls this, and afterwards RunPipelineAndBridge calls TempBlob.CreateInStream(Stream) again — reading the blob a second time. TempBlob InStreams are position-based; creating a new InStream rewinds to the start, so this works correctly, but it means the XML is read/parsed twice.

Recommendation:

  • Cache the namespace result or pass it through to avoid double-parsing. This is especially relevant for large PEPPOL documents with many attachments.
Suggested change
TempBlob.CreateInStream(Stream);
// Pass namespace through or cache:
local procedure ReadIntoDraft(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"): Enum "E-Doc. Process Draft"
var
BestDefCode: Code[20];
BestDocType: Enum "E-Document Type";
begin
FindBestDataExchDef(TempBlob, BestDefCode, BestDocType); // reads namespace
RunPipelineAndBridge(EDocument, TempBlob, BestDefCode); // TempBlob rewound automatically
exit(MapDocumentTypeToProcessDraft(BestDocType));
end; // acceptable as-is if TempBlob rewind is cheap; add comment to clarify

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

RecordRef.GetTable(EDocumentPurchaseHeader);
repeat
if RecordRef.FieldExist(IntermediateDataImport."Field ID") then begin
FieldRef := RecordRef.Field(IntermediateDataImport."Field ID");
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\ —\ Performance} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

BuildEndpointIdentifiers does redundant SetRange reuse

BuildEndpointIdentifiers calls DataExchColumnDef.SetRange(Name, 'CustomerEndpointSchemeID') without first clearing the previous SetRange(Name, 'CustomerEndpointID') filter. The field Name is reset via SetRange so this works correctly for the column lookup, but then DataExchField.SetRange re-uses the same variable after changing "Column No." filter without resetting the previous range — meaning if the first FindFirst() for EndpointValue did not find anything, DataExchField still has stale filter state.

Recommendation:

  • Call DataExchField.Reset() or explicitly DataExchField.SetRange("Data Exch. No.", DataExch."Entry No.") before the second SetRange("Column No.", SchemeColNo) to avoid unexpected filter accumulation.
Suggested change
FieldRef := RecordRef.Field(IntermediateDataImport."Field ID");
DataExchField.SetRange("Data Exch. No.", DataExch."Entry No.");
DataExchField.SetRange("Column No.", ValueColNo);
if DataExchField.FindFirst() then
EndpointValue := DataExchField.Value;
DataExchField.SetRange("Column No.", SchemeColNo); // Column No. is reset; Data Exch. No. stays
if DataExchField.FindFirst() then
EndpointScheme := DataExchField.Value;

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


DataExchField.SetRange("Data Exch. No.", DataExch."Entry No.");
DataExchField.SetRange("Column No.", ValueColNo);
if DataExchField.FindFirst() 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\ —\ Performance} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

MapIntermediateToLines doesn't reset RecordRef between records

MapIntermediateToLines uses a RecordRef obtained from a new EDocumentPurchaseLine record but never calls Clear() or reinitialises the RecordRef when processing a new Record No. If a field was set in a previous iteration and the current iteration's intermediate data omits that field, the stale value from the previous line will silently carry forward.

Recommendation:

  • For each new Record No., initialise a fresh EDocumentPurchaseLine and reset the RecordRef before assigning fields.
Suggested change
if DataExchField.FindFirst() then
if CurrRecordNo <> IntermediateDataImport."Record No." then begin
if CurrRecordNo <> 0 then begin
RecordRef.SetTable(EDocumentPurchaseLine);
EDocumentPurchaseLine.Insert();
end;
CurrRecordNo := IntermediateDataImport."Record No.";
Clear(EDocumentPurchaseLine);
EDocumentPurchaseLine."E-Document Entry No." := EDocument."Entry No.";
RecordRef.GetTable(EDocumentPurchaseLine);
end;

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

if CurrRecordNo <> -1 then
if FileName <> '' then begin
AttachmentTempBlob.CreateInStream(InStream);
EDocAttachmentProcessor.Insert(EDocument, InStream, FileName);
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\ —\ Performance} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

AttachmentTempBlob not reset between attachment records

In ProcessAttachments, the AttachmentTempBlob variable is never cleared between iterations of different Record No. groups. If one attachment record writes blob data and a subsequent attachment record sets only the FileName (e.g. the Document Reference ID field is missing or processed later), the new attachment will be associated with the previous record's binary data.

Recommendation:

  • Reset AttachmentTempBlob at the start of each new record group by declaring a new variable or explicitly calling Clear(AttachmentTempBlob).
Suggested change
EDocAttachmentProcessor.Insert(EDocument, InStream, FileName);
if CurrRecordNo <> IntermediateDataImport."Record No." then begin
// flush previous
if (CurrRecordNo <> -1) and (FileName <> '') then begin
AttachmentTempBlob.CreateInStream(InStream);
EDocAttachmentProcessor.Insert(EDocument, InStream, FileName);
end;
CurrRecordNo := IntermediateDataImport."Record No.";
FileName := '';
Clear(AttachmentTempBlob); // reset blob for new record
end;

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

IntermediateDataImport: Record "Intermediate Data Import";
begin
DataExchField.SetRange("Data Exch. No.", DataExch."Entry No.");
DataExchField.DeleteAll();
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\ —\ Performance} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

DeleteIntermediateData called only on success path

DeleteIntermediateData is the last call in RunPipelineAndBridge and cleans up DataExchField and IntermediateDataImport rows. If BridgeToStagingTables throws an error, this cleanup is never reached, leaving intermediate import rows in the database indefinitely.

Recommendation:

  • Ensure DeleteIntermediateData is called even on failure, either by wrapping the pipeline in a try-function with finally-equivalent cleanup, or by calling it inside an error handler.
Suggested change
DataExchField.DeleteAll();
local procedure RunPipelineAndBridge(...)
begin
// ... setup ...
if not TryRunBridge(EDocument, DataExch, DataExchDef) then begin
DeleteIntermediateData(DataExch);
DataExch.Delete(true);
Error(GetLastErrorText());
end;
DeleteIntermediateData(DataExch);
end;

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

end;
end;
until IntermediateDataImport.Next() = 0;

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\ —\ Security} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Base64 decoded attachment data not validated

In ProcessAttachments, the Base64-decoded attachment data from IntermediateDataImport.GetValue() is decoded and inserted directly via EDocAttachmentProcessor.Insert without any validation of file type, size, or content. A malformed or oversized attachment embedded in an e-document XML could cause unexpected behavior or storage issues.

Recommendation:

  • Add a size check on the decoded blob and consider validating the file extension against an allowlist before inserting the attachment.
Suggested change
// After decoding:
Base64Convert.FromBase64(Base64Data, OutStream);
// Validate size (example: 10 MB max)
AttachmentTempBlob.CreateInStream(InStream);
if AttachmentTempBlob.Length() > 10 * 1024 * 1024 then
Error('Attachment exceeds maximum allowed size.');

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

RunPipelineAndBridge(EDocument, TempBlob, BestDefCode);
exit(MapDocumentTypeToProcessDraft(BestDocType));
end;

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}}$

View() uses hardcoded non-localized error string

Error('A view is not implemented for this handler.') uses a string literal instead of a labeled text constant. This is not translatable and violates AL localization requirements — all user-facing strings must be declared as Label constants.

Recommendation:

  • Declare the message as a locked or translatable Label and reference it in the Error call.
Suggested change
var
ViewNotImplementedErr: Label 'A view is not implemented for this handler.', Locked = true;
procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob")
begin
Error(ViewNotImplementedErr);
end;

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

@github-actions
Copy link
Copy Markdown
Contributor

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

Error label missing translator Comment

ProcessFailedErr: Label 'Failed to process the file with data exchange.' has no Comment property. Translators need context comments to understand where and why the message appears, especially for technical messages involving "data exchange".

Recommendation:

  • Add a Comment parameter to the Label declaration to describe the context for translators.
ProcessFailedErr: Label 'Failed to process the file with data exchange.', Comment = 'Error shown when PEPPOL BIS 3 document processing via Data Exchange fails — shown to the user in the E-Document processing page.';

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

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

ResInStream: InStream;
begin
if DataExchDef.Get('EDOCPEPINVIMPV2') then
DataExchDef.Delete(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\ 1}}$

DataExchDef deleted before re-import without rollback

ImportInvoiceV2XML (and ImportCreditMemoV2XML) unconditionally delete the existing Data Exchange Definition record before reimporting it. If NavApp.GetResource or Xmlport.Import subsequently fails, the definition is permanently destroyed — there is no rollback or restore mechanism.

Recommendation:

  • Only delete the old definition after a successful import, or use a transaction-safe pattern: import into a temp-check first, then replace. Alternatively, check whether a delete-and-reimport is actually needed at install time vs. a conditional insert.
Suggested change
DataExchDef.Delete(true);
// Do not pre-delete; let the Xmlport handle upsert, or:
// 1. Import into a temporary staging area
// 2. Only delete existing definition if staging import succeeded
if DataExchDef.Get('EDOCPEPINVIMPV2') then begin
NavApp.GetResource('DataExchange/eDocPEPPOLInvoiceImportV2.xml', ResInStream);
// ... import ...
DataExchDef.Delete(true); // delete AFTER successful import
end;

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

@github-actions
Copy link
Copy Markdown
Contributor

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

Field 40 renamed without deprecation

Field 40 on table "E-Document Purchase Header" was renamed from "Vendor Invoice No." to "Applies-to Ext. Invoice No." without an intermediate ObsoleteState::Pending period. Any extension referencing the old name — including the existing EDocumentPEPPOLHandler before this PR — will break at compile time.

Recommendation:

  • Introduce ObsoleteState = Pending on the old field and add a new field with the new name and number, or if this table is still in preview, document the breaking change explicitly in release notes and ensure all callers in the same PR are updated.
field(40; "Vendor Invoice No."; Text[100])
{
    ObsoleteState = Pending;
    ObsoleteReason = 'Use "Applies-to Ext. Invoice No." instead.';
    ObsoleteTag = '26.0';
}
field(41; "Applies-to Ext. Invoice No."; Text[100])
{
    Caption = 'Applies-to Ext. Invoice No.';
}

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

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

Implementation = IStructuredFormatReader = "E-Document MLLM Handler";
}
value(5; "PEPPOL Data Exchange")
{
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\ 1}}$

New enum value ordinal may conflict with extensions

A new enum value value(5; "PEPPOL Data Exchange") is inserted into "E-Doc. Read into Draft". If any partner extension has already added a value with ordinal 5 to this enum, the two definitions will conflict at compile time. There is no way for Microsoft to know this without checking the extension ecosystem.

Recommendation:

  • Use a higher, partner-unlikely ordinal (e.g. 100+ or following an established spacing convention) for platform-reserved new enum values, or document the ordinal reservation scheme so partners know which ranges to avoid.
Suggested change
{
// Consider using a higher ordinal to avoid collision:
value(100; "PEPPOL Data Exchange")
{
Caption = 'PEPPOL BIS 3 - Data Exchange';
Implementation = IStructuredFormatReader = "E-Doc. PEPPOL DX Handler";
}

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AL: Apps (W1) Add-on apps for W1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants