[E-Document] Data Exchange v2 import handler (bridge pattern)#7565
[E-Document] Data Exchange v2 import handler (bridge pattern)#7565Groenbech96 wants to merge 34 commits into
Conversation
…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>
…line compatibility
… 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
There was a problem hiding this comment.
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.
…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.
There was a problem hiding this comment.
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.
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>
There was a problem hiding this comment.
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.
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>
There was a problem hiding this comment.
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.
…shar/edoc-import-v2-data-exchange
| FieldValue := CopyStr(IntermediateDataImport.GetValue(), 1, GetFieldMaxLength(FieldRef)); | ||
| if FieldValue <> '' then | ||
| AssignFieldValue(FieldRef, FieldValue); | ||
| RecordRef.SetTable(EDocumentPurchaseLine); |
There was a problem hiding this comment.
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.SetTablecall to only occur once per record group boundary (after the loop or at the start of a new record), rather than per-field. The intermediateGetTableafter each field assignment can be replaced by working entirely withFieldRefuntil the record needs to be committed.
| 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"; |
There was a problem hiding this comment.
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 callingCreateOutStream, to ensure each attachment starts with a clean blob.
| 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); |
There was a problem hiding this comment.
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 thePurchaseHeader) before callingFindFirst()to restrict the lookup to the correct vendor.
| 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.'); |
There was a problem hiding this comment.
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
Labelvariable for the error message and reference it in theErrorcall.
| 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]) |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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, callDataExch.Delete(true)in an error handler so intermediate data is rolled back or cleaned up.
| 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
FindFirst without index on Vendor Invoice No.
Recommendation:
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); |
There was a problem hiding this comment.
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.
| 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"); |
There was a problem hiding this comment.
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 explicitlyDataExchField.SetRange("Data Exch. No.", DataExch."Entry No.")before the secondSetRange("Column No.", SchemeColNo)to avoid unexpected filter accumulation.
| 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 |
There was a problem hiding this comment.
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
EDocumentPurchaseLineand reset the RecordRef before assigning fields.
| 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); |
There was a problem hiding this comment.
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
AttachmentTempBlobat the start of each new record group by declaring a new variable or explicitly callingClear(AttachmentTempBlob).
| 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(); |
There was a problem hiding this comment.
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
DeleteIntermediateDatais 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.
| 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; | ||
|
|
There was a problem hiding this comment.
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.
| // 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; | ||
|
|
There was a problem hiding this comment.
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.
| 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
Error label missing translator Comment
Recommendation:
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); |
There was a problem hiding this comment.
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.
| 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
Field 40 renamed without deprecationField 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:
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") | ||
| { |
There was a problem hiding this comment.
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.
| { | |
| // 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
Summary
IStructuredFormatReaderimplementation for Data Exchange format (codeunit 6407)E-Doc. Read into DraftE-Document Purchase Header/E-Document Purchase Linevia field-ID bridgeNew codeunit
Key design decisions
Test plan
AB#630822
AB#599123