diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/CIIXMLBuilder.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/CIIXMLBuilder.Codeunit.al index ad44e43245d..f216f1c3e8e 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/CIIXMLBuilder.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/CIIXMLBuilder.Codeunit.al @@ -304,6 +304,10 @@ codeunit 10978 "CII XML Builder" exit('0009'); ElecAddressScheme::"0002": exit('0002'); + ElecAddressScheme::"0225": + exit('0225'); + ElecAddressScheme::"9957": + exit('9957'); else exit(Format(ElecAddressScheme)); end; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/EDocHelpers.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/EDocHelpers.Codeunit.al index 16370d26867..a68e88e16e9 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/EDocHelpers.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/EDocHelpers.Codeunit.al @@ -4,12 +4,71 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Formats; +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Service.Participant; using Microsoft.Foundation.Company; using Microsoft.Sales.Customer; +using Microsoft.Utilities; codeunit 10991 "EDoc. Helpers" { Access = Internal; + Permissions = tabledata "E-Document" = m, + tabledata "E-Document Service Status" = m; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Data Classification Eval. Data", 'OnCreateEvaluationDataOnAfterClassifyTablesToNormal', '', false, false)] + local procedure ClassifyDataSensitivity() + var + DataClassificationEvalData: Codeunit "Data Classification Eval. Data"; + begin + DataClassificationEvalData.SetTableFieldsToNormal(Database::"FR E-Invoice Lifecycle"); + DataClassificationEvalData.SetTableFieldsToNormal(Database::"FR E-Invoice Lifecycle VAT"); + end; + + [EventSubscriber(ObjectType::Table, Database::"E-Document", 'OnBeforeModifyEvent', '', false, false)] + local procedure SetClearanceDateOnModify(var Rec: Record "E-Document"; var xRec: Record "E-Document"; RunTrigger: Boolean) + var + EDocumentServiceStatus: Record "E-Document Service Status"; + EDocumentService: Record "E-Document Service"; + begin + if not GetFrenchEDocumentService(Rec, EDocumentService, EDocumentServiceStatus) then + exit; + + case EDocumentServiceStatus.Status of + EDocumentServiceStatus.Status::Approved, + EDocumentServiceStatus.Status::Cleared: + if Rec."Clearance Date" = 0DT then + Rec."Clearance Date" := CurrentDateTime(); + EDocumentServiceStatus.Status::Rejected, + EDocumentServiceStatus.Status::"Not Cleared": + Rec."Clearance Date" := 0DT; + end; + end; + + local procedure IsFrenchElectronicDocumentFormat(EDocumentFormat: Enum "E-Document Format"): Boolean + begin + exit( + EDocumentFormat in + [EDocumentFormat::"E-Reporting FR", EDocumentFormat::"Peppol BIS 3.0 FR", EDocumentFormat::"Factur-X FR"]); + end; + + procedure GetFrenchEDocumentService(EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; var EDocumentServiceStatus: Record "E-Document Service Status"): Boolean + begin + EDocumentService.SetLoadFields("Document Format"); + if (EDocument.Service <> '') and EDocumentService.Get(EDocument.Service) then + if IsFrenchElectronicDocumentFormat(EDocumentService."Document Format") then + exit(EDocumentServiceStatus.Get(EDocument."Entry No", EDocumentService.Code)); + + EDocumentServiceStatus.SetRange("E-Document Entry No", EDocument."Entry No"); + if EDocumentServiceStatus.FindSet() then + repeat + if EDocumentService.Get(EDocumentServiceStatus."E-Document Service Code") then + if IsFrenchElectronicDocumentFormat(EDocumentService."Document Format") then + exit(true); + until EDocumentServiceStatus.Next() = 0; + + exit(false); + end; procedure FindFieldByName(RecRef: RecordRef; FieldName: Text; var FieldRefResult: FieldRef): Boolean var @@ -46,7 +105,9 @@ codeunit 10991 "EDoc. Helpers" begin CompanyInformation.Get(); if CompanyInformation."Registration No." = '' then - Error(SIRENRequiredErr); + RaiseCompanyInformationError( + CompanyInformation, + StrSubstNo(SIRENRequiredErr, CompanyInformation.FieldCaption("Registration No."), CompanyInformation.TableCaption())); end; procedure CheckSIRETNotEmpty() @@ -55,7 +116,33 @@ codeunit 10991 "EDoc. Helpers" begin CompanyInformation.Get(); if CompanyInformation."SIRET No." = '' then - Error(SIRETRequiredErr); + RaiseCompanyInformationError( + CompanyInformation, + StrSubstNo(SIRETRequiredErr, CompanyInformation.FieldCaption("SIRET No."), CompanyInformation.TableCaption())); + end; + + procedure CheckSellerElectronicAddress(EDocumentServiceCode: Code[20]) + var + CompanyInformation: Record "Company Information"; + ServiceParticipant: Record "Service Participant"; + begin + if HasServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Company, '') then + exit; + + CompanyInformation.Get(); + if CompanyInformation."SIRET No." <> '' then + exit; + if CompanyInformation."Registration No." <> '' then + exit; + if CompanyInformation.GetVATRegistrationNumber() <> '' then + exit; + + RaiseCompanyInformationError( + CompanyInformation, + StrSubstNo( + SellerElectronicAddressRequiredErr, CompanyInformation.FieldCaption("SIRET No."), + CompanyInformation.FieldCaption("Registration No."), CompanyInformation.FieldCaption("VAT Registration No."), + ServiceParticipant.TableCaption())); end; procedure CheckSellerCountryCode() @@ -64,12 +151,20 @@ codeunit 10991 "EDoc. Helpers" begin CompanyInformation.Get(); if CompanyInformation."Country/Region Code" = '' then - Error(SellerCountryCodeRequiredErr); + RaiseCompanyInformationError( + CompanyInformation, + StrSubstNo(SellerCountryCodeRequiredErr, CompanyInformation.FieldCaption("Country/Region Code"), CompanyInformation.TableCaption())); end; procedure CheckBuyerElectronicAddress(var SourceDocumentHeader: RecordRef) + begin + CheckBuyerElectronicAddress(SourceDocumentHeader, ''); + end; + + procedure CheckBuyerElectronicAddress(var SourceDocumentHeader: RecordRef; EDocumentServiceCode: Code[20]) var Customer: Record Customer; + ServiceParticipant: Record "Service Participant"; FRCIIXMLBuilder: Codeunit "CII XML Builder"; CustomerNoFieldRef: FieldRef; CustomerNo: Code[20]; @@ -81,16 +176,97 @@ codeunit 10991 "EDoc. Helpers" if CustomerNo = '' then exit; + if HasServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Customer, CustomerNo) then + exit; + + Customer.SetLoadFields("FR Electronic Address", "FR Elec. Address Scheme", "VAT Registration No."); if not Customer.Get(CustomerNo) then exit; - if Customer."FR Electronic Address" = '' then - Error(BuyerElectronicAddressRequiredErr, Customer."No."); + if Customer."FR Electronic Address" <> '' then begin + if Customer."FR Elec. Address Scheme" = Customer."FR Elec. Address Scheme"::" " then + RaiseCustomerError( + Customer, StrSubstNo(BuyerElectronicAddressSchemeRequiredErr, Customer.FieldCaption("FR Elec. Address Scheme"))); + exit; + end; + if Customer."VAT Registration No." <> '' then + exit; + + RaiseCustomerError( + Customer, + StrSubstNo( + BuyerElectronicAddressRequiredErr, Customer.FieldCaption("FR Electronic Address"), + Customer.FieldCaption("VAT Registration No."), ServiceParticipant.TableCaption())); + end; + + procedure HasServiceParticipantAddress(EDocumentServiceCode: Code[20]; ParticipantType: Enum "E-Document Source Type"; ParticipantNo: Code[20]): Boolean + var + ServiceParticipant: Record "Service Participant"; + begin + exit(HasServiceParticipantAddress(EDocumentServiceCode, ParticipantType, ParticipantNo, ServiceParticipant)); + end; + + procedure HasServiceParticipantAddress(EDocumentServiceCode: Code[20]; ParticipantType: Enum "E-Document Source Type"; ParticipantNo: Code[20]; var ServiceParticipant: Record "Service Participant"): Boolean + var + ParticipantAddressErrorInfo: ErrorInfo; + HasIdentifier: Boolean; + HasScheme: Boolean; + begin + if EDocumentServiceCode = '' then + exit(false); + if not ServiceParticipant.Get(EDocumentServiceCode, ParticipantType, ParticipantNo) then + exit(false); + + HasIdentifier := ServiceParticipant."Participant Identifier" <> ''; + HasScheme := ServiceParticipant."FR Identifier Scheme" <> ServiceParticipant."FR Identifier Scheme"::" "; + if HasIdentifier <> HasScheme then begin + ParticipantAddressErrorInfo.Title(StrSubstNo(SetupIncompleteTitleLbl, ServiceParticipant.TableCaption())); + ParticipantAddressErrorInfo.Message( + StrSubstNo(ServiceParticipantAddressIncompleteErr, ServiceParticipant.FieldCaption("Participant Identifier"), ServiceParticipant.FieldCaption("FR Identifier Scheme"))); + ParticipantAddressErrorInfo.DetailedMessage(ParticipantAddressErrorInfo.Message()); + ParticipantAddressErrorInfo.RecordId(ServiceParticipant.RecordId()); + ParticipantAddressErrorInfo.PageNo(Page::"Service Participants"); + ParticipantAddressErrorInfo.AddNavigationAction(StrSubstNo(ShowRecordLbl, ServiceParticipant.TableCaption())); + Error(ParticipantAddressErrorInfo); + end; + + exit(HasIdentifier); + end; + + local procedure RaiseCompanyInformationError(CompanyInformation: Record "Company Information"; ErrorMessage: Text) + var + CompanyInformationErrorInfo: ErrorInfo; + begin + CompanyInformationErrorInfo.Title(StrSubstNo(SetupIncompleteTitleLbl, CompanyInformation.TableCaption())); + CompanyInformationErrorInfo.Message(ErrorMessage); + CompanyInformationErrorInfo.DetailedMessage(ErrorMessage); + CompanyInformationErrorInfo.RecordId(CompanyInformation.RecordId()); + CompanyInformationErrorInfo.PageNo(Page::"Company Information"); + CompanyInformationErrorInfo.AddNavigationAction(StrSubstNo(ShowRecordLbl, CompanyInformation.TableCaption())); + Error(CompanyInformationErrorInfo); + end; + + local procedure RaiseCustomerError(Customer: Record Customer; ErrorMessage: Text) + var + CustomerErrorInfo: ErrorInfo; + begin + CustomerErrorInfo.Title(StrSubstNo(SetupIncompleteTitleLbl, Customer.TableCaption())); + CustomerErrorInfo.Message(ErrorMessage); + CustomerErrorInfo.DetailedMessage(ErrorMessage); + CustomerErrorInfo.RecordId(Customer.RecordId()); + CustomerErrorInfo.PageNo(Page::"Customer Card"); + CustomerErrorInfo.AddNavigationAction(StrSubstNo(ShowRecordLbl, Customer.TableCaption())); + Error(CustomerErrorInfo); end; var - SIRENRequiredErr: Label 'Registration No. must be specified in Company Information for French e-invoicing.'; - SIRETRequiredErr: Label 'SIRET No. must be specified in Company Information for French e-invoicing.'; - BuyerElectronicAddressRequiredErr: Label 'Electronic Address must be specified for Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.'; - SellerCountryCodeRequiredErr: Label 'Country/Region Code must be specified in Company Information for French e-invoicing.'; + SetupIncompleteTitleLbl: Label '%1 setup is incomplete', Comment = '%1 = table caption'; + ShowRecordLbl: Label 'Show %1', Comment = '%1 = table caption'; + SIRENRequiredErr: Label '%1 must be specified in %2 for French e-invoicing.', Comment = '%1 = Registration No. field caption, %2 = Company Information table caption'; + SIRETRequiredErr: Label '%1 must be specified in %2 for French e-invoicing.', Comment = '%1 = SIRET No. field caption, %2 = Company Information table caption'; + SellerElectronicAddressRequiredErr: Label '%1, %2, %3, or a %4 identifier must be specified for the company for French e-invoicing.', Comment = '%1 = SIRET No. field caption, %2 = Registration No. field caption, %3 = VAT Registration No. field caption, %4 = Service Participant table caption'; + BuyerElectronicAddressRequiredErr: Label '%1, %2, or a %3 identifier must be specified for the customer for French e-invoicing.', Comment = '%1 = Electronic Address field caption, %2 = VAT Registration No. field caption, %3 = Service Participant table caption'; + BuyerElectronicAddressSchemeRequiredErr: Label '%1 must be specified for the customer for French e-invoicing.', Comment = '%1 = Electronic Address Scheme field caption'; + SellerCountryCodeRequiredErr: Label '%1 must be specified in %2 for French e-invoicing.', Comment = '%1 = Country/Region Code field caption, %2 = Company Information table caption'; + ServiceParticipantAddressIncompleteErr: Label '%1 and %2 must both be specified for French electronic invoicing.', Comment = '%1 = Participant Identifier field caption, %2 = French Identifier Scheme field caption'; } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/ElectronicAddressScheme.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/ElectronicAddressScheme.Enum.al index 2b58475e75b..b4478366bd1 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/ElectronicAddressScheme.Enum.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/ElectronicAddressScheme.Enum.al @@ -20,4 +20,16 @@ enum 10976 "Electronic Address Scheme" { Caption = 'SIREN (0002)'; } + value(3; " ") + { + Caption = ' '; + } + value(4; "0225") + { + Caption = 'FRCTC electronic address (0225)'; + } + value(5; "9957") + { + Caption = 'French VAT number (9957)'; + } } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycle.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycle.Table.al new file mode 100644 index 00000000000..f0fb8ac7347 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycle.Table.al @@ -0,0 +1,238 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +using Microsoft.eServices.EDocument; +using Microsoft.Finance.Currency; +using Microsoft.Sales.Receivables; + +table 10970 "FR E-Invoice Lifecycle" +{ + Caption = 'FR E-Invoice Lifecycle'; + DataClassification = CustomerContent; + DrillDownPageId = "FR E-Invoice Lifecycles"; + InherentPermissions = X; + LookupPageId = "FR E-Invoice Lifecycles"; + ReplicateData = false; + + fields + { + field(1; "Entry No."; Integer) + { + AutoIncrement = true; + Caption = 'Entry No.'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies the entry number of the lifecycle occurrence.'; + } + field(2; "E-Document Entry No."; Integer) + { + Caption = 'E-Document Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "E-Document"."Entry No"; + ToolTip = 'Specifies the related e-document entry.'; + } + field(3; "Lifecycle Status"; Enum "FR E-Invoice Lifecycle Status") + { + Caption = 'Lifecycle Status'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies the French electronic invoice lifecycle status.'; + } + field(4; "Source Occurrence ID"; Guid) + { + Caption = 'Source Occurrence ID'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies the unique identifier of the source event that triggered this occurrence.'; + } + field(5; "Original Occurrence Entry No."; Integer) + { + Caption = 'Original Occurrence Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "FR E-Invoice Lifecycle"."Entry No."; + ToolTip = 'Specifies the entry number of the original Collected occurrence that this reversal references.'; + } + field(6; "Reported Amount"; Decimal) + { + AutoFormatExpression = Rec."Currency Code"; + AutoFormatType = 1; + Caption = 'Reported Amount'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the amount reported for this lifecycle occurrence.'; + } + field(7; "Currency Code"; Code[10]) + { + Caption = 'Currency Code'; + DataClassification = CustomerContent; + TableRelation = Currency; + ToolTip = 'Specifies the currency of the reported amount. Blank represents the local currency.'; + } + field(8; "Event Date"; Date) + { + Caption = 'Event Date'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the date of the lifecycle event.'; + } + field(9; "Invoice Cust. Ledger Entry No."; Integer) + { + Caption = 'Invoice Customer Ledger Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "Cust. Ledger Entry"."Entry No."; + ToolTip = 'Specifies the customer ledger entry of the invoice related to this lifecycle occurrence.'; + } + field(10; "Payment Cust. Ledger Entry No."; Integer) + { + Caption = 'Payment Customer Ledger Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "Cust. Ledger Entry"."Entry No."; + ToolTip = 'Specifies the customer ledger entry of the payment related to this lifecycle occurrence.'; + } + field(11; "Detailed Ledger Entry No."; Integer) + { + Caption = 'Detailed Customer Ledger Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "Detailed Cust. Ledg. Entry"."Entry No."; + ToolTip = 'Specifies the detailed customer ledger entry that triggered this lifecycle occurrence.'; + } + field(12; "E-Document Message Entry No."; Integer) + { + Caption = 'E-Document Message Entry No.'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies the related E-Document message entry.'; + } + field(13; "Processing Status"; Enum "FR E-Invoice Lifecycle Proc.") + { + Caption = 'Processing Status'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies the processing status of the lifecycle occurrence.'; + } + field(14; "Created At"; DateTime) + { + Caption = 'Created At'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies when the lifecycle occurrence was created.'; + } + field(15; "Last Error"; Text[2048]) + { + Caption = 'Last Error'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the last processing error for the lifecycle occurrence.'; + } + field(16; "Invoice Issue Date"; Date) + { + Caption = 'Invoice Issue Date'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the issue date of the invoice frozen at the time of capture.'; + } + field(17; "Invoice Receipt At"; DateTime) + { + Caption = 'Invoice Receipt At'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the date and time the invoice was received by the platform, frozen at capture.'; + } + field(18; "Sender Platform ID"; Text[50]) + { + Caption = 'Sender Platform ID'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the identifier of the approved platform that sent the invoice.'; + } + field(19; "Sender Platform Scheme"; Code[4]) + { + Caption = 'Sender Platform Scheme'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies the identifier scheme of the sender platform.'; + } + field(20; "Sender Platform Name"; Text[100]) + { + Caption = 'Sender Platform Name'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the name of the approved platform that sent the invoice.'; + } + field(21; "Invoice Issuer ID"; Text[50]) + { + Caption = 'Invoice Issuer ID'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the identifier (SIREN) of the invoice issuer.'; + } + field(22; "Invoice Issuer Scheme"; Code[4]) + { + Caption = 'Invoice Issuer Scheme'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies the identifier scheme of the invoice issuer.'; + } + field(23; "Invoice Issuer Name"; Text[100]) + { + Caption = 'Invoice Issuer Name'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the name of the invoice issuer.'; + } + } + + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + key(Occurrence; "E-Document Entry No.", "Source Occurrence ID", "Lifecycle Status") + { + Unique = true; + } + key(EDocument; "E-Document Entry No.", "Created At") + { + } + key(QueuedMessage; "E-Document Entry No.", "E-Document Message Entry No.", "Processing Status", "Created At") + { + } + key(DetailedLedgerEntry; "Lifecycle Status", "Detailed Ledger Entry No.") + { + } + } + + trigger OnModify() + begin + TestImmutableFields(); + end; + + trigger OnDelete() + begin + Error(DeleteOccurrenceErr); + end; + + trigger OnRename() + begin + Error(ImmutableOccurrenceErr); + end; + + local procedure TestImmutableFields() + var + PersistedFREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + begin + PersistedFREInvoiceLifecycle.Get(Rec."Entry No."); + if (Rec."E-Document Entry No." <> PersistedFREInvoiceLifecycle."E-Document Entry No.") or + (Rec."Lifecycle Status" <> PersistedFREInvoiceLifecycle."Lifecycle Status") or + (Rec."Source Occurrence ID" <> PersistedFREInvoiceLifecycle."Source Occurrence ID") or + (Rec."Original Occurrence Entry No." <> PersistedFREInvoiceLifecycle."Original Occurrence Entry No.") or + (Rec."Reported Amount" <> PersistedFREInvoiceLifecycle."Reported Amount") or + (Rec."Currency Code" <> PersistedFREInvoiceLifecycle."Currency Code") or + (Rec."Event Date" <> PersistedFREInvoiceLifecycle."Event Date") or + (Rec."Invoice Cust. Ledger Entry No." <> PersistedFREInvoiceLifecycle."Invoice Cust. Ledger Entry No.") or + (Rec."Payment Cust. Ledger Entry No." <> PersistedFREInvoiceLifecycle."Payment Cust. Ledger Entry No.") or + (Rec."Detailed Ledger Entry No." <> PersistedFREInvoiceLifecycle."Detailed Ledger Entry No.") or + (Rec."Created At" <> PersistedFREInvoiceLifecycle."Created At") or + (Rec."Invoice Issue Date" <> PersistedFREInvoiceLifecycle."Invoice Issue Date") or + (Rec."Invoice Receipt At" <> PersistedFREInvoiceLifecycle."Invoice Receipt At") or + (Rec."Sender Platform ID" <> PersistedFREInvoiceLifecycle."Sender Platform ID") or + (Rec."Sender Platform Scheme" <> PersistedFREInvoiceLifecycle."Sender Platform Scheme") or + (Rec."Sender Platform Name" <> PersistedFREInvoiceLifecycle."Sender Platform Name") or + (Rec."Invoice Issuer ID" <> PersistedFREInvoiceLifecycle."Invoice Issuer ID") or + (Rec."Invoice Issuer Scheme" <> PersistedFREInvoiceLifecycle."Invoice Issuer Scheme") or + (Rec."Invoice Issuer Name" <> PersistedFREInvoiceLifecycle."Invoice Issuer Name") + then + Error(ImmutableOccurrenceErr); + end; + + var + DeleteOccurrenceErr: Label 'A French electronic invoice lifecycle occurrence cannot be deleted.'; + ImmutableOccurrenceErr: Label 'The regulatory identity and values of a French electronic invoice lifecycle occurrence cannot be changed.'; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleError.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleError.Codeunit.al new file mode 100644 index 00000000000..3282c8508c4 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleError.Codeunit.al @@ -0,0 +1,35 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +codeunit 10985 "FR E-Invoice Lifecycle Error" +{ + Access = Internal; + InherentEntitlements = X; + Permissions = tabledata "FR E-Invoice Lifecycle" = m; + TableNo = "FR E-Invoice Lifecycle"; + + trigger OnRun() + begin + Rec.TestField("Processing Status", Rec."Processing Status"::Queued); + Rec.TestField("E-Document Message Entry No.", 0); + Session.LogMessage( + '0000TDQ', LifecycleWorkerFailedTelemetryMsg, Verbosity::Error, + DataClassification::SystemMetadata, TelemetryScope::All, + 'Category', LifecycleTelemetryCategoryTok); + Session.LogMessage( + '0000TDT', LifecycleWorkerDiagnosticTelemetryMsg, Verbosity::Error, + DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, + 'Category', LifecycleTelemetryCategoryTok, 'ErrorCallStack', GetLastErrorCallStack()); + Rec."Processing Status" := Rec."Processing Status"::Failed; + Rec."Last Error" := CopyStr(GetLastErrorText(true), 1, MaxStrLen(Rec."Last Error")); + Rec.Modify(); + end; + + var + LifecycleWorkerFailedTelemetryMsg: Label 'French e-invoice lifecycle message creation failed.', Locked = true; + LifecycleWorkerDiagnosticTelemetryMsg: Label 'French e-invoice lifecycle message creation failed with an internal error.', Locked = true; + LifecycleTelemetryCategoryTok: Label 'French E-Invoice Lifecycle', Locked = true; +} diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleMgt.Codeunit.al new file mode 100644 index 00000000000..8fe51a6bf57 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleMgt.Codeunit.al @@ -0,0 +1,482 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Message; +using Microsoft.Finance.Currency; +using Microsoft.Finance.GeneralLedger.Journal; +using Microsoft.Finance.GeneralLedger.Posting; +using Microsoft.Finance.GeneralLedger.Setup; +using Microsoft.Finance.ReceivablesPayables; +using Microsoft.Finance.VAT.Ledger; +using Microsoft.Finance.VAT.Setup; +using Microsoft.Foundation.Company; +using Microsoft.Sales.Customer; +using Microsoft.Sales.History; +using Microsoft.Sales.Receivables; +using System.Utilities; + +codeunit 10971 "FR E-Invoice Lifecycle Mgt." +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata "FR E-Invoice Lifecycle" = rim, + tabledata "FR E-Invoice Lifecycle VAT" = ri; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntry', '', false, false)] + local procedure OnAfterInsertDtldCustLedgEntry(var DtldCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; GenJournalLine: Record "Gen. Journal Line"; DtldCVLedgEntryBuffer: Record "Detailed CV Ledg. Entry Buffer"; Offset: Integer) + begin + ProcessDetailedLedgerApplication(DtldCustLedgEntry); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntryUnapply', '', false, false)] + local procedure OnAfterInsertDtldCustLedgEntryUnapply(var CustomerPostingGroup: Record "Customer Posting Group"; var OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; var GenJnlLine: Record "Gen. Journal Line"; var NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + begin + ProcessDetailedLedgerUnapplication(OldDetailedCustLedgEntry, NewDetailedCustLedgEntry); + end; + + internal procedure ProcessDetailedLedgerApplication(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; + PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; + begin + if not IsInvoiceApplication(DetailedCustLedgEntry) then + exit; + if not InvoiceCustLedgerEntry.Get(DetailedCustLedgEntry."Cust. Ledger Entry No.") then + exit; + if not PaymentCustLedgerEntry.Get(DetailedCustLedgEntry."Applied Cust. Ledger Entry No.") then + exit; + if PaymentCustLedgerEntry."Document Type" <> PaymentCustLedgerEntry."Document Type"::Payment then + exit; + if not FindInvoiceEDocuments(EDocument, InvoiceCustLedgerEntry) then + exit; + + repeat + if IsFREInvoiceEDocument(EDocument) then begin + FREInvoiceLifecycle := CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, DetailedCustLedgEntry.SystemId, + -DetailedCustLedgEntry.Amount, DetailedCustLedgEntry."Currency Code", DetailedCustLedgEntry."Posting Date", + InvoiceCustLedgerEntry."Entry No.", PaymentCustLedgerEntry."Entry No.", DetailedCustLedgEntry."Entry No.", 0); + ScheduleMessageCreation(FREInvoiceLifecycle); + end; + until EDocument.Next() = 0; + end; + + internal procedure ProcessDetailedLedgerUnapplication(OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + CollectedLifecycle: Record "FR E-Invoice Lifecycle"; + NegativeCollectedLifecycle: Record "FR E-Invoice Lifecycle"; + begin + if not IsInvoiceApplication(OldDetailedCustLedgEntry) then + exit; + if not FindCollectedOccurrences(CollectedLifecycle, OldDetailedCustLedgEntry."Entry No.") then + exit; + + repeat + NegativeCollectedLifecycle := CapturePaymentOccurrence( + CollectedLifecycle."E-Document Entry No.", "FR E-Invoice Lifecycle Status"::"Negative Collected", NewDetailedCustLedgEntry.SystemId, + -CollectedLifecycle."Reported Amount", CollectedLifecycle."Currency Code", NewDetailedCustLedgEntry."Posting Date", + CollectedLifecycle."Invoice Cust. Ledger Entry No.", CollectedLifecycle."Payment Cust. Ledger Entry No.", NewDetailedCustLedgEntry."Entry No.", CollectedLifecycle."Entry No."); + ScheduleMessageCreation(NegativeCollectedLifecycle); + until CollectedLifecycle.Next() = 0; + end; + + internal procedure CapturePaymentOccurrence(EDocumentEntryNo: Integer; LifecycleStatus: Enum "FR E-Invoice Lifecycle Status"; SourceOccurrenceID: Guid; ReportedAmount: Decimal; CurrencyCode: Code[10]; EventDate: Date; InvoiceCustLedgerEntryNo: Integer; PaymentCustLedgerEntryNo: Integer; DetailedLedgerEntryNo: Integer; OriginalOccurrenceEntryNo: Integer) FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle" + var + ResolvedCurrencyCode: Code[10]; + begin + ResolvedCurrencyCode := ResolveCurrencyCode(CurrencyCode); + ValidatePaymentOccurrence(EDocumentEntryNo, LifecycleStatus, SourceOccurrenceID, ReportedAmount, EventDate, OriginalOccurrenceEntryNo); + + if FindOccurrence(FREInvoiceLifecycle, EDocumentEntryNo, LifecycleStatus, SourceOccurrenceID) then begin + VerifyReplay(FREInvoiceLifecycle, ReportedAmount, CurrencyCode, EventDate, InvoiceCustLedgerEntryNo, PaymentCustLedgerEntryNo, DetailedLedgerEntryNo, OriginalOccurrenceEntryNo); + exit(FREInvoiceLifecycle); + end; + + FREInvoiceLifecycle.Init(); + FREInvoiceLifecycle."E-Document Entry No." := EDocumentEntryNo; + FREInvoiceLifecycle."Lifecycle Status" := LifecycleStatus; + FREInvoiceLifecycle."Source Occurrence ID" := SourceOccurrenceID; + FREInvoiceLifecycle."Original Occurrence Entry No." := OriginalOccurrenceEntryNo; + FREInvoiceLifecycle."Reported Amount" := ReportedAmount; + FREInvoiceLifecycle."Currency Code" := CurrencyCode; + FREInvoiceLifecycle."Event Date" := EventDate; + FREInvoiceLifecycle."Invoice Cust. Ledger Entry No." := InvoiceCustLedgerEntryNo; + FREInvoiceLifecycle."Payment Cust. Ledger Entry No." := PaymentCustLedgerEntryNo; + FREInvoiceLifecycle."Detailed Ledger Entry No." := DetailedLedgerEntryNo; + FREInvoiceLifecycle."Processing Status" := FREInvoiceLifecycle."Processing Status"::Captured; + FREInvoiceLifecycle."Created At" := CurrentDateTime(); + if InvoiceCustLedgerEntryNo <> 0 then + PopulatePPFContext(FREInvoiceLifecycle); + FREInvoiceLifecycle.Insert(); + + if InvoiceCustLedgerEntryNo <> 0 then + if LifecycleStatus = LifecycleStatus::Collected then + CreateVATBreakdown(FREInvoiceLifecycle, ResolvedCurrencyCode) + else + CreateReversalVATBreakdown(FREInvoiceLifecycle, OriginalOccurrenceEntryNo); + end; + + local procedure PopulatePPFContext(var FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + var + CompanyInformation: Record "Company Information"; + EDocument: Record "E-Document"; + EDocumentService: Record "E-Document Service"; + EDocumentServiceStatus: Record "E-Document Service Status"; + EDocHelpers: Codeunit "EDoc. Helpers"; + begin + EDocument.SetLoadFields(Service, "Document Date", "Clearance Date"); + EDocument.Get(FREInvoiceLifecycle."E-Document Entry No."); + if not EDocHelpers.GetFrenchEDocumentService(EDocument, EDocumentService, EDocumentServiceStatus) then + exit; + if EDocumentService."FR Sender Platform ID" = '' then + exit; + + EDocument.TestField("Document Date"); + EDocument.TestField("Clearance Date"); + EDocumentService.TestField("FR Sender Platform Scheme"); + EDocumentService.TestField("FR Sender Platform Name"); + CompanyInformation.Get(); + CompanyInformation.TestField("Registration No."); + CompanyInformation.TestField(Name); + + FREInvoiceLifecycle."Invoice Issue Date" := EDocument."Document Date"; + FREInvoiceLifecycle."Invoice Receipt At" := EDocument."Clearance Date"; + FREInvoiceLifecycle."Sender Platform ID" := EDocumentService."FR Sender Platform ID"; + FREInvoiceLifecycle."Sender Platform Scheme" := EDocumentService."FR Sender Platform Scheme"; + FREInvoiceLifecycle."Sender Platform Name" := EDocumentService."FR Sender Platform Name"; + FREInvoiceLifecycle."Invoice Issuer ID" := CopyStr(CompanyInformation."Registration No.", 1, 9); + FREInvoiceLifecycle."Invoice Issuer Scheme" := SIRENSchemeTok; + FREInvoiceLifecycle."Invoice Issuer Name" := CompanyInformation.Name; + end; + + local procedure CreateVATBreakdown(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; CurrencyCode: Code[10]) + var + InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; + VATEntry: Record "VAT Entry"; + VATPostingSetup: Record "VAT Posting Setup"; + AmountByVATRate: Dictionary of [Decimal, Decimal]; + VATRates: List of [Decimal]; + VATRate: Decimal; + GrossAmount: Decimal; + TotalGrossAmount: Decimal; + begin + InvoiceCustLedgerEntry.Get(FREInvoiceLifecycle."Invoice Cust. Ledger Entry No."); + FindInvoiceVATEntries(VATEntry, InvoiceCustLedgerEntry); + VATEntry.SetLoadFields("VAT Bus. Posting Group", "VAT Prod. Posting Group", "Source Currency Code", "Source Currency VAT Base", "Source Currency VAT Amount", Base, Amount); + if VATEntry.FindSet() then + repeat + VATPostingSetup.Get(VATEntry."VAT Bus. Posting Group", VATEntry."VAT Prod. Posting Group"); + VATRate := VATPostingSetup."VAT %"; + GrossAmount := GetVATEntryGrossAmount(VATEntry, CurrencyCode); + AddVATRateAmount(AmountByVATRate, VATRates, VATRate, GrossAmount); + TotalGrossAmount += GrossAmount; + until VATEntry.Next() = 0; + + if (VATRates.Count() = 0) or (TotalGrossAmount = 0) then + Error(VATBreakdownErr, InvoiceCustLedgerEntry."Document No."); + + InsertAllocatedVATAmounts(FREInvoiceLifecycle, AmountByVATRate, VATRates, TotalGrossAmount, CurrencyCode); + end; + + local procedure FindInvoiceVATEntries(var VATEntry: Record "VAT Entry"; InvoiceCustLedgerEntry: Record "Cust. Ledger Entry") + begin + VATEntry.SetRange(Type, VATEntry.Type::Sale); + VATEntry.SetRange("Document Type", VATEntry."Document Type"::Invoice); + VATEntry.SetRange("Document No.", InvoiceCustLedgerEntry."Document No."); + VATEntry.SetRange("Posting Date", InvoiceCustLedgerEntry."Posting Date"); + VATEntry.SetRange("Transaction No.", InvoiceCustLedgerEntry."Transaction No."); + end; + + local procedure GetVATEntryGrossAmount(VATEntry: Record "VAT Entry"; CurrencyCode: Code[10]): Decimal + var + VATEntryCurrencyErrorInfo: ErrorInfo; + begin + if VATEntry."Source Currency Code" = CurrencyCode then + exit(-(VATEntry."Source Currency VAT Base" + VATEntry."Source Currency VAT Amount")); + + if VATEntry."Source Currency Code" = '' then + exit(-(VATEntry.Base + VATEntry.Amount)); + + VATEntryCurrencyErrorInfo.ErrorType(ErrorType::Internal); + VATEntryCurrencyErrorInfo.Message(StrSubstNo(VATEntryCurrencyErr, VATEntry."Entry No.", CurrencyCode)); + Error(VATEntryCurrencyErrorInfo); + end; + + local procedure AddVATRateAmount(var AmountByVATRate: Dictionary of [Decimal, Decimal]; var VATRates: List of [Decimal]; VATRate: Decimal; GrossAmount: Decimal) + begin + if AmountByVATRate.ContainsKey(VATRate) then begin + AmountByVATRate.Set(VATRate, AmountByVATRate.Get(VATRate) + GrossAmount); + exit; + end; + + AmountByVATRate.Add(VATRate, GrossAmount); + VATRates.Add(VATRate); + end; + + local procedure InsertAllocatedVATAmounts(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; AmountByVATRate: Dictionary of [Decimal, Decimal]; VATRates: List of [Decimal]; TotalGrossAmount: Decimal; CurrencyCode: Code[10]) + var + Currency: Record Currency; + FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; + AllocatedAmount: Decimal; + RemainingAmount: Decimal; + RoundingPrecision: Decimal; + VATRate: Decimal; + LineNo: Integer; + begin + RoundingPrecision := GetAmountRoundingPrecision(Currency, CurrencyCode); + RemainingAmount := FREInvoiceLifecycle."Reported Amount"; + LineNo := 0; + foreach VATRate in VATRates do begin + LineNo += 10000; + if LineNo div 10000 = VATRates.Count() then + AllocatedAmount := RemainingAmount + else begin + AllocatedAmount := Round(FREInvoiceLifecycle."Reported Amount" * AmountByVATRate.Get(VATRate) / TotalGrossAmount, RoundingPrecision); + RemainingAmount -= AllocatedAmount; + end; + + InsertVATBreakdown(FREInvoiceLifecycleVAT, FREInvoiceLifecycle."Entry No.", LineNo, VATRate, AllocatedAmount, CurrencyCode); + end; + end; + + local procedure GetAmountRoundingPrecision(var Currency: Record Currency; CurrencyCode: Code[10]): Decimal + var + GeneralLedgerSetup: Record "General Ledger Setup"; + begin + GeneralLedgerSetup.Get(); + if CurrencyCode = GeneralLedgerSetup."LCY Code" then + exit(GeneralLedgerSetup."Amount Rounding Precision"); + + Currency.Get(CurrencyCode); + Currency.TestField("Amount Rounding Precision"); + exit(Currency."Amount Rounding Precision"); + end; + + local procedure CreateReversalVATBreakdown(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; OriginalOccurrenceEntryNo: Integer) + var + OriginalLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; + ReversalLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; + begin + OriginalLifecycleVAT.SetRange("Lifecycle Entry No.", OriginalOccurrenceEntryNo); + if not OriginalLifecycleVAT.FindSet() then + Error(OriginalVATBreakdownErr, OriginalOccurrenceEntryNo); + + repeat + InsertVATBreakdown( + ReversalLifecycleVAT, FREInvoiceLifecycle."Entry No.", OriginalLifecycleVAT."Line No.", OriginalLifecycleVAT."VAT %", + -OriginalLifecycleVAT."Reported Amount", FREInvoiceLifecycle."Currency Code"); + until OriginalLifecycleVAT.Next() = 0; + end; + + local procedure InsertVATBreakdown(var FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; LifecycleEntryNo: Integer; LineNo: Integer; VATRate: Decimal; ReportedAmount: Decimal; CurrencyCode: Code[10]) + begin + FREInvoiceLifecycleVAT.Init(); + FREInvoiceLifecycleVAT."Lifecycle Entry No." := LifecycleEntryNo; + FREInvoiceLifecycleVAT."Line No." := LineNo; + FREInvoiceLifecycleVAT."VAT %" := VATRate; + FREInvoiceLifecycleVAT."Reported Amount" := ReportedAmount; + FREInvoiceLifecycleVAT."Currency Code" := CurrencyCode; + FREInvoiceLifecycleVAT.Insert(); + end; + + internal procedure CreateLifecycleMessage(var FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + var + EDocument: Record "E-Document"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceLifecycleMsg: Codeunit "FR E-Invoice Lifecycle Msg."; + TempBlob: Codeunit "Temp Blob"; + begin + if FREInvoiceLifecycle."E-Document Message Entry No." <> 0 then + exit; + + EDocument.Get(FREInvoiceLifecycle."E-Document Entry No."); + FREInvoiceLifecycleMsg.BuildLifecycleMessage(EDocument, FREInvoiceLifecycle, TempBlob); + FREInvoiceLifecycle."E-Document Message Entry No." := EDocumentMessageAPI.CreateMessage( + EDocument, "E-Document Message Type"::"FR Invoice Lifecycle", EDocument.Direction::Outgoing, TempBlob); + FREInvoiceLifecycle."Processing Status" := FREInvoiceLifecycle."Processing Status"::"Message Created"; + Clear(FREInvoiceLifecycle."Last Error"); + FREInvoiceLifecycle.Modify(); + Session.LogMessage( + '0000TDS', LifecycleMessageCreatedTelemetryMsg, Verbosity::Normal, + DataClassification::SystemMetadata, TelemetryScope::All, 'Category', LifecycleTelemetryCategoryTok); + end; + + internal procedure RetryLifecycleMessage(var FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + begin + FREInvoiceLifecycle.TestField("Processing Status", FREInvoiceLifecycle."Processing Status"::Failed); + FREInvoiceLifecycle.TestField("E-Document Message Entry No.", 0); + ScheduleMessageCreation(FREInvoiceLifecycle); + end; + + local procedure ValidatePaymentOccurrence(EDocumentEntryNo: Integer; LifecycleStatus: Enum "FR E-Invoice Lifecycle Status"; SourceOccurrenceID: Guid; ReportedAmount: Decimal; EventDate: Date; OriginalOccurrenceEntryNo: Integer) + var + EDocument: Record "E-Document"; + OriginalOccurrence: Record "FR E-Invoice Lifecycle"; + UnsupportedStatusErrorInfo: ErrorInfo; + begin + EDocument.Get(EDocumentEntryNo); + if IsNullGuid(SourceOccurrenceID) then + RaiseClientError(SourceOccurrenceIDErr); + if EventDate = 0D then + RaiseClientError(EventDateErr); + + case LifecycleStatus of + LifecycleStatus::Collected: + begin + if ReportedAmount <= 0 then + RaiseClientError(CollectedAmountErr); + if OriginalOccurrenceEntryNo <> 0 then + RaiseClientError(CollectedOriginalOccurrenceErr); + end; + LifecycleStatus::"Negative Collected": + begin + if ReportedAmount >= 0 then + RaiseClientError(NegativeCollectedAmountErr); + if not OriginalOccurrence.Get(OriginalOccurrenceEntryNo) then + RaiseClientError(OriginalOccurrenceErr); + OriginalOccurrence.TestField("E-Document Entry No.", EDocumentEntryNo); + OriginalOccurrence.TestField("Lifecycle Status", OriginalOccurrence."Lifecycle Status"::Collected); + if ReportedAmount <> -OriginalOccurrence."Reported Amount" then + RaiseClientError(ReversalAmountErr); + end; + else begin + UnsupportedStatusErrorInfo.ErrorType(ErrorType::Internal); + UnsupportedStatusErrorInfo.Message(StrSubstNo(PaymentStatusErr, LifecycleStatus)); + UnsupportedStatusErrorInfo.DetailedMessage(InternalLifecycleStatusErr); + Error(UnsupportedStatusErrorInfo); + end; + end; + end; + + local procedure FindOccurrence(var FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; EDocumentEntryNo: Integer; LifecycleStatus: Enum "FR E-Invoice Lifecycle Status"; SourceOccurrenceID: Guid): Boolean + begin + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocumentEntryNo); + FREInvoiceLifecycle.SetRange("Source Occurrence ID", SourceOccurrenceID); + FREInvoiceLifecycle.SetRange("Lifecycle Status", LifecycleStatus); + exit(FREInvoiceLifecycle.FindFirst()); + end; + + local procedure FindCollectedOccurrences(var FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; DetailedLedgerEntryNo: Integer): Boolean + begin + FREInvoiceLifecycle.SetRange("Lifecycle Status", FREInvoiceLifecycle."Lifecycle Status"::Collected); + FREInvoiceLifecycle.SetRange("Detailed Ledger Entry No.", DetailedLedgerEntryNo); + exit(FREInvoiceLifecycle.FindSet()); + end; + + local procedure ScheduleMessageCreation(var FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + begin + if FREInvoiceLifecycle."E-Document Message Entry No." <> 0 then + exit; + if FREInvoiceLifecycle."Processing Status" = FREInvoiceLifecycle."Processing Status"::Queued then + exit; + + FREInvoiceLifecycle."Processing Status" := FREInvoiceLifecycle."Processing Status"::Queued; + Clear(FREInvoiceLifecycle."Last Error"); + FREInvoiceLifecycle.Modify(); + TaskScheduler.CreateTask( + Codeunit::"FR E-Invoice Lifecycle Worker", Codeunit::"FR E-Invoice Lifecycle Error", true, + CompanyName(), CurrentDateTime(), FREInvoiceLifecycle.RecordId); + Session.LogMessage( + '0000TDR', LifecycleMessageQueuedTelemetryMsg, Verbosity::Normal, + DataClassification::SystemMetadata, TelemetryScope::All, 'Category', LifecycleTelemetryCategoryTok); + end; + + local procedure RaiseClientError(ErrorMessage: Text) + var + ClientErrorInfo: ErrorInfo; + begin + ClientErrorInfo.ErrorType(ErrorType::Client); + ClientErrorInfo.Message(ErrorMessage); + Error(ClientErrorInfo); + end; + + local procedure ResolveCurrencyCode(CurrencyCode: Code[10]): Code[10] + var + GeneralLedgerSetup: Record "General Ledger Setup"; + begin + if CurrencyCode <> '' then + exit(CurrencyCode); + + GeneralLedgerSetup.Get(); + GeneralLedgerSetup.TestField("LCY Code"); + exit(GeneralLedgerSetup."LCY Code"); + end; + + local procedure FindInvoiceEDocuments(var EDocument: Record "E-Document"; InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"): Boolean + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + begin + if InvoiceCustLedgerEntry."Document Type" <> InvoiceCustLedgerEntry."Document Type"::Invoice then + exit(false); + if not SalesInvoiceHeader.Get(InvoiceCustLedgerEntry."Document No.") then + exit(false); + + EDocument.SetRange("Document Record ID", SalesInvoiceHeader.RecordId); + EDocument.SetRange(Direction, EDocument.Direction::Outgoing); + EDocument.SetRange("Document Type", EDocument."Document Type"::"Sales Invoice"); + exit(EDocument.FindSet()); + end; + + local procedure IsFREInvoiceEDocument(EDocument: Record "E-Document"): Boolean + var + EDocumentService: Record "E-Document Service"; + EDocumentServiceStatus: Record "E-Document Service Status"; + EDocHelpers: Codeunit "EDoc. Helpers"; + begin + exit( + EDocHelpers.GetFrenchEDocumentService(EDocument, EDocumentService, EDocumentServiceStatus) and + IsFREInvoiceFormat(EDocumentService."Document Format")); + end; + + local procedure IsFREInvoiceFormat(EDocumentFormat: Enum "E-Document Format"): Boolean + begin + exit(EDocumentFormat in [EDocumentFormat::"Peppol BIS 3.0 FR", EDocumentFormat::"Factur-X FR"]); + end; + + local procedure IsInvoiceApplication(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"): Boolean + begin + exit( + (DetailedCustLedgEntry."Entry Type" = DetailedCustLedgEntry."Entry Type"::Application) and + (DetailedCustLedgEntry."Initial Document Type" = DetailedCustLedgEntry."Initial Document Type"::Invoice) and + (DetailedCustLedgEntry.Amount < 0)); + end; + + local procedure VerifyReplay(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; ReportedAmount: Decimal; CurrencyCode: Code[10]; EventDate: Date; InvoiceCustLedgerEntryNo: Integer; PaymentCustLedgerEntryNo: Integer; DetailedLedgerEntryNo: Integer; OriginalOccurrenceEntryNo: Integer) + begin + if (FREInvoiceLifecycle."Reported Amount" <> ReportedAmount) or + (FREInvoiceLifecycle."Currency Code" <> CurrencyCode) or + (FREInvoiceLifecycle."Event Date" <> EventDate) or + (FREInvoiceLifecycle."Invoice Cust. Ledger Entry No." <> InvoiceCustLedgerEntryNo) or + (FREInvoiceLifecycle."Payment Cust. Ledger Entry No." <> PaymentCustLedgerEntryNo) or + (FREInvoiceLifecycle."Detailed Ledger Entry No." <> DetailedLedgerEntryNo) or + (FREInvoiceLifecycle."Original Occurrence Entry No." <> OriginalOccurrenceEntryNo) + then + Error(ConflictingReplayErr); + end; + + var + SourceOccurrenceIDErr: Label 'A source occurrence ID is required for a French payment lifecycle occurrence.'; + EventDateErr: Label 'An event date is required for a French payment lifecycle occurrence.'; + CollectedAmountErr: Label 'The reported amount for a Collected occurrence must be positive.'; + NegativeCollectedAmountErr: Label 'The reported amount for a Negative Collected occurrence must be negative.'; + CollectedOriginalOccurrenceErr: Label 'A Collected occurrence cannot reference an original occurrence.'; + OriginalOccurrenceErr: Label 'The original Collected occurrence does not exist.'; + ReversalAmountErr: Label 'A Negative Collected occurrence must exactly reverse the reported amount of the original Collected occurrence.'; + PaymentStatusErr: Label 'Lifecycle status %1 is not a payment lifecycle status.', Comment = '%1 = lifecycle status'; + InternalLifecycleStatusErr: Label 'An internal lifecycle status error occurred.'; + ConflictingReplayErr: Label 'The payment lifecycle occurrence was already captured with different values.'; + VATBreakdownErr: Label 'A VAT breakdown could not be determined for posted sales invoice %1.', Comment = '%1 = posted sales invoice number'; + VATEntryCurrencyErr: Label 'VAT entry %1 does not contain amounts in lifecycle currency %2.', Comment = '%1 = VAT entry number, %2 = currency code'; + OriginalVATBreakdownErr: Label 'The VAT breakdown for original lifecycle occurrence %1 does not exist.', Comment = '%1 = lifecycle occurrence entry number'; + LifecycleMessageCreatedTelemetryMsg: Label 'French e-invoice lifecycle message created.', Locked = true; + LifecycleMessageQueuedTelemetryMsg: Label 'French e-invoice lifecycle message creation queued.', Locked = true; + LifecycleTelemetryCategoryTok: Label 'French E-Invoice Lifecycle', Locked = true; + SIRENSchemeTok: Label '0002', Locked = true; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleMsg.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleMsg.Codeunit.al new file mode 100644 index 00000000000..01cbdb2e16b --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleMsg.Codeunit.al @@ -0,0 +1,308 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.eServices.EDocument.Processing.Message; +using Microsoft.Finance.GeneralLedger.Setup; +using System.Utilities; + +codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + procedure BuildMessage(EDocument: Record "E-Document"; ResponseType: Enum "E-Doc. Response Type"; var TempBlob: Codeunit "Temp Blob") + var + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + NoCapturedOccurrenceErrorInfo: ErrorInfo; + begin + FREInvoiceLifecycle.SetCurrentKey("E-Document Entry No.", "E-Document Message Entry No.", "Processing Status", "Created At"); + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceLifecycle.SetRange("E-Document Message Entry No.", 0); + FREInvoiceLifecycle.SetRange("Processing Status", FREInvoiceLifecycle."Processing Status"::Queued); + if not FREInvoiceLifecycle.FindFirst() then begin + NoCapturedOccurrenceErrorInfo.ErrorType(ErrorType::Internal); + NoCapturedOccurrenceErrorInfo.Message(InternalLifecycleStateErr); + NoCapturedOccurrenceErrorInfo.DetailedMessage(StrSubstNo(NoCapturedOccurrenceErr, EDocument."Entry No")); + Error(NoCapturedOccurrenceErrorInfo); + end; + + BuildLifecycleMessage(EDocument, FREInvoiceLifecycle, TempBlob); + end; + + internal procedure BuildLifecycleMessage(EDocument: Record "E-Document"; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; var TempBlob: Codeunit "Temp Blob") + var + XmlDoc: XmlDocument; + RootElement: XmlElement; + OutStream: OutStream; + begin + FREInvoiceLifecycle.TestField("E-Document Entry No.", EDocument."Entry No"); + + XmlDoc := XmlDocument.Create(); + XmlDoc.SetDeclaration(XmlDeclaration.Create('1.0', 'UTF-8', 'no')); + RootElement := BuildLifecycleElement(EDocument, FREInvoiceLifecycle); + XmlDoc.Add(RootElement); + + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + XmlDoc.WriteTo(OutStream); + end; + + local procedure BuildLifecycleElement(EDocument: Record "E-Document"; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") RootElement: XmlElement + begin + RootElement := XmlElement.Create('CrossDomainAcknowledgementAndResponse', RsmNamespaceTok); + RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('qdt', QdtNamespaceTok)); + RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('udt', UdtNamespaceTok)); + RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('ram', RamNamespaceTok)); + RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('rsm', RsmNamespaceTok)); + + if IsPPFMessage(FREInvoiceLifecycle) then + ValidatePPFContext(FREInvoiceLifecycle); + + AddExchangedDocumentContext(RootElement, FREInvoiceLifecycle); + AddExchangedDocument(RootElement, FREInvoiceLifecycle); + AddAcknowledgementDocument(RootElement, EDocument, FREInvoiceLifecycle); + end; + + local procedure AddExchangedDocumentContext(var RootElement: XmlElement; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + var + BusinessProcessElement: XmlElement; + ContextElement: XmlElement; + GuidelineElement: XmlElement; + begin + ContextElement := XmlElement.Create('ExchangedDocumentContext', RsmNamespaceTok); + if not IsPPFMessage(FREInvoiceLifecycle) then begin + BusinessProcessElement := XmlElement.Create('BusinessProcessSpecifiedDocumentContextParameter', RamNamespaceTok); + BusinessProcessElement.Add(XmlElement.Create('ID', RamNamespaceTok, RegulatedBusinessProcessTok)); + ContextElement.Add(BusinessProcessElement); + end; + GuidelineElement := XmlElement.Create('GuidelineSpecifiedDocumentContextParameter', RamNamespaceTok); + GuidelineElement.Add(XmlElement.Create('ID', RamNamespaceTok, GetProfileID(FREInvoiceLifecycle))); + ContextElement.Add(GuidelineElement); + RootElement.Add(ContextElement); + end; + + local procedure AddExchangedDocument(var RootElement: XmlElement; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + var + ExchangedDocumentElement: XmlElement; + IssueDateTimeElement: XmlElement; + begin + ExchangedDocumentElement := XmlElement.Create('ExchangedDocument', RsmNamespaceTok); + ExchangedDocumentElement.Add(XmlElement.Create('ID', RamNamespaceTok, Format(FREInvoiceLifecycle."Source Occurrence ID"))); + ExchangedDocumentElement.Add(XmlElement.Create('Name', RamNamespaceTok, LifecycleMessageNameTok)); + IssueDateTimeElement := XmlElement.Create('IssueDateTime', RamNamespaceTok); + IssueDateTimeElement.Add(CreateDateTimeString(FREInvoiceLifecycle."Created At")); + ExchangedDocumentElement.Add(IssueDateTimeElement); + if IsPPFMessage(FREInvoiceLifecycle) then begin + ExchangedDocumentElement.Add( + CreateTradeParty( + 'SenderTradeParty', FREInvoiceLifecycle."Sender Platform ID", FREInvoiceLifecycle."Sender Platform Scheme", + FREInvoiceLifecycle."Sender Platform Name", SenderRoleCodeTok)); + ExchangedDocumentElement.Add( + CreateTradeParty( + 'IssuerTradeParty', FREInvoiceLifecycle."Invoice Issuer ID", FREInvoiceLifecycle."Invoice Issuer Scheme", + FREInvoiceLifecycle."Invoice Issuer Name", SellerRoleCodeTok)); + ExchangedDocumentElement.Add( + CreateTradeParty('RecipientTradeParty', PPFIdentifierTok, PPFIdentifierSchemeTok, PPFNameTok, PPFRoleCodeTok)); + end; + RootElement.Add(ExchangedDocumentElement); + end; + + local procedure AddAcknowledgementDocument(var RootElement: XmlElement; EDocument: Record "E-Document"; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + var + AcknowledgementDocumentElement: XmlElement; + IssueDateTimeElement: XmlElement; + MultipleReferencesElement: XmlElement; + ReferenceDocumentElement: XmlElement; + begin + ValidatePaymentStatus(FREInvoiceLifecycle."Lifecycle Status"); + + AcknowledgementDocumentElement := XmlElement.Create('AcknowledgementDocument', RsmNamespaceTok); + MultipleReferencesElement := XmlElement.Create('MultipleReferencesIndicator', RamNamespaceTok); + MultipleReferencesElement.Add(XmlElement.Create('Indicator', UdtNamespaceTok, 'false')); + AcknowledgementDocumentElement.Add(MultipleReferencesElement); + AcknowledgementDocumentElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, InformationTypeCodeTok)); + + IssueDateTimeElement := XmlElement.Create('IssueDateTime', RamNamespaceTok); + IssueDateTimeElement.Add(CreateDateTimeString(CreateDateTime(FREInvoiceLifecycle."Event Date", 000000T))); + AcknowledgementDocumentElement.Add(IssueDateTimeElement); + + ReferenceDocumentElement := XmlElement.Create('ReferenceReferencedDocument', RamNamespaceTok); + ReferenceDocumentElement.Add(XmlElement.Create('IssuerAssignedID', RamNamespaceTok, EDocument."Document No.")); + ReferenceDocumentElement.Add(XmlElement.Create('StatusCode', RamNamespaceTok, InvoiceReferenceStatusCodeTok)); + ReferenceDocumentElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, InvoiceTypeCodeTok)); + if IsPPFMessage(FREInvoiceLifecycle) then begin + ReferenceDocumentElement.Add(CreateDateTimeElement('ReceiptDateTime', FREInvoiceLifecycle."Invoice Receipt At")); + ReferenceDocumentElement.Add(XmlElement.Create('ReferenceTypeCode', RamNamespaceTok, PPFInvoiceProfileTok)); + ReferenceDocumentElement.Add(CreateFormattedIssueDateTime(FREInvoiceLifecycle."Invoice Issue Date")); + end; + ReferenceDocumentElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, CollectedStatusCodeTok)); + ReferenceDocumentElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, CollectedStatusNameTok)); + if IsPPFMessage(FREInvoiceLifecycle) then + ReferenceDocumentElement.Add( + CreateTradeParty( + 'IssuerTradeParty', FREInvoiceLifecycle."Invoice Issuer ID", FREInvoiceLifecycle."Invoice Issuer Scheme", + '', '')); + AddVATBreakdown(ReferenceDocumentElement, FREInvoiceLifecycle); + AcknowledgementDocumentElement.Add(ReferenceDocumentElement); + RootElement.Add(AcknowledgementDocumentElement); + end; + + local procedure AddVATBreakdown(var ReferenceDocumentElement: XmlElement; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + var + FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; + MissingVATBreakdownErrorInfo: ErrorInfo; + SpecifiedDocumentStatusElement: XmlElement; + CurrencyCode: Code[10]; + begin + FREInvoiceLifecycleVAT.SetRange("Lifecycle Entry No.", FREInvoiceLifecycle."Entry No."); + if not FREInvoiceLifecycleVAT.FindSet() then begin + MissingVATBreakdownErrorInfo.ErrorType(ErrorType::Internal); + MissingVATBreakdownErrorInfo.Message(StrSubstNo(VATBreakdownErr, FREInvoiceLifecycle."Entry No.")); + Error(MissingVATBreakdownErrorInfo); + end; + + CurrencyCode := ResolveCurrencyCode(FREInvoiceLifecycle."Currency Code"); + SpecifiedDocumentStatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); + repeat + SpecifiedDocumentStatusElement.Add(CreateVATCharacteristic(FREInvoiceLifecycleVAT, CurrencyCode)); + until FREInvoiceLifecycleVAT.Next() = 0; + ReferenceDocumentElement.Add(SpecifiedDocumentStatusElement); + end; + + local procedure CreateVATCharacteristic(FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; CurrencyCode: Code[10]) CharacteristicElement: XmlElement + var + AmountElement: XmlElement; + ValueChangedElement: XmlElement; + begin + CharacteristicElement := XmlElement.Create('SpecifiedDocumentCharacteristic', RamNamespaceTok); + CharacteristicElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, CollectedAmountTypeCodeTok)); + ValueChangedElement := XmlElement.Create('ValueChangedIndicator', RamNamespaceTok); + ValueChangedElement.Add(XmlElement.Create('IndicatorString', UdtNamespaceTok, 'false')); + CharacteristicElement.Add(ValueChangedElement); + AmountElement := XmlElement.Create('ValueAmount', RamNamespaceTok, Format(FREInvoiceLifecycleVAT."Reported Amount", 0, 9)); + AmountElement.Add(XmlAttribute.Create('currencyID', CurrencyCode)); + CharacteristicElement.Add(AmountElement); + CharacteristicElement.Add(XmlElement.Create('ValuePercent', RamNamespaceTok, Format(FREInvoiceLifecycleVAT."VAT %", 0, 9))); + end; + + local procedure ResolveCurrencyCode(CurrencyCode: Code[10]): Code[10] + var + GeneralLedgerSetup: Record "General Ledger Setup"; + begin + if CurrencyCode <> '' then + exit(CurrencyCode); + + GeneralLedgerSetup.Get(); + GeneralLedgerSetup.TestField("LCY Code"); + exit(GeneralLedgerSetup."LCY Code"); + end; + + local procedure CreateDateTimeString(Value: DateTime) DateTimeStringElement: XmlElement + begin + DateTimeStringElement := XmlElement.Create('DateTimeString', UdtNamespaceTok, Format(Value, 0, '')); + DateTimeStringElement.Add(XmlAttribute.Create('format', DateTimeFormatCodeTok)); + end; + + local procedure CreateDateTimeElement(ElementName: Text; Value: DateTime) DateTimeElement: XmlElement + begin + DateTimeElement := XmlElement.Create(ElementName, RamNamespaceTok); + DateTimeElement.Add(CreateDateTimeString(Value)); + end; + + local procedure CreateFormattedIssueDateTime(Value: Date) FormattedIssueDateTimeElement: XmlElement + var + DateTimeStringElement: XmlElement; + begin + FormattedIssueDateTimeElement := XmlElement.Create('FormattedIssueDateTime', RamNamespaceTok); + DateTimeStringElement := XmlElement.Create('DateTimeString', QdtNamespaceTok, Format(Value, 0, '')); + DateTimeStringElement.Add(XmlAttribute.Create('format', DateFormatCodeTok)); + FormattedIssueDateTimeElement.Add(DateTimeStringElement); + end; + + local procedure CreateTradeParty(ElementName: Text; Identifier: Text; IdentifierScheme: Text; PartyName: Text; RoleCode: Text) TradePartyElement: XmlElement + var + GlobalIDElement: XmlElement; + begin + TradePartyElement := XmlElement.Create(ElementName, RamNamespaceTok); + GlobalIDElement := XmlElement.Create('GlobalID', RamNamespaceTok, Identifier); + GlobalIDElement.Add(XmlAttribute.Create('schemeID', IdentifierScheme)); + TradePartyElement.Add(GlobalIDElement); + if PartyName <> '' then + TradePartyElement.Add(XmlElement.Create('Name', RamNamespaceTok, PartyName)); + if RoleCode <> '' then + TradePartyElement.Add(XmlElement.Create('RoleCode', RamNamespaceTok, RoleCode)); + end; + + local procedure IsPPFMessage(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"): Boolean + begin + exit(FREInvoiceLifecycle."Sender Platform ID" <> ''); + end; + + local procedure GetProfileID(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"): Text + begin + if IsPPFMessage(FREInvoiceLifecycle) then + exit(PPFInvoiceProfileTok); + exit(CDVInvoiceProfileTok); + end; + + local procedure ValidatePPFContext(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + begin + FREInvoiceLifecycle.TestField("Invoice Issue Date"); + FREInvoiceLifecycle.TestField("Invoice Receipt At"); + FREInvoiceLifecycle.TestField("Sender Platform ID"); + FREInvoiceLifecycle.TestField("Sender Platform Scheme"); + FREInvoiceLifecycle.TestField("Sender Platform Name"); + FREInvoiceLifecycle.TestField("Invoice Issuer ID"); + FREInvoiceLifecycle.TestField("Invoice Issuer Scheme"); + FREInvoiceLifecycle.TestField("Invoice Issuer Name"); + end; + + local procedure ValidatePaymentStatus(LifecycleStatus: Enum "FR E-Invoice Lifecycle Status") + var + UnsupportedStatusErrorInfo: ErrorInfo; + begin + if LifecycleStatus in [LifecycleStatus::Collected, LifecycleStatus::"Negative Collected"] then + exit; + + Session.LogMessage( + '0000TDP', StrSubstNo(UnsupportedLifecycleStatusErr, LifecycleStatus), Verbosity::Error, + DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', LifecycleTelemetryCategoryTok); + UnsupportedStatusErrorInfo.ErrorType(ErrorType::Internal); + UnsupportedStatusErrorInfo.Message(StrSubstNo(UnsupportedLifecycleStatusErr, LifecycleStatus)); + Error(UnsupportedStatusErrorInfo); + end; + + var + NoCapturedOccurrenceErr: Label 'No unprocessed French invoice lifecycle occurrence exists for E-Document entry %1.', Comment = '%1 = E-Document entry number'; + VATBreakdownErr: Label 'Lifecycle occurrence %1 does not have the VAT breakdown required for a French collected status message.', Comment = '%1 = lifecycle occurrence entry number'; + UnsupportedLifecycleStatusErr: Label 'Lifecycle status %1 is not supported by the French collected status message builder.', Comment = '%1 = lifecycle status'; + InternalLifecycleStateErr: Label 'An internal lifecycle processing error occurred.'; + LifecycleTelemetryCategoryTok: Label 'French E-Invoice Lifecycle', Locked = true; + LifecycleMessageNameTok: Label 'Invoice lifecycle collected status', Locked = true; + RsmNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:CrossDomainAcknowledgementAndResponse:100', Locked = true; + RamNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', Locked = true; + QdtNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100', Locked = true; + UdtNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100', Locked = true; + RegulatedBusinessProcessTok: Label 'REGULATED', Locked = true; + CDVInvoiceProfileTok: Label 'urn.cpro.gouv.fr:1p0:CDV:invoice', Locked = true; + PPFInvoiceProfileTok: Label 'urn.cpro.gouv.fr:1p0:CDV:einvoicingF2', Locked = true; + InformationTypeCodeTok: Label '23', Locked = true; + InvoiceReferenceStatusCodeTok: Label '47', Locked = true; + InvoiceTypeCodeTok: Label '380', Locked = true; + CollectedStatusCodeTok: Label '212', Locked = true; + CollectedStatusNameTok: Label 'Encaissée', Locked = true; + CollectedAmountTypeCodeTok: Label 'MEN', Locked = true; + DateTimeFormatCodeTok: Label '204', Locked = true; + DateFormatCodeTok: Label '102', Locked = true; + SenderRoleCodeTok: Label 'WK', Locked = true; + SellerRoleCodeTok: Label 'SE', Locked = true; + PPFIdentifierTok: Label '9998', Locked = true; + PPFIdentifierSchemeTok: Label '0238', Locked = true; + PPFNameTok: Label 'PPF', Locked = true; + PPFRoleCodeTok: Label 'DFH', Locked = true; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProc.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProc.Enum.al new file mode 100644 index 00000000000..dc8ec1dd1da --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProc.Enum.al @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +enum 10974 "FR E-Invoice Lifecycle Proc." +{ + Extensible = false; + + value(0; Captured) + { + Caption = 'Captured'; + } + value(1; "Message Created") + { + Caption = 'Message Created'; + } + value(2; Queued) + { + Caption = 'Queued'; + } + value(4; Failed) + { + Caption = 'Failed'; + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleStatus.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleStatus.Enum.al new file mode 100644 index 00000000000..8e8b2880ed1 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleStatus.Enum.al @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +enum 10973 "FR E-Invoice Lifecycle Status" +{ + Extensible = false; + + value(0; Collected) + { + Caption = 'Collected'; + } + value(1; "Negative Collected") + { + Caption = 'Negative Collected'; + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleVAT.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleVAT.Table.al new file mode 100644 index 00000000000..41198fe826f --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleVAT.Table.al @@ -0,0 +1,82 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +table 10971 "FR E-Invoice Lifecycle VAT" +{ + Caption = 'FR E-Invoice Lifecycle VAT'; + DataClassification = CustomerContent; + InherentPermissions = X; + ReplicateData = false; + + fields + { + field(1; "Lifecycle Entry No."; Integer) + { + Caption = 'Lifecycle Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "FR E-Invoice Lifecycle"."Entry No."; + ToolTip = 'Specifies the parent lifecycle occurrence entry.'; + } + field(2; "Line No."; Integer) + { + Caption = 'Line No.'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies the line number of the VAT breakdown entry.'; + } + field(3; "VAT %"; Decimal) + { + AutoFormatType = 0; + Caption = 'VAT %'; + DataClassification = CustomerContent; + DecimalPlaces = 0 : 5; + ToolTip = 'Specifies the VAT rate for this breakdown line.'; + } + field(4; "Reported Amount"; Decimal) + { + AutoFormatExpression = Rec."Currency Code"; + AutoFormatType = 1; + Caption = 'Reported Amount'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the amount reported for this VAT rate.'; + } + field(5; "Currency Code"; Code[10]) + { + Caption = 'Currency Code'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the currency code for the reported amount.'; + } + } + + keys + { + key(PK; "Lifecycle Entry No.", "Line No.") + { + Clustered = true; + } + key(VATRate; "Lifecycle Entry No.", "VAT %") + { + Unique = true; + } + } + + trigger OnModify() + begin + Error(ImmutableVATBreakdownErr); + end; + + trigger OnDelete() + begin + Error(ImmutableVATBreakdownErr); + end; + + trigger OnRename() + begin + Error(ImmutableVATBreakdownErr); + end; + + var + ImmutableVATBreakdownErr: Label 'A French electronic invoice lifecycle VAT breakdown cannot be changed.'; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleWorker.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleWorker.Codeunit.al new file mode 100644 index 00000000000..223982b5c32 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleWorker.Codeunit.al @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +codeunit 10976 "FR E-Invoice Lifecycle Worker" +{ + Access = Internal; + InherentEntitlements = X; + TableNo = "FR E-Invoice Lifecycle"; + + trigger OnRun() + var + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + Rec.TestField("Processing Status", Rec."Processing Status"::Queued); + Rec.TestField("E-Document Message Entry No.", 0); + FREInvoiceLifecycleMgt.CreateLifecycleMessage(Rec); + end; +} \ No newline at end of file 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 new file mode 100644 index 00000000000..459ca00012c --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al @@ -0,0 +1,115 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +enum 10971 "FR Regulatory Comment Type" +{ + Extensible = true; + + value(0; None) + { + Caption = ''; + } + value(1; AAA) + { + Caption = 'AAA - Goods description'; + } + value(2; AAB) + { + Caption = 'AAB - Terms of payments'; + } + value(3; AAC) + { + Caption = 'AAC - Dangerous goods additional information'; + } + value(4; AAI) + { + Caption = 'AAI - General information'; + } + value(5; AAJ) + { + Caption = 'AAJ - Additional conditions of sale/purchase'; + } + value(6; AAK) + { + Caption = 'AAK - Price conditions'; + } + value(7; ABN) + { + Caption = 'ABN - Accounting information'; + } + value(8; ABR) + { + Caption = 'ABR - Documents delivery instructions'; + } + value(9; ACB) + { + Caption = 'ACB - Additional information'; + } + value(10; ACD) + { + Caption = 'ACD - Reason'; + } + value(11; ACE) + { + Caption = 'ACE - Dispute'; + } + value(12; ALC) + { + Caption = 'ALC - Allowance/charge information'; + } + value(13; CUR) + { + Caption = 'CUR - Customer remarks'; + } + value(14; DEL) + { + Caption = 'DEL - Delivery information'; + } + value(15; GEN) + { + Caption = 'GEN - Entire transaction set'; + } + value(16; INV) + { + Caption = 'INV - Invoice instruction'; + } + value(17; PAI) + { + Caption = 'PAI - Payment instructions information'; + } + value(18; PMD) + { + Caption = 'PMD - Payment detail/remittance information'; + } + value(19; PMT) + { + Caption = 'PMT - Payment information'; + } + value(20; PRD) + { + Caption = 'PRD - Product information'; + } + value(21; REG) + { + Caption = 'REG - Regulatory information'; + } + value(22; SUR) + { + Caption = 'SUR - Supplier remarks'; + } + value(23; TXD) + { + Caption = 'TXD - Tax declaration'; + } + value(24; ZZZ) + { + Caption = 'ZZZ - Mutually defined'; + } + value(25; BAR) + { + Caption = 'BAR - Information for archiving'; + } +} \ No newline at end of file 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 cbaabe92c66..970c51477ab 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,10 +20,18 @@ 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/EReportingEDocService.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.PageExt.al new file mode 100644 index 00000000000..edcc83152d1 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.PageExt.al @@ -0,0 +1,35 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +using Microsoft.eServices.EDocument; + +pageextension 10977 "E-Reporting E-Doc. Service" extends "E-Document Service" +{ + layout + { + addlast(ExportProcessing) + { + group(FrenchLifecycle) + { + Caption = 'French Invoice Lifecycle'; + Visible = (Rec."Document Format" = Rec."Document Format"::"Peppol BIS 3.0 FR") or (Rec."Document Format" = Rec."Document Format"::"Factur-X FR"); + + field("FR Sender Platform ID"; Rec."FR Sender Platform ID") + { + ApplicationArea = All; + } + field("FR Sender Platform Scheme"; Rec."FR Sender Platform Scheme") + { + ApplicationArea = All; + } + field("FR Sender Platform Name"; Rec."FR Sender Platform Name") + { + ApplicationArea = All; + } + } + } + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.TableExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.TableExt.al new file mode 100644 index 00000000000..ee1bbdf1b77 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.TableExt.al @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +using Microsoft.eServices.EDocument; + +tableextension 10977 "E-Reporting E-Doc. Service" extends "E-Document Service" +{ + fields + { + field(10970; "FR Sender Platform ID"; Text[50]) + { + Caption = 'FR Sender Platform ID'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies the identifier of the French approved platform that sends lifecycle messages.'; + } + field(10971; "FR Sender Platform Scheme"; Code[4]) + { + Caption = 'FR Sender Platform Scheme'; + DataClassification = SystemMetadata; + InitValue = '0238'; + ToolTip = 'Specifies the identifier scheme of the French approved platform.'; + } + field(10972; "FR Sender Platform Name"; Text[100]) + { + Caption = 'FR Sender Platform Name'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies the name of the French approved platform that sends lifecycle messages.'; + } + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al index e2d2204b402..4a7d3cd7bd3 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al @@ -16,7 +16,29 @@ pageextension 10974 "E-Reporting E-Documents" extends "E-Documents" { ApplicationArea = Basic, Suite; Caption = 'E-Reporting Acceptance Date'; - ToolTip = 'Specifies the date and time when the e-reporting transaction was accepted by the tax authority.'; + } + } + } + + actions + { + addlast(Processing) + { + action(ViewFREInvoiceLifecycles) + { + ApplicationArea = Basic, Suite; + Caption = 'E-Invoice Lifecycles'; + Image = History; + ToolTip = 'View French electronic invoice lifecycle occurrences for this e-document.'; + + trigger OnAction() + var + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + begin + FREInvoiceLifecycle.SetCurrentKey("E-Document Entry No.", "Created At"); + FREInvoiceLifecycle.SetRange("E-Document Entry No.", Rec."Entry No"); + Page.Run(Page::"FR E-Invoice Lifecycles", FREInvoiceLifecycle); + end; } } } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocumentMessageType.EnumExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocumentMessageType.EnumExt.al new file mode 100644 index 00000000000..18c5a363c9e --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocumentMessageType.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.Formats; + +using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.eServices.EDocument.Processing.Message; + +enumextension 10973 "FR E-Document Message Type" extends "E-Document Message Type" +{ + value(10970; "FR Invoice Lifecycle") + { + Caption = 'FR Invoice Lifecycle'; + Implementation = IEDocMessageBuilder = "FR E-Invoice Lifecycle Msg."; + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREInvoiceLifecycleVAT.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREInvoiceLifecycleVAT.Page.al new file mode 100644 index 00000000000..c730fe70ac3 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREInvoiceLifecycleVAT.Page.al @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +page 10971 "FR E-Invoice Lifecycle VAT" +{ + ApplicationArea = Basic, Suite; + Caption = 'VAT Breakdown'; + Editable = false; + InherentPermissions = X; + PageType = ListPart; + SourceTable = "FR E-Invoice Lifecycle VAT"; + + layout + { + area(Content) + { + repeater(VATBreakdown) + { + field("VAT %"; Rec."VAT %") + { + ApplicationArea = Basic, Suite; + } + field("Reported Amount"; Rec."Reported Amount") + { + ApplicationArea = Basic, Suite; + } + } + } + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREInvoiceLifecycles.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREInvoiceLifecycles.Page.al new file mode 100644 index 00000000000..5255f747254 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREInvoiceLifecycles.Page.al @@ -0,0 +1,99 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +page 10970 "FR E-Invoice Lifecycles" +{ + ApplicationArea = Basic, Suite; + Caption = 'E-Invoice Lifecycles'; + Editable = false; + InherentPermissions = X; + PageType = List; + SourceTable = "FR E-Invoice Lifecycle"; + SourceTableView = sorting("E-Document Entry No.", "Created At") order(descending); + UsageCategory = History; + + layout + { + area(Content) + { + repeater(Lifecycles) + { + field("E-Document Entry No."; Rec."E-Document Entry No.") + { + ApplicationArea = Basic, Suite; + } + field("Lifecycle Status"; Rec."Lifecycle Status") + { + ApplicationArea = Basic, Suite; + } + field("Reported Amount"; Rec."Reported Amount") + { + ApplicationArea = Basic, Suite; + } + field("Currency Code"; Rec."Currency Code") + { + ApplicationArea = Basic, Suite; + } + field("Event Date"; Rec."Event Date") + { + ApplicationArea = Basic, Suite; + } + field("Processing Status"; Rec."Processing Status") + { + ApplicationArea = Basic, Suite; + } + field("E-Document Message Entry No."; Rec."E-Document Message Entry No.") + { + ApplicationArea = Basic, Suite; + } + field("Created At"; Rec."Created At") + { + ApplicationArea = Basic, Suite; + } + field("Last Error"; Rec."Last Error") + { + ApplicationArea = Basic, Suite; + } + } + part(VATBreakdown; "FR E-Invoice Lifecycle VAT") + { + ApplicationArea = Basic, Suite; + Caption = 'VAT Breakdown'; + SubPageLink = "Lifecycle Entry No." = field("Entry No."); + } + } + } + + actions + { + area(Processing) + { + action(RetryMessageCreation) + { + AccessByPermission = tabledata "FR E-Invoice Lifecycle" = M; + ApplicationArea = Basic, Suite; + Caption = 'Retry Message Creation'; + Enabled = Rec."Processing Status" = Rec."Processing Status"::Failed; + Image = Refresh; + ToolTip = 'Schedules another attempt to create the lifecycle message.'; + + trigger OnAction() + var + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + FREInvoiceLifecycleMgt.RetryLifecycleMessage(Rec); + CurrPage.Update(false); + end; + } + } + area(Promoted) + { + actionref(RetryMessageCreationPromoted; RetryMessageCreation) + { + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..5b0422abf47 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentLine.TableExt.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +using Microsoft.Sales.Comment; + +tableextension 10975 "FR Sales Comment Line" extends "Sales Comment Line" +{ + fields + { + field(10970; "FR Regulatory Comment Type"; Enum "FR Regulatory Comment Type") + { + Caption = 'French Regulatory Type'; + DataClassification = CustomerContent; + 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/Extensions/FRSalesCommentSheet.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentSheet.PageExt.al new file mode 100644 index 00000000000..1cb3ad99fa7 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentSheet.PageExt.al @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +using Microsoft.Sales.Comment; + +pageextension 10975 "FR Sales Comment Sheet" extends "Sales Comment Sheet" +{ + layout + { + addafter(Comment) + { + field("FR Regulatory Comment Type"; Rec."FR Regulatory Comment Type") + { + ApplicationArea = Comments; + } + } + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRServiceParticipant.TableExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRServiceParticipant.TableExt.al new file mode 100644 index 00000000000..2e56852eb75 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRServiceParticipant.TableExt.al @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +using Microsoft.eServices.EDocument.Service.Participant; + +tableextension 10974 "FR Service Participant" extends "Service Participant" +{ + fields + { + field(10970; "FR Identifier Scheme"; Enum "Electronic Address Scheme") + { + Caption = 'French Identifier Scheme'; + DataClassification = CustomerContent; + InitValue = " "; + ToolTip = 'Specifies the electronic address scheme used for French electronic invoicing.'; + } + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRServiceParticipants.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRServiceParticipants.PageExt.al new file mode 100644 index 00000000000..a36b9e257d2 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRServiceParticipants.PageExt.al @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +using Microsoft.eServices.EDocument.Service.Participant; + +pageextension 10980 "FR Service Participants" extends "Service Participants" +{ + layout + { + addafter("Participant Identifier") + { + field("FR Identifier Scheme"; Rec."FR Identifier Scheme") + { + ApplicationArea = All; + } + } + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/EReporting/EReportingFormat.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/EReporting/EReportingFormat.Codeunit.al index 0ca100bb306..9a9ba469062 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/EReporting/EReportingFormat.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/EReporting/EReportingFormat.Codeunit.al @@ -89,35 +89,6 @@ codeunit 10970 "E-Reporting Format" implements "E-Document" EDocServiceSupportedType.Insert(); end; - [EventSubscriber(ObjectType::Table, Database::"E-Document", 'OnBeforeModifyEvent', '', false, false)] - local procedure SetClearanceDateOnModify(var Rec: Record "E-Document"; var xRec: Record "E-Document"; RunTrigger: Boolean) - var - EDocumentServiceStatus: Record "E-Document Service Status"; - EDocumentService: Record "E-Document Service"; - begin - if Rec.Service = '' then - exit; - - EDocumentService.SetLoadFields("Document Format"); - if not EDocumentService.Get(Rec.Service) then - exit; - - if EDocumentService."Document Format" <> EDocumentService."Document Format"::"E-Reporting FR" then - exit; - - if not EDocumentServiceStatus.Get(Rec."Entry No", Rec.Service) then - exit; - - case EDocumentServiceStatus.Status of - EDocumentServiceStatus.Status::Approved, - EDocumentServiceStatus.Status::Cleared: - Rec."Clearance Date" := CurrentDateTime(); - EDocumentServiceStatus.Status::Rejected, - EDocumentServiceStatus.Status::"Not Cleared": - Rec."Clearance Date" := 0DT; - end; - end; - var GetCompleteInfoNotSupportedErr: Label 'Getting complete info from received document is not supported for this e-document format.'; } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/FacturX/EDocumentFacturXHandler.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/FacturX/EDocumentFacturXHandler.Codeunit.al index a905a419da7..6927543018c 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/FacturX/EDocumentFacturXHandler.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/FacturX/EDocumentFacturXHandler.Codeunit.al @@ -49,7 +49,7 @@ codeunit 10986 "E-Document Factur-X Handler" implements IStructuredFormatReader, /// The E-Document record that contains the document metadata and information. /// A temporary blob containing the Factur-X PDF/A-3 file or its CII XML. /// The draft preparation implementation that should process the created draft. - internal procedure ReadIntoDraft(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"): Enum "E-Doc. Process Draft" + procedure ReadIntoDraft(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"): Enum "E-Doc. Process Draft" var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; CIIXml: XmlDocument; @@ -87,7 +87,7 @@ codeunit 10986 "E-Document Factur-X Handler" implements IStructuredFormatReader, /// /// The E-Document record that contains the document to be displayed. /// A temporary blob containing the document data. Not used by this implementation. - internal procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob") + procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob") var EDocPurchaseHeader: Record "E-Document Purchase Header"; EDocPurchaseLine: Record "E-Document Purchase Line"; @@ -120,7 +120,7 @@ codeunit 10986 "E-Document Factur-X Handler" implements IStructuredFormatReader, /// /// The data storage entry that holds the received file. /// The structured data that the Read into Draft stage consumes. - internal procedure StructureReceivedEDocument(EDocumentDataStorage: Record "E-Doc. Data Storage"): Interface IStructuredDataType + procedure StructureReceivedEDocument(EDocumentDataStorage: Record "E-Doc. Data Storage"): Interface IStructuredDataType var CIIXml: XmlDocument; XmlNamespaces: XmlNamespaceManager; @@ -138,17 +138,17 @@ codeunit 10986 "E-Document Factur-X Handler" implements IStructuredFormatReader, SourceBlob.FromRecord(EDocumentDataStorage, EDocumentDataStorage.FieldNo("Data Storage")); end; - internal procedure GetFileFormat(): Enum "E-Doc. File Format" + procedure GetFileFormat(): Enum "E-Doc. File Format" begin exit("E-Doc. File Format"::XML); end; - internal procedure GetContent(): Text + procedure GetContent(): Text begin exit(StructuredData); end; - internal procedure GetReadIntoDraftImpl(): Enum "E-Doc. Read into Draft" + procedure GetReadIntoDraftImpl(): Enum "E-Doc. Read into Draft" begin exit("E-Doc. Read into Draft"::"Factur-X FR"); end; @@ -171,17 +171,29 @@ codeunit 10986 "E-Document Factur-X Handler" implements IStructuredFormatReader, end; /// - /// Returns the embedded CII XML of a PDF/A-3 container. When the blob is not a PDF, or the PDF does - /// not embed an attachment, the blob is returned unchanged so that plain CII XML is supported too. + /// Returns the embedded CII XML of a PDF/A-3 container. Non-PDF blobs are returned unchanged so + /// that plain CII XML is supported; PDFs without an embedded invoice are rejected. /// local procedure ExtractCIIXml(TempBlob: Codeunit "Temp Blob"): Codeunit "Temp Blob" var PdfInStream: InStream; begin TempBlob.CreateInStream(PdfInStream); - if TryGetEmbeddedAttachment(PdfInStream) then - exit(GlobalEmbeddedBlob); - exit(TempBlob); + if not IsPdf(PdfInStream) then + exit(TempBlob); + + TempBlob.CreateInStream(PdfInStream); + if not TryGetEmbeddedAttachment(PdfInStream) then + Error(NoEmbeddedInvoiceErr); + exit(GlobalEmbeddedBlob); + end; + + local procedure IsPdf(PdfInStream: InStream): Boolean + var + Signature: Text; + begin + PdfInStream.Read(Signature, 4); + exit(Signature = '%PDF'); end; [TryFunction] diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/EDocPeppolBIS30FRHdlr.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/EDocPeppolBIS30FRHdlr.Codeunit.al index 13d8fbd749b..036a005c095 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/EDocPeppolBIS30FRHdlr.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/EDocPeppolBIS30FRHdlr.Codeunit.al @@ -17,7 +17,7 @@ using System.Utilities; /// outbound side injects, so those are mapped back on import. /// Spec reference: https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/tree/ /// -codeunit 10985 "E-Doc. Peppol BIS 3.0 FR Hdlr" implements IStructuredFormatReader +codeunit 10980 "E-Doc. Peppol BIS 3.0 FR Hdlr" implements IStructuredFormatReader { Access = Internal; InherentEntitlements = X; @@ -41,7 +41,7 @@ codeunit 10985 "E-Doc. Peppol BIS 3.0 FR Hdlr" implements IStructuredFormatReade /// The E-Document record that contains the document metadata and information. /// A temporary blob containing the XML document stream to be processed. /// The draft preparation implementation that should process the created draft. - internal procedure ReadIntoDraft(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"): Enum "E-Doc. Process Draft" + procedure ReadIntoDraft(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"): Enum "E-Doc. Process Draft" var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; PeppolXml: XmlDocument; @@ -91,7 +91,7 @@ codeunit 10985 "E-Doc. Peppol BIS 3.0 FR Hdlr" implements IStructuredFormatReade /// /// The E-Document record that contains the document to be displayed. /// A temporary blob containing the document data. Not used by this implementation. - internal procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob") + procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob") var EDocPurchaseHeader: Record "E-Document Purchase Header"; EDocPurchaseLine: Record "E-Document Purchase Line"; 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 4b5586292cc..7a1b8ee0605 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 @@ -6,9 +6,15 @@ namespace Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.IO.Peppol; +using Microsoft.eServices.EDocument.Service.Participant; using Microsoft.Foundation.Company; using Microsoft.Purchases.Document; +using Microsoft.Sales.Comment; using Microsoft.Sales.Customer; +using Microsoft.Sales.FinanceCharge; +using Microsoft.Sales.History; +using Microsoft.Sales.Reminder; +using Microsoft.Service.History; using System.Utilities; codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" @@ -24,9 +30,9 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" PeppolBIS30: Codeunit "EDoc PEPPOL BIS 3.0"; begin FREDocHelpers.CheckSIRENNotEmpty(); - FREDocHelpers.CheckSIRETNotEmpty(); + FREDocHelpers.CheckSellerElectronicAddress(EDocumentService.Code); FREDocHelpers.CheckSellerCountryCode(); - FREDocHelpers.CheckBuyerElectronicAddress(SourceDocumentHeader); + FREDocHelpers.CheckBuyerElectronicAddress(SourceDocumentHeader, EDocumentService.Code); // Delegate standard PEPPOL validation PeppolBIS30.Check(SourceDocumentHeader, EDocumentService, EDocumentProcessingPhase); @@ -40,7 +46,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" PeppolBIS30.Create(EDocumentService, EDocument, SourceDocumentHeader, SourceDocumentLines, TempBlob); // Post-process XML to inject French-specific elements - InjectFrenchElements(TempBlob, SourceDocumentHeader); + InjectFrenchElements(TempBlob, SourceDocumentHeader, SourceDocumentLines, EDocumentService); end; procedure CreateBatch(EDocService: Record "E-Document Service"; var EDocument: Record "E-Document"; var SourceDocumentHeaders: RecordRef; var SourceDocumentsLines: RecordRef; var TempBlob: Codeunit "Temp Blob") @@ -63,7 +69,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" CreatedDocumentLines.GetTable(TempPurchaseLine); end; - local procedure InjectFrenchElements(var TempBlob: Codeunit "Temp Blob"; SourceDocumentHeader: RecordRef) + local procedure InjectFrenchElements(var TempBlob: Codeunit "Temp Blob"; SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; EDocumentService: Record "E-Document Service") var CompanyInformation: Record "Company Information"; XmlDoc: XmlDocument; @@ -72,19 +78,28 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" NamespaceMgr: XmlNamespaceManager; ElecAddress: Text[250]; ElecAddressScheme: Enum "Electronic Address Scheme"; + FrenchPostProcessingErrorInfo: ErrorInfo; HasElecAddress: Boolean; begin CompanyInformation.Get(); TempBlob.CreateInStream(InStr, TextEncoding::UTF8); - XmlDocument.ReadFrom(InStr, XmlDoc); + if not XmlDocument.ReadFrom(InStr, XmlDoc) then begin + FrenchPostProcessingErrorInfo.ErrorType(ErrorType::Internal); + FrenchPostProcessingErrorInfo.Message(FrenchPostProcessingXmlErr); + Error(FrenchPostProcessingErrorInfo); + end; InitNamespaceManager(NamespaceMgr, XmlDoc); + SetFrenchBillingMode(XmlDoc, NamespaceMgr, SourceDocumentLines); + RemoveZeroAllowanceTotal(XmlDoc, NamespaceMgr); InjectSupplierIdentification(XmlDoc, NamespaceMgr, CompanyInformation); - InjectSupplierEndpoint(XmlDoc, NamespaceMgr, CompanyInformation); + InjectSupplierEndpoint(XmlDoc, NamespaceMgr, CompanyInformation, EDocumentService.Code); + InjectRegulatoryComments(XmlDoc, NamespaceMgr, SourceDocumentHeader); + InjectExtendedCTCFranceElements(XmlDoc, NamespaceMgr, SourceDocumentHeader); - HasElecAddress := GetCustomerElecAddress(SourceDocumentHeader, ElecAddress, ElecAddressScheme); + HasElecAddress := GetCustomerElecAddress(SourceDocumentHeader, EDocumentService.Code, ElecAddress, ElecAddressScheme); InjectBuyerEndpoint(XmlDoc, NamespaceMgr, HasElecAddress, ElecAddress, ElecAddressScheme); InjectBuyerIdentification(XmlDoc, NamespaceMgr, HasElecAddress, ElecAddress, ElecAddressScheme); @@ -93,6 +108,224 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" XmlDoc.WriteTo(OutStr); end; + local procedure SetFrenchBillingMode(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; var SourceDocumentLines: RecordRef) + var + ProfileIdNode: XmlNode; + NewProfileIdNode: XmlNode; + begin + if not XmlDoc.SelectSingleNode('/*/cbc:ProfileID', NamespaceMgr, ProfileIdNode) then + exit; + NewProfileIdNode := XmlElement.Create('ProfileID', CbcNamespaceTok, GetFrenchBillingMode(SourceDocumentLines)).AsXmlNode(); + ProfileIdNode.ReplaceWith(NewProfileIdNode); + end; + + local procedure GetFrenchBillingMode(var SourceDocumentLines: RecordRef): Text + var + SalesInvoiceLine: Record "Sales Invoice Line"; + SalesCrMemoLine: Record "Sales Cr.Memo Line"; + ServiceInvoiceLine: Record "Service Invoice Line"; + ServiceCrMemoLine: Record "Service Cr.Memo Line"; + HasItemLines: Boolean; + HasNonItemLines: Boolean; + begin + case SourceDocumentLines.Number of + Database::"Sales Invoice Line": + begin + SourceDocumentLines.SetTable(SalesInvoiceLine); + SalesInvoiceLine.SetRange(Type, SalesInvoiceLine.Type::Item); + HasItemLines := not SalesInvoiceLine.IsEmpty(); + SalesInvoiceLine.SetFilter(Type, '<>%1&<>%2', SalesInvoiceLine.Type::" ", SalesInvoiceLine.Type::Item); + HasNonItemLines := not SalesInvoiceLine.IsEmpty(); + end; + Database::"Sales Cr.Memo Line": + begin + SourceDocumentLines.SetTable(SalesCrMemoLine); + SalesCrMemoLine.SetRange(Type, SalesCrMemoLine.Type::Item); + HasItemLines := not SalesCrMemoLine.IsEmpty(); + SalesCrMemoLine.SetFilter(Type, '<>%1&<>%2', SalesCrMemoLine.Type::" ", SalesCrMemoLine.Type::Item); + HasNonItemLines := not SalesCrMemoLine.IsEmpty(); + end; + Database::"Service Invoice Line": + begin + SourceDocumentLines.SetTable(ServiceInvoiceLine); + ServiceInvoiceLine.SetRange(Type, ServiceInvoiceLine.Type::Item); + HasItemLines := not ServiceInvoiceLine.IsEmpty(); + ServiceInvoiceLine.SetFilter(Type, '<>%1&<>%2', ServiceInvoiceLine.Type::" ", ServiceInvoiceLine.Type::Item); + HasNonItemLines := not ServiceInvoiceLine.IsEmpty(); + end; + Database::"Service Cr.Memo Line": + begin + SourceDocumentLines.SetTable(ServiceCrMemoLine); + ServiceCrMemoLine.SetRange(Type, ServiceCrMemoLine.Type::Item); + HasItemLines := not ServiceCrMemoLine.IsEmpty(); + ServiceCrMemoLine.SetFilter(Type, '<>%1&<>%2', ServiceCrMemoLine.Type::" ", ServiceCrMemoLine.Type::Item); + HasNonItemLines := not ServiceCrMemoLine.IsEmpty(); + end; + Database::"Issued Reminder Line", + Database::"Issued Fin. Charge Memo Line": + HasNonItemLines := true; + end; + + if HasItemLines and HasNonItemLines then + exit(BillingModeM1Tok); + if HasNonItemLines then + exit(BillingModeS1Tok); + exit(BillingModeB1Tok); + end; + + local procedure RemoveZeroAllowanceTotal(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager) + var + ZeroAllowanceTotalNode: XmlNode; + begin + if XmlDoc.SelectSingleNode('/*/cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount[number(normalize-space(.)) = 0]', NamespaceMgr, ZeroAllowanceTotalNode) then + ZeroAllowanceTotalNode.Remove(); + end; + + local procedure InjectExtendedCTCFranceElements(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; SourceDocumentHeader: RecordRef) + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + ShipmentPostingDates: Dictionary of [Code[20], Date]; + CustomizationIdNode: XmlNode; + NewCustomizationIdNode: XmlNode; + begin + if SourceDocumentHeader.Number <> Database::"Sales Invoice Header" then + exit; + + SourceDocumentHeader.SetTable(SalesInvoiceHeader); + if not RequiresExtendedCTCFrance(SalesInvoiceHeader."No.", ShipmentPostingDates) then + exit; + + if XmlDoc.SelectSingleNode('/*/cbc:CustomizationID', NamespaceMgr, CustomizationIdNode) then begin + NewCustomizationIdNode := XmlElement.Create('CustomizationID', CbcNamespaceTok, ExtendedCTCFranceCustomizationIdTok).AsXmlNode(); + CustomizationIdNode.ReplaceWith(NewCustomizationIdNode); + end; + + SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + SalesInvoiceLine.SetLoadFields("Line No.", "Order No.", "Order Line No.", "Shipment No."); + if SalesInvoiceLine.FindSet() then + repeat + InjectExtendedLineReferences(XmlDoc, NamespaceMgr, SalesInvoiceLine, ShipmentPostingDates); + until SalesInvoiceLine.Next() = 0; + end; + + local procedure RequiresExtendedCTCFrance(DocumentNo: Code[20]; var ShipmentPostingDates: Dictionary of [Code[20], Date]): Boolean + var + SalesInvoiceLine: Record "Sales Invoice Line"; + SalesShipmentHeader: Record "Sales Shipment Header"; + ShipmentNos: Dictionary of [Code[20], Boolean]; + OrderNos: Dictionary of [Text, Boolean]; + DeliveryDates: Dictionary of [Text, Boolean]; + ShipmentNo: Code[20]; + begin + SalesInvoiceLine.SetRange("Document No.", DocumentNo); + SalesInvoiceLine.SetLoadFields("Shipment No.", "Order No."); + if SalesInvoiceLine.FindSet() then + repeat + if SalesInvoiceLine."Shipment No." <> '' then + if not ShipmentNos.ContainsKey(SalesInvoiceLine."Shipment No.") then + ShipmentNos.Add(SalesInvoiceLine."Shipment No.", true); + if SalesInvoiceLine."Order No." <> '' then + AddDistinctValue(OrderNos, SalesInvoiceLine."Order No."); + until SalesInvoiceLine.Next() = 0; + + SalesShipmentHeader.SetLoadFields("No.", "Posting Date"); + foreach ShipmentNo in ShipmentNos.Keys() do + if SalesShipmentHeader.Get(ShipmentNo) then begin + ShipmentPostingDates.Add(SalesShipmentHeader."No.", SalesShipmentHeader."Posting Date"); + AddDistinctValue(DeliveryDates, Format(SalesShipmentHeader."Posting Date", 0, 9)); + end; + + exit((ShipmentNos.Count() > 1) or (OrderNos.Count() > 1) or (DeliveryDates.Count() > 1)); + end; + + local procedure AddDistinctValue(var Values: Dictionary of [Text, Boolean]; Value: Text) + begin + if not Values.ContainsKey(Value) then + Values.Add(Value, true); + end; + + local procedure InjectExtendedLineReferences(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; SalesInvoiceLine: Record "Sales Invoice Line"; ShipmentPostingDates: Dictionary of [Code[20], Date]) + var + InvoiceLineNode: XmlNode; + ItemNode: XmlNode; + OrderLineReferenceElement: XmlElement; + OrderReferenceElement: XmlElement; + DeliveryElement: XmlElement; + LineXPath: Text; + ShipmentPostingDate: Date; + begin + LineXPath := StrSubstNo(InvoiceLineXPathTok, Format(SalesInvoiceLine."Line No.", 0, 9)); + if not XmlDoc.SelectSingleNode(LineXPath, NamespaceMgr, InvoiceLineNode) then + exit; + if not InvoiceLineNode.SelectSingleNode('cac:Item', NamespaceMgr, ItemNode) then + exit; + + if SalesInvoiceLine."Order No." <> '' 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.")); + OrderLineReferenceElement.Add(OrderReferenceElement); + ItemNode.AddBeforeSelf(OrderLineReferenceElement); + end; + + if SalesInvoiceLine."Shipment No." = '' then + exit; + + if not ShipmentPostingDates.Get(SalesInvoiceLine."Shipment No.", ShipmentPostingDate) then + exit; + + DeliveryElement := XmlElement.Create('Delivery', CacNamespaceTok); + DeliveryElement.Add(XmlElement.Create('ID', CbcNamespaceTok, SalesInvoiceLine."Shipment No.")); + DeliveryElement.Add(XmlElement.Create('ActualDeliveryDate', CbcNamespaceTok, Format(ShipmentPostingDate, 0, 9))); + ItemNode.AddBeforeSelf(DeliveryElement); + end; + + local procedure InjectRegulatoryComments(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; SourceDocumentHeader: RecordRef) + var + SalesCommentLine: Record "Sales Comment Line"; + AnchorNode: XmlNode; + NoteElement: XmlElement; + DocumentNo: Code[20]; + DocumentType: Enum "Sales Comment Document Type"; + begin + case SourceDocumentHeader.Number of + Database::"Sales Invoice Header": + begin + DocumentType := DocumentType::"Posted Invoice"; + DocumentNo := CopyStr(SourceDocumentHeader.Field(3).Value(), 1, MaxStrLen(DocumentNo)); + if not XmlDoc.SelectSingleNode('/*/cbc:InvoiceTypeCode', NamespaceMgr, AnchorNode) then + exit; + end; + Database::"Sales Cr.Memo Header": + begin + DocumentType := DocumentType::"Posted Credit Memo"; + DocumentNo := CopyStr(SourceDocumentHeader.Field(3).Value(), 1, MaxStrLen(DocumentNo)); + if not XmlDoc.SelectSingleNode('/*/cbc:CreditNoteTypeCode', NamespaceMgr, AnchorNode) then + exit; + end; + else + exit; + end; + + SalesCommentLine.SetRange("Document Type", DocumentType); + SalesCommentLine.SetRange("No.", DocumentNo); + SalesCommentLine.SetFilter("FR Regulatory Comment Type", '<>%1', SalesCommentLine."FR Regulatory Comment Type"::None); + SalesCommentLine.SetLoadFields("FR Regulatory Comment Type", Comment); + if SalesCommentLine.FindSet() then + repeat + NoteElement := XmlElement.Create('Note', CbcNamespaceTok, StrSubstNo(RegulatoryCommentFormatTok, GetRegulatoryCommentTypeCode(SalesCommentLine."FR Regulatory Comment Type"), SalesCommentLine.Comment)); + AnchorNode.AddAfterSelf(NoteElement); + AnchorNode := NoteElement.AsXmlNode(); + until SalesCommentLine.Next() = 0; + end; + + local procedure GetRegulatoryCommentTypeCode(RegulatoryCommentType: Enum "FR Regulatory Comment Type"): Text + begin + exit(RegulatoryCommentType.Names.Get(RegulatoryCommentType.Ordinals.IndexOf(RegulatoryCommentType.AsInteger()))); + end; + local procedure InitNamespaceManager(var NamespaceMgr: XmlNamespaceManager; XmlDoc: XmlDocument) var RootElement: XmlElement; @@ -160,25 +393,39 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" InsertAsFirstChild(LegalEntityElement.AsXmlNode(), CompanyIdElement); end; - local procedure InjectSupplierEndpoint(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; CompanyInformation: Record "Company Information") + local procedure InjectSupplierEndpoint(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; CompanyInformation: Record "Company Information"; EDocumentServiceCode: Code[20]) var SupplierPartyNode: XmlNode; ExistingEndpointNode: XmlNode; EndpointElement: XmlElement; + ElecAddress: Text[250]; + ElecAddressScheme: Enum "Electronic Address Scheme"; begin if not XmlDoc.SelectSingleNode('//cac:AccountingSupplierParty/cac:Party', NamespaceMgr, SupplierPartyNode) then exit; - // Use SIRET as endpoint with scheme 0009 - if CompanyInformation."SIRET No." = '' then + if not GetServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Company, '', ElecAddress, ElecAddressScheme) then + if CompanyInformation."SIRET No." <> '' then begin + ElecAddress := CompanyInformation."SIRET No."; + ElecAddressScheme := ElecAddressScheme::"0009"; + end else + if CompanyInformation."Registration No." <> '' then begin + ElecAddress := CompanyInformation."Registration No."; + ElecAddressScheme := ElecAddressScheme::"0002"; + end else begin + ElecAddress := CopyStr(CompanyInformation.GetVATRegistrationNumber(), 1, MaxStrLen(ElecAddress)); + ElecAddressScheme := ElecAddressScheme::"9957"; + end; + + if ElecAddress = '' then exit; // Remove existing EndpointID if present if SupplierPartyNode.SelectSingleNode('cbc:EndpointID', NamespaceMgr, ExistingEndpointNode) then ExistingEndpointNode.Remove(); - EndpointElement := XmlElement.Create('EndpointID', CbcNamespaceTok, CompanyInformation."SIRET No."); - EndpointElement.SetAttribute('schemeID', '0009'); + EndpointElement := XmlElement.Create('EndpointID', CbcNamespaceTok, ElecAddress); + EndpointElement.SetAttribute('schemeID', GetElecAddressSchemeCode(ElecAddressScheme)); InsertAsFirstChild(SupplierPartyNode, EndpointElement); end; @@ -203,7 +450,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" InsertAsFirstChild(BuyerPartyNode, EndpointElement); end; - local procedure GetCustomerElecAddress(SourceDocumentHeader: RecordRef; var ElecAddress: Text[250]; var ElecAddressScheme: Enum "Electronic Address Scheme"): Boolean + local procedure GetCustomerElecAddress(SourceDocumentHeader: RecordRef; EDocumentServiceCode: Code[20]; var ElecAddress: Text[250]; var ElecAddressScheme: Enum "Electronic Address Scheme"): Boolean var Customer: Record Customer; FRCIIXMLBuilder: Codeunit "CII XML Builder"; @@ -217,15 +464,35 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" if CustomerNo = '' then exit(false); - Customer.SetLoadFields("FR Electronic Address", "FR Elec. Address Scheme"); + if GetServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Customer, CustomerNo, ElecAddress, ElecAddressScheme) then + exit(true); + + Customer.SetLoadFields("FR Electronic Address", "FR Elec. Address Scheme", "VAT Registration No."); if not Customer.Get(CustomerNo) then exit(false); ElecAddress := Customer."FR Electronic Address"; ElecAddressScheme := Customer."FR Elec. Address Scheme"; + if ElecAddress = '' then begin + ElecAddress := Customer."VAT Registration No."; + ElecAddressScheme := ElecAddressScheme::"9957"; + end; exit(ElecAddress <> ''); end; + local procedure GetServiceParticipantAddress(EDocumentServiceCode: Code[20]; ParticipantType: Enum "E-Document Source Type"; ParticipantNo: Code[20]; var ElecAddress: Text[250]; var ElecAddressScheme: Enum "Electronic Address Scheme"): Boolean + var + ServiceParticipant: Record "Service Participant"; + FREDocHelpers: Codeunit "EDoc. Helpers"; + begin + if not FREDocHelpers.HasServiceParticipantAddress(EDocumentServiceCode, ParticipantType, ParticipantNo, ServiceParticipant) then + exit(false); + + ElecAddress := CopyStr(ServiceParticipant."Participant Identifier", 1, MaxStrLen(ElecAddress)); + ElecAddressScheme := ServiceParticipant."FR Identifier Scheme"; + exit(true); + end; + local procedure InjectBuyerIdentification(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; HasElecAddress: Boolean; ElecAddress: Text[250]; ElecAddressScheme: Enum "Electronic Address Scheme") var BuyerPartyNode: XmlNode; @@ -258,6 +525,10 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" exit('0009'); ElecAddressScheme::"0002": exit('0002'); + ElecAddressScheme::"0225": + exit('0225'); + ElecAddressScheme::"9957": + exit('9957'); else exit(Format(ElecAddressScheme)); end; @@ -288,6 +559,13 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" ParentNode.AsXmlElement().Add(NewElement); end; + [EventSubscriber(ObjectType::Table, Database::"E-Document Log", OnBeforeExportDataStorage, '', false, false)] + local procedure AddXmlExtensionOnBeforeExportDataStorage(EDocumentLog: Record "E-Document Log"; var FileName: Text) + begin + if EDocumentLog."Document Format" = EDocumentLog."Document Format"::"Peppol BIS 3.0 FR" then + FileName += '.xml'; + end; + [EventSubscriber(ObjectType::Table, Database::"E-Document Service", 'OnAfterValidateEvent', 'Document Format', false, false)] local procedure OnAfterValidateDocumentFormat(var Rec: Record "E-Document Service"; var xRec: Record "E-Document Service"; CurrFieldNo: Integer) var @@ -323,12 +601,16 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" EDocServiceSupportedType."Source Document Type" := EDocServiceSupportedType."Source Document Type"::"Issued Finance Charge Memo"; EDocServiceSupportedType.Insert(); - - EDocServiceSupportedType."Source Document Type" := EDocServiceSupportedType."Source Document Type"::"Purchase Invoice"; - EDocServiceSupportedType.Insert(); 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; + RegulatoryCommentFormatTok: Label '#%1#%2', Comment = '%1 = Regulatory comment type, %2 = Comment text', Locked = true; + BillingModeB1Tok: Label 'B1', Locked = true; + BillingModeS1Tok: Label 'S1', Locked = true; + BillingModeM1Tok: Label 'M1', Locked = true; + InvoiceLineXPathTok: Label '/*/cac:InvoiceLine[cbc:ID=''%1'']', Locked = true; + FrenchPostProcessingXmlErr: Label 'The PEPPOL document could not be read while applying the French electronic invoicing requirements.'; } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFREdit.PermissionSet.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFREdit.PermissionSet.al new file mode 100644 index 00000000000..722c3a5af28 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFREdit.PermissionSet.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +permissionset 10972 "E-Reporting FR Edit" +{ + Access = Public; + Assignable = true; + Caption = 'E-Reporting FR - Edit'; + + IncludedPermissionSets = "E-Reporting FR Read"; + + Permissions = tabledata "FR E-Invoice Lifecycle" = im, + tabledata "FR E-Invoice Lifecycle VAT" = i, + codeunit "FR E-Invoice Lifecycle Worker" = X, + codeunit "FR E-Invoice Lifecycle Error" = X, + codeunit "FR E-Invoice Lifecycle Mgt." = X; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFREditExt.PermissionSetExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFREditExt.PermissionSetExt.al new file mode 100644 index 00000000000..e028e689480 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFREditExt.PermissionSetExt.al @@ -0,0 +1,12 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +using Microsoft.eServices.EDocument; + +permissionsetextension 10975 "E-Reporting FR - Edit Ext" extends "E-Doc. Core - User" +{ + IncludedPermissionSets = "E-Reporting FR Edit"; +} diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRRead.PermissionSet.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRRead.PermissionSet.al new file mode 100644 index 00000000000..90cb7183dae --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRRead.PermissionSet.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.Formats; + +permissionset 10971 "E-Reporting FR Read" +{ + Access = Public; + Assignable = true; + Caption = 'E-Reporting FR - Read'; + + Permissions = tabledata "FR E-Invoice Lifecycle" = R, + tabledata "FR E-Invoice Lifecycle VAT" = R, + page "FR E-Invoice Lifecycles" = X, + page "FR E-Invoice Lifecycle VAT" = X; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRReadExt.PermissionSetExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRReadExt.PermissionSetExt.al new file mode 100644 index 00000000000..abeab2d8f80 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRReadExt.PermissionSetExt.al @@ -0,0 +1,12 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +using Microsoft.eServices.EDocument; + +permissionsetextension 10976 "E-Reporting FR - Read Ext" extends "E-Doc. Core - Read" +{ + IncludedPermissionSets = "E-Reporting FR Read"; +} diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/.resources/peppolfr/peppol-bis-fr-creditmemo-0.xml b/src/Apps/FR/EDocument_FR/EReportingFR/test/.resources/peppolfr/peppol-bis-fr-creditmemo-0.xml new file mode 100644 index 00000000000..df719b2d253 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/.resources/peppolfr/peppol-bis-fr-creditmemo-0.xml @@ -0,0 +1,67 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:international:fr:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + PBIS-FR-CM-6002 + 2026-03-03 + 381 + XYZ + + + PBIS-FR-6001 + + + + + + Fournisseur SARL + + + FR12345678901 + + VAT + + + + + + + + Acheteur SA + + + + + 24 + + + 120 + 120 + 144 + 0 + 144 + + + 1 + 1 + 120 + + Chaise de bureau + + ART-500 + + + S + 20 + + VAT + + + + + 120 + + + 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..7b53ca15305 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 @@ -1,3 +1,11 @@ +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; + // ------------------------------------------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. @@ -13,7 +21,11 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" FacturXInvoiceTok: Label 'facturx/facturx-invoice-0.xml', Locked = true; FacturXCreditMemoTok: Label 'facturx/facturx-creditmemo-0.xml', Locked = true; PeppolBIS30FRInvoiceTok: Label 'peppolfr/peppol-bis-fr-invoice-0.xml', Locked = true; + PeppolBIS30FRCreditMemoTok: Label 'peppolfr/peppol-bis-fr-creditmemo-0.xml', Locked = true; UnsupportedXmlTok: Label '', Locked = true; + MalformedXmlTok: Label '%1', Locked = true; #region Factur-X [Test] @@ -25,11 +37,11 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" EDocumentFacturXHandler: Codeunit "E-Document Factur-X Handler"; ProcessDraft: Enum "E-Doc. Process Draft"; begin - // [FEATURE] [E-Document] [Factur-X] [Import] + // [FEATURE] [AI test] // [SCENARIO] A Factur-X invoice is read into a purchase invoice draft + Initialize(); // [GIVEN] A Factur-X CII invoice - Initialize(); CreateEDocument(EDocument); // [WHEN] The document is read into draft @@ -74,11 +86,11 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" EDocumentFacturXHandler: Codeunit "E-Document Factur-X Handler"; ProcessDraft: Enum "E-Doc. Process Draft"; begin - // [FEATURE] [E-Document] [Factur-X] [Import] + // [FEATURE] [AI test] // [SCENARIO] A Factur-X credit memo is read into a purchase credit memo draft + Initialize(); // [GIVEN] A Factur-X CII credit memo - Initialize(); CreateEDocument(EDocument); // [WHEN] The document is read into draft @@ -97,11 +109,11 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" EDocument: Record "E-Document"; EDocumentFacturXHandler: Codeunit "E-Document Factur-X Handler"; begin - // [FEATURE] [E-Document] [Factur-X] [Import] + // [FEATURE] [AI test] // [SCENARIO] Reading a document that is not a Cross Industry Invoice fails with a clear error + Initialize(); // [GIVEN] An XML document with an unsupported root element - Initialize(); CreateEDocument(EDocument); // [WHEN] The document is read into draft @@ -111,6 +123,66 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" Assert.ExpectedError('Unsupported XML root element'); end; + [Test] + procedure FacturXMalformedXmlFails() + var + EDocument: Record "E-Document"; + EDocumentFacturXHandler: Codeunit "E-Document Factur-X Handler"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Reading malformed Factur-X XML fails with a clear error + Initialize(); + + // [GIVEN] A malformed XML document + CreateEDocument(EDocument); + + // [WHEN] The document is read into draft + asserterror EDocumentFacturXHandler.ReadIntoDraft(EDocument, CreateBlob(MalformedXmlTok)); + + // [THEN] The reader reports that the document cannot be read as XML + Assert.ExpectedError('The received document could not be read as XML.'); + end; + + [Test] + procedure FacturXUnsupportedTypeCodeFails() + var + EDocument: Record "E-Document"; + EDocumentFacturXHandler: Codeunit "E-Document Factur-X Handler"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Reading a Factur-X document with an unsupported type code fails with a clear error + Initialize(); + + // [GIVEN] A Cross Industry Invoice with unsupported document type code 999 + CreateEDocument(EDocument); + + // [WHEN] The document is read into draft + asserterror EDocumentFacturXHandler.ReadIntoDraft(EDocument, GetFacturXBlobWithTypeCode('999')); + + // [THEN] The reader reports the unsupported document type code + Assert.ExpectedError('Unsupported document type code 999.'); + end; + + [Test] + procedure FacturXPdfWithoutEmbeddedInvoiceFails() + var + EDocument: Record "E-Document"; + EDocumentFacturXHandler: Codeunit "E-Document Factur-X Handler"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Reading a PDF without an embedded Factur-X invoice fails with a clear error + Initialize(); + + // [GIVEN] A PDF document without an embedded invoice + CreateEDocument(EDocument); + + // [WHEN] The document is read into draft + asserterror EDocumentFacturXHandler.ReadIntoDraft(EDocument, CreateBlob(PdfWithoutEmbeddedInvoiceTok)); + + // [THEN] The reader reports the missing embedded invoice + Assert.ExpectedError('The PDF file does not contain an embedded Factur-X invoice.'); + end; + [Test] procedure FacturXInvoiceCanBeReadIntoDraftTwice() var @@ -118,11 +190,11 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" EDocumentPurchaseLine: Record "E-Document Purchase Line"; EDocumentFacturXHandler: Codeunit "E-Document Factur-X Handler"; begin - // [FEATURE] [E-Document] [Factur-X] [Import] + // [FEATURE] [AI test] // [SCENARIO] Re-running Read into Draft replaces the previous draft instead of duplicating it + Initialize(); // [GIVEN] A Factur-X invoice that has been read into draft - Initialize(); CreateEDocument(EDocument); EDocumentFacturXHandler.ReadIntoDraft(EDocument, GetResourceBlob(FacturXInvoiceTok)); @@ -145,11 +217,11 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" EDocPeppolBIS30FRHandler: Codeunit "E-Doc. Peppol BIS 3.0 FR Hdlr"; ProcessDraft: Enum "E-Doc. Process Draft"; begin - // [FEATURE] [E-Document] [Peppol BIS 3.0 FR] [Import] + // [FEATURE] [AI test] // [SCENARIO] A Peppol BIS 3.0 FR invoice is read into a purchase invoice draft + Initialize(); // [GIVEN] A Peppol BIS 3.0 FR UBL invoice - Initialize(); CreateEDocument(EDocument); // [WHEN] The document is read into draft @@ -183,17 +255,50 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" Assert.AreEqual(20, EDocumentPurchaseLine."VAT Rate", 'Wrong VAT rate.'); end; + [Test] + procedure PeppolBIS30FRCreditMemoIsReadIntoDraft() + var + EDocument: Record "E-Document"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + EDocPeppolBIS30FRHandler: Codeunit "E-Doc. Peppol BIS 3.0 FR Hdlr"; + ProcessDraft: Enum "E-Doc. Process Draft"; + begin + // [FEATURE] [AI test] + // [SCENARIO] A Peppol BIS 3.0 FR credit note is read into a purchase credit memo draft + Initialize(); + + // [GIVEN] A Peppol BIS 3.0 FR UBL credit note + CreateEDocument(EDocument); + + // [WHEN] The document is read into draft + ProcessDraft := EDocPeppolBIS30FRHandler.ReadIntoDraft(EDocument, GetResourceBlob(PeppolBIS30FRCreditMemoTok)); + + // [THEN] The draft is prepared as a purchase credit memo referring to the original invoice + Assert.AreEqual(Format("E-Doc. Process Draft"::"Purchase Credit Memo"), Format(ProcessDraft), 'The draft should be processed as a purchase credit memo.'); + EDocumentPurchaseHeader.GetFromEDocument(EDocument); + Assert.AreEqual('PBIS-FR-CM-6002', EDocumentPurchaseHeader."Sales Invoice No.", 'Wrong document number.'); + Assert.AreEqual('PBIS-FR-6001', EDocumentPurchaseHeader."Applies-to Ext. Invoice No.", 'Wrong applies-to external invoice number.'); + + // [THEN] The credit note line is extracted + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); + Assert.AreEqual(1, EDocumentPurchaseLine.Count(), 'Wrong number of draft lines.'); + EDocumentPurchaseLine.FindFirst(); + Assert.AreEqual(1, EDocumentPurchaseLine.Quantity, 'Wrong quantity.'); + Assert.AreEqual(120, EDocumentPurchaseLine."Sub Total", 'Wrong line sub total.'); + end; + [Test] procedure PeppolBIS30FRUnsupportedRootElementFails() var EDocument: Record "E-Document"; EDocPeppolBIS30FRHandler: Codeunit "E-Doc. Peppol BIS 3.0 FR Hdlr"; begin - // [FEATURE] [E-Document] [Peppol BIS 3.0 FR] [Import] + // [FEATURE] [AI test] // [SCENARIO] Reading a document that is neither an Invoice nor a CreditNote fails with a clear error + Initialize(); // [GIVEN] An XML document with an unsupported root element - Initialize(); CreateEDocument(EDocument); // [WHEN] The document is read into draft @@ -202,6 +307,49 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" // [THEN] The reader rejects the document Assert.ExpectedError('Unsupported XML root element'); end; + + [Test] + procedure PeppolBIS30FRMalformedXmlFails() + var + EDocument: Record "E-Document"; + EDocPeppolBIS30FRHandler: Codeunit "E-Doc. Peppol BIS 3.0 FR Hdlr"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Reading malformed Peppol BIS 3.0 FR XML fails with a clear error + Initialize(); + + // [GIVEN] A malformed XML document + CreateEDocument(EDocument); + + // [WHEN] The document is read into draft + asserterror EDocPeppolBIS30FRHandler.ReadIntoDraft(EDocument, CreateBlob(MalformedXmlTok)); + + // [THEN] The reader reports that the document cannot be read as XML + Assert.ExpectedError('The received document could not be read as XML.'); + end; + + [Test] + procedure PeppolBIS30FRInvoiceCanBeReadIntoDraftTwice() + var + EDocument: Record "E-Document"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + EDocPeppolBIS30FRHandler: Codeunit "E-Doc. Peppol BIS 3.0 FR Hdlr"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Re-reading a Peppol BIS 3.0 FR invoice replaces the existing draft + Initialize(); + + // [GIVEN] A Peppol BIS 3.0 FR invoice that has been read into draft + CreateEDocument(EDocument); + EDocPeppolBIS30FRHandler.ReadIntoDraft(EDocument, GetResourceBlob(PeppolBIS30FRInvoiceTok)); + + // [WHEN] The document is read into draft again + EDocPeppolBIS30FRHandler.ReadIntoDraft(EDocument, GetResourceBlob(PeppolBIS30FRInvoiceTok)); + + // [THEN] The draft still contains a single line + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); + Assert.AreEqual(1, EDocumentPurchaseLine.Count(), 'Re-reading the document should not duplicate the draft lines.'); + end; #endregion local procedure Initialize() @@ -228,6 +376,15 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" exit(CreateBlob(NavApp.GetResourceAsText(FilePath, TextEncoding::UTF8))); end; + local procedure GetFacturXBlobWithTypeCode(TypeCode: Text): Codeunit "Temp Blob" + var + FacturXXml: Text; + begin + FacturXXml := NavApp.GetResourceAsText(FacturXInvoiceTok, TextEncoding::UTF8); + FacturXXml := FacturXXml.Replace('380', StrSubstNo(FacturXTypeCodeTok, TypeCode)); + exit(CreateBlob(FacturXXml)); + end; + local procedure CreateBlob(Content: Text) TempBlob: Codeunit "Temp Blob" var OutStr: OutStream; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/ExportEReportingTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/ExportEReportingTests.Codeunit.al index 0c22599c6f9..35abdc04152 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/ExportEReportingTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/ExportEReportingTests.Codeunit.al @@ -698,6 +698,7 @@ codeunit 148145 "Export E-Reporting Tests" CreateEDocumentServiceStatus(EDocument."Entry No", 'ERFR-APPR', "E-Document Service Status"::Approved); // [WHEN] E-Document is modified + EDocument.Validate(Status, EDocument.Status::Processed); EDocument.Modify(true); // [THEN] Clearance Date is set to non-zero DateTime @@ -723,6 +724,7 @@ codeunit 148145 "Export E-Reporting Tests" CreateEDocumentServiceStatus(EDocument."Entry No", 'ERFR-CLR', "E-Document Service Status"::Cleared); // [WHEN] E-Document is modified + EDocument.Validate(Status, EDocument.Status::Processed); EDocument.Modify(true); // [THEN] Clearance Date is set to non-zero DateTime @@ -750,6 +752,7 @@ codeunit 148145 "Export E-Reporting Tests" CreateEDocumentServiceStatus(EDocument."Entry No", 'ERFR-REJ', "E-Document Service Status"::Rejected); // [WHEN] E-Document is modified + EDocument.Validate(Status, EDocument.Status::Processed); EDocument.Modify(true); // [THEN] Clearance Date is reset to 0DT @@ -777,6 +780,7 @@ codeunit 148145 "Export E-Reporting Tests" CreateEDocumentServiceStatus(EDocument."Entry No", 'ERFR-NCLR', "E-Document Service Status"::"Not Cleared"); // [WHEN] E-Document is modified + EDocument.Validate(Status, EDocument.Status::Processed); EDocument.Modify(true); // [THEN] Clearance Date is reset to 0DT @@ -831,6 +835,29 @@ codeunit 148145 "Export E-Reporting Tests" EDocument.Get(EDocument."Entry No"); Assert.AreEqual(0DT, EDocument."Clearance Date", 'Clearance Date should remain 0DT for blank Service'); end; + + [Test] + procedure BlankServiceUsesFacturXStatusForClearanceDate() + var + EDocument: Record "E-Document"; + begin + // [FEATURE] [AI test] + // [SCENARIO] A blank document service uses its Factur-X service status to set the clearance date + Initialize(); + + // [GIVEN] An E-Document with no direct service and an approved Factur-X service status + CreateEDocumentService('FACTURX-FALLBACK', "E-Document Format"::"Factur-X FR"); + CreateEDocumentWithService(EDocument, ''); + CreateEDocumentServiceStatus(EDocument."Entry No", 'FACTURX-FALLBACK', "E-Document Service Status"::Approved); + + // [WHEN] The E-Document status changes + EDocument.Validate(Status, EDocument.Status::Processed); + EDocument.Modify(true); + + // [THEN] The fallback service status sets the clearance date + EDocument.Get(EDocument."Entry No"); + Assert.AreNotEqual(0DT, EDocument."Clearance Date", 'The approved Factur-X fallback status must set the clearance date.'); + end; #endregion local procedure Initialize() diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al index d73ef22afad..71c0b1425b9 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al @@ -8,6 +8,7 @@ using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Formats; using Microsoft.Finance.GeneralLedger.Account; using Microsoft.Finance.GeneralLedger.Setup; +using Microsoft.Finance.VAT.Setup; using Microsoft.Foundation.Address; using Microsoft.Foundation.Company; using Microsoft.Foundation.UOM; @@ -23,7 +24,8 @@ codeunit 148148 "Factur-X CII XML Tests" { Subtype = Test; Permissions = tabledata "Company Information" = rimd, - tabledata Customer = rimd; + tabledata Customer = rimd, + tabledata "VAT Business Posting Group" = rimd; trigger OnRun() begin @@ -40,7 +42,6 @@ codeunit 148148 "Factur-X CII XML Tests" CIIXMLBuilder: Codeunit "CII XML Builder"; IncorrectValueErr: Label 'Incorrect value for %1', Comment = '%1 = XML element path', Locked = true; FacturXProfileIdTok: Label 'urn:cen.eu:en16931:2017', Locked = true; - CustomerVATNoSequence: Integer; IsInitialized: Boolean; #region SalesInvoice @@ -361,14 +362,6 @@ codeunit 148148 "Factur-X CII XML Tests" // [SCENARIO] Factur-X CII XML has seller postal address from Company Information Initialize(); - // [GIVEN] Company Information with address - CompanyInformation.Get(); - if CompanyInformation.Address = '' then begin - CompanyInformation.Address := '123 Test Street'; - CompanyInformation.City := 'Paris'; - CompanyInformation."Post Code" := '75001'; - CompanyInformation.Modify(true); - end; // [WHEN] Create CII XML CreateSalesInvoiceCIIXML(TempBlob); @@ -476,18 +469,18 @@ codeunit 148148 "Factur-X CII XML Tests" // [SCENARIO] Factur-X CII XML buyer electronic address URIID has schemeID from customer setting Initialize(); - // [GIVEN] Customer with FR Electronic Address and a SIRET electronic address scheme + // [GIVEN] Customer with FR Electronic Address and a French CTC electronic address scheme ElecAddress := '98765432101234'; SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithElecAddress(ElecAddress)); Customer.Get(SalesInvoiceHeader."Sell-to Customer No."); - Customer.Validate("FR Elec. Address Scheme", Customer."FR Elec. Address Scheme"::"0009"); + Customer.Validate("FR Elec. Address Scheme", Customer."FR Elec. Address Scheme"::"0225"); Customer.Modify(true); // [WHEN] Create CII XML CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); // [THEN] BuyerTradeParty/URIUniversalCommunication/URIID/@schemeID = bare scheme code (not the enum caption) - Assert.AreEqual('0009', + Assert.AreEqual('0225', GetCIIAttributeValue(TempBlob, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID/@schemeID'), StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID/@schemeID')); end; @@ -1016,14 +1009,14 @@ codeunit 148148 "Factur-X CII XML Tests" CompanyInformation.Get(); CompanyInformation.Validate("Registration No.", '123456789'); CompanyInformation.Validate("SIRET No.", '12345678901234'); - if CompanyInformation."VAT Registration No." = '' then - CompanyInformation.Validate("VAT Registration No.", 'FR12345678901'); - if CompanyInformation.Name = '' then - CompanyInformation.Name := 'Test Company FR'; - if CompanyInformation."Country/Region Code" = '' then begin - EnsureCountryRegionExists('FR'); - CompanyInformation.Validate("Country/Region Code", 'FR'); - end; + CompanyInformation.Validate("VAT Registration No.", 'FR12345678901'); + CompanyInformation.Name := 'Test Company FR'; + EnsureCountryRegionExists('FR'); + CompanyInformation.Validate("Country/Region Code", 'FR'); + + CompanyInformation.Address := '123 Test Street'; + CompanyInformation.City := 'Paris'; + CompanyInformation."Post Code" := '75001'; CompanyInformation.Modify(true); SetupGeneralLedger(); @@ -1087,11 +1080,14 @@ codeunit 148148 "Factur-X CII XML Tests" LibraryUtility.UpdateSetupNoSeriesCode( DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Posted Credit Memo Nos.")); GLAccount.Get(LibraryERM.CreateGLAccountWithSalesSetup()); + EnsureVATBusinessPostingGroupExists(GLAccount."VAT Bus. Posting Group"); Customer.Get(CustomerNo); Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); Customer.Validate("VAT Bus. Posting Group", GLAccount."VAT Bus. Posting Group"); Customer.Modify(true); LibrarySales.CreateSalesHeader(SalesHeader, DocType, CustomerNo); + SalesHeader.Validate("Your Reference", 'FR-BUYER-REF'); + SalesHeader.Modify(true); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); SalesLine.Validate("Unit Price", 100); SalesLine.Validate("Unit of Measure Code", GetUnitOfMeasureCode()); @@ -1104,23 +1100,28 @@ codeunit 148148 "Factur-X CII XML Tests" Customer: Record Customer; begin LibrarySales.CreateCustomer(Customer); - if Customer."Country/Region Code" = '' then - Customer.Validate("Country/Region Code", CompanyInformation."Country/Region Code"); - Customer.Validate("VAT Registration No.", GetNextCustomerVATRegistrationNo()); + EnsureVATBusinessPostingGroupExists(Customer."VAT Bus. Posting Group"); + Customer.Address := CopyStr(LibraryUtility.GenerateRandomText(MaxStrLen(Customer.Address)), 1, MaxStrLen(Customer.Address)); + Customer.City := 'Paris'; + Customer."Post Code" := '75001'; + Customer.Validate("Country/Region Code", CompanyInformation."Country/Region Code"); + Customer."VAT Registration No." := LibraryERM.GenerateVATRegistrationNo('FR'); Customer.Validate("FR Electronic Address", FRElecAddress); Customer.Modify(true); exit(Customer."No."); end; - local procedure GetNextCustomerVATRegistrationNo(): Text[20] + local procedure EnsureVATBusinessPostingGroupExists(VATBusPostingGroupCode: Code[20]) var - VATNoBody: Text[11]; - SequenceText: Text; + VATBusinessPostingGroup: Record "VAT Business Posting Group"; begin - CustomerVATNoSequence += 1; - SequenceText := Format(CustomerVATNoSequence); - VATNoBody := CopyStr(PadStr('', 11 - StrLen(SequenceText), '0') + SequenceText, 1, 11); - exit('FR' + VATNoBody); + if (VATBusPostingGroupCode = '') or VATBusinessPostingGroup.Get(VATBusPostingGroupCode) then + exit; + + VATBusinessPostingGroup.Init(); + VATBusinessPostingGroup.Code := VATBusPostingGroupCode; + VATBusinessPostingGroup.Description := VATBusPostingGroupCode; + VATBusinessPostingGroup.Insert(); end; local procedure CreateSalesInvoiceCIIXML(var TempBlob: Codeunit "Temp Blob") @@ -1261,13 +1262,14 @@ codeunit 148148 "Factur-X CII XML Tests" var UnitOfMeasure: Record "Unit of Measure"; begin - UnitOfMeasure.SetRange(Code, 'EA'); - if not UnitOfMeasure.FindFirst() then begin + if not UnitOfMeasure.Get('EA') then begin UnitOfMeasure.Init(); UnitOfMeasure.Code := 'EA'; UnitOfMeasure.Description := 'Each'; UnitOfMeasure.Insert(true); end; + UnitOfMeasure.Validate("International Standard Code", 'EA'); + UnitOfMeasure.Modify(true); exit(UnitOfMeasure.Code); end; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/IdentificationTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/IdentificationTests.Codeunit.al index a228fbcae14..bdd57e089c6 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/IdentificationTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/IdentificationTests.Codeunit.al @@ -4,13 +4,38 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Formats.Test; +using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Formats; +using Microsoft.eServices.EDocument.Processing.Message; +using Microsoft.Finance.Currency; +using Microsoft.Finance.GeneralLedger.Journal; +using Microsoft.Finance.GeneralLedger.Setup; +using Microsoft.Finance.VAT.Ledger; +using Microsoft.Finance.VAT.Setup; using Microsoft.Foundation.Company; +using Microsoft.Inventory.Location; +using Microsoft.Sales.Customer; +using Microsoft.Sales.Document; +using Microsoft.Sales.History; +using Microsoft.Sales.Receivables; +using Microsoft.Sales.Setup; +using System.Utilities; codeunit 148146 "Identification Tests" { Subtype = Test; - Permissions = tabledata "Company Information" = rimd; + Permissions = tabledata "Company Information" = rimd, + tabledata "E-Document" = rimd, + tabledata "E-Document Service" = rimd, + tabledata "E-Document Service Status" = rimd, + tabledata "General Ledger Setup" = rimd, + tabledata "Sales Invoice Header" = rimd, + tabledata "Cust. Ledger Entry" = rimd, + tabledata "Detailed Cust. Ledg. Entry" = rimd, + tabledata "FR E-Invoice Lifecycle" = rimd, + tabledata "FR E-Invoice Lifecycle VAT" = rimd, + tabledata "VAT Entry" = rimd, + tabledata "VAT Posting Setup" = rimd; trigger OnRun() begin @@ -19,7 +44,21 @@ codeunit 148146 "Identification Tests" var Assert: Codeunit Assert; + LibraryERM: Codeunit "Library - ERM"; + LibraryInventory: Codeunit "Library - Inventory"; + LibrarySales: Codeunit "Library - Sales"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + LibraryUtility: Codeunit "Library - Utility"; EDocHelpers: Codeunit "EDoc. Helpers"; + ImmutableLifecycleErr: Label 'The regulatory identity and values of a French electronic invoice lifecycle occurrence cannot be changed.', Locked = true; + DeleteLifecycleErr: Label 'A French electronic invoice lifecycle occurrence cannot be deleted.', Locked = true; + ImmutableLifecycleVATErr: Label 'A French electronic invoice lifecycle VAT breakdown cannot be changed.', Locked = true; + WorkerFailureErr: Label 'Lifecycle worker test failure.', Locked = true; + XmlNodeMissingErr: Label 'The payload must contain XML node %1.', Comment = '%1 = XML path', Locked = true; + XmlNodeValueErr: Label 'The XML node %1 has an unexpected value.', Comment = '%1 = XML path', Locked = true; + XmlAttributeMissingErr: Label 'The XML node %1 must contain attribute %2.', Comment = '%1 = XML path, %2 = attribute name', Locked = true; + XmlAttributeValueErr: Label 'The XML attribute %1 on node %2 has an unexpected value.', Comment = '%1 = attribute name, %2 = XML path', Locked = true; + IsInitialized: Boolean; [Test] procedure CheckSIRENNotEmptyRaisesErrorWhenEmpty() @@ -29,6 +68,7 @@ codeunit 148146 "Identification Tests" begin // [FEATURE] [AI test] // [SCENARIO] CheckSIRENNotEmpty raises error when Registration No. is blank + Initialize(); // [GIVEN] Company Information with blank Registration No. CompanyInformation.Get(); @@ -41,7 +81,6 @@ codeunit 148146 "Identification Tests" asserterror EDocHelpers.CheckSIRENNotEmpty(); Assert.ExpectedError('Registration No. must be specified in Company Information for French e-invoicing.'); - // Cleanup CompanyInformation.Get(); CompanyInformation."Registration No." := CopyStr(OriginalRegistrationNo, 1, MaxStrLen(CompanyInformation."Registration No.")); CompanyInformation.Modify(); @@ -55,6 +94,7 @@ codeunit 148146 "Identification Tests" begin // [FEATURE] [AI test] // [SCENARIO] CheckSIRETNotEmpty raises error when SIRET is blank + Initialize(); // [GIVEN] Company Information with blank SIRET No. CompanyInformation.Get(); @@ -67,7 +107,6 @@ codeunit 148146 "Identification Tests" asserterror EDocHelpers.CheckSIRETNotEmpty(); Assert.ExpectedError('SIRET No. must be specified in Company Information for French e-invoicing.'); - // Cleanup CompanyInformation.Get(); CompanyInformation."SIRET No." := OriginalSIRETNo; CompanyInformation.Modify(); @@ -77,49 +116,1364 @@ codeunit 148146 "Identification Tests" procedure CheckSIRENNotEmptyDoesNotErrorWhenRegistrationNoPresent() var CompanyInformation: Record "Company Information"; - OriginalRegistrationNo: Text[20]; begin // [FEATURE] [AI test] // [SCENARIO] CheckSIRENNotEmpty succeeds when Registration No. is set + Initialize(); // [GIVEN] Company Information with Registration No. set CompanyInformation.Get(); - OriginalRegistrationNo := CompanyInformation."Registration No."; CompanyInformation."Registration No." := '123456789'; CompanyInformation.Modify(); // [WHEN] CheckSIRENNotEmpty is called // [THEN] No error is raised EDocHelpers.CheckSIRENNotEmpty(); - - // Cleanup - CompanyInformation.Get(); - CompanyInformation."Registration No." := CopyStr(OriginalRegistrationNo, 1, MaxStrLen(CompanyInformation."Registration No.")); - CompanyInformation.Modify(); end; [Test] procedure CheckSIRETNotEmptyDoesNotErrorWhenSIRETPresent() var CompanyInformation: Record "Company Information"; - OriginalSIRETNo: Code[14]; begin // [FEATURE] [AI test] // [SCENARIO] CheckSIRETNotEmpty succeeds when SIRET No. is set + Initialize(); // [GIVEN] Company Information with SIRET No. set CompanyInformation.Get(); - OriginalSIRETNo := CompanyInformation."SIRET No."; CompanyInformation."SIRET No." := '12345678901234'; CompanyInformation.Modify(); // [WHEN] CheckSIRETNotEmpty is called // [THEN] No error is raised EDocHelpers.CheckSIRETNotEmpty(); + end; + + [Test] + procedure CaptureCollectedOccurrenceCreatesCapturedLifecycle() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + SourceOccurrenceID: Guid; + begin + // [FEATURE] [AI test] + // [SCENARIO] A payment application is captured as an immutable Collected lifecycle occurrence + Initialize(); + + // [GIVEN] An E-Document "ED" and a source occurrence ID + CreateEDocument(EDocument); + SourceOccurrenceID := CreateGuid(); + + // [WHEN] A Collected lifecycle occurrence is captured for "ED" + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, SourceOccurrenceID, + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + // [THEN] The lifecycle occurrence retains its regulatory values and is Captured + Assert.AreEqual(EDocument."Entry No", FREInvoiceLifecycle."E-Document Entry No.", 'The e-document entry must be retained.'); + Assert.AreEqual(1250, FREInvoiceLifecycle."Reported Amount", 'The reported amount must be retained.'); + Assert.AreEqual('EUR', FREInvoiceLifecycle."Currency Code", 'A foreign currency code must be retained.'); + Assert.AreEqual(FREInvoiceLifecycle."Processing Status"::Captured, FREInvoiceLifecycle."Processing Status", 'A new occurrence must be captured.'); + Assert.IsTrue(FREInvoiceLifecycle."Created At" <> 0DT, 'The creation timestamp must be populated.'); + end; + + [Test] + procedure CaptureCollectedOccurrenceKeepsBlankCurrencyForLCY() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] A local-currency payment occurrence keeps the Business Central blank currency representation + Initialize(); + + // [GIVEN] An E-Document "ED" in a company with LCY configured + CreateEDocument(EDocument); + + // [WHEN] A Collected lifecycle occurrence is captured with blank currency + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, '', WorkDate(), 0, 0, 0, 0); + + // [THEN] The lifecycle currency remains blank + Assert.AreEqual('', FREInvoiceLifecycle."Currency Code", 'Local currency must remain blank on the lifecycle record.'); + end; + + [Test] + procedure CaptureCollectedOccurrenceReplayReturnsExistingLifecycle() + var + EDocument: Record "E-Document"; + FirstLifecycle: Record "FR E-Invoice Lifecycle"; + ReplayedLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + SourceOccurrenceID: Guid; + begin + // [FEATURE] [AI test] + // [SCENARIO] Replaying the same payment event does not create a duplicate occurrence + Initialize(); + + // [GIVEN] A Collected occurrence for E-Document "ED" + CreateEDocument(EDocument); + SourceOccurrenceID := CreateGuid(); + FirstLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, SourceOccurrenceID, + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + // [WHEN] The same source occurrence is captured again + ReplayedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, SourceOccurrenceID, + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + // [THEN] The original lifecycle occurrence is returned + Assert.AreEqual(FirstLifecycle."Entry No.", ReplayedLifecycle."Entry No.", 'An identical replay must return the existing occurrence.'); + end; + + [Test] + procedure CaptureCollectedOccurrenceRejectsConflictingReplay() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + SourceOccurrenceID: Guid; + begin + // [FEATURE] [AI test] + // [SCENARIO] A replay with the same identity but different regulatory values is rejected + Initialize(); + + // [GIVEN] A Collected occurrence for E-Document "ED" + CreateEDocument(EDocument); + SourceOccurrenceID := CreateGuid(); + FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, SourceOccurrenceID, + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + // [WHEN] The same source occurrence is captured with a different amount + asserterror FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, SourceOccurrenceID, + 1200, 'EUR', WorkDate(), 0, 0, 0, 0); + + // [THEN] A conflicting replay error is raised + Assert.ExpectedError('The payment lifecycle occurrence was already captured with different values.'); + end; + + [Test] + procedure CaptureNegativeCollectedLinksExactReversal() + var + EDocument: Record "E-Document"; + CollectedLifecycle: Record "FR E-Invoice Lifecycle"; + NegativeCollectedLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Unapplication creates a separate Negative Collected occurrence linked to Collected + Initialize(); + + // [GIVEN] A Collected occurrence for E-Document "ED" + CreateEDocument(EDocument); + CollectedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + // [WHEN] A matching Negative Collected occurrence is captured + NegativeCollectedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::"Negative Collected", CreateGuid(), + -1250, 'EUR', WorkDate(), 0, 0, 0, CollectedLifecycle."Entry No."); + + // [THEN] The reversal is linked and exactly negates the original amount + Assert.AreEqual(CollectedLifecycle."Entry No.", NegativeCollectedLifecycle."Original Occurrence Entry No.", 'The reversal must reference the Collected occurrence.'); + Assert.AreEqual(-CollectedLifecycle."Reported Amount", NegativeCollectedLifecycle."Reported Amount", 'The reversal must negate the original amount.'); + end; + + [Test] + procedure CaptureNegativeCollectedRejectsDifferentAmount() + var + EDocument: Record "E-Document"; + CollectedLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Negative Collected cannot reverse a different amount than its original occurrence + Initialize(); + + // [GIVEN] A Collected occurrence for E-Document "ED" + CreateEDocument(EDocument); + CollectedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + // [WHEN] A Negative Collected occurrence is captured with a different amount + asserterror FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::"Negative Collected", CreateGuid(), + -1200, 'EUR', WorkDate(), 0, 0, 0, CollectedLifecycle."Entry No."); + + // [THEN] An exact reversal error is raised + Assert.ExpectedError('A Negative Collected occurrence must exactly reverse the reported amount of the original Collected occurrence.'); + end; + + [Test] + procedure CaptureNegativeCollectedRejectsMissingOriginalOccurrence() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Negative Collected cannot reference an original occurrence that does not exist + Initialize(); + + // [GIVEN] An E-Document without a Collected occurrence + CreateEDocument(EDocument); + + // [WHEN] A Negative Collected occurrence references a nonexistent original occurrence + asserterror FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::"Negative Collected", CreateGuid(), + -1250, 'EUR', WorkDate(), 0, 0, 0, 999999); + + // [THEN] The orphan reversal is rejected + Assert.ExpectedError('The original Collected occurrence does not exist.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoCommit)] + procedure CapturedOccurrenceRejectsRegulatoryValueChanges() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Captured regulatory values cannot be changed in place + Initialize(); + + // [GIVEN] A committed Collected occurrence for E-Document "ED" + CreateEDocument(EDocument); + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + Commit(); + + // [WHEN] The reported amount is changed + FREInvoiceLifecycle."Reported Amount" := 1200; + asserterror FREInvoiceLifecycle.Modify(true); + + // [THEN] The change is rejected + Assert.ExpectedError('The regulatory identity and values of a French electronic invoice lifecycle occurrence cannot be changed.'); + + // [WHEN] The sender platform ID is changed + FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No."); + FREInvoiceLifecycle."Sender Platform ID" := 'CHANGED'; + asserterror FREInvoiceLifecycle.Modify(true); + + // [THEN] The change is rejected + Assert.ExpectedError('The regulatory identity and values of a French electronic invoice lifecycle occurrence cannot be changed.'); + end; + + [Test] + procedure SenderPlatformSchemeDefaultsTo0238() + var + EDocumentService: Record "E-Document Service"; + begin + // [FEATURE] [AI test] + // [SCENARIO] French lifecycle setup defaults the sender platform identifier scheme to 0238 + Initialize(); + + // [WHEN] A new E-Document Service is initialized + EDocumentService.Init(); + + // [THEN] The sender platform scheme defaults to 0238 + Assert.AreEqual('0238', EDocumentService."FR Sender Platform Scheme", 'The sender platform scheme must default to 0238.'); + end; + + [Test] + procedure DetailedApplicationCapturesCollectedForFREInvoice() + var + EDocument: Record "E-Document"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Applying a payment to a French electronic invoice captures a Collected occurrence + Initialize(); + + // [GIVEN] A payment application for French E-Invoice "ED" + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + + // [WHEN] The detailed ledger application is processed + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + // [THEN] A Collected occurrence retains the source and invoice values + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceLifecycle.FindFirst(); + Assert.AreEqual(FREInvoiceLifecycle."Lifecycle Status"::Collected, FREInvoiceLifecycle."Lifecycle Status", 'A payment application must create a Collected occurrence.'); + Assert.AreEqual(-DetailedCustLedgEntry.Amount, FREInvoiceLifecycle."Reported Amount", 'The reported amount must equal the negated detailed ledger entry amount.'); + Assert.IsTrue(FREInvoiceLifecycle."Reported Amount" > 0, 'The collected amount must be positive.'); + Assert.AreEqual(DetailedCustLedgEntry."Entry No.", FREInvoiceLifecycle."Detailed Ledger Entry No.", 'The source detail entry must be retained.'); + Assert.AreEqual(DetailedCustLedgEntry.SystemId, FREInvoiceLifecycle."Source Occurrence ID", 'The detail entry system ID must identify the occurrence.'); + Assert.AreEqual(EDocument."Document Date", FREInvoiceLifecycle."Invoice Issue Date", 'The invoice issue date must be frozen at capture.'); + Assert.AreEqual(EDocument."Clearance Date", FREInvoiceLifecycle."Invoice Receipt At", 'The PPF receipt timestamp must be frozen at capture.'); + Assert.AreEqual('PLATFORM-ID', FREInvoiceLifecycle."Sender Platform ID", 'The sender platform identifier must be frozen at capture.'); + Assert.AreEqual('123456789', FREInvoiceLifecycle."Invoice Issuer ID", 'The seller SIREN must be frozen at capture.'); + end; + + [Test] + procedure DetailedApplicationCreatesPPFLifecycleMessage() + var + EDocument: Record "E-Document"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + TempBlob: Codeunit "Temp Blob"; + InStream: InStream; + XmlDoc: XmlDocument; + XmlNode: XmlNode; + begin + // [FEATURE] [AI test] + // [SCENARIO] A real French invoice occurrence creates the PPF einvoicingF2 lifecycle envelope + Initialize(); + + // [GIVEN] A captured occurrence for French E-Invoice "ED" + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceLifecycle.FindFirst(); + + // [WHEN] The lifecycle message is created + FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); + + // [THEN] The payload contains the PPF einvoicingF2 envelope values + EDocumentMessageAPI.GetMessageBlob(FREInvoiceLifecycle."E-Document Message Entry No.", TempBlob); + TempBlob.CreateInStream(InStream); + XmlDocument.ReadFrom(InStream, XmlDoc); + AssertXmlValue(XmlDoc, '//*[local-name()="GuidelineSpecifiedDocumentContextParameter"]/*[local-name()="ID"]', 'urn.cpro.gouv.fr:1p0:CDV:einvoicingF2'); + AssertXmlValue(XmlDoc, '//*[local-name()="SenderTradeParty"]/*[local-name()="GlobalID"]', 'PLATFORM-ID'); + AssertXmlAttribute(XmlDoc, '//*[local-name()="SenderTradeParty"]/*[local-name()="GlobalID"]', 'schemeID', '0238'); + AssertXmlValue(XmlDoc, '//*[local-name()="SenderTradeParty"]/*[local-name()="RoleCode"]', 'WK'); + AssertXmlValue(XmlDoc, '//*[local-name()="ExchangedDocument"]/*[local-name()="IssuerTradeParty"]/*[local-name()="GlobalID"]', '123456789'); + AssertXmlAttribute(XmlDoc, '//*[local-name()="ExchangedDocument"]/*[local-name()="IssuerTradeParty"]/*[local-name()="GlobalID"]', 'schemeID', '0002'); + AssertXmlValue(XmlDoc, '//*[local-name()="ExchangedDocument"]/*[local-name()="IssuerTradeParty"]/*[local-name()="RoleCode"]', 'SE'); + AssertXmlValue(XmlDoc, '//*[local-name()="RecipientTradeParty"]/*[local-name()="GlobalID"]', '9998'); + AssertXmlAttribute(XmlDoc, '//*[local-name()="RecipientTradeParty"]/*[local-name()="GlobalID"]', 'schemeID', '0238'); + AssertXmlValue(XmlDoc, '//*[local-name()="RecipientTradeParty"]/*[local-name()="RoleCode"]', 'DFH'); + AssertXmlValue(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="ReferenceTypeCode"]', 'urn.cpro.gouv.fr:1p0:CDV:einvoicingF2'); + AssertXmlValue( + XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="ReceiptDateTime"]/*[local-name()="DateTimeString"]', + Format(EDocument."Clearance Date", 0, '')); + AssertXmlAttribute(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="ReceiptDateTime"]/*[local-name()="DateTimeString"]', 'format', '204'); + AssertXmlValue( + XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="FormattedIssueDateTime"]/*[local-name()="DateTimeString"]', + Format(EDocument."Document Date", 0, '')); + AssertXmlAttribute(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="FormattedIssueDateTime"]/*[local-name()="DateTimeString"]', 'format', '102'); + AssertXmlValue(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="IssuerTradeParty"]/*[local-name()="GlobalID"]', '123456789'); + AssertXmlAttribute(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="IssuerTradeParty"]/*[local-name()="GlobalID"]', 'schemeID', '0002'); + Assert.IsFalse(XmlDoc.SelectSingleNode('//*[local-name()="BusinessProcessSpecifiedDocumentContextParameter"]', XmlNode), 'The PPF profile must not contain the generic REGULATED business process context.'); + end; + + [Test] + procedure DetailedApplicationAggregatesVATEntriesWithSameRate() + var + EDocument: Record "E-Document"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Multiple invoice VAT entries with the same rate create one lifecycle amount + Initialize(); + + // [GIVEN] A French E-Invoice "ED" with multiple VAT entries at 20 percent + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + SetInvoiceVATRate(EDocument."Document No.", 20, false); + + // [WHEN] The detailed ledger application is processed + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + // [THEN] One aggregated VAT breakdown line is captured + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceLifecycle.FindFirst(); + FREInvoiceLifecycleVAT.SetRange("Lifecycle Entry No.", FREInvoiceLifecycle."Entry No."); + Assert.RecordCount(FREInvoiceLifecycleVAT, 1); + FREInvoiceLifecycleVAT.FindFirst(); + Assert.AreEqual(20, FREInvoiceLifecycleVAT."VAT %", 'The aggregated lifecycle amount must retain the common VAT rate.'); + Assert.AreEqual(1000, FREInvoiceLifecycleVAT."Reported Amount", 'The aggregated lifecycle amount must equal the collected amount.'); + end; + + [Test] + procedure DetailedApplicationSupportsZeroRatedVAT() + var + EDocument: Record "E-Document"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] A zero-rated invoice retains VAT rate zero in the lifecycle breakdown + Initialize(); + + // [GIVEN] A French E-Invoice "ED" with zero-rated VAT + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + SetInvoiceVATRate(EDocument."Document No.", 0, true); + + // [WHEN] The detailed ledger application is processed + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + // [THEN] One zero-rated VAT breakdown line is captured + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceLifecycle.FindFirst(); + FREInvoiceLifecycleVAT.SetRange("Lifecycle Entry No.", FREInvoiceLifecycle."Entry No."); + Assert.RecordCount(FREInvoiceLifecycleVAT, 1); + FREInvoiceLifecycleVAT.FindFirst(); + Assert.AreEqual(0, FREInvoiceLifecycleVAT."VAT %", 'The lifecycle breakdown must retain the zero VAT rate.'); + Assert.AreEqual(1000, FREInvoiceLifecycleVAT."Reported Amount", 'The zero-rated lifecycle amount must equal the collected amount.'); + end; + + [Test] + procedure DetailedApplicationUsesForeignCurrencyRoundingPrecision() + var + Currency: Record Currency; + EDocument: Record "E-Document"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + CurrencyCode: Code[10]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Foreign-currency VAT allocation uses that currency's rounding precision and preserves the remainder + Initialize(); + + // [GIVEN] A French E-Invoice "ED" paid in a currency with 0.05 rounding precision + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + CurrencyCode := CopyStr(CreateGuid(), 1, MaxStrLen(CurrencyCode)); + Currency.Code := CurrencyCode; + Currency."Amount Rounding Precision" := 0.05; + Currency.Insert(); + DetailedCustLedgEntry.Amount := -1000.03; + DetailedCustLedgEntry."Currency Code" := CurrencyCode; + DetailedCustLedgEntry.Modify(); + SetInvoiceVATCurrency(EDocument."Document No.", CurrencyCode); + + // [WHEN] The detailed ledger application is processed + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + // [THEN] VAT is rounded by currency and the final line retains the remainder + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceLifecycle.FindFirst(); + FREInvoiceLifecycleVAT.SetRange("Lifecycle Entry No.", FREInvoiceLifecycle."Entry No."); + FREInvoiceLifecycleVAT.SetRange("VAT %", 20); + FREInvoiceLifecycleVAT.FindFirst(); + Assert.AreEqual(480, FREInvoiceLifecycleVAT."Reported Amount", 'The first VAT amount must use the foreign currency rounding precision.'); + FREInvoiceLifecycleVAT.SetRange("VAT %", 10); + FREInvoiceLifecycleVAT.FindFirst(); + Assert.AreEqual(520.03, FREInvoiceLifecycleVAT."Reported Amount", 'The final VAT amount must retain the exact allocation remainder.'); + end; + + [Test] + procedure DetailedApplicationRejectsMissingVATBreakdown() + var + EDocument: Record "E-Document"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + VATEntry: Record "VAT Entry"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] A French lifecycle occurrence is not retained when its posted invoice has no VAT breakdown + Initialize(); + + // [GIVEN] A French E-Invoice "ED" whose VAT entries were deleted + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + VATEntry.SetRange("Document No.", EDocument."Document No."); + VATEntry.DeleteAll(); + + // [WHEN] The detailed ledger application is processed + asserterror FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + // [THEN] A missing VAT breakdown error is raised and no occurrence remains + Assert.ExpectedError('A VAT breakdown could not be determined for posted sales invoice'); + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + Assert.RecordIsEmpty(FREInvoiceLifecycle); + end; + + [Test] + procedure DetailedApplicationWithoutSenderPlatformCreatesCDVLifecycleMessage() + var + EDocument: Record "E-Document"; + EDocumentService: Record "E-Document Service"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + TempBlob: Codeunit "Temp Blob"; + InStream: InStream; + XmlDoc: XmlDocument; + begin + // [FEATURE] [AI test] + // [SCENARIO] A production payment flow without a configured sender platform creates the general CDV profile + Initialize(); + + // [GIVEN] A captured French E-Invoice occurrence without a sender platform ID + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + EDocumentService.Get(EDocument.Service); + Clear(EDocumentService."FR Sender Platform ID"); + EDocumentService.Modify(); + + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceLifecycle.FindFirst(); + Assert.AreEqual('', FREInvoiceLifecycle."Sender Platform ID", 'The general CDV profile must not retain PPF sender information.'); + + // [WHEN] The lifecycle message is created + FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); + + // [THEN] The payload uses the general CDV invoice profile + EDocumentMessageAPI.GetMessageBlob(FREInvoiceLifecycle."E-Document Message Entry No.", TempBlob); + TempBlob.CreateInStream(InStream); + XmlDocument.ReadFrom(InStream, XmlDoc); + AssertXmlValue(XmlDoc, '//*[local-name()="GuidelineSpecifiedDocumentContextParameter"]/*[local-name()="ID"]', 'urn.cpro.gouv.fr:1p0:CDV:invoice'); + end; + + [Test] + procedure DetailedApplicationRejectsMissingVATPostingSetup() + var + EDocument: Record "E-Document"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + VATEntry: Record "VAT Entry"; + VATPostingSetup: Record "VAT Posting Setup"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] A lifecycle occurrence is not retained when a posted VAT entry has no matching setup + Initialize(); + + // [GIVEN] A French E-Invoice "ED" whose VAT Posting Setup was deleted + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + VATEntry.SetRange("Document No.", EDocument."Document No."); + VATEntry.FindFirst(); + VATPostingSetup.Get(VATEntry."VAT Bus. Posting Group", VATEntry."VAT Prod. Posting Group"); + VATPostingSetup.Delete(); + + // [WHEN] The detailed ledger application is processed + asserterror FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + // [THEN] A missing setup error is raised and no occurrence remains + Assert.ExpectedError('VAT Posting Setup does not exist'); + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + Assert.RecordIsEmpty(FREInvoiceLifecycle); + end; + + [Test] + [TransactionModel(TransactionModel::AutoCommit)] + procedure LifecycleOccurrenceAndVATBreakdownRejectDeletion() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Captured lifecycle occurrences and their VAT rows cannot be deleted + Initialize(); + + // [GIVEN] A committed lifecycle occurrence with a VAT breakdown + CreateEDocument(EDocument); + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + CreateLifecycleVATBreakdown(FREInvoiceLifecycle, 20, 1250); + Commit(); + + // [WHEN] The VAT breakdown line is deleted + FREInvoiceLifecycleVAT.Get(FREInvoiceLifecycle."Entry No.", 10000); + asserterror FREInvoiceLifecycleVAT.Delete(true); + + // [THEN] The deletion is rejected + Assert.ExpectedError(ImmutableLifecycleVATErr); + + // [WHEN] The lifecycle occurrence is renamed + asserterror FREInvoiceLifecycle.Rename(FREInvoiceLifecycle."Entry No." + 1); + + // [THEN] The rename is rejected + Assert.ExpectedError(ImmutableLifecycleErr); + + // [WHEN] The lifecycle occurrence is deleted + asserterror FREInvoiceLifecycle.Delete(true); + + // [THEN] The deletion is rejected + Assert.ExpectedError(DeleteLifecycleErr); + end; + + [Test] + procedure DetailedApplicationIgnoresNonFREInvoice() + var + EDocument: Record "E-Document"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Applying a payment to an electronic invoice in a non-French format creates no French lifecycle occurrence + Initialize(); + + // [GIVEN] A payment application for non-French E-Invoice "ED" + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"PEPPOL BIS 3.0"); + + // [WHEN] The detailed ledger application is processed + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); - // Cleanup + // [THEN] No French lifecycle occurrence is created + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + Assert.RecordIsEmpty(FREInvoiceLifecycle); + end; + + [Test] + procedure DetailedApplicationReplayDoesNotRequeueCreatedMessage() + var + EDocument: Record "E-Document"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + MessageEntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO] Replaying an application whose lifecycle message exists does not requeue the occurrence + Initialize(); + + // [GIVEN] A processed application with an existing lifecycle message + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceLifecycle.FindFirst(); + FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); + MessageEntryNo := FREInvoiceLifecycle."E-Document Message Entry No."; + + // [WHEN] The detailed ledger application is replayed + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + // [THEN] The occurrence remains Message Created and retains its message link + FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No."); + Assert.AreEqual(FREInvoiceLifecycle."Processing Status"::"Message Created", FREInvoiceLifecycle."Processing Status", 'A replay must not requeue an occurrence whose message exists.'); + Assert.AreEqual(MessageEntryNo, FREInvoiceLifecycle."E-Document Message Entry No.", 'A replay must retain the existing message link.'); + end; + + [Test] + procedure DetailedApplicationReplayDoesNotDuplicateQueuedOccurrence() + var + EDocument: Record "E-Document"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Replaying an application while message creation is queued does not duplicate the occurrence + Initialize(); + + // [GIVEN] A processed application with a queued lifecycle occurrence + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + // [WHEN] The detailed ledger application is replayed + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + // [THEN] One queued occurrence remains + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + Assert.RecordCount(FREInvoiceLifecycle, 1); + FREInvoiceLifecycle.FindFirst(); + Assert.AreEqual(FREInvoiceLifecycle."Processing Status"::Queued, FREInvoiceLifecycle."Processing Status", 'A replay must retain the queued status.'); + end; + + [Test] + procedure DetailedApplicationCapturesOccurrenceForEachFREDocument() + var + EDocument: Record "E-Document"; + AdditionalEDocument: Record "E-Document"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Applying a payment creates an occurrence for every eligible E-Document of the invoice + Initialize(); + + // [GIVEN] A payment application with two eligible French E-Documents + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + CreateAdditionalEDocument(AdditionalEDocument, EDocument); + + // [WHEN] The detailed ledger application is processed + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + // [THEN] One occurrence is captured for each E-Document + FREInvoiceLifecycle.SetRange("Source Occurrence ID", DetailedCustLedgEntry.SystemId); + Assert.RecordCount(FREInvoiceLifecycle, 2); + end; + + [Test] + procedure DetailedUnapplicationCapturesLinkedNegativeCollected() + var + EDocument: Record "E-Document"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + CollectedLifecycle: Record "FR E-Invoice Lifecycle"; + NegativeCollectedLifecycle: Record "FR E-Invoice Lifecycle"; + CollectedLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; + NegativeCollectedLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Unapplying a captured payment creates an exact linked Negative Collected occurrence + Initialize(); + + // [GIVEN] A captured payment application with a corresponding unapplication detail + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Peppol BIS 3.0 FR"); + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + CollectedLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + CollectedLifecycle.FindFirst(); + CreateUnapplicationDetail(NewDetailedCustLedgEntry, DetailedCustLedgEntry); + + // [WHEN] The detailed ledger unapplication is processed + FREInvoiceLifecycleMgt.ProcessDetailedLedgerUnapplication(DetailedCustLedgEntry, NewDetailedCustLedgEntry); + + // [THEN] A linked Negative Collected occurrence exactly reverses the amount and VAT breakdown + NegativeCollectedLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + NegativeCollectedLifecycle.SetRange("Lifecycle Status", NegativeCollectedLifecycle."Lifecycle Status"::"Negative Collected"); + NegativeCollectedLifecycle.FindFirst(); + Assert.AreEqual(-CollectedLifecycle."Reported Amount", NegativeCollectedLifecycle."Reported Amount", 'The unapplication must exactly negate the collected amount.'); + Assert.AreEqual(CollectedLifecycle."Entry No.", NegativeCollectedLifecycle."Original Occurrence Entry No.", 'The unapplication must reference the Collected occurrence.'); + Assert.AreEqual(NewDetailedCustLedgEntry."Entry No.", NegativeCollectedLifecycle."Detailed Ledger Entry No.", 'The unapplication detail entry must be retained.'); + CollectedLifecycleVAT.SetRange("Lifecycle Entry No.", CollectedLifecycle."Entry No."); + Assert.RecordCount(CollectedLifecycleVAT, 2); + CollectedLifecycleVAT.SetRange("VAT %", 20); + CollectedLifecycleVAT.FindFirst(); + Assert.AreEqual(480, CollectedLifecycleVAT."Reported Amount", 'The payment amount must be allocated proportionally to the 20% VAT gross amount.'); + CollectedLifecycleVAT.SetRange("VAT %", 10); + CollectedLifecycleVAT.FindFirst(); + Assert.AreEqual(520, CollectedLifecycleVAT."Reported Amount", 'The payment remainder must be allocated to the 10% VAT gross amount.'); + CollectedLifecycleVAT.SetRange("VAT %"); + CollectedLifecycleVAT.FindSet(); + repeat + NegativeCollectedLifecycleVAT.Get(NegativeCollectedLifecycle."Entry No.", CollectedLifecycleVAT."Line No."); + Assert.AreEqual(CollectedLifecycleVAT."VAT %", NegativeCollectedLifecycleVAT."VAT %", 'The reversal must retain each VAT rate.'); + Assert.AreEqual(-CollectedLifecycleVAT."Reported Amount", NegativeCollectedLifecycleVAT."Reported Amount", 'The reversal must exactly negate each VAT-rate amount.'); + until CollectedLifecycleVAT.Next() = 0; + end; + + [Test] + procedure CreateLifecycleMessageStoresPayloadThroughEDocumentMessageMgt() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + TempBlob: Codeunit "Temp Blob"; + InStream: InStream; + XmlDoc: XmlDocument; + ProfileNode: XmlNode; + StatusNode: XmlNode; + VATPercentNode: XmlNode; + begin + // [FEATURE] [AI test] + // [SCENARIO] A captured occurrence creates and links a PR 8698 E-Document Message payload + Initialize(); + + // [GIVEN] A Collected occurrence with a VAT breakdown + CreateEDocument(EDocument); + CreatePaymentCustLedgerEntry(PaymentCustLedgerEntry, 'EUR'); + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, PaymentCustLedgerEntry."Entry No.", 0, 0); + CreateLifecycleVATBreakdown(FREInvoiceLifecycle, 20, 1250); + + // [WHEN] The lifecycle message is created + FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); + + // [THEN] The linked E-Document Message contains the expected lifecycle payload + Assert.IsTrue(FREInvoiceLifecycle."E-Document Message Entry No." <> 0, 'The lifecycle occurrence must link to the created E-Document Message.'); + Assert.AreEqual(FREInvoiceLifecycle."Processing Status"::"Message Created", FREInvoiceLifecycle."Processing Status", 'The occurrence must record successful message creation.'); + EDocumentMessageAPI.GetMessageBlob(FREInvoiceLifecycle."E-Document Message Entry No.", TempBlob); + TempBlob.CreateInStream(InStream); + XmlDocument.ReadFrom(InStream, XmlDoc); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ProcessConditionCode"]', StatusNode), 'The payload must contain the lifecycle status.'); + Assert.AreEqual('212', StatusNode.AsXmlElement().InnerText(), 'The payload must map Collected to the French Encaissée status code.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="SpecifiedDocumentCharacteristic"]/*[local-name()="TypeCode"]', StatusNode), 'The payload must qualify the reported amount.'); + Assert.AreEqual('MEN', StatusNode.AsXmlElement().InnerText(), 'The payload must qualify the amount as Montant encaissé.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="GuidelineSpecifiedDocumentContextParameter"]/*[local-name()="ID"]', ProfileNode), 'The payload must identify the French invoice lifecycle profile.'); + Assert.AreEqual('urn.cpro.gouv.fr:1p0:CDV:invoice', ProfileNode.AsXmlElement().InnerText(), 'The payload must use the general French invoice lifecycle profile.'); + AssertXmlValue(XmlDoc, '//*[local-name()="BusinessProcessSpecifiedDocumentContextParameter"]/*[local-name()="ID"]', 'REGULATED'); + Assert.IsFalse(XmlDoc.SelectSingleNode('//*[local-name()="SenderTradeParty"]', ProfileNode), 'The general lifecycle profile must not contain PPF sender information.'); + Assert.IsFalse(XmlDoc.SelectSingleNode('//*[local-name()="RecipientTradeParty"]', ProfileNode), 'The general lifecycle profile must not contain the PPF recipient.'); + AssertXmlAttribute(XmlDoc, '//*[local-name()="ValueAmount"]', 'currencyID', 'EUR'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ValuePercent"]', VATPercentNode), 'The payload must contain the VAT percentage.'); + Assert.AreEqual('20', VATPercentNode.AsXmlElement().InnerText(), 'The payload must retain the frozen VAT percentage.'); + end; + + [Test] + procedure CreateLifecycleMessageIsIdempotent() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + MessageEntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO] Retrying message creation does not create or link a second message + Initialize(); + + // [GIVEN] A Collected occurrence with an existing lifecycle message + CreateEDocument(EDocument); + CreatePaymentCustLedgerEntry(PaymentCustLedgerEntry, 'EUR'); + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, PaymentCustLedgerEntry."Entry No.", 0, 0); + CreateLifecycleVATBreakdown(FREInvoiceLifecycle, 20, 1250); + FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); + MessageEntryNo := FREInvoiceLifecycle."E-Document Message Entry No."; + + // [WHEN] Lifecycle message creation is retried + FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); + + // [THEN] The original message link is retained + Assert.AreEqual(MessageEntryNo, FREInvoiceLifecycle."E-Document Message Entry No.", 'A retry must retain the existing message link.'); + end; + + [Test] + procedure CreateNegativeCollectedMessageUses212AndNegativeAmount() + var + EDocument: Record "E-Document"; + CollectedLifecycle: Record "FR E-Invoice Lifecycle"; + NegativeCollectedLifecycle: Record "FR E-Invoice Lifecycle"; + PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + TempBlob: Codeunit "Temp Blob"; + InStream: InStream; + XmlDoc: XmlDocument; + AmountNode: XmlNode; + StatusNode: XmlNode; + begin + // [FEATURE] [AI test] + // [SCENARIO] A Negative Collected occurrence uses status 212 with a negative collected amount + Initialize(); + + // [GIVEN] A Negative Collected occurrence with a VAT breakdown + CreateEDocument(EDocument); + CreatePaymentCustLedgerEntry(PaymentCustLedgerEntry, 'EUR'); + CollectedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, PaymentCustLedgerEntry."Entry No.", 0, 0); + CreateLifecycleVATBreakdown(CollectedLifecycle, 20, 1250); + NegativeCollectedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::"Negative Collected", CreateGuid(), + -1250, 'EUR', WorkDate() + 1, 0, PaymentCustLedgerEntry."Entry No.", 0, CollectedLifecycle."Entry No."); + CreateLifecycleVATBreakdown(NegativeCollectedLifecycle, 20, -1250); + + // [WHEN] The Negative Collected lifecycle message is created + FREInvoiceLifecycleMgt.CreateLifecycleMessage(NegativeCollectedLifecycle); + + // [THEN] The payload uses status 212 and reports a negative amount + EDocumentMessageAPI.GetMessageBlob(NegativeCollectedLifecycle."E-Document Message Entry No.", TempBlob); + TempBlob.CreateInStream(InStream); + XmlDocument.ReadFrom(InStream, XmlDoc); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ProcessConditionCode"]', StatusNode), 'The payload must contain the lifecycle status.'); + Assert.AreEqual('212', StatusNode.AsXmlElement().InnerText(), 'An unapplication must retain the Encaissée status code.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ValueAmount"]', AmountNode), 'The payload must contain the collected amount.'); + Assert.AreEqual( + Format(NegativeCollectedLifecycle."Reported Amount", 0, ''), + AmountNode.AsXmlElement().InnerText(), 'An unapplication must report a negative collected amount.'); + end; + + [Test] + procedure RetryFailedLifecycleMessageQueuesOccurrence() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Retrying failed message creation queues the occurrence and clears its error + Initialize(); + + // [GIVEN] A failed lifecycle occurrence with a stored error + CreateEDocument(EDocument); + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + FREInvoiceLifecycle."Processing Status" := FREInvoiceLifecycle."Processing Status"::Failed; + FREInvoiceLifecycle."Last Error" := 'Message creation failed.'; + FREInvoiceLifecycle.Modify(); + + // [WHEN] The lifecycle message is retried + FREInvoiceLifecycleMgt.RetryLifecycleMessage(FREInvoiceLifecycle); + + // [THEN] The occurrence is queued and its previous error is cleared + Assert.AreEqual(FREInvoiceLifecycle."Processing Status"::Queued, FREInvoiceLifecycle."Processing Status", 'A retry must queue the occurrence.'); + Assert.AreEqual('', FREInvoiceLifecycle."Last Error", 'A retry must clear the previous error.'); + end; + + [Test] + procedure LifecycleWorkerCreatesQueuedMessage() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + FREInvoiceLifecycleWorker: Codeunit "FR E-Invoice Lifecycle Worker"; + begin + // [FEATURE] [AI test] + // [SCENARIO] The background worker creates a message for a queued lifecycle occurrence + Initialize(); + + // [GIVEN] A queued lifecycle occurrence with a VAT breakdown + CreateEDocument(EDocument); + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + CreateLifecycleVATBreakdown(FREInvoiceLifecycle, 20, 1250); + FREInvoiceLifecycle."Processing Status" := FREInvoiceLifecycle."Processing Status"::Queued; + FREInvoiceLifecycle.Modify(); + + // [WHEN] The lifecycle worker runs + FREInvoiceLifecycleWorker.Run(FREInvoiceLifecycle); + + // [THEN] The occurrence is linked to a created E-Document Message + FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No."); + Assert.AreEqual(FREInvoiceLifecycle."Processing Status"::"Message Created", FREInvoiceLifecycle."Processing Status", 'The worker must create the queued lifecycle message.'); + Assert.IsTrue(FREInvoiceLifecycle."E-Document Message Entry No." <> 0, 'The worker must link the created E-Document message.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoCommit)] + procedure LifecycleErrorHandlerStoresTaskError() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleError: Codeunit "FR E-Invoice Lifecycle Error"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] The background error handler marks a queued occurrence failed and stores the task error + Initialize(); + + // [GIVEN] A committed queued lifecycle occurrence and a task error + CreateEDocument(EDocument); + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + FREInvoiceLifecycle."Processing Status" := FREInvoiceLifecycle."Processing Status"::Queued; + FREInvoiceLifecycle.Modify(); + Commit(); + asserterror Error(WorkerFailureErr); + Assert.ExpectedError(WorkerFailureErr); + + // [WHEN] The lifecycle error handler runs + FREInvoiceLifecycleError.Run(FREInvoiceLifecycle); + + // [THEN] The occurrence is Failed and retains the task error + FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No."); + Assert.AreEqual(FREInvoiceLifecycle."Processing Status"::Failed, FREInvoiceLifecycle."Processing Status", 'The error handler must mark the queued lifecycle occurrence as failed.'); + Assert.ExpectedMessage(WorkerFailureErr, FREInvoiceLifecycle."Last Error"); + end; + + [Test] + [TransactionModel(TransactionModel::AutoCommit)] + procedure PostedPaymentApplicationCreatesCollectedLifecycle() + var + Customer: Record Customer; + GeneralPostingSetup: Record "General Posting Setup"; + Location: Record Location; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + CustLedgerEntry: Record "Cust. Ledger Entry"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; + VATEntry: Record "VAT Entry"; + GenJournalLine: Record "Gen. Journal Line"; + GenJournalBatch: Record "Gen. Journal Batch"; + PostedDocNo: Code[20]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Posting a payment applied to a Factur-X FR sales invoice creates a Collected lifecycle occurrence + Initialize(); + + // [GIVEN] Clean customer ledger and VAT entries + DetailedCustLedgEntry.DeleteAll(); + CustLedgerEntry.DeleteAll(); + VATEntry.DeleteAll(); + + // [GIVEN] A posted sales invoice "SI" with an outgoing Factur-X FR E-Document + LibrarySales.CreateSalesInvoice(SalesHeader); + SalesLine.SetRange("Document Type", SalesHeader."Document Type"); + SalesLine.SetRange("Document No.", SalesHeader."No."); + SalesLine.FindFirst(); + Location.Code := SalesLine."Location Code"; + LibraryInventory.UpdateInventoryPostingSetup(Location); + if not GeneralPostingSetup.Get(SalesLine."Gen. Bus. Posting Group", SalesLine."Gen. Prod. Posting Group") then + LibraryERM.CreateGeneralPostingSetup(GeneralPostingSetup, SalesLine."Gen. Bus. Posting Group", SalesLine."Gen. Prod. Posting Group"); + GeneralPostingSetup.Validate("Sales Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("Purch. Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("COGS Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("Inventory Adjmt. Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Modify(true); + Customer.Get(SalesHeader."Sell-to Customer No."); + Customer."VAT Registration No." := LibraryERM.GenerateVATRegistrationNo('FR'); + Customer.Modify(true); + SalesHeader.Validate("Your Reference", 'FR-BUYER-REF'); + SalesHeader.Validate("Bill-to Address", '123 Rue de Paris'); + SalesHeader.Validate("Bill-to City", 'Paris'); + SalesHeader.Validate("Bill-to Post Code", '75001'); + SalesHeader.Validate("Ship-to Address", SalesHeader."Bill-to Address"); + SalesHeader.Validate("Ship-to City", SalesHeader."Bill-to City"); + SalesHeader.Validate("Ship-to Post Code", SalesHeader."Bill-to Post Code"); + SalesHeader.Modify(true); + PostedDocNo := LibrarySales.PostSalesDocument(SalesHeader, true, true); + SalesInvoiceHeader.Get(PostedDocNo); + CreateFRFacturXEDocument(EDocument, SalesInvoiceHeader); + + // [GIVEN] The remaining amount on the invoice customer ledger entry for "SI" + CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Invoice); + CustLedgerEntry.SetRange("Document No.", PostedDocNo); + CustLedgerEntry.FindFirst(); + CustLedgerEntry.CalcFields("Remaining Amount"); + + // [WHEN] A customer payment is posted and applied to "SI" + LibraryERM.SelectGenJnlBatch(GenJournalBatch); + LibraryERM.ClearGenJournalLines(GenJournalBatch); + LibraryERM.CreateGeneralJnlLine( + GenJournalLine, GenJournalBatch."Journal Template Name", GenJournalBatch.Name, + GenJournalLine."Document Type"::Payment, GenJournalLine."Account Type"::Customer, + SalesHeader."Sell-to Customer No.", -CustLedgerEntry."Remaining Amount"); + GenJournalLine.Validate("Applies-to Doc. Type", GenJournalLine."Applies-to Doc. Type"::Invoice); + GenJournalLine.Validate("Applies-to Doc. No.", PostedDocNo); + GenJournalLine.Modify(true); + LibraryERM.PostGeneralJnlLine(GenJournalLine); + + // [THEN] A Collected lifecycle occurrence is created from the actual posted Detailed Cust. Ledg. Entry + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceLifecycle.SetRange("Lifecycle Status", FREInvoiceLifecycle."Lifecycle Status"::Collected); + FREInvoiceLifecycle.FindFirst(); + DetailedCustLedgEntry.Get(FREInvoiceLifecycle."Detailed Ledger Entry No."); + Assert.AreEqual("FR E-Invoice Lifecycle Status"::Collected, FREInvoiceLifecycle."Lifecycle Status", 'The lifecycle status must be Collected.'); + Assert.AreEqual(-DetailedCustLedgEntry.Amount, FREInvoiceLifecycle."Reported Amount", 'The reported amount must equal the negated DCLE amount.'); + Assert.IsTrue(FREInvoiceLifecycle."Reported Amount" > 0, 'The collected amount must be positive.'); + Assert.AreEqual(DetailedCustLedgEntry."Posting Date", FREInvoiceLifecycle."Event Date", 'The event date must match the DCLE posting date.'); + Assert.AreEqual(DetailedCustLedgEntry.SystemId, FREInvoiceLifecycle."Source Occurrence ID", 'The source occurrence ID must match the DCLE system ID.'); + FREInvoiceLifecycleVAT.SetRange("Lifecycle Entry No.", FREInvoiceLifecycle."Entry No."); + FREInvoiceLifecycleVAT.CalcSums("Reported Amount"); + Assert.AreEqual(FREInvoiceLifecycle."Reported Amount", FREInvoiceLifecycleVAT."Reported Amount", 'The VAT breakdown must equal the collected amount.'); + end; + + local procedure Initialize() + var + CompanyInformation: Record "Company Information"; + GeneralLedgerSetup: Record "General Ledger Setup"; + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Identification Tests"); CompanyInformation.Get(); - CompanyInformation."SIRET No." := OriginalSIRETNo; + CompanyInformation.Validate("Registration No.", '123456789'); + CompanyInformation.Validate("SIRET No.", '12345678901234'); + CompanyInformation.Modify(true); + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Identification Tests"); + + GeneralLedgerSetup.Get(); + GeneralLedgerSetup."LCY Code" := 'EUR'; + GeneralLedgerSetup.Modify(true); + + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Invoice Nos.")); + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Posted Invoice Nos.")); + + IsInitialized := true; + Commit(); + + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Identification Tests"); + end; + + local procedure CreateFRFacturXEDocument(var EDocument: Record "E-Document"; SalesInvoiceHeader: Record "Sales Invoice Header") + var + EDocumentService: Record "E-Document Service"; + EDocumentServiceStatus: Record "E-Document Service Status"; + begin + EDocumentService.Code := CopyStr(CreateGuid(), 1, MaxStrLen(EDocumentService.Code)); + EDocumentService."Document Format" := "E-Document Format"::"Factur-X FR"; + ConfigurePPFService(EDocumentService); + EDocumentService.Insert(); + + EDocument.Init(); + EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; + EDocument."Document No." := SalesInvoiceHeader."No."; + EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; + EDocument.Direction := EDocument.Direction::Outgoing; + EDocument.Service := EDocumentService.Code; + EDocument."Document Date" := SalesInvoiceHeader."Posting Date"; + EDocument."Clearance Date" := CurrentDateTime(); + EDocument.Insert(); + + EDocumentServiceStatus."E-Document Entry No" := EDocument."Entry No"; + EDocumentServiceStatus."E-Document Service Code" := EDocumentService.Code; + EDocumentServiceStatus.Status := EDocumentServiceStatus.Status::Approved; + EDocumentServiceStatus.Insert(); + end; + + local procedure CreateEDocument(var EDocument: Record "E-Document") + begin + EDocument.Init(); + EDocument."Document No." := CopyStr(CreateGuid(), 1, MaxStrLen(EDocument."Document No.")); + EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; + EDocument.Direction := EDocument.Direction::Outgoing; + EDocument.Insert(); + end; + + local procedure CreateAdditionalEDocument(var AdditionalEDocument: Record "E-Document"; EDocument: Record "E-Document") + var + EDocumentServiceStatus: Record "E-Document Service Status"; + begin + AdditionalEDocument.Init(); + AdditionalEDocument."Document Record ID" := EDocument."Document Record ID"; + AdditionalEDocument."Document No." := EDocument."Document No."; + AdditionalEDocument."Document Type" := EDocument."Document Type"; + AdditionalEDocument.Direction := EDocument.Direction; + AdditionalEDocument.Service := EDocument.Service; + AdditionalEDocument."Document Date" := EDocument."Document Date"; + AdditionalEDocument."Clearance Date" := EDocument."Clearance Date"; + AdditionalEDocument.Insert(); + + EDocumentServiceStatus."E-Document Entry No" := AdditionalEDocument."Entry No"; + EDocumentServiceStatus."E-Document Service Code" := AdditionalEDocument.Service; + EDocumentServiceStatus.Status := EDocumentServiceStatus.Status::Approved; + EDocumentServiceStatus.Insert(); + end; + + local procedure CreatePostedInvoiceApplication(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; EDocumentFormat: Enum "E-Document Format") + var + EDocumentService: Record "E-Document Service"; + EDocumentServiceStatus: Record "E-Document Service Status"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; + PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; + VATEntry: Record "VAT Entry"; + VATPostingSetup: Record "VAT Posting Setup"; + DocumentNo: Code[20]; + VATBusPostingGroup: Code[20]; + VATProdPostingGroup: Code[20]; + begin + DocumentNo := CopyStr(CreateGuid(), 1, MaxStrLen(DocumentNo)); + SalesInvoiceHeader."No." := DocumentNo; + SalesInvoiceHeader.Insert(); + + EDocumentService.Code := CopyStr(CreateGuid(), 1, MaxStrLen(EDocumentService.Code)); + EDocumentService."Document Format" := EDocumentFormat; + ConfigurePPFService(EDocumentService); + EDocumentService.Insert(); + + EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; + EDocument."Document No." := DocumentNo; + EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; + EDocument.Direction := EDocument.Direction::Outgoing; + EDocument.Service := EDocumentService.Code; + EDocument."Document Date" := WorkDate(); + EDocument.Insert(); + + EDocumentServiceStatus."E-Document Entry No" := EDocument."Entry No"; + EDocumentServiceStatus."E-Document Service Code" := EDocument.Service; + EDocumentServiceStatus.Status := EDocumentServiceStatus.Status::Approved; + EDocumentServiceStatus.Insert(); + EDocument.Validate(Status, EDocument.Status::Processed); + EDocument.Modify(true); + + InvoiceCustLedgerEntry."Entry No." := GetNextCustLedgerEntryNo(); + InvoiceCustLedgerEntry."Document Type" := InvoiceCustLedgerEntry."Document Type"::Invoice; + InvoiceCustLedgerEntry."Document No." := DocumentNo; + InvoiceCustLedgerEntry."Posting Date" := WorkDate(); + InvoiceCustLedgerEntry."Transaction No." := InvoiceCustLedgerEntry."Entry No."; + InvoiceCustLedgerEntry.Insert(); + + VATBusPostingGroup := CopyStr(CreateGuid(), 1, MaxStrLen(VATBusPostingGroup)); + VATProdPostingGroup := CopyStr(CreateGuid(), 1, MaxStrLen(VATProdPostingGroup)); + VATPostingSetup."VAT Bus. Posting Group" := VATBusPostingGroup; + VATPostingSetup."VAT Prod. Posting Group" := VATProdPostingGroup; + VATPostingSetup."VAT %" := 20; + VATPostingSetup.Insert(); + + VATEntry."Entry No." := GetNextVATEntryNo(); + VATEntry.Type := VATEntry.Type::Sale; + VATEntry."Document Type" := VATEntry."Document Type"::Invoice; + VATEntry."Document No." := DocumentNo; + VATEntry."Posting Date" := WorkDate(); + VATEntry."Transaction No." := InvoiceCustLedgerEntry."Transaction No."; + VATEntry."VAT Bus. Posting Group" := VATBusPostingGroup; + VATEntry."VAT Prod. Posting Group" := VATProdPostingGroup; + VATEntry."Source Currency Code" := 'EUR'; + VATEntry."Source Currency VAT Base" := -500; + VATEntry."Source Currency VAT Amount" := -100; + VATEntry.Insert(); + + Clear(VATPostingSetup); + VATBusPostingGroup := CopyStr(CreateGuid(), 1, MaxStrLen(VATBusPostingGroup)); + VATProdPostingGroup := CopyStr(CreateGuid(), 1, MaxStrLen(VATProdPostingGroup)); + VATPostingSetup."VAT Bus. Posting Group" := VATBusPostingGroup; + VATPostingSetup."VAT Prod. Posting Group" := VATProdPostingGroup; + VATPostingSetup."VAT %" := 10; + VATPostingSetup.Insert(); + + Clear(VATEntry); + VATEntry."Entry No." := GetNextVATEntryNo(); + VATEntry.Type := VATEntry.Type::Sale; + VATEntry."Document Type" := VATEntry."Document Type"::Invoice; + VATEntry."Document No." := DocumentNo; + VATEntry."Posting Date" := WorkDate(); + VATEntry."Transaction No." := InvoiceCustLedgerEntry."Transaction No."; + VATEntry."VAT Bus. Posting Group" := VATBusPostingGroup; + VATEntry."VAT Prod. Posting Group" := VATProdPostingGroup; + VATEntry."Source Currency Code" := 'EUR'; + VATEntry."Source Currency VAT Base" := -590.91; + VATEntry."Source Currency VAT Amount" := -59.09; + VATEntry.Insert(); + + PaymentCustLedgerEntry."Entry No." := InvoiceCustLedgerEntry."Entry No." + 1; + PaymentCustLedgerEntry."Document Type" := PaymentCustLedgerEntry."Document Type"::Payment; + PaymentCustLedgerEntry."Document No." := CopyStr(CreateGuid(), 1, MaxStrLen(PaymentCustLedgerEntry."Document No.")); + PaymentCustLedgerEntry.Insert(); + + DetailedCustLedgEntry."Entry No." := GetNextDetailedCustLedgerEntryNo(); + DetailedCustLedgEntry."Cust. Ledger Entry No." := InvoiceCustLedgerEntry."Entry No."; + DetailedCustLedgEntry."Applied Cust. Ledger Entry No." := PaymentCustLedgerEntry."Entry No."; + DetailedCustLedgEntry."Entry Type" := DetailedCustLedgEntry."Entry Type"::Application; + DetailedCustLedgEntry."Initial Document Type" := DetailedCustLedgEntry."Initial Document Type"::Invoice; + DetailedCustLedgEntry.Amount := -1000; + DetailedCustLedgEntry."Currency Code" := 'EUR'; + DetailedCustLedgEntry."Posting Date" := WorkDate(); + DetailedCustLedgEntry.Insert(true); + end; + + local procedure CreateUnapplicationDetail(var NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + begin + NewDetailedCustLedgEntry := DetailedCustLedgEntry; + NewDetailedCustLedgEntry."Entry No." := GetNextDetailedCustLedgerEntryNo(); + NewDetailedCustLedgEntry.Amount := -DetailedCustLedgEntry.Amount; + NewDetailedCustLedgEntry."Posting Date" := WorkDate() + 1; + NewDetailedCustLedgEntry.Unapplied := true; + NewDetailedCustLedgEntry."Unapplied by Entry No." := DetailedCustLedgEntry."Entry No."; + NewDetailedCustLedgEntry.Insert(true); + end; + + local procedure GetNextCustLedgerEntryNo(): Integer + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + begin + if CustLedgerEntry.FindLast() then + exit(CustLedgerEntry."Entry No." + 1); + exit(1); + end; + + local procedure GetNextDetailedCustLedgerEntryNo(): Integer + var + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + begin + if DetailedCustLedgEntry.FindLast() then + exit(DetailedCustLedgEntry."Entry No." + 1); + exit(1); + end; + + local procedure CreatePaymentCustLedgerEntry(var PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; CurrencyCode: Code[10]) + begin + PaymentCustLedgerEntry."Entry No." := GetNextCustLedgerEntryNo(); + PaymentCustLedgerEntry."Document Type" := PaymentCustLedgerEntry."Document Type"::Payment; + PaymentCustLedgerEntry."Document No." := CopyStr(CreateGuid(), 1, MaxStrLen(PaymentCustLedgerEntry."Document No.")); + PaymentCustLedgerEntry."Currency Code" := CurrencyCode; + PaymentCustLedgerEntry.Insert(); + end; + + local procedure GetNextVATEntryNo(): Integer + var + VATEntry: Record "VAT Entry"; + begin + if VATEntry.FindLast() then + exit(VATEntry."Entry No." + 1); + exit(1); + end; + + local procedure CreateLifecycleVATBreakdown(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; VATRate: Decimal; ReportedAmount: Decimal) + var + FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; + begin + FREInvoiceLifecycleVAT."Lifecycle Entry No." := FREInvoiceLifecycle."Entry No."; + FREInvoiceLifecycleVAT."Line No." := 10000; + FREInvoiceLifecycleVAT."VAT %" := VATRate; + FREInvoiceLifecycleVAT."Reported Amount" := ReportedAmount; + FREInvoiceLifecycleVAT.Insert(); + end; + + local procedure SetInvoiceVATRate(DocumentNo: Code[20]; VATRate: Decimal; ClearVATAmount: Boolean) + var + VATEntry: Record "VAT Entry"; + VATPostingSetup: Record "VAT Posting Setup"; + begin + VATEntry.SetRange("Document No.", DocumentNo); + VATEntry.FindSet(); + repeat + VATPostingSetup.Get(VATEntry."VAT Bus. Posting Group", VATEntry."VAT Prod. Posting Group"); + VATPostingSetup."VAT %" := VATRate; + VATPostingSetup.Modify(); + if ClearVATAmount then begin + VATEntry."Source Currency VAT Amount" := 0; + VATEntry.Modify(); + end; + until VATEntry.Next() = 0; + end; + + local procedure SetInvoiceVATCurrency(DocumentNo: Code[20]; CurrencyCode: Code[10]) + var + VATEntry: Record "VAT Entry"; + begin + VATEntry.SetRange("Document No.", DocumentNo); + VATEntry.FindSet(); + repeat + VATEntry."Source Currency Code" := CurrencyCode; + VATEntry.Modify(); + until VATEntry.Next() = 0; + end; + + local procedure ConfigurePPFService(var EDocumentService: Record "E-Document Service") + var + CompanyInformation: Record "Company Information"; + begin + EDocumentService."FR Sender Platform ID" := 'PLATFORM-ID'; + EDocumentService."FR Sender Platform Scheme" := '0238'; + EDocumentService."FR Sender Platform Name" := 'Test Approved Platform'; + CompanyInformation.Get(); + CompanyInformation.Name := 'Test Company'; + CompanyInformation."Registration No." := '123456789'; CompanyInformation.Modify(); end; + + local procedure AssertXmlValue(XmlDoc: XmlDocument; XPath: Text; ExpectedValue: Text) + var + XmlNode: XmlNode; + begin + Assert.IsTrue(XmlDoc.SelectSingleNode(XPath, XmlNode), StrSubstNo(XmlNodeMissingErr, XPath)); + Assert.AreEqual(ExpectedValue, XmlNode.AsXmlElement().InnerText(), StrSubstNo(XmlNodeValueErr, XPath)); + end; + + local procedure AssertXmlAttribute(XmlDoc: XmlDocument; XPath: Text; AttributeName: Text; ExpectedValue: Text) + var + XmlAttribute: XmlAttribute; + XmlNode: XmlNode; + begin + Assert.IsTrue(XmlDoc.SelectSingleNode(XPath, XmlNode), StrSubstNo(XmlNodeMissingErr, XPath)); + Assert.IsTrue(XmlNode.AsXmlElement().Attributes().Get(AttributeName, XmlAttribute), StrSubstNo(XmlAttributeMissingErr, XPath, AttributeName)); + Assert.AreEqual(ExpectedValue, XmlAttribute.Value(), StrSubstNo(XmlAttributeValueErr, AttributeName, XPath)); + end; } 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 094363b074a..3e5ba76e83d 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 @@ -6,10 +6,13 @@ namespace Microsoft.eServices.EDocument.Formats.Test; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Formats; +using Microsoft.eServices.EDocument.Service.Participant; using Microsoft.Finance.GeneralLedger.Account; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Foundation.Address; using Microsoft.Foundation.Company; +using Microsoft.Inventory.Item; +using Microsoft.Sales.Comment; using Microsoft.Sales.Customer; using Microsoft.Sales.Document; using Microsoft.Sales.History; @@ -21,6 +24,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Subtype = Test; Permissions = tabledata "E-Document Service" = rimd, tabledata "Company Information" = rimd, + tabledata "Sales Comment Line" = rimd, + tabledata "Service Participant" = rimd, + tabledata "Sales Invoice Line" = rimd, + tabledata "Sales Shipment Header" = rimd, + tabledata "Sales Shipment Line" = rimd, tabledata "Sales & Receivables Setup" = rimd, tabledata Customer = rimd; @@ -34,14 +42,16 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" EDocumentService: Record "E-Document Service"; LibrarySales: Codeunit "Library - Sales"; LibraryERM: Codeunit "Library - ERM"; + LibraryInventory: Codeunit "Library - Inventory"; LibraryUtility: Codeunit "Library - Utility"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; Assert: Codeunit Assert; PeppolBIS30FRFormat: Codeunit "Peppol BIS 3.0 FR Format"; + BuyerElectronicAddressRequiredErr: Label 'Electronic Address, VAT Registration No., or a Service Participant identifier must be specified for the customer for French e-invoicing.', Locked = true; + BuyerElectronicAddressSchemeRequiredErr: Label 'Electronic Address Scheme must be specified for the customer for French e-invoicing.', Locked = true; + ParticipantAddressIncompleteErr: Label 'must both be specified for French electronic invoicing.', Locked = true; IncorrectValueErr: Label 'Incorrect value for %1', Comment = '%1 = XML element path', Locked = true; - CustomerElectronicAddressErr: Label 'Electronic Address must be specified for Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.'; IsInitialized: Boolean; - CustomerVATNoSequence: Integer; #region SalesInvoice [Test] @@ -279,6 +289,148 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID/@schemeID'), StrSubstNo(IncorrectValueErr, 'Buyer PartyIdentification schemeID')); end; + + [Test] + procedure ExportSalesInvIncludesRegulatoryCommentAsNote() + var + SalesCommentLine: Record "Sales Comment Line"; + SalesHeader: Record "Sales Header"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + CommentText: Text[80]; + CustomerNo: Code[20]; + InvoiceNo: Code[20]; + begin + // [FEATURE] [AI test] + // [SCENARIO] A French regulatory comment is carried through posting and exported with its type in a UBL header note + Initialize(); + + // [GIVEN] A sales invoice with ordinary and French regulatory comments + CustomerNo := CreateCustomer('', "Electronic Address Scheme"::"EM"); + InvoiceNo := CreateSalesInvoiceWithLine(CustomerNo); + CommentText := 'No discount is granted for early payment.'; + SalesCommentLine."Document Type" := SalesCommentLine."Document Type"::Invoice; + SalesCommentLine."No." := InvoiceNo; + SalesCommentLine."Line No." := 5000; + SalesCommentLine.Comment := 'Ordinary comment that must not be exported'; + SalesCommentLine.Insert(); + SalesCommentLine.Init(); + SalesCommentLine."Document Type" := SalesCommentLine."Document Type"::Invoice; + SalesCommentLine."No." := InvoiceNo; + SalesCommentLine."Line No." := 10000; + SalesCommentLine."FR Regulatory Comment Type" := SalesCommentLine."FR Regulatory Comment Type"::AAB; + SalesCommentLine.Comment := CommentText; + SalesCommentLine.Insert(); + SalesHeader.Get("Sales Document Type"::Invoice, InvoiceNo); + SalesInvoiceHeader.Get(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + + // [WHEN] The posted invoice is exported + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] Only the regulatory comment is exported with its type prefix + Assert.AreEqual('#AAB#' + CommentText, GetNodeByPath(XmlDoc, '/Invoice/cbc:Note'), StrSubstNo(IncorrectValueErr, 'Note')); + end; + + [Test] + procedure ExportSalesInvSetsS1ForServiceLines() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + begin + // [FEATURE] [AI test] + // [SCENARIO] An invoice containing only service lines uses billing mode S1 + Initialize(); + + // [GIVEN] A posted sales invoice containing a service line + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('', "Electronic Address Scheme"::"EM"))); + + // [WHEN] The posted invoice is exported + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] The billing mode is S1 + Assert.AreEqual('S1', GetNodeByPath(XmlDoc, '/Invoice/cbc:ProfileID'), StrSubstNo(IncorrectValueErr, 'ProfileID')); + end; + + [Test] + procedure ExportSalesInvSetsB1ForItemLines() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + begin + // [FEATURE] [AI test] + // [SCENARIO] An invoice containing only item lines uses billing mode B1 + Initialize(); + + // [GIVEN] A posted sales invoice containing only an item line + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemLine(CreateCustomer('', "Electronic Address Scheme"::"EM"), false)); + + // [WHEN] The posted invoice is exported + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] The billing mode is B1 + Assert.AreEqual('B1', GetNodeByPath(XmlDoc, '/Invoice/cbc:ProfileID'), StrSubstNo(IncorrectValueErr, 'ProfileID')); + end; + + [Test] + procedure ExportSalesInvSetsM1ForMixedLines() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + begin + // [FEATURE] [AI test] + // [SCENARIO] An invoice containing item and service lines uses billing mode M1 + Initialize(); + + // [GIVEN] A posted sales invoice containing an item line and a service line + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemLine(CreateCustomer('', "Electronic Address Scheme"::"EM"), true)); + + // [WHEN] The posted invoice is exported + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] The billing mode is M1 + Assert.AreEqual('M1', GetNodeByPath(XmlDoc, '/Invoice/cbc:ProfileID'), StrSubstNo(IncorrectValueErr, 'ProfileID')); + end; + + [Test] + procedure ExportSalesInvUsesServiceParticipantEndpointWithScheme0225() + var + ServiceParticipant: Record "Service Participant"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + CustomerNo: Code[20]; + EndpointId: Text[200]; + begin + // [FEATURE] [AI test] + // [SCENARIO] A service-specific routing identifier overrides the endpoint on the customer card + Initialize(); + + // [GIVEN] A posted invoice for customer "C" with a service participant using scheme 0225 + CustomerNo := CreateCustomer('12345678901234', "Electronic Address Scheme"::"0009"); + EndpointId := '123456789_001'; + ServiceParticipant.Service := EDocumentService.Code; + ServiceParticipant."Participant Type" := ServiceParticipant."Participant Type"::Customer; + ServiceParticipant.Participant := CustomerNo; + ServiceParticipant."Participant Identifier" := EndpointId; + ServiceParticipant."FR Identifier Scheme" := ServiceParticipant."FR Identifier Scheme"::"0225"; + ServiceParticipant.Insert(); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + + // [WHEN] The invoice is checked and exported + CheckInvoice(SalesInvoiceHeader); + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] The service participant identifier is exported as the buyer endpoint + Assert.AreEqual(EndpointId, + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID'), + StrSubstNo(IncorrectValueErr, 'Buyer EndpointID')); + Assert.AreEqual('0225', + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'), + StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); + Assert.AreEqual('', + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID'), + StrSubstNo(IncorrectValueErr, 'Buyer PartyIdentification ID')); + end; + #endregion #region SalesCreditMemo @@ -322,6 +474,116 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" GetNodeByPath(XmlDoc, '/CreditNote/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'), StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); end; + + [Test] + procedure ExportSalesCrMemoIncludesRegulatoryCommentAsNote() + var + SalesCommentLine: Record "Sales Comment Line"; + SalesCrMemoHeader: Record "Sales Cr.Memo Header"; + XmlDoc: XmlDocument; + CommentText: Text[80]; + begin + // [FEATURE] [AI test] + // [SCENARIO] A French regulatory comment on a posted credit memo is prefixed with its type in a UBL header note + Initialize(); + + // [GIVEN] A posted sales credit memo with a French regulatory comment + SalesCrMemoHeader.Get(CreateAndPostSalesCrMemo(CreateCustomer('', "Electronic Address Scheme"::"EM"))); + CommentText := 'No discount is granted for early payment.'; + SalesCommentLine."Document Type" := SalesCommentLine."Document Type"::"Posted Credit Memo"; + SalesCommentLine."No." := SalesCrMemoHeader."No."; + SalesCommentLine."Line No." := 10000; + SalesCommentLine."FR Regulatory Comment Type" := SalesCommentLine."FR Regulatory Comment Type"::AAB; + SalesCommentLine.Comment := CommentText; + SalesCommentLine.Insert(); + + // [WHEN] The posted credit memo is exported + ExportCrMemo(SalesCrMemoHeader, XmlDoc); + + // [THEN] The regulatory comment is exported with its type prefix + Assert.AreEqual('#AAB#' + CommentText, GetNodeByPath(XmlDoc, '/CreditNote/cbc:Note'), StrSubstNo(IncorrectValueErr, 'Note')); + end; + + [Test] + procedure ExportSalesInvSelectsExtendedCTCForMultipleOrders() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + XmlDoc: XmlDocument; + begin + // [FEATURE] [AI test] + // [SCENARIO] An invoice containing lines from distinct orders uses the Extended CTC profile + Initialize(); + + // [GIVEN] A posted sales invoice created from multiple orders + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceFromMultipleOrders(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + + // [WHEN] The posted invoice is exported + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] The Extended CTC profile and order line references are exported + Assert.AreEqual('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/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')); + end; + + [Test] + procedure ExportSalesInvSelectsExtendedCTCForMultipleShipments() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + SalesShipmentHeader: Record "Sales Shipment Header"; + XmlDoc: XmlDocument; + begin + // [FEATURE] [AI test] + // [SCENARIO] An invoice containing lines from distinct shipments uses the Extended CTC profile + Initialize(); + + // [GIVEN] A posted sales invoice created from multiple shipments + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceFromMultipleShipments(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + + // [WHEN] The posted invoice is exported + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] The Extended CTC profile and shipment references are exported + Assert.AreEqual('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(); + SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No."); + Assert.AreEqual(SalesInvoiceLine."Shipment No.", GetNodeByPath(XmlDoc, '/Invoice/cac:InvoiceLine/cac:Delivery/cbc:ID'), + StrSubstNo(IncorrectValueErr, 'Delivery ID')); + Assert.AreEqual(Format(SalesShipmentHeader."Posting Date", 0, 9), GetNodeByPath(XmlDoc, '/Invoice/cac:InvoiceLine/cac:Delivery/cbc:ActualDeliveryDate'), + StrSubstNo(IncorrectValueErr, 'ActualDeliveryDate')); + end; + + [Test] + procedure ExportSalesInvKeepsBasicCTCForRepeatedReferences() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + begin + // [FEATURE] [AI test] + // [SCENARIO] Repeated references to one shipment and one order do not select the Extended CTC profile + Initialize(); + + // [GIVEN] A posted sales invoice with repeated references to one shipment + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceFromSingleShipment(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + + // [WHEN] The posted invoice is exported + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] The basic CTC profile is retained + Assert.AreEqual('urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), + StrSubstNo(IncorrectValueErr, 'CustomizationID')); + end; #endregion #region Validation @@ -354,8 +616,8 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Initialize(); // [GIVEN] Company with blank Registration No. - OriginalRegistrationNo := CompanyInformation."Registration No."; CompanyInformation.Get(); + OriginalRegistrationNo := CompanyInformation."Registration No."; CompanyInformation."Registration No." := ''; CompanyInformation.Modify(true); @@ -367,40 +629,43 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" // [THEN] Error about Registration No. is raised Assert.ExpectedError('Registration No. must be specified in Company Information for French e-invoicing.'); - // Cleanup CompanyInformation.Get(); CompanyInformation."Registration No." := CopyStr(OriginalRegistrationNo, 1, MaxStrLen(CompanyInformation."Registration No.")); CompanyInformation.Modify(true); end; [Test] - procedure CheckRaisesErrorWhenSIRETIsEmpty() + procedure ExportSalesInvUsesCompanyServiceParticipantEndpoint() var + ServiceParticipant: Record "Service Participant"; SalesInvoiceHeader: Record "Sales Invoice Header"; - OriginalSIRETNo: Code[14]; + XmlDoc: XmlDocument; + EndpointId: Text[200]; begin // [FEATURE] [AI test] - // [SCENARIO] Check raises error when company SIRET No. is blank + // [SCENARIO] A service-specific company participant overrides the company endpoint fallbacks Initialize(); - // [GIVEN] Company with blank SIRET No. - OriginalSIRETNo := CompanyInformation."SIRET No."; - CompanyInformation.Get(); - CompanyInformation."SIRET No." := ''; - CompanyInformation.Modify(true); - + // [GIVEN] A posted invoice and a company service participant + EndpointId := CompanyInformation."Registration No."; + ServiceParticipant.Service := EDocumentService.Code; + ServiceParticipant."Participant Type" := ServiceParticipant."Participant Type"::Company; + ServiceParticipant."Participant Identifier" := EndpointId; + ServiceParticipant."FR Identifier Scheme" := ServiceParticipant."FR Identifier Scheme"::"0002"; + ServiceParticipant.Insert(); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); - // [WHEN] Check is called - asserterror CheckInvoice(SalesInvoiceHeader); - - // [THEN] Error about SIRET No. is raised - Assert.ExpectedError('SIRET No. must be specified in Company Information for French e-invoicing.'); + // [WHEN] The invoice is checked and exported + CheckInvoice(SalesInvoiceHeader); + ExportInvoice(SalesInvoiceHeader, XmlDoc); - // Cleanup - CompanyInformation.Get(); - CompanyInformation."SIRET No." := OriginalSIRETNo; - CompanyInformation.Modify(true); + // [THEN] The company participant identifier is exported as the seller endpoint + Assert.AreEqual(EndpointId, + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID'), + StrSubstNo(IncorrectValueErr, 'Seller EndpointID')); + Assert.AreEqual('0002', + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID'), + StrSubstNo(IncorrectValueErr, 'Seller EndpointID schemeID')); end; [Test] @@ -414,8 +679,8 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Initialize(); // [GIVEN] Company with blank Country/Region Code - OriginalCountryCode := CompanyInformation."Country/Region Code"; CompanyInformation.Get(); + OriginalCountryCode := CompanyInformation."Country/Region Code"; CompanyInformation."Country/Region Code" := ''; CompanyInformation.Modify(true); @@ -427,54 +692,249 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" // [THEN] Error about Country/Region Code is raised Assert.ExpectedError('Country/Region Code must be specified in Company Information for French e-invoicing.'); - // Cleanup CompanyInformation.Get(); CompanyInformation."Country/Region Code" := OriginalCountryCode; CompanyInformation.Modify(true); end; [Test] - procedure CheckRaisesErrorWhenBuyerElectronicAddressIsEmpty() + procedure ExportSalesInvUsesBuyerVATFallbackWhenElectronicAddressIsEmpty() var SalesInvoiceHeader: Record "Sales Invoice Header"; - ExpectedErrorText: Text; + XmlDoc: XmlDocument; begin // [FEATURE] [AI test] - // [SCENARIO] Check raises error when customer FR Electronic Address is blank + // [SCENARIO] Customer VAT registration number is used when the electronic address is blank Initialize(); - // [GIVEN] Posted sales invoice for customer with blank FR electronic address + // [GIVEN] Posted sales invoice for a customer with blank electronic address and a VAT number SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('', "Electronic Address Scheme"::"EM"))); - // [WHEN] Check is called + // [WHEN] The invoice is checked and exported + CheckInvoice(SalesInvoiceHeader); + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] The buyer endpoint uses the VAT identifier and French VAT scheme 9957 + Assert.AreEqual(GetCustomerVATRegistrationNo(SalesInvoiceHeader."Sell-to Customer No."), + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID'), + StrSubstNo(IncorrectValueErr, 'Buyer EndpointID')); + Assert.AreEqual('9957', + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'), + StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); + end; + + [Test] + procedure ExportSalesInvUsesSupplierSIRENWhenSIRETIsMissing() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + OriginalSIRETNo: Code[14]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Company Registration No. is used as the seller endpoint when SIRET is missing + Initialize(); + + // [GIVEN] A posted invoice for a company with SIRET missing + OriginalSIRETNo := CompanyInformation."SIRET No."; + CompanyInformation."SIRET No." := ''; + CompanyInformation.Modify(true); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + + // [WHEN] The invoice is checked and exported + CheckInvoice(SalesInvoiceHeader); + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + // [THEN] The company SIREN is exported as the seller endpoint + Assert.AreEqual(CompanyInformation."Registration No.", + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID'), + StrSubstNo(IncorrectValueErr, 'Seller EndpointID')); + Assert.AreEqual('0002', + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID'), + StrSubstNo(IncorrectValueErr, 'Seller EndpointID schemeID')); + + CompanyInformation.Get(); + CompanyInformation."SIRET No." := OriginalSIRETNo; + CompanyInformation.Modify(true); + end; + + [Test] + procedure CheckRaisesErrorWhenCompanyParticipantSchemeIsMissing() + var + ServiceParticipant: Record "Service Participant"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Check rejects a company participant identifier without its scheme even when SIRET is valid + Initialize(); + + // [GIVEN] A posted invoice and a company participant without an identifier scheme + ServiceParticipant.Init(); + ServiceParticipant.Service := EDocumentService.Code; + ServiceParticipant."Participant Type" := ServiceParticipant."Participant Type"::Company; + ServiceParticipant."Participant Identifier" := CompanyInformation."Registration No."; + ServiceParticipant.Insert(); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + + // [WHEN] The invoice is checked asserterror CheckInvoice(SalesInvoiceHeader); - // [THEN] Error about Electronic Address is raised - ExpectedErrorText := StrSubstNo(CustomerElectronicAddressErr, SalesInvoiceHeader."Sell-to Customer No."); - Assert.ExpectedError(ExpectedErrorText); + // [THEN] An incomplete participant address error is raised + Assert.ExpectedError(ParticipantAddressIncompleteErr); + end; + + [Test] + procedure CheckRaisesErrorWhenCompanyParticipantIdentifierIsMissing() + var + ServiceParticipant: Record "Service Participant"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Check rejects a company participant scheme without its identifier even when SIRET is valid + Initialize(); + + // [GIVEN] A posted invoice and a company participant without an identifier + ServiceParticipant.Init(); + ServiceParticipant.Service := EDocumentService.Code; + ServiceParticipant."Participant Type" := ServiceParticipant."Participant Type"::Company; + ServiceParticipant."FR Identifier Scheme" := ServiceParticipant."FR Identifier Scheme"::"0225"; + ServiceParticipant.Insert(); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + + // [WHEN] The invoice is checked + asserterror CheckInvoice(SalesInvoiceHeader); + + // [THEN] An incomplete participant address error is raised + Assert.ExpectedError(ParticipantAddressIncompleteErr); + end; + + [Test] + procedure CheckRaisesErrorWhenBuyerElectronicAddressIsMissing() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + CustomerNo: Code[20]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Check rejects a buyer without an electronic address, VAT registration number, or a service participant identifier + Initialize(); + + // [GIVEN] A posted invoice for buyer "B" without an electronic address or VAT number + CustomerNo := CreateCustomer('', "Electronic Address Scheme"::"EM"); + ClearCustomerVATRegistrationNo(CustomerNo); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + + // [WHEN] The invoice is checked + asserterror CheckInvoice(SalesInvoiceHeader); + + // [THEN] A buyer electronic address error is raised + Assert.ExpectedError(BuyerElectronicAddressRequiredErr); + end; + + [Test] + procedure CheckRaisesErrorWhenBuyerElectronicAddressSchemeIsMissing() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + CustomerNo: Code[20]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Check rejects a buyer electronic address without its scheme + Initialize(); + + // [GIVEN] A posted invoice for buyer "B" with an address but no scheme + CustomerNo := CreateCustomer('buyer@example.com', "Electronic Address Scheme"::" "); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + + // [WHEN] The invoice is checked + asserterror CheckInvoice(SalesInvoiceHeader); + + // [THEN] A buyer electronic address scheme error is raised + Assert.ExpectedError(BuyerElectronicAddressSchemeRequiredErr); + end; + + [Test] + procedure CheckRaisesErrorWhenParticipantSchemeIsMissing() + var + ServiceParticipant: Record "Service Participant"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + CustomerNo: Code[20]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Check rejects a service participant identifier without its French identifier scheme even when the customer endpoint is valid + Initialize(); + + // [GIVEN] A posted invoice and a buyer service participant without a scheme + CustomerNo := CreateCustomer('buyer@example.com', "Electronic Address Scheme"::"EM"); + ServiceParticipant.Init(); + ServiceParticipant.Service := EDocumentService.Code; + ServiceParticipant."Participant Type" := ServiceParticipant."Participant Type"::Customer; + ServiceParticipant.Participant := CustomerNo; + ServiceParticipant."Participant Identifier" := '123456789_001'; + ServiceParticipant.Insert(); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + + // [WHEN] The invoice is checked + asserterror CheckInvoice(SalesInvoiceHeader); + + // [THEN] An incomplete participant address error is raised + Assert.ExpectedError(ParticipantAddressIncompleteErr); + end; + + [Test] + procedure CheckRaisesErrorWhenParticipantIdentifierIsMissing() + var + ServiceParticipant: Record "Service Participant"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + CustomerNo: Code[20]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Check rejects a French identifier scheme without its service participant identifier even when the customer endpoint is valid + Initialize(); + + // [GIVEN] A posted invoice and a buyer service participant without an identifier + CustomerNo := CreateCustomer('buyer@example.com', "Electronic Address Scheme"::"EM"); + ServiceParticipant.Init(); + ServiceParticipant.Service := EDocumentService.Code; + ServiceParticipant."Participant Type" := ServiceParticipant."Participant Type"::Customer; + ServiceParticipant.Participant := CustomerNo; + ServiceParticipant."FR Identifier Scheme" := ServiceParticipant."FR Identifier Scheme"::"0225"; + ServiceParticipant.Insert(); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + + // [WHEN] The invoice is checked + asserterror CheckInvoice(SalesInvoiceHeader); + + // [THEN] An incomplete participant address error is raised + Assert.ExpectedError(ParticipantAddressIncompleteErr); end; #endregion local procedure Initialize() var + CountryRegion: Record "Country/Region"; + ServiceParticipant: Record "Service Participant"; ServiceCode: Code[20]; begin LibraryTestInitialize.OnTestInitialize(Codeunit::"PEPPOL BIS 3.0 XML Tests"); - if IsInitialized then + ServiceParticipant.SetRange(Service, EDocumentService.Code); + ServiceParticipant.DeleteAll(); + if IsInitialized then begin + InitializeCompanyIdentity(); + Commit(); exit; + end; LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"PEPPOL BIS 3.0 XML Tests"); + if not CountryRegion.Get('FR') then begin + CountryRegion.Code := 'FR'; + CountryRegion.Insert(); + end; + CountryRegion.Validate("ISO Code", 'FR'); + CountryRegion.Modify(true); + CompanyInformation.Get(); - EnsureCountryRegionExists('FR'); CompanyInformation.Name := 'Test Company FR'; CompanyInformation.Address := '123 Rue de Paris'; CompanyInformation.City := 'Paris'; CompanyInformation."Post Code" := '75001'; - CompanyInformation."Country/Region Code" := 'FR'; - CompanyInformation.Validate("Registration No.", '123456789'); - CompanyInformation.Validate("SIRET No.", '12345678901234'); - if CompanyInformation."VAT Registration No." = '' then - CompanyInformation.Validate("VAT Registration No.", 'FR12345678901'); CompanyInformation.Validate(IBAN, 'FR1420041010050500013M02606'); CompanyInformation.Validate("SWIFT Code", 'CCBPFRPPVER'); CompanyInformation.Validate("Bank Branch No.", '20041'); @@ -492,12 +952,23 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" EDocumentService.Validate("Document Format", EDocumentService."Document Format"::"Peppol BIS 3.0 FR"); EDocumentService.Modify(true); + InitializeCompanyIdentity(); IsInitialized := true; Commit(); LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"PEPPOL BIS 3.0 XML Tests"); end; + local procedure InitializeCompanyIdentity() + begin + CompanyInformation.Get(); + CompanyInformation.Validate("Country/Region Code", 'FR'); + CompanyInformation.Validate("Registration No.", '123456789'); + CompanyInformation.Validate("SIRET No.", '12345678901234'); + CompanyInformation.Validate("VAT Registration No.", 'FR12345678901'); + CompanyInformation.Modify(true); + end; + local procedure CreateAndPostSalesInvoice(CustomerNo: Code[20]): Code[20] var SalesHeader: Record "Sales Header"; @@ -506,6 +977,140 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); end; + local procedure CreateAndPostSalesInvoiceWithItemLine(CustomerNo: Code[20]; KeepServiceLine: Boolean): Code[20] + var + GeneralPostingSetup: Record "General Posting Setup"; + Item: Record Item; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + begin + SalesHeader.Get("Sales Document Type"::Invoice, CreateSalesInvoiceWithLine(CustomerNo)); + if not KeepServiceLine then begin + SalesLine.SetRange("Document Type", SalesHeader."Document Type"); + SalesLine.SetRange("Document No.", SalesHeader."No."); + SalesLine.DeleteAll(true); + SalesLine.Reset(); + end; + + LibraryInventory.CreateItem(Item); + if not GeneralPostingSetup.Get(SalesHeader."Gen. Bus. Posting Group", Item."Gen. Prod. Posting Group") then + LibraryERM.CreateGeneralPostingSetup(GeneralPostingSetup, SalesHeader."Gen. Bus. Posting Group", Item."Gen. Prod. Posting Group"); + GeneralPostingSetup.Validate("Sales Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("Purch. Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("COGS Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("Inventory Adjmt. Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Modify(true); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + + exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + end; + + local procedure CreateAndPostSalesInvoiceFromMultipleOrders(CustomerNo: Code[20]): Code[20] + var + FirstShipmentNo: Code[20]; + SecondShipmentNo: Code[20]; + begin + FirstShipmentNo := CreateAndPostSalesOrderShipment(CustomerNo, 1, 1); + SecondShipmentNo := CreateAndPostSalesOrderShipment(CustomerNo, 1, 1); + exit(CreateAndPostSalesInvoiceFromShipments(CustomerNo, FirstShipmentNo + '|' + SecondShipmentNo)); + end; + + local procedure CreateAndPostSalesInvoiceFromMultipleShipments(CustomerNo: Code[20]): Code[20] + var + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + FirstShipmentNo: Code[20]; + SecondShipmentNo: Code[20]; + OriginalWorkDate: Date; + begin + CreateSalesOrderWithLines(SalesHeader, CustomerNo, 1, 2); + SalesLine.SetRange("Document Type", SalesHeader."Document Type"); + SalesLine.SetRange("Document No.", SalesHeader."No."); + SalesLine.FindFirst(); + SalesLine.Validate("Qty. to Ship", 1); + SalesLine.Modify(true); + OriginalWorkDate := WorkDate(); + SalesHeader.Validate("Posting Date", CalcDate('<-1D>', OriginalWorkDate)); + SalesHeader.Modify(true); + FirstShipmentNo := LibrarySales.PostSalesDocument(SalesHeader, true, false); + + SalesHeader.Get(SalesHeader."Document Type", SalesHeader."No."); + SalesHeader.Validate("Posting Date", OriginalWorkDate); + SalesHeader.Modify(true); + SalesLine.FindFirst(); + SalesLine.Validate("Qty. to Ship", 1); + SalesLine.Modify(true); + SecondShipmentNo := LibrarySales.PostSalesDocument(SalesHeader, true, false); + + exit(CreateAndPostSalesInvoiceFromShipments(CustomerNo, FirstShipmentNo + '|' + SecondShipmentNo)); + end; + + local procedure CreateAndPostSalesInvoiceFromSingleShipment(CustomerNo: Code[20]): Code[20] + var + ShipmentNo: Code[20]; + begin + ShipmentNo := CreateAndPostSalesOrderShipment(CustomerNo, 2, 1); + exit(CreateAndPostSalesInvoiceFromShipments(CustomerNo, ShipmentNo)); + end; + + local procedure CreateAndPostSalesOrderShipment(CustomerNo: Code[20]; NumberOfLines: Integer; Quantity: Decimal): Code[20] + var + SalesHeader: Record "Sales Header"; + begin + CreateSalesOrderWithLines(SalesHeader, CustomerNo, NumberOfLines, Quantity); + exit(LibrarySales.PostSalesDocument(SalesHeader, true, false)); + end; + + local procedure CreateSalesOrderWithLines(var SalesHeader: Record "Sales Header"; CustomerNo: Code[20]; NumberOfLines: Integer; Quantity: Decimal) + var + Customer: Record Customer; + GLAccount: Record "G/L Account"; + SalesLine: Record "Sales Line"; + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + LineIndex: Integer; + begin + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Order Nos.")); + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Posted Shipment Nos.")); + GLAccount.Get(LibraryERM.CreateGLAccountWithSalesSetup()); + Customer.Get(CustomerNo); + Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); + Customer.Validate("VAT Bus. Posting Group", GLAccount."VAT Bus. Posting Group"); + Customer.Modify(true); + + LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Order, CustomerNo); + for LineIndex := 1 to NumberOfLines do begin + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", Quantity); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + end; + end; + + local procedure CreateAndPostSalesInvoiceFromShipments(CustomerNo: Code[20]; ShipmentNoFilter: Text): Code[20] + var + SalesHeader: Record "Sales Header"; + SalesShipmentLine: Record "Sales Shipment Line"; + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + SalesGetShipment: Codeunit "Sales-Get Shipment"; + begin + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Invoice Nos.")); + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Posted Invoice Nos.")); + LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo); + SalesHeader.Validate("Your Reference", 'FR-BUYER-REF'); + SalesHeader.Modify(true); + + SalesShipmentLine.SetFilter("Document No.", ShipmentNoFilter); + SalesGetShipment.SetSalesHeader(SalesHeader); + SalesGetShipment.CreateInvLines(SalesShipmentLine); + + exit(LibrarySales.PostSalesDocument(SalesHeader, false, true)); + end; + local procedure CreateSalesInvoiceWithLine(CustomerNo: Code[20]): Code[20] var Customer: Record Customer; @@ -547,15 +1152,18 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" local procedure CreateCustomer(FRElectronicAddress: Text[250]; AddressScheme: Enum "Electronic Address Scheme"): Code[20] var Customer: Record Customer; + CustomerPostingGroup: Record "Customer Posting Group"; begin + LibrarySales.CreateCustomerPostingGroup(CustomerPostingGroup); LibrarySales.CreateCustomer(Customer); + Customer.Validate("Customer Posting Group", CustomerPostingGroup.Code); if Customer."Country/Region Code" = '' then Customer.Validate("Country/Region Code", CompanyInformation."Country/Region Code"); if Customer.Address = '' then Customer.Address := CopyStr(LibraryUtility.GenerateRandomText(MaxStrLen(Customer.Address)), 1, MaxStrLen(Customer.Address)); if Customer."Post Code" = '' then Customer.Validate("Post Code", '75001'); - Customer.Validate("VAT Registration No.", GetNextCustomerVATRegistrationNo()); + Customer."VAT Registration No." := LibraryERM.GenerateVATRegistrationNo('FR'); Customer.Validate("FR Electronic Address", FRElectronicAddress); Customer.Validate("FR Elec. Address Scheme", AddressScheme); @@ -564,15 +1172,21 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" exit(Customer."No."); end; - local procedure GetNextCustomerVATRegistrationNo(): Text[20] + local procedure GetCustomerVATRegistrationNo(CustomerNo: Code[20]): Text[20] + var + Customer: Record Customer; + begin + Customer.Get(CustomerNo); + exit(Customer."VAT Registration No."); + end; + + local procedure ClearCustomerVATRegistrationNo(CustomerNo: Code[20]) var - VATNoBody: Text[11]; - SequenceText: Text; + Customer: Record Customer; begin - CustomerVATNoSequence += 1; - SequenceText := Format(CustomerVATNoSequence); - VATNoBody := CopyStr(PadStr('', 11 - StrLen(SequenceText), '0') + SequenceText, 1, 11); - exit('FR' + VATNoBody); + Customer.Get(CustomerNo); + Customer."VAT Registration No." := ''; + Customer.Modify(true); end; local procedure CheckInvoice(SalesInvoiceHeader: Record "Sales Invoice Header") @@ -706,20 +1320,4 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" end; end; - local procedure EnsureCountryRegionExists(CountryCode: Code[10]) - var - CountryRegion: Record "Country/Region"; - begin - if not CountryRegion.Get(CountryCode) then begin - CountryRegion.Init(); - CountryRegion.Code := CountryCode; - CountryRegion.Name := CountryCode; - CountryRegion."ISO Code" := CountryCode; - CountryRegion.Insert(true); - end else - if CountryRegion."ISO Code" = '' then begin - CountryRegion."ISO Code" := CountryCode; - CountryRegion.Modify(true); - end; - end; } diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al index 604fba02221..53a1fea0272 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al @@ -25,6 +25,7 @@ permissionset 6105 "E-Doc. Core - User" IncludedPermissionSets = "E-Doc. Core - Read"; Permissions = + codeunit "E-Document Message API" = X, tabledata "E-Document" = iMD, #region Service tabledata "E-Document Service" = im, diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al new file mode 100644 index 00000000000..60c5089b3a0 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------------------------------ +// 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.Processing.Message; + +using Microsoft.eServices.EDocument; +using System.Utilities; + +/// +/// Provides public operations for E-Document messages. +/// +codeunit 6532 "E-Document Message API" +{ + Access = Public; + InherentEntitlements = X; + + /// + /// Creates an E-Document message with an explicit direction and stores its payload. + /// + /// The E-Document for which to create the message. + /// The type of E-Document message to create. + /// The direction of the message. + /// The message payload. + /// The entry number of the created E-Document message. + procedure CreateMessage(EDocument: Record "E-Document"; MessageType: Enum "E-Document Message Type"; Direction: Enum "E-Document Direction"; var TempBlob: Codeunit "Temp Blob"): Integer + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + exit(EDocMessageMgt.CreateMessage(EDocument, MessageType, Direction, TempBlob)); + end; + + /// + /// Loads the payload for the specified E-Document message. + /// + /// The entry number of the E-Document message whose payload to load. + /// The codeunit that receives the message payload. + procedure GetMessageBlob(MessageEntryNo: Integer; var TempBlob: Codeunit "Temp Blob") + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + EDocMessageMgt.GetMessageBlob(MessageEntryNo, TempBlob); + end; +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageType.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageType.Enum.al index 0ba029125cf..d3137fac498 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageType.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageType.Enum.al @@ -17,6 +17,7 @@ enum 6436 "E-Document Message Type" implements IEDocMessageBuilder { Extensible = true; DefaultImplementation = IEDocMessageBuilder = "E-Doc. Unspecified Impl."; + UnknownValueImplementation = IEDocMessageBuilder = "E-Doc. Unspecified Impl."; value(0; Unknown) {