From 2d3bdf3587e273bdf60431f2d819b01ac1b0275d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Thu, 23 Jul 2026 10:06:38 +0200 Subject: [PATCH 1/4] [E-Document Formats][OIOUBL] Migrate NAV PR 247162 into BCApps Adds the OIOUBL (Danish UBL) structured import reader to the E-Document V2.0 pipeline, mirroring internal NAV PR 247162 (which itself mirrors ALAppExtensions #29056, Invoice + CreditNote only). Follows the same pattern as the PINT A-NZ / Factura-E migration (#9426). Changes (src/Apps/DK/EDocumentFormatOIOUBL): - E-Document OIOUBL Handler: new IStructuredFormatReader.ReadIntoDraft implementation that parses OIOUBL Invoice/CreditNote XML into the purchase draft staging tables. Vendor lookup via VAT reg. no., GLN, or PEPPOL participant id. - OIOUBL EDoc Read into Draft: enum extension registering the handler on "E-Doc. Read into Draft". - Test app with structured integration tests, validation codeunit and OIOUBL invoice test resource. E-Document Core (src/Apps/W1/EDocument): grant internalsVisibleTo to the new "E-Document Format for OIOUBL Tests" app. Adaptations from the NAV source: - Uses the public "E-Document PEPPOL Utility" helper (BCApps) instead of NAV's "EDocument XML Helper"; View() uses temporary buffer records for the now-public SetBuffer. - Test app depends on "E-Document Core Tests"; the duplicate format-mock files and NAV-only Eng/Core/Build registrations are not mirrored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/EDocumentOIOUBLHandler.Codeunit.al | 334 ++++++++++++++++++ .../src/OIOUBLEDocReadIntoDraft.EnumExt.al | 17 + .../.resources/oioubl/oioubl-invoice-0.xml | 201 +++++++++++ .../test/ExtensionLogo.png | Bin 0 -> 5446 bytes .../DK/EDocumentFormatOIOUBL/test/app.json | 72 ++++ .../src/EDocumentStructuredTests.Codeunit.al | 276 +++++++++++++++ .../OIOUBLStructuredValidations.Codeunit.al | 111 ++++++ src/Apps/W1/EDocument/App/app.json | 5 + 8 files changed, 1016 insertions(+) create mode 100644 src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al create mode 100644 src/Apps/DK/EDocumentFormatOIOUBL/app/src/OIOUBLEDocReadIntoDraft.EnumExt.al create mode 100644 src/Apps/DK/EDocumentFormatOIOUBL/test/.resources/oioubl/oioubl-invoice-0.xml create mode 100644 src/Apps/DK/EDocumentFormatOIOUBL/test/ExtensionLogo.png create mode 100644 src/Apps/DK/EDocumentFormatOIOUBL/test/app.json create mode 100644 src/Apps/DK/EDocumentFormatOIOUBL/test/src/EDocumentStructuredTests.Codeunit.al create mode 100644 src/Apps/DK/EDocumentFormatOIOUBL/test/src/OIOUBLStructuredValidations.Codeunit.al diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al b/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al new file mode 100644 index 00000000000..694086cacb6 --- /dev/null +++ b/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al @@ -0,0 +1,334 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument; + +using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.eServices.EDocument.Processing.Import; +using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.eServices.EDocument.Service.Participant; +using Microsoft.Purchases.Vendor; +using System.Utilities; + +codeunit 13913 "E-Document OIOUBL Handler" implements IStructuredFormatReader +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + GLNTok: Label 'GLN', Locked = true; + InvoiceLineTok: Label 'cac:InvoiceLine', Locked = true; + CreditNoteLineTok: Label 'cac:CreditNoteLine', Locked = true; + + /// + /// Reads an OIOUBL format XML document and converts it into a draft purchase document. + /// This procedure processes Invoice and CreditNote document types and populates the E-Document Purchase Header with the extracted data. + /// + /// The E-Document record that contains the document metadata and information. + /// A temporary blob containing the XML document stream to be processed. + /// Returns an enum indicating that the process resulted in a purchase document draft. + internal procedure ReadIntoDraft(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"): Enum "E-Doc. Process Draft" + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + OIOUBLXml: XmlDocument; + XmlNamespaces: XmlNamespaceManager; + XmlElement: XmlElement; + CommonAggregateComponentsTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', Locked = true; + CommonBasicComponentsTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', Locked = true; + DefaultInvoiceTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', Locked = true; + DefaultCreditNoteTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2', Locked = true; + begin + EDocumentPurchaseHeader.InsertForEDocument(EDocument); + + XmlDocument.ReadFrom(TempBlob.CreateInStream(TextEncoding::UTF8), OIOUBLXml); + + XmlNamespaces.AddNamespace('cac', CommonAggregateComponentsTok); + XmlNamespaces.AddNamespace('cbc', CommonBasicComponentsTok); + XmlNamespaces.AddNamespace('inv', DefaultInvoiceTok); + XmlNamespaces.AddNamespace('cn', DefaultCreditNoteTok); + + OIOUBLXml.GetRoot(XmlElement); + case UpperCase(XmlElement.LocalName()) of + 'INVOICE': + PopulateEDocumentForInvoice(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader, EDocument); + 'CREDITNOTE': + PopulateEDocumentForCreditNote(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader, EDocument); + end; + + EDocumentPurchaseHeader.Modify(false); + EDocument.Direction := EDocument.Direction::Incoming; + exit(Enum::"E-Doc. Process Draft"::"Purchase Document"); + end; + + /// + /// Displays a readable view of the processed E-Document purchase information. + /// This procedure opens a page showing the purchase header and lines in a user-friendly format for review. + /// + /// The E-Document record that contains the document to be displayed. + /// A temporary blob containing the document data (not used in current implementation). + internal procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob") + var + EDocPurchaseHeader: Record "E-Document Purchase Header"; + EDocPurchaseLine: Record "E-Document Purchase Line"; + TempEDocPurchaseHeader: Record "E-Document Purchase Header" temporary; + TempEDocPurchaseLine: Record "E-Document Purchase Line" temporary; + EDocReadablePurchaseDoc: Page "E-Doc. Readable Purchase Doc."; + begin + EDocPurchaseHeader.GetFromEDocument(EDocument); + TempEDocPurchaseHeader := EDocPurchaseHeader; + TempEDocPurchaseHeader.Insert(); + + EDocPurchaseLine.SetRange("E-Document Entry No.", EDocPurchaseHeader."E-Document Entry No."); + if EDocPurchaseLine.FindSet() then + repeat + TempEDocPurchaseLine := EDocPurchaseLine; + TempEDocPurchaseLine.Insert(); + until EDocPurchaseLine.Next() = 0; + + EDocReadablePurchaseDoc.SetBuffer(TempEDocPurchaseHeader, TempEDocPurchaseLine); + EDocReadablePurchaseDoc.Run(); + end; + + local procedure PopulateEDocumentForInvoice(OIOUBLXml: XmlDocument; XmlNamespaces: XmlNamespaceManager; var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; var EDocument: Record "E-Document") + var + EDocumentXMLHelper: Codeunit "E-Document PEPPOL Utility"; + VendorNo: Code[20]; + begin + EDocumentPurchaseHeader."E-Document Type" := "E-Document Type"::"Purchase Invoice"; +#pragma warning disable AA0139 + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Sales Invoice No."), EDocumentPurchaseHeader."Sales Invoice No."); + EDocumentXMLHelper.SetDateValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cbc:IssueDate', EDocumentPurchaseHeader."Document Date"); + EDocumentXMLHelper.SetDateValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cbc:DueDate', EDocumentPurchaseHeader."Due Date"); + EDocumentXMLHelper.SetCurrencyValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cbc:DocumentCurrencyCode', MaxStrLen(EDocumentPurchaseHeader."Currency Code"), EDocumentPurchaseHeader."Currency Code"); + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cac:OrderReference/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Purchase Order No."), EDocumentPurchaseHeader."Purchase Order No."); +#pragma warning restore AA0139 + EDocumentXMLHelper.SetNumberValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount', EDocumentPurchaseHeader."Sub Total"); + EDocumentXMLHelper.SetNumberValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount', EDocumentPurchaseHeader.Total); + EDocumentXMLHelper.SetNumberValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cac:LegalMonetaryTotal/cbc:PayableAmount', EDocumentPurchaseHeader."Amount Due"); + EDocumentPurchaseHeader."Total VAT" := EDocumentPurchaseHeader."Total" - EDocumentPurchaseHeader."Sub Total" - EDocumentPurchaseHeader."Total Discount"; + VendorNo := ParseAccountingSupplierParty(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader, EDocument, 'inv:Invoice'); + ParseAccountingCustomerParty(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader, 'inv:Invoice'); + if VendorNo <> '' then + EDocumentPurchaseHeader."[BC] Vendor No." := VendorNo; + InsertOIOUBLPurchaseLines(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader."E-Document Entry No.", EDocumentPurchaseHeader."E-Document Type"); + end; + + local procedure PopulateEDocumentForCreditNote(OIOUBLXml: XmlDocument; XmlNamespaces: XmlNamespaceManager; var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; var EDocument: Record "E-Document") + var + EDocumentXMLHelper: Codeunit "E-Document PEPPOL Utility"; + VendorNo: Code[20]; + begin + EDocumentPurchaseHeader."E-Document Type" := "E-Document Type"::"Purchase Credit Memo"; +#pragma warning disable AA0139 + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Sales Invoice No."), EDocumentPurchaseHeader."Sales Invoice No."); + EDocumentXMLHelper.SetDateValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cbc:IssueDate', EDocumentPurchaseHeader."Document Date"); + EDocumentXMLHelper.SetDateValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cbc:DueDate', EDocumentPurchaseHeader."Due Date"); + EDocumentXMLHelper.SetCurrencyValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cbc:DocumentCurrencyCode', MaxStrLen(EDocumentPurchaseHeader."Currency Code"), EDocumentPurchaseHeader."Currency Code"); + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cac:OrderReference/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Purchase Order No."), EDocumentPurchaseHeader."Purchase Order No."); + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Applies-to Doc. No."), EDocumentPurchaseHeader."Applies-to Doc. No."); +#pragma warning restore AA0139 + EDocumentXMLHelper.SetNumberValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount', EDocumentPurchaseHeader."Sub Total"); + EDocumentXMLHelper.SetNumberValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount', EDocumentPurchaseHeader.Total); + EDocumentXMLHelper.SetNumberValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cac:LegalMonetaryTotal/cbc:PayableAmount', EDocumentPurchaseHeader."Amount Due"); + EDocumentPurchaseHeader."Total VAT" := EDocumentPurchaseHeader."Total" - EDocumentPurchaseHeader."Sub Total" - EDocumentPurchaseHeader."Total Discount"; + VendorNo := ParseAccountingSupplierParty(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader, EDocument, 'cn:CreditNote'); + ParseAccountingCustomerParty(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader, 'cn:CreditNote'); + if VendorNo <> '' then + EDocumentPurchaseHeader."[BC] Vendor No." := VendorNo; + InsertOIOUBLPurchaseLines(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader."E-Document Entry No.", EDocumentPurchaseHeader."E-Document Type"); + end; + + local procedure ParseAccountingSupplierParty(OIOUBLXml: XmlDocument; XmlNamespaces: XmlNamespaceManager; var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; var EDocument: Record "E-Document"; DocumentType: Text) VendorNo: Code[20] + var + EDocumentXMLHelper: Codeunit "E-Document PEPPOL Utility"; + EDocumentImportHelper: Codeunit "E-Document Import Helper"; + DKCVRTok: Label 'DK:CVR', Locked = true; + VendorName, VendorAddress, VendorParticipantId : Text; + VATRegistrationNo: Text[20]; + EndpointID, SchemeID : Text; + GLN: Code[13]; + BasePathTxt: Text; + XMLNode: XmlNode; + begin +#pragma warning disable AA0139 // false positive: overflow handled by SetStringValueInField + BasePathTxt := '/' + DocumentType + '/cac:AccountingSupplierParty/cac:Party'; + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, BasePathTxt + '/cac:PartyName/cbc:Name', MaxStrLen(EDocumentPurchaseHeader."Vendor Company Name"), EDocumentPurchaseHeader."Vendor Company Name"); + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, BasePathTxt + '/cac:PostalAddress/cbc:StreetName', MaxStrLen(EDocumentPurchaseHeader."Vendor Address"), EDocumentPurchaseHeader."Vendor Address"); + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, BasePathTxt + '/cac:PostalAddress/cbc:AdditionalStreetName', MaxStrLen(EDocumentPurchaseHeader."Vendor Address Recipient"), EDocumentPurchaseHeader."Vendor Address Recipient"); + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, BasePathTxt + '/cac:PartyTaxScheme/cbc:CompanyID', MaxStrLen(EDocumentPurchaseHeader."Vendor VAT Id"), EDocumentPurchaseHeader."Vendor VAT Id"); + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, BasePathTxt + '/cac:Contact/cbc:Name', MaxStrLen(EDocumentPurchaseHeader."Vendor Contact Name"), EDocumentPurchaseHeader."Vendor Contact Name"); +#pragma warning restore AA0139 + if OIOUBLXml.SelectSingleNode(BasePathTxt + '/cbc:EndpointID/@schemeID', XmlNamespaces, XMLNode) then begin + SchemeID := XMLNode.AsXmlAttribute().Value(); + EndpointID := EDocumentXMLHelper.GetNodeValue(OIOUBLXml, XmlNamespaces, BasePathTxt + '/cbc:EndpointID'); + case SchemeID of + DKCVRTok: + VATRegistrationNo := CopyStr(EndpointID, 1, MaxStrLen(VATRegistrationNo)); + GLNTok: + begin + GLN := CopyStr(EndpointID, 1, MaxStrLen(GLN)); + EDocumentPurchaseHeader."Vendor GLN" := GLN; + end; + end; + VendorParticipantId := SchemeID + ':' + EndpointID; + end; + VATRegistrationNo := CopyStr(EDocumentPurchaseHeader."Vendor VAT Id", 1, MaxStrLen(VATRegistrationNo)); + VendorName := EDocumentPurchaseHeader."Vendor Company Name"; + VendorAddress := EDocumentPurchaseHeader."Vendor Address"; + if not FindVendorByVATRegNoOrGLN(VendorNo, VATRegistrationNo, GLN) then + if not FindVendorByParticipantId(VendorNo, EDocument, VendorParticipantId) then + VendorNo := EDocumentImportHelper.FindVendorByNameAndAddress(VendorName, VendorAddress); + end; + + local procedure ParseAccountingCustomerParty(OIOUBLXml: XmlDocument; XmlNamespaces: XmlNamespaceManager; var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; DocumentType: Text) + var + EDocumentXMLHelper: Codeunit "E-Document PEPPOL Utility"; + BasePathTxt: Text; + XMLNode: XmlNode; + SchemeID, EndpointID : Text; + begin +#pragma warning disable AA0139 // false positive: overflow handled by SetStringValueInField + BasePathTxt := '/' + DocumentType + '/cac:AccountingCustomerParty/cac:Party'; + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, BasePathTxt + '/cac:PartyName/cbc:Name', MaxStrLen(EDocumentPurchaseHeader."Customer Company Name"), EDocumentPurchaseHeader."Customer Company Name"); + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, BasePathTxt + '/cac:PostalAddress/cbc:StreetName', MaxStrLen(EDocumentPurchaseHeader."Customer Address"), EDocumentPurchaseHeader."Customer Address"); + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, BasePathTxt + '/cac:PostalAddress/cbc:AdditionalStreetName', MaxStrLen(EDocumentPurchaseHeader."Customer Address Recipient"), EDocumentPurchaseHeader."Customer Address Recipient"); + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, BasePathTxt + '/cac:PartyTaxScheme/cbc:CompanyID', MaxStrLen(EDocumentPurchaseHeader."Customer VAT Id"), EDocumentPurchaseHeader."Customer VAT Id"); +#pragma warning restore AA0139 + if OIOUBLXml.SelectSingleNode(BasePathTxt + '/cbc:EndpointID/@schemeID', XmlNamespaces, XMLNode) then begin + SchemeID := XMLNode.AsXmlAttribute().Value(); + EndpointID := EDocumentXMLHelper.GetNodeValue(OIOUBLXml, XmlNamespaces, BasePathTxt + '/cbc:EndpointID'); + if SchemeID = GLNTok then + EDocumentPurchaseHeader."Customer GLN" := CopyStr(EndpointID, 1, MaxStrLen(EDocumentPurchaseHeader."Customer GLN")); + end; + end; + + local procedure InsertOIOUBLPurchaseLines(OIOUBLXml: XmlDocument; XmlNamespaces: XmlNamespaceManager; EDocumentEntryNo: Integer; DocumentType: Enum "E-Document Type") + var + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + NewLineXML: XmlDocument; + LineXMLList: XmlNodeList; + LineXMLNode: XmlNode; + LineXPath: Text; + LineElementName: Text; + begin + case DocumentType of + "E-Document Type"::"Purchase Invoice": + begin + LineXPath := '/inv:Invoice/cac:InvoiceLine'; + LineElementName := InvoiceLineTok; + end; + "E-Document Type"::"Purchase Credit Memo": + begin + LineXPath := '/cn:CreditNote/cac:CreditNoteLine'; + LineElementName := CreditNoteLineTok; + end; + end; + + if not OIOUBLXml.SelectNodes(LineXPath, XmlNamespaces, LineXMLList) then + exit; + + foreach LineXMLNode in LineXMLList do begin + Clear(EDocumentPurchaseLine); + EDocumentPurchaseLine.Validate("E-Document Entry No.", EDocumentEntryNo); + EDocumentPurchaseLine."Line No." := EDocumentPurchaseLine.GetNextLineNo(EDocumentEntryNo); + NewLineXML.ReplaceNodes(LineXMLNode); + + PopulateOIOUBLPurchaseLine(NewLineXML, XmlNamespaces, EDocumentPurchaseLine, LineElementName); + EDocumentPurchaseLine.Insert(false); + end; + end; + + local procedure PopulateOIOUBLPurchaseLine(LineXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; var EDocumentPurchaseLine: Record "E-Document Purchase Line"; LineElementName: Text) + var + EDocumentXMLHelper: Codeunit "E-Document PEPPOL Utility"; + LineIdFieldName: Text; + QuantityFieldName: Text; + begin +#pragma warning disable AA0139 // false positive: overflow handled by SetStringValueInField + case LineElementName of + InvoiceLineTok: + begin + LineIdFieldName := 'cac:InvoiceLine/cac:Item/cac:SellersItemIdentification/cbc:ID'; + QuantityFieldName := 'cac:InvoiceLine/cbc:InvoicedQuantity'; + end; + CreditNoteLineTok: + begin + LineIdFieldName := 'cac:CreditNoteLine/cac:Item/cac:SellersItemIdentification/cbc:ID'; + QuantityFieldName := 'cac:CreditNoteLine/cbc:CreditedQuantity'; + end; + end; + + EDocumentXMLHelper.SetStringValueInField(LineXML, XmlNamespaces, LineIdFieldName, MaxStrLen(EDocumentPurchaseLine."Product Code"), EDocumentPurchaseLine."Product Code"); + EDocumentXMLHelper.SetStringValueInField(LineXML, XmlNamespaces, LineElementName + '/cac:Item/cbc:Name', MaxStrLen(EDocumentPurchaseLine.Description), EDocumentPurchaseLine.Description); + EDocumentXMLHelper.SetStringValueInField(LineXML, XmlNamespaces, LineElementName + '/cac:Item/cac:SellersItemIdentification/cbc:ID', MaxStrLen(EDocumentPurchaseLine."Product Code"), EDocumentPurchaseLine."Product Code"); + EDocumentXMLHelper.SetStringValueInField(LineXML, XmlNamespaces, LineElementName + '/cac:Item/cac:StandardItemIdentification/cbc:ID', MaxStrLen(EDocumentPurchaseLine."Product Code"), EDocumentPurchaseLine."Product Code"); + EDocumentXMLHelper.SetNumberValueInField(LineXML, XmlNamespaces, QuantityFieldName, EDocumentPurchaseLine.Quantity); + EDocumentXMLHelper.SetStringValueInField(LineXML, XmlNamespaces, QuantityFieldName + '/@unitCode', MaxStrLen(EDocumentPurchaseLine."Unit of Measure"), EDocumentPurchaseLine."Unit of Measure"); + EDocumentXMLHelper.SetNumberValueInField(LineXML, XmlNamespaces, LineElementName + '/cac:Price/cbc:PriceAmount', EDocumentPurchaseLine."Unit Price"); + EDocumentXMLHelper.SetNumberValueInField(LineXML, XmlNamespaces, LineElementName + '/cbc:LineExtensionAmount', EDocumentPurchaseLine."Sub Total"); + EDocumentXMLHelper.SetCurrencyValueInField(LineXML, XmlNamespaces, LineElementName + '/cbc:LineExtensionAmount/@currencyID', MaxStrLen(EDocumentPurchaseLine."Currency Code"), EDocumentPurchaseLine."Currency Code"); + EDocumentXMLHelper.SetNumberValueInField(LineXML, XmlNamespaces, LineElementName + '/cac:Item/cac:ClassifiedTaxCategory/cbc:Percent', EDocumentPurchaseLine."VAT Rate"); +#pragma warning restore AA0139 + end; + + local procedure FindVendorByVATRegNoOrGLN(var VendorNo: Code[20]; VATRegistrationNo: Text[20]; GLN: Code[13]): Boolean + var + Vendor: Record Vendor; + begin + if VATRegistrationNo <> '' then begin + Vendor.Reset(); + Vendor.SetLoadFields("VAT Registration No."); + Vendor.SetRange("VAT Registration No.", VATRegistrationNo); + if Vendor.FindFirst() then begin + VendorNo := Vendor."No."; + exit(true); + end; + end; + + // Try to find vendor by GLN + if GLN <> '' then begin + Vendor.Reset(); + Vendor.SetLoadFields("GLN"); + Vendor.SetRange("GLN", GLN); + if Vendor.FindFirst() then begin + VendorNo := Vendor."No."; + exit(true); + end; + end; + + exit(false); + end; + + local procedure FindVendorByParticipantId(var VendorNo: Code[20]; EDocument: Record "E-Document"; ParticipantId: Text): Boolean + var + EDocServiceParticipant: Record "Service Participant"; + EDocumentService: Record "E-Document Service"; + EDocumentHelper: Codeunit "E-Document Helper"; + begin + if ParticipantId = '' then + exit(false); + + EDocumentHelper.GetEdocumentService(EDocument, EDocumentService); + EDocServiceParticipant.SetRange("Participant Type", EDocServiceParticipant."Participant Type"::Vendor); + EDocServiceParticipant.SetRange("Participant Identifier", ParticipantId); + EDocServiceParticipant.SetRange(Service, EDocumentService.Code); + if not EDocServiceParticipant.FindFirst() then begin + EDocServiceParticipant.SetRange(Service); + if not EDocServiceParticipant.FindFirst() then + exit(false); + end; + + VendorNo := EDocServiceParticipant.Participant; + exit(true); + end; + + procedure ResetDraft(EDocument: Record "E-Document") + var + EDocPurchaseHeader: Record "E-Document Purchase Header"; + begin + EDocPurchaseHeader.GetFromEDocument(EDocument); + EDocPurchaseHeader.Delete(true); + end; +} diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/app/src/OIOUBLEDocReadIntoDraft.EnumExt.al b/src/Apps/DK/EDocumentFormatOIOUBL/app/src/OIOUBLEDocReadIntoDraft.EnumExt.al new file mode 100644 index 00000000000..7f3be0041a2 --- /dev/null +++ b/src/Apps/DK/EDocumentFormatOIOUBL/app/src/OIOUBLEDocReadIntoDraft.EnumExt.al @@ -0,0 +1,17 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument; + +using Microsoft.eServices.EDocument.Processing.Import; +using Microsoft.eServices.EDocument.Processing.Interfaces; + +enumextension 13911 "OIOUBL EDoc Read into Draft" extends "E-Doc. Read into Draft" +{ + value(13910; "OIOUBL") + { + Caption = 'OIOUBL'; + Implementation = IStructuredFormatReader = "E-Document OIOUBL Handler"; + } +} diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/test/.resources/oioubl/oioubl-invoice-0.xml b/src/Apps/DK/EDocumentFormatOIOUBL/test/.resources/oioubl/oioubl-invoice-0.xml new file mode 100644 index 00000000000..72316a124bc --- /dev/null +++ b/src/Apps/DK/EDocumentFormatOIOUBL/test/.resources/oioubl/oioubl-invoice-0.xml @@ -0,0 +1,201 @@ + + + 2.0 + OIOUBL-2.01 + Procurement-OrdSimR-BilSim-1.0 + 103033 + false + 01d4dd7d-ac2b-460d-ae44-5f20d540952a + 2026-01-22 + 12:00:00 + 2026-02-22 + 380 + XYZ + + 2 + false + 9756b468-8815-1029-857a-e388fe63f399 + 2025-07-20 + 12:00:00 + + + + GB123456789 + + GB123456789 + + + CRONUS International + + + StructuredDK + Main Street, 14 + Birmingham + B27 4KT + + DK + + + + GB123456789 + + 63 + Moms + + + + CRONUS International + GB123456789 + + + Jim Olive + JO@contoso.com + + + + + + GB789456278 + + GB789456278 + + + The Cannon Group PLC + + + StructuredDK + 192 Market Square + Birmingham + B27 4KT + + DK + + + + GB789456278 + + 63 + Moms + + + + The Cannon Group PLC + GB789456278 + + + Contact Person + mr.andy.teal@cannongroup.com + + + + + 1000.00 + + 14000.00 + 1000.00 + + StandardRated + 25 + + 63 + Moms + + + + + + 14000.00 + 14000.00 + 14140.00 + 0 + 0.00 + 0 + 14140.00 + + + 10000 + Vare + 1 + 4000.00 + + 1000.00 + + 4000.00 + 1000.00 + + StandardRated + 25 + + 63 + Moms + + + + + + Bicycle + Bicycle + + 1000 + + + 1000 + + + StandardRated + 25 + + 63 + Moms + + + + + 4000.00 + 1 + + + + 20000 + Vare + 2 + 10000.00 + + 0.00 + + 10000.00 + 0.00 + + StandardRated + 25 + + 63 + Moms + + + + + + Bicycle v2 + Bicycle v2 + + 2000 + + + 2000 + + + StandardRated + 25 + + 63 + Moms + + + + + 5000.00 + 1 + + + \ No newline at end of file diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/test/ExtensionLogo.png b/src/Apps/DK/EDocumentFormatOIOUBL/test/ExtensionLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..4d2c9a626cb9617350617c40cd73904129d4c108 GIT binary patch literal 5446 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 literal 0 HcmV?d00001 diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/test/app.json b/src/Apps/DK/EDocumentFormatOIOUBL/test/app.json new file mode 100644 index 00000000000..3cb2e93002c --- /dev/null +++ b/src/Apps/DK/EDocumentFormatOIOUBL/test/app.json @@ -0,0 +1,72 @@ +{ + "id": "f78b9487-b911-4db1-893b-5a5c310919b8", + "name": "E-Document Format for OIOUBL Tests", + "publisher": "Microsoft", + "brief": "E-Document Format for OIOUBL Tests.", + "description": "Tests for E-Document Format for OIOUBL.", + "version": "29.0.0.0", + "privacyStatement": "https://go.microsoft.com/fwlink/?LinkId=724009", + "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", + "help": "https://go.microsoft.com/fwlink/?linkid=2299409", + "url": "https://go.microsoft.com/fwlink/?LinkId=724011", + "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2299409", + "logo": "ExtensionLogo.png", + "dependencies": [ + { + "id": "e1d97edc-c239-46b4-8d84-6368bdf67c8b", + "name": "E-Document Core", + "publisher": "Microsoft", + "version": "29.0.0.0" + }, + { + "id": "e1d97edc-c239-46b4-8d84-6368bdf67c8c", + "name": "E-Document Core Tests", + "publisher": "Microsoft", + "version": "29.0.0.0" + }, + { + "id": "5d86850b-0d76-4eca-bd7b-951ad998e997", + "name": "Tests-TestLibraries", + "publisher": "Microsoft", + "version": "29.0.0.0" + }, + { + "id": "9856ae4f-d1a7-46ef-89bb-6ef056398228", + "name": "System Application Test Library", + "publisher": "Microsoft", + "version": "29.0.0.0" + }, + { + "id": "5095f467-0a01-4b99-99d1-9ff1237d286f", + "publisher": "Microsoft", + "name": "Library Variable Storage", + "version": "29.0.0.0" + }, + { + "id": "8228f99b-cce5-4b9c-b247-9ee1145b7470", + "name": "E-Document format for OIOUBL", + "publisher": "Microsoft", + "version": "29.0.0.0" + } + ], + "screenshots": [], + "platform": "29.0.0.0", + "features": [ + "TranslationFile" + ], + "idRanges": [ + { + "from": 13850, + "to": 13855 + } + ], + "resourceFolders": [ + ".resources" + ], + "resourceExposurePolicy": { + "allowDebugging": true, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "application": "29.0.0.0" +} diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/test/src/EDocumentStructuredTests.Codeunit.al b/src/Apps/DK/EDocumentFormatOIOUBL/test/src/EDocumentStructuredTests.Codeunit.al new file mode 100644 index 00000000000..9324f14d7d1 --- /dev/null +++ b/src/Apps/DK/EDocumentFormatOIOUBL/test/src/EDocumentStructuredTests.Codeunit.al @@ -0,0 +1,276 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +codeunit 13851 "E-Document Structured Tests" +{ + Subtype = Test; + TestType = IntegrationTest; + + var + Customer: Record Customer; + Vendor: Record Vendor; + EDocumentService: Record "E-Document Service"; + Assert: Codeunit Assert; + LibraryVariableStorage: Codeunit "Library - Variable Storage"; + LibraryEDoc: Codeunit "Library - E-Document"; + LibraryLowerPermission: Codeunit "Library - Lower Permissions"; + OIOUBLStructuredValidations: Codeunit "OIOUBL Structured Validations"; + IsInitialized: Boolean; + EDocumentStatusNotUpdatedErr: Label 'The status of the EDocument was not updated to the expected status after the step was executed.'; + TestFileTok: Label 'oioubl/oioubl-invoice-0.xml', Locked = true; + MockCurrencyCode: Code[10]; + MockDate: Date; + + #region OIOUBL XML + [Test] + procedure TestOIOUBLInvoice_ValidDocument() + var + EDocument: Record "E-Document"; + begin + // [FEATURE] [E-Document] [OIOUBL] [Content Extraction] + // [SCENARIO] Import and extract content from a valid OIOUBL invoice document + + // [GIVEN] A valid OIOUBL XML invoice document is imported + Initialize(Enum::"Service Integration"::"No Integration"); + SetupOIOUBLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, TestFileTok); + + // [WHEN] The document is processed to read into draft step + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin + OIOUBLStructuredValidations.SetMockCurrencyCode(MockCurrencyCode); + OIOUBLStructuredValidations.SetMockDate(MockDate); + + // [THEN] All document content is correctly extracted and validated + OIOUBLStructuredValidations.AssertFullEDocumentContentExtracted(EDocument."Entry No"); + end else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + [HandlerFunctions('EDocumentPurchaseHeaderPageHandler')] + procedure TestOIOUBLInvoice_ValidDocument_ViewExtractedData() + var + EDocument: Record "E-Document"; + EDocImport: Codeunit "E-Doc. Import"; + begin + // [FEATURE] [E-Document] [OIOUBL] [View Data] + // [SCENARIO] View extracted data from a valid OIOUBL invoice document + + // [GIVEN] A valid OIOUBL XML invoice document is imported + Initialize(Enum::"Service Integration"::"No Integration"); + SetupOIOUBLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, TestFileTok); + + // [WHEN] The document is processed to draft status + ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft"); + EDocument.Get(EDocument."Entry No"); + + // [WHEN] View extracted data is called + EDocImport.ViewExtractedData(EDocument); + + // [THEN] The extracted data page opens and can be handled properly (verified by page handler) + // EDocumentPurchaseHeaderPageHandler + end; + + [Test] + procedure TestOIOUBLInvoice_ValidDocument_PurchaseInvoiceCreated() + var + EDocument: Record "E-Document"; + PurchaseHeader: Record "Purchase Header"; + DummyItem: Record Item; + EDocumentProcessing: Codeunit "E-Document Processing"; + DataTypeManagement: Codeunit "Data Type Management"; + RecRef: RecordRef; + VariantRecord: Variant; + begin + // [FEATURE] [E-Document] [OIOUBL] [Purchase Invoice Creation] + // [SCENARIO] Create a purchase invoice from a valid OIOUBL invoice document + + // [GIVEN] A valid OIOUBL XML invoice document is imported + Initialize(Enum::"Service Integration"::"No Integration"); + Vendor."VAT Registration No." := 'GB123456789'; + Vendor.Modify(true); + SetupOIOUBLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, TestFileTok); + + // [WHEN] The document is processed through finish draft step + ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Finish draft"); + EDocument.Get(EDocument."Entry No"); + + // [WHEN] The created purchase record is retrieved + EDocumentProcessing.GetRecord(EDocument, VariantRecord); + DataTypeManagement.GetRecordRef(VariantRecord, RecRef); + RecRef.SetTable(PurchaseHeader); + + // [THEN] The purchase header is correctly created with OIOUBL data + OIOUBLStructuredValidations.SetMockCurrencyCode(MockCurrencyCode); + OIOUBLStructuredValidations.SetMockDate(MockDate); + OIOUBLStructuredValidations.AssertPurchaseDocument(Vendor."No.", PurchaseHeader, DummyItem); + end; + + [Test] + procedure TestOIOUBLInvoice_ValidDocument_UpdateDraftAndFinalize() + var + EDocument: Record "E-Document"; + PurchaseHeader: Record "Purchase Header"; + Item: Record Item; + EDocImportParameters: Record "E-Doc. Import Parameters"; + EDocImport: Codeunit "E-Doc. Import"; + EDocumentProcessing: Codeunit "E-Document Processing"; + DataTypeManagement: Codeunit "Data Type Management"; + RecRef: RecordRef; + EDocPurchaseDraft: TestPage "E-Document Purchase Draft"; + VariantRecord: Variant; + begin + // [FEATURE] [E-Document] [OIOUBL] [Draft Update] + // [SCENARIO] Update draft purchase document data and finalize processing + + // [GIVEN] A valid OIOUBL XML invoice document is imported and processed to draft preparation + Initialize(Enum::"Service Integration"::"No Integration"); + Vendor."VAT Registration No." := 'GB123456789'; + Vendor.Modify(true); + SetupOIOUBLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, TestFileTok); + ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Prepare draft"); + + // [GIVEN] A generic item is created for manual assignment + LibraryEDoc.CreateGenericItem(Item, ''); + + // [WHEN] The draft document is opened and modified through UI + EDocPurchaseDraft.OpenEdit(); + EDocPurchaseDraft.GoToRecord(EDocument); + EDocPurchaseDraft.Lines.First(); + EDocPurchaseDraft.Lines."No.".SetValue(Item."No."); + EDocPurchaseDraft.Lines.Next(); + + // [WHEN] The processing is completed to finish draft step + EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; + EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + EDocument.Get(EDocument."Entry No"); + + // [WHEN] The final purchase record is retrieved + EDocumentProcessing.GetRecord(EDocument, VariantRecord); + DataTypeManagement.GetRecordRef(VariantRecord, RecRef); + RecRef.SetTable(PurchaseHeader); + + // [THEN] The purchase header contains both imported OIOUBL data and manual updates + OIOUBLStructuredValidations.SetMockCurrencyCode(MockCurrencyCode); + OIOUBLStructuredValidations.SetMockDate(MockDate); + OIOUBLStructuredValidations.AssertPurchaseDocument(Vendor."No.", PurchaseHeader, Item); + end; + + [PageHandler] + procedure EDocumentPurchaseHeaderPageHandler(var EDocReadablePurchaseDoc: TestPage "E-Doc. Readable Purchase Doc.") + begin + EDocReadablePurchaseDoc.Close(); + end; + #endregion + + local procedure Initialize(Integration: Enum "Service Integration") + var + TransformationRule: Record "Transformation Rule"; + EDocument: Record "E-Document"; + EDocDataStorage: Record "E-Doc. Data Storage"; + EDocumentsSetup: Record "E-Documents Setup"; + EDocumentServiceStatus: Record "E-Document Service Status"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + DocumentAttachment: Record "Document Attachment"; + Currency: Record Currency; + begin + LibraryLowerPermission.SetOutsideO365Scope(); + LibraryVariableStorage.Clear(); + Clear(LibraryVariableStorage); + + if IsInitialized then + exit; + + EDocument.DeleteAll(false); + EDocumentServiceStatus.DeleteAll(false); + EDocumentService.DeleteAll(false); + EDocDataStorage.DeleteAll(false); + EDocumentPurchaseHeader.DeleteAll(false); + EDocumentPurchaseLine.DeleteAll(false); + DocumentAttachment.DeleteAll(false); + + LibraryEDoc.SetupStandardVAT(); + LibraryEDoc.SetupStandardSalesScenario(Customer, EDocumentService, Enum::"E-Document Format"::OIOUBL, Integration); + LibraryEDoc.SetupStandardPurchaseScenario(Vendor, EDocumentService, Enum::"E-Document Format"::OIOUBL, Integration); + EDocumentService."Import Process" := "E-Document Import Process"::"Version 2.0"; + EDocumentService."Read into Draft Impl." := "E-Doc. Read into Draft"::OIOUBL; + EDocumentService.Modify(false); + EDocumentsSetup.InsertNewExperienceSetup(); + + // Set a currency that can be used across all localizations + MockCurrencyCode := 'XYZ'; + Currency.Init(); + Currency.Validate(Code, MockCurrencyCode); + if Currency.Insert(true) then; + CreateCurrencyExchangeRate(); + + MockDate := DMY2Date(22, 01, 2026); + + TransformationRule.DeleteAll(false); + TransformationRule.CreateDefaultTransformations(); + + IsInitialized := true; + end; + + local procedure SetupOIOUBLEDocumentService() + begin + EDocumentService."Read into Draft Impl." := "E-Doc. Read into Draft"::OIOUBL; + EDocumentService.Modify(false); + end; + + local procedure CreateInboundEDocumentFromXML(var EDocument: Record "E-Document"; FilePath: Text) + var + EDocLogRecord: Record "E-Document Log"; + EDocumentLog: Codeunit "E-Document Log"; + begin + LibraryEDoc.CreateInboundEDocument(EDocument, EDocumentService); + + EDocumentLog.SetBlob('Test', Enum::"E-Doc. File Format"::XML, NavApp.GetResourceAsText(FilePath)); + EDocumentLog.SetFields(EDocument, EDocumentService); + EDocLogRecord := EDocumentLog.InsertLog(Enum::"E-Document Service Status"::Imported, Enum::"Import E-Doc. Proc. Status"::Readable); + + EDocument."Structured Data Entry No." := EDocLogRecord."E-Doc. Data Storage Entry No."; + EDocument.Modify(false); + end; + + local procedure ProcessEDocumentToStep(var EDocument: Record "E-Document"; ProcessingStep: Enum "Import E-Document Steps"): Boolean + var + EDocImportParameters: Record "E-Doc. Import Parameters"; + EDocImport: Codeunit "E-Doc. Import"; + EDocumentProcessing: Codeunit "E-Document Processing"; + begin + EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::Readable); + EDocImportParameters."Step to Run" := ProcessingStep; + EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + EDocument.CalcFields("Import Processing Status"); + + // Update the exit condition to handle different processing steps + case ProcessingStep of + "Import E-Document Steps"::"Read into Draft": + exit(EDocument."Import Processing Status" = Enum::"Import E-Doc. Proc. Status"::"Ready for draft"); + "Import E-Document Steps"::"Finish draft": + exit(EDocument."Import Processing Status" = Enum::"Import E-Doc. Proc. Status"::Processed); + "Import E-Document Steps"::"Prepare draft": + exit(EDocument."Import Processing Status" = Enum::"Import E-Doc. Proc. Status"::"Draft Ready"); + else + exit(EDocument."Import Processing Status" = Enum::"Import E-Doc. Proc. Status"::"Ready for draft"); + end; + end; + + local procedure CreateCurrencyExchangeRate() + var + CurrencyExchangeRate: Record "Currency Exchange Rate"; + begin + CurrencyExchangeRate.Init(); + CurrencyExchangeRate."Currency Code" := MockCurrencyCode; + CurrencyExchangeRate."Starting Date" := WorkDate(); + CurrencyExchangeRate."Exchange Rate Amount" := 10; + CurrencyExchangeRate."Relational Exch. Rate Amount" := 1.23; + CurrencyExchangeRate.Insert(true); + end; +} diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/test/src/OIOUBLStructuredValidations.Codeunit.al b/src/Apps/DK/EDocumentFormatOIOUBL/test/src/OIOUBLStructuredValidations.Codeunit.al new file mode 100644 index 00000000000..bb82d37f39b --- /dev/null +++ b/src/Apps/DK/EDocumentFormatOIOUBL/test/src/OIOUBLStructuredValidations.Codeunit.al @@ -0,0 +1,111 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +codeunit 13852 "OIOUBL Structured Validations" +{ + var + Assert: Codeunit Assert; + UnitOfMeasureCodeTok: Label 'PCS', Locked = true; + SalesInvoiceNoTok: Label '103033', Locked = true; + PurchaseorderNoTok: Label '2', Locked = true; + MockDate: Date; + MockCurrencyCode: Code[10]; + MockDataMismatchErr: Label 'The %1 in %2 does not align with the mock data. Expected: %3, Actual: %4', Locked = true, Comment = '%1 = Field caption, %2 = Table caption, %3 = Expected value, %4 = Actual value'; + + + internal procedure AssertFullEDocumentContentExtracted(EDocumentEntryNo: Integer) + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual(SalesInvoiceNoTok, EDocumentPurchaseHeader."Sales Invoice No.", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Sales Invoice No."), EDocumentPurchaseHeader.TableCaption(), SalesInvoiceNoTok, EDocumentPurchaseHeader."Sales Invoice No.")); + Assert.AreEqual(MockDate, EDocumentPurchaseHeader."Document Date", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Document Date"), EDocumentPurchaseHeader.TableCaption(), MockDate, EDocumentPurchaseHeader."Document Date")); + Assert.AreEqual(CalcDate('<+1M>', MockDate), EDocumentPurchaseHeader."Due Date", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Due Date"), EDocumentPurchaseHeader.TableCaption(), CalcDate('<+1M>', MockDate), EDocumentPurchaseHeader."Due Date")); + Assert.AreEqual(MockCurrencyCode, EDocumentPurchaseHeader."Currency Code", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Currency Code"), EDocumentPurchaseHeader.TableCaption(), MockCurrencyCode, EDocumentPurchaseHeader."Currency Code")); + Assert.AreEqual(PurchaseorderNoTok, EDocumentPurchaseHeader."Purchase Order No.", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Purchase Order No."), EDocumentPurchaseHeader.TableCaption(), PurchaseorderNoTok, EDocumentPurchaseHeader."Purchase Order No.")); + Assert.AreEqual('CRONUS International', EDocumentPurchaseHeader."Vendor Company Name", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Vendor Company Name"), EDocumentPurchaseHeader.TableCaption(), 'CRONUS International', EDocumentPurchaseHeader."Vendor Company Name")); + Assert.AreEqual('Main Street, 14', EDocumentPurchaseHeader."Vendor Address", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Vendor Address"), EDocumentPurchaseHeader.TableCaption(), 'Main Street, 14', EDocumentPurchaseHeader."Vendor Address")); + Assert.AreEqual('GB123456789', EDocumentPurchaseHeader."Vendor VAT Id", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Vendor VAT Id"), EDocumentPurchaseHeader.TableCaption(), 'GB123456789', EDocumentPurchaseHeader."Vendor VAT Id")); + Assert.AreEqual('Jim Olive', EDocumentPurchaseHeader."Vendor Contact Name", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Vendor Contact Name"), EDocumentPurchaseHeader.TableCaption(), 'Jim Olive', EDocumentPurchaseHeader."Vendor Contact Name")); + Assert.AreEqual('The Cannon Group PLC', EDocumentPurchaseHeader."Customer Company Name", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Customer Company Name"), EDocumentPurchaseHeader.TableCaption(), 'The Cannon Group PLC', EDocumentPurchaseHeader."Customer Company Name")); + Assert.AreEqual('GB789456278', EDocumentPurchaseHeader."Customer VAT Id", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Customer VAT Id"), EDocumentPurchaseHeader.TableCaption(), 'GB789456278', EDocumentPurchaseHeader."Customer VAT Id")); + Assert.AreEqual('192 Market Square', EDocumentPurchaseHeader."Customer Address", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Customer Address"), EDocumentPurchaseHeader.TableCaption(), '192 Market Square', EDocumentPurchaseHeader."Customer Address")); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + EDocumentPurchaseLine.FindSet(); + Assert.AreEqual(1, EDocumentPurchaseLine."Quantity", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Quantity"), EDocumentPurchaseLine.TableCaption(), 1, EDocumentPurchaseLine."Quantity")); + Assert.AreEqual(UnitOfMeasureCodeTok, EDocumentPurchaseLine."Unit of Measure", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Unit of Measure"), EDocumentPurchaseLine.TableCaption(), UnitOfMeasureCodeTok, EDocumentPurchaseLine."Unit of Measure")); + Assert.AreEqual(4000, EDocumentPurchaseLine."Sub Total", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Sub Total"), EDocumentPurchaseLine.TableCaption(), 4000, EDocumentPurchaseLine."Sub Total")); + Assert.AreEqual(MockCurrencyCode, EDocumentPurchaseLine."Currency Code", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Currency Code"), EDocumentPurchaseLine.TableCaption(), MockCurrencyCode, EDocumentPurchaseLine."Currency Code")); + Assert.AreEqual(0, EDocumentPurchaseLine."Total Discount", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Total Discount"), EDocumentPurchaseLine.TableCaption(), 0, EDocumentPurchaseLine."Total Discount")); + Assert.AreEqual('Bicycle', EDocumentPurchaseLine.Description, StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption(Description), EDocumentPurchaseLine.TableCaption(), 'Bicycle', EDocumentPurchaseLine.Description)); + Assert.AreEqual('1000', EDocumentPurchaseLine."Product Code", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Product Code"), EDocumentPurchaseLine.TableCaption(), '1000', EDocumentPurchaseLine."Product Code")); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("VAT Rate"), EDocumentPurchaseLine.TableCaption(), 25, EDocumentPurchaseLine."VAT Rate")); + Assert.AreEqual(4000, EDocumentPurchaseLine."Unit Price", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Unit Price"), EDocumentPurchaseLine.TableCaption(), 4000, EDocumentPurchaseLine."Unit Price")); + + EDocumentPurchaseLine.Next(); + Assert.AreEqual(2, EDocumentPurchaseLine."Quantity", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Quantity"), EDocumentPurchaseLine.TableCaption(), 2, EDocumentPurchaseLine."Quantity")); + Assert.AreEqual(UnitOfMeasureCodeTok, EDocumentPurchaseLine."Unit of Measure", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Unit of Measure"), EDocumentPurchaseLine.TableCaption(), UnitOfMeasureCodeTok, EDocumentPurchaseLine."Unit of Measure")); + Assert.AreEqual(10000, EDocumentPurchaseLine."Sub Total", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Sub Total"), EDocumentPurchaseLine.TableCaption(), 10000, EDocumentPurchaseLine."Sub Total")); + Assert.AreEqual(MockCurrencyCode, EDocumentPurchaseLine."Currency Code", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Currency Code"), EDocumentPurchaseLine.TableCaption(), MockCurrencyCode, EDocumentPurchaseLine."Currency Code")); + Assert.AreEqual(0, EDocumentPurchaseLine."Total Discount", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Total Discount"), EDocumentPurchaseLine.TableCaption(), 0, EDocumentPurchaseLine."Total Discount")); + Assert.AreEqual('Bicycle v2', EDocumentPurchaseLine.Description, StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption(Description), EDocumentPurchaseLine.TableCaption(), 'Bicycle v2', EDocumentPurchaseLine.Description)); + Assert.AreEqual('2000', EDocumentPurchaseLine."Product Code", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Product Code"), EDocumentPurchaseLine.TableCaption(), '2000', EDocumentPurchaseLine."Product Code")); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("VAT Rate"), EDocumentPurchaseLine.TableCaption(), 25, EDocumentPurchaseLine."VAT Rate")); + Assert.AreEqual(5000, EDocumentPurchaseLine."Unit Price", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseLine.FieldCaption("Unit Price"), EDocumentPurchaseLine.TableCaption(), 5000, EDocumentPurchaseLine."Unit Price")); + end; + + internal procedure AssertPurchaseDocument(VendorNo: Code[20]; PurchaseHeader: Record "Purchase Header"; Item: Record Item) + var + PurchaseLine: Record "Purchase Line"; + Item1NoTok: Label 'GL00000001', Locked = true; + Item2NoTok: Label 'GL00000003', Locked = true; + begin + Assert.AreEqual(SalesInvoiceNoTok, PurchaseHeader."Vendor Invoice No.", StrSubstNo(MockDataMismatchErr, PurchaseHeader.FieldCaption("Vendor Invoice No."), PurchaseHeader.TableCaption(), SalesInvoiceNoTok, PurchaseHeader."Vendor Invoice No.")); + Assert.AreEqual(MockDate, PurchaseHeader."Document Date", StrSubstNo(MockDataMismatchErr, PurchaseHeader.FieldCaption("Document Date"), PurchaseHeader.TableCaption(), MockDate, PurchaseHeader."Document Date")); + Assert.AreEqual(CalcDate('<+1M>', MockDate), PurchaseHeader."Due Date", StrSubstNo(MockDataMismatchErr, PurchaseHeader.FieldCaption("Due Date"), PurchaseHeader.TableCaption(), CalcDate('<+1M>', MockDate), PurchaseHeader."Due Date")); + Assert.AreEqual(MockCurrencyCode, PurchaseHeader."Currency Code", StrSubstNo(MockDataMismatchErr, PurchaseHeader.FieldCaption("Currency Code"), PurchaseHeader.TableCaption(), MockCurrencyCode, PurchaseHeader."Currency Code")); + Assert.AreEqual(PurchaseorderNoTok, PurchaseHeader."Vendor Order No.", StrSubstNo(MockDataMismatchErr, PurchaseHeader.FieldCaption("Vendor Order No."), PurchaseHeader.TableCaption(), PurchaseorderNoTok, PurchaseHeader."Vendor Order No.")); + Assert.AreEqual(VendorNo, PurchaseHeader."Buy-from Vendor No.", StrSubstNo(MockDataMismatchErr, PurchaseHeader.FieldCaption("Buy-from Vendor No."), PurchaseHeader.TableCaption(), VendorNo, PurchaseHeader."Buy-from Vendor No.")); + + PurchaseLine.SetRange("Document Type", PurchaseHeader."Document Type"); + PurchaseLine.SetRange("Document No.", PurchaseHeader."No."); + PurchaseLine.FindSet(); + Assert.AreEqual(1, PurchaseLine.Quantity, StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption(Quantity), PurchaseLine.TableCaption(), 1, PurchaseLine.Quantity)); + Assert.AreEqual(4000, PurchaseLine."Direct Unit Cost", StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption("Direct Unit Cost"), PurchaseLine.TableCaption(), 4000, PurchaseLine."Direct Unit Cost")); + Assert.AreEqual(MockCurrencyCode, PurchaseLine."Currency Code", StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption("Currency Code"), PurchaseLine.TableCaption(), MockCurrencyCode, PurchaseLine."Currency Code")); + Assert.AreEqual(0, PurchaseLine."Line Discount Amount", StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption("Line Discount Amount"), PurchaseLine.TableCaption(), 0, PurchaseLine."Line Discount Amount")); + // In the import file we have a name 'Bicycle' but because of Item Cross Reference validation Item description is being used + if Item."No." <> '' then begin + Assert.AreEqual('Bicycle', PurchaseLine.Description, StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption(Description), PurchaseLine.TableCaption(), Item."No.", PurchaseLine.Description)); + Assert.AreEqual(Item."No.", PurchaseLine."No.", StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption("No."), PurchaseLine.TableCaption(), Item."No.", PurchaseLine."No.")); + Assert.AreEqual(Item."Purch. Unit of Measure", PurchaseLine."Unit of Measure Code", StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption("Unit of Measure Code"), PurchaseLine.TableCaption(), UnitOfMeasureCodeTok, PurchaseLine."Unit of Measure Code")); + end else begin + Assert.AreEqual(Item1NoTok, PurchaseLine.Description, StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption(Description), PurchaseLine.TableCaption(), Item1NoTok, PurchaseLine.Description)); + Assert.AreEqual(Item1NoTok, PurchaseLine."No.", StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption("No."), PurchaseLine.TableCaption(), Item1NoTok, PurchaseLine."No.")); + Assert.AreEqual(UnitOfMeasureCodeTok, PurchaseLine."Unit of Measure Code", StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption("Unit of Measure Code"), PurchaseLine.TableCaption(), UnitOfMeasureCodeTok, PurchaseLine."Unit of Measure Code")); + end; + + PurchaseLine.Next(); + Assert.AreEqual(2, PurchaseLine.Quantity, StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption(Quantity), PurchaseLine.TableCaption(), 2, PurchaseLine.Quantity)); + Assert.AreEqual(UnitOfMeasureCodeTok, PurchaseLine."Unit of Measure Code", StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption("Unit of Measure Code"), PurchaseLine.TableCaption(), UnitOfMeasureCodeTok, PurchaseLine."Unit of Measure Code")); + Assert.AreEqual(5000, PurchaseLine."Direct Unit Cost", StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption("Direct Unit Cost"), PurchaseLine.TableCaption(), 5000, PurchaseLine."Direct Unit Cost")); + Assert.AreEqual(MockCurrencyCode, PurchaseLine."Currency Code", StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption("Currency Code"), PurchaseLine.TableCaption(), MockCurrencyCode, PurchaseLine."Currency Code")); + Assert.AreEqual(0, PurchaseLine."Line Discount Amount", StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption("Line Discount Amount"), PurchaseLine.TableCaption(), 0, PurchaseLine."Line Discount Amount")); + // In the import file we have a name 'Bicycle v2' but because of Item Cross Reference validation Item description is being used + Assert.AreEqual(Item2NoTok, PurchaseLine.Description, StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption(Description), PurchaseLine.TableCaption(), Item2NoTok, PurchaseLine.Description)); + Assert.AreEqual(Item2NoTok, PurchaseLine."No.", StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption("No."), PurchaseLine.TableCaption(), Item2NoTok, PurchaseLine."No.")); + end; + + procedure SetMockDate(MockDate: Date) + begin + this.MockDate := MockDate; + end; + + procedure SetMockCurrencyCode(MockCurrencyCode: Code[10]) + begin + this.MockCurrencyCode := MockCurrencyCode; + end; +} diff --git a/src/Apps/W1/EDocument/App/app.json b/src/Apps/W1/EDocument/App/app.json index 298740e589e..16f919f5b5e 100644 --- a/src/Apps/W1/EDocument/App/app.json +++ b/src/Apps/W1/EDocument/App/app.json @@ -54,6 +54,11 @@ "id": "8238f78b-cbe5-4b9c-b247-7771145b7470", "name": "E-Document Format for Factura-E Tests", "publisher": "Microsoft" + }, + { + "id": "f78b9487-b911-4db1-893b-5a5c310919b8", + "name": "E-Document Format for OIOUBL Tests", + "publisher": "Microsoft" } ], "screenshots": [], From 2863fb5712dfd6e5808b764ff7f5de660ff3fbd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Thu, 23 Jul 2026 15:25:51 +0200 Subject: [PATCH 2/4] Fix OIOUBL structured e-document handler defects and CI/quality issues - Return correct E-Doc. Process Draft enum (Purchase Invoice / Purchase Credit Memo) instead of the obsolete Purchase Document value. - Validate root element and namespace; reject unsupported roots. - Reset draft header and lines on re-read to avoid duplicate lines. - Read credit-note due date and invoice fallback from PaymentMeans/PaymentDueDate. - Store credit-note BillingReference in Applies-to Ext. Invoice No. - Fall back to PartyIdentification/ID for vendor identifier when VAT Id empty. - Move test app/objects into valid Danish test id range (148061-148062) and hoist procedure-local labels to codeunit scope. - Add regression tests and OIOUBL credit-note test fixture. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f304ccff-cedb-4561-b6da-97f63a379b2d --- .../src/EDocumentOIOUBLHandler.Codeunit.al | 49 +++++-- .../.resources/oioubl/oioubl-creditnote-0.xml | 100 +++++++++++++ .../.resources/oioubl/oioubl-invoice-0.xml | 5 +- .../DK/EDocumentFormatOIOUBL/test/app.json | 4 +- .../src/EDocumentStructuredTests.Codeunit.al | 131 +++++++++++++++++- .../OIOUBLStructuredValidations.Codeunit.al | 32 ++++- 6 files changed, 299 insertions(+), 22 deletions(-) create mode 100644 src/Apps/DK/EDocumentFormatOIOUBL/test/.resources/oioubl/oioubl-creditnote-0.xml diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al b/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al index 694086cacb6..97914cd6360 100644 --- a/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al +++ b/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al @@ -19,8 +19,14 @@ codeunit 13913 "E-Document OIOUBL Handler" implements IStructuredFormatReader var GLNTok: Label 'GLN', Locked = true; + DKCVRTok: Label 'DK:CVR', Locked = true; InvoiceLineTok: Label 'cac:InvoiceLine', Locked = true; CreditNoteLineTok: Label 'cac:CreditNoteLine', Locked = true; + CommonAggregateComponentsTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', Locked = true; + CommonBasicComponentsTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', Locked = true; + DefaultInvoiceTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', Locked = true; + DefaultCreditNoteTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2', Locked = true; + UnsupportedXmlRootElementErr: Label 'Unsupported XML root element: %1.', Comment = '%1 = local name of the XML root element'; /// /// Reads an OIOUBL format XML document and converts it into a draft purchase document. @@ -35,11 +41,9 @@ codeunit 13913 "E-Document OIOUBL Handler" implements IStructuredFormatReader OIOUBLXml: XmlDocument; XmlNamespaces: XmlNamespaceManager; XmlElement: XmlElement; - CommonAggregateComponentsTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', Locked = true; - CommonBasicComponentsTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', Locked = true; - DefaultInvoiceTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', Locked = true; - DefaultCreditNoteTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2', Locked = true; + ProcessDraft: Enum "E-Doc. Process Draft"; begin + ResetDraft(EDocument); EDocumentPurchaseHeader.InsertForEDocument(EDocument); XmlDocument.ReadFrom(TempBlob.CreateInStream(TextEncoding::UTF8), OIOUBLXml); @@ -50,16 +54,26 @@ codeunit 13913 "E-Document OIOUBL Handler" implements IStructuredFormatReader XmlNamespaces.AddNamespace('cn', DefaultCreditNoteTok); OIOUBLXml.GetRoot(XmlElement); - case UpperCase(XmlElement.LocalName()) of - 'INVOICE': + case XmlElement.LocalName() of + 'Invoice': begin + if XmlElement.NamespaceUri() <> DefaultInvoiceTok then + Error(UnsupportedXmlRootElementErr, XmlElement.LocalName()); PopulateEDocumentForInvoice(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader, EDocument); - 'CREDITNOTE': + ProcessDraft := Enum::"E-Doc. Process Draft"::"Purchase Invoice"; + end; + 'CreditNote': begin + if XmlElement.NamespaceUri() <> DefaultCreditNoteTok then + Error(UnsupportedXmlRootElementErr, XmlElement.LocalName()); PopulateEDocumentForCreditNote(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader, EDocument); + ProcessDraft := Enum::"E-Doc. Process Draft"::"Purchase Credit Memo"; + end; + else + Error(UnsupportedXmlRootElementErr, XmlElement.LocalName()); end; EDocumentPurchaseHeader.Modify(false); EDocument.Direction := EDocument.Direction::Incoming; - exit(Enum::"E-Doc. Process Draft"::"Purchase Document"); + exit(ProcessDraft); end; /// @@ -101,6 +115,8 @@ codeunit 13913 "E-Document OIOUBL Handler" implements IStructuredFormatReader EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Sales Invoice No."), EDocumentPurchaseHeader."Sales Invoice No."); EDocumentXMLHelper.SetDateValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cbc:IssueDate', EDocumentPurchaseHeader."Document Date"); EDocumentXMLHelper.SetDateValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cbc:DueDate', EDocumentPurchaseHeader."Due Date"); + if EDocumentPurchaseHeader."Due Date" = 0D then + EDocumentXMLHelper.SetDateValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cac:PaymentMeans/cbc:PaymentDueDate', EDocumentPurchaseHeader."Due Date"); EDocumentXMLHelper.SetCurrencyValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cbc:DocumentCurrencyCode', MaxStrLen(EDocumentPurchaseHeader."Currency Code"), EDocumentPurchaseHeader."Currency Code"); EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cac:OrderReference/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Purchase Order No."), EDocumentPurchaseHeader."Purchase Order No."); #pragma warning restore AA0139 @@ -124,10 +140,10 @@ codeunit 13913 "E-Document OIOUBL Handler" implements IStructuredFormatReader #pragma warning disable AA0139 EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Sales Invoice No."), EDocumentPurchaseHeader."Sales Invoice No."); EDocumentXMLHelper.SetDateValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cbc:IssueDate', EDocumentPurchaseHeader."Document Date"); - EDocumentXMLHelper.SetDateValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cbc:DueDate', EDocumentPurchaseHeader."Due Date"); + EDocumentXMLHelper.SetDateValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cac:PaymentMeans/cbc:PaymentDueDate', EDocumentPurchaseHeader."Due Date"); EDocumentXMLHelper.SetCurrencyValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cbc:DocumentCurrencyCode', MaxStrLen(EDocumentPurchaseHeader."Currency Code"), EDocumentPurchaseHeader."Currency Code"); EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cac:OrderReference/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Purchase Order No."), EDocumentPurchaseHeader."Purchase Order No."); - EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Applies-to Doc. No."), EDocumentPurchaseHeader."Applies-to Doc. No."); + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Applies-to Ext. Invoice No."), EDocumentPurchaseHeader."Applies-to Ext. Invoice No."); #pragma warning restore AA0139 EDocumentXMLHelper.SetNumberValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount', EDocumentPurchaseHeader."Sub Total"); EDocumentXMLHelper.SetNumberValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount', EDocumentPurchaseHeader.Total); @@ -144,7 +160,6 @@ codeunit 13913 "E-Document OIOUBL Handler" implements IStructuredFormatReader var EDocumentXMLHelper: Codeunit "E-Document PEPPOL Utility"; EDocumentImportHelper: Codeunit "E-Document Import Helper"; - DKCVRTok: Label 'DK:CVR', Locked = true; VendorName, VendorAddress, VendorParticipantId : Text; VATRegistrationNo: Text[20]; EndpointID, SchemeID : Text; @@ -174,7 +189,10 @@ codeunit 13913 "E-Document OIOUBL Handler" implements IStructuredFormatReader end; VendorParticipantId := SchemeID + ':' + EndpointID; end; - VATRegistrationNo := CopyStr(EDocumentPurchaseHeader."Vendor VAT Id", 1, MaxStrLen(VATRegistrationNo)); + if EDocumentPurchaseHeader."Vendor VAT Id" <> '' then + VATRegistrationNo := CopyStr(EDocumentPurchaseHeader."Vendor VAT Id", 1, MaxStrLen(VATRegistrationNo)); + if VATRegistrationNo = '' then + EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, BasePathTxt + '/cac:PartyIdentification/cbc:ID', MaxStrLen(VATRegistrationNo), VATRegistrationNo); VendorName := EDocumentPurchaseHeader."Vendor Company Name"; VendorAddress := EDocumentPurchaseHeader."Vendor Address"; if not FindVendorByVATRegNoOrGLN(VendorNo, VATRegistrationNo, GLN) then @@ -327,8 +345,11 @@ codeunit 13913 "E-Document OIOUBL Handler" implements IStructuredFormatReader procedure ResetDraft(EDocument: Record "E-Document") var EDocPurchaseHeader: Record "E-Document Purchase Header"; + EDocPurchaseLine: Record "E-Document Purchase Line"; begin - EDocPurchaseHeader.GetFromEDocument(EDocument); - EDocPurchaseHeader.Delete(true); + EDocPurchaseHeader.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPurchaseHeader.DeleteAll(); + EDocPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPurchaseLine.DeleteAll(); end; } diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/test/.resources/oioubl/oioubl-creditnote-0.xml b/src/Apps/DK/EDocumentFormatOIOUBL/test/.resources/oioubl/oioubl-creditnote-0.xml new file mode 100644 index 00000000000..929e055d989 --- /dev/null +++ b/src/Apps/DK/EDocumentFormatOIOUBL/test/.resources/oioubl/oioubl-creditnote-0.xml @@ -0,0 +1,100 @@ + + + 2.0 + OIOUBL-2.01 + Procurement-OrdSimR-BilSim-1.0 + CN-5001 + 2026-02-15 + 381 + XYZ + + 5 + + + + 103033 + 2026-01-22 + + + + + GB123456789 + + CRONUS International + + + Main Street, 14 + Birmingham + B27 4KT + + DK + + + + GB123456789 + + VAT + + + + Jim Olive + + + + + + GB789456278 + + The Cannon Group PLC + + + 192 Market Square + Birmingham + B27 4KT + + DK + + + + GB789456278 + + VAT + + + + + + 30 + 2026-03-15 + + + 500 + + + 2000 + 2000 + 2500 + 2500 + + + 10000 + 1 + 2000 + + Bicycle - Return + + 1000 + + + S + 25 + + VAT + + + + + 2000.00 + + + diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/test/.resources/oioubl/oioubl-invoice-0.xml b/src/Apps/DK/EDocumentFormatOIOUBL/test/.resources/oioubl/oioubl-invoice-0.xml index 72316a124bc..63e53c02467 100644 --- a/src/Apps/DK/EDocumentFormatOIOUBL/test/.resources/oioubl/oioubl-invoice-0.xml +++ b/src/Apps/DK/EDocumentFormatOIOUBL/test/.resources/oioubl/oioubl-invoice-0.xml @@ -8,7 +8,6 @@ 01d4dd7d-ac2b-460d-ae44-5f20d540952a 2026-01-22 12:00:00 - 2026-02-22 380 XYZ @@ -88,6 +87,10 @@ + + 42 + 2026-02-22 + 1000.00 diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/test/app.json b/src/Apps/DK/EDocumentFormatOIOUBL/test/app.json index 3cb2e93002c..c0fe56e23a4 100644 --- a/src/Apps/DK/EDocumentFormatOIOUBL/test/app.json +++ b/src/Apps/DK/EDocumentFormatOIOUBL/test/app.json @@ -56,8 +56,8 @@ ], "idRanges": [ { - "from": 13850, - "to": 13855 + "from": 148061, + "to": 148062 } ], "resourceFolders": [ diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/test/src/EDocumentStructuredTests.Codeunit.al b/src/Apps/DK/EDocumentFormatOIOUBL/test/src/EDocumentStructuredTests.Codeunit.al index 9324f14d7d1..cb015ae3cf3 100644 --- a/src/Apps/DK/EDocumentFormatOIOUBL/test/src/EDocumentStructuredTests.Codeunit.al +++ b/src/Apps/DK/EDocumentFormatOIOUBL/test/src/EDocumentStructuredTests.Codeunit.al @@ -2,7 +2,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. // ------------------------------------------------------------------------------------------------ -codeunit 13851 "E-Document Structured Tests" +using Microsoft.eServices.EDocument.Processing.Interfaces; + +codeunit 148061 "E-Document Structured Tests" { Subtype = Test; TestType = IntegrationTest; @@ -19,6 +21,8 @@ codeunit 13851 "E-Document Structured Tests" IsInitialized: Boolean; EDocumentStatusNotUpdatedErr: Label 'The status of the EDocument was not updated to the expected status after the step was executed.'; TestFileTok: Label 'oioubl/oioubl-invoice-0.xml', Locked = true; + CreditNoteTestFileTok: Label 'oioubl/oioubl-creditnote-0.xml', Locked = true; + UnsupportedXmlRootElementErr: Label 'Unsupported XML root element: %1.', Comment = '%1 = local name of the XML root element'; MockCurrencyCode: Code[10]; MockDate: Date; @@ -47,6 +51,131 @@ codeunit 13851 "E-Document Structured Tests" Assert.Fail(EDocumentStatusNotUpdatedErr); end; + [Test] + procedure TestOIOUBLInvoice_ReturnsInvoiceProcessDraftImpl() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] An OIOUBL Invoice selects the purchase invoice draft implementation + Initialize(Enum::"Service Integration"::"No Integration"); + SetupOIOUBLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, TestFileTok); + + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin + EDocument.Get(EDocument."Entry No"); + Assert.AreEqual(Enum::"E-Doc. Process Draft"::"Purchase Invoice", EDocument."Process Draft Impl.", 'The process draft implementation should be set to Purchase Invoice for invoices.'); + end else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestOIOUBLInvoice_ReReadingReplacesExistingDraftLines() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] Re-reading an OIOUBL Invoice replaces the previous staging data + Initialize(Enum::"Service Integration"::"No Integration"); + SetupOIOUBLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, TestFileTok); + ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft"); + + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then + OIOUBLStructuredValidations.AssertPurchaseLineCount(EDocument."Entry No", 2) + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestOIOUBLCreditNote_ReturnsCreditMemoProcessDraftImpl() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] An OIOUBL CreditNote selects the purchase credit memo draft implementation + Initialize(Enum::"Service Integration"::"No Integration"); + SetupOIOUBLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, CreditNoteTestFileTok); + + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin + EDocument.Get(EDocument."Entry No"); + Assert.AreEqual(Enum::"E-Doc. Process Draft"::"Purchase Credit Memo", EDocument."Process Draft Impl.", 'The process draft implementation should be set to Purchase Credit Memo for credit notes.'); + end else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestOIOUBLCreditNote_ExtractsPaymentDueDate() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] An OIOUBL CreditNote reads its due date from PaymentMeans + Initialize(Enum::"Service Integration"::"No Integration"); + SetupOIOUBLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, CreditNoteTestFileTok); + + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then + OIOUBLStructuredValidations.AssertCreditNoteDueDate(EDocument."Entry No") + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestOIOUBLCreditNote_ExtractsExternalInvoiceReference() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] An OIOUBL CreditNote stores BillingReference as the vendor's external invoice number + Initialize(Enum::"Service Integration"::"No Integration"); + SetupOIOUBLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, CreditNoteTestFileTok); + + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then + OIOUBLStructuredValidations.AssertCreditNoteExternalInvoiceReference(EDocument."Entry No") + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestOIOUBLUnsupportedRootElement_IsRejected() + var + EDocument: Record "E-Document"; + TempBlob: Codeunit "Temp Blob"; + StructuredFormatReader: Interface IStructuredFormatReader; + XmlOutStream: OutStream; + begin + // [SCENARIO] An unsupported OIOUBL document type is rejected instead of creating an empty draft + Initialize(Enum::"Service Integration"::"No Integration"); + LibraryEDoc.CreateInboundEDocument(EDocument, EDocumentService); + TempBlob.CreateOutStream(XmlOutStream, TextEncoding::UTF8); + XmlOutStream.WriteText(''); + StructuredFormatReader := Enum::"E-Doc. Read into Draft"::OIOUBL; + + asserterror StructuredFormatReader.ReadIntoDraft(EDocument, TempBlob); + + Assert.ExpectedError(StrSubstNo(UnsupportedXmlRootElementErr, 'Reminder')); + Assert.ExpectedErrorCode('Dialog'); + end; + + [Test] + procedure TestOIOUBLUnexpectedRootNamespace_IsRejected() + var + EDocument: Record "E-Document"; + TempBlob: Codeunit "Temp Blob"; + StructuredFormatReader: Interface IStructuredFormatReader; + XmlOutStream: OutStream; + begin + // [SCENARIO] An Invoice from an unsupported namespace is rejected instead of creating an empty draft + Initialize(Enum::"Service Integration"::"No Integration"); + LibraryEDoc.CreateInboundEDocument(EDocument, EDocumentService); + TempBlob.CreateOutStream(XmlOutStream, TextEncoding::UTF8); + XmlOutStream.WriteText(''); + StructuredFormatReader := Enum::"E-Doc. Read into Draft"::OIOUBL; + + asserterror StructuredFormatReader.ReadIntoDraft(EDocument, TempBlob); + + Assert.ExpectedError(StrSubstNo(UnsupportedXmlRootElementErr, 'Invoice')); + Assert.ExpectedErrorCode('Dialog'); + end; + [Test] [HandlerFunctions('EDocumentPurchaseHeaderPageHandler')] procedure TestOIOUBLInvoice_ValidDocument_ViewExtractedData() diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/test/src/OIOUBLStructuredValidations.Codeunit.al b/src/Apps/DK/EDocumentFormatOIOUBL/test/src/OIOUBLStructuredValidations.Codeunit.al index bb82d37f39b..b99b4062102 100644 --- a/src/Apps/DK/EDocumentFormatOIOUBL/test/src/OIOUBLStructuredValidations.Codeunit.al +++ b/src/Apps/DK/EDocumentFormatOIOUBL/test/src/OIOUBLStructuredValidations.Codeunit.al @@ -2,18 +2,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. // ------------------------------------------------------------------------------------------------ -codeunit 13852 "OIOUBL Structured Validations" +codeunit 148062 "OIOUBL Structured Validations" { var Assert: Codeunit Assert; UnitOfMeasureCodeTok: Label 'PCS', Locked = true; SalesInvoiceNoTok: Label '103033', Locked = true; PurchaseorderNoTok: Label '2', Locked = true; + Item1NoTok: Label 'GL00000001', Locked = true; + Item2NoTok: Label 'GL00000003', Locked = true; MockDate: Date; MockCurrencyCode: Code[10]; MockDataMismatchErr: Label 'The %1 in %2 does not align with the mock data. Expected: %3, Actual: %4', Locked = true, Comment = '%1 = Field caption, %2 = Table caption, %3 = Expected value, %4 = Actual value'; - internal procedure AssertFullEDocumentContentExtracted(EDocumentEntryNo: Integer) var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; @@ -60,8 +61,6 @@ codeunit 13852 "OIOUBL Structured Validations" internal procedure AssertPurchaseDocument(VendorNo: Code[20]; PurchaseHeader: Record "Purchase Header"; Item: Record Item) var PurchaseLine: Record "Purchase Line"; - Item1NoTok: Label 'GL00000001', Locked = true; - Item2NoTok: Label 'GL00000003', Locked = true; begin Assert.AreEqual(SalesInvoiceNoTok, PurchaseHeader."Vendor Invoice No.", StrSubstNo(MockDataMismatchErr, PurchaseHeader.FieldCaption("Vendor Invoice No."), PurchaseHeader.TableCaption(), SalesInvoiceNoTok, PurchaseHeader."Vendor Invoice No.")); Assert.AreEqual(MockDate, PurchaseHeader."Document Date", StrSubstNo(MockDataMismatchErr, PurchaseHeader.FieldCaption("Document Date"), PurchaseHeader.TableCaption(), MockDate, PurchaseHeader."Document Date")); @@ -99,6 +98,31 @@ codeunit 13852 "OIOUBL Structured Validations" Assert.AreEqual(Item2NoTok, PurchaseLine."No.", StrSubstNo(MockDataMismatchErr, PurchaseLine.FieldCaption("No."), PurchaseLine.TableCaption(), Item2NoTok, PurchaseLine."No.")); end; + internal procedure AssertCreditNoteDueDate(EDocumentEntryNo: Integer) + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + begin + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual(DMY2Date(15, 3, 2026), EDocumentPurchaseHeader."Due Date", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Due Date"), EDocumentPurchaseHeader.TableCaption(), DMY2Date(15, 3, 2026), EDocumentPurchaseHeader."Due Date")); + end; + + internal procedure AssertCreditNoteExternalInvoiceReference(EDocumentEntryNo: Integer) + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + begin + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual(SalesInvoiceNoTok, EDocumentPurchaseHeader."Applies-to Ext. Invoice No.", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Applies-to Ext. Invoice No."), EDocumentPurchaseHeader.TableCaption(), SalesInvoiceNoTok, EDocumentPurchaseHeader."Applies-to Ext. Invoice No.")); + Assert.AreEqual('', EDocumentPurchaseHeader."Applies-to Doc. No.", StrSubstNo(MockDataMismatchErr, EDocumentPurchaseHeader.FieldCaption("Applies-to Doc. No."), EDocumentPurchaseHeader.TableCaption(), '', EDocumentPurchaseHeader."Applies-to Doc. No.")); + end; + + internal procedure AssertPurchaseLineCount(EDocumentEntryNo: Integer; ExpectedCount: Integer) + var + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + Assert.RecordCount(EDocumentPurchaseLine, ExpectedCount); + end; + procedure SetMockDate(MockDate: Date) begin this.MockDate := MockDate; From 04224b6f3d2f0633aed814a3a42f6e1ab0cf6299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Fri, 24 Jul 2026 08:58:57 +0200 Subject: [PATCH 3/4] fix(ci): pass E-Document Type enum literal instead of missing header field Resolves 'Build Apps DK' compile failure in Pull Request Build: AL0132 'Record "E-Document Purchase Header"' does not contain a definition for 'E-Document Type'. The document type is constant per populate procedure and only used to branch invoice vs. credit-memo line parsing, so pass the enum literal directly into InsertOIOUBLPurchaseLines instead of storing it on a non-existent record field. [bcapps-fix-loop attempt 1] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../app/src/EDocumentOIOUBLHandler.Codeunit.al | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al b/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al index 97914cd6360..065df411401 100644 --- a/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al +++ b/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al @@ -110,7 +110,6 @@ codeunit 13913 "E-Document OIOUBL Handler" implements IStructuredFormatReader EDocumentXMLHelper: Codeunit "E-Document PEPPOL Utility"; VendorNo: Code[20]; begin - EDocumentPurchaseHeader."E-Document Type" := "E-Document Type"::"Purchase Invoice"; #pragma warning disable AA0139 EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Sales Invoice No."), EDocumentPurchaseHeader."Sales Invoice No."); EDocumentXMLHelper.SetDateValueInField(OIOUBLXml, XmlNamespaces, '/inv:Invoice/cbc:IssueDate', EDocumentPurchaseHeader."Document Date"); @@ -128,7 +127,7 @@ codeunit 13913 "E-Document OIOUBL Handler" implements IStructuredFormatReader ParseAccountingCustomerParty(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader, 'inv:Invoice'); if VendorNo <> '' then EDocumentPurchaseHeader."[BC] Vendor No." := VendorNo; - InsertOIOUBLPurchaseLines(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader."E-Document Entry No.", EDocumentPurchaseHeader."E-Document Type"); + InsertOIOUBLPurchaseLines(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader."E-Document Entry No.", "E-Document Type"::"Purchase Invoice"); end; local procedure PopulateEDocumentForCreditNote(OIOUBLXml: XmlDocument; XmlNamespaces: XmlNamespaceManager; var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; var EDocument: Record "E-Document") @@ -136,7 +135,6 @@ codeunit 13913 "E-Document OIOUBL Handler" implements IStructuredFormatReader EDocumentXMLHelper: Codeunit "E-Document PEPPOL Utility"; VendorNo: Code[20]; begin - EDocumentPurchaseHeader."E-Document Type" := "E-Document Type"::"Purchase Credit Memo"; #pragma warning disable AA0139 EDocumentXMLHelper.SetStringValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Sales Invoice No."), EDocumentPurchaseHeader."Sales Invoice No."); EDocumentXMLHelper.SetDateValueInField(OIOUBLXml, XmlNamespaces, '/cn:CreditNote/cbc:IssueDate', EDocumentPurchaseHeader."Document Date"); @@ -153,7 +151,7 @@ codeunit 13913 "E-Document OIOUBL Handler" implements IStructuredFormatReader ParseAccountingCustomerParty(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader, 'cn:CreditNote'); if VendorNo <> '' then EDocumentPurchaseHeader."[BC] Vendor No." := VendorNo; - InsertOIOUBLPurchaseLines(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader."E-Document Entry No.", EDocumentPurchaseHeader."E-Document Type"); + InsertOIOUBLPurchaseLines(OIOUBLXml, XmlNamespaces, EDocumentPurchaseHeader."E-Document Entry No.", "E-Document Type"::"Purchase Credit Memo"); end; local procedure ParseAccountingSupplierParty(OIOUBLXml: XmlDocument; XmlNamespaces: XmlNamespaceManager; var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; var EDocument: Record "E-Document"; DocumentType: Text) VendorNo: Code[20] From 8dab83345355f692cf0819d812ca5d3631a394b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Fri, 24 Jul 2026 09:34:14 +0200 Subject: [PATCH 4/4] fix(ci): sort using statements in E-Document OIOUBL Handler Resolves Build Apps DK (Clean) compile failure: AA0477 'The using statements are not in a sorted order.' The Processing.Interfaces using was listed before Processing.Import/Processing.Import.Purchase; reordered so 'Import' sorts before 'Interfaces'. [bcapps-fix-loop attempt 2] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../app/src/EDocumentOIOUBLHandler.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al b/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al index 065df411401..bf03d9bc33b 100644 --- a/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al +++ b/src/Apps/DK/EDocumentFormatOIOUBL/app/src/EDocumentOIOUBLHandler.Codeunit.al @@ -4,9 +4,9 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument; -using Microsoft.eServices.EDocument.Processing.Interfaces; using Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.eServices.EDocument.Processing.Interfaces; using Microsoft.eServices.EDocument.Service.Participant; using Microsoft.Purchases.Vendor; using System.Utilities;