diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al index 459ca00012c..3db0d09ebd8 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al @@ -10,7 +10,7 @@ enum 10971 "FR Regulatory Comment Type" value(0; None) { - Caption = ''; + Caption = ' '; } value(1; AAA) { diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingCustomerCard.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingCustomerCard.PageExt.al index 970c51477ab..cbaabe92c66 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingCustomerCard.PageExt.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingCustomerCard.PageExt.al @@ -20,18 +20,10 @@ pageextension 10972 "E-Reporting Customer Card" extends "Customer Card" { ApplicationArea = Basic, Suite; } -#if not CLEAN29 -#pragma warning disable AS0105 field("FR Elec. Address Scheme"; Rec."FR Elec. Address Scheme") { ApplicationArea = Basic, Suite; - ObsoleteReason = 'French buyer electronic addresses always use scheme 0225.'; - ObsoleteState = Pending; - ObsoleteTag = '29.0'; - Visible = false; } -#pragma warning restore AS0105 -#endif } } } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentLine.TableExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentLine.TableExt.al index 5b0422abf47..3b45dde1c83 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentLine.TableExt.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentLine.TableExt.al @@ -17,4 +17,4 @@ tableextension 10975 "FR Sales Comment Line" extends "Sales Comment Line" ToolTip = 'Specifies the French regulatory purpose of the comment. The selected type is automatically added as a prefix when the comment is exported to an electronic invoice.'; } } -} \ No newline at end of file +} diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al index c7c2255aa52..702dd6eef75 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al @@ -170,9 +170,11 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" InitNamespaceManager(NamespaceMgr, XmlDoc); + SetCustomizationId(XmlDoc, NamespaceMgr); SetFrenchBillingMode(XmlDoc, NamespaceMgr, SourceDocumentLines); RemoveZeroAllowanceTotal(XmlDoc, NamespaceMgr); InjectSupplierIdentification(XmlDoc, NamespaceMgr, CompanyInformation); + InjectBuyerIdentification(XmlDoc, NamespaceMgr, SourceDocumentHeader); InjectSupplierEndpoint(XmlDoc, NamespaceMgr, CompanyInformation, EDocumentService.Code); InjectRegulatoryComments(XmlDoc, NamespaceMgr, SourceDocumentHeader); InjectBillingReference(XmlDoc, NamespaceMgr, SourceDocumentHeader); @@ -188,14 +190,15 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" OnAfterInjectFrenchElements(TempBlob, SourceDocumentHeader, SourceDocumentLines, EDocumentService); end; - [IntegrationEvent(false, false)] - local procedure OnBeforeInjectFrenchElements(var TempBlob: Codeunit "Temp Blob"; SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; EDocumentService: Record "E-Document Service"; var IsHandled: Boolean) - begin - end; - - [IntegrationEvent(false, false)] - local procedure OnAfterInjectFrenchElements(var TempBlob: Codeunit "Temp Blob"; SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; EDocumentService: Record "E-Document Service") + local procedure SetCustomizationId(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager) + var + CustomizationIdNode: XmlNode; + NewCustomizationIdNode: XmlNode; begin + if not XmlDoc.SelectSingleNode('/*/cbc:CustomizationID', NamespaceMgr, CustomizationIdNode) then + exit; + NewCustomizationIdNode := XmlElement.Create('CustomizationID', CbcNamespaceTok, FranceCustomizationIdTok).AsXmlNode(); + CustomizationIdNode.ReplaceWith(NewCustomizationIdNode); end; local procedure SetFrenchBillingMode(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; var SourceDocumentLines: RecordRef) @@ -276,6 +279,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" SalesInvoiceHeader: Record "Sales Invoice Header"; SalesInvoiceLine: Record "Sales Invoice Line"; ShipmentPostingDates: Dictionary of [Code[20], Date]; + ShipmentBuyerReferences: Dictionary of [Code[20], Text]; CustomizationIdNode: XmlNode; NewCustomizationIdNode: XmlNode; begin @@ -283,7 +287,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" exit; SourceDocumentHeader.SetTable(SalesInvoiceHeader); - if not RequiresExtendedCTCFrance(SalesInvoiceHeader."No.", ShipmentPostingDates) then + if not RequiresExtendedCTCFrance(SalesInvoiceHeader."No.", ShipmentPostingDates, ShipmentBuyerReferences) then exit; if XmlDoc.SelectSingleNode('/*/cbc:CustomizationID', NamespaceMgr, CustomizationIdNode) then begin @@ -292,14 +296,14 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" end; SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); - SalesInvoiceLine.SetLoadFields("Line No.", "Order No.", "Order Line No.", "Shipment No."); + SalesInvoiceLine.SetLoadFields("Line No.", "Order Line No.", "Shipment No."); if SalesInvoiceLine.FindSet() then repeat - InjectExtendedLineReferences(XmlDoc, NamespaceMgr, SalesInvoiceLine, ShipmentPostingDates); + InjectExtendedLineReferences(XmlDoc, NamespaceMgr, SalesInvoiceLine, ShipmentPostingDates, ShipmentBuyerReferences); until SalesInvoiceLine.Next() = 0; end; - local procedure RequiresExtendedCTCFrance(DocumentNo: Code[20]; var ShipmentPostingDates: Dictionary of [Code[20], Date]): Boolean + local procedure RequiresExtendedCTCFrance(DocumentNo: Code[20]; var ShipmentPostingDates: Dictionary of [Code[20], Date]; var ShipmentBuyerReferences: Dictionary of [Code[20], Text]): Boolean var SalesInvoiceLine: Record "Sales Invoice Line"; SalesShipmentHeader: Record "Sales Shipment Header"; @@ -307,6 +311,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" OrderNos: Dictionary of [Text, Boolean]; DeliveryDates: Dictionary of [Text, Boolean]; ShipmentNoFilterBuilder: TextBuilder; + BuyerReference: Text; begin SalesInvoiceLine.SetRange("Document No.", DocumentNo); SalesInvoiceLine.SetLoadFields("Shipment No.", "Order No."); @@ -323,10 +328,15 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" if ShipmentNoFilterBuilder.Length() > 0 then begin SalesShipmentHeader.SetFilter("No.", ShipmentNoFilterBuilder.ToText()); - SalesShipmentHeader.SetLoadFields("Posting Date"); + SalesShipmentHeader.SetLoadFields("Posting Date", "Your Reference", "External Document No."); if SalesShipmentHeader.FindSet() then repeat ShipmentPostingDates.Add(SalesShipmentHeader."No.", SalesShipmentHeader."Posting Date"); + BuyerReference := SalesShipmentHeader."Your Reference"; + if BuyerReference = '' then + BuyerReference := SalesShipmentHeader."External Document No."; + if BuyerReference <> '' then + ShipmentBuyerReferences.Add(SalesShipmentHeader."No.", BuyerReference); AddDistinctValue(DeliveryDates, Format(SalesShipmentHeader."Posting Date", 0, 9)); until SalesShipmentHeader.Next() = 0; end; @@ -359,13 +369,14 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" Values.Add(Value, true); end; - local procedure InjectExtendedLineReferences(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; SalesInvoiceLine: Record "Sales Invoice Line"; ShipmentPostingDates: Dictionary of [Code[20], Date]) + local procedure InjectExtendedLineReferences(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; SalesInvoiceLine: Record "Sales Invoice Line"; ShipmentPostingDates: Dictionary of [Code[20], Date]; ShipmentBuyerReferences: Dictionary of [Code[20], Text]) var InvoiceLineNode: XmlNode; LineContentAnchorNode: XmlNode; OrderLineReferenceElement: XmlElement; OrderReferenceElement: XmlElement; DeliveryElement: XmlElement; + BuyerReference: Text; LineXPath: Text; ShipmentPostingDate: Date; begin @@ -375,11 +386,12 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" if not InvoiceLineNode.SelectSingleNode('cac:AllowanceCharge | cac:TaxTotal | cac:WithholdingTaxTotal | cac:Item', NamespaceMgr, LineContentAnchorNode) then exit; - if SalesInvoiceLine."Order No." <> '' then begin + BuyerReference := GetExtendedLineBuyerReference(SalesInvoiceLine, ShipmentBuyerReferences); + if BuyerReference <> '' then begin OrderLineReferenceElement := XmlElement.Create('OrderLineReference', CacNamespaceTok); OrderLineReferenceElement.Add(XmlElement.Create('LineID', CbcNamespaceTok, Format(SalesInvoiceLine."Order Line No.", 0, 9))); OrderReferenceElement := XmlElement.Create('OrderReference', CacNamespaceTok); - OrderReferenceElement.Add(XmlElement.Create('ID', CbcNamespaceTok, SalesInvoiceLine."Order No.")); + OrderReferenceElement.Add(XmlElement.Create('ID', CbcNamespaceTok, BuyerReference)); OrderLineReferenceElement.Add(OrderReferenceElement); LineContentAnchorNode.AddBeforeSelf(OrderLineReferenceElement); end; @@ -396,12 +408,25 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" LineContentAnchorNode.AddBeforeSelf(DeliveryElement); end; + local procedure GetExtendedLineBuyerReference(SalesInvoiceLine: Record "Sales Invoice Line"; ShipmentBuyerReferences: Dictionary of [Code[20], Text]) BuyerReference: Text + begin + if SalesInvoiceLine."Shipment No." <> '' then + if not ShipmentBuyerReferences.Get(SalesInvoiceLine."Shipment No.", BuyerReference) then + Clear(BuyerReference); + + OnAfterGetExtendedLineBuyerReference(SalesInvoiceLine, BuyerReference); + exit(BuyerReference); + end; + local procedure InjectRegulatoryComments(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; SourceDocumentHeader: RecordRef) var SalesCommentLine: Record "Sales Comment Line"; AnchorNode: XmlNode; DocumentNo: Code[20]; DocumentType: Enum "Sales Comment Document Type"; + RegulatoryComments: Dictionary of [Text, TextBuilder]; + RegulatoryCommentTypeCodes: List of [Text]; + RegulatoryCommentBuilder: TextBuilder; RegulatoryCommentTypeCode: Text; begin case SourceDocumentHeader.Number of @@ -437,8 +462,19 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" if SalesCommentLine.FindSet() then repeat RegulatoryCommentTypeCode := GetRegulatoryCommentTypeCode(SalesCommentLine."FR Regulatory Comment Type"); - AddRegulatoryComment(AnchorNode, RegulatoryCommentTypeCode, SalesCommentLine.Comment); + if not RegulatoryComments.Get(RegulatoryCommentTypeCode, RegulatoryCommentBuilder) then begin + Clear(RegulatoryCommentBuilder); + RegulatoryComments.Add(RegulatoryCommentTypeCode, RegulatoryCommentBuilder); + RegulatoryCommentTypeCodes.Add(RegulatoryCommentTypeCode); + end; + if SalesCommentLine.Comment.Trim() <> '' then + RegulatoryCommentBuilder.Append(SalesCommentLine.Comment.Trim()); until SalesCommentLine.Next() = 0; + + foreach RegulatoryCommentTypeCode in RegulatoryCommentTypeCodes do begin + RegulatoryComments.Get(RegulatoryCommentTypeCode, RegulatoryCommentBuilder); + AddRegulatoryComment(AnchorNode, RegulatoryCommentTypeCode, RegulatoryCommentBuilder.ToText()); + end; end; end; @@ -545,6 +581,39 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" InjectLegalEntitySIREN(PartyNode, NamespaceMgr, CopyStr(SIRENNo, 1, 20)); end; + local procedure InjectBuyerIdentification(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; SourceDocumentHeader: RecordRef) + var + Customer: Record Customer; + FRCIIXMLBuilder: Codeunit "CII XML Builder"; + CustomerNoFieldRef: FieldRef; + BuyerPartyNode: XmlNode; + CustomerNo: Code[20]; + SIRENNo: Text; + VATRegistrationNo: Text; + begin + if not XmlDoc.SelectSingleNode('//cac:AccountingCustomerParty/cac:Party', NamespaceMgr, BuyerPartyNode) then + exit; + if not FRCIIXMLBuilder.TryGetCustomerNoFieldRef(SourceDocumentHeader, CustomerNoFieldRef) then + exit; + CustomerNo := CustomerNoFieldRef.Value(); + Customer.SetLoadFields("Registration Number", "FR Electronic Address", "VAT Registration No."); + if not Customer.Get(CustomerNo) then + exit; + + SIRENNo := GetSIRENNo(Customer."Registration Number", Customer."Registration Number"); + if SIRENNo = '' then + if IsNumericIdentifier(CopyStr(Customer."FR Electronic Address", 1, 9), 9) then + SIRENNo := CopyStr(Customer."FR Electronic Address", 1, 9); + if SIRENNo = '' then begin + VATRegistrationNo := DelChr(Customer."VAT Registration No.", '=', ' '); + if (StrLen(VATRegistrationNo) = 13) and (CopyStr(VATRegistrationNo, 1, 2).ToUpper() = 'FR') then + if IsNumericIdentifier(CopyStr(VATRegistrationNo, 5, 9), 9) then + SIRENNo := CopyStr(VATRegistrationNo, 5, 9); + end; + if SIRENNo <> '' then + InjectLegalEntitySIREN(BuyerPartyNode, NamespaceMgr, CopyStr(SIRENNo, 1, 20)); + end; + local procedure GetSIRENNo(RegistrationNo: Text; SIRETNo: Text): Text begin if IsNumericIdentifier(SIRETNo, 14) then @@ -663,18 +732,19 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" if CustomerNo = '' then exit(false); - if GetServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Customer, CustomerNo, ElecAddress, ElecAddressScheme) then begin - ElecAddressScheme := ElecAddressScheme::"0225"; + if GetServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Customer, CustomerNo, ElecAddress, ElecAddressScheme) then exit(true); - end; - Customer.SetLoadFields("FR Electronic Address", "Registration Number", "VAT Registration No."); + Customer.SetLoadFields("FR Electronic Address", "FR Elec. Address Scheme", "Registration Number", "VAT Registration No."); if not Customer.Get(CustomerNo) then exit(false); if not FREDocHelpers.GetBuyerElectronicAddress(Customer, ElecAddress) then exit(false); - ElecAddressScheme := ElecAddressScheme::"0225"; + if (Customer."FR Electronic Address" <> '') and (Customer."FR Elec. Address Scheme" <> Customer."FR Elec. Address Scheme"::" ") then + ElecAddressScheme := Customer."FR Elec. Address Scheme" + else + ElecAddressScheme := ElecAddressScheme::"0225"; exit(true); end; @@ -778,10 +848,26 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" EDocServiceSupportedType.Insert(); end; + [IntegrationEvent(false, false)] + local procedure OnBeforeInjectFrenchElements(var TempBlob: Codeunit "Temp Blob"; SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; EDocumentService: Record "E-Document Service"; var IsHandled: Boolean) + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnAfterInjectFrenchElements(var TempBlob: Codeunit "Temp Blob"; SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; EDocumentService: Record "E-Document Service") + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnAfterGetExtendedLineBuyerReference(SalesInvoiceLine: Record "Sales Invoice Line"; var BuyerReference: Text) + begin + end; + var CbcNamespaceTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', Locked = true; CacNamespaceTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', Locked = true; - ExtendedCTCFranceCustomizationIdTok: Label 'EXTENDED-CTC-FR', Locked = true; + FranceCustomizationIdTok: Label 'urn:cen.eu:en16931:2017', Locked = true; + ExtendedCTCFranceCustomizationIdTok: Label 'urn:cen.eu:en16931:2017#conformant#urn.cpro.gouv.fr:1p0:extended-ctc-fr', Locked = true; RegulatoryCommentFormatTok: Label '#%1#%2', Comment = '%1 = Regulatory comment type, %2 = Comment text', Locked = true; BillingModeB1Tok: Label 'B1', Locked = true; BillingModeS1Tok: Label 'S1', Locked = true; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/EDocFRStructImportTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/EDocFRStructImportTests.Codeunit.al index af1abb67b3a..4c4842b88ed 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/EDocFRStructImportTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/EDocFRStructImportTests.Codeunit.al @@ -2,6 +2,14 @@ // 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.Formats.Test; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Formats; +using Microsoft.eServices.EDocument.Processing.Import; +using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using System.Utilities; + codeunit 148149 "E-Doc. FR Struct. Import Tests" { Subtype = Test; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al index 3b49ed337f2..2980877166e 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al @@ -54,6 +54,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" EDocHelpers: Codeunit "EDoc. Helpers"; PeppolBIS30FRFormat: Codeunit "Peppol BIS 3.0 FR Format"; IncorrectValueErr: Label 'Incorrect value for %1', Comment = '%1 = XML element path', Locked = true; + InvoiceLineXPathLbl: Label '/Invoice/cac:InvoiceLine[cbc:ID=''%1'']/cac:OrderLineReference/cac:OrderReference/cbc:ID', Comment = '%1 = Invoice line number', Locked = true; DialogErrorCodeTok: Label 'Dialog', Locked = true; IsInitialized: Boolean; @@ -146,7 +147,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" // [GIVEN] Posted sales invoice for customer with FR electronic address CustomerAddress := '123456789'; - SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer(CustomerAddress, "Electronic Address Scheme"::"0002"))); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer(CustomerAddress, "Electronic Address Scheme"::"0225"))); // [WHEN] Export FR PEPPOL XML ExportInvoice(SalesInvoiceHeader, XmlDoc); @@ -198,7 +199,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" // [GIVEN] Posted sales invoice for customer with FR electronic address in SIREN_suffix format CustomerAddress := '123456789_001'; - SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer(CustomerAddress, "Electronic Address Scheme"::"0009"))); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer(CustomerAddress, "Electronic Address Scheme"::"0225"))); // [WHEN] Export FR PEPPOL XML ExportInvoice(SalesInvoiceHeader, XmlDoc); @@ -213,14 +214,14 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" end; [Test] - procedure ExportSalesInvIgnoresConfiguredSchemeForBuyerEndpoint() + procedure ExportSalesInvUsesConfiguredSchemeForBuyerEndpoint() var SalesInvoiceHeader: Record "Sales Invoice Header"; XmlDoc: XmlDocument; CustomerAddress: Text[250]; begin // [FEATURE] [AI test] - // [SCENARIO] Export in PEPPOL BIS 3.0 FR always uses scheme 0225 regardless of customer configured scheme + // [SCENARIO] Export in PEPPOL BIS 3.0 FR uses the scheme configured for the customer electronic address Initialize(); // [GIVEN] Posted sales invoice for customer with FR electronic address and configured scheme 0002 @@ -230,8 +231,8 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" // [WHEN] Export FR PEPPOL XML ExportInvoice(SalesInvoiceHeader, XmlDoc); - // [THEN] Buyer EndpointID uses scheme 0225 regardless of configured 0002 - Assert.AreEqual('0225', + // [THEN] Buyer EndpointID uses the configured scheme 0002 + Assert.AreEqual('0002', GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'), StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); @@ -242,7 +243,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" end; [Test] - procedure ExportSalesInvNormalizesBuyerServiceParticipantScheme() + procedure ExportSalesInvUsesBuyerServiceParticipantScheme() var ServiceParticipant: Record "Service Participant"; SalesInvoiceHeader: Record "Sales Invoice Header"; @@ -251,7 +252,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" EndpointId: Text[200]; begin // [FEATURE] [AI test] - // [SCENARIO] Export in PEPPOL BIS 3.0 FR normalizes service participant scheme to 0225 regardless of configured enum + // [SCENARIO] Export in PEPPOL BIS 3.0 FR uses the scheme configured for the service participant Initialize(); // [GIVEN] Customer with service participant using configured scheme 0002 @@ -269,24 +270,55 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" CheckInvoice(SalesInvoiceHeader); ExportInvoice(SalesInvoiceHeader, XmlDoc); - // [THEN] Buyer EndpointID uses scheme 0225 even though participant was configured with 0002 + // [THEN] Buyer EndpointID uses the participant's configured scheme 0002 Assert.AreEqual(EndpointId, GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID'), StrSubstNo(IncorrectValueErr, 'Buyer EndpointID')); - Assert.AreEqual('0225', + Assert.AreEqual('0002', GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'), StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); end; [Test] - procedure ExportSalesInvDoesNotSynthesizeBuyerPartyIdentification() + procedure ExportSalesInvInjectsBuyerLegalEntitySIREN() + var + Customer: Record Customer; + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + CustomerNo: Code[20]; + begin + // [FEATURE] [AI test 0.4] + // [SCENARIO] Export in PEPPOL BIS 3.0 FR injects the buyer SIREN as the legal registration identifier + Initialize(); + + // [GIVEN] Customer "C" with blank Registration Number and FR electronic address in SIREN_suffix format + CustomerNo := CreateCustomer('123456789_001', "Electronic Address Scheme"::"0225"); + Customer.Get(CustomerNo); + Customer.Validate("Registration Number", ''); + Customer.Modify(true); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + + // [WHEN] Posted sales invoice "SI" is exported in PEPPOL BIS 3.0 FR + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] Buyer PartyLegalEntity CompanyID contains the SIREN from the FR electronic address with scheme 0002 + Assert.AreEqual('123456789', + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID'), + StrSubstNo(IncorrectValueErr, 'Buyer CompanyID')); + Assert.AreEqual('0002', + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID/@schemeID'), + StrSubstNo(IncorrectValueErr, 'Buyer CompanyID schemeID')); + end; + + [Test] + procedure ExportSalesInvPreservesConfiguredSIRETScheme() var SalesInvoiceHeader: Record "Sales Invoice Header"; XmlDoc: XmlDocument; CustomerAddress: Text[250]; begin // [FEATURE] [AI test] - // [SCENARIO] Export in PEPPOL BIS 3.0 FR does not synthesize buyer PartyIdentification even with 0009 configured + // [SCENARIO] Export in PEPPOL BIS 3.0 FR preserves a configured SIRET endpoint scheme without synthesizing PartyIdentification Initialize(); // [GIVEN] Posted sales invoice for customer with FR electronic address and configured scheme 0009 @@ -296,11 +328,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" // [WHEN] Export FR PEPPOL XML ExportInvoice(SalesInvoiceHeader, XmlDoc); - // [THEN] Buyer EndpointID uses scheme 0225 (not the configured 0009) + // [THEN] Buyer EndpointID uses the configured scheme 0009 Assert.AreEqual(CustomerAddress, GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID'), StrSubstNo(IncorrectValueErr, 'Buyer EndpointID')); - Assert.AreEqual('0225', + Assert.AreEqual('0009', GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'), StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); @@ -360,6 +392,90 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" StrSubstNo(IncorrectValueErr, 'Ordinary note should be empty')); end; + [Test] + procedure ExportSalesInvConcatenatesRegulatoryCommentsOfSameType() + var + SalesCommentLine: Record "Sales Comment Line"; + SalesHeader: Record "Sales Header"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + FirstCommentLine: Text[80]; + SecondCommentLine: Text[80]; + CustomerNo: Code[20]; + InvoiceNo: Code[20]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Multiple PMD comment lines are concatenated into one tagged UBL note + Initialize(); + + // [GIVEN] Sales invoice "SI" with the mandatory PMD text split across two comment lines + CustomerNo := CreateCustomer('', "Electronic Address Scheme"::"EM"); + InvoiceNo := CreateSalesInvoiceWithLine(CustomerNo); + FirstCommentLine := 'Tout retard de paiement engendre une pénalité exigible à compter de la date'; + SecondCommentLine := 'd''échéance, calculée sur la base de trois fois le taux d''intérêt légal.'; + LibrarySales.CreateSalesCommentLine(SalesCommentLine, "Sales Document Type"::Invoice, InvoiceNo, 0); + SalesCommentLine.Validate("FR Regulatory Comment Type", SalesCommentLine."FR Regulatory Comment Type"::PMD); + SalesCommentLine.Validate(Comment, FirstCommentLine); + SalesCommentLine.Modify(true); + LibrarySales.CreateSalesCommentLine(SalesCommentLine, "Sales Document Type"::Invoice, InvoiceNo, 0); + SalesCommentLine.Validate("FR Regulatory Comment Type", SalesCommentLine."FR Regulatory Comment Type"::PMD); + SalesCommentLine.Validate(Comment, SecondCommentLine); + SalesCommentLine.Modify(true); + SalesHeader.Get("Sales Document Type"::Invoice, InvoiceNo); + SalesInvoiceHeader.Get(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + + // [WHEN] Posted sales invoice "SI" is exported in PEPPOL BIS 3.0 FR + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] Both lines are concatenated into one PMD note without repeating the tag + Assert.AreEqual('#PMD#' + FirstCommentLine + SecondCommentLine, + GetNodeByPath(XmlDoc, '/Invoice/cbc:Note[contains(., ''#PMD#'')]'), + StrSubstNo(IncorrectValueErr, 'PMD regulatory note')); + Assert.AreEqual('', + GetNodeByPath(XmlDoc, '/Invoice/cbc:Note[contains(., ''#PMD#'')][2]'), + StrSubstNo(IncorrectValueErr, 'Second PMD regulatory note should be empty')); + Assert.AreEqual('', + GetNodeByPath(XmlDoc, '/Invoice/cbc:Note[contains(substring-after(., ''#PMD#''), ''#PMD#'')]'), + StrSubstNo(IncorrectValueErr, 'PMD regulatory note with repeated tag should be empty')); + end; + + [Test] + procedure ExportSalesInvTrimsRegulatoryCommentsBeforeConcatenating() + var + SalesCommentLine: Record "Sales Comment Line"; + SalesHeader: Record "Sales Header"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + CustomerNo: Code[20]; + InvoiceNo: Code[20]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Regulatory comment lines are trimmed and concatenated without a separator + Initialize(); + + // [GIVEN] Sales invoice "SI" with two padded PMD comment lines + CustomerNo := CreateCustomer('', "Electronic Address Scheme"::"EM"); + InvoiceNo := CreateSalesInvoiceWithLine(CustomerNo); + LibrarySales.CreateSalesCommentLine(SalesCommentLine, "Sales Document Type"::Invoice, InvoiceNo, 0); + SalesCommentLine.Validate("FR Regulatory Comment Type", SalesCommentLine."FR Regulatory Comment Type"::PMD); + SalesCommentLine.Validate(Comment, ' First comment '); + SalesCommentLine.Modify(true); + LibrarySales.CreateSalesCommentLine(SalesCommentLine, "Sales Document Type"::Invoice, InvoiceNo, 0); + SalesCommentLine.Validate("FR Regulatory Comment Type", SalesCommentLine."FR Regulatory Comment Type"::PMD); + SalesCommentLine.Validate(Comment, ' Second comment '); + SalesCommentLine.Modify(true); + SalesHeader.Get("Sales Document Type"::Invoice, InvoiceNo); + SalesInvoiceHeader.Get(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + + // [WHEN] Posted sales invoice "SI" is exported in PEPPOL BIS 3.0 FR + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] The trimmed lines are concatenated directly + Assert.AreEqual('#PMD#First commentSecond comment', + GetNodeByPath(XmlDoc, '/Invoice/cbc:Note[contains(., ''#PMD#'')]'), + StrSubstNo(IncorrectValueErr, 'PMD regulatory note')); + end; + [Test] procedure ExportSalesInvDoesNotIncludeBillingReference() var @@ -500,7 +616,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" // [GIVEN] Posted sales credit memo for customer with FR electronic address CustomerAddress := '123456789'; - SalesCrMemoHeader.Get(CreateAndPostSalesCrMemo(CreateCustomer(CustomerAddress, "Electronic Address Scheme"::"0002"))); + SalesCrMemoHeader.Get(CreateAndPostSalesCrMemo(CreateCustomer(CustomerAddress, "Electronic Address Scheme"::"0225"))); // [WHEN] Export FR PEPPOL XML ExportCrMemo(SalesCrMemoHeader, XmlDoc); @@ -657,12 +773,12 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" ExportInvoice(SalesInvoiceHeader, XmlDoc); - Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), + Assert.AreEqual('urn:cen.eu:en16931:2017#conformant#urn.cpro.gouv.fr:1p0:extended-ctc-fr', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); SalesInvoiceLine.SetFilter(Type, '<>%1', SalesInvoiceLine.Type::" "); SalesInvoiceLine.FindFirst(); - Assert.AreEqual(SalesInvoiceLine."Order No.", GetNodeByPath(XmlDoc, '/Invoice/cac:InvoiceLine/cac:OrderLineReference[following-sibling::cac:AllowanceCharge]/cac:OrderReference/cbc:ID'), + Assert.AreEqual('FR-BUYER-REF', GetNodeByPath(XmlDoc, '/Invoice/cac:InvoiceLine/cac:OrderLineReference[following-sibling::cac:AllowanceCharge]/cac:OrderReference/cbc:ID'), StrSubstNo(IncorrectValueErr, 'OrderReference ID')); Assert.AreEqual(Format(SalesInvoiceLine."Order Line No.", 0, 9), GetNodeByPath(XmlDoc, '/Invoice/cac:InvoiceLine/cac:OrderLineReference/cbc:LineID'), StrSubstNo(IncorrectValueErr, 'OrderLineReference LineID')); @@ -685,7 +801,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" ExportInvoice(SalesInvoiceHeader, XmlDoc); - Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), + Assert.AreEqual('urn:cen.eu:en16931:2017#conformant#urn.cpro.gouv.fr:1p0:extended-ctc-fr', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); SalesInvoiceLine.SetFilter(Type, '<>%1', SalesInvoiceLine.Type::" "); @@ -697,6 +813,79 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" StrSubstNo(IncorrectValueErr, 'ActualDeliveryDate')); end; + [Test] + procedure ExportSalesInvUsesShipmentExternalDocumentNoAsBuyerReference() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + BuyerReference: Text[35]; + CustomerNo: Code[20]; + FirstShipmentNo: Code[20]; + SecondShipmentNo: Code[20]; + begin + // [FEATURE] [AI test 0.4] + // [SCENARIO] An empty shipment buyer reference falls back to its external document number + Initialize(); + + // [GIVEN] Customer "C" with two distinct posted shipments "S1" and "S2" using the same external document number + CustomerNo := CreateCustomer('123456789', "Electronic Address Scheme"::"0002"); + FirstShipmentNo := CreateAndPostSalesOrderShipment(CustomerNo, 1, 1, 10); + SecondShipmentNo := CreateAndPostSalesOrderShipment(CustomerNo, 1, 1, 10); + BuyerReference := 'FR-EXTERNAL-REF'; + SetSalesShipmentExternalDocumentNo(FirstShipmentNo, BuyerReference); + SetSalesShipmentExternalDocumentNo(SecondShipmentNo, BuyerReference); + + // [WHEN] Combined invoice "I" is created from "S1" and "S2" and exported + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceFromShipments(CustomerNo, FirstShipmentNo + '|' + SecondShipmentNo)); + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] Invoice "I" uses the Extended CTC France customization + Assert.AreEqual('urn:cen.eu:en16931:2017#conformant#urn.cpro.gouv.fr:1p0:extended-ctc-fr', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), + StrSubstNo(IncorrectValueErr, 'CustomizationID')); + + // [THEN] Invoice "I" uses the shipment external document number as buyer reference + Assert.AreEqual(BuyerReference, GetNodeByPath(XmlDoc, '/Invoice/cac:InvoiceLine/cac:OrderLineReference/cac:OrderReference/cbc:ID'), + StrSubstNo(IncorrectValueErr, 'OrderReference ID')); + end; + + [Test] + procedure ExportSalesInvAllowsShipmentWithoutBuyerReference() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + XmlDoc: XmlDocument; + BuyerReference: Text[35]; + CustomerNo: Code[20]; + FirstShipmentNo: Code[20]; + InvoiceLineXPath: Text; + SecondShipmentNo: Code[20]; + begin + // [FEATURE] [AI test 0.4] + // [SCENARIO] An Extended CTC invoice can include a shipment without a buyer reference + Initialize(); + + // [GIVEN] Customer "C" with two distinct shipments where only "S2" has a buyer reference + CustomerNo := CreateCustomer('123456789', "Electronic Address Scheme"::"0002"); + FirstShipmentNo := CreateAndPostSalesOrderShipment(CustomerNo, 1, 1, 10); + SecondShipmentNo := CreateAndPostSalesOrderShipment(CustomerNo, 1, 1, 10); + BuyerReference := 'FR-EXTERNAL-REF'; + SetSalesShipmentExternalDocumentNo(SecondShipmentNo, BuyerReference); + + // [WHEN] Combined invoice "I" is created from "S1" and "S2" and exported + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceFromShipments(CustomerNo, FirstShipmentNo + '|' + SecondShipmentNo)); + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] Invoice "I" uses Extended CTC France and exports the available buyer reference + Assert.AreEqual('urn:cen.eu:en16931:2017#conformant#urn.cpro.gouv.fr:1p0:extended-ctc-fr', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), + StrSubstNo(IncorrectValueErr, 'CustomizationID')); + SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + SalesInvoiceLine.SetRange("Shipment No.", SecondShipmentNo); + SalesInvoiceLine.FindFirst(); + InvoiceLineXPath := StrSubstNo(InvoiceLineXPathLbl, Format(SalesInvoiceLine."Line No.", 0, 9)); + Assert.AreEqual(BuyerReference, GetNodeByPath(XmlDoc, InvoiceLineXPath), + StrSubstNo(IncorrectValueErr, 'OrderReference ID')); + end; + [Test] procedure ExportSalesInvKeepsBasicCTCForRepeatedReferences() var @@ -710,7 +899,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" ExportInvoice(SalesInvoiceHeader, XmlDoc); - Assert.AreEqual('urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), + Assert.AreEqual('urn:cen.eu:en16931:2017', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); end; #endregion @@ -961,6 +1150,12 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Assert.AreEqual('0225', GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'), StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); + Assert.AreEqual('945627890', + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID'), + StrSubstNo(IncorrectValueErr, 'Buyer CompanyID')); + Assert.AreEqual('0002', + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID/@schemeID'), + StrSubstNo(IncorrectValueErr, 'Buyer CompanyID schemeID')); end; [Test] @@ -1353,6 +1548,16 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" exit(LibrarySales.PostSalesDocument(SalesHeader, true, false)); end; + local procedure SetSalesShipmentExternalDocumentNo(ShipmentNo: Code[20]; ExternalDocumentNo: Code[35]) + var + SalesShipmentHeader: Record "Sales Shipment Header"; + begin + SalesShipmentHeader.Get(ShipmentNo); + SalesShipmentHeader.Validate("Your Reference", ''); + SalesShipmentHeader.Validate("External Document No.", ExternalDocumentNo); + SalesShipmentHeader.Modify(true); + end; + local procedure CreateSalesOrderWithLines(var SalesHeader: Record "Sales Header"; CustomerNo: Code[20]; NumberOfLines: Integer; Quantity: Decimal) var Customer: Record Customer; @@ -1372,6 +1577,8 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Customer.Modify(true); LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Order, CustomerNo); + SalesHeader.Validate("Your Reference", 'FR-BUYER-REF'); + SalesHeader.Modify(true); for LineIndex := 1 to NumberOfLines do begin LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", Quantity); SalesLine.Validate("Unit Price", 100); diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al index 5e12050a6e8..144b441a974 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al @@ -570,6 +570,7 @@ codeunit 6103 "E-Document Subscribers" begin DataClassificationEvalData.SetTableFieldsToNormal(Database::"E-Doc. Service Data Exch. Def."); DataClassificationEvalData.SetTableFieldsToNormal(Database::"E-Document"); + DataClassificationEvalData.SetTableFieldsToNormal(Database::"E-Document Message"); #if not CLEAN28 #pragma warning disable AL0432 DataClassificationEvalData.SetTableFieldsToNormal(Database::"E-Documents Setup");