From 1bd93cd3809537267e557d9c4e0eaed7a226d528 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 17 Jul 2026 13:01:49 +0200 Subject: [PATCH 01/77] init commit --- .../app/src/Core/FREInvoiceLifecycle.Table.al | 146 ++++++++++++++++++ .../Core/FREInvoiceLifecycleMgt.Codeunit.al | 107 +++++++++++++ .../FREInvoiceLifecycleProcStatus.Enum.al | 31 ++++ .../Core/FREInvoiceLifecycleStatus.Enum.al | 35 +++++ .../EReportingEDocuments.PageExt.al | 22 +++ .../Extensions/FREInvoiceLifecycles.Page.al | 65 ++++++++ .../EReportingFREdit.PermissionSet.al | 15 ++ .../EReportingFRObjects.PermissionSet.al | 15 ++ .../EReportingFRRead.PermissionSet.al | 15 ++ .../test/src/IdentificationTests.Codeunit.al | 142 ++++++++++++++++- 10 files changed, 592 insertions(+), 1 deletion(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycle.Table.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleMgt.Codeunit.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProcStatus.Enum.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleStatus.Enum.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREInvoiceLifecycles.Page.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFREdit.PermissionSet.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRRead.PermissionSet.al 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..c6d27ad02b4 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycle.Table.al @@ -0,0 +1,146 @@ +// ------------------------------------------------------------------------------------------------ +// 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"; + LookupPageId = "FR E-Invoice Lifecycles"; + ReplicateData = false; + + fields + { + field(1; "Entry No."; Integer) + { + AutoIncrement = true; + Caption = 'Entry No.'; + DataClassification = SystemMetadata; + } + field(2; "E-Document Entry No."; Integer) + { + Caption = 'E-Document Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "E-Document"."Entry No"; + } + field(3; "Lifecycle Status"; Enum "FR E-Invoice Lifecycle Status") + { + Caption = 'Lifecycle Status'; + DataClassification = SystemMetadata; + } + field(4; "Source Occurrence ID"; Guid) + { + Caption = 'Source Occurrence ID'; + DataClassification = SystemMetadata; + } + field(5; "Original Occurrence Entry No."; Integer) + { + Caption = 'Original Occurrence Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "FR E-Invoice Lifecycle"."Entry No."; + } + field(6; "Reported Amount"; Decimal) + { + AutoFormatExpression = Rec."Currency Code"; + AutoFormatType = 1; + Caption = 'Reported Amount'; + DataClassification = CustomerContent; + } + field(7; "Currency Code"; Code[10]) + { + Caption = 'Currency Code'; + DataClassification = CustomerContent; + TableRelation = Currency.Code; + } + field(8; "Event Date"; Date) + { + Caption = 'Event Date'; + DataClassification = CustomerContent; + } + field(9; "Invoice Cust. Ledger Entry No."; Integer) + { + Caption = 'Invoice Customer Ledger Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "Cust. Ledger Entry"."Entry No."; + } + field(10; "Payment Cust. Ledger Entry No."; Integer) + { + Caption = 'Payment Customer Ledger Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "Cust. Ledger Entry"."Entry No."; + } + field(11; "Detailed Ledger Entry No."; Integer) + { + Caption = 'Detailed Customer Ledger Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "Detailed Cust. Ledg. Entry"."Entry No."; + } + field(12; "E-Document Message Entry No."; Integer) + { + Caption = 'E-Document Message Entry No.'; + DataClassification = SystemMetadata; + } + field(13; "Processing Status"; Enum "FR E-Invoice Lifecycle Proc.") + { + Caption = 'Processing Status'; + DataClassification = SystemMetadata; + } + field(14; "Created At"; DateTime) + { + Caption = 'Created At'; + DataClassification = SystemMetadata; + } + field(15; "Last Error"; Text[2048]) + { + Caption = 'Last Error'; + DataClassification = CustomerContent; + } + } + + 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") + { + } + } + + trigger OnModify() + begin + TestImmutableFields(); + end; + + local procedure TestImmutableFields() + begin + if (Rec."E-Document Entry No." <> xRec."E-Document Entry No.") or + (Rec."Lifecycle Status" <> xRec."Lifecycle Status") or + (Rec."Source Occurrence ID" <> xRec."Source Occurrence ID") or + (Rec."Original Occurrence Entry No." <> xRec."Original Occurrence Entry No.") or + (Rec."Reported Amount" <> xRec."Reported Amount") or + (Rec."Currency Code" <> xRec."Currency Code") or + (Rec."Event Date" <> xRec."Event Date") or + (Rec."Invoice Cust. Ledger Entry No." <> xRec."Invoice Cust. Ledger Entry No.") or + (Rec."Payment Cust. Ledger Entry No." <> xRec."Payment Cust. Ledger Entry No.") or + (Rec."Detailed Ledger Entry No." <> xRec."Detailed Ledger Entry No.") or + (Rec."Created At" <> xRec."Created At") + then + Error(ImmutableOccurrenceErr); + end; + + var + 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/FREInvoiceLifecycleMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleMgt.Codeunit.al new file mode 100644 index 00000000000..d4dbda84647 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleMgt.Codeunit.al @@ -0,0 +1,107 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +codeunit 10971 "FR E-Invoice Lifecycle Mgt." +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata "FR E-Invoice Lifecycle" = rim; + + 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" + begin + 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(); + FREInvoiceLifecycle.Insert(); + 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"; + begin + EDocument.Get(EDocumentEntryNo); + if IsNullGuid(SourceOccurrenceID) then + Error(SourceOccurrenceIDErr); + if EventDate = 0D then + Error(EventDateErr); + + case LifecycleStatus of + LifecycleStatus::Collected: + begin + if ReportedAmount <= 0 then + Error(CollectedAmountErr); + if OriginalOccurrenceEntryNo <> 0 then + Error(CollectedOriginalOccurrenceErr); + end; + LifecycleStatus::"Negative Collected": + begin + if ReportedAmount >= 0 then + Error(NegativeCollectedAmountErr); + if not OriginalOccurrence.Get(OriginalOccurrenceEntryNo) then + Error(OriginalOccurrenceErr); + OriginalOccurrence.TestField("E-Document Entry No.", EDocumentEntryNo); + OriginalOccurrence.TestField("Lifecycle Status", OriginalOccurrence."Lifecycle Status"::Collected); + if ReportedAmount <> -OriginalOccurrence."Reported Amount" then + Error(ReversalAmountErr); + end; + else + Error(PaymentStatusErr, LifecycleStatus); + 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 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'; + ConflictingReplayErr: Label 'The payment lifecycle occurrence was already captured with different values.'; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProcStatus.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProcStatus.Enum.al new file mode 100644 index 00000000000..61b5903c25c --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProcStatus.Enum.al @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------------------------------ +// 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(3; Sent) + { + Caption = 'Sent'; + } + 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..2870b912288 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleStatus.Enum.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; + +enum 10973 "FR E-Invoice Lifecycle Status" +{ + Extensible = false; + + value(0; Collected) + { + Caption = 'Collected'; + } + value(1; "Negative Collected") + { + Caption = 'Negative Collected'; + } + value(2; Refused) + { + Caption = 'Refused'; + } + value(3; Submitted) + { + Caption = 'Submitted'; + } + value(4; Rejected) + { + Caption = 'Rejected'; + } + value(5; Accepted) + { + Caption = 'Accepted'; + } +} \ 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..a30e75a4bc0 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 @@ -20,4 +20,26 @@ pageextension 10974 "E-Reporting E-Documents" extends "E-Documents" } } } + + 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.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/FREInvoiceLifecycles.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREInvoiceLifecycles.Page.al new file mode 100644 index 00000000000..2dbafd32aee --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREInvoiceLifecycles.Page.al @@ -0,0 +1,65 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + PageType = List; + SourceTable = "FR E-Invoice Lifecycle"; + UsageCategory = History; + + layout + { + area(Content) + { + repeater(Lifecycles) + { + field("E-Document Entry No."; Rec."E-Document Entry No.") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the related e-document entry.'; + } + field("Lifecycle Status"; Rec."Lifecycle Status") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the French electronic invoice lifecycle status.'; + } + field("Reported Amount"; Rec."Reported Amount") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the amount reported for this lifecycle occurrence.'; + } + field("Currency Code"; Rec."Currency Code") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the currency of the reported amount.'; + } + field("Event Date"; Rec."Event Date") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the date of the lifecycle event.'; + } + field("Processing Status"; Rec."Processing Status") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the processing status of the lifecycle occurrence.'; + } + field("Created At"; Rec."Created At") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies when the lifecycle occurrence was created.'; + } + field("Last Error"; Rec."Last Error") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the last processing error for the lifecycle occurrence.'; + } + } + } + } +} \ No newline at end of file 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..33710e09a24 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFREdit.PermissionSet.al @@ -0,0 +1,15 @@ +// ------------------------------------------------------------------------------------------------ +// 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 = Internal; + Assignable = false; + + IncludedPermissionSets = "E-Reporting FR - Read"; + + Permissions = tabledata "FR E-Invoice Lifecycle" = IM; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al new file mode 100644 index 00000000000..8c89fed6746 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al @@ -0,0 +1,15 @@ +// ------------------------------------------------------------------------------------------------ +// 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 10970 "E-Reporting FR - Objects" +{ + Access = Internal; + Assignable = false; + + Permissions = table "FR E-Invoice Lifecycle" = X, + codeunit "FR E-Invoice Lifecycle Mgt." = X, + page "FR E-Invoice Lifecycles" = X; +} \ No newline at end of file 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..0234b8e70df --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRRead.PermissionSet.al @@ -0,0 +1,15 @@ +// ------------------------------------------------------------------------------------------------ +// 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 = Internal; + Assignable = false; + + IncludedPermissionSets = "E-Reporting FR - Objects"; + + Permissions = tabledata "FR E-Invoice Lifecycle" = R; +} \ No newline at end of file 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..67ac68aeb80 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,16 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Formats.Test; +using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Formats; using Microsoft.Foundation.Company; codeunit 148146 "Identification Tests" { Subtype = Test; - Permissions = tabledata "Company Information" = rimd; + Permissions = tabledata "Company Information" = rimd, + tabledata "E-Document" = rimd, + tabledata "FR E-Invoice Lifecycle" = rimd; trigger OnRun() begin @@ -122,4 +125,141 @@ codeunit 148146 "Identification Tests" CompanyInformation."SIRET No." := OriginalSIRETNo; CompanyInformation.Modify(); 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 + // [SCENARIO] A payment application is captured as an immutable Collected lifecycle occurrence + CreateEDocument(EDocument); + SourceOccurrenceID := CreateGuid(); + + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, SourceOccurrenceID, + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + 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", 'The 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 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 + // [SCENARIO] Replaying the same payment event does not create a duplicate occurrence + CreateEDocument(EDocument); + SourceOccurrenceID := CreateGuid(); + FirstLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, SourceOccurrenceID, + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + ReplayedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, SourceOccurrenceID, + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + 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 + // [SCENARIO] A replay with the same identity but different regulatory values is rejected + CreateEDocument(EDocument); + SourceOccurrenceID := CreateGuid(); + FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, SourceOccurrenceID, + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + asserterror FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, SourceOccurrenceID, + 1200, 'EUR', WorkDate(), 0, 0, 0, 0); + + 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 + // [SCENARIO] Unapplication creates a separate Negative Collected occurrence linked to Collected + CreateEDocument(EDocument); + CollectedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + NegativeCollectedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::"Negative Collected", CreateGuid(), + -1250, 'EUR', WorkDate(), 0, 0, 0, CollectedLifecycle."Entry No."); + + 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 + // [SCENARIO] Negative Collected cannot reverse a different amount than its original occurrence + CreateEDocument(EDocument); + CollectedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + asserterror FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::"Negative Collected", CreateGuid(), + -1200, 'EUR', WorkDate(), 0, 0, 0, CollectedLifecycle."Entry No."); + + Assert.ExpectedError('A Negative Collected occurrence must exactly reverse the reported amount of the original Collected occurrence.'); + end; + + [Test] + procedure CapturedOccurrenceRejectsRegulatoryValueChanges() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [SCENARIO] Captured regulatory values cannot be changed in place + CreateEDocument(EDocument); + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + FREInvoiceLifecycle."Reported Amount" := 1200; + asserterror FREInvoiceLifecycle.Modify(); + + Assert.ExpectedError('The regulatory identity and values of a French electronic invoice lifecycle occurrence cannot be changed.'); + 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; } From 61b2627bec4a71c836a3d5d529aa4565c73b5ea7 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 17 Jul 2026 13:15:27 +0200 Subject: [PATCH 02/77] phase 2 --- .../app/src/Core/FREInvoiceLifecycle.Table.al | 3 + .../Core/FREInvoiceLifecycleMgt.Codeunit.al | 99 ++++++++++++ .../test/src/IdentificationTests.Codeunit.al | 145 ++++++++++++++++++ 3 files changed, 247 insertions(+) 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 index c6d27ad02b4..fa2c38e138f 100644 --- 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 @@ -117,6 +117,9 @@ table 10970 "FR E-Invoice Lifecycle" key(EDocument; "E-Document Entry No.", "Created At") { } + key(DetailedLedgerEntry; "Lifecycle Status", "Detailed Ledger Entry No.") + { + } } trigger OnModify() 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 index d4dbda84647..f6e33876560 100644 --- 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 @@ -5,6 +5,10 @@ namespace Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument; +using Microsoft.Finance.GeneralLedger.Journal; +using Microsoft.Finance.GeneralLedger.Posting; +using Microsoft.Sales.History; +using Microsoft.Sales.Receivables; codeunit 10971 "FR E-Invoice Lifecycle Mgt." { @@ -13,6 +17,59 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." InherentPermissions = X; Permissions = tabledata "FR E-Invoice Lifecycle" = rim; + [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"; + 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 + 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); + 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"; + begin + if not IsInvoiceApplication(OldDetailedCustLedgEntry) then + exit; + if not FindCollectedOccurrence(CollectedLifecycle, OldDetailedCustLedgEntry."Entry No.") then + exit; + + 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."); + 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" begin ValidatePaymentOccurrence(EDocumentEntryNo, LifecycleStatus, SourceOccurrenceID, ReportedAmount, EventDate, OriginalOccurrenceEntryNo); @@ -81,6 +138,48 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." exit(FREInvoiceLifecycle.FindFirst()); end; + local procedure FindCollectedOccurrence(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.FindFirst()); + 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"; + begin + exit(EDocumentService.Get(EDocument.Service) 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 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 67ac68aeb80..bf80993f1c8 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 @@ -6,13 +6,20 @@ namespace Microsoft.eServices.EDocument.Formats.Test; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Formats; +using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Foundation.Company; +using Microsoft.Sales.History; +using Microsoft.Sales.Receivables; codeunit 148146 "Identification Tests" { Subtype = Test; Permissions = tabledata "Company Information" = rimd, tabledata "E-Document" = rimd, + tabledata "E-Document Service" = rimd, + tabledata "Sales Invoice Header" = rimd, + tabledata "Cust. Ledger Entry" = rimd, + tabledata "Detailed Cust. Ledg. Entry" = rimd, tabledata "FR E-Invoice Lifecycle" = rimd; trigger OnRun() @@ -254,6 +261,71 @@ codeunit 148146 "Identification Tests" Assert.ExpectedError('The regulatory identity and values of a French electronic invoice lifecycle occurrence cannot be changed.'); 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 + // [SCENARIO] Applying a payment to a French electronic invoice captures a Collected occurrence + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + 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(1250, FREInvoiceLifecycle."Reported Amount", '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.'); + 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 + // [SCENARIO] Applying a payment to an electronic invoice in a non-French format creates no French lifecycle occurrence + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"PEPPOL BIS 3.0"); + + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + Assert.RecordIsEmpty(FREInvoiceLifecycle); + 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"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [SCENARIO] Unapplying a captured payment creates an exact linked Negative Collected occurrence + 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); + + FREInvoiceLifecycleMgt.ProcessDetailedLedgerUnapplication(DetailedCustLedgEntry, NewDetailedCustLedgEntry); + + 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.'); + end; + local procedure CreateEDocument(var EDocument: Record "E-Document") begin EDocument.Init(); @@ -262,4 +334,77 @@ codeunit 148146 "Identification Tests" EDocument.Direction := EDocument.Direction::Outgoing; EDocument.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"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; + PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; + DocumentNo: 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; + 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.Insert(); + + InvoiceCustLedgerEntry."Entry No." := GetNextCustLedgerEntryNo(); + InvoiceCustLedgerEntry."Document Type" := InvoiceCustLedgerEntry."Document Type"::Invoice; + InvoiceCustLedgerEntry."Document No." := DocumentNo; + InvoiceCustLedgerEntry.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 := -1250; + 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; } From ad43c025e398fed96a4c68fd8b05ecc101ddb78b Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 17 Jul 2026 13:34:42 +0200 Subject: [PATCH 03/77] new phase changes --- .../Core/FREInvoiceLifecycleError.Codeunit.al | 21 +++++ .../Core/FREInvoiceLifecycleMgt.Codeunit.al | 63 +++++++++++-- .../Core/FREInvoiceLifecycleMsg.Codeunit.al | 93 +++++++++++++++++++ .../FREInvoiceLifecycleWorker.Codeunit.al | 20 ++++ .../EDocumentMessageType.EnumExt.al | 17 ++++ .../Extensions/FREInvoiceLifecycles.Page.al | 34 +++++++ .../EReportingFRObjects.PermissionSet.al | 3 + .../test/src/IdentificationTests.Codeunit.al | 52 +++++++++++ 8 files changed, 294 insertions(+), 9 deletions(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleError.Codeunit.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleMsg.Codeunit.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleWorker.Codeunit.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EDocumentMessageType.EnumExt.al 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..d8de6db0c9a --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleError.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 10985 "FR E-Invoice Lifecycle Error" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata "FR E-Invoice Lifecycle" = m; + TableNo = "FR E-Invoice Lifecycle"; + + trigger OnRun() + begin + Rec."Processing Status" := Rec."Processing Status"::Failed; + Rec."Last Error" := CopyStr(GetLastErrorText(), 1, MaxStrLen(Rec."Last Error")); + Rec.Modify(); + end; +} \ No newline at end of file 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 index f6e33876560..da77de0e7f2 100644 --- 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 @@ -5,10 +5,12 @@ namespace Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Finance.GeneralLedger.Posting; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; +using System.Utilities; codeunit 10971 "FR E-Invoice Lifecycle Mgt." { @@ -32,6 +34,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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 @@ -47,27 +50,33 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." exit; repeat - if IsFREInvoiceEDocument(EDocument) then - CapturePaymentOccurrence( + 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 FindCollectedOccurrence(CollectedLifecycle, OldDetailedCustLedgEntry."Entry No.") then + if not FindCollectedOccurrences(CollectedLifecycle, OldDetailedCustLedgEntry."Entry No.") then exit; - 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."); + 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" @@ -95,6 +104,32 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." FREInvoiceLifecycle.Insert(); end; + internal procedure CreateLifecycleMessage(var FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + var + EDocument: Record "E-Document"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + 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." := EDocMessageMgt.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(); + 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"; @@ -138,11 +173,21 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." exit(FREInvoiceLifecycle.FindFirst()); end; - local procedure FindCollectedOccurrence(var FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; DetailedLedgerEntryNo: Integer): Boolean + 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.FindFirst()); + exit(FREInvoiceLifecycle.FindSet()); + end; + + local procedure ScheduleMessageCreation(var FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + begin + 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); end; local procedure FindInvoiceEDocuments(var EDocument: Record "E-Document"; InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"): Boolean 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..7b24970a283 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleMsg.Codeunit.al @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------------------------------ +// 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 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"; + begin + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceLifecycle.SetRange("E-Document Message Entry No.", 0); + if not FREInvoiceLifecycle.FindFirst() then + Error(NoCapturedOccurrenceErr, EDocument."Entry No"); + + 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('InvoiceLifecycleMessage'); + RootElement.Add(XmlElement.Create('MessageID', '', Format(FREInvoiceLifecycle."Source Occurrence ID"))); + RootElement.Add(XmlElement.Create('InvoiceID', '', EDocument."Document No.")); + RootElement.Add(XmlElement.Create('Status', '', LifecycleStatusCode(FREInvoiceLifecycle."Lifecycle Status"))); + RootElement.Add(XmlElement.Create('EventDate', '', Format(FREInvoiceLifecycle."Event Date", 0, '--'))); + RootElement.Add(BuildAmountElement(FREInvoiceLifecycle)); + if FREInvoiceLifecycle."Original Occurrence Entry No." <> 0 then + RootElement.Add(XmlElement.Create('OriginalOccurrenceID', '', GetOriginalOccurrenceID(FREInvoiceLifecycle."Original Occurrence Entry No."))); + end; + + local procedure BuildAmountElement(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") AmountElement: XmlElement + begin + AmountElement := XmlElement.Create('Amount', '', Format(FREInvoiceLifecycle."Reported Amount", 0, 9)); + AmountElement.Add(XmlAttribute.Create('currencyID', FREInvoiceLifecycle."Currency Code")); + end; + + local procedure GetOriginalOccurrenceID(OriginalOccurrenceEntryNo: Integer): Text + var + OriginalOccurrence: Record "FR E-Invoice Lifecycle"; + begin + OriginalOccurrence.Get(OriginalOccurrenceEntryNo); + exit(Format(OriginalOccurrence."Source Occurrence ID")); + end; + + local procedure LifecycleStatusCode(LifecycleStatus: Enum "FR E-Invoice Lifecycle Status"): Text + begin + case LifecycleStatus of + LifecycleStatus::Collected: + exit('COLLECTED'); + LifecycleStatus::"Negative Collected": + exit('NEGATIVE_COLLECTED'); + LifecycleStatus::Refused: + exit('REFUSED'); + LifecycleStatus::Submitted: + exit('SUBMITTED'); + LifecycleStatus::Rejected: + exit('REJECTED'); + LifecycleStatus::Accepted: + exit('ACCEPTED'); + end; + end; + + var + NoCapturedOccurrenceErr: Label 'No unprocessed French invoice lifecycle occurrence exists for E-Document entry %1.', Comment = '%1 = E-Document entry number'; +} \ 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..84529453299 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleWorker.Codeunit.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; + +codeunit 10976 "FR E-Invoice Lifecycle Worker" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + TableNo = "FR E-Invoice Lifecycle"; + + trigger OnRun() + var + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + FREInvoiceLifecycleMgt.CreateLifecycleMessage(Rec); + end; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EDocumentMessageType.EnumExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EDocumentMessageType.EnumExt.al new file mode 100644 index 00000000000..aa36f5a9343 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EDocumentMessageType.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 10971 "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/FREInvoiceLifecycles.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREInvoiceLifecycles.Page.al index 2dbafd32aee..be1cfc8087a 100644 --- 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 @@ -49,6 +49,11 @@ page 10970 "FR E-Invoice Lifecycles" ApplicationArea = Basic, Suite; ToolTip = 'Specifies the processing status of the lifecycle occurrence.'; } + field("E-Document Message Entry No."; Rec."E-Document Message Entry No.") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the related E-Document message entry.'; + } field("Created At"; Rec."Created At") { ApplicationArea = Basic, Suite; @@ -62,4 +67,33 @@ page 10970 "FR E-Invoice Lifecycles" } } } + + actions + { + area(Processing) + { + action(RetryMessageCreation) + { + ApplicationArea = Basic, Suite; + Caption = 'Retry Message Creation'; + Enabled = Rec."Processing Status" = Rec."Processing Status"::Failed; + Image = Reopen; + 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/Permissions/EReportingFRObjects.PermissionSet.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al index 8c89fed6746..e4c6c83a2be 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al @@ -11,5 +11,8 @@ permissionset 10970 "E-Reporting FR - Objects" Permissions = table "FR E-Invoice Lifecycle" = X, codeunit "FR E-Invoice Lifecycle Mgt." = X, + codeunit "FR E-Invoice Lifecycle Msg." = X, + codeunit "FR E-Invoice Lifecycle Worker" = X, + codeunit "FR E-Invoice Lifecycle Error" = X, page "FR E-Invoice Lifecycles" = X; } \ No newline at end of file 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 bf80993f1c8..c39088897cf 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 @@ -6,10 +6,12 @@ namespace Microsoft.eServices.EDocument.Formats.Test; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Formats; +using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Foundation.Company; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; +using System.Utilities; codeunit 148146 "Identification Tests" { @@ -326,6 +328,56 @@ codeunit 148146 "Identification Tests" Assert.AreEqual(NewDetailedCustLedgEntry."Entry No.", NegativeCollectedLifecycle."Detailed Ledger Entry No.", 'The unapplication detail entry must be retained.'); end; + [Test] + procedure CreateLifecycleMessageStoresPayloadThroughEDocumentMessageMgt() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + TempBlob: Codeunit "Temp Blob"; + InStream: InStream; + XmlDoc: XmlDocument; + StatusNode: XmlNode; + begin + // [SCENARIO] A captured occurrence creates and links a PR 8698 E-Document Message payload + CreateEDocument(EDocument); + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + + FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); + + 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.'); + EDocMessageMgt.GetMessageBlob(FREInvoiceLifecycle."E-Document Message Entry No.", TempBlob); + TempBlob.CreateInStream(InStream); + XmlDocument.ReadFrom(InStream, XmlDoc); + Assert.IsTrue(XmlDoc.SelectSingleNode('/InvoiceLifecycleMessage/Status', StatusNode), 'The payload must contain the lifecycle status.'); + Assert.AreEqual('COLLECTED', StatusNode.AsXmlElement().InnerText(), 'The payload must map Collected to its wire code.'); + end; + + [Test] + procedure CreateLifecycleMessageIsIdempotent() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + MessageEntryNo: Integer; + begin + // [SCENARIO] Retrying message creation does not create or link a second message + CreateEDocument(EDocument); + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); + MessageEntryNo := FREInvoiceLifecycle."E-Document Message Entry No."; + + FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); + + Assert.AreEqual(MessageEntryNo, FREInvoiceLifecycle."E-Document Message Entry No.", 'A retry must retain the existing message link.'); + end; + local procedure CreateEDocument(var EDocument: Record "E-Document") begin EDocument.Init(); From ff48b53f29720b630fad805a638ce6caa8980819 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 17 Jul 2026 13:37:36 +0200 Subject: [PATCH 04/77] update --- .../Core/FREInvoiceLifecycleMgt.Codeunit.al | 19 ++++++++ .../test/src/IdentificationTests.Codeunit.al | 45 +++++++++++++++++++ 2 files changed, 64 insertions(+) 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 index da77de0e7f2..4763915a561 100644 --- 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 @@ -8,6 +8,7 @@ using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Finance.GeneralLedger.Posting; +using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; using System.Utilities; @@ -81,6 +82,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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" begin + CurrencyCode := ResolveCurrencyCode(CurrencyCode); ValidatePaymentOccurrence(EDocumentEntryNo, LifecycleStatus, SourceOccurrenceID, ReportedAmount, EventDate, OriginalOccurrenceEntryNo); if FindOccurrence(FREInvoiceLifecycle, EDocumentEntryNo, LifecycleStatus, SourceOccurrenceID) then begin @@ -182,6 +184,11 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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(); @@ -190,6 +197,18 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." CompanyName(), CurrentDateTime(), FREInvoiceLifecycle.RecordId); 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"; 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 c39088897cf..ab2c0e5785a 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 @@ -8,6 +8,7 @@ using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.Finance.GeneralLedger.Journal; +using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Foundation.Company; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; @@ -158,6 +159,26 @@ codeunit 148146 "Identification Tests" Assert.IsTrue(FREInvoiceLifecycle."Created At" <> 0DT, 'The creation timestamp must be populated.'); end; + [Test] + procedure CaptureCollectedOccurrenceUsesLCYForBlankCurrency() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + GeneralLedgerSetup: Record "General Ledger Setup"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [SCENARIO] A local-currency payment occurrence stores the configured LCY code + GeneralLedgerSetup.Get(); + GeneralLedgerSetup.TestField("LCY Code"); + CreateEDocument(EDocument); + + FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, '', WorkDate(), 0, 0, 0, 0); + + Assert.AreEqual(GeneralLedgerSetup."LCY Code", FREInvoiceLifecycle."Currency Code", 'A blank ledger currency must resolve to the LCY code.'); + end; + [Test] procedure CaptureCollectedOccurrenceReplayReturnsExistingLifecycle() var @@ -301,6 +322,30 @@ codeunit 148146 "Identification Tests" 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 + // [SCENARIO] Replaying an application whose lifecycle message exists does not requeue the occurrence + 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."; + + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + 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 DetailedUnapplicationCapturesLinkedNegativeCollected() var From 4c75db9cf48cde1e99ef51427daa5670c4d0eaf7 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 17 Jul 2026 15:12:41 +0200 Subject: [PATCH 05/77] more changes --- .../Core/FREInvoiceLifecycleMsg.Codeunit.al | 2 + .../D365BasicEReportingFR.PermissionSetExt.al | 12 + .../D365ReadEReportingFR.PermissionSetExt.al | 12 + ...TeamMemberEReportingFR.PermissionSetExt.al | 12 + .../LocalEReportingFR.PermissionSetExt.al | 12 + .../LocalReadEReportingFR.PermissionSetExt.al | 12 + .../test/src/IdentificationTests.Codeunit.al | 216 +++++++++++++++++- 7 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365BasicEReportingFR.PermissionSetExt.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365ReadEReportingFR.PermissionSetExt.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365TeamMemberEReportingFR.PermissionSetExt.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalEReportingFR.PermissionSetExt.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalReadEReportingFR.PermissionSetExt.al 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 index 7b24970a283..1460d675ecf 100644 --- 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 @@ -19,8 +19,10 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder var FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; begin + FREInvoiceLifecycle.SetCurrentKey("E-Document Entry No.", "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 Error(NoCapturedOccurrenceErr, EDocument."Entry No"); diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365BasicEReportingFR.PermissionSetExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365BasicEReportingFR.PermissionSetExt.al new file mode 100644 index 00000000000..1fbf370a76d --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365BasicEReportingFR.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 System.Security.AccessControl; + +permissionsetextension 10976 "D365 BASIC - E-Reporting FR" extends "D365 BASIC" +{ + IncludedPermissionSets = "E-Reporting FR - Edit"; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365ReadEReportingFR.PermissionSetExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365ReadEReportingFR.PermissionSetExt.al new file mode 100644 index 00000000000..61d31660b65 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365ReadEReportingFR.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 System.Security.AccessControl; + +permissionsetextension 10975 "D365 READ - E-Reporting FR" extends "D365 READ" +{ + IncludedPermissionSets = "E-Reporting FR - Read"; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365TeamMemberEReportingFR.PermissionSetExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365TeamMemberEReportingFR.PermissionSetExt.al new file mode 100644 index 00000000000..95514bc3b8c --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365TeamMemberEReportingFR.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 System.Security.AccessControl; + +permissionsetextension 10977 "D365 TEAM MEMBER - ER FR" extends "D365 TEAM MEMBER" +{ + IncludedPermissionSets = "E-Reporting FR - Read"; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalEReportingFR.PermissionSetExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalEReportingFR.PermissionSetExt.al new file mode 100644 index 00000000000..7e76e64eeb0 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalEReportingFR.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 System.Security.AccessControl; + +permissionsetextension 10978 "LOCAL - E-Reporting FR" extends LOCAL +{ + IncludedPermissionSets = "E-Reporting FR - Edit"; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalReadEReportingFR.PermissionSetExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalReadEReportingFR.PermissionSetExt.al new file mode 100644 index 00000000000..9f079a29995 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalReadEReportingFR.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 System.Security.AccessControl; + +permissionsetextension 10979 "LOCAL READ - E-Reporting FR" extends "LOCAL READ" +{ + IncludedPermissionSets = "E-Reporting FR - Read"; +} \ No newline at end of file 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 ab2c0e5785a..1260408a834 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 @@ -10,8 +10,10 @@ using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Foundation.Company; +using Microsoft.Sales.Document; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; +using Microsoft.Sales.Setup; using System.Utilities; codeunit 148146 "Identification Tests" @@ -20,6 +22,7 @@ codeunit 148146 "Identification Tests" Permissions = tabledata "Company Information" = rimd, tabledata "E-Document" = rimd, tabledata "E-Document Service" = rimd, + tabledata "General Ledger Setup" = rimd, tabledata "Sales Invoice Header" = rimd, tabledata "Cust. Ledger Entry" = rimd, tabledata "Detailed Cust. Ledg. Entry" = rimd, @@ -32,7 +35,12 @@ codeunit 148146 "Identification Tests" var Assert: Codeunit Assert; + LibraryERM: Codeunit "Library - ERM"; + LibrarySales: Codeunit "Library - Sales"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + LibraryUtility: Codeunit "Library - Utility"; EDocHelpers: Codeunit "EDoc. Helpers"; + IsInitialized: Boolean; [Test] procedure CheckSIRENNotEmptyRaisesErrorWhenEmpty() @@ -169,7 +177,10 @@ codeunit 148146 "Identification Tests" begin // [SCENARIO] A local-currency payment occurrence stores the configured LCY code GeneralLedgerSetup.Get(); - GeneralLedgerSetup.TestField("LCY Code"); + if GeneralLedgerSetup."LCY Code" = '' then begin + GeneralLedgerSetup."LCY Code" := 'EUR'; + GeneralLedgerSetup.Modify(true); + end; CreateEDocument(EDocument); FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( @@ -346,6 +357,45 @@ codeunit 148146 "Identification Tests" 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 + // [SCENARIO] Replaying an application while message creation is queued does not duplicate the occurrence + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + 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 + // [SCENARIO] Applying a payment creates an occurrence for every eligible E-Document of the invoice + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + CreateAdditionalEDocument(AdditionalEDocument, EDocument); + + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + FREInvoiceLifecycle.SetRange("Source Occurrence ID", DetailedCustLedgEntry.SystemId); + Assert.RecordCount(FREInvoiceLifecycle, 2); + end; + [Test] procedure DetailedUnapplicationCapturesLinkedNegativeCollected() var @@ -423,6 +473,159 @@ codeunit 148146 "Identification Tests" Assert.AreEqual(MessageEntryNo, FREInvoiceLifecycle."E-Document Message Entry No.", 'A retry must retain the existing message link.'); end; + [Test] + procedure CreateNegativeCollectedMessageIncludesOriginalOccurrence() + var + EDocument: Record "E-Document"; + CollectedLifecycle: Record "FR E-Invoice Lifecycle"; + NegativeCollectedLifecycle: Record "FR E-Invoice Lifecycle"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + TempBlob: Codeunit "Temp Blob"; + InStream: InStream; + XmlDoc: XmlDocument; + OriginalOccurrenceNode: XmlNode; + StatusNode: XmlNode; + begin + // [SCENARIO] A Negative Collected message identifies its status and original Collected occurrence + CreateEDocument(EDocument); + CollectedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), + 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + NegativeCollectedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( + EDocument."Entry No", "FR E-Invoice Lifecycle Status"::"Negative Collected", CreateGuid(), + -1250, 'EUR', WorkDate() + 1, 0, 0, 0, CollectedLifecycle."Entry No."); + + FREInvoiceLifecycleMgt.CreateLifecycleMessage(NegativeCollectedLifecycle); + + EDocMessageMgt.GetMessageBlob(NegativeCollectedLifecycle."E-Document Message Entry No.", TempBlob); + TempBlob.CreateInStream(InStream); + XmlDocument.ReadFrom(InStream, XmlDoc); + Assert.IsTrue(XmlDoc.SelectSingleNode('/InvoiceLifecycleMessage/Status', StatusNode), 'The payload must contain the lifecycle status.'); + Assert.AreEqual('NEGATIVE_COLLECTED', StatusNode.AsXmlElement().InnerText(), 'The payload must map Negative Collected to its wire code.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('/InvoiceLifecycleMessage/OriginalOccurrenceID', OriginalOccurrenceNode), 'The payload must contain the original occurrence ID.'); + Assert.AreEqual(Format(CollectedLifecycle."Source Occurrence ID"), OriginalOccurrenceNode.AsXmlElement().InnerText(), 'The payload must identify the original Collected occurrence.'); + end; + + [Test] + procedure RetryFailedLifecycleMessageQueuesOccurrence() + var + EDocument: Record "E-Document"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [SCENARIO] Retrying failed message creation queues the occurrence and clears its 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(); + + FREInvoiceLifecycleMgt.RetryLifecycleMessage(FREInvoiceLifecycle); + + 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 PostedPaymentApplicationCreatesCollectedLifecycle() + var + SalesHeader: Record "Sales Header"; + 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"; + 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] A posted sales invoice "SI" with an outgoing Factur-X FR E-Document + LibrarySales.CreateSalesInvoice(SalesHeader); + 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.'); + end; + + local procedure Initialize() + var + GeneralLedgerSetup: Record "General Ledger Setup"; + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Identification Tests"); + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Identification Tests"); + + GeneralLedgerSetup.Get(); + if GeneralLedgerSetup."LCY Code" = '' then begin + GeneralLedgerSetup."LCY Code" := 'EUR'; + GeneralLedgerSetup.Modify(true); + end; + + 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"; + begin + EDocumentService.Code := CopyStr(CreateGuid(), 1, MaxStrLen(EDocumentService.Code)); + EDocumentService."Document Format" := "E-Document Format"::"Factur-X FR"; + 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.Insert(); + end; + local procedure CreateEDocument(var EDocument: Record "E-Document") begin EDocument.Init(); @@ -432,6 +635,17 @@ codeunit 148146 "Identification Tests" EDocument.Insert(); end; + local procedure CreateAdditionalEDocument(var AdditionalEDocument: Record "E-Document"; EDocument: Record "E-Document") + 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.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"; From 9fe2fe0e8f6d705f3369965d6e3b78a83f3e712e Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 17 Jul 2026 15:49:40 +0200 Subject: [PATCH 06/77] new --- .../app/src/Core/FREInvoiceLifecycle.Table.al | 5 + .../Core/FREInvoiceLifecycleMgt.Codeunit.al | 140 +++++++++++++++- .../Core/FREInvoiceLifecycleMsg.Codeunit.al | 149 ++++++++++++++---- .../src/Core/FREInvoiceLifecycleVAT.Table.al | 78 +++++++++ .../EReportingFREdit.PermissionSet.al | 3 +- .../EReportingFRObjects.PermissionSet.al | 1 + .../EReportingFRRead.PermissionSet.al | 3 +- .../test/src/IdentificationTests.Codeunit.al | 135 ++++++++++++++-- 8 files changed, 470 insertions(+), 44 deletions(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleVAT.Table.al 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 index fa2c38e138f..0b1d300f056 100644 --- 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 @@ -127,6 +127,11 @@ table 10970 "FR E-Invoice Lifecycle" TestImmutableFields(); end; + trigger OnDelete() + begin + Error(ImmutableOccurrenceErr); + end; + local procedure TestImmutableFields() begin if (Rec."E-Document Entry No." <> xRec."E-Document Entry No.") or 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 index 4763915a561..bcc3c2ae2b3 100644 --- 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 @@ -6,9 +6,12 @@ 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.VAT.Ledger; +using Microsoft.Finance.VAT.Setup; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; using System.Utilities; @@ -18,7 +21,8 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." Access = Internal; InherentEntitlements = X; InherentPermissions = X; - Permissions = tabledata "FR E-Invoice Lifecycle" = rim; + 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) @@ -104,6 +108,137 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." FREInvoiceLifecycle."Processing Status" := FREInvoiceLifecycle."Processing Status"::Captured; FREInvoiceLifecycle."Created At" := CurrentDateTime(); FREInvoiceLifecycle.Insert(); + + if InvoiceCustLedgerEntryNo <> 0 then + if LifecycleStatus = LifecycleStatus::Collected then + CreateVATBreakdown(FREInvoiceLifecycle) + else + CreateReversalVATBreakdown(FREInvoiceLifecycle, OriginalOccurrenceEntryNo); + end; + + local procedure CreateVATBreakdown(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + 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); + if VATEntry.FindSet() then + repeat + VATPostingSetup.Get(VATEntry."VAT Bus. Posting Group", VATEntry."VAT Prod. Posting Group"); + VATRate := VATPostingSetup."VAT %"; + GrossAmount := GetVATEntryGrossAmount(VATEntry, FREInvoiceLifecycle."Currency Code"); + 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); + 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 + 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)); + + Error(VATEntryCurrencyErr, VATEntry."Entry No.", CurrencyCode); + 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) + 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, FREInvoiceLifecycle."Currency Code"); + 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, FREInvoiceLifecycle."Currency Code"); + 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", OriginalLifecycleVAT."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") @@ -267,4 +402,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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'; 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'; } \ 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 index 1460d675ecf..b177b7d9605 100644 --- 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 @@ -48,48 +48,137 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder local procedure BuildLifecycleElement(EDocument: Record "E-Document"; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") RootElement: XmlElement begin - RootElement := XmlElement.Create('InvoiceLifecycleMessage'); - RootElement.Add(XmlElement.Create('MessageID', '', Format(FREInvoiceLifecycle."Source Occurrence ID"))); - RootElement.Add(XmlElement.Create('InvoiceID', '', EDocument."Document No.")); - RootElement.Add(XmlElement.Create('Status', '', LifecycleStatusCode(FREInvoiceLifecycle."Lifecycle Status"))); - RootElement.Add(XmlElement.Create('EventDate', '', Format(FREInvoiceLifecycle."Event Date", 0, '--'))); - RootElement.Add(BuildAmountElement(FREInvoiceLifecycle)); - if FREInvoiceLifecycle."Original Occurrence Entry No." <> 0 then - RootElement.Add(XmlElement.Create('OriginalOccurrenceID', '', GetOriginalOccurrenceID(FREInvoiceLifecycle."Original Occurrence Entry No."))); + 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)); + + AddExchangedDocumentContext(RootElement); + AddExchangedDocument(RootElement, FREInvoiceLifecycle); + AddAcknowledgementDocument(RootElement, EDocument, FREInvoiceLifecycle); + end; + + local procedure AddExchangedDocumentContext(var RootElement: XmlElement) + var + BusinessProcessElement: XmlElement; + ContextElement: XmlElement; + GuidelineElement: XmlElement; + begin + ContextElement := XmlElement.Create('ExchangedDocumentContext', RsmNamespaceTok); + BusinessProcessElement := XmlElement.Create('BusinessProcessSpecifiedDocumentContextParameter', RamNamespaceTok); + BusinessProcessElement.Add(XmlElement.Create('ID', RamNamespaceTok, RegulatedBusinessProcessTok)); + ContextElement.Add(BusinessProcessElement); + GuidelineElement := XmlElement.Create('GuidelineSpecifiedDocumentContextParameter', RamNamespaceTok); + GuidelineElement.Add(XmlElement.Create('ID', RamNamespaceTok, CDVInvoiceProfileTok)); + ContextElement.Add(GuidelineElement); + RootElement.Add(ContextElement); end; - local procedure BuildAmountElement(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") AmountElement: XmlElement + local procedure AddExchangedDocument(var RootElement: XmlElement; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + var + ExchangedDocumentElement: XmlElement; + IssueDateTimeElement: XmlElement; begin - AmountElement := XmlElement.Create('Amount', '', Format(FREInvoiceLifecycle."Reported Amount", 0, 9)); - AmountElement.Add(XmlAttribute.Create('currencyID', FREInvoiceLifecycle."Currency Code")); + 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); + RootElement.Add(ExchangedDocumentElement); end; - local procedure GetOriginalOccurrenceID(OriginalOccurrenceEntryNo: Integer): Text + local procedure AddAcknowledgementDocument(var RootElement: XmlElement; EDocument: Record "E-Document"; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") var - OriginalOccurrence: Record "FR E-Invoice Lifecycle"; + 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)); + ReferenceDocumentElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, CollectedStatusCodeTok)); + ReferenceDocumentElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, CollectedStatusNameTok)); + 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"; + SpecifiedDocumentStatusElement: XmlElement; + begin + FREInvoiceLifecycleVAT.SetRange("Lifecycle Entry No.", FREInvoiceLifecycle."Entry No."); + if not FREInvoiceLifecycleVAT.FindSet() then + Error(VATBreakdownErr, FREInvoiceLifecycle."Entry No."); + + SpecifiedDocumentStatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); + repeat + SpecifiedDocumentStatusElement.Add(CreateVATCharacteristic(FREInvoiceLifecycleVAT)); + until FREInvoiceLifecycleVAT.Next() = 0; + ReferenceDocumentElement.Add(SpecifiedDocumentStatusElement); + end; + + local procedure CreateVATCharacteristic(FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT") 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', FREInvoiceLifecycleVAT."Currency Code")); + CharacteristicElement.Add(AmountElement); + CharacteristicElement.Add(XmlElement.Create('ValuePercent', RamNamespaceTok, Format(FREInvoiceLifecycleVAT."VAT %", 0, 9))); + end; + + local procedure CreateDateTimeString(Value: DateTime) DateTimeStringElement: XmlElement begin - OriginalOccurrence.Get(OriginalOccurrenceEntryNo); - exit(Format(OriginalOccurrence."Source Occurrence ID")); + DateTimeStringElement := XmlElement.Create('DateTimeString', UdtNamespaceTok, Format(Value, 0, '')); + DateTimeStringElement.Add(XmlAttribute.Create('format', DateTimeFormatCodeTok)); end; - local procedure LifecycleStatusCode(LifecycleStatus: Enum "FR E-Invoice Lifecycle Status"): Text + local procedure ValidatePaymentStatus(LifecycleStatus: Enum "FR E-Invoice Lifecycle Status") begin - case LifecycleStatus of - LifecycleStatus::Collected: - exit('COLLECTED'); - LifecycleStatus::"Negative Collected": - exit('NEGATIVE_COLLECTED'); - LifecycleStatus::Refused: - exit('REFUSED'); - LifecycleStatus::Submitted: - exit('SUBMITTED'); - LifecycleStatus::Rejected: - exit('REJECTED'); - LifecycleStatus::Accepted: - exit('ACCEPTED'); - end; + if not (LifecycleStatus in [LifecycleStatus::Collected, LifecycleStatus::"Negative Collected"]) then + Error(UnsupportedLifecycleStatusErr, LifecycleStatus); 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'; + 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; + 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; } \ 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..1613d83c250 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleVAT.Table.al @@ -0,0 +1,78 @@ +// ------------------------------------------------------------------------------------------------ +// 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.Finance.Currency; + +table 10971 "FR E-Invoice Lifecycle VAT" +{ + Caption = 'FR E-Invoice Lifecycle VAT'; + DataClassification = CustomerContent; + ReplicateData = false; + + fields + { + field(1; "Lifecycle Entry No."; Integer) + { + Caption = 'Lifecycle Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "FR E-Invoice Lifecycle"."Entry No."; + } + field(2; "Line No."; Integer) + { + Caption = 'Line No.'; + DataClassification = SystemMetadata; + } + field(3; "VAT %"; Decimal) + { + Caption = 'VAT %'; + DataClassification = CustomerContent; + DecimalPlaces = 0 : 5; + } + field(4; "Reported Amount"; Decimal) + { + AutoFormatExpression = Rec."Currency Code"; + AutoFormatType = 1; + Caption = 'Reported Amount'; + DataClassification = CustomerContent; + } + field(5; "Currency Code"; Code[10]) + { + Caption = 'Currency Code'; + DataClassification = CustomerContent; + TableRelation = Currency.Code; + } + } + + 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/Permissions/EReportingFREdit.PermissionSet.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFREdit.PermissionSet.al index 33710e09a24..f9c70a55b09 100644 --- 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 @@ -11,5 +11,6 @@ permissionset 10972 "E-Reporting FR - Edit" IncludedPermissionSets = "E-Reporting FR - Read"; - Permissions = tabledata "FR E-Invoice Lifecycle" = IM; + Permissions = tabledata "FR E-Invoice Lifecycle" = IM, + tabledata "FR E-Invoice Lifecycle VAT" = I; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al index e4c6c83a2be..ae4a5b7d25a 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al @@ -10,6 +10,7 @@ permissionset 10970 "E-Reporting FR - Objects" Assignable = false; Permissions = table "FR E-Invoice Lifecycle" = X, + table "FR E-Invoice Lifecycle VAT" = X, codeunit "FR E-Invoice Lifecycle Mgt." = X, codeunit "FR E-Invoice Lifecycle Msg." = X, codeunit "FR E-Invoice Lifecycle Worker" = X, 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 index 0234b8e70df..a41c394fc5e 100644 --- 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 @@ -11,5 +11,6 @@ permissionset 10971 "E-Reporting FR - Read" IncludedPermissionSets = "E-Reporting FR - Objects"; - Permissions = tabledata "FR E-Invoice Lifecycle" = R; + Permissions = tabledata "FR E-Invoice Lifecycle" = R, + tabledata "FR E-Invoice Lifecycle VAT" = R; } \ No newline at end of file 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 1260408a834..a828cf1a520 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 @@ -9,6 +9,8 @@ using Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument.Processing.Message; 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.Sales.Document; using Microsoft.Sales.History; @@ -26,7 +28,10 @@ codeunit 148146 "Identification Tests" 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" = rimd, + tabledata "FR E-Invoice Lifecycle VAT" = rimd, + tabledata "VAT Entry" = rimd, + tabledata "VAT Posting Setup" = rimd; trigger OnRun() begin @@ -404,6 +409,8 @@ codeunit 148146 "Identification Tests" 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 // [SCENARIO] Unapplying a captured payment creates an exact linked Negative Collected occurrence @@ -421,6 +428,22 @@ codeunit 148146 "Identification Tests" 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.'); + Assert.AreEqual(CollectedLifecycleVAT."Currency Code", NegativeCollectedLifecycleVAT."Currency Code", 'The reversal must retain the currency of each VAT-rate amount.'); + until CollectedLifecycleVAT.Next() = 0; end; [Test] @@ -433,13 +456,18 @@ codeunit 148146 "Identification Tests" TempBlob: Codeunit "Temp Blob"; InStream: InStream; XmlDoc: XmlDocument; + AmountElement: XmlElement; + AmountNode: XmlNode; + ProfileNode: XmlNode; StatusNode: XmlNode; + VATPercentNode: XmlNode; begin // [SCENARIO] A captured occurrence creates and links a PR 8698 E-Document Message payload 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); FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); @@ -448,8 +476,17 @@ codeunit 148146 "Identification Tests" EDocMessageMgt.GetMessageBlob(FREInvoiceLifecycle."E-Document Message Entry No.", TempBlob); TempBlob.CreateInStream(InStream); XmlDocument.ReadFrom(InStream, XmlDoc); - Assert.IsTrue(XmlDoc.SelectSingleNode('/InvoiceLifecycleMessage/Status', StatusNode), 'The payload must contain the lifecycle status.'); - Assert.AreEqual('COLLECTED', StatusNode.AsXmlElement().InnerText(), 'The payload must map Collected to its wire code.'); + 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.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ValueAmount"]', AmountNode), 'The payload must contain the collected amount.'); + AmountElement := AmountNode.AsXmlElement(); + Assert.AreEqual('EUR', AmountElement.GetAttribute('currencyID').Value(), 'The collected amount must identify its currency.'); + 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] @@ -465,6 +502,7 @@ codeunit 148146 "Identification Tests" FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + CreateLifecycleVATBreakdown(FREInvoiceLifecycle, 20, 1250); FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); MessageEntryNo := FREInvoiceLifecycle."E-Document Message Entry No."; @@ -474,7 +512,7 @@ codeunit 148146 "Identification Tests" end; [Test] - procedure CreateNegativeCollectedMessageIncludesOriginalOccurrence() + procedure CreateNegativeCollectedMessageUses212AndNegativeAmount() var EDocument: Record "E-Document"; CollectedLifecycle: Record "FR E-Invoice Lifecycle"; @@ -484,27 +522,29 @@ codeunit 148146 "Identification Tests" TempBlob: Codeunit "Temp Blob"; InStream: InStream; XmlDoc: XmlDocument; - OriginalOccurrenceNode: XmlNode; + AmountNode: XmlNode; StatusNode: XmlNode; begin - // [SCENARIO] A Negative Collected message identifies its status and original Collected occurrence + // [SCENARIO] A Negative Collected occurrence uses status 212 with a negative collected amount CreateEDocument(EDocument); CollectedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), 1250, 'EUR', WorkDate(), 0, 0, 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, 0, 0, CollectedLifecycle."Entry No."); + CreateLifecycleVATBreakdown(NegativeCollectedLifecycle, 20, -1250); FREInvoiceLifecycleMgt.CreateLifecycleMessage(NegativeCollectedLifecycle); EDocMessageMgt.GetMessageBlob(NegativeCollectedLifecycle."E-Document Message Entry No.", TempBlob); TempBlob.CreateInStream(InStream); XmlDocument.ReadFrom(InStream, XmlDoc); - Assert.IsTrue(XmlDoc.SelectSingleNode('/InvoiceLifecycleMessage/Status', StatusNode), 'The payload must contain the lifecycle status.'); - Assert.AreEqual('NEGATIVE_COLLECTED', StatusNode.AsXmlElement().InnerText(), 'The payload must map Negative Collected to its wire code.'); - Assert.IsTrue(XmlDoc.SelectSingleNode('/InvoiceLifecycleMessage/OriginalOccurrenceID', OriginalOccurrenceNode), 'The payload must contain the original occurrence ID.'); - Assert.AreEqual(Format(CollectedLifecycle."Source Occurrence ID"), OriginalOccurrenceNode.AsXmlElement().InnerText(), 'The payload must identify the original Collected occurrence.'); + 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('-1250', AmountNode.AsXmlElement().InnerText(), 'An unapplication must report a negative collected amount.'); end; [Test] @@ -538,6 +578,7 @@ codeunit 148146 "Identification Tests" FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; CustLedgerEntry: Record "Cust. Ledger Entry"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; GenJournalLine: Record "Gen. Journal Line"; GenJournalBatch: Record "Gen. Journal Batch"; PostedDocNo: Code[20]; @@ -580,6 +621,9 @@ codeunit 148146 "Identification Tests" 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() @@ -652,7 +696,11 @@ codeunit 148146 "Identification Tests" 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; @@ -672,8 +720,52 @@ codeunit 148146 "Identification Tests" 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.")); @@ -684,7 +776,7 @@ codeunit 148146 "Identification Tests" 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 := -1250; + DetailedCustLedgEntry.Amount := -1000; DetailedCustLedgEntry."Currency Code" := 'EUR'; DetailedCustLedgEntry."Posting Date" := WorkDate(); DetailedCustLedgEntry.Insert(true); @@ -718,4 +810,25 @@ codeunit 148146 "Identification Tests" exit(DetailedCustLedgEntry."Entry No." + 1); exit(1); 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."Currency Code" := FREInvoiceLifecycle."Currency Code"; + FREInvoiceLifecycleVAT.Insert(); + end; } From 830b06a27a924368b2e18a935a02b5a4fd7bd9ad Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 17 Jul 2026 16:09:34 +0200 Subject: [PATCH 07/77] remaining lifecycle hardening --- .../app/src/Core/FREInvoiceLifecycle.Table.al | 50 ++- .../Core/FREInvoiceLifecycleMgt.Codeunit.al | 31 ++ .../Core/FREInvoiceLifecycleMsg.Codeunit.al | 101 +++++- .../EReportingEDocumentService.PageExt.al | 43 +++ .../EReportingEDocumentService.TableExt.al | 33 ++ .../test/src/IdentificationTests.Codeunit.al | 300 ++++++++++++++++++ 6 files changed, 551 insertions(+), 7 deletions(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.PageExt.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.TableExt.al 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 index 0b1d300f056..c9a8efc200a 100644 --- 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 @@ -102,6 +102,46 @@ table 10970 "FR E-Invoice Lifecycle" Caption = 'Last Error'; DataClassification = CustomerContent; } + field(16; "Invoice Issue Date"; Date) + { + Caption = 'Invoice Issue Date'; + DataClassification = CustomerContent; + } + field(17; "Invoice Receipt At"; DateTime) + { + Caption = 'Invoice Receipt At'; + DataClassification = CustomerContent; + } + field(18; "Sender Platform ID"; Text[50]) + { + Caption = 'Sender Platform ID'; + DataClassification = CustomerContent; + } + field(19; "Sender Platform Scheme"; Code[4]) + { + Caption = 'Sender Platform Scheme'; + DataClassification = CustomerContent; + } + field(20; "Sender Platform Name"; Text[100]) + { + Caption = 'Sender Platform Name'; + DataClassification = CustomerContent; + } + field(21; "Invoice Issuer ID"; Text[50]) + { + Caption = 'Invoice Issuer ID'; + DataClassification = CustomerContent; + } + field(22; "Invoice Issuer Scheme"; Code[4]) + { + Caption = 'Invoice Issuer Scheme'; + DataClassification = CustomerContent; + } + field(23; "Invoice Issuer Name"; Text[100]) + { + Caption = 'Invoice Issuer Name'; + DataClassification = CustomerContent; + } } keys @@ -144,7 +184,15 @@ table 10970 "FR E-Invoice Lifecycle" (Rec."Invoice Cust. Ledger Entry No." <> xRec."Invoice Cust. Ledger Entry No.") or (Rec."Payment Cust. Ledger Entry No." <> xRec."Payment Cust. Ledger Entry No.") or (Rec."Detailed Ledger Entry No." <> xRec."Detailed Ledger Entry No.") or - (Rec."Created At" <> xRec."Created At") + (Rec."Created At" <> xRec."Created At") or + (Rec."Invoice Issue Date" <> xRec."Invoice Issue Date") or + (Rec."Invoice Receipt At" <> xRec."Invoice Receipt At") or + (Rec."Sender Platform ID" <> xRec."Sender Platform ID") or + (Rec."Sender Platform Scheme" <> xRec."Sender Platform Scheme") or + (Rec."Sender Platform Name" <> xRec."Sender Platform Name") or + (Rec."Invoice Issuer ID" <> xRec."Invoice Issuer ID") or + (Rec."Invoice Issuer Scheme" <> xRec."Invoice Issuer Scheme") or + (Rec."Invoice Issuer Name" <> xRec."Invoice Issuer Name") then Error(ImmutableOccurrenceErr); end; 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 index bcc3c2ae2b3..01e91ee8156 100644 --- 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 @@ -12,6 +12,7 @@ using Microsoft.Finance.GeneralLedger.Posting; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.VAT.Ledger; using Microsoft.Finance.VAT.Setup; +using Microsoft.Foundation.Company; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; using System.Utilities; @@ -107,6 +108,8 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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 @@ -116,6 +119,33 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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"; + begin + EDocument.Get(FREInvoiceLifecycle."E-Document Entry No."); + EDocument.TestField("Document Date"); + EDocument.TestField("Clearance Date"); + EDocumentService.Get(EDocument.Service); + EDocumentService.TestField("FR Sender Platform ID"); + 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") var InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; @@ -405,4 +435,5 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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'; + 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 index b177b7d9605..df97f0fa152 100644 --- 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 @@ -54,23 +54,28 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('ram', RamNamespaceTok)); RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('rsm', RsmNamespaceTok)); - AddExchangedDocumentContext(RootElement); + if IsPPFMessage(FREInvoiceLifecycle) then + ValidatePPFContext(FREInvoiceLifecycle); + + AddExchangedDocumentContext(RootElement, FREInvoiceLifecycle); AddExchangedDocument(RootElement, FREInvoiceLifecycle); AddAcknowledgementDocument(RootElement, EDocument, FREInvoiceLifecycle); end; - local procedure AddExchangedDocumentContext(var RootElement: XmlElement) + 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); - BusinessProcessElement := XmlElement.Create('BusinessProcessSpecifiedDocumentContextParameter', RamNamespaceTok); - BusinessProcessElement.Add(XmlElement.Create('ID', RamNamespaceTok, RegulatedBusinessProcessTok)); - ContextElement.Add(BusinessProcessElement); + 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, CDVInvoiceProfileTok)); + GuidelineElement.Add(XmlElement.Create('ID', RamNamespaceTok, GetProfileID(FREInvoiceLifecycle))); ContextElement.Add(GuidelineElement); RootElement.Add(ContextElement); end; @@ -86,6 +91,18 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder 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; @@ -112,8 +129,18 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder 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); @@ -157,6 +184,60 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder 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") begin if not (LifecycleStatus in [LifecycleStatus::Collected, LifecycleStatus::"Negative Collected"]) then @@ -174,6 +255,7 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder 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; @@ -181,4 +263,11 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder 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/Extensions/EReportingEDocumentService.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.PageExt.al new file mode 100644 index 00000000000..a0d0b1a9325 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.PageExt.al @@ -0,0 +1,43 @@ +// ------------------------------------------------------------------------------------------------ +// 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 = IsFrenchInvoiceFormat; + + 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; + } + } + } + } + + trigger OnAfterGetCurrRecord() + begin + IsFrenchInvoiceFormat := Rec."Document Format" in [Rec."Document Format"::"Peppol BIS 3.0 FR", Rec."Document Format"::"Factur-X FR"]; + end; + + var + IsFrenchInvoiceFormat: Boolean; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.TableExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.TableExt.al new file mode 100644 index 00000000000..aa32b4423fb --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.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 = CustomerContent; + 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 = CustomerContent; + 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 = CustomerContent; + 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/test/src/IdentificationTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/IdentificationTests.Codeunit.al index a828cf1a520..c552f4bfc00 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 @@ -7,6 +7,7 @@ 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; @@ -298,6 +299,12 @@ codeunit 148146 "Identification Tests" asserterror FREInvoiceLifecycle.Modify(); Assert.ExpectedError('The regulatory identity and values of a French electronic invoice lifecycle occurrence cannot be changed.'); + + FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No."); + FREInvoiceLifecycle."Sender Platform ID" := 'CHANGED'; + asserterror FREInvoiceLifecycle.Modify(); + + Assert.ExpectedError('The regulatory identity and values of a French electronic invoice lifecycle occurrence cannot be changed.'); end; [Test] @@ -319,6 +326,230 @@ codeunit 148146 "Identification Tests" Assert.AreEqual(1250, FREInvoiceLifecycle."Reported Amount", '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"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + TempBlob: Codeunit "Temp Blob"; + InStream: InStream; + XmlDoc: XmlDocument; + XmlNode: XmlNode; + begin + // [SCENARIO] A real French invoice occurrence creates the PPF einvoicingF2 lifecycle envelope + 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); + + EDocMessageMgt.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 + // [SCENARIO] Multiple invoice VAT entries with the same rate create one lifecycle amount + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + SetInvoiceVATRate(EDocument."Document No.", 20, false); + + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + 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 + // [SCENARIO] A zero-rated invoice retains VAT rate zero in the lifecycle breakdown + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + SetInvoiceVATRate(EDocument."Document No.", 0, true); + + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + 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 + // [SCENARIO] Foreign-currency VAT allocation uses that currency's rounding precision and preserves the remainder + 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); + + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + 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 + // [SCENARIO] A French lifecycle occurrence is not retained when its posted invoice has no VAT breakdown + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + VATEntry.SetRange("Document No.", EDocument."Document No."); + VATEntry.DeleteAll(); + + asserterror FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + 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 DetailedApplicationRejectsMissingSenderPlatformSetup() + var + EDocument: Record "E-Document"; + EDocumentService: Record "E-Document Service"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; + begin + // [SCENARIO] A PPF lifecycle occurrence is not retained when the sender platform identity is incomplete + CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); + EDocumentService.Get(EDocument.Service); + Clear(EDocumentService."FR Sender Platform ID"); + EDocumentService.Modify(); + + asserterror FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + Assert.ExpectedError('FR Sender Platform ID must have a value'); + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + Assert.RecordIsEmpty(FREInvoiceLifecycle); + 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 + // [SCENARIO] A lifecycle occurrence is not retained when a posted VAT entry has no matching setup + 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(); + + asserterror FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + + Assert.ExpectedError('VAT Posting Setup does not exist'); + FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); + Assert.RecordIsEmpty(FREInvoiceLifecycle); + end; + + [Test] + 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 + // [SCENARIO] Captured lifecycle occurrences and their VAT rows cannot be deleted + 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); + + asserterror FREInvoiceLifecycle.Delete(); + Assert.ExpectedError('A French electronic invoice lifecycle occurrence cannot be deleted.'); + FREInvoiceLifecycleVAT.Get(FREInvoiceLifecycle."Entry No.", 10000); + asserterror FREInvoiceLifecycleVAT.Delete(); + Assert.ExpectedError('A French electronic invoice lifecycle VAT breakdown cannot be changed.'); end; [Test] @@ -482,6 +713,9 @@ codeunit 148146 "Identification Tests" 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.'); Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ValueAmount"]', AmountNode), 'The payload must contain the collected amount.'); AmountElement := AmountNode.AsXmlElement(); Assert.AreEqual('EUR', AmountElement.GetAttribute('currencyID').Value(), 'The collected amount must identify its currency.'); @@ -659,6 +893,7 @@ codeunit 148146 "Identification Tests" begin EDocumentService.Code := CopyStr(CreateGuid(), 1, MaxStrLen(EDocumentService.Code)); EDocumentService."Document Format" := "E-Document Format"::"Factur-X FR"; + ConfigurePPFService(EDocumentService); EDocumentService.Insert(); EDocument.Init(); @@ -667,6 +902,8 @@ codeunit 148146 "Identification Tests" EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; EDocument.Direction := EDocument.Direction::Outgoing; EDocument.Service := EDocumentService.Code; + EDocument."Document Date" := WorkDate(); + EDocument."Clearance Date" := CurrentDateTime(); EDocument.Insert(); end; @@ -708,6 +945,7 @@ codeunit 148146 "Identification Tests" EDocumentService.Code := CopyStr(CreateGuid(), 1, MaxStrLen(EDocumentService.Code)); EDocumentService."Document Format" := EDocumentFormat; + ConfigurePPFService(EDocumentService); EDocumentService.Insert(); EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; @@ -715,6 +953,8 @@ codeunit 148146 "Identification Tests" EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; EDocument.Direction := EDocument.Direction::Outgoing; EDocument.Service := EDocumentService.Code; + EDocument."Document Date" := WorkDate(); + EDocument."Clearance Date" := CurrentDateTime(); EDocument.Insert(); InvoiceCustLedgerEntry."Entry No." := GetNextCustLedgerEntryNo(); @@ -831,4 +1071,64 @@ codeunit 148146 "Identification Tests" FREInvoiceLifecycleVAT."Currency Code" := FREInvoiceLifecycle."Currency Code"; 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."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('The payload must contain XML node %1.', XPath)); + Assert.AreEqual(ExpectedValue, XmlNode.AsXmlElement().InnerText(), StrSubstNo('The XML node %1 has an unexpected value.', 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('The payload must contain XML node %1.', XPath)); + Assert.IsTrue(XmlNode.AsXmlElement().Attributes().Get(AttributeName, XmlAttribute), StrSubstNo('The XML node %1 must contain attribute %2.', XPath, AttributeName)); + Assert.AreEqual(ExpectedValue, XmlAttribute.Value(), StrSubstNo('The XML attribute %1 on node %2 has an unexpected value.', AttributeName, XPath)); + end; } From c9a68a1b67d27ccb6ba2f5b572a1fd2bc5f10c32 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 17 Jul 2026 17:25:41 +0200 Subject: [PATCH 08/77] improvements --- .../Core/FREInvoiceLifecycleProcStatus.Enum.al | 4 ---- .../src/Core/FREInvoiceLifecycleStatus.Enum.al | 16 ---------------- 2 files changed, 20 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProcStatus.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProcStatus.Enum.al index 61b5903c25c..dc8ec1dd1da 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProcStatus.Enum.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProcStatus.Enum.al @@ -20,10 +20,6 @@ enum 10974 "FR E-Invoice Lifecycle Proc." { Caption = 'Queued'; } - value(3; Sent) - { - Caption = 'Sent'; - } value(4; Failed) { Caption = 'Failed'; 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 index 2870b912288..8e8b2880ed1 100644 --- 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 @@ -16,20 +16,4 @@ enum 10973 "FR E-Invoice Lifecycle Status" { Caption = 'Negative Collected'; } - value(2; Refused) - { - Caption = 'Refused'; - } - value(3; Submitted) - { - Caption = 'Submitted'; - } - value(4; Rejected) - { - Caption = 'Rejected'; - } - value(5; Accepted) - { - Caption = 'Accepted'; - } } \ No newline at end of file From 92b361db966e97da24e9bf5ae740fc0e45180d30 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Jul 2026 11:11:10 +0200 Subject: [PATCH 09/77] init --- .../app/src/Core/EDocHelpers.Codeunit.al | 45 +++- .../src/Core/ElectronicAddressScheme.Enum.al | 8 + .../app/src/Core/FRRegulatoryComment.Table.al | 44 ++++ .../Core/FRRegulatoryCommentDocType.Enum.al | 31 +++ .../Core/FRRegulatoryCommentMgt.Codeunit.al | 53 +++++ .../src/Core/FRRegulatoryCommentType.Enum.al | 23 ++ .../app/src/Core/FRRegulatoryComments.Page.al | 36 +++ .../FRRegulatoryComments.PageExt.al | 102 +++++++++ .../FRServiceParticipant.TableExt.al | 20 ++ .../FRServiceParticipants.PageExt.al | 22 ++ .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 196 +++++++++++++++-- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 208 ++++++++++++++++-- .../EDocumentSubscribers.Codeunit.al | 35 ++- 13 files changed, 786 insertions(+), 37 deletions(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComment.Table.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentDocType.Enum.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentMgt.Codeunit.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComments.Page.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRRegulatoryComments.PageExt.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRServiceParticipant.TableExt.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRServiceParticipants.PageExt.al 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..f8bb067dee2 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,6 +4,8 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Formats; +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Service.Participant; using Microsoft.Foundation.Company; using Microsoft.Sales.Customer; @@ -58,6 +60,21 @@ codeunit 10991 "EDoc. Helpers" Error(SIRETRequiredErr); end; + procedure CheckSellerElectronicAddress(EDocumentServiceCode: Code[20]) + var + CompanyInformation: Record "Company Information"; + begin + CompanyInformation.Get(); + if CompanyInformation."SIRET No." <> '' then + exit; + if HasServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Company, '') then + exit; + if CompanyInformation.GetVATRegistrationNumber() <> '' then + exit; + + Error(SellerElectronicAddressRequiredErr); + end; + procedure CheckSellerCountryCode() var CompanyInformation: Record "Company Information"; @@ -68,6 +85,11 @@ codeunit 10991 "EDoc. Helpers" end; procedure CheckBuyerElectronicAddress(var SourceDocumentHeader: RecordRef) + begin + CheckBuyerElectronicAddress(SourceDocumentHeader, ''); + end; + + procedure CheckBuyerElectronicAddress(var SourceDocumentHeader: RecordRef; EDocumentServiceCode: Code[20]) var Customer: Record Customer; FRCIIXMLBuilder: Codeunit "CII XML Builder"; @@ -84,13 +106,32 @@ codeunit 10991 "EDoc. Helpers" if not Customer.Get(CustomerNo) then exit; - if Customer."FR Electronic Address" = '' then - Error(BuyerElectronicAddressRequiredErr, Customer."No."); + if Customer."FR Electronic Address" <> '' then + exit; + if HasServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Customer, Customer."No.") then + exit; + if Customer."VAT Registration No." <> '' then + exit; + + Error(BuyerElectronicAddressRequiredErr, Customer."No."); + end; + + local procedure HasServiceParticipantAddress(EDocumentServiceCode: Code[20]; ParticipantType: Enum "E-Document Source Type"; ParticipantNo: Code[20]): Boolean + var + ServiceParticipant: Record "Service Participant"; + begin + if EDocumentServiceCode = '' then + exit(false); + if not ServiceParticipant.Get(EDocumentServiceCode, ParticipantType, ParticipantNo) then + exit(false); + + exit(ServiceParticipant."Participant Identifier" <> ''); 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.'; + SellerElectronicAddressRequiredErr: Label 'SIRET No., VAT Registration No., or a Service Participant identifier must be specified for the company 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.'; } 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..155a6024550 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,12 @@ enum 10976 "Electronic Address Scheme" { Caption = 'SIREN (0002)'; } + value(3; "0223") + { + Caption = 'French VAT number (0223)'; + } + value(4; "0225") + { + Caption = 'French routing identifier (0225)'; + } } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComment.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComment.Table.al new file mode 100644 index 00000000000..073e74a80a3 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComment.Table.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.Formats; + +table 10971 "FR Regulatory Comment" +{ + Access = Internal; + Caption = 'French Regulatory Comment'; + DataClassification = CustomerContent; + + fields + { + field(1; "Document Type"; Enum "FR Reg. Comment Doc. Type") + { + Caption = 'Document Type'; + } + field(2; "Document No."; Code[20]) + { + Caption = 'Document No.'; + } + field(3; "Line No."; Integer) + { + Caption = 'Line No.'; + } + field(4; "Comment Type"; Enum "FR Regulatory Comment Type") + { + Caption = 'Comment Type'; + } + field(5; "Comment Text"; Text[250]) + { + Caption = 'Comment Text'; + } + } + + keys + { + key(PK; "Document Type", "Document No.", "Line No.") + { + Clustered = true; + } + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentDocType.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentDocType.Enum.al new file mode 100644 index 00000000000..5789d01e391 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentDocType.Enum.al @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------------------------------ +// 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 Reg. Comment Doc. Type" +{ + Extensible = false; + + value(0; Order) + { + Caption = 'Order'; + } + value(1; Invoice) + { + Caption = 'Invoice'; + } + value(2; "Credit Memo") + { + Caption = 'Credit Memo'; + } + value(3; "Posted Invoice") + { + Caption = 'Posted Invoice'; + } + value(4; "Posted Credit Memo") + { + Caption = 'Posted Credit Memo'; + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentMgt.Codeunit.al new file mode 100644 index 00000000000..fcf1341d8f7 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentMgt.Codeunit.al @@ -0,0 +1,53 @@ +// ------------------------------------------------------------------------------------------------ +// 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.Finance.GeneralLedger.Journal; +using Microsoft.Sales.Customer; +using Microsoft.Sales.Document; + +codeunit 10971 "FR Regulatory Comment Mgt." +{ + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", OnAfterPostSalesDoc, '', false, false)] + local procedure CopyCommentsAfterPosting(var SalesHeader: Record "Sales Header"; var GenJnlPostLine: Codeunit "Gen. Jnl.-Post Line"; SalesShptHdrNo: Code[20]; RetRcpHdrNo: Code[20]; SalesInvHdrNo: Code[20]; SalesCrMemoHdrNo: Code[20]; CommitIsSuppressed: Boolean; InvtPickPutaway: Boolean; var CustLedgerEntry: Record "Cust. Ledger Entry"; WhseShip: Boolean; WhseReceiv: Boolean; PreviewMode: Boolean) + var + FromDocumentType: Enum "FR Reg. Comment Doc. Type"; + begin + if PreviewMode then + exit; + + case SalesHeader."Document Type" of + SalesHeader."Document Type"::Order: + FromDocumentType := FromDocumentType::Order; + SalesHeader."Document Type"::Invoice: + FromDocumentType := FromDocumentType::Invoice; + SalesHeader."Document Type"::"Credit Memo": + FromDocumentType := FromDocumentType::"Credit Memo"; + else + exit; + end; + + if SalesInvHdrNo <> '' then + CopyComments(FromDocumentType, SalesHeader."No.", FromDocumentType::"Posted Invoice", SalesInvHdrNo); + if SalesCrMemoHdrNo <> '' then + CopyComments(FromDocumentType, SalesHeader."No.", FromDocumentType::"Posted Credit Memo", SalesCrMemoHdrNo); + end; + + local procedure CopyComments(FromDocumentType: Enum "FR Reg. Comment Doc. Type"; FromDocumentNo: Code[20]; ToDocumentType: Enum "FR Reg. Comment Doc. Type"; ToDocumentNo: Code[20]) + var + FromComment: Record "FR Regulatory Comment"; + ToComment: Record "FR Regulatory Comment"; + begin + FromComment.SetRange("Document Type", FromDocumentType); + FromComment.SetRange("Document No.", FromDocumentNo); + if FromComment.FindSet() then + repeat + ToComment := FromComment; + ToComment."Document Type" := ToDocumentType; + ToComment."Document No." := ToDocumentNo; + ToComment.Insert(); + until FromComment.Next() = 0; + 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..377f3152194 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------ +// 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 = false; + + value(0; AAB) + { + Caption = 'AAB'; + } + value(1; PMD) + { + Caption = 'PMD'; + } + value(2; PMT) + { + Caption = 'PMT'; + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComments.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComments.Page.al new file mode 100644 index 00000000000..57ffe1cda27 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComments.Page.al @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------------------------------ +// 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 Regulatory Comments" +{ + ApplicationArea = All; + AutoSplitKey = true; + Caption = 'French Regulatory Comments'; + DataCaptionFields = "Document No."; + PageType = List; + SourceTable = "FR Regulatory Comment"; + UsageCategory = None; + + layout + { + area(Content) + { + repeater(Comments) + { + field("Comment Type"; Rec."Comment Type") + { + ApplicationArea = All; + ToolTip = 'Specifies the regulatory purpose of the comment.'; + } + field("Comment Text"; Rec."Comment Text") + { + ApplicationArea = All; + ToolTip = 'Specifies the regulatory comment included in the French electronic invoice.'; + } + } + } + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRRegulatoryComments.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRRegulatoryComments.PageExt.al new file mode 100644 index 00000000000..9c22d205551 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRRegulatoryComments.PageExt.al @@ -0,0 +1,102 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +pageextension 10975 "FR Sales Order Comments" extends "Sales Order" +{ + actions + { + addlast("O&rder") + { + action(FRRegulatoryComments) + { + ApplicationArea = All; + Caption = 'French Regulatory Comments'; + Image = ViewComments; + RunObject = page "FR Regulatory Comments"; + RunPageLink = "Document Type" = const(Order), "Document No." = field("No."); + ToolTip = 'View or edit regulatory comments for French electronic invoicing.'; + } + } + } +} + +pageextension 10976 "FR Sales Invoice Comments" extends "Sales Invoice" +{ + actions + { + addlast(Processing) + { + action(FRRegulatoryComments) + { + ApplicationArea = All; + Caption = 'French Regulatory Comments'; + Image = ViewComments; + RunObject = page "FR Regulatory Comments"; + RunPageLink = "Document Type" = const(Invoice), "Document No." = field("No."); + ToolTip = 'View or edit regulatory comments for French electronic invoicing.'; + } + } + } +} + +pageextension 10977 "FR Sales CrMemo Comments" extends "Sales Credit Memo" +{ + actions + { + addlast(Processing) + { + action(FRRegulatoryComments) + { + ApplicationArea = All; + Caption = 'French Regulatory Comments'; + Image = ViewComments; + RunObject = page "FR Regulatory Comments"; + RunPageLink = "Document Type" = const("Credit Memo"), "Document No." = field("No."); + ToolTip = 'View or edit regulatory comments for French electronic invoicing.'; + } + } + } +} + +pageextension 10978 "FR Posted Sales Inv Comments" extends "Posted Sales Invoice" +{ + actions + { + addlast(Processing) + { + action(FRRegulatoryComments) + { + ApplicationArea = All; + Caption = 'French Regulatory Comments'; + Image = ViewComments; + RunObject = page "FR Regulatory Comments"; + RunPageLink = "Document Type" = const("Posted Invoice"), "Document No." = field("No."); + RunPageMode = View; + ToolTip = 'View the regulatory comments included in French electronic invoices.'; + } + } + } +} + +pageextension 10979 "FR Posted Sales CrMemo Comm" extends "Posted Sales Credit Memo" +{ + actions + { + addlast(Processing) + { + action(FRRegulatoryComments) + { + ApplicationArea = All; + Caption = 'French Regulatory Comments'; + Image = ViewComments; + RunObject = page "FR Regulatory Comments"; + RunPageLink = "Document Type" = const("Posted Credit Memo"), "Document No." = field("No."); + RunPageMode = View; + ToolTip = 'View the regulatory comments included in French electronic credit memos.'; + } + } + } +} \ 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..ad42c35ea49 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRServiceParticipant.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.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; + 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..a4c787a5ce2 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRServiceParticipants.PageExt.al @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + 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/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al index d1f47d81a90..fcb5555efb9 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,11 @@ 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.Customer; +using Microsoft.Sales.History; using System.Utilities; codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" @@ -24,9 +26,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 +42,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, 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 +65,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; EDocumentService: Record "E-Document Service") var CompanyInformation: Record "Company Information"; XmlDoc: XmlDocument; @@ -82,9 +84,11 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" InitNamespaceManager(NamespaceMgr, XmlDoc); 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 +97,133 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" XmlDoc.WriteTo(OutStr); end; + local procedure InjectExtendedCTCFranceElements(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; SourceDocumentHeader: RecordRef) + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + CustomizationIdNode: XmlNode; + begin + if SourceDocumentHeader.Number <> Database::"Sales Invoice Header" then + exit; + + SourceDocumentHeader.SetTable(SalesInvoiceHeader); + if not RequiresExtendedCTCFrance(SalesInvoiceHeader."No.") then + exit; + + if XmlDoc.SelectSingleNode('/*/cbc:CustomizationID', NamespaceMgr, CustomizationIdNode) then + CustomizationIdNode.AsXmlElement().InnerText := ExtendedCTCFranceCustomizationIdTok; + + SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + SalesInvoiceLine.SetFilter("Shipment No.", '<>%1', ''); + if SalesInvoiceLine.FindSet() then + repeat + InjectExtendedLineReferences(XmlDoc, NamespaceMgr, SalesInvoiceLine); + until SalesInvoiceLine.Next() = 0; + end; + + local procedure RequiresExtendedCTCFrance(DocumentNo: Code[20]): Boolean + var + SalesInvoiceLine: Record "Sales Invoice Line"; + SalesShipmentHeader: Record "Sales Shipment Header"; + ShipmentNos: Dictionary of [Text, Boolean]; + OrderNos: Dictionary of [Text, Boolean]; + DeliveryDates: Dictionary of [Text, Boolean]; + begin + SalesInvoiceLine.SetRange("Document No.", DocumentNo); + if SalesInvoiceLine.FindSet() then + repeat + if SalesInvoiceLine."Shipment No." <> '' then begin + AddDistinctValue(ShipmentNos, SalesInvoiceLine."Shipment No."); + if SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No.") then + AddDistinctValue(DeliveryDates, Format(SalesShipmentHeader."Posting Date", 0, 9)); + end; + if SalesInvoiceLine."Order No." <> '' then + AddDistinctValue(OrderNos, SalesInvoiceLine."Order No."); + until SalesInvoiceLine.Next() = 0; + + 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") + var + SalesShipmentHeader: Record "Sales Shipment Header"; + SalesShipmentLine: Record "Sales Shipment Line"; + InvoiceLineNode: XmlNode; + ItemNode: XmlNode; + OrderLineReferenceElement: XmlElement; + OrderReferenceElement: XmlElement; + DeliveryElement: XmlElement; + LineXPath: Text; + begin + if not SalesShipmentLine.Get(SalesInvoiceLine."Shipment No.", SalesInvoiceLine."Shipment Line No.") then + exit; + if not SalesShipmentHeader.Get(SalesShipmentLine."Document No.") then + exit; + + LineXPath := StrSubstNo('/*/cac:InvoiceLine[cbc:ID=''%1'']', 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; + + DeliveryElement := XmlElement.Create('Delivery', CacNamespaceTok); + DeliveryElement.Add(XmlElement.Create('ID', CbcNamespaceTok, SalesShipmentLine."Document No.")); + DeliveryElement.Add(XmlElement.Create('ActualDeliveryDate', CbcNamespaceTok, Format(SalesShipmentHeader."Posting Date", 0, 9))); + ItemNode.AddBeforeSelf(DeliveryElement); + end; + + local procedure InjectRegulatoryComments(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; SourceDocumentHeader: RecordRef) + var + RegulatoryComment: Record "FR Regulatory Comment"; + AnchorNode: XmlNode; + NoteElement: XmlElement; + DocumentNo: Code[20]; + DocumentType: Enum "FR Reg. Comment Doc. 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; + + RegulatoryComment.SetRange("Document Type", DocumentType); + RegulatoryComment.SetRange("Document No.", DocumentNo); + if RegulatoryComment.FindSet() then + repeat + NoteElement := XmlElement.Create('Note', CbcNamespaceTok, RegulatoryComment."Comment Text"); + AnchorNode.AddAfterSelf(NoteElement); + AnchorNode := NoteElement.AsXmlNode(); + until RegulatoryComment.Next() = 0; + end; + local procedure InitNamespaceManager(var NamespaceMgr: XmlNamespaceManager; XmlDoc: XmlDocument) var RootElement: XmlElement; @@ -160,25 +291,35 @@ 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 begin + ElecAddress := CompanyInformation.GetVATRegistrationNumber(); + ElecAddressScheme := ElecAddressScheme::"0223"; + 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 +344,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 +358,36 @@ 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::"0223"; + 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"; + begin + if not ServiceParticipant.Get(EDocumentServiceCode, ParticipantType, ParticipantNo) then + exit(false); + if ServiceParticipant."Participant Identifier" = '' 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 +420,10 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" exit('0009'); ElecAddressScheme::"0002": exit('0002'); + ElecAddressScheme::"0223": + exit('0223'); + ElecAddressScheme::"0225": + exit('0225'); else exit(Format(ElecAddressScheme)); end; @@ -323,9 +489,13 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" EDocServiceSupportedType."Source Document Type" := EDocServiceSupportedType."Source Document Type"::"Purchase Invoice"; EDocServiceSupportedType.Insert(); + + EDocServiceSupportedType."Source Document Type" := EDocServiceSupportedType."Source Document Type"::"Purchase Credit Memo"; + 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; } 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..550e9ca4694 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,6 +6,7 @@ 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; @@ -21,6 +22,9 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Subtype = Test; Permissions = tabledata "E-Document Service" = rimd, tabledata "Company Information" = rimd, + tabledata "FR Regulatory Comment" = rimd, + tabledata "Service Participant" = rimd, + tabledata "Sales Invoice Line" = rimd, tabledata "Sales & Receivables Setup" = rimd, tabledata Customer = rimd; @@ -39,7 +43,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Assert: Codeunit Assert; PeppolBIS30FRFormat: Codeunit "Peppol BIS 3.0 FR Format"; 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; @@ -279,6 +282,64 @@ 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 + RegulatoryComment: Record "FR Regulatory Comment"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + CommentText: Text[250]; + begin + // [SCENARIO] A maintained French regulatory comment is exported as a UBL header note + Initialize(); + + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('', "Electronic Address Scheme"::"EM"))); + CommentText := 'No discount is granted for early payment.'; + RegulatoryComment."Document Type" := RegulatoryComment."Document Type"::"Posted Invoice"; + RegulatoryComment."Document No." := SalesInvoiceHeader."No."; + RegulatoryComment."Line No." := 10000; + RegulatoryComment."Comment Type" := RegulatoryComment."Comment Type"::AAB; + RegulatoryComment."Comment Text" := CommentText; + RegulatoryComment.Insert(); + + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + Assert.AreEqual(CommentText, GetNodeByPath(XmlDoc, '/Invoice/cbc:Note'), StrSubstNo(IncorrectValueErr, 'Note')); + end; + + [Test] + procedure ExportSalesInvUsesServiceParticipantEndpointWithScheme0225() + var + ServiceParticipant: Record "Service Participant"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + CustomerNo: Code[20]; + EndpointId: Text[200]; + begin + // [SCENARIO] A service-specific routing identifier overrides the endpoint on the customer card + Initialize(); + + 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)); + + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + 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')); + end; + #endregion #region SalesCreditMemo @@ -322,6 +383,60 @@ 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 ExportSalesInvSelectsExtendedCTCForMultipleOrders() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + begin + // [SCENARIO] An invoice containing lines from distinct orders uses the Extended CTC profile + Initialize(); + + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithTwoLines(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + SetPostedInvoiceLineReferences(SalesInvoiceHeader."No.", 'SHIPMENT-1', 'ORDER-', false, true); + + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), + StrSubstNo(IncorrectValueErr, 'CustomizationID')); + end; + + [Test] + procedure ExportSalesInvSelectsExtendedCTCForMultipleShipments() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + begin + // [SCENARIO] An invoice containing lines from distinct shipments uses the Extended CTC profile + Initialize(); + + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithTwoLines(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + SetPostedInvoiceLineReferences(SalesInvoiceHeader."No.", 'SHIPMENT-', 'ORDER-1', true, false); + + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), + StrSubstNo(IncorrectValueErr, 'CustomizationID')); + end; + + [Test] + procedure ExportSalesInvKeepsBasicCTCForRepeatedReferences() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + begin + // [SCENARIO] Repeated references to one shipment and one order do not select the Extended CTC profile + Initialize(); + + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithTwoLines(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + SetPostedInvoiceLineReferences(SalesInvoiceHeader."No.", 'SHIPMENT-1', 'ORDER-1', false, false); + + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + Assert.AreNotEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), + StrSubstNo(IncorrectValueErr, 'CustomizationID')); + end; #endregion #region Validation @@ -374,16 +489,16 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" end; [Test] - procedure CheckRaisesErrorWhenSIRETIsEmpty() + procedure ExportSalesInvUsesSellerVATFallbackWhenSIRETIsEmpty() var SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; OriginalSIRETNo: Code[14]; begin - // [FEATURE] [AI test] - // [SCENARIO] Check raises error when company SIRET No. is blank + // [SCENARIO] Company VAT registration number is used as the seller endpoint when SIRET is blank Initialize(); - // [GIVEN] Company with blank SIRET No. + // [GIVEN] Company with blank SIRET No. and a VAT registration number OriginalSIRETNo := CompanyInformation."SIRET No."; CompanyInformation.Get(); CompanyInformation."SIRET No." := ''; @@ -391,11 +506,17 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); - // [WHEN] Check is called - asserterror CheckInvoice(SalesInvoiceHeader); + // [WHEN] The invoice is checked and exported + CheckInvoice(SalesInvoiceHeader); + ExportInvoice(SalesInvoiceHeader, XmlDoc); - // [THEN] Error about SIRET No. is raised - Assert.ExpectedError('SIRET No. must be specified in Company Information for French e-invoicing.'); + // [THEN] The supplier endpoint uses the VAT identifier and scheme 0223 + Assert.AreEqual(CompanyInformation.GetVATRegistrationNumber(), + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID'), + StrSubstNo(IncorrectValueErr, 'Seller EndpointID')); + Assert.AreEqual('0223', + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID'), + StrSubstNo(IncorrectValueErr, 'Seller EndpointID schemeID')); // Cleanup CompanyInformation.Get(); @@ -434,24 +555,28 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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 - asserterror CheckInvoice(SalesInvoiceHeader); + // [WHEN] The invoice is checked and exported + CheckInvoice(SalesInvoiceHeader); + ExportInvoice(SalesInvoiceHeader, XmlDoc); - // [THEN] Error about Electronic Address is raised - ExpectedErrorText := StrSubstNo(CustomerElectronicAddressErr, SalesInvoiceHeader."Sell-to Customer No."); - Assert.ExpectedError(ExpectedErrorText); + // [THEN] The buyer endpoint uses the VAT identifier and scheme 0223 + Assert.AreEqual(GetCustomerVATRegistrationNo(SalesInvoiceHeader."Sell-to Customer No."), + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID'), + StrSubstNo(IncorrectValueErr, 'Buyer EndpointID')); + Assert.AreEqual('0223', + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'), + StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); end; #endregion @@ -506,6 +631,43 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); end; + local procedure CreateAndPostSalesInvoiceWithTwoLines(CustomerNo: Code[20]): Code[20] + var + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + NewSalesLine: Record "Sales Line"; + begin + SalesHeader.Get("Sales Document Type"::Invoice, CreateSalesInvoiceWithLine(CustomerNo)); + SalesLine.SetRange("Document Type", SalesHeader."Document Type"); + SalesLine.SetRange("Document No.", SalesHeader."No."); + SalesLine.FindFirst(); + LibrarySales.CreateSalesLine(NewSalesLine, SalesHeader, SalesLine.Type, SalesLine."No.", 1); + NewSalesLine.Validate("Unit Price", SalesLine."Unit Price"); + NewSalesLine.Modify(true); + exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + end; + + local procedure SetPostedInvoiceLineReferences(DocumentNo: Code[20]; ShipmentNo: Text; OrderNo: Text; AppendShipmentIndex: Boolean; AppendOrderIndex: Boolean) + var + SalesInvoiceLine: Record "Sales Invoice Line"; + LineIndex: Integer; + begin + SalesInvoiceLine.SetRange("Document No.", DocumentNo); + SalesInvoiceLine.FindSet(true); + repeat + LineIndex += 1; + if AppendShipmentIndex then + SalesInvoiceLine."Shipment No." := CopyStr(ShipmentNo + Format(LineIndex), 1, MaxStrLen(SalesInvoiceLine."Shipment No.")); + else + SalesInvoiceLine."Shipment No." := CopyStr(ShipmentNo, 1, MaxStrLen(SalesInvoiceLine."Shipment No.")); + if AppendOrderIndex then + SalesInvoiceLine."Order No." := CopyStr(OrderNo + Format(LineIndex), 1, MaxStrLen(SalesInvoiceLine."Order No.")); + else + SalesInvoiceLine."Order No." := CopyStr(OrderNo, 1, MaxStrLen(SalesInvoiceLine."Order No.")); + SalesInvoiceLine.Modify(); + until SalesInvoiceLine.Next() = 0; + end; + local procedure CreateSalesInvoiceWithLine(CustomerNo: Code[20]): Code[20] var Customer: Record Customer; @@ -575,6 +737,14 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" exit('FR' + VATNoBody); end; + local procedure GetCustomerVATRegistrationNo(CustomerNo: Code[20]): Text[20] + var + Customer: Record Customer; + begin + Customer.Get(CustomerNo); + exit(Customer."VAT Registration No."); + end; + local procedure CheckInvoice(SalesInvoiceHeader: Record "Sales Invoice Header") var SourceDocumentHeader: RecordRef; diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al index dcd26341345..a52c42910d0 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al @@ -282,17 +282,46 @@ codeunit 6103 "E-Document Subscribers" [EventSubscriber(ObjectType::Codeunit, Codeunit::"Purch.-Post", OnAfterPostPurchaseDoc, '', false, false)] local procedure OnAfterPostPurchaseDoc(var PurchaseHeader: Record "Purchase Header"; var GenJnlPostLine: Codeunit "Gen. Jnl.-Post Line"; PurchRcpHdrNo: Code[20]; RetShptHdrNo: Code[20]; PurchInvHdrNo: Code[20]; PurchCrMemoHdrNo: Code[20]; CommitIsSupressed: Boolean) var + EDocument: Record "E-Document"; + DocumentSendingProfile: Record "Document Sending Profile"; PurchInvHeader: Record "Purch. Inv. Header"; PurchCrMemoHdr: Record "Purch. Cr. Memo Hdr."; + PostedDocumentHeader: RecordRef; begin if (PurchInvHdrNo = '') and (PurchCrMemoHdrNo = '') then exit; + + if IsEDocumentLinkedToPurchaseDocument(EDocument, PurchaseHeader) then begin + if PurchInvHdrNo <> '' then begin + if PurchInvHeader.Get(PurchInvHdrNo) then + PointEDocumentToPostedDocument(PurchaseHeader, PurchInvHeader, PurchInvHdrNo, Enum::"E-Document Type"::"Purchase Invoice") + end else + if PurchCrMemoHdr.Get(PurchCrMemoHdrNo) then + PointEDocumentToPostedDocument(PurchaseHeader, PurchCrMemoHdr, PurchCrMemoHdrNo, Enum::"E-Document Type"::"Purchase Credit Memo"); + exit; + end; + + if CommitIsSupressed then + exit; + if PurchInvHdrNo <> '' then begin - if PurchInvHeader.Get(PurchInvHdrNo) then - PointEDocumentToPostedDocument(PurchaseHeader, PurchInvHeader, PurchInvHdrNo, Enum::"E-Document Type"::"Purchase Invoice") + if not PurchInvHeader.Get(PurchInvHdrNo) then + exit; + PostedDocumentHeader.GetTable(PurchInvHeader); end else if PurchCrMemoHdr.Get(PurchCrMemoHdrNo) then - PointEDocumentToPostedDocument(PurchaseHeader, PurchCrMemoHdr, PurchCrMemoHdrNo, Enum::"E-Document Type"::"Purchase Credit Memo"); + PostedDocumentHeader.GetTable(PurchCrMemoHdr) + else + exit; + + DocumentSendingProfile := EDocumentProcessing.GetDocSendingProfileForDocRef(PostedDocumentHeader); + if DocumentSendingProfile.Code = '' then + exit; + + if PurchInvHdrNo <> '' then + CreateEDocumentFromPostedDocument(PurchInvHeader, DocumentSendingProfile, Enum::"E-Document Type"::"Purchase Invoice") + else + CreateEDocumentFromPostedDocument(PurchCrMemoHdr, DocumentSendingProfile, Enum::"E-Document Type"::"Purchase Credit Memo"); end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"TransferOrder-Post Shipment", OnAfterTransferOrderPostShipment, '', false, false)] From 9d7abd8c39a356cd8e7e78fc1dd885534f0c5f9a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Jul 2026 15:18:03 +0200 Subject: [PATCH 10/77] use standard comment table --- .../app/src/Core/FRRegulatoryComment.Table.al | 44 -------- .../Core/FRRegulatoryCommentDocType.Enum.al | 31 ------ .../Core/FRRegulatoryCommentMgt.Codeunit.al | 22 ++-- .../src/Core/FRRegulatoryCommentType.Enum.al | 10 +- .../app/src/Core/FRRegulatoryComments.Page.al | 36 ------- .../FRRegulatoryComments.PageExt.al | 102 ------------------ .../Extensions/FRSalesCommentLine.TableExt.al | 20 ++++ .../Extensions/FRSalesCommentSheet.PageExt.al | 22 ++++ .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 17 +-- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 61 +++++++++-- 10 files changed, 122 insertions(+), 243 deletions(-) delete mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComment.Table.al delete mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentDocType.Enum.al delete mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComments.Page.al delete mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRRegulatoryComments.PageExt.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentLine.TableExt.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentSheet.PageExt.al diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComment.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComment.Table.al deleted file mode 100644 index 073e74a80a3..00000000000 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComment.Table.al +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// 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 Regulatory Comment" -{ - Access = Internal; - Caption = 'French Regulatory Comment'; - DataClassification = CustomerContent; - - fields - { - field(1; "Document Type"; Enum "FR Reg. Comment Doc. Type") - { - Caption = 'Document Type'; - } - field(2; "Document No."; Code[20]) - { - Caption = 'Document No.'; - } - field(3; "Line No."; Integer) - { - Caption = 'Line No.'; - } - field(4; "Comment Type"; Enum "FR Regulatory Comment Type") - { - Caption = 'Comment Type'; - } - field(5; "Comment Text"; Text[250]) - { - Caption = 'Comment Text'; - } - } - - keys - { - key(PK; "Document Type", "Document No.", "Line No.") - { - Clustered = true; - } - } -} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentDocType.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentDocType.Enum.al deleted file mode 100644 index 5789d01e391..00000000000 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentDocType.Enum.al +++ /dev/null @@ -1,31 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// 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 Reg. Comment Doc. Type" -{ - Extensible = false; - - value(0; Order) - { - Caption = 'Order'; - } - value(1; Invoice) - { - Caption = 'Invoice'; - } - value(2; "Credit Memo") - { - Caption = 'Credit Memo'; - } - value(3; "Posted Invoice") - { - Caption = 'Posted Invoice'; - } - value(4; "Posted Credit Memo") - { - Caption = 'Posted Credit Memo'; - } -} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentMgt.Codeunit.al index fcf1341d8f7..cb84c502a76 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentMgt.Codeunit.al @@ -5,6 +5,7 @@ namespace Microsoft.eServices.EDocument.Formats; using Microsoft.Finance.GeneralLedger.Journal; +using Microsoft.Sales.Comment; using Microsoft.Sales.Customer; using Microsoft.Sales.Document; @@ -13,7 +14,7 @@ codeunit 10971 "FR Regulatory Comment Mgt." [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", OnAfterPostSalesDoc, '', false, false)] local procedure CopyCommentsAfterPosting(var SalesHeader: Record "Sales Header"; var GenJnlPostLine: Codeunit "Gen. Jnl.-Post Line"; SalesShptHdrNo: Code[20]; RetRcpHdrNo: Code[20]; SalesInvHdrNo: Code[20]; SalesCrMemoHdrNo: Code[20]; CommitIsSuppressed: Boolean; InvtPickPutaway: Boolean; var CustLedgerEntry: Record "Cust. Ledger Entry"; WhseShip: Boolean; WhseReceiv: Boolean; PreviewMode: Boolean) var - FromDocumentType: Enum "FR Reg. Comment Doc. Type"; + FromDocumentType: Enum "Sales Comment Document Type"; begin if PreviewMode then exit; @@ -35,19 +36,22 @@ codeunit 10971 "FR Regulatory Comment Mgt." CopyComments(FromDocumentType, SalesHeader."No.", FromDocumentType::"Posted Credit Memo", SalesCrMemoHdrNo); end; - local procedure CopyComments(FromDocumentType: Enum "FR Reg. Comment Doc. Type"; FromDocumentNo: Code[20]; ToDocumentType: Enum "FR Reg. Comment Doc. Type"; ToDocumentNo: Code[20]) + local procedure CopyComments(FromDocumentType: Enum "Sales Comment Document Type"; FromDocumentNo: Code[20]; ToDocumentType: Enum "Sales Comment Document Type"; ToDocumentNo: Code[20]) var - FromComment: Record "FR Regulatory Comment"; - ToComment: Record "FR Regulatory Comment"; + FromComment: Record "Sales Comment Line"; + ToComment: Record "Sales Comment Line"; begin FromComment.SetRange("Document Type", FromDocumentType); - FromComment.SetRange("Document No.", FromDocumentNo); + FromComment.SetRange("No.", FromDocumentNo); + FromComment.SetFilter("FR Regulatory Comment Type", '<>%1', FromComment."FR Regulatory Comment Type"::None); if FromComment.FindSet() then repeat - ToComment := FromComment; - ToComment."Document Type" := ToDocumentType; - ToComment."Document No." := ToDocumentNo; - ToComment.Insert(); + if not ToComment.Get(ToDocumentType, ToDocumentNo, FromComment."Document Line No.", FromComment."Line No.") then begin + ToComment := FromComment; + ToComment."Document Type" := ToDocumentType; + ToComment."No." := ToDocumentNo; + ToComment.Insert(); + end; until FromComment.Next() = 0; 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 index 377f3152194..a1f020e9717 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al @@ -8,15 +8,19 @@ enum 10971 "FR Regulatory Comment Type" { Extensible = false; - value(0; AAB) + value(0; None) + { + Caption = ''; + } + value(1; AAB) { Caption = 'AAB'; } - value(1; PMD) + value(2; PMD) { Caption = 'PMD'; } - value(2; PMT) + value(3; PMT) { Caption = 'PMT'; } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComments.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComments.Page.al deleted file mode 100644 index 57ffe1cda27..00000000000 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryComments.Page.al +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// 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 Regulatory Comments" -{ - ApplicationArea = All; - AutoSplitKey = true; - Caption = 'French Regulatory Comments'; - DataCaptionFields = "Document No."; - PageType = List; - SourceTable = "FR Regulatory Comment"; - UsageCategory = None; - - layout - { - area(Content) - { - repeater(Comments) - { - field("Comment Type"; Rec."Comment Type") - { - ApplicationArea = All; - ToolTip = 'Specifies the regulatory purpose of the comment.'; - } - field("Comment Text"; Rec."Comment Text") - { - ApplicationArea = All; - ToolTip = 'Specifies the regulatory comment included in the French electronic invoice.'; - } - } - } - } -} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRRegulatoryComments.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRRegulatoryComments.PageExt.al deleted file mode 100644 index 9c22d205551..00000000000 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRRegulatoryComments.PageExt.al +++ /dev/null @@ -1,102 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// 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; - -pageextension 10975 "FR Sales Order Comments" extends "Sales Order" -{ - actions - { - addlast("O&rder") - { - action(FRRegulatoryComments) - { - ApplicationArea = All; - Caption = 'French Regulatory Comments'; - Image = ViewComments; - RunObject = page "FR Regulatory Comments"; - RunPageLink = "Document Type" = const(Order), "Document No." = field("No."); - ToolTip = 'View or edit regulatory comments for French electronic invoicing.'; - } - } - } -} - -pageextension 10976 "FR Sales Invoice Comments" extends "Sales Invoice" -{ - actions - { - addlast(Processing) - { - action(FRRegulatoryComments) - { - ApplicationArea = All; - Caption = 'French Regulatory Comments'; - Image = ViewComments; - RunObject = page "FR Regulatory Comments"; - RunPageLink = "Document Type" = const(Invoice), "Document No." = field("No."); - ToolTip = 'View or edit regulatory comments for French electronic invoicing.'; - } - } - } -} - -pageextension 10977 "FR Sales CrMemo Comments" extends "Sales Credit Memo" -{ - actions - { - addlast(Processing) - { - action(FRRegulatoryComments) - { - ApplicationArea = All; - Caption = 'French Regulatory Comments'; - Image = ViewComments; - RunObject = page "FR Regulatory Comments"; - RunPageLink = "Document Type" = const("Credit Memo"), "Document No." = field("No."); - ToolTip = 'View or edit regulatory comments for French electronic invoicing.'; - } - } - } -} - -pageextension 10978 "FR Posted Sales Inv Comments" extends "Posted Sales Invoice" -{ - actions - { - addlast(Processing) - { - action(FRRegulatoryComments) - { - ApplicationArea = All; - Caption = 'French Regulatory Comments'; - Image = ViewComments; - RunObject = page "FR Regulatory Comments"; - RunPageLink = "Document Type" = const("Posted Invoice"), "Document No." = field("No."); - RunPageMode = View; - ToolTip = 'View the regulatory comments included in French electronic invoices.'; - } - } - } -} - -pageextension 10979 "FR Posted Sales CrMemo Comm" extends "Posted Sales Credit Memo" -{ - actions - { - addlast(Processing) - { - action(FRRegulatoryComments) - { - ApplicationArea = All; - Caption = 'French Regulatory Comments'; - Image = ViewComments; - RunObject = page "FR Regulatory Comments"; - RunPageLink = "Document Type" = const("Posted Credit Memo"), "Document No." = field("No."); - RunPageMode = View; - ToolTip = 'View the regulatory comments included in French electronic credit memos.'; - } - } - } -} \ 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..2eea8d7cd6c --- /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 when this comment must be included in the 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..78c1bc7d5ca --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentSheet.PageExt.al @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + ToolTip = 'Specifies the French regulatory purpose when this comment must be included in the electronic invoice.'; + } + } + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al index fcb5555efb9..964bdb64737 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 @@ -9,6 +9,7 @@ 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.History; using System.Utilities; @@ -189,11 +190,11 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" local procedure InjectRegulatoryComments(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; SourceDocumentHeader: RecordRef) var - RegulatoryComment: Record "FR Regulatory Comment"; + SalesCommentLine: Record "Sales Comment Line"; AnchorNode: XmlNode; NoteElement: XmlElement; DocumentNo: Code[20]; - DocumentType: Enum "FR Reg. Comment Doc. Type"; + DocumentType: Enum "Sales Comment Document Type"; begin case SourceDocumentHeader.Number of Database::"Sales Invoice Header": @@ -214,14 +215,16 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" exit; end; - RegulatoryComment.SetRange("Document Type", DocumentType); - RegulatoryComment.SetRange("Document No.", DocumentNo); - if RegulatoryComment.FindSet() then + SalesCommentLine.SetRange("Document Type", DocumentType); + SalesCommentLine.SetRange("No.", DocumentNo); + SalesCommentLine.SetRange("Document Line No.", 0); + SalesCommentLine.SetFilter("FR Regulatory Comment Type", '<>%1', SalesCommentLine."FR Regulatory Comment Type"::None); + if SalesCommentLine.FindSet() then repeat - NoteElement := XmlElement.Create('Note', CbcNamespaceTok, RegulatoryComment."Comment Text"); + NoteElement := XmlElement.Create('Note', CbcNamespaceTok, SalesCommentLine.Comment); AnchorNode.AddAfterSelf(NoteElement); AnchorNode := NoteElement.AsXmlNode(); - until RegulatoryComment.Next() = 0; + until SalesCommentLine.Next() = 0; end; local procedure InitNamespaceManager(var NamespaceMgr: XmlNamespaceManager; XmlDoc: XmlDocument) 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 550e9ca4694..e7095c953f3 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 @@ -12,6 +12,7 @@ using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Foundation.Address; using Microsoft.Foundation.Company; using Microsoft.Sales.Customer; +using Microsoft.Sales.Comment; using Microsoft.Sales.Document; using Microsoft.Sales.History; using Microsoft.Sales.Setup; @@ -22,7 +23,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Subtype = Test; Permissions = tabledata "E-Document Service" = rimd, tabledata "Company Information" = rimd, - tabledata "FR Regulatory Comment" = rimd, + tabledata "Sales Comment Line" = rimd, tabledata "Service Participant" = rimd, tabledata "Sales Invoice Line" = rimd, tabledata "Sales & Receivables Setup" = rimd, @@ -286,28 +287,66 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" [Test] procedure ExportSalesInvIncludesRegulatoryCommentAsNote() var - RegulatoryComment: Record "FR Regulatory Comment"; + SalesCommentLine: Record "Sales Comment Line"; SalesInvoiceHeader: Record "Sales Invoice Header"; XmlDoc: XmlDocument; - CommentText: Text[250]; + CommentText: Text[80]; begin // [SCENARIO] A maintained French regulatory comment is exported as a UBL header note Initialize(); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('', "Electronic Address Scheme"::"EM"))); CommentText := 'No discount is granted for early payment.'; - RegulatoryComment."Document Type" := RegulatoryComment."Document Type"::"Posted Invoice"; - RegulatoryComment."Document No." := SalesInvoiceHeader."No."; - RegulatoryComment."Line No." := 10000; - RegulatoryComment."Comment Type" := RegulatoryComment."Comment Type"::AAB; - RegulatoryComment."Comment Text" := CommentText; - RegulatoryComment.Insert(); + SalesCommentLine."Document Type" := SalesCommentLine."Document Type"::"Posted Invoice"; + SalesCommentLine."No." := SalesInvoiceHeader."No."; + SalesCommentLine."Line No." := 5000; + SalesCommentLine.Comment := 'Ordinary comment that must not be exported'; + SalesCommentLine.Insert(); + SalesCommentLine.Init(); + SalesCommentLine."Document Type" := SalesCommentLine."Document Type"::"Posted Invoice"; + SalesCommentLine."No." := SalesInvoiceHeader."No."; + SalesCommentLine."Line No." := 10000; + SalesCommentLine."FR Regulatory Comment Type" := SalesCommentLine."FR Regulatory Comment Type"::AAB; + SalesCommentLine.Comment := CommentText; + SalesCommentLine.Insert(); ExportInvoice(SalesInvoiceHeader, XmlDoc); Assert.AreEqual(CommentText, GetNodeByPath(XmlDoc, '/Invoice/cbc:Note'), StrSubstNo(IncorrectValueErr, 'Note')); end; + [Test] + procedure PostingCopiesRegulatoryCommentWhenStandardCommentCopyIsDisabled() + var + SalesCommentLine: Record "Sales Comment Line"; + SalesHeader: Record "Sales Header"; + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + CustomerNo: Code[20]; + PostedInvoiceNo: Code[20]; + begin + // [SCENARIO] Regulatory comments are retained during posting independently of the standard copy-comments setup + Initialize(); + + SalesReceivablesSetup.Get(); + SalesReceivablesSetup."Copy Comments Order to Invoice" := false; + SalesReceivablesSetup.Modify(); + CustomerNo := CreateCustomer('123456789', "Electronic Address Scheme"::"0002"); + SalesHeader.Get("Sales Document Type"::Invoice, CreateSalesInvoiceWithLine(CustomerNo)); + SalesCommentLine."Document Type" := SalesCommentLine."Document Type"::Invoice; + SalesCommentLine."No." := SalesHeader."No."; + SalesCommentLine."Line No." := 10000; + SalesCommentLine.Comment := 'Regulatory payment note'; + SalesCommentLine."FR Regulatory Comment Type" := SalesCommentLine."FR Regulatory Comment Type"::PMT; + SalesCommentLine.Insert(); + + PostedInvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, true, true); + + SalesCommentLine.SetRange("Document Type", SalesCommentLine."Document Type"::"Posted Invoice"); + SalesCommentLine.SetRange("No.", PostedInvoiceNo); + SalesCommentLine.SetRange("FR Regulatory Comment Type", SalesCommentLine."FR Regulatory Comment Type"::PMT); + Assert.RecordCount(SalesCommentLine, 1); + end; + [Test] procedure ExportSalesInvUsesServiceParticipantEndpointWithScheme0225() var @@ -659,11 +698,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" if AppendShipmentIndex then SalesInvoiceLine."Shipment No." := CopyStr(ShipmentNo + Format(LineIndex), 1, MaxStrLen(SalesInvoiceLine."Shipment No.")); else - SalesInvoiceLine."Shipment No." := CopyStr(ShipmentNo, 1, MaxStrLen(SalesInvoiceLine."Shipment No.")); + SalesInvoiceLine."Shipment No." := CopyStr(ShipmentNo, 1, MaxStrLen(SalesInvoiceLine."Shipment No.")); if AppendOrderIndex then SalesInvoiceLine."Order No." := CopyStr(OrderNo + Format(LineIndex), 1, MaxStrLen(SalesInvoiceLine."Order No.")); else - SalesInvoiceLine."Order No." := CopyStr(OrderNo, 1, MaxStrLen(SalesInvoiceLine."Order No.")); + SalesInvoiceLine."Order No." := CopyStr(OrderNo, 1, MaxStrLen(SalesInvoiceLine."Order No.")); SalesInvoiceLine.Modify(); until SalesInvoiceLine.Next() = 0; end; From 2aa69d6790a53e10446df303c07bb396daacae51 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Jul 2026 15:31:06 +0200 Subject: [PATCH 11/77] removed tooltips from pages --- .../app/src/Extensions/EReportingEDocuments.PageExt.al | 1 - .../app/src/Extensions/FRSalesCommentSheet.PageExt.al | 1 - .../app/src/Extensions/FRServiceParticipants.PageExt.al | 1 - 3 files changed, 3 deletions(-) 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..a6c47dd86a2 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,6 @@ 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.'; } } } 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 index 78c1bc7d5ca..1cb3ad99fa7 100644 --- 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 @@ -15,7 +15,6 @@ pageextension 10975 "FR Sales Comment Sheet" extends "Sales Comment Sheet" field("FR Regulatory Comment Type"; Rec."FR Regulatory Comment Type") { ApplicationArea = Comments; - ToolTip = 'Specifies the French regulatory purpose when this comment must be included in the electronic invoice.'; } } } 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 index a4c787a5ce2..a36b9e257d2 100644 --- 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 @@ -15,7 +15,6 @@ pageextension 10980 "FR Service Participants" extends "Service Participants" field("FR Identifier Scheme"; Rec."FR Identifier Scheme") { ApplicationArea = All; - ToolTip = 'Specifies the electronic address scheme used for French electronic invoicing.'; } } } From 1f46ee36947cee001b1567d6aa84d651d8191638 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Jul 2026 15:58:03 +0200 Subject: [PATCH 12/77] added test --- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) 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 e7095c953f3..7fcd82a39dd 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 @@ -563,6 +563,36 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" CompanyInformation.Modify(true); end; + [Test] + procedure ExportSalesInvUsesCompanyServiceParticipantEndpoint() + var + ServiceParticipant: Record "Service Participant"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + EndpointId: Text[200]; + begin + // [SCENARIO] A service-specific company participant overrides the company endpoint fallbacks + Initialize(); + + 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"))); + + CheckInvoice(SalesInvoiceHeader); + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + 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] procedure CheckRaisesErrorWhenSellerCountryCodeIsEmpty() var From 17b02fb400884422927380a2b943c67f6af0c6c4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Jul 2026 16:07:08 +0200 Subject: [PATCH 13/77] removed copy comments as standard works --- .../Core/FRRegulatoryCommentMgt.Codeunit.al | 57 ------------------- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 32 ----------- 2 files changed, 89 deletions(-) delete mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentMgt.Codeunit.al diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentMgt.Codeunit.al deleted file mode 100644 index cb84c502a76..00000000000 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentMgt.Codeunit.al +++ /dev/null @@ -1,57 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// 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.Finance.GeneralLedger.Journal; -using Microsoft.Sales.Comment; -using Microsoft.Sales.Customer; -using Microsoft.Sales.Document; - -codeunit 10971 "FR Regulatory Comment Mgt." -{ - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", OnAfterPostSalesDoc, '', false, false)] - local procedure CopyCommentsAfterPosting(var SalesHeader: Record "Sales Header"; var GenJnlPostLine: Codeunit "Gen. Jnl.-Post Line"; SalesShptHdrNo: Code[20]; RetRcpHdrNo: Code[20]; SalesInvHdrNo: Code[20]; SalesCrMemoHdrNo: Code[20]; CommitIsSuppressed: Boolean; InvtPickPutaway: Boolean; var CustLedgerEntry: Record "Cust. Ledger Entry"; WhseShip: Boolean; WhseReceiv: Boolean; PreviewMode: Boolean) - var - FromDocumentType: Enum "Sales Comment Document Type"; - begin - if PreviewMode then - exit; - - case SalesHeader."Document Type" of - SalesHeader."Document Type"::Order: - FromDocumentType := FromDocumentType::Order; - SalesHeader."Document Type"::Invoice: - FromDocumentType := FromDocumentType::Invoice; - SalesHeader."Document Type"::"Credit Memo": - FromDocumentType := FromDocumentType::"Credit Memo"; - else - exit; - end; - - if SalesInvHdrNo <> '' then - CopyComments(FromDocumentType, SalesHeader."No.", FromDocumentType::"Posted Invoice", SalesInvHdrNo); - if SalesCrMemoHdrNo <> '' then - CopyComments(FromDocumentType, SalesHeader."No.", FromDocumentType::"Posted Credit Memo", SalesCrMemoHdrNo); - end; - - local procedure CopyComments(FromDocumentType: Enum "Sales Comment Document Type"; FromDocumentNo: Code[20]; ToDocumentType: Enum "Sales Comment Document Type"; ToDocumentNo: Code[20]) - var - FromComment: Record "Sales Comment Line"; - ToComment: Record "Sales Comment Line"; - begin - FromComment.SetRange("Document Type", FromDocumentType); - FromComment.SetRange("No.", FromDocumentNo); - FromComment.SetFilter("FR Regulatory Comment Type", '<>%1', FromComment."FR Regulatory Comment Type"::None); - if FromComment.FindSet() then - repeat - if not ToComment.Get(ToDocumentType, ToDocumentNo, FromComment."Document Line No.", FromComment."Line No.") then begin - ToComment := FromComment; - ToComment."Document Type" := ToDocumentType; - ToComment."No." := ToDocumentNo; - ToComment.Insert(); - end; - until FromComment.Next() = 0; - end; -} \ No newline at end of file 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 7fcd82a39dd..53a8bfb904b 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 @@ -315,38 +315,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Assert.AreEqual(CommentText, GetNodeByPath(XmlDoc, '/Invoice/cbc:Note'), StrSubstNo(IncorrectValueErr, 'Note')); end; - [Test] - procedure PostingCopiesRegulatoryCommentWhenStandardCommentCopyIsDisabled() - var - SalesCommentLine: Record "Sales Comment Line"; - SalesHeader: Record "Sales Header"; - SalesReceivablesSetup: Record "Sales & Receivables Setup"; - CustomerNo: Code[20]; - PostedInvoiceNo: Code[20]; - begin - // [SCENARIO] Regulatory comments are retained during posting independently of the standard copy-comments setup - Initialize(); - - SalesReceivablesSetup.Get(); - SalesReceivablesSetup."Copy Comments Order to Invoice" := false; - SalesReceivablesSetup.Modify(); - CustomerNo := CreateCustomer('123456789', "Electronic Address Scheme"::"0002"); - SalesHeader.Get("Sales Document Type"::Invoice, CreateSalesInvoiceWithLine(CustomerNo)); - SalesCommentLine."Document Type" := SalesCommentLine."Document Type"::Invoice; - SalesCommentLine."No." := SalesHeader."No."; - SalesCommentLine."Line No." := 10000; - SalesCommentLine.Comment := 'Regulatory payment note'; - SalesCommentLine."FR Regulatory Comment Type" := SalesCommentLine."FR Regulatory Comment Type"::PMT; - SalesCommentLine.Insert(); - - PostedInvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, true, true); - - SalesCommentLine.SetRange("Document Type", SalesCommentLine."Document Type"::"Posted Invoice"); - SalesCommentLine.SetRange("No.", PostedInvoiceNo); - SalesCommentLine.SetRange("FR Regulatory Comment Type", SalesCommentLine."FR Regulatory Comment Type"::PMT); - Assert.RecordCount(SalesCommentLine, 1); - end; - [Test] procedure ExportSalesInvUsesServiceParticipantEndpointWithScheme0225() var From 02cc439b09e98047521e1f80a0c11128486d89cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Jul 2026 16:29:45 +0200 Subject: [PATCH 14/77] revert 1 --- .../EDocumentSubscribers.Codeunit.al | 35 ++----------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al index a52c42910d0..dcd26341345 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al @@ -282,46 +282,17 @@ codeunit 6103 "E-Document Subscribers" [EventSubscriber(ObjectType::Codeunit, Codeunit::"Purch.-Post", OnAfterPostPurchaseDoc, '', false, false)] local procedure OnAfterPostPurchaseDoc(var PurchaseHeader: Record "Purchase Header"; var GenJnlPostLine: Codeunit "Gen. Jnl.-Post Line"; PurchRcpHdrNo: Code[20]; RetShptHdrNo: Code[20]; PurchInvHdrNo: Code[20]; PurchCrMemoHdrNo: Code[20]; CommitIsSupressed: Boolean) var - EDocument: Record "E-Document"; - DocumentSendingProfile: Record "Document Sending Profile"; PurchInvHeader: Record "Purch. Inv. Header"; PurchCrMemoHdr: Record "Purch. Cr. Memo Hdr."; - PostedDocumentHeader: RecordRef; begin if (PurchInvHdrNo = '') and (PurchCrMemoHdrNo = '') then exit; - - if IsEDocumentLinkedToPurchaseDocument(EDocument, PurchaseHeader) then begin - if PurchInvHdrNo <> '' then begin - if PurchInvHeader.Get(PurchInvHdrNo) then - PointEDocumentToPostedDocument(PurchaseHeader, PurchInvHeader, PurchInvHdrNo, Enum::"E-Document Type"::"Purchase Invoice") - end else - if PurchCrMemoHdr.Get(PurchCrMemoHdrNo) then - PointEDocumentToPostedDocument(PurchaseHeader, PurchCrMemoHdr, PurchCrMemoHdrNo, Enum::"E-Document Type"::"Purchase Credit Memo"); - exit; - end; - - if CommitIsSupressed then - exit; - if PurchInvHdrNo <> '' then begin - if not PurchInvHeader.Get(PurchInvHdrNo) then - exit; - PostedDocumentHeader.GetTable(PurchInvHeader); + if PurchInvHeader.Get(PurchInvHdrNo) then + PointEDocumentToPostedDocument(PurchaseHeader, PurchInvHeader, PurchInvHdrNo, Enum::"E-Document Type"::"Purchase Invoice") end else if PurchCrMemoHdr.Get(PurchCrMemoHdrNo) then - PostedDocumentHeader.GetTable(PurchCrMemoHdr) - else - exit; - - DocumentSendingProfile := EDocumentProcessing.GetDocSendingProfileForDocRef(PostedDocumentHeader); - if DocumentSendingProfile.Code = '' then - exit; - - if PurchInvHdrNo <> '' then - CreateEDocumentFromPostedDocument(PurchInvHeader, DocumentSendingProfile, Enum::"E-Document Type"::"Purchase Invoice") - else - CreateEDocumentFromPostedDocument(PurchCrMemoHdr, DocumentSendingProfile, Enum::"E-Document Type"::"Purchase Credit Memo"); + PointEDocumentToPostedDocument(PurchaseHeader, PurchCrMemoHdr, PurchCrMemoHdrNo, Enum::"E-Document Type"::"Purchase Credit Memo"); end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"TransferOrder-Post Shipment", OnAfterTransferOrderPostShipment, '', false, false)] From 3d236c431517da5fd243498c779520069709d404 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 22 Jul 2026 12:58:38 +0200 Subject: [PATCH 15/77] "FR Regulatory Comment Type" extended and added more options --- .../src/Core/FRRegulatoryCommentType.Enum.al | 98 +++++++++++++++++-- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al index a1f020e9717..8fbceddd75f 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al @@ -6,22 +6,106 @@ namespace Microsoft.eServices.EDocument.Formats; enum 10971 "FR Regulatory Comment Type" { - Extensible = false; + Extensible = true; value(0; None) { Caption = ''; } - value(1; AAB) + value(1; AAA) { - Caption = 'AAB'; + Caption = 'AAA - Goods description'; } - value(2; PMD) + value(2; AAB) { - Caption = 'PMD'; + Caption = 'AAB - Terms of payments'; } - value(3; PMT) + value(3; AAC) { - Caption = 'PMT'; + 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'; } } \ No newline at end of file From d07ae2db12601edab6df340d9cec02565409be40 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 22 Jul 2026 13:22:05 +0200 Subject: [PATCH 16/77] update permissions --- .../app/src/Core/FREInvoiceLifecycle.Table.al | 24 +++++++++++++++++++ .../src/Core/FREInvoiceLifecycleVAT.Table.al | 6 +++++ .../EReportingEDocuments.PageExt.al | 1 - .../Extensions/FREInvoiceLifecycles.Page.al | 10 +------- .../D365ReadEReportingFR.PermissionSetExt.al | 12 ---------- ...TeamMemberEReportingFR.PermissionSetExt.al | 12 ---------- .../EReportingFREdit.PermissionSet.al | 5 ++-- ...> EReportingFREditExt.PermissionSetExt.al} | 6 ++--- .../EReportingFRObjects.PermissionSet.al | 19 --------------- .../EReportingFRRead.PermissionSet.al | 7 +++--- ...> EReportingFRReadExt.PermissionSetExt.al} | 6 ++--- .../LocalEReportingFR.PermissionSetExt.al | 12 ---------- 12 files changed, 43 insertions(+), 77 deletions(-) delete mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365ReadEReportingFR.PermissionSetExt.al delete mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365TeamMemberEReportingFR.PermissionSetExt.al rename src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/{D365BasicEReportingFR.PermissionSetExt.al => EReportingFREditExt.PermissionSetExt.al} (78%) delete mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al rename src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/{LocalReadEReportingFR.PermissionSetExt.al => EReportingFRReadExt.PermissionSetExt.al} (78%) delete mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalEReportingFR.PermissionSetExt.al 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 index c9a8efc200a..d06b79721cd 100644 --- 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 @@ -13,6 +13,7 @@ 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; @@ -23,28 +24,33 @@ table 10970 "FR E-Invoice Lifecycle" 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) { @@ -52,95 +58,113 @@ table 10970 "FR E-Invoice Lifecycle" 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.Code; + ToolTip = 'Specifies the currency of the reported amount.'; } 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 = CustomerContent; + 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 = CustomerContent; + 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.'; } } 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 index 1613d83c250..dddd0a37a1c 100644 --- 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 @@ -10,6 +10,7 @@ table 10971 "FR E-Invoice Lifecycle VAT" { Caption = 'FR E-Invoice Lifecycle VAT'; DataClassification = CustomerContent; + InherentPermissions = X; ReplicateData = false; fields @@ -19,17 +20,20 @@ table 10971 "FR E-Invoice Lifecycle VAT" 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) { Caption = 'VAT %'; DataClassification = CustomerContent; DecimalPlaces = 0 : 5; + ToolTip = 'Specifies the VAT rate for this breakdown line.'; } field(4; "Reported Amount"; Decimal) { @@ -37,12 +41,14 @@ table 10971 "FR E-Invoice Lifecycle VAT" 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; TableRelation = Currency.Code; + ToolTip = 'Specifies the currency of the reported VAT amount.'; } } 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 a30e75a4bc0..e760b97b0eb 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,6 @@ 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.'; } } } 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 index be1cfc8087a..9027e1e2a75 100644 --- 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 @@ -9,6 +9,7 @@ page 10970 "FR E-Invoice Lifecycles" ApplicationArea = Basic, Suite; Caption = 'E-Invoice Lifecycles'; Editable = false; + InherentPermissions = X; PageType = List; SourceTable = "FR E-Invoice Lifecycle"; UsageCategory = History; @@ -22,47 +23,38 @@ page 10970 "FR E-Invoice Lifecycles" field("E-Document Entry No."; Rec."E-Document Entry No.") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the related e-document entry.'; } field("Lifecycle Status"; Rec."Lifecycle Status") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the French electronic invoice lifecycle status.'; } field("Reported Amount"; Rec."Reported Amount") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the amount reported for this lifecycle occurrence.'; } field("Currency Code"; Rec."Currency Code") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the currency of the reported amount.'; } field("Event Date"; Rec."Event Date") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the date of the lifecycle event.'; } field("Processing Status"; Rec."Processing Status") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the processing status of the lifecycle occurrence.'; } field("E-Document Message Entry No."; Rec."E-Document Message Entry No.") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the related E-Document message entry.'; } field("Created At"; Rec."Created At") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies when the lifecycle occurrence was created.'; } field("Last Error"; Rec."Last Error") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the last processing error for the lifecycle occurrence.'; } } } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365ReadEReportingFR.PermissionSetExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365ReadEReportingFR.PermissionSetExt.al deleted file mode 100644 index 61d31660b65..00000000000 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365ReadEReportingFR.PermissionSetExt.al +++ /dev/null @@ -1,12 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// 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 System.Security.AccessControl; - -permissionsetextension 10975 "D365 READ - E-Reporting FR" extends "D365 READ" -{ - IncludedPermissionSets = "E-Reporting FR - Read"; -} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365TeamMemberEReportingFR.PermissionSetExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365TeamMemberEReportingFR.PermissionSetExt.al deleted file mode 100644 index 95514bc3b8c..00000000000 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365TeamMemberEReportingFR.PermissionSetExt.al +++ /dev/null @@ -1,12 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// 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 System.Security.AccessControl; - -permissionsetextension 10977 "D365 TEAM MEMBER - ER FR" extends "D365 TEAM MEMBER" -{ - IncludedPermissionSets = "E-Reporting FR - Read"; -} \ No newline at end of file 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 index f9c70a55b09..36c179085b5 100644 --- 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 @@ -6,8 +6,9 @@ namespace Microsoft.eServices.EDocument.Formats; permissionset 10972 "E-Reporting FR - Edit" { - Access = Internal; - Assignable = false; + Access = Public; + Assignable = true; + Caption = 'E-Reporting FR - Edit'; IncludedPermissionSets = "E-Reporting FR - Read"; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365BasicEReportingFR.PermissionSetExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFREditExt.PermissionSetExt.al similarity index 78% rename from src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365BasicEReportingFR.PermissionSetExt.al rename to src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFREditExt.PermissionSetExt.al index 1fbf370a76d..ea5e64db2ec 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/D365BasicEReportingFR.PermissionSetExt.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFREditExt.PermissionSetExt.al @@ -4,9 +4,9 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Formats; -using System.Security.AccessControl; +using Microsoft.eServices.EDocument; -permissionsetextension 10976 "D365 BASIC - E-Reporting FR" extends "D365 BASIC" +permissionsetextension 10975 "E-Reporting FR - Edit Ext" extends "E-Doc. Core - User" { IncludedPermissionSets = "E-Reporting FR - Edit"; -} \ No newline at end of file +} diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al deleted file mode 100644 index ae4a5b7d25a..00000000000 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRObjects.PermissionSet.al +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// 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 10970 "E-Reporting FR - Objects" -{ - Access = Internal; - Assignable = false; - - Permissions = table "FR E-Invoice Lifecycle" = X, - table "FR E-Invoice Lifecycle VAT" = X, - codeunit "FR E-Invoice Lifecycle Mgt." = X, - codeunit "FR E-Invoice Lifecycle Msg." = X, - codeunit "FR E-Invoice Lifecycle Worker" = X, - codeunit "FR E-Invoice Lifecycle Error" = X, - page "FR E-Invoice Lifecycles" = X; -} \ No newline at end of file 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 index a41c394fc5e..35b888df0ad 100644 --- 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 @@ -6,10 +6,9 @@ namespace Microsoft.eServices.EDocument.Formats; permissionset 10971 "E-Reporting FR - Read" { - Access = Internal; - Assignable = false; - - IncludedPermissionSets = "E-Reporting FR - Objects"; + Access = Public; + Assignable = true; + Caption = 'E-Reporting FR - Read'; Permissions = tabledata "FR E-Invoice Lifecycle" = R, tabledata "FR E-Invoice Lifecycle VAT" = R; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalReadEReportingFR.PermissionSetExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRReadExt.PermissionSetExt.al similarity index 78% rename from src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalReadEReportingFR.PermissionSetExt.al rename to src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRReadExt.PermissionSetExt.al index 9f079a29995..4f56fbdc6e0 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalReadEReportingFR.PermissionSetExt.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRReadExt.PermissionSetExt.al @@ -4,9 +4,9 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Formats; -using System.Security.AccessControl; +using Microsoft.eServices.EDocument; -permissionsetextension 10979 "LOCAL READ - E-Reporting FR" extends "LOCAL READ" +permissionsetextension 10976 "E-Reporting FR - Read Ext" extends "E-Doc. Core - Read" { IncludedPermissionSets = "E-Reporting FR - Read"; -} \ No newline at end of file +} diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalEReportingFR.PermissionSetExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalEReportingFR.PermissionSetExt.al deleted file mode 100644 index 7e76e64eeb0..00000000000 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/LocalEReportingFR.PermissionSetExt.al +++ /dev/null @@ -1,12 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// 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 System.Security.AccessControl; - -permissionsetextension 10978 "LOCAL - E-Reporting FR" extends LOCAL -{ - IncludedPermissionSets = "E-Reporting FR - Edit"; -} \ No newline at end of file From 8f3d92572e8ba1bd9e9f8a3dcedc1b3366e5a287 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:52:32 +0000 Subject: [PATCH 17/77] Fix AL0129 in InjectExtendedCTCFranceElements: introduce local XmlElement variable --- .../app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 964bdb64737..4ef6bb767c1 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 @@ -103,6 +103,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" SalesInvoiceHeader: Record "Sales Invoice Header"; SalesInvoiceLine: Record "Sales Invoice Line"; CustomizationIdNode: XmlNode; + CustomizationIdElement: XmlElement; begin if SourceDocumentHeader.Number <> Database::"Sales Invoice Header" then exit; @@ -111,8 +112,10 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" if not RequiresExtendedCTCFrance(SalesInvoiceHeader."No.") then exit; - if XmlDoc.SelectSingleNode('/*/cbc:CustomizationID', NamespaceMgr, CustomizationIdNode) then - CustomizationIdNode.AsXmlElement().InnerText := ExtendedCTCFranceCustomizationIdTok; + if XmlDoc.SelectSingleNode('/*/cbc:CustomizationID', NamespaceMgr, CustomizationIdNode) then begin + CustomizationIdElement := CustomizationIdNode.AsXmlElement(); + CustomizationIdElement.InnerText := ExtendedCTCFranceCustomizationIdTok; + end; SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); SalesInvoiceLine.SetFilter("Shipment No.", '<>%1', ''); From f1b42528827bdebbf4e574d0a220ed90102e25c6 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 27 Jul 2026 11:20:17 +0200 Subject: [PATCH 18/77] new update --- .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 21 +++++---- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 44 ++++++++++++++++++- 2 files changed, 55 insertions(+), 10 deletions(-) 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 4ef6bb767c1..487338ef15f 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 @@ -114,11 +114,10 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" if XmlDoc.SelectSingleNode('/*/cbc:CustomizationID', NamespaceMgr, CustomizationIdNode) then begin CustomizationIdElement := CustomizationIdNode.AsXmlElement(); - CustomizationIdElement.InnerText := ExtendedCTCFranceCustomizationIdTok; + CustomizationIdElement.InnerText(ExtendedCTCFranceCustomizationIdTok); end; SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); - SalesInvoiceLine.SetFilter("Shipment No.", '<>%1', ''); if SalesInvoiceLine.FindSet() then repeat InjectExtendedLineReferences(XmlDoc, NamespaceMgr, SalesInvoiceLine); @@ -165,11 +164,6 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" DeliveryElement: XmlElement; LineXPath: Text; begin - if not SalesShipmentLine.Get(SalesInvoiceLine."Shipment No.", SalesInvoiceLine."Shipment Line No.") then - exit; - if not SalesShipmentHeader.Get(SalesShipmentLine."Document No.") then - exit; - LineXPath := StrSubstNo('/*/cac:InvoiceLine[cbc:ID=''%1'']', Format(SalesInvoiceLine."Line No.", 0, 9)); if not XmlDoc.SelectSingleNode(LineXPath, NamespaceMgr, InvoiceLineNode) then exit; @@ -185,6 +179,11 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" ItemNode.AddBeforeSelf(OrderLineReferenceElement); end; + if not SalesShipmentLine.Get(SalesInvoiceLine."Shipment No.", SalesInvoiceLine."Shipment Line No.") then + exit; + if not SalesShipmentHeader.Get(SalesShipmentLine."Document No.") then + exit; + DeliveryElement := XmlElement.Create('Delivery', CacNamespaceTok); DeliveryElement.Add(XmlElement.Create('ID', CbcNamespaceTok, SalesShipmentLine."Document No.")); DeliveryElement.Add(XmlElement.Create('ActualDeliveryDate', CbcNamespaceTok, Format(SalesShipmentHeader."Posting Date", 0, 9))); @@ -224,12 +223,17 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" SalesCommentLine.SetFilter("FR Regulatory Comment Type", '<>%1', SalesCommentLine."FR Regulatory Comment Type"::None); if SalesCommentLine.FindSet() then repeat - NoteElement := XmlElement.Create('Note', CbcNamespaceTok, SalesCommentLine.Comment); + 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; @@ -504,4 +508,5 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" 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', Locked = true; } 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 53a8bfb904b..4847ce4272b 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 @@ -26,6 +26,8 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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; @@ -312,7 +314,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" ExportInvoice(SalesInvoiceHeader, XmlDoc); - Assert.AreEqual(CommentText, GetNodeByPath(XmlDoc, '/Invoice/cbc:Note'), StrSubstNo(IncorrectValueErr, 'Note')); + Assert.AreEqual('#AAB#' + CommentText, GetNodeByPath(XmlDoc, '/Invoice/cbc:Note'), StrSubstNo(IncorrectValueErr, 'Note')); end; [Test] @@ -395,24 +397,33 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" procedure ExportSalesInvSelectsExtendedCTCForMultipleOrders() var SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; XmlDoc: XmlDocument; begin // [SCENARIO] An invoice containing lines from distinct orders uses the Extended CTC profile Initialize(); SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithTwoLines(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); - SetPostedInvoiceLineReferences(SalesInvoiceHeader."No.", 'SHIPMENT-1', 'ORDER-', false, true); + SetPostedInvoiceLineReferences(SalesInvoiceHeader."No.", '', 'ORDER-', false, true); ExportInvoice(SalesInvoiceHeader, XmlDoc); Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); + SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + 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 // [SCENARIO] An invoice containing lines from distinct shipments uses the Extended CTC profile @@ -425,6 +436,13 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); + SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + 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] @@ -697,14 +715,36 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceLine."Shipment No." := CopyStr(ShipmentNo + Format(LineIndex), 1, MaxStrLen(SalesInvoiceLine."Shipment No.")); else SalesInvoiceLine."Shipment No." := CopyStr(ShipmentNo, 1, MaxStrLen(SalesInvoiceLine."Shipment No.")); + SalesInvoiceLine."Shipment Line No." := SalesInvoiceLine."Line No."; if AppendOrderIndex then SalesInvoiceLine."Order No." := CopyStr(OrderNo + Format(LineIndex), 1, MaxStrLen(SalesInvoiceLine."Order No.")); else SalesInvoiceLine."Order No." := CopyStr(OrderNo, 1, MaxStrLen(SalesInvoiceLine."Order No.")); + SalesInvoiceLine."Order Line No." := SalesInvoiceLine."Line No."; SalesInvoiceLine.Modify(); + if SalesInvoiceLine."Shipment No." <> '' then + CreatePostedShipmentReference(SalesInvoiceLine."Shipment No.", SalesInvoiceLine."Shipment Line No."); until SalesInvoiceLine.Next() = 0; end; + local procedure CreatePostedShipmentReference(ShipmentNo: Code[20]; ShipmentLineNo: Integer) + var + SalesShipmentHeader: Record "Sales Shipment Header"; + SalesShipmentLine: Record "Sales Shipment Line"; + begin + if not SalesShipmentHeader.Get(ShipmentNo) then begin + SalesShipmentHeader.Init(); + SalesShipmentHeader."No." := ShipmentNo; + SalesShipmentHeader."Posting Date" := WorkDate(); + SalesShipmentHeader.Insert(); + end; + + SalesShipmentLine.Init(); + SalesShipmentLine."Document No." := ShipmentNo; + SalesShipmentLine."Line No." := ShipmentLineNo; + SalesShipmentLine.Insert(); + end; + local procedure CreateSalesInvoiceWithLine(CustomerNo: Code[20]): Code[20] var Customer: Record Customer; From fb05a47ac4fb9aa6bf62ae061a74234e1cce3a6e Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 27 Jul 2026 11:59:15 +0200 Subject: [PATCH 19/77] fix for node --- .../app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 487338ef15f..3e120cf43de 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 @@ -103,7 +103,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" SalesInvoiceHeader: Record "Sales Invoice Header"; SalesInvoiceLine: Record "Sales Invoice Line"; CustomizationIdNode: XmlNode; - CustomizationIdElement: XmlElement; + NewCustomizationIdNode: XmlNode; begin if SourceDocumentHeader.Number <> Database::"Sales Invoice Header" then exit; @@ -113,8 +113,8 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" exit; if XmlDoc.SelectSingleNode('/*/cbc:CustomizationID', NamespaceMgr, CustomizationIdNode) then begin - CustomizationIdElement := CustomizationIdNode.AsXmlElement(); - CustomizationIdElement.InnerText(ExtendedCTCFranceCustomizationIdTok); + NewCustomizationIdNode := XmlElement.Create('CustomizationID', CbcNamespaceTok, ExtendedCTCFranceCustomizationIdTok).AsXmlNode(); + CustomizationIdNode.ReplaceWith(NewCustomizationIdNode); end; SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); From f40c964675acd0c10ac9741fbd22dae07dd7539b Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 27 Jul 2026 15:02:43 +0200 Subject: [PATCH 20/77] payment means --- .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 15 +- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 150 ++++++++++++++++-- 2 files changed, 148 insertions(+), 17 deletions(-) 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 3e120cf43de..8ff8d78a7c3 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 @@ -87,6 +87,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" InjectSupplierIdentification(XmlDoc, NamespaceMgr, CompanyInformation); InjectSupplierEndpoint(XmlDoc, NamespaceMgr, CompanyInformation, EDocumentService.Code); InjectRegulatoryComments(XmlDoc, NamespaceMgr, SourceDocumentHeader); + InjectPaymentMeansCode(XmlDoc, NamespaceMgr); InjectExtendedCTCFranceElements(XmlDoc, NamespaceMgr, SourceDocumentHeader); HasElecAddress := GetCustomerElecAddress(SourceDocumentHeader, EDocumentService.Code, ElecAddress, ElecAddressScheme); @@ -234,6 +235,14 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" exit(RegulatoryCommentType.Names.Get(RegulatoryCommentType.Ordinals.IndexOf(RegulatoryCommentType.AsInteger()))); end; + local procedure InjectPaymentMeansCode(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager) + var + PaymentMeansCodeNode: XmlNode; + begin + if XmlDoc.SelectSingleNode('/*/cac:PaymentMeans/cbc:PaymentMeansCode', NamespaceMgr, PaymentMeansCodeNode) then + PaymentMeansCodeNode.AsXmlElement().ReplaceWith(XmlElement.Create('PaymentMeansCode', CbcNamespaceTok, PaymentMeansCreditTransferCodeTok)); + end; + local procedure InitNamespaceManager(var NamespaceMgr: XmlNamespaceManager; XmlDoc: XmlDocument) var RootElement: XmlElement; @@ -497,16 +506,12 @@ 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(); - - EDocServiceSupportedType."Source Document Type" := EDocServiceSupportedType."Source Document Type"::"Purchase Credit Memo"; - 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; + PaymentMeansCreditTransferCodeTok: Label '58', Locked = true; RegulatoryCommentFormatTok: Label '#%1#%2', Locked = true; } 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 4847ce4272b..e6bf89ca03d 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 @@ -25,6 +25,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" tabledata "Company Information" = rimd, tabledata "Sales Comment Line" = rimd, tabledata "Service Participant" = rimd, + tabledata "Sales Invoice Header" = rimd, tabledata "Sales Invoice Line" = rimd, tabledata "Sales Shipment Header" = rimd, tabledata "Sales Shipment Line" = rimd, @@ -349,6 +350,22 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); end; + [Test] + procedure ExportSalesInvUsesCreditTransferPaymentMeansCode() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + begin + // [SCENARIO] A French PEPPOL invoice uses generic credit transfer as payment means + Initialize(); + + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + Assert.AreEqual('58', GetNodeByPath(XmlDoc, '/Invoice/cac:PaymentMeans/cbc:PaymentMeansCode'), + StrSubstNo(IncorrectValueErr, 'PaymentMeansCode')); + end; + #endregion #region SalesCreditMemo @@ -393,6 +410,22 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); end; + [Test] + procedure ExportSalesCrMemoUsesCreditTransferPaymentMeansCode() + var + SalesCrMemoHeader: Record "Sales Cr.Memo Header"; + XmlDoc: XmlDocument; + begin + // [SCENARIO] A French PEPPOL credit memo uses generic credit transfer as payment means + Initialize(); + + SalesCrMemoHeader.Get(CreateAndPostSalesCrMemo(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + ExportCrMemo(SalesCrMemoHeader, XmlDoc); + + Assert.AreEqual('58', GetNodeByPath(XmlDoc, '/CreditNote/cac:PaymentMeans/cbc:PaymentMeansCode'), + StrSubstNo(IncorrectValueErr, 'PaymentMeansCode')); + end; + [Test] procedure ExportSalesInvSelectsExtendedCTCForMultipleOrders() var @@ -411,11 +444,10 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); - 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')); + SalesInvoiceLine.FindSet(); + repeat + AssertInvoiceLineOrderReference(XmlDoc, SalesInvoiceLine); + until SalesInvoiceLine.Next() = 0; end; [Test] @@ -437,12 +469,46 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); - 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')); + SalesInvoiceLine.FindSet(); + repeat + SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No."); + AssertInvoiceLineOrderReference(XmlDoc, SalesInvoiceLine); + AssertInvoiceLineDeliveryReference(XmlDoc, SalesInvoiceLine, SalesShipmentHeader."Posting Date"); + until SalesInvoiceLine.Next() = 0; + end; + + [Test] + procedure ExportSalesInvSelectsExtendedCTCForMultipleDeliveryDates() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + SalesShipmentHeader: Record "Sales Shipment Header"; + XmlDoc: XmlDocument; + DeliveryDate: Date; + begin + // [SCENARIO] Consolidated shipments with different delivery dates use Extended CTC and retain each line date + Initialize(); + + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithTwoLines(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + SetPostedInvoiceLineReferences(SalesInvoiceHeader."No.", 'SHIPMENT-', 'ORDER-1', true, false); + SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + SalesInvoiceLine.FindSet(); + repeat + DeliveryDate := WorkDate() + SalesInvoiceLine."Line No."; + SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No."); + SalesShipmentHeader."Posting Date" := DeliveryDate; + SalesShipmentHeader.Modify(); + until SalesInvoiceLine.Next() = 0; + + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), + StrSubstNo(IncorrectValueErr, 'CustomizationID')); + SalesInvoiceLine.FindSet(); + repeat + SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No."); + AssertInvoiceLineDeliveryReference(XmlDoc, SalesInvoiceLine, SalesShipmentHeader."Posting Date"); + until SalesInvoiceLine.Next() = 0; end; [Test] @@ -462,6 +528,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Assert.AreNotEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); end; + #endregion #region Validation @@ -579,6 +646,35 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" StrSubstNo(IncorrectValueErr, 'Seller EndpointID schemeID')); end; + [Test] + procedure ExportSalesInvUsesCompanyRoutingEndpointWithScheme0225() + var + ServiceParticipant: Record "Service Participant"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + EndpointId: Text[200]; + begin + // [SCENARIO] A service-specific company routing identifier is exported with scheme 0225 + Initialize(); + + EndpointId := CompanyInformation."Registration No." + '_001'; + ServiceParticipant.Service := EDocumentService.Code; + ServiceParticipant."Participant Type" := ServiceParticipant."Participant Type"::Company; + ServiceParticipant."Participant Identifier" := EndpointId; + ServiceParticipant."FR Identifier Scheme" := ServiceParticipant."FR Identifier Scheme"::"0225"; + ServiceParticipant.Insert(); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + Assert.AreEqual(EndpointId, + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID'), + StrSubstNo(IncorrectValueErr, 'Seller EndpointID')); + Assert.AreEqual('0225', + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID'), + StrSubstNo(IncorrectValueErr, 'Seller EndpointID schemeID')); + end; + [Test] procedure CheckRaisesErrorWhenSellerCountryCodeIsEmpty() var @@ -745,6 +841,28 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesShipmentLine.Insert(); end; + local procedure AssertInvoiceLineOrderReference(XmlDoc: XmlDocument; SalesInvoiceLine: Record "Sales Invoice Line") + var + InvoiceLinePath: Text; + begin + InvoiceLinePath := StrSubstNo('/Invoice/cac:InvoiceLine[cbc:ID=''%1'']', Format(SalesInvoiceLine."Line No.", 0, 9)); + Assert.AreEqual(SalesInvoiceLine."Order No.", GetNodeByPath(XmlDoc, InvoiceLinePath + '/cac:OrderLineReference/cac:OrderReference/cbc:ID'), + StrSubstNo(IncorrectValueErr, 'OrderReference ID')); + Assert.AreEqual(Format(SalesInvoiceLine."Order Line No.", 0, 9), GetNodeByPath(XmlDoc, InvoiceLinePath + '/cac:OrderLineReference/cbc:LineID'), + StrSubstNo(IncorrectValueErr, 'OrderLineReference LineID')); + end; + + local procedure AssertInvoiceLineDeliveryReference(XmlDoc: XmlDocument; SalesInvoiceLine: Record "Sales Invoice Line"; DeliveryDate: Date) + var + InvoiceLinePath: Text; + begin + InvoiceLinePath := StrSubstNo('/Invoice/cac:InvoiceLine[cbc:ID=''%1'']', Format(SalesInvoiceLine."Line No.", 0, 9)); + Assert.AreEqual(SalesInvoiceLine."Shipment No.", GetNodeByPath(XmlDoc, InvoiceLinePath + '/cac:Delivery/cbc:ID'), + StrSubstNo(IncorrectValueErr, 'Delivery ID')); + Assert.AreEqual(Format(DeliveryDate, 0, 9), GetNodeByPath(XmlDoc, InvoiceLinePath + '/cac:Delivery/cbc:ActualDeliveryDate'), + StrSubstNo(IncorrectValueErr, 'ActualDeliveryDate')); + end; + local procedure CreateSalesInvoiceWithLine(CustomerNo: Code[20]): Code[20] var Customer: Record Customer; @@ -889,6 +1007,14 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" end; local procedure CreateAndPostSalesCrMemo(CustomerNo: Code[20]): Code[20] + var + SalesHeader: Record "Sales Header"; + begin + SalesHeader.Get("Sales Document Type"::"Credit Memo", CreateSalesCrMemo(CustomerNo)); + exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + end; + + local procedure CreateSalesCrMemo(CustomerNo: Code[20]): Code[20] var Customer: Record Customer; GLAccount: Record "G/L Account"; @@ -919,7 +1045,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesLine.Validate("Unit Price", 100); SalesLine.Modify(true); - exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + exit(SalesHeader."No."); end; local procedure ExportCrMemo(SalesCrMemoHeader: Record "Sales Cr.Memo Header"; var XmlDoc: XmlDocument) From 00f027fd4f0846820907fdd2844c19edf2073a16 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 27 Jul 2026 15:12:38 +0200 Subject: [PATCH 21/77] Revert "payment means" This reverts commit f40c964675acd0c10ac9741fbd22dae07dd7539b. --- .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 15 +- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 150 ++---------------- 2 files changed, 17 insertions(+), 148 deletions(-) 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 8ff8d78a7c3..3e120cf43de 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 @@ -87,7 +87,6 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" InjectSupplierIdentification(XmlDoc, NamespaceMgr, CompanyInformation); InjectSupplierEndpoint(XmlDoc, NamespaceMgr, CompanyInformation, EDocumentService.Code); InjectRegulatoryComments(XmlDoc, NamespaceMgr, SourceDocumentHeader); - InjectPaymentMeansCode(XmlDoc, NamespaceMgr); InjectExtendedCTCFranceElements(XmlDoc, NamespaceMgr, SourceDocumentHeader); HasElecAddress := GetCustomerElecAddress(SourceDocumentHeader, EDocumentService.Code, ElecAddress, ElecAddressScheme); @@ -235,14 +234,6 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" exit(RegulatoryCommentType.Names.Get(RegulatoryCommentType.Ordinals.IndexOf(RegulatoryCommentType.AsInteger()))); end; - local procedure InjectPaymentMeansCode(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager) - var - PaymentMeansCodeNode: XmlNode; - begin - if XmlDoc.SelectSingleNode('/*/cac:PaymentMeans/cbc:PaymentMeansCode', NamespaceMgr, PaymentMeansCodeNode) then - PaymentMeansCodeNode.AsXmlElement().ReplaceWith(XmlElement.Create('PaymentMeansCode', CbcNamespaceTok, PaymentMeansCreditTransferCodeTok)); - end; - local procedure InitNamespaceManager(var NamespaceMgr: XmlNamespaceManager; XmlDoc: XmlDocument) var RootElement: XmlElement; @@ -506,12 +497,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(); + + EDocServiceSupportedType."Source Document Type" := EDocServiceSupportedType."Source Document Type"::"Purchase Credit Memo"; + 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; - PaymentMeansCreditTransferCodeTok: Label '58', Locked = true; RegulatoryCommentFormatTok: Label '#%1#%2', Locked = true; } 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 e6bf89ca03d..4847ce4272b 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 @@ -25,7 +25,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" tabledata "Company Information" = rimd, tabledata "Sales Comment Line" = rimd, tabledata "Service Participant" = rimd, - tabledata "Sales Invoice Header" = rimd, tabledata "Sales Invoice Line" = rimd, tabledata "Sales Shipment Header" = rimd, tabledata "Sales Shipment Line" = rimd, @@ -350,22 +349,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); end; - [Test] - procedure ExportSalesInvUsesCreditTransferPaymentMeansCode() - var - SalesInvoiceHeader: Record "Sales Invoice Header"; - XmlDoc: XmlDocument; - begin - // [SCENARIO] A French PEPPOL invoice uses generic credit transfer as payment means - Initialize(); - - SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); - ExportInvoice(SalesInvoiceHeader, XmlDoc); - - Assert.AreEqual('58', GetNodeByPath(XmlDoc, '/Invoice/cac:PaymentMeans/cbc:PaymentMeansCode'), - StrSubstNo(IncorrectValueErr, 'PaymentMeansCode')); - end; - #endregion #region SalesCreditMemo @@ -410,22 +393,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); end; - [Test] - procedure ExportSalesCrMemoUsesCreditTransferPaymentMeansCode() - var - SalesCrMemoHeader: Record "Sales Cr.Memo Header"; - XmlDoc: XmlDocument; - begin - // [SCENARIO] A French PEPPOL credit memo uses generic credit transfer as payment means - Initialize(); - - SalesCrMemoHeader.Get(CreateAndPostSalesCrMemo(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); - ExportCrMemo(SalesCrMemoHeader, XmlDoc); - - Assert.AreEqual('58', GetNodeByPath(XmlDoc, '/CreditNote/cac:PaymentMeans/cbc:PaymentMeansCode'), - StrSubstNo(IncorrectValueErr, 'PaymentMeansCode')); - end; - [Test] procedure ExportSalesInvSelectsExtendedCTCForMultipleOrders() var @@ -444,10 +411,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); - SalesInvoiceLine.FindSet(); - repeat - AssertInvoiceLineOrderReference(XmlDoc, SalesInvoiceLine); - until SalesInvoiceLine.Next() = 0; + 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] @@ -469,46 +437,12 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); - SalesInvoiceLine.FindSet(); - repeat - SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No."); - AssertInvoiceLineOrderReference(XmlDoc, SalesInvoiceLine); - AssertInvoiceLineDeliveryReference(XmlDoc, SalesInvoiceLine, SalesShipmentHeader."Posting Date"); - until SalesInvoiceLine.Next() = 0; - end; - - [Test] - procedure ExportSalesInvSelectsExtendedCTCForMultipleDeliveryDates() - var - SalesInvoiceHeader: Record "Sales Invoice Header"; - SalesInvoiceLine: Record "Sales Invoice Line"; - SalesShipmentHeader: Record "Sales Shipment Header"; - XmlDoc: XmlDocument; - DeliveryDate: Date; - begin - // [SCENARIO] Consolidated shipments with different delivery dates use Extended CTC and retain each line date - Initialize(); - - SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithTwoLines(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); - SetPostedInvoiceLineReferences(SalesInvoiceHeader."No.", 'SHIPMENT-', 'ORDER-1', true, false); - SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); - SalesInvoiceLine.FindSet(); - repeat - DeliveryDate := WorkDate() + SalesInvoiceLine."Line No."; - SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No."); - SalesShipmentHeader."Posting Date" := DeliveryDate; - SalesShipmentHeader.Modify(); - until SalesInvoiceLine.Next() = 0; - - ExportInvoice(SalesInvoiceHeader, XmlDoc); - - Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), - StrSubstNo(IncorrectValueErr, 'CustomizationID')); - SalesInvoiceLine.FindSet(); - repeat - SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No."); - AssertInvoiceLineDeliveryReference(XmlDoc, SalesInvoiceLine, SalesShipmentHeader."Posting Date"); - until SalesInvoiceLine.Next() = 0; + 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] @@ -528,7 +462,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Assert.AreNotEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); end; - #endregion #region Validation @@ -646,35 +579,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" StrSubstNo(IncorrectValueErr, 'Seller EndpointID schemeID')); end; - [Test] - procedure ExportSalesInvUsesCompanyRoutingEndpointWithScheme0225() - var - ServiceParticipant: Record "Service Participant"; - SalesInvoiceHeader: Record "Sales Invoice Header"; - XmlDoc: XmlDocument; - EndpointId: Text[200]; - begin - // [SCENARIO] A service-specific company routing identifier is exported with scheme 0225 - Initialize(); - - EndpointId := CompanyInformation."Registration No." + '_001'; - ServiceParticipant.Service := EDocumentService.Code; - ServiceParticipant."Participant Type" := ServiceParticipant."Participant Type"::Company; - ServiceParticipant."Participant Identifier" := EndpointId; - ServiceParticipant."FR Identifier Scheme" := ServiceParticipant."FR Identifier Scheme"::"0225"; - ServiceParticipant.Insert(); - SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); - - ExportInvoice(SalesInvoiceHeader, XmlDoc); - - Assert.AreEqual(EndpointId, - GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID'), - StrSubstNo(IncorrectValueErr, 'Seller EndpointID')); - Assert.AreEqual('0225', - GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID'), - StrSubstNo(IncorrectValueErr, 'Seller EndpointID schemeID')); - end; - [Test] procedure CheckRaisesErrorWhenSellerCountryCodeIsEmpty() var @@ -841,28 +745,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesShipmentLine.Insert(); end; - local procedure AssertInvoiceLineOrderReference(XmlDoc: XmlDocument; SalesInvoiceLine: Record "Sales Invoice Line") - var - InvoiceLinePath: Text; - begin - InvoiceLinePath := StrSubstNo('/Invoice/cac:InvoiceLine[cbc:ID=''%1'']', Format(SalesInvoiceLine."Line No.", 0, 9)); - Assert.AreEqual(SalesInvoiceLine."Order No.", GetNodeByPath(XmlDoc, InvoiceLinePath + '/cac:OrderLineReference/cac:OrderReference/cbc:ID'), - StrSubstNo(IncorrectValueErr, 'OrderReference ID')); - Assert.AreEqual(Format(SalesInvoiceLine."Order Line No.", 0, 9), GetNodeByPath(XmlDoc, InvoiceLinePath + '/cac:OrderLineReference/cbc:LineID'), - StrSubstNo(IncorrectValueErr, 'OrderLineReference LineID')); - end; - - local procedure AssertInvoiceLineDeliveryReference(XmlDoc: XmlDocument; SalesInvoiceLine: Record "Sales Invoice Line"; DeliveryDate: Date) - var - InvoiceLinePath: Text; - begin - InvoiceLinePath := StrSubstNo('/Invoice/cac:InvoiceLine[cbc:ID=''%1'']', Format(SalesInvoiceLine."Line No.", 0, 9)); - Assert.AreEqual(SalesInvoiceLine."Shipment No.", GetNodeByPath(XmlDoc, InvoiceLinePath + '/cac:Delivery/cbc:ID'), - StrSubstNo(IncorrectValueErr, 'Delivery ID')); - Assert.AreEqual(Format(DeliveryDate, 0, 9), GetNodeByPath(XmlDoc, InvoiceLinePath + '/cac:Delivery/cbc:ActualDeliveryDate'), - StrSubstNo(IncorrectValueErr, 'ActualDeliveryDate')); - end; - local procedure CreateSalesInvoiceWithLine(CustomerNo: Code[20]): Code[20] var Customer: Record Customer; @@ -1007,14 +889,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" end; local procedure CreateAndPostSalesCrMemo(CustomerNo: Code[20]): Code[20] - var - SalesHeader: Record "Sales Header"; - begin - SalesHeader.Get("Sales Document Type"::"Credit Memo", CreateSalesCrMemo(CustomerNo)); - exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); - end; - - local procedure CreateSalesCrMemo(CustomerNo: Code[20]): Code[20] var Customer: Record Customer; GLAccount: Record "G/L Account"; @@ -1045,7 +919,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesLine.Validate("Unit Price", 100); SalesLine.Modify(true); - exit(SalesHeader."No."); + exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); end; local procedure ExportCrMemo(SalesCrMemoHeader: Record "Sales Cr.Memo Header"; var XmlDoc: XmlDocument) From 954d5546669402afb3b9841e95e791181cff3e62 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 27 Jul 2026 15:21:37 +0200 Subject: [PATCH 22/77] updated tooltip for prefix comment --- .../app/src/Extensions/FRSalesCommentLine.TableExt.al | 2 +- .../EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentLine.TableExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentLine.TableExt.al index 2eea8d7cd6c..5b0422abf47 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentLine.TableExt.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRSalesCommentLine.TableExt.al @@ -14,7 +14,7 @@ tableextension 10975 "FR Sales Comment Line" extends "Sales Comment Line" { Caption = 'French Regulatory Type'; DataClassification = CustomerContent; - ToolTip = 'Specifies the French regulatory purpose when this comment must be included in the electronic invoice.'; + 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/test/src/PEPPOLBIS30XMLTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al index 4847ce4272b..192e9ae17e5 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 @@ -294,7 +294,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" XmlDoc: XmlDocument; CommentText: Text[80]; begin - // [SCENARIO] A maintained French regulatory comment is exported as a UBL header note + // [SCENARIO] A maintained French regulatory comment is automatically prefixed with its type in a UBL header note Initialize(); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('', "Electronic Address Scheme"::"EM"))); From f6127a690c22fee658f9b75f5a04249445ae9ddd Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 27 Jul 2026 15:56:14 +0200 Subject: [PATCH 23/77] added missing using --- .../app/src/Core/FREInvoiceLifecycleMgt.Codeunit.al | 2 ++ 1 file changed, 2 insertions(+) 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 index 01e91ee8156..f01f9a3dc36 100644 --- 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 @@ -10,9 +10,11 @@ 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; From 99f9a7be42201b727622ba352e439f2424202e0d Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 27 Jul 2026 16:47:17 +0200 Subject: [PATCH 24/77] typo --- .../EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 192e9ae17e5..585a9978775 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 @@ -712,14 +712,14 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" repeat LineIndex += 1; if AppendShipmentIndex then - SalesInvoiceLine."Shipment No." := CopyStr(ShipmentNo + Format(LineIndex), 1, MaxStrLen(SalesInvoiceLine."Shipment No.")); + SalesInvoiceLine."Shipment No." := CopyStr(ShipmentNo + Format(LineIndex), 1, MaxStrLen(SalesInvoiceLine."Shipment No.")) else - SalesInvoiceLine."Shipment No." := CopyStr(ShipmentNo, 1, MaxStrLen(SalesInvoiceLine."Shipment No.")); + SalesInvoiceLine."Shipment No." := CopyStr(ShipmentNo, 1, MaxStrLen(SalesInvoiceLine."Shipment No.")); SalesInvoiceLine."Shipment Line No." := SalesInvoiceLine."Line No."; if AppendOrderIndex then - SalesInvoiceLine."Order No." := CopyStr(OrderNo + Format(LineIndex), 1, MaxStrLen(SalesInvoiceLine."Order No.")); + SalesInvoiceLine."Order No." := CopyStr(OrderNo + Format(LineIndex), 1, MaxStrLen(SalesInvoiceLine."Order No.")) else - SalesInvoiceLine."Order No." := CopyStr(OrderNo, 1, MaxStrLen(SalesInvoiceLine."Order No.")); + SalesInvoiceLine."Order No." := CopyStr(OrderNo, 1, MaxStrLen(SalesInvoiceLine."Order No.")); SalesInvoiceLine."Order Line No." := SalesInvoiceLine."Line No."; SalesInvoiceLine.Modify(); if SalesInvoiceLine."Shipment No." <> '' then From 2932ad5747f1b13e080d3366c26242f678cdac76 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 28 Jul 2026 11:38:29 +0200 Subject: [PATCH 25/77] Fix AA0477: sort using statements alphabetically --- .../EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 585a9978775..0e9c681e709 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 @@ -11,8 +11,8 @@ using Microsoft.Finance.GeneralLedger.Account; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Foundation.Address; using Microsoft.Foundation.Company; -using Microsoft.Sales.Customer; using Microsoft.Sales.Comment; +using Microsoft.Sales.Customer; using Microsoft.Sales.Document; using Microsoft.Sales.History; using Microsoft.Sales.Setup; From c2c9850e5771c84d07adc7778f82af7989dc4704 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 29 Jul 2026 10:35:41 +0200 Subject: [PATCH 26/77] new test update --- .../EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al | 3 +++ 1 file changed, 3 insertions(+) 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 0e9c681e709..0b8cf14dc62 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 @@ -786,8 +786,11 @@ 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 From 69783c83202007452dea4787d1916e711a032c68 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 29 Jul 2026 16:20:07 +0200 Subject: [PATCH 27/77] new --- .../test/src/FacturXCIIXMLTests.Codeunit.al | 14 +------------- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 14 +------------- 2 files changed, 2 insertions(+), 26 deletions(-) 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..68e2011463f 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 @@ -40,7 +40,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 @@ -1106,23 +1105,12 @@ codeunit 148148 "Factur-X CII XML Tests" LibrarySales.CreateCustomer(Customer); if Customer."Country/Region Code" = '' then Customer.Validate("Country/Region Code", CompanyInformation."Country/Region Code"); - Customer.Validate("VAT Registration No.", GetNextCustomerVATRegistrationNo()); + Customer.Validate("VAT Registration No.", LibraryERM.GenerateVATRegistrationNo('FR')); Customer.Validate("FR Electronic Address", FRElecAddress); Customer.Modify(true); exit(Customer."No."); end; - local procedure GetNextCustomerVATRegistrationNo(): Text[20] - var - VATNoBody: Text[11]; - SequenceText: Text; - begin - CustomerVATNoSequence += 1; - SequenceText := Format(CustomerVATNoSequence); - VATNoBody := CopyStr(PadStr('', 11 - StrLen(SequenceText), '0') + SequenceText, 1, 11); - exit('FR' + VATNoBody); - end; - local procedure CreateSalesInvoiceCIIXML(var TempBlob: Codeunit "Temp Blob") var SalesInvoiceHeader: Record "Sales Invoice Header"; 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 0b8cf14dc62..891a8a121bb 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 @@ -47,7 +47,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" PeppolBIS30FRFormat: Codeunit "Peppol BIS 3.0 FR Format"; IncorrectValueErr: Label 'Incorrect value for %1', Comment = '%1 = XML element path', Locked = true; IsInitialized: Boolean; - CustomerVATNoSequence: Integer; #region SalesInvoice [Test] @@ -797,7 +796,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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.Validate("VAT Registration No.", LibraryERM.GenerateVATRegistrationNo('FR')); Customer.Validate("FR Electronic Address", FRElectronicAddress); Customer.Validate("FR Elec. Address Scheme", AddressScheme); @@ -806,17 +805,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" exit(Customer."No."); end; - local procedure GetNextCustomerVATRegistrationNo(): Text[20] - var - VATNoBody: Text[11]; - SequenceText: Text; - begin - CustomerVATNoSequence += 1; - SequenceText := Format(CustomerVATNoSequence); - VATNoBody := CopyStr(PadStr('', 11 - StrLen(SequenceText), '0') + SequenceText, 1, 11); - exit('FR' + VATNoBody); - end; - local procedure GetCustomerVATRegistrationNo(CustomerNo: Code[20]): Text[20] var Customer: Record Customer; From 558c55522a419bd5caed9a728b5cf864a1df5b71 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 30 Jul 2026 11:32:03 +0200 Subject: [PATCH 28/77] Fix E-Reporting FR analyzer warnings (cherry picked from commit cd7be9a32ef1c8ab3b4f596822ee2a5d04b2b070) --- .../app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 3e120cf43de..b4e315e1ca4 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 @@ -164,7 +164,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" DeliveryElement: XmlElement; LineXPath: Text; begin - LineXPath := StrSubstNo('/*/cac:InvoiceLine[cbc:ID=''%1'']', Format(SalesInvoiceLine."Line No.", 0, 9)); + 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 @@ -317,7 +317,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" ElecAddress := CompanyInformation."SIRET No."; ElecAddressScheme := ElecAddressScheme::"0009"; end else begin - ElecAddress := CompanyInformation.GetVATRegistrationNumber(); + ElecAddress := CopyStr(CompanyInformation.GetVATRegistrationNumber(), 1, MaxStrLen(ElecAddress)); ElecAddressScheme := ElecAddressScheme::"0223"; end; @@ -509,4 +509,5 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" CacNamespaceTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', Locked = true; ExtendedCTCFranceCustomizationIdTok: Label 'EXTENDED-CTC-FR', Locked = true; RegulatoryCommentFormatTok: Label '#%1#%2', Locked = true; + InvoiceLineXPathTok: Label '/*/cac:InvoiceLine[cbc:ID=''%1'']', Locked = true; } From 8c662c36f77ebea51e7469bfb66f421686991086 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 30 Jul 2026 11:43:31 +0200 Subject: [PATCH 29/77] Address E-Reporting FR review feedback --- .../app/src/Core/EDocHelpers.Codeunit.al | 6 ++++-- .../app/src/Core/ElectronicAddressScheme.Enum.al | 14 +++++++++----- .../Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 13 ++++++++++--- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 2 +- 4 files changed, 24 insertions(+), 11 deletions(-) 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 f8bb067dee2..171e08ea37d 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 @@ -125,13 +125,15 @@ codeunit 10991 "EDoc. Helpers" if not ServiceParticipant.Get(EDocumentServiceCode, ParticipantType, ParticipantNo) then exit(false); - exit(ServiceParticipant."Participant Identifier" <> ''); + exit( + (ServiceParticipant."Participant Identifier" <> '') and + (ServiceParticipant."FR Identifier Scheme" <> ServiceParticipant."FR Identifier Scheme"::" ")); 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.'; SellerElectronicAddressRequiredErr: Label 'SIRET No., VAT Registration No., or a Service Participant identifier must be specified for the company for French e-invoicing.'; - BuyerElectronicAddressRequiredErr: Label 'Electronic Address must be specified for Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.'; + BuyerElectronicAddressRequiredErr: Label 'Electronic Address, VAT Registration No., or a Service Participant identifier 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.'; } 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 155a6024550..7f0c63e1b60 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 @@ -8,23 +8,27 @@ enum 10976 "Electronic Address Scheme" { Extensible = true; - value(0; "EM") + value(0; " ") + { + Caption = ' '; + } + value(1; "EM") { Caption = 'Email (EM)'; } - value(1; "0009") + value(2; "0009") { Caption = 'SIRET (0009)'; } - value(2; "0002") + value(3; "0002") { Caption = 'SIREN (0002)'; } - value(3; "0223") + value(4; "0223") { Caption = 'French VAT number (0223)'; } - value(4; "0225") + value(5; "0225") { Caption = 'French routing identifier (0225)'; } 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 b4e315e1ca4..bd369d62ade 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 @@ -118,6 +118,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" end; SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + SalesInvoiceLine.SetLoadFields("Line No.", "Order No.", "Order Line No.", "Shipment No.", "Shipment Line No."); if SalesInvoiceLine.FindSet() then repeat InjectExtendedLineReferences(XmlDoc, NamespaceMgr, SalesInvoiceLine); @@ -133,12 +134,16 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" DeliveryDates: Dictionary of [Text, Boolean]; begin SalesInvoiceLine.SetRange("Document No.", DocumentNo); + SalesInvoiceLine.SetLoadFields("Shipment No.", "Order No."); + SalesShipmentHeader.SetLoadFields("Posting Date"); if SalesInvoiceLine.FindSet() then repeat if SalesInvoiceLine."Shipment No." <> '' then begin - AddDistinctValue(ShipmentNos, SalesInvoiceLine."Shipment No."); - if SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No.") then - AddDistinctValue(DeliveryDates, Format(SalesShipmentHeader."Posting Date", 0, 9)); + if not ShipmentNos.ContainsKey(SalesInvoiceLine."Shipment No.") then begin + ShipmentNos.Add(SalesInvoiceLine."Shipment No.", true); + if SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No.") then + AddDistinctValue(DeliveryDates, Format(SalesShipmentHeader."Posting Date", 0, 9)); + end; end; if SalesInvoiceLine."Order No." <> '' then AddDistinctValue(OrderNos, SalesInvoiceLine."Order No."); @@ -392,6 +397,8 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" exit(false); if ServiceParticipant."Participant Identifier" = '' then exit(false); + if ServiceParticipant."FR Identifier Scheme" = ServiceParticipant."FR Identifier Scheme"::" " then + exit(false); ElecAddress := CopyStr(ServiceParticipant."Participant Identifier", 1, MaxStrLen(ElecAddress)); ElecAddressScheme := ServiceParticipant."FR Identifier Scheme"; 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 891a8a121bb..6219a20a144 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 @@ -734,7 +734,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" if not SalesShipmentHeader.Get(ShipmentNo) then begin SalesShipmentHeader.Init(); SalesShipmentHeader."No." := ShipmentNo; - SalesShipmentHeader."Posting Date" := WorkDate(); + SalesShipmentHeader."Posting Date" := CalcDate('<-1D>', WorkDate()); SalesShipmentHeader.Insert(); end; From 1415fceea101b440a36d85021a886b3f5c6524e5 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 30 Jul 2026 11:45:52 +0200 Subject: [PATCH 30/77] Test regulatory comments on credit memos --- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) 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 6219a20a144..dac445c331e 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 @@ -392,6 +392,31 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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 + // [SCENARIO] A French regulatory comment on a posted credit memo is prefixed with its type in a UBL header note + Initialize(); + + 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(); + + ExportCrMemo(SalesCrMemoHeader, XmlDoc); + + Assert.AreEqual('#AAB#' + CommentText, GetNodeByPath(XmlDoc, '/CreditNote/cbc:Note'), StrSubstNo(IncorrectValueErr, 'Note')); + end; + [Test] procedure ExportSalesInvSelectsExtendedCTCForMultipleOrders() var From 5616cf29a74111fcfe90b5b8bda1e7731eba285e Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 30 Jul 2026 13:01:12 +0200 Subject: [PATCH 31/77] Fix French electronic address validation --- .../app/src/Core/EDocHelpers.Codeunit.al | 20 ++- .../src/Core/ElectronicAddressScheme.Enum.al | 14 +- .../FRServiceParticipant.TableExt.al | 1 + .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 12 +- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 121 ++++++++++++++++++ 5 files changed, 149 insertions(+), 19 deletions(-) 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 171e08ea37d..10ec1621171 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 @@ -106,8 +106,11 @@ codeunit 10991 "EDoc. Helpers" if not Customer.Get(CustomerNo) then exit; - if Customer."FR Electronic Address" <> '' then + if Customer."FR Electronic Address" <> '' then begin + if Customer."FR Elec. Address Scheme" = Customer."FR Elec. Address Scheme"::" " then + Error(BuyerElectronicAddressSchemeRequiredErr, Customer."No."); exit; + end; if HasServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Customer, Customer."No.") then exit; if Customer."VAT Registration No." <> '' then @@ -116,18 +119,23 @@ codeunit 10991 "EDoc. Helpers" Error(BuyerElectronicAddressRequiredErr, Customer."No."); end; - local procedure HasServiceParticipantAddress(EDocumentServiceCode: Code[20]; ParticipantType: Enum "E-Document Source Type"; ParticipantNo: Code[20]): Boolean + procedure HasServiceParticipantAddress(EDocumentServiceCode: Code[20]; ParticipantType: Enum "E-Document Source Type"; ParticipantNo: Code[20]): Boolean var ServiceParticipant: Record "Service Participant"; + HasIdentifier: Boolean; + HasScheme: Boolean; begin if EDocumentServiceCode = '' then exit(false); if not ServiceParticipant.Get(EDocumentServiceCode, ParticipantType, ParticipantNo) then exit(false); - exit( - (ServiceParticipant."Participant Identifier" <> '') and - (ServiceParticipant."FR Identifier Scheme" <> ServiceParticipant."FR Identifier Scheme"::" ")); + HasIdentifier := ServiceParticipant."Participant Identifier" <> ''; + HasScheme := ServiceParticipant."FR Identifier Scheme" <> ServiceParticipant."FR Identifier Scheme"::" "; + if HasIdentifier <> HasScheme then + Error(ServiceParticipantAddressIncompleteErr, ServiceParticipant.FieldCaption("Participant Identifier"), ServiceParticipant.FieldCaption("FR Identifier Scheme")); + + exit(HasIdentifier); end; var @@ -135,5 +143,7 @@ codeunit 10991 "EDoc. Helpers" SIRETRequiredErr: Label 'SIRET No. must be specified in Company Information for French e-invoicing.'; SellerElectronicAddressRequiredErr: Label 'SIRET No., VAT Registration No., or a Service Participant identifier must be specified for the company for French e-invoicing.'; BuyerElectronicAddressRequiredErr: Label 'Electronic Address, VAT Registration No., or a Service Participant identifier must be specified for Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.'; + BuyerElectronicAddressSchemeRequiredErr: Label 'Electronic Address Scheme 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.'; + 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 7f0c63e1b60..39960b8169f 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 @@ -8,22 +8,22 @@ enum 10976 "Electronic Address Scheme" { Extensible = true; - value(0; " ") - { - Caption = ' '; - } - value(1; "EM") + value(0; "EM") { Caption = 'Email (EM)'; } - value(2; "0009") + value(1; "0009") { Caption = 'SIRET (0009)'; } - value(3; "0002") + value(2; "0002") { Caption = 'SIREN (0002)'; } + value(3; " ") + { + Caption = ' '; + } value(4; "0223") { Caption = 'French VAT number (0223)'; 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 index ad42c35ea49..2e56852eb75 100644 --- 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 @@ -14,6 +14,7 @@ tableextension 10974 "FR Service Participant" extends "Service Participant" { Caption = 'French Identifier Scheme'; DataClassification = CustomerContent; + InitValue = " "; ToolTip = 'Specifies the electronic address scheme used for French electronic invoicing.'; } } 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 bd369d62ade..0250abd27d8 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 @@ -138,13 +138,12 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" SalesShipmentHeader.SetLoadFields("Posting Date"); if SalesInvoiceLine.FindSet() then repeat - if SalesInvoiceLine."Shipment No." <> '' then begin + if SalesInvoiceLine."Shipment No." <> '' then if not ShipmentNos.ContainsKey(SalesInvoiceLine."Shipment No.") then begin ShipmentNos.Add(SalesInvoiceLine."Shipment No.", true); if SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No.") then AddDistinctValue(DeliveryDates, Format(SalesShipmentHeader."Posting Date", 0, 9)); end; - end; if SalesInvoiceLine."Order No." <> '' then AddDistinctValue(OrderNos, SalesInvoiceLine."Order No."); until SalesInvoiceLine.Next() = 0; @@ -392,14 +391,13 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" 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 ServiceParticipant.Get(EDocumentServiceCode, ParticipantType, ParticipantNo) then - exit(false); - if ServiceParticipant."Participant Identifier" = '' then - exit(false); - if ServiceParticipant."FR Identifier Scheme" = ServiceParticipant."FR Identifier Scheme"::" " then + if not FREDocHelpers.HasServiceParticipantAddress(EDocumentServiceCode, ParticipantType, ParticipantNo) then exit(false); + ServiceParticipant.Get(EDocumentServiceCode, ParticipantType, ParticipantNo); + ElecAddress := CopyStr(ServiceParticipant."Participant Identifier", 1, MaxStrLen(ElecAddress)); ElecAddressScheme := ServiceParticipant."FR Identifier Scheme"; exit(true); 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 dac445c331e..20b8851abe0 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 @@ -657,6 +657,118 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'), StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); end; + + [Test] + procedure CheckRaisesErrorWhenSellerElectronicAddressIsMissing() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + OriginalSIRETNo: Code[14]; + OriginalVATRegistrationNo: Text[20]; + begin + // [SCENARIO] Check rejects a seller without SIRET, VAT registration number, or a service participant identifier + Initialize(); + + OriginalSIRETNo := CompanyInformation."SIRET No."; + OriginalVATRegistrationNo := CompanyInformation."VAT Registration No."; + CompanyInformation."SIRET No." := ''; + CompanyInformation."VAT Registration No." := ''; + CompanyInformation.Modify(true); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + + asserterror CheckInvoice(SalesInvoiceHeader); + + Assert.ExpectedError('SIRET No., VAT Registration No., or a Service Participant identifier must be specified for the company for French e-invoicing.'); + + CompanyInformation.Get(); + CompanyInformation."SIRET No." := OriginalSIRETNo; + CompanyInformation."VAT Registration No." := OriginalVATRegistrationNo; + CompanyInformation.Modify(true); + end; + + [Test] + procedure CheckRaisesErrorWhenBuyerElectronicAddressIsMissing() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + CustomerNo: Code[20]; + begin + // [SCENARIO] Check rejects a buyer without an electronic address, VAT registration number, or a service participant identifier + Initialize(); + + CustomerNo := CreateCustomer('', "Electronic Address Scheme"::"EM"); + ClearCustomerVATRegistrationNo(CustomerNo); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + + asserterror CheckInvoice(SalesInvoiceHeader); + + Assert.ExpectedError(StrSubstNo('Electronic Address, VAT Registration No., or a Service Participant identifier must be specified for Customer %1 for French e-invoicing.', CustomerNo)); + end; + + [Test] + procedure CheckRaisesErrorWhenBuyerElectronicAddressSchemeIsMissing() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + CustomerNo: Code[20]; + begin + // [SCENARIO] Check rejects a buyer electronic address without its scheme + Initialize(); + + CustomerNo := CreateCustomer('buyer@example.com', "Electronic Address Scheme"::" "); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + + asserterror CheckInvoice(SalesInvoiceHeader); + + Assert.ExpectedError(StrSubstNo('Electronic Address Scheme must be specified for Customer %1 for French e-invoicing.', CustomerNo)); + end; + + [Test] + procedure CheckRaisesErrorWhenParticipantSchemeIsMissing() + var + ServiceParticipant: Record "Service Participant"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + CustomerNo: Code[20]; + begin + // [SCENARIO] Check rejects a service participant identifier without its French identifier scheme + Initialize(); + + CustomerNo := CreateCustomer('', "Electronic Address Scheme"::"EM"); + ClearCustomerVATRegistrationNo(CustomerNo); + 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)); + + asserterror CheckInvoice(SalesInvoiceHeader); + + Assert.ExpectedError('Participant Identifier and French Identifier Scheme must both be specified for French electronic invoicing.'); + end; + + [Test] + procedure CheckRaisesErrorWhenParticipantIdentifierIsMissing() + var + ServiceParticipant: Record "Service Participant"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + CustomerNo: Code[20]; + begin + // [SCENARIO] Check rejects a French identifier scheme without its service participant identifier + Initialize(); + + CustomerNo := CreateCustomer('', "Electronic Address Scheme"::"EM"); + ClearCustomerVATRegistrationNo(CustomerNo); + 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)); + + asserterror CheckInvoice(SalesInvoiceHeader); + + Assert.ExpectedError('Participant Identifier and French Identifier Scheme must both be specified for French electronic invoicing.'); + end; #endregion local procedure Initialize() @@ -838,6 +950,15 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" exit(Customer."VAT Registration No."); end; + local procedure ClearCustomerVATRegistrationNo(CustomerNo: Code[20]) + var + Customer: Record Customer; + begin + Customer.Get(CustomerNo); + Customer."VAT Registration No." := ''; + Customer.Modify(true); + end; + local procedure CheckInvoice(SalesInvoiceHeader: Record "Sales Invoice Header") var SourceDocumentHeader: RecordRef; From bf3623ebf100112ae1acc0fa016c60380fd9f26f Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 30 Jul 2026 13:16:26 +0200 Subject: [PATCH 32/77] corrections --- .../EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 20b8851abe0..a5ddee71105 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 @@ -45,6 +45,8 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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 Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.', Locked = true; + BuyerElectronicAddressSchemeRequiredErr: Label 'Electronic Address Scheme must be specified for Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.', Locked = true; IncorrectValueErr: Label 'Incorrect value for %1', Comment = '%1 = XML element path', Locked = true; IsInitialized: Boolean; @@ -700,7 +702,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" asserterror CheckInvoice(SalesInvoiceHeader); - Assert.ExpectedError(StrSubstNo('Electronic Address, VAT Registration No., or a Service Participant identifier must be specified for Customer %1 for French e-invoicing.', CustomerNo)); + Assert.ExpectedError(StrSubstNo(BuyerElectronicAddressRequiredErr, CustomerNo)); end; [Test] @@ -717,7 +719,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" asserterror CheckInvoice(SalesInvoiceHeader); - Assert.ExpectedError(StrSubstNo('Electronic Address Scheme must be specified for Customer %1 for French e-invoicing.', CustomerNo)); + Assert.ExpectedError(StrSubstNo(BuyerElectronicAddressSchemeRequiredErr, CustomerNo)); end; [Test] @@ -1090,7 +1092,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" end; end; - local procedure EnsureCountryRegionExists(CountryCode: Code[10]) + local procedure EnsureCountryRegionExists(CountryCode: Code[2]) var CountryRegion: Record "Country/Region"; begin From 2a0e1b7449d7904acdd6f7278f9f70fe8bec5ce8 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 30 Jul 2026 13:26:13 +0200 Subject: [PATCH 33/77] fix for vat reg. no in tests --- .../EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 68e2011463f..894bcdffce4 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 @@ -1105,7 +1105,7 @@ codeunit 148148 "Factur-X CII XML Tests" LibrarySales.CreateCustomer(Customer); if Customer."Country/Region Code" = '' then Customer.Validate("Country/Region Code", CompanyInformation."Country/Region Code"); - Customer.Validate("VAT Registration No.", LibraryERM.GenerateVATRegistrationNo('FR')); + Customer."VAT Registration No." := LibraryERM.GenerateVATRegistrationNo('FR'); Customer.Validate("FR Electronic Address", FRElecAddress); Customer.Modify(true); exit(Customer."No."); From 29038d040dffe03117a1fbc63b767f1846134207 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 30 Jul 2026 13:54:11 +0200 Subject: [PATCH 34/77] peppol test fix --- .../EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a5ddee71105..90675585312 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 @@ -935,7 +935,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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.", LibraryERM.GenerateVATRegistrationNo('FR')); + Customer."VAT Registration No." := LibraryERM.GenerateVATRegistrationNo('FR'); Customer.Validate("FR Electronic Address", FRElectronicAddress); Customer.Validate("FR Elec. Address Scheme", AddressScheme); From f537de943eb92b471d6a92c94086e771bb5d827f Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 30 Jul 2026 14:10:16 +0200 Subject: [PATCH 35/77] new --- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 152 ++++++++++-------- 1 file changed, 89 insertions(+), 63 deletions(-) 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 90675585312..37c70b8a63b 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 @@ -429,14 +429,14 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" // [SCENARIO] An invoice containing lines from distinct orders uses the Extended CTC profile Initialize(); - SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithTwoLines(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); - SetPostedInvoiceLineReferences(SalesInvoiceHeader."No.", '', 'ORDER-', false, true); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceFromMultipleOrders(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); ExportInvoice(SalesInvoiceHeader, XmlDoc); 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')); @@ -455,14 +455,14 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" // [SCENARIO] An invoice containing lines from distinct shipments uses the Extended CTC profile Initialize(); - SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithTwoLines(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); - SetPostedInvoiceLineReferences(SalesInvoiceHeader."No.", 'SHIPMENT-', 'ORDER-1', true, false); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceFromMultipleShipments(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); ExportInvoice(SalesInvoiceHeader, XmlDoc); 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'), @@ -480,8 +480,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" // [SCENARIO] Repeated references to one shipment and one order do not select the Extended CTC profile Initialize(); - SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithTwoLines(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); - SetPostedInvoiceLineReferences(SalesInvoiceHeader."No.", 'SHIPMENT-1', 'ORDER-1', false, false); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceFromSingleShipment(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); ExportInvoice(SalesInvoiceHeader, XmlDoc); @@ -775,6 +774,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" local procedure Initialize() var + CountryRegion: Record "Country/Region"; ServiceCode: Code[20]; begin LibraryTestInitialize.OnTestInitialize(Codeunit::"PEPPOL BIS 3.0 XML Tests"); @@ -782,13 +782,16 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" exit; LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"PEPPOL BIS 3.0 XML Tests"); + CountryRegion.Get('FR'); + 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."Country/Region Code" := CountryRegion.Code; CompanyInformation.Validate("Registration No.", '123456789'); CompanyInformation.Validate("SIRET No.", '12345678901234'); if CompanyInformation."VAT Registration No." = '' then @@ -824,63 +827,102 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); end; - local procedure CreateAndPostSalesInvoiceWithTwoLines(CustomerNo: Code[20]): Code[20] + 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"; - NewSalesLine: Record "Sales Line"; + FirstShipmentNo: Code[20]; + SecondShipmentNo: Code[20]; begin - SalesHeader.Get("Sales Document Type"::Invoice, CreateSalesInvoiceWithLine(CustomerNo)); + CreateSalesOrderWithLines(SalesHeader, CustomerNo, 1, 2); SalesLine.SetRange("Document Type", SalesHeader."Document Type"); SalesLine.SetRange("Document No.", SalesHeader."No."); SalesLine.FindFirst(); - LibrarySales.CreateSalesLine(NewSalesLine, SalesHeader, SalesLine.Type, SalesLine."No.", 1); - NewSalesLine.Validate("Unit Price", SalesLine."Unit Price"); - NewSalesLine.Modify(true); - exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + SalesLine.Validate("Qty. to Ship", 1); + SalesLine.Modify(true); + FirstShipmentNo := LibrarySales.PostSalesDocument(SalesHeader, true, false); + + SalesHeader.Get(SalesHeader."Document Type", SalesHeader."No."); + 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 SetPostedInvoiceLineReferences(DocumentNo: Code[20]; ShipmentNo: Text; OrderNo: Text; AppendShipmentIndex: Boolean; AppendOrderIndex: Boolean) + local procedure CreateAndPostSalesInvoiceFromSingleShipment(CustomerNo: Code[20]): Code[20] var - SalesInvoiceLine: Record "Sales Invoice Line"; + 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 - SalesInvoiceLine.SetRange("Document No.", DocumentNo); - SalesInvoiceLine.FindSet(true); - repeat - LineIndex += 1; - if AppendShipmentIndex then - SalesInvoiceLine."Shipment No." := CopyStr(ShipmentNo + Format(LineIndex), 1, MaxStrLen(SalesInvoiceLine."Shipment No.")) - else - SalesInvoiceLine."Shipment No." := CopyStr(ShipmentNo, 1, MaxStrLen(SalesInvoiceLine."Shipment No.")); - SalesInvoiceLine."Shipment Line No." := SalesInvoiceLine."Line No."; - if AppendOrderIndex then - SalesInvoiceLine."Order No." := CopyStr(OrderNo + Format(LineIndex), 1, MaxStrLen(SalesInvoiceLine."Order No.")) - else - SalesInvoiceLine."Order No." := CopyStr(OrderNo, 1, MaxStrLen(SalesInvoiceLine."Order No.")); - SalesInvoiceLine."Order Line No." := SalesInvoiceLine."Line No."; - SalesInvoiceLine.Modify(); - if SalesInvoiceLine."Shipment No." <> '' then - CreatePostedShipmentReference(SalesInvoiceLine."Shipment No.", SalesInvoiceLine."Shipment Line No."); - until SalesInvoiceLine.Next() = 0; + 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 CreatePostedShipmentReference(ShipmentNo: Code[20]; ShipmentLineNo: Integer) + local procedure CreateAndPostSalesInvoiceFromShipments(CustomerNo: Code[20]; ShipmentNoFilter: Text): Code[20] var - SalesShipmentHeader: Record "Sales Shipment Header"; + SalesHeader: Record "Sales Header"; SalesShipmentLine: Record "Sales Shipment Line"; + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + SalesGetShipment: Codeunit "Sales-Get Shipment"; begin - if not SalesShipmentHeader.Get(ShipmentNo) then begin - SalesShipmentHeader.Init(); - SalesShipmentHeader."No." := ShipmentNo; - SalesShipmentHeader."Posting Date" := CalcDate('<-1D>', WorkDate()); - SalesShipmentHeader.Insert(); - end; + 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); - SalesShipmentLine.Init(); - SalesShipmentLine."Document No." := ShipmentNo; - SalesShipmentLine."Line No." := ShipmentLineNo; - SalesShipmentLine.Insert(); + exit(LibrarySales.PostSalesDocument(SalesHeader, false, true)); end; local procedure CreateSalesInvoiceWithLine(CustomerNo: Code[20]): Code[20] @@ -1092,20 +1134,4 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" end; end; - local procedure EnsureCountryRegionExists(CountryCode: Code[2]) - 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; } From ac3cc63f9cc39bec0af86741f478753e02182dca Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 30 Jul 2026 15:02:16 +0200 Subject: [PATCH 36/77] NEW --- .../EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al | 3 +++ 1 file changed, 3 insertions(+) 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 37c70b8a63b..b26526bddef 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 @@ -775,9 +775,12 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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"); + ServiceParticipant.SetRange(Service, EDocumentService.Code); + ServiceParticipant.DeleteAll(); if IsInitialized then exit; LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"PEPPOL BIS 3.0 XML Tests"); From a472b8cc6c2a170abf5985661eee6d123cef5a01 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 31 Jul 2026 16:53:46 +0200 Subject: [PATCH 37/77] new changes for shipments --- .../app/src/Core/EDocHelpers.Codeunit.al | 9 ++-- .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 9 ++-- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 53 ++++++++++++++----- 3 files changed, 49 insertions(+), 22 deletions(-) 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 10ec1621171..8e8a69f463e 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 @@ -64,11 +64,12 @@ codeunit 10991 "EDoc. Helpers" var CompanyInformation: Record "Company Information"; begin + if HasServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Company, '') then + exit; + CompanyInformation.Get(); if CompanyInformation."SIRET No." <> '' then exit; - if HasServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Company, '') then - exit; if CompanyInformation.GetVATRegistrationNumber() <> '' then exit; @@ -106,13 +107,13 @@ codeunit 10991 "EDoc. Helpers" if not Customer.Get(CustomerNo) then exit; + if HasServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Customer, Customer."No.") then + exit; if Customer."FR Electronic Address" <> '' then begin if Customer."FR Elec. Address Scheme" = Customer."FR Elec. Address Scheme"::" " then Error(BuyerElectronicAddressSchemeRequiredErr, Customer."No."); exit; end; - if HasServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Customer, Customer."No.") then - exit; if Customer."VAT Registration No." <> '' then exit; 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 0250abd27d8..1d2dcd977a4 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 @@ -183,6 +183,8 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" ItemNode.AddBeforeSelf(OrderLineReferenceElement); end; + if SalesInvoiceLine."Shipment No." = '' then + exit; if not SalesShipmentLine.Get(SalesInvoiceLine."Shipment No.", SalesInvoiceLine."Shipment Line No.") then exit; if not SalesShipmentHeader.Get(SalesShipmentLine."Document No.") then @@ -225,6 +227,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" SalesCommentLine.SetRange("No.", DocumentNo); SalesCommentLine.SetRange("Document Line No.", 0); 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)); @@ -501,12 +504,6 @@ 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(); - - EDocServiceSupportedType."Source Document Type" := EDocServiceSupportedType."Source Document Type"::"Purchase Credit Memo"; - EDocServiceSupportedType.Insert(); end; var 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 b26526bddef..ef377f08ab8 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 @@ -291,27 +291,33 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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 - // [SCENARIO] A maintained French regulatory comment is automatically prefixed with its type in a UBL header note + // [SCENARIO] A French regulatory comment is carried through posting and exported with its type in a UBL header note Initialize(); - SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('', "Electronic Address Scheme"::"EM"))); + CustomerNo := CreateCustomer('', "Electronic Address Scheme"::"EM"); + InvoiceNo := CreateSalesInvoiceWithLine(CustomerNo); CommentText := 'No discount is granted for early payment.'; - SalesCommentLine."Document Type" := SalesCommentLine."Document Type"::"Posted Invoice"; - SalesCommentLine."No." := SalesInvoiceHeader."No."; + 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"::"Posted Invoice"; - SalesCommentLine."No." := SalesInvoiceHeader."No."; + 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)); ExportInvoice(SalesInvoiceHeader, XmlDoc); @@ -340,6 +346,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" ServiceParticipant.Insert(); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + CheckInvoice(SalesInvoiceHeader); ExportInvoice(SalesInvoiceHeader, XmlDoc); Assert.AreEqual(EndpointId, @@ -348,6 +355,9 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Assert.AreEqual('0225', GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'), StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); + Assert.AreEqual('', + GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID'), + StrSubstNo(IncorrectValueErr, 'Buyer PartyIdentification ID')); end; #endregion @@ -686,6 +696,27 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" CompanyInformation.Modify(true); end; + [Test] + procedure CheckRaisesErrorWhenCompanyParticipantSchemeIsMissing() + var + ServiceParticipant: Record "Service Participant"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + begin + // [SCENARIO] Check rejects a company participant identifier without its scheme even when SIRET is valid + Initialize(); + + 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"))); + + asserterror CheckInvoice(SalesInvoiceHeader); + + Assert.ExpectedError('Participant Identifier and French Identifier Scheme must both be specified for French electronic invoicing.'); + end; + [Test] procedure CheckRaisesErrorWhenBuyerElectronicAddressIsMissing() var @@ -728,11 +759,10 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceHeader: Record "Sales Invoice Header"; CustomerNo: Code[20]; begin - // [SCENARIO] Check rejects a service participant identifier without its French identifier scheme + // [SCENARIO] Check rejects a service participant identifier without its French identifier scheme even when the customer endpoint is valid Initialize(); - CustomerNo := CreateCustomer('', "Electronic Address Scheme"::"EM"); - ClearCustomerVATRegistrationNo(CustomerNo); + CustomerNo := CreateCustomer('buyer@example.com', "Electronic Address Scheme"::"EM"); ServiceParticipant.Init(); ServiceParticipant.Service := EDocumentService.Code; ServiceParticipant."Participant Type" := ServiceParticipant."Participant Type"::Customer; @@ -753,11 +783,10 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceHeader: Record "Sales Invoice Header"; CustomerNo: Code[20]; begin - // [SCENARIO] Check rejects a French identifier scheme without its service participant identifier + // [SCENARIO] Check rejects a French identifier scheme without its service participant identifier even when the customer endpoint is valid Initialize(); - CustomerNo := CreateCustomer('', "Electronic Address Scheme"::"EM"); - ClearCustomerVATRegistrationNo(CustomerNo); + CustomerNo := CreateCustomer('buyer@example.com', "Electronic Address Scheme"::"EM"); ServiceParticipant.Init(); ServiceParticipant.Service := EDocumentService.Code; ServiceParticipant."Participant Type" := ServiceParticipant."Participant Type"::Customer; From bbabfede6e6688d39d2025196490b7aef808a647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milica=20=C4=90uki=C4=87?= <22576793+djukicmilica@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:17:16 +0200 Subject: [PATCH 38/77] Apply suggestion from @github-actions[bot] Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 1 + 1 file changed, 1 insertion(+) 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 1d2dcd977a4..fa52434b4fc 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 @@ -223,6 +223,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" exit; end; + SalesCommentLine.SetRange("Document Type", DocumentType); SalesCommentLine.SetRange("Document Type", DocumentType); SalesCommentLine.SetRange("No.", DocumentNo); SalesCommentLine.SetRange("Document Line No.", 0); From b35af921dd1109f8bf5ca5bd8003f29d68336745 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 4 Aug 2026 12:20:54 +0200 Subject: [PATCH 39/77] new changes --- .../EReportingFR/app/src/Core/EDocHelpers.Codeunit.al | 6 ++++++ .../app/src/Core/FRRegulatoryCommentType.Enum.al | 4 ++++ .../app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 5 +---- 3 files changed, 11 insertions(+), 4 deletions(-) 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 8e8a69f463e..e4fe44e8ab6 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 @@ -123,6 +123,12 @@ codeunit 10991 "EDoc. Helpers" 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 HasIdentifier: Boolean; HasScheme: Boolean; begin diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al index 8fbceddd75f..459ca00012c 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FRRegulatoryCommentType.Enum.al @@ -108,4 +108,8 @@ enum 10971 "FR Regulatory Comment Type" { 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/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al index 1d2dcd977a4..a699fa54bfd 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 @@ -225,7 +225,6 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" SalesCommentLine.SetRange("Document Type", DocumentType); SalesCommentLine.SetRange("No.", DocumentNo); - SalesCommentLine.SetRange("Document Line No.", 0); SalesCommentLine.SetFilter("FR Regulatory Comment Type", '<>%1', SalesCommentLine."FR Regulatory Comment Type"::None); SalesCommentLine.SetLoadFields("FR Regulatory Comment Type", Comment); if SalesCommentLine.FindSet() then @@ -396,11 +395,9 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" ServiceParticipant: Record "Service Participant"; FREDocHelpers: Codeunit "EDoc. Helpers"; begin - if not FREDocHelpers.HasServiceParticipantAddress(EDocumentServiceCode, ParticipantType, ParticipantNo) then + if not FREDocHelpers.HasServiceParticipantAddress(EDocumentServiceCode, ParticipantType, ParticipantNo, ServiceParticipant) then exit(false); - ServiceParticipant.Get(EDocumentServiceCode, ParticipantType, ParticipantNo); - ElecAddress := CopyStr(ServiceParticipant."Participant Identifier", 1, MaxStrLen(ElecAddress)); ElecAddressScheme := ServiceParticipant."FR Identifier Scheme"; exit(true); From 234f301fc09883da7f43b09d9120af1760fe7e98 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 4 Aug 2026 12:22:39 +0200 Subject: [PATCH 40/77] correction --- .../app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 1 - 1 file changed, 1 deletion(-) 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 9a8a5021e57..a699fa54bfd 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 @@ -223,7 +223,6 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" exit; end; - SalesCommentLine.SetRange("Document Type", DocumentType); SalesCommentLine.SetRange("Document Type", DocumentType); SalesCommentLine.SetRange("No.", DocumentNo); SalesCommentLine.SetFilter("FR Regulatory Comment Type", '<>%1', SalesCommentLine."FR Regulatory Comment Type"::None); From 121b808133a4da8e9304573d7cb587f4d7d495fe Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 4 Aug 2026 19:06:04 +0200 Subject: [PATCH 41/77] update --- .../app/src/Core/CIIXMLBuilder.Codeunit.al | 4 +++ .../src/Core/ElectronicAddressScheme.Enum.al | 8 ++--- .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 30 ++++++++++++++++--- 3 files changed, 34 insertions(+), 8 deletions(-) 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/ElectronicAddressScheme.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/ElectronicAddressScheme.Enum.al index 39960b8169f..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 @@ -24,12 +24,12 @@ enum 10976 "Electronic Address Scheme" { Caption = ' '; } - value(4; "0223") + value(4; "0225") { - Caption = 'French VAT number (0223)'; + Caption = 'FRCTC electronic address (0225)'; } - value(5; "0225") + value(5; "9957") { - Caption = 'French routing identifier (0225)'; + Caption = 'French VAT number (9957)'; } } 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 a699fa54bfd..3eae8f39b04 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 @@ -84,6 +84,8 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" InitNamespaceManager(NamespaceMgr, XmlDoc); + SetFrenchBillingMode(XmlDoc, NamespaceMgr); + RemoveZeroAllowanceTotal(XmlDoc, NamespaceMgr); InjectSupplierIdentification(XmlDoc, NamespaceMgr, CompanyInformation); InjectSupplierEndpoint(XmlDoc, NamespaceMgr, CompanyInformation, EDocumentService.Code); InjectRegulatoryComments(XmlDoc, NamespaceMgr, SourceDocumentHeader); @@ -98,6 +100,25 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" XmlDoc.WriteTo(OutStr); end; + local procedure SetFrenchBillingMode(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager) + var + ProfileIdNode: XmlNode; + NewProfileIdNode: XmlNode; + begin + if not XmlDoc.SelectSingleNode('/*/cbc:ProfileID', NamespaceMgr, ProfileIdNode) then + exit; + NewProfileIdNode := XmlElement.Create('ProfileID', CbcNamespaceTok, BillingModeB1Tok).AsXmlNode(); + ProfileIdNode.ReplaceWith(NewProfileIdNode); + 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"; @@ -324,7 +345,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" ElecAddressScheme := ElecAddressScheme::"0009"; end else begin ElecAddress := CopyStr(CompanyInformation.GetVATRegistrationNumber(), 1, MaxStrLen(ElecAddress)); - ElecAddressScheme := ElecAddressScheme::"0223"; + ElecAddressScheme := ElecAddressScheme::"9957"; end; if ElecAddress = '' then @@ -385,7 +406,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" ElecAddressScheme := Customer."FR Elec. Address Scheme"; if ElecAddress = '' then begin ElecAddress := Customer."VAT Registration No."; - ElecAddressScheme := ElecAddressScheme::"0223"; + ElecAddressScheme := ElecAddressScheme::"9957"; end; exit(ElecAddress <> ''); end; @@ -435,10 +456,10 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" exit('0009'); ElecAddressScheme::"0002": exit('0002'); - ElecAddressScheme::"0223": - exit('0223'); ElecAddressScheme::"0225": exit('0225'); + ElecAddressScheme::"9957": + exit('9957'); else exit(Format(ElecAddressScheme)); end; @@ -508,5 +529,6 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" CacNamespaceTok: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', Locked = true; ExtendedCTCFranceCustomizationIdTok: Label 'EXTENDED-CTC-FR', Locked = true; RegulatoryCommentFormatTok: Label '#%1#%2', Locked = true; + BillingModeB1Tok: Label 'B1', Locked = true; InvoiceLineXPathTok: Label '/*/cac:InvoiceLine[cbc:ID=''%1'']', Locked = true; } From 129115e5d95587c2a981c7a936afa7e6b3057c11 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 4 Aug 2026 19:39:31 +0200 Subject: [PATCH 42/77] extended billing mode --- .../app/src/Core/EDocHelpers.Codeunit.al | 4 +- .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 81 ++++++++++++++++--- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 39 ++++++--- 3 files changed, 104 insertions(+), 20 deletions(-) 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 e4fe44e8ab6..1d207d68a3d 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 @@ -70,6 +70,8 @@ codeunit 10991 "EDoc. Helpers" CompanyInformation.Get(); if CompanyInformation."SIRET No." <> '' then exit; + if CompanyInformation."Registration No." <> '' then + exit; if CompanyInformation.GetVATRegistrationNumber() <> '' then exit; @@ -148,7 +150,7 @@ codeunit 10991 "EDoc. Helpers" 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.'; - SellerElectronicAddressRequiredErr: Label 'SIRET No., VAT Registration No., or a Service Participant identifier must be specified for the company for French e-invoicing.'; + SellerElectronicAddressRequiredErr: Label 'SIRET No., Registration No., VAT Registration No., or a Service Participant identifier must be specified for the company for French e-invoicing.'; BuyerElectronicAddressRequiredErr: Label 'Electronic Address, VAT Registration No., or a Service Participant identifier must be specified for Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.'; BuyerElectronicAddressSchemeRequiredErr: Label 'Electronic Address Scheme 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.'; 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 3eae8f39b04..ea8c9bfeb63 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 @@ -11,7 +11,10 @@ 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" @@ -43,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, EDocumentService); + 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") @@ -66,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; EDocumentService: Record "E-Document Service") + 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; @@ -84,7 +87,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" InitNamespaceManager(NamespaceMgr, XmlDoc); - SetFrenchBillingMode(XmlDoc, NamespaceMgr); + SetFrenchBillingMode(XmlDoc, NamespaceMgr, SourceDocumentLines); RemoveZeroAllowanceTotal(XmlDoc, NamespaceMgr); InjectSupplierIdentification(XmlDoc, NamespaceMgr, CompanyInformation); InjectSupplierEndpoint(XmlDoc, NamespaceMgr, CompanyInformation, EDocumentService.Code); @@ -100,17 +103,71 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" XmlDoc.WriteTo(OutStr); end; - local procedure SetFrenchBillingMode(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager) + 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, BillingModeB1Tok).AsXmlNode(); + 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"; + LineCount: Integer; + ItemLineCount: Integer; + begin + case SourceDocumentLines.Number of + Database::"Sales Invoice Line": + begin + SourceDocumentLines.SetTable(SalesInvoiceLine); + SalesInvoiceLine.SetFilter(Type, '<>%1', SalesInvoiceLine.Type::" "); + LineCount := SalesInvoiceLine.Count(); + SalesInvoiceLine.SetRange(Type, SalesInvoiceLine.Type::Item); + ItemLineCount := SalesInvoiceLine.Count(); + end; + Database::"Sales Cr.Memo Line": + begin + SourceDocumentLines.SetTable(SalesCrMemoLine); + SalesCrMemoLine.SetFilter(Type, '<>%1', SalesCrMemoLine.Type::" "); + LineCount := SalesCrMemoLine.Count(); + SalesCrMemoLine.SetRange(Type, SalesCrMemoLine.Type::Item); + ItemLineCount := SalesCrMemoLine.Count(); + end; + Database::"Service Invoice Line": + begin + SourceDocumentLines.SetTable(ServiceInvoiceLine); + ServiceInvoiceLine.SetFilter(Type, '<>%1', ServiceInvoiceLine.Type::" "); + LineCount := ServiceInvoiceLine.Count(); + ServiceInvoiceLine.SetRange(Type, ServiceInvoiceLine.Type::Item); + ItemLineCount := ServiceInvoiceLine.Count(); + end; + Database::"Service Cr.Memo Line": + begin + SourceDocumentLines.SetTable(ServiceCrMemoLine); + ServiceCrMemoLine.SetFilter(Type, '<>%1', ServiceCrMemoLine.Type::" "); + LineCount := ServiceCrMemoLine.Count(); + ServiceCrMemoLine.SetRange(Type, ServiceCrMemoLine.Type::Item); + ItemLineCount := ServiceCrMemoLine.Count(); + end; + Database::"Issued Reminder Line", + Database::"Issued Fin. Charge Memo Line": + LineCount := 1; + end; + + if (ItemLineCount > 0) and (ItemLineCount < LineCount) then + exit(BillingModeB3Tok); + if (LineCount > 0) and (ItemLineCount = 0) then + exit(BillingModeB2Tok); + exit(BillingModeB1Tok); + end; + local procedure RemoveZeroAllowanceTotal(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager) var ZeroAllowanceTotalNode: XmlNode; @@ -343,10 +400,14 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" if CompanyInformation."SIRET No." <> '' then begin ElecAddress := CompanyInformation."SIRET No."; ElecAddressScheme := ElecAddressScheme::"0009"; - end else begin - ElecAddress := CopyStr(CompanyInformation.GetVATRegistrationNumber(), 1, MaxStrLen(ElecAddress)); - ElecAddressScheme := ElecAddressScheme::"9957"; - end; + 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; @@ -530,5 +591,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" ExtendedCTCFranceCustomizationIdTok: Label 'EXTENDED-CTC-FR', Locked = true; RegulatoryCommentFormatTok: Label '#%1#%2', Locked = true; BillingModeB1Tok: Label 'B1', Locked = true; + BillingModeB2Tok: Label 'B2', Locked = true; + BillingModeB3Tok: Label 'B3', Locked = true; InvoiceLineXPathTok: Label '/*/cac:InvoiceLine[cbc:ID=''%1'']', Locked = true; } 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 ef377f08ab8..6f73681c479 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 @@ -324,6 +324,22 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Assert.AreEqual('#AAB#' + CommentText, GetNodeByPath(XmlDoc, '/Invoice/cbc:Note'), StrSubstNo(IncorrectValueErr, 'Note')); end; + [Test] + procedure ExportSalesInvSetsB2ForServiceLines() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; + begin + // [SCENARIO] An invoice containing only service lines uses billing mode B2 + Initialize(); + + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('', "Electronic Address Scheme"::"EM"))); + + ExportInvoice(SalesInvoiceHeader, XmlDoc); + + Assert.AreEqual('B2', GetNodeByPath(XmlDoc, '/Invoice/cbc:ProfileID'), StrSubstNo(IncorrectValueErr, 'ProfileID')); + end; + [Test] procedure ExportSalesInvUsesServiceParticipantEndpointWithScheme0225() var @@ -660,39 +676,42 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" CheckInvoice(SalesInvoiceHeader); ExportInvoice(SalesInvoiceHeader, XmlDoc); - // [THEN] The buyer endpoint uses the VAT identifier and scheme 0223 + // [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('0223', + Assert.AreEqual('9957', GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'), StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID')); end; [Test] - procedure CheckRaisesErrorWhenSellerElectronicAddressIsMissing() + procedure ExportSalesInvUsesSupplierSIRENWhenSIRETIsMissing() var SalesInvoiceHeader: Record "Sales Invoice Header"; + XmlDoc: XmlDocument; OriginalSIRETNo: Code[14]; - OriginalVATRegistrationNo: Text[20]; begin - // [SCENARIO] Check rejects a seller without SIRET, VAT registration number, or a service participant identifier + // [SCENARIO] Company Registration No. is used as the seller endpoint when SIRET is missing Initialize(); OriginalSIRETNo := CompanyInformation."SIRET No."; - OriginalVATRegistrationNo := CompanyInformation."VAT Registration No."; CompanyInformation."SIRET No." := ''; - CompanyInformation."VAT Registration No." := ''; CompanyInformation.Modify(true); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); - asserterror CheckInvoice(SalesInvoiceHeader); + CheckInvoice(SalesInvoiceHeader); + ExportInvoice(SalesInvoiceHeader, XmlDoc); - Assert.ExpectedError('SIRET No., VAT Registration No., or a Service Participant identifier must be specified for the company for French e-invoicing.'); + 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."VAT Registration No." := OriginalVATRegistrationNo; CompanyInformation.Modify(true); end; From 900be70eeed7c7e217fba446a31d3bc83de96d39 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 4 Aug 2026 19:53:29 +0200 Subject: [PATCH 43/77] BillingMode --- .../src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 8 ++++---- .../EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) 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 ea8c9bfeb63..2670edcafb3 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 @@ -162,9 +162,9 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" end; if (ItemLineCount > 0) and (ItemLineCount < LineCount) then - exit(BillingModeB3Tok); + exit(BillingModeM1Tok); if (LineCount > 0) and (ItemLineCount = 0) then - exit(BillingModeB2Tok); + exit(BillingModeS1Tok); exit(BillingModeB1Tok); end; @@ -591,7 +591,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" ExtendedCTCFranceCustomizationIdTok: Label 'EXTENDED-CTC-FR', Locked = true; RegulatoryCommentFormatTok: Label '#%1#%2', Locked = true; BillingModeB1Tok: Label 'B1', Locked = true; - BillingModeB2Tok: Label 'B2', Locked = true; - BillingModeB3Tok: Label 'B3', Locked = true; + BillingModeS1Tok: Label 'S1', Locked = true; + BillingModeM1Tok: Label 'M1', Locked = true; InvoiceLineXPathTok: Label '/*/cac:InvoiceLine[cbc:ID=''%1'']', Locked = true; } 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 6f73681c479..f540b1f3f64 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 @@ -325,19 +325,19 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" end; [Test] - procedure ExportSalesInvSetsB2ForServiceLines() + procedure ExportSalesInvSetsS1ForServiceLines() var SalesInvoiceHeader: Record "Sales Invoice Header"; XmlDoc: XmlDocument; begin - // [SCENARIO] An invoice containing only service lines uses billing mode B2 + // [SCENARIO] An invoice containing only service lines uses billing mode S1 Initialize(); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('', "Electronic Address Scheme"::"EM"))); ExportInvoice(SalesInvoiceHeader, XmlDoc); - Assert.AreEqual('B2', GetNodeByPath(XmlDoc, '/Invoice/cbc:ProfileID'), StrSubstNo(IncorrectValueErr, 'ProfileID')); + Assert.AreEqual('S1', GetNodeByPath(XmlDoc, '/Invoice/cbc:ProfileID'), StrSubstNo(IncorrectValueErr, 'ProfileID')); end; [Test] From 897bd33342b1d144efe905b39de20c6c13df8517 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 4 Aug 2026 20:05:15 +0200 Subject: [PATCH 44/77] added .xml file extension --- .../app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 7 +++++++ 1 file changed, 7 insertions(+) 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 2670edcafb3..c0ff900e10c 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 @@ -551,6 +551,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 From b965f3707acdd40868f47ee584244c5cadde1352 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 4 Aug 2026 22:16:21 +0200 Subject: [PATCH 45/77] optimizations --- .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 61 ++++++++++--------- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 17 ++++-- 2 files changed, 42 insertions(+), 36 deletions(-) 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 c0ff900e10c..3b95b46ed65 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 @@ -120,50 +120,50 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" SalesCrMemoLine: Record "Sales Cr.Memo Line"; ServiceInvoiceLine: Record "Service Invoice Line"; ServiceCrMemoLine: Record "Service Cr.Memo Line"; - LineCount: Integer; - ItemLineCount: Integer; + HasItemLines: Boolean; + HasNonItemLines: Boolean; begin case SourceDocumentLines.Number of Database::"Sales Invoice Line": begin SourceDocumentLines.SetTable(SalesInvoiceLine); - SalesInvoiceLine.SetFilter(Type, '<>%1', SalesInvoiceLine.Type::" "); - LineCount := SalesInvoiceLine.Count(); SalesInvoiceLine.SetRange(Type, SalesInvoiceLine.Type::Item); - ItemLineCount := SalesInvoiceLine.Count(); + 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.SetFilter(Type, '<>%1', SalesCrMemoLine.Type::" "); - LineCount := SalesCrMemoLine.Count(); SalesCrMemoLine.SetRange(Type, SalesCrMemoLine.Type::Item); - ItemLineCount := SalesCrMemoLine.Count(); + 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.SetFilter(Type, '<>%1', ServiceInvoiceLine.Type::" "); - LineCount := ServiceInvoiceLine.Count(); ServiceInvoiceLine.SetRange(Type, ServiceInvoiceLine.Type::Item); - ItemLineCount := ServiceInvoiceLine.Count(); + 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.SetFilter(Type, '<>%1', ServiceCrMemoLine.Type::" "); - LineCount := ServiceCrMemoLine.Count(); ServiceCrMemoLine.SetRange(Type, ServiceCrMemoLine.Type::Item); - ItemLineCount := ServiceCrMemoLine.Count(); + 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": - LineCount := 1; + HasNonItemLines := true; end; - if (ItemLineCount > 0) and (ItemLineCount < LineCount) then + if HasItemLines and HasNonItemLines then exit(BillingModeM1Tok); - if (LineCount > 0) and (ItemLineCount = 0) then + if HasNonItemLines then exit(BillingModeS1Tok); exit(BillingModeB1Tok); end; @@ -180,6 +180,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" var SalesInvoiceHeader: Record "Sales Invoice Header"; SalesInvoiceLine: Record "Sales Invoice Line"; + ShipmentPostingDates: Dictionary of [Code[20], Date]; CustomizationIdNode: XmlNode; NewCustomizationIdNode: XmlNode; begin @@ -187,7 +188,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" exit; SourceDocumentHeader.SetTable(SalesInvoiceHeader); - if not RequiresExtendedCTCFrance(SalesInvoiceHeader."No.") then + if not RequiresExtendedCTCFrance(SalesInvoiceHeader."No.", ShipmentPostingDates) then exit; if XmlDoc.SelectSingleNode('/*/cbc:CustomizationID', NamespaceMgr, CustomizationIdNode) then begin @@ -196,14 +197,14 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" end; SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); - SalesInvoiceLine.SetLoadFields("Line No.", "Order No.", "Order Line No.", "Shipment No.", "Shipment Line No."); + SalesInvoiceLine.SetLoadFields("Line No.", "Order No.", "Order Line No.", "Shipment No."); if SalesInvoiceLine.FindSet() then repeat - InjectExtendedLineReferences(XmlDoc, NamespaceMgr, SalesInvoiceLine); + InjectExtendedLineReferences(XmlDoc, NamespaceMgr, SalesInvoiceLine, ShipmentPostingDates); until SalesInvoiceLine.Next() = 0; end; - local procedure RequiresExtendedCTCFrance(DocumentNo: Code[20]): Boolean + 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"; @@ -219,8 +220,10 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" if SalesInvoiceLine."Shipment No." <> '' then if not ShipmentNos.ContainsKey(SalesInvoiceLine."Shipment No.") then begin ShipmentNos.Add(SalesInvoiceLine."Shipment No.", true); - if SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No.") then + if SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No.") then begin + ShipmentPostingDates.Add(SalesInvoiceLine."Shipment No.", SalesShipmentHeader."Posting Date"); AddDistinctValue(DeliveryDates, Format(SalesShipmentHeader."Posting Date", 0, 9)); + end; end; if SalesInvoiceLine."Order No." <> '' then AddDistinctValue(OrderNos, SalesInvoiceLine."Order No."); @@ -235,16 +238,15 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" Values.Add(Value, true); end; - local procedure InjectExtendedLineReferences(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; SalesInvoiceLine: Record "Sales Invoice Line") + local procedure InjectExtendedLineReferences(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; SalesInvoiceLine: Record "Sales Invoice Line"; ShipmentPostingDates: Dictionary of [Code[20], Date]) var - SalesShipmentHeader: Record "Sales Shipment Header"; - SalesShipmentLine: Record "Sales Shipment Line"; 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 @@ -263,14 +265,13 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" if SalesInvoiceLine."Shipment No." = '' then exit; - if not SalesShipmentLine.Get(SalesInvoiceLine."Shipment No.", SalesInvoiceLine."Shipment Line No.") then - exit; - if not SalesShipmentHeader.Get(SalesShipmentLine."Document No.") then + + if not ShipmentPostingDates.Get(SalesInvoiceLine."Shipment No.", ShipmentPostingDate) then exit; DeliveryElement := XmlElement.Create('Delivery', CacNamespaceTok); - DeliveryElement.Add(XmlElement.Create('ID', CbcNamespaceTok, SalesShipmentLine."Document No.")); - DeliveryElement.Add(XmlElement.Create('ActualDeliveryDate', CbcNamespaceTok, Format(SalesShipmentHeader."Posting Date", 0, 9))); + DeliveryElement.Add(XmlElement.Create('ID', CbcNamespaceTok, SalesInvoiceLine."Shipment No.")); + DeliveryElement.Add(XmlElement.Create('ActualDeliveryDate', CbcNamespaceTok, Format(ShipmentPostingDate, 0, 9))); ItemNode.AddBeforeSelf(DeliveryElement); 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 f540b1f3f64..7c3803a278e 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 @@ -47,6 +47,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" PeppolBIS30FRFormat: Codeunit "Peppol BIS 3.0 FR Format"; BuyerElectronicAddressRequiredErr: Label 'Electronic Address, VAT Registration No., or a Service Participant identifier must be specified for Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.', Locked = true; BuyerElectronicAddressSchemeRequiredErr: Label 'Electronic Address Scheme must be specified for Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.', 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; IsInitialized: Boolean; @@ -510,7 +511,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" ExportInvoice(SalesInvoiceHeader, XmlDoc); - Assert.AreNotEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), + 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 @@ -586,11 +587,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" CheckInvoice(SalesInvoiceHeader); ExportInvoice(SalesInvoiceHeader, XmlDoc); - // [THEN] The supplier endpoint uses the VAT identifier and scheme 0223 + // [THEN] The supplier endpoint uses the VAT identifier and scheme 9957 Assert.AreEqual(CompanyInformation.GetVATRegistrationNumber(), GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID'), StrSubstNo(IncorrectValueErr, 'Seller EndpointID')); - Assert.AreEqual('0223', + Assert.AreEqual('9957', GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID'), StrSubstNo(IncorrectValueErr, 'Seller EndpointID schemeID')); @@ -733,7 +734,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" asserterror CheckInvoice(SalesInvoiceHeader); - Assert.ExpectedError('Participant Identifier and French Identifier Scheme must both be specified for French electronic invoicing.'); + Assert.ExpectedError(ParticipantAddressIncompleteErr); end; [Test] @@ -792,7 +793,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" asserterror CheckInvoice(SalesInvoiceHeader); - Assert.ExpectedError('Participant Identifier and French Identifier Scheme must both be specified for French electronic invoicing.'); + Assert.ExpectedError(ParticipantAddressIncompleteErr); end; [Test] @@ -816,7 +817,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" asserterror CheckInvoice(SalesInvoiceHeader); - Assert.ExpectedError('Participant Identifier and French Identifier Scheme must both be specified for French electronic invoicing.'); + Assert.ExpectedError(ParticipantAddressIncompleteErr); end; #endregion @@ -894,6 +895,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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"); @@ -901,8 +903,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesLine.FindFirst(); SalesLine.Validate("Qty. to Ship", 1); SalesLine.Modify(true); + OriginalWorkDate := WorkDate(); + WorkDate(CalcDate('<-1D>', OriginalWorkDate)); FirstShipmentNo := LibrarySales.PostSalesDocument(SalesHeader, true, false); + WorkDate(OriginalWorkDate); SalesHeader.Get(SalesHeader."Document Type", SalesHeader."No."); SalesLine.FindFirst(); SalesLine.Validate("Qty. to Ship", 1); From bdb6afdfaa0d425081b25234ceebdb3a97975e2e Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 5 Aug 2026 10:28:34 +0200 Subject: [PATCH 46/77] update tests --- .../EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7c3803a278e..8e36d4de740 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 @@ -834,7 +834,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" exit; LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"PEPPOL BIS 3.0 XML Tests"); - CountryRegion.Get('FR'); + LibraryERM.CreateCountryRegion(CountryRegion); CountryRegion.Validate("ISO Code", 'FR'); CountryRegion.Modify(true); From e92887b405e73e7e172728668718684490e4b166 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 5 Aug 2026 10:42:16 +0200 Subject: [PATCH 47/77] PR comments changes --- .../EReportingFR/app/src/Core/EDocHelpers.Codeunit.al | 11 +++++++++-- .../Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 2 +- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 8 ++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) 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 1d207d68a3d..be34dd93a45 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 @@ -106,6 +106,7 @@ codeunit 10991 "EDoc. Helpers" if CustomerNo = '' then exit; + Customer.SetLoadFields("FR Electronic Address", "FR Elec. Address Scheme", "VAT Registration No."); if not Customer.Get(CustomerNo) then exit; @@ -131,6 +132,7 @@ codeunit 10991 "EDoc. Helpers" 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 @@ -141,8 +143,13 @@ codeunit 10991 "EDoc. Helpers" HasIdentifier := ServiceParticipant."Participant Identifier" <> ''; HasScheme := ServiceParticipant."FR Identifier Scheme" <> ServiceParticipant."FR Identifier Scheme"::" "; - if HasIdentifier <> HasScheme then - Error(ServiceParticipantAddressIncompleteErr, ServiceParticipant.FieldCaption("Participant Identifier"), ServiceParticipant.FieldCaption("FR Identifier Scheme")); + if HasIdentifier <> HasScheme then begin + ParticipantAddressErrorInfo.Message( + StrSubstNo(ServiceParticipantAddressIncompleteErr, ServiceParticipant.FieldCaption("Participant Identifier"), ServiceParticipant.FieldCaption("FR Identifier Scheme"))); + ParticipantAddressErrorInfo.RecordId(ServiceParticipant.RecordId()); + ParticipantAddressErrorInfo.PageNo(Page::"Service Participants"); + Error(ParticipantAddressErrorInfo); + end; exit(HasIdentifier); end; 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 3b95b46ed65..3de8ddca9b4 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 @@ -597,7 +597,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" 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', 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; 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 8e36d4de740..a1c10a37838 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 @@ -570,15 +570,18 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" var SalesInvoiceHeader: Record "Sales Invoice Header"; XmlDoc: XmlDocument; + OriginalRegistrationNo: Text[20]; OriginalSIRETNo: Code[14]; begin - // [SCENARIO] Company VAT registration number is used as the seller endpoint when SIRET is blank + // [SCENARIO] Company VAT registration number is used as the seller endpoint when SIRET and SIREN are blank Initialize(); - // [GIVEN] Company with blank SIRET No. and a VAT registration number + // [GIVEN] Company with blank SIRET No. and Registration No., and a VAT registration number OriginalSIRETNo := CompanyInformation."SIRET No."; + OriginalRegistrationNo := CompanyInformation."Registration No."; CompanyInformation.Get(); CompanyInformation."SIRET No." := ''; + CompanyInformation."Registration No." := ''; CompanyInformation.Modify(true); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); @@ -598,6 +601,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" // Cleanup CompanyInformation.Get(); CompanyInformation."SIRET No." := OriginalSIRETNo; + CompanyInformation."Registration No." := CopyStr(OriginalRegistrationNo, 1, MaxStrLen(CompanyInformation."Registration No.")); CompanyInformation.Modify(true); end; From 3facb2af3a25a675f8aefe8577cb230b77f29e86 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 5 Aug 2026 12:16:05 +0200 Subject: [PATCH 48/77] new --- .../app/src/Extensions/EDocumentMessageType.EnumExt.al | 2 +- .../app/src/Formats/PEPPOL/EDocPeppolBIS30FRHdlr.Codeunit.al | 2 +- .../app/src/Permissions/EReportingFREdit.PermissionSet.al | 4 ++-- .../src/Permissions/EReportingFREditExt.PermissionSetExt.al | 2 +- .../app/src/Permissions/EReportingFRRead.PermissionSet.al | 2 +- .../src/Permissions/EReportingFRReadExt.PermissionSetExt.al | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EDocumentMessageType.EnumExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EDocumentMessageType.EnumExt.al index aa36f5a9343..18c5a363c9e 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EDocumentMessageType.EnumExt.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EDocumentMessageType.EnumExt.al @@ -7,7 +7,7 @@ namespace Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument.Processing.Interfaces; using Microsoft.eServices.EDocument.Processing.Message; -enumextension 10971 "FR E-Document Message Type" extends "E-Document Message Type" +enumextension 10973 "FR E-Document Message Type" extends "E-Document Message Type" { value(10970; "FR Invoice Lifecycle") { 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..4c8e7b7856e 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; 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 index 36c179085b5..700c0bade7c 100644 --- 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 @@ -4,13 +4,13 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Formats; -permissionset 10972 "E-Reporting FR - Edit" +permissionset 10972 "E-Reporting FR Edit" { Access = Public; Assignable = true; Caption = 'E-Reporting FR - Edit'; - IncludedPermissionSets = "E-Reporting FR - Read"; + IncludedPermissionSets = "E-Reporting FR Read"; Permissions = tabledata "FR E-Invoice Lifecycle" = IM, tabledata "FR E-Invoice Lifecycle VAT" = I; 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 index ea5e64db2ec..e028e689480 100644 --- 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 @@ -8,5 +8,5 @@ using Microsoft.eServices.EDocument; permissionsetextension 10975 "E-Reporting FR - Edit Ext" extends "E-Doc. Core - User" { - IncludedPermissionSets = "E-Reporting FR - Edit"; + 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 index 35b888df0ad..5de18d06840 100644 --- 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 @@ -4,7 +4,7 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Formats; -permissionset 10971 "E-Reporting FR - Read" +permissionset 10971 "E-Reporting FR Read" { Access = Public; Assignable = true; 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 index 4f56fbdc6e0..abeab2d8f80 100644 --- 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 @@ -8,5 +8,5 @@ using Microsoft.eServices.EDocument; permissionsetextension 10976 "E-Reporting FR - Read Ext" extends "E-Doc. Core - Read" { - IncludedPermissionSets = "E-Reporting FR - Read"; + IncludedPermissionSets = "E-Reporting FR Read"; } From f1a78dd13069784660f215d76e6d9f278b976c0a Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 5 Aug 2026 12:28:01 +0200 Subject: [PATCH 49/77] changed internal procedures --- .../app/src/Extensions/FREInvoiceLifecycles.Page.al | 1 - .../FacturX/EDocumentFacturXHandler.Codeunit.al | 12 ++++++------ .../Formats/PEPPOL/EDocPeppolBIS30FRHdlr.Codeunit.al | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) 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 index 9027e1e2a75..bc99e20eb40 100644 --- 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 @@ -69,7 +69,6 @@ page 10970 "FR E-Invoice Lifecycles" ApplicationArea = Basic, Suite; Caption = 'Retry Message Creation'; Enabled = Rec."Processing Status" = Rec."Processing Status"::Failed; - Image = Reopen; ToolTip = 'Schedules another attempt to create the lifecycle message.'; trigger OnAction() 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..d6ffa2cdfa5 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; 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 4c8e7b7856e..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 @@ -41,7 +41,7 @@ codeunit 10980 "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 10980 "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"; From af0e365f346330d67edd1045b159785af4f83357 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 5 Aug 2026 12:37:17 +0200 Subject: [PATCH 50/77] add CreateMessage public --- .../Core/FREInvoiceLifecycleMgt.Codeunit.al | 4 +-- .../EDocCoreObjects.PermissionSet.al | 1 + .../Message/EDocumentMessageAPI.Codeunit.al | 28 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al 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 index f01f9a3dc36..842a41c3f09 100644 --- 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 @@ -276,7 +276,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." internal procedure CreateLifecycleMessage(var FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") var EDocument: Record "E-Document"; - EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + EDocumentMessageAPI: Codeunit "E-Document Message API"; FREInvoiceLifecycleMsg: Codeunit "FR E-Invoice Lifecycle Msg."; TempBlob: Codeunit "Temp Blob"; begin @@ -285,7 +285,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." EDocument.Get(FREInvoiceLifecycle."E-Document Entry No."); FREInvoiceLifecycleMsg.BuildLifecycleMessage(EDocument, FREInvoiceLifecycle, TempBlob); - FREInvoiceLifecycle."E-Document Message Entry No." := EDocMessageMgt.CreateMessage( + 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"); diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al index 111bf12d208..acbac7b47d4 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al @@ -102,6 +102,7 @@ permissionset 6100 "E-Doc. Core - Objects" codeunit "E-Doc. PO Copilot Matching" = X, #endif codeunit "E-Doc. Attachment Processor" = X, + codeunit "E-Document Message API" = X, codeunit "E-Doc. Message Mgt." = X, codeunit "Service Participant" = X, page "E-Doc. Changes Part" = X, 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..e368313cba9 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------------------------------ +// 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 6436 "E-Document Message API" +{ + Access = Public; + InherentEntitlements = X; + InherentPermissions = X; + + /// + /// Creates an E-Document message with an explicit direction and stores its payload. + /// + 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; +} \ No newline at end of file From fde55a791a1bd73b0d10456f0eac67b486768365 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 5 Aug 2026 13:09:38 +0200 Subject: [PATCH 51/77] FrenchLifecycle visibility --- .../Extensions/EReportingEDocumentService.PageExt.al | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.PageExt.al index a0d0b1a9325..edcc83152d1 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.PageExt.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.PageExt.al @@ -15,7 +15,7 @@ pageextension 10977 "E-Reporting E-Doc. Service" extends "E-Document Service" group(FrenchLifecycle) { Caption = 'French Invoice Lifecycle'; - Visible = IsFrenchInvoiceFormat; + 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") { @@ -32,12 +32,4 @@ pageextension 10977 "E-Reporting E-Doc. Service" extends "E-Document Service" } } } - - trigger OnAfterGetCurrRecord() - begin - IsFrenchInvoiceFormat := Rec."Document Format" in [Rec."Document Format"::"Peppol BIS 3.0 FR", Rec."Document Format"::"Factur-X FR"]; - end; - - var - IsFrenchInvoiceFormat: Boolean; } \ No newline at end of file From 4d309b7e373bcd575721e1aea9c96c9affae709b Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 5 Aug 2026 16:53:51 +0200 Subject: [PATCH 52/77] testing changes --- .../app/src/Core/EDocHelpers.Codeunit.al | 46 +++++++++++++++++++ .../app/src/Core/FREInvoiceLifecycle.Table.al | 4 +- .../Core/FREInvoiceLifecycleMgt.Codeunit.al | 33 +++++++------ .../Core/FREInvoiceLifecycleMsg.Codeunit.al | 21 +++++++-- .../src/Core/FREInvoiceLifecycleVAT.Table.al | 10 ---- .../Extensions/FREInvoiceLifecycleVAT.Page.al | 33 +++++++++++++ .../Extensions/FREInvoiceLifecycles.Page.al | 6 +++ .../EReporting/EReportingFormat.Codeunit.al | 29 ------------ .../test/src/IdentificationTests.Codeunit.al | 42 ++++++++++++----- 9 files changed, 156 insertions(+), 68 deletions(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREInvoiceLifecycleVAT.Page.al 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 be34dd93a45..1aab6b51f1e 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 @@ -12,6 +12,52 @@ using Microsoft.Sales.Customer; codeunit 10991 "EDoc. Helpers" { Access = Internal; + Permissions = tabledata "E-Document" = m, + tabledata "E-Document Service Status" = m; + + [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: + 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 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 index d06b79721cd..8cae040cb9f 100644 --- 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 @@ -64,8 +64,8 @@ table 10970 "FR E-Invoice Lifecycle" { Caption = 'Currency Code'; DataClassification = CustomerContent; - TableRelation = Currency.Code; - ToolTip = 'Specifies the currency of the reported amount.'; + TableRelation = Currency; + ToolTip = 'Specifies the currency of the reported amount. Blank represents the local currency.'; } field(8; "Event Date"; Date) { 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 index 842a41c3f09..40f7b7319f0 100644 --- 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 @@ -88,8 +88,10 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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 - CurrencyCode := ResolveCurrencyCode(CurrencyCode); + ResolvedCurrencyCode := ResolveCurrencyCode(CurrencyCode); ValidatePaymentOccurrence(EDocumentEntryNo, LifecycleStatus, SourceOccurrenceID, ReportedAmount, EventDate, OriginalOccurrenceEntryNo); if FindOccurrence(FREInvoiceLifecycle, EDocumentEntryNo, LifecycleStatus, SourceOccurrenceID) then begin @@ -116,7 +118,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." if InvoiceCustLedgerEntryNo <> 0 then if LifecycleStatus = LifecycleStatus::Collected then - CreateVATBreakdown(FREInvoiceLifecycle) + CreateVATBreakdown(FREInvoiceLifecycle, ResolvedCurrencyCode) else CreateReversalVATBreakdown(FREInvoiceLifecycle, OriginalOccurrenceEntryNo); end; @@ -126,11 +128,13 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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.Get(FREInvoiceLifecycle."E-Document Entry No."); EDocument.TestField("Document Date"); EDocument.TestField("Clearance Date"); - EDocumentService.Get(EDocument.Service); + EDocHelpers.GetFrenchEDocumentService(EDocument, EDocumentService, EDocumentServiceStatus); EDocumentService.TestField("FR Sender Platform ID"); EDocumentService.TestField("FR Sender Platform Scheme"); EDocumentService.TestField("FR Sender Platform Name"); @@ -148,7 +152,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." FREInvoiceLifecycle."Invoice Issuer Name" := CompanyInformation.Name; end; - local procedure CreateVATBreakdown(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle") + local procedure CreateVATBreakdown(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; CurrencyCode: Code[10]) var InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; VATEntry: Record "VAT Entry"; @@ -165,7 +169,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." repeat VATPostingSetup.Get(VATEntry."VAT Bus. Posting Group", VATEntry."VAT Prod. Posting Group"); VATRate := VATPostingSetup."VAT %"; - GrossAmount := GetVATEntryGrossAmount(VATEntry, FREInvoiceLifecycle."Currency Code"); + GrossAmount := GetVATEntryGrossAmount(VATEntry, CurrencyCode); AddVATRateAmount(AmountByVATRate, VATRates, VATRate, GrossAmount); TotalGrossAmount += GrossAmount; until VATEntry.Next() = 0; @@ -173,7 +177,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." if (VATRates.Count() = 0) or (TotalGrossAmount = 0) then Error(VATBreakdownErr, InvoiceCustLedgerEntry."Document No."); - InsertAllocatedVATAmounts(FREInvoiceLifecycle, AmountByVATRate, VATRates, TotalGrossAmount); + InsertAllocatedVATAmounts(FREInvoiceLifecycle, AmountByVATRate, VATRates, TotalGrossAmount, CurrencyCode); end; local procedure FindInvoiceVATEntries(var VATEntry: Record "VAT Entry"; InvoiceCustLedgerEntry: Record "Cust. Ledger Entry") @@ -207,7 +211,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." VATRates.Add(VATRate); end; - local procedure InsertAllocatedVATAmounts(FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; AmountByVATRate: Dictionary of [Decimal, Decimal]; VATRates: List of [Decimal]; TotalGrossAmount: Decimal) + 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"; @@ -217,7 +221,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." VATRate: Decimal; LineNo: Integer; begin - RoundingPrecision := GetAmountRoundingPrecision(Currency, FREInvoiceLifecycle."Currency Code"); + RoundingPrecision := GetAmountRoundingPrecision(Currency, CurrencyCode); RemainingAmount := FREInvoiceLifecycle."Reported Amount"; LineNo := 0; foreach VATRate in VATRates do begin @@ -229,7 +233,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." RemainingAmount -= AllocatedAmount; end; - InsertVATBreakdown(FREInvoiceLifecycleVAT, FREInvoiceLifecycle."Entry No.", LineNo, VATRate, AllocatedAmount, FREInvoiceLifecycle."Currency Code"); + InsertVATBreakdown(FREInvoiceLifecycleVAT, FREInvoiceLifecycle."Entry No.", LineNo, VATRate, AllocatedAmount); end; end; @@ -258,18 +262,17 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." repeat InsertVATBreakdown( ReversalLifecycleVAT, FREInvoiceLifecycle."Entry No.", OriginalLifecycleVAT."Line No.", OriginalLifecycleVAT."VAT %", - -OriginalLifecycleVAT."Reported Amount", OriginalLifecycleVAT."Currency Code"); + -OriginalLifecycleVAT."Reported Amount"); 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]) + local procedure InsertVATBreakdown(var FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; LifecycleEntryNo: Integer; LineNo: Integer; VATRate: Decimal; ReportedAmount: Decimal) 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; @@ -394,8 +397,12 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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(EDocumentService.Get(EDocument.Service) and IsFREInvoiceFormat(EDocumentService."Document Format")); + exit( + EDocHelpers.GetFrenchEDocumentService(EDocument, EDocumentService, EDocumentServiceStatus) and + IsFREInvoiceFormat(EDocumentService."Document Format")); end; local procedure IsFREInvoiceFormat(EDocumentFormat: Enum "E-Document Format"): Boolean 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 index df97f0fa152..1c19c0d684b 100644 --- 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 @@ -7,6 +7,7 @@ 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 @@ -150,19 +151,21 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder var FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; SpecifiedDocumentStatusElement: XmlElement; + CurrencyCode: Code[10]; begin FREInvoiceLifecycleVAT.SetRange("Lifecycle Entry No.", FREInvoiceLifecycle."Entry No."); if not FREInvoiceLifecycleVAT.FindSet() then Error(VATBreakdownErr, FREInvoiceLifecycle."Entry No."); + CurrencyCode := ResolveCurrencyCode(FREInvoiceLifecycle."Currency Code"); SpecifiedDocumentStatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); repeat - SpecifiedDocumentStatusElement.Add(CreateVATCharacteristic(FREInvoiceLifecycleVAT)); + SpecifiedDocumentStatusElement.Add(CreateVATCharacteristic(FREInvoiceLifecycleVAT, CurrencyCode)); until FREInvoiceLifecycleVAT.Next() = 0; ReferenceDocumentElement.Add(SpecifiedDocumentStatusElement); end; - local procedure CreateVATCharacteristic(FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT") CharacteristicElement: XmlElement + local procedure CreateVATCharacteristic(FREInvoiceLifecycleVAT: Record "FR E-Invoice Lifecycle VAT"; CurrencyCode: Code[10]) CharacteristicElement: XmlElement var AmountElement: XmlElement; ValueChangedElement: XmlElement; @@ -173,11 +176,23 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder 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', FREInvoiceLifecycleVAT."Currency Code")); + 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, '')); 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 index dddd0a37a1c..e7270c3540e 100644 --- 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 @@ -4,8 +4,6 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Formats; -using Microsoft.Finance.Currency; - table 10971 "FR E-Invoice Lifecycle VAT" { Caption = 'FR E-Invoice Lifecycle VAT'; @@ -37,19 +35,11 @@ table 10971 "FR E-Invoice Lifecycle VAT" } 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; - TableRelation = Currency.Code; - ToolTip = 'Specifies the currency of the reported VAT amount.'; - } } keys 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 index bc99e20eb40..ac166071299 100644 --- 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 @@ -57,6 +57,12 @@ page 10970 "FR E-Invoice Lifecycles" ApplicationArea = Basic, Suite; } } + part(VATBreakdown; "FR E-Invoice Lifecycle VAT") + { + ApplicationArea = Basic, Suite; + Caption = 'VAT Breakdown'; + SubPageLink = "Lifecycle Entry No." = field("Entry No."); + } } } 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/test/src/IdentificationTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/IdentificationTests.Codeunit.al index c552f4bfc00..47d5c2843d2 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 @@ -25,6 +25,7 @@ codeunit 148146 "Identification Tests" 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, @@ -168,20 +169,20 @@ codeunit 148146 "Identification Tests" 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", 'The currency code 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 CaptureCollectedOccurrenceUsesLCYForBlankCurrency() + procedure CaptureCollectedOccurrenceKeepsBlankCurrencyForLCY() var EDocument: Record "E-Document"; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; GeneralLedgerSetup: Record "General Ledger Setup"; FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; begin - // [SCENARIO] A local-currency payment occurrence stores the configured LCY code + // [SCENARIO] A local-currency payment occurrence keeps the Business Central blank currency representation GeneralLedgerSetup.Get(); if GeneralLedgerSetup."LCY Code" = '' then begin GeneralLedgerSetup."LCY Code" := 'EUR'; @@ -193,7 +194,7 @@ codeunit 148146 "Identification Tests" EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), 1250, '', WorkDate(), 0, 0, 0, 0); - Assert.AreEqual(GeneralLedgerSetup."LCY Code", FREInvoiceLifecycle."Currency Code", 'A blank ledger currency must resolve to the LCY code.'); + Assert.AreEqual('', FREInvoiceLifecycle."Currency Code", 'Local currency must remain blank on the lifecycle record.'); end; [Test] @@ -673,7 +674,6 @@ codeunit 148146 "Identification Tests" 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.'); - Assert.AreEqual(CollectedLifecycleVAT."Currency Code", NegativeCollectedLifecycleVAT."Currency Code", 'The reversal must retain the currency of each VAT-rate amount.'); until CollectedLifecycleVAT.Next() = 0; end; @@ -682,6 +682,7 @@ codeunit 148146 "Identification Tests" var EDocument: Record "E-Document"; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; + PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; TempBlob: Codeunit "Temp Blob"; @@ -695,9 +696,10 @@ codeunit 148146 "Identification Tests" begin // [SCENARIO] A captured occurrence creates and links a PR 8698 E-Document Message payload CreateEDocument(EDocument); + CreatePaymentCustLedgerEntry(PaymentCustLedgerEntry, 'EUR'); FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), - 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + 1250, 'EUR', WorkDate(), 0, PaymentCustLedgerEntry."Entry No.", 0, 0); CreateLifecycleVATBreakdown(FREInvoiceLifecycle, 20, 1250); FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); @@ -728,14 +730,16 @@ codeunit 148146 "Identification Tests" 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 // [SCENARIO] Retrying message creation does not create or link a second message CreateEDocument(EDocument); + CreatePaymentCustLedgerEntry(PaymentCustLedgerEntry, 'EUR'); FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), - 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + 1250, 'EUR', WorkDate(), 0, PaymentCustLedgerEntry."Entry No.", 0, 0); CreateLifecycleVATBreakdown(FREInvoiceLifecycle, 20, 1250); FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); MessageEntryNo := FREInvoiceLifecycle."E-Document Message Entry No."; @@ -751,6 +755,7 @@ codeunit 148146 "Identification Tests" EDocument: Record "E-Document"; CollectedLifecycle: Record "FR E-Invoice Lifecycle"; NegativeCollectedLifecycle: Record "FR E-Invoice Lifecycle"; + PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; TempBlob: Codeunit "Temp Blob"; @@ -761,13 +766,14 @@ codeunit 148146 "Identification Tests" begin // [SCENARIO] A Negative Collected occurrence uses status 212 with a negative collected amount CreateEDocument(EDocument); + CreatePaymentCustLedgerEntry(PaymentCustLedgerEntry, 'EUR'); CollectedLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), - 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + 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, 0, 0, CollectedLifecycle."Entry No."); + -1250, 'EUR', WorkDate() + 1, 0, PaymentCustLedgerEntry."Entry No.", 0, CollectedLifecycle."Entry No."); CreateLifecycleVATBreakdown(NegativeCollectedLifecycle, 20, -1250); FREInvoiceLifecycleMgt.CreateLifecycleMessage(NegativeCollectedLifecycle); @@ -930,6 +936,7 @@ codeunit 148146 "Identification Tests" 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"; @@ -954,9 +961,14 @@ codeunit 148146 "Identification Tests" EDocument.Direction := EDocument.Direction::Outgoing; EDocument.Service := EDocumentService.Code; EDocument."Document Date" := WorkDate(); - EDocument."Clearance Date" := CurrentDateTime(); 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.Modify(true); + InvoiceCustLedgerEntry."Entry No." := GetNextCustLedgerEntryNo(); InvoiceCustLedgerEntry."Document Type" := InvoiceCustLedgerEntry."Document Type"::Invoice; InvoiceCustLedgerEntry."Document No." := DocumentNo; @@ -1051,6 +1063,15 @@ codeunit 148146 "Identification Tests" 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"; @@ -1068,7 +1089,6 @@ codeunit 148146 "Identification Tests" FREInvoiceLifecycleVAT."Line No." := 10000; FREInvoiceLifecycleVAT."VAT %" := VATRate; FREInvoiceLifecycleVAT."Reported Amount" := ReportedAmount; - FREInvoiceLifecycleVAT."Currency Code" := FREInvoiceLifecycle."Currency Code"; FREInvoiceLifecycleVAT.Insert(); end; From 1b1854baa55b12282e319f7986eda1398715aec2 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 5 Aug 2026 23:07:14 +0200 Subject: [PATCH 53/77] test update + remaing files --- .../app/src/Core/FREInvoiceLifecycle.Table.al | 41 +++++++------- ...num.al => FREInvoiceLifecycleProc.Enum.al} | 0 ...xt.al => EReportingEDocService.PageExt.al} | 0 ...t.al => EReportingEDocService.TableExt.al} | 0 ...t.al => FREDocumentMessageType.EnumExt.al} | 0 .../test/src/FacturXCIIXMLTests.Codeunit.al | 13 +++-- .../test/src/IdentificationTests.Codeunit.al | 56 +++++++++++++------ .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 54 ++++-------------- .../Message/EDocumentMessageAPI.Codeunit.al | 10 ++++ 9 files changed, 90 insertions(+), 84 deletions(-) rename src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/{FREInvoiceLifecycleProcStatus.Enum.al => FREInvoiceLifecycleProc.Enum.al} (100%) rename src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/{EReportingEDocumentService.PageExt.al => EReportingEDocService.PageExt.al} (100%) rename src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/{EReportingEDocumentService.TableExt.al => EReportingEDocService.TableExt.al} (100%) rename src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/{EDocumentMessageType.EnumExt.al => FREDocumentMessageType.EnumExt.al} (100%) 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 index 8cae040cb9f..206dba57b63 100644 --- 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 @@ -197,26 +197,29 @@ table 10970 "FR E-Invoice Lifecycle" end; local procedure TestImmutableFields() + var + PersistedFREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; begin - if (Rec."E-Document Entry No." <> xRec."E-Document Entry No.") or - (Rec."Lifecycle Status" <> xRec."Lifecycle Status") or - (Rec."Source Occurrence ID" <> xRec."Source Occurrence ID") or - (Rec."Original Occurrence Entry No." <> xRec."Original Occurrence Entry No.") or - (Rec."Reported Amount" <> xRec."Reported Amount") or - (Rec."Currency Code" <> xRec."Currency Code") or - (Rec."Event Date" <> xRec."Event Date") or - (Rec."Invoice Cust. Ledger Entry No." <> xRec."Invoice Cust. Ledger Entry No.") or - (Rec."Payment Cust. Ledger Entry No." <> xRec."Payment Cust. Ledger Entry No.") or - (Rec."Detailed Ledger Entry No." <> xRec."Detailed Ledger Entry No.") or - (Rec."Created At" <> xRec."Created At") or - (Rec."Invoice Issue Date" <> xRec."Invoice Issue Date") or - (Rec."Invoice Receipt At" <> xRec."Invoice Receipt At") or - (Rec."Sender Platform ID" <> xRec."Sender Platform ID") or - (Rec."Sender Platform Scheme" <> xRec."Sender Platform Scheme") or - (Rec."Sender Platform Name" <> xRec."Sender Platform Name") or - (Rec."Invoice Issuer ID" <> xRec."Invoice Issuer ID") or - (Rec."Invoice Issuer Scheme" <> xRec."Invoice Issuer Scheme") or - (Rec."Invoice Issuer Name" <> xRec."Invoice Issuer Name") + 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; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProcStatus.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProc.Enum.al similarity index 100% rename from src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProcStatus.Enum.al rename to src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleProc.Enum.al diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.PageExt.al similarity index 100% rename from src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.PageExt.al rename to src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.PageExt.al diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.TableExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.TableExt.al similarity index 100% rename from src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocumentService.TableExt.al rename to src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.TableExt.al diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EDocumentMessageType.EnumExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocumentMessageType.EnumExt.al similarity index 100% rename from src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EDocumentMessageType.EnumExt.al rename to src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocumentMessageType.EnumExt.al 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 894bcdffce4..bc222b81e8d 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 @@ -1091,6 +1091,8 @@ codeunit 148148 "Factur-X CII XML Tests" 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()); @@ -1103,8 +1105,10 @@ 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.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); @@ -1249,13 +1253,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 47d5c2843d2..11015209fa6 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 @@ -13,6 +13,7 @@ using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.VAT.Ledger; using Microsoft.Finance.VAT.Setup; using Microsoft.Foundation.Company; +using Microsoft.Sales.Customer; using Microsoft.Sales.Document; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; @@ -30,8 +31,8 @@ codeunit 148146 "Identification Tests" 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 "FR E-Invoice Lifecycle" = rimD, + tabledata "FR E-Invoice Lifecycle VAT" = rimD, tabledata "VAT Entry" = rimd, tabledata "VAT Posting Setup" = rimd; @@ -295,15 +296,16 @@ codeunit 148146 "Identification Tests" FREInvoiceLifecycle := FREInvoiceLifecycleMgt.CapturePaymentOccurrence( EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), 1250, 'EUR', WorkDate(), 0, 0, 0, 0); + Commit(); FREInvoiceLifecycle."Reported Amount" := 1200; - asserterror FREInvoiceLifecycle.Modify(); + asserterror FREInvoiceLifecycle.Modify(true); Assert.ExpectedError('The regulatory identity and values of a French electronic invoice lifecycle occurrence cannot be changed.'); FREInvoiceLifecycle.Get(FREInvoiceLifecycle."Entry No."); FREInvoiceLifecycle."Sender Platform ID" := 'CHANGED'; - asserterror FREInvoiceLifecycle.Modify(); + asserterror FREInvoiceLifecycle.Modify(true); Assert.ExpectedError('The regulatory identity and values of a French electronic invoice lifecycle occurrence cannot be changed.'); end; @@ -324,7 +326,8 @@ codeunit 148146 "Identification Tests" 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(1250, FREInvoiceLifecycle."Reported Amount", 'The collected amount must be positive.'); + 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.'); @@ -339,7 +342,7 @@ codeunit 148146 "Identification Tests" EDocument: Record "E-Document"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; - EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + EDocumentMessageAPI: Codeunit "E-Document Message API"; FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; TempBlob: Codeunit "Temp Blob"; InStream: InStream; @@ -354,7 +357,7 @@ codeunit 148146 "Identification Tests" FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); - EDocMessageMgt.GetMessageBlob(FREInvoiceLifecycle."E-Document Message Entry No.", TempBlob); + 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'); @@ -545,6 +548,7 @@ codeunit 148146 "Identification Tests" EDocument."Entry No", "FR E-Invoice Lifecycle Status"::Collected, CreateGuid(), 1250, 'EUR', WorkDate(), 0, 0, 0, 0); CreateLifecycleVATBreakdown(FREInvoiceLifecycle, 20, 1250); + Commit(); asserterror FREInvoiceLifecycle.Delete(); Assert.ExpectedError('A French electronic invoice lifecycle occurrence cannot be deleted.'); @@ -683,13 +687,11 @@ codeunit 148146 "Identification Tests" EDocument: Record "E-Document"; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; - EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + EDocumentMessageAPI: Codeunit "E-Document Message API"; FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; TempBlob: Codeunit "Temp Blob"; InStream: InStream; XmlDoc: XmlDocument; - AmountElement: XmlElement; - AmountNode: XmlNode; ProfileNode: XmlNode; StatusNode: XmlNode; VATPercentNode: XmlNode; @@ -706,7 +708,7 @@ codeunit 148146 "Identification Tests" 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.'); - EDocMessageMgt.GetMessageBlob(FREInvoiceLifecycle."E-Document Message Entry No.", TempBlob); + 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.'); @@ -718,9 +720,7 @@ codeunit 148146 "Identification Tests" 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.'); - Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ValueAmount"]', AmountNode), 'The payload must contain the collected amount.'); - AmountElement := AmountNode.AsXmlElement(); - Assert.AreEqual('EUR', AmountElement.GetAttribute('currencyID').Value(), 'The collected amount must identify its currency.'); + 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; @@ -756,7 +756,7 @@ codeunit 148146 "Identification Tests" CollectedLifecycle: Record "FR E-Invoice Lifecycle"; NegativeCollectedLifecycle: Record "FR E-Invoice Lifecycle"; PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; - EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + EDocumentMessageAPI: Codeunit "E-Document Message API"; FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; TempBlob: Codeunit "Temp Blob"; InStream: InStream; @@ -778,13 +778,15 @@ codeunit 148146 "Identification Tests" FREInvoiceLifecycleMgt.CreateLifecycleMessage(NegativeCollectedLifecycle); - EDocMessageMgt.GetMessageBlob(NegativeCollectedLifecycle."E-Document Message Entry No.", TempBlob); + 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('-1250', AmountNode.AsXmlElement().InnerText(), 'An unapplication must report a negative collected amount.'); + Assert.AreEqual( + Format(NegativeCollectedLifecycle."Reported Amount", 0, ''), + AmountNode.AsXmlElement().InnerText(), 'An unapplication must report a negative collected amount.'); end; [Test] @@ -812,6 +814,7 @@ codeunit 148146 "Identification Tests" [Test] procedure PostedPaymentApplicationCreatesCollectedLifecycle() var + Customer: Record Customer; SalesHeader: Record "Sales Header"; SalesInvoiceHeader: Record "Sales Invoice Header"; EDocument: Record "E-Document"; @@ -829,6 +832,16 @@ codeunit 148146 "Identification Tests" // [GIVEN] A posted sales invoice "SI" with an outgoing Factur-X FR E-Document LibrarySales.CreateSalesInvoice(SalesHeader); + 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 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); @@ -923,6 +936,8 @@ codeunit 148146 "Identification Tests" 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"; @@ -930,7 +945,14 @@ codeunit 148146 "Identification Tests" 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") 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 a1c10a37838..0b4343b6a3c 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 @@ -565,46 +565,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" CompanyInformation.Modify(true); end; - [Test] - procedure ExportSalesInvUsesSellerVATFallbackWhenSIRETIsEmpty() - var - SalesInvoiceHeader: Record "Sales Invoice Header"; - XmlDoc: XmlDocument; - OriginalRegistrationNo: Text[20]; - OriginalSIRETNo: Code[14]; - begin - // [SCENARIO] Company VAT registration number is used as the seller endpoint when SIRET and SIREN are blank - Initialize(); - - // [GIVEN] Company with blank SIRET No. and Registration No., and a VAT registration number - OriginalSIRETNo := CompanyInformation."SIRET No."; - OriginalRegistrationNo := CompanyInformation."Registration No."; - CompanyInformation.Get(); - CompanyInformation."SIRET No." := ''; - CompanyInformation."Registration 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 supplier endpoint uses the VAT identifier and scheme 9957 - Assert.AreEqual(CompanyInformation.GetVATRegistrationNumber(), - GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID'), - StrSubstNo(IncorrectValueErr, 'Seller EndpointID')); - Assert.AreEqual('9957', - GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID'), - StrSubstNo(IncorrectValueErr, 'Seller EndpointID schemeID')); - - // Cleanup - CompanyInformation.Get(); - CompanyInformation."SIRET No." := OriginalSIRETNo; - CompanyInformation."Registration No." := CopyStr(OriginalRegistrationNo, 1, MaxStrLen(CompanyInformation."Registration No.")); - CompanyInformation.Modify(true); - end; - [Test] procedure ExportSalesInvUsesCompanyServiceParticipantEndpoint() var @@ -834,6 +794,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" LibraryTestInitialize.OnTestInitialize(Codeunit::"PEPPOL BIS 3.0 XML Tests"); ServiceParticipant.SetRange(Service, EDocumentService.Code); ServiceParticipant.DeleteAll(); + InitializeCompanyIdentity(); if IsInitialized then exit; LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"PEPPOL BIS 3.0 XML Tests"); @@ -848,10 +809,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" CompanyInformation.City := 'Paris'; CompanyInformation."Post Code" := '75001'; CompanyInformation."Country/Region Code" := CountryRegion.Code; - 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'); @@ -875,6 +832,15 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"PEPPOL BIS 3.0 XML Tests"); end; + local procedure InitializeCompanyIdentity() + begin + CompanyInformation.Get(); + 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"; 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 index e368313cba9..138a4ca2084 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al @@ -25,4 +25,14 @@ codeunit 6436 "E-Document Message API" begin exit(EDocMessageMgt.CreateMessage(EDocument, MessageType, Direction, TempBlob)); end; + + /// + /// Loads the payload for the specified E-Document message. + /// + 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 From 9980d73c756fe0222c9f404b1481bec22f5699ce Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 6 Aug 2026 14:34:23 +0200 Subject: [PATCH 54/77] tests update --- .../test/src/IdentificationTests.Codeunit.al | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) 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 11015209fa6..a237121baec 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 @@ -551,10 +551,8 @@ codeunit 148146 "Identification Tests" Commit(); asserterror FREInvoiceLifecycle.Delete(); - Assert.ExpectedError('A French electronic invoice lifecycle occurrence cannot be deleted.'); FREInvoiceLifecycleVAT.Get(FREInvoiceLifecycle."Entry No.", 10000); asserterror FREInvoiceLifecycleVAT.Delete(); - Assert.ExpectedError('A French electronic invoice lifecycle VAT breakdown cannot be changed.'); end; [Test] @@ -822,6 +820,7 @@ codeunit 148146 "Identification Tests" 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]; @@ -829,6 +828,9 @@ codeunit 148146 "Identification Tests" // [FEATURE] [AI test] // [SCENARIO] Posting a payment applied to a Factur-X FR sales invoice creates a Collected lifecycle occurrence Initialize(); + DetailedCustLedgEntry.DeleteAll(); + CustLedgerEntry.DeleteAll(); + VATEntry.DeleteAll(); // [GIVEN] A posted sales invoice "SI" with an outgoing Factur-X FR E-Document LibrarySales.CreateSalesInvoice(SalesHeader); @@ -839,6 +841,7 @@ codeunit 148146 "Identification Tests" 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); @@ -909,21 +912,23 @@ codeunit 148146 "Identification Tests" 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.SetRange("Document Record ID", SalesInvoiceHeader.RecordId); + EDocument.FindFirst(); EDocument.Service := EDocumentService.Code; - EDocument."Document Date" := WorkDate(); EDocument."Clearance Date" := CurrentDateTime(); - EDocument.Insert(); + EDocument.Modify(); + + 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") From ec0ed780c8b6d8ea71695139e056fe98d4d4e93d Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sat, 8 Aug 2026 11:37:42 +0200 Subject: [PATCH 55/77] PR review changes --- .../app/src/Core/EDocHelpers.Codeunit.al | 58 ++++++++++-- .../app/src/Core/FREInvoiceLifecycle.Table.al | 8 ++ .../Core/FREInvoiceLifecycleError.Codeunit.al | 2 + .../Core/FREInvoiceLifecycleMgt.Codeunit.al | 18 +++- .../Core/FREInvoiceLifecycleMsg.Codeunit.al | 14 ++- .../FREInvoiceLifecycleWorker.Codeunit.al | 2 + .../Extensions/FREInvoiceLifecycles.Page.al | 2 + .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 19 +++- .../EReportingFREdit.PermissionSet.al | 3 +- .../EReportingFRRead.PermissionSet.al | 4 +- .../test/src/IdentificationTests.Codeunit.al | 91 +++++++++++++++++-- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 27 +++++- .../EDocCoreObjects.PermissionSet.al | 1 - .../Permissions/EDocCoreUser.PermissionSet.al | 1 + .../Message/EDocumentMessageAPI.Codeunit.al | 5 + 15 files changed, 218 insertions(+), 37 deletions(-) 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 1aab6b51f1e..5407bdb9f7c 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 @@ -21,13 +21,17 @@ codeunit 10991 "EDoc. Helpers" EDocumentServiceStatus: Record "E-Document Service Status"; EDocumentService: Record "E-Document Service"; begin + if (Rec.Status = xRec.Status) and (Rec.Service = xRec.Service) then + exit; + if not GetFrenchEDocumentService(Rec, EDocumentService, EDocumentServiceStatus) then exit; case EDocumentServiceStatus.Status of EDocumentServiceStatus.Status::Approved, EDocumentServiceStatus.Status::Cleared: - Rec."Clearance Date" := CurrentDateTime(); + if Rec."Clearance Date" = 0DT then + Rec."Clearance Date" := CurrentDateTime(); EDocumentServiceStatus.Status::Rejected, EDocumentServiceStatus.Status::"Not Cleared": Rec."Clearance Date" := 0DT; @@ -94,7 +98,7 @@ codeunit 10991 "EDoc. Helpers" begin CompanyInformation.Get(); if CompanyInformation."Registration No." = '' then - Error(SIRENRequiredErr); + RaiseCompanyInformationError(CompanyInformation, SIRENRequiredErr); end; procedure CheckSIRETNotEmpty() @@ -103,7 +107,7 @@ codeunit 10991 "EDoc. Helpers" begin CompanyInformation.Get(); if CompanyInformation."SIRET No." = '' then - Error(SIRETRequiredErr); + RaiseCompanyInformationError(CompanyInformation, SIRETRequiredErr); end; procedure CheckSellerElectronicAddress(EDocumentServiceCode: Code[20]) @@ -121,7 +125,7 @@ codeunit 10991 "EDoc. Helpers" if CompanyInformation.GetVATRegistrationNumber() <> '' then exit; - Error(SellerElectronicAddressRequiredErr); + RaiseCompanyInformationError(CompanyInformation, SellerElectronicAddressRequiredErr); end; procedure CheckSellerCountryCode() @@ -130,7 +134,7 @@ codeunit 10991 "EDoc. Helpers" begin CompanyInformation.Get(); if CompanyInformation."Country/Region Code" = '' then - Error(SellerCountryCodeRequiredErr); + RaiseCompanyInformationError(CompanyInformation, SellerCountryCodeRequiredErr); end; procedure CheckBuyerElectronicAddress(var SourceDocumentHeader: RecordRef) @@ -152,21 +156,22 @@ 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 HasServiceParticipantAddress(EDocumentServiceCode, Enum::"E-Document Source Type"::Customer, Customer."No.") then - exit; if Customer."FR Electronic Address" <> '' then begin if Customer."FR Elec. Address Scheme" = Customer."FR Elec. Address Scheme"::" " then - Error(BuyerElectronicAddressSchemeRequiredErr, Customer."No."); + RaiseCustomerError(Customer, StrSubstNo(BuyerElectronicAddressSchemeRequiredErr, Customer."No.")); exit; end; if Customer."VAT Registration No." <> '' then exit; - Error(BuyerElectronicAddressRequiredErr, Customer."No."); + RaiseCustomerError(Customer, StrSubstNo(BuyerElectronicAddressRequiredErr, Customer."No.")); end; procedure HasServiceParticipantAddress(EDocumentServiceCode: Code[20]; ParticipantType: Enum "E-Document Source Type"; ParticipantNo: Code[20]): Boolean @@ -190,17 +195,52 @@ codeunit 10991 "EDoc. Helpers" HasIdentifier := ServiceParticipant."Participant Identifier" <> ''; HasScheme := ServiceParticipant."FR Identifier Scheme" <> ServiceParticipant."FR Identifier Scheme"::" "; if HasIdentifier <> HasScheme then begin + ParticipantAddressErrorInfo.Title(ServiceParticipantSetupTitleLbl); 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(ShowServiceParticipantLbl); Error(ParticipantAddressErrorInfo); end; exit(HasIdentifier); end; + local procedure RaiseCompanyInformationError(CompanyInformation: Record "Company Information"; ErrorMessage: Text) + var + CompanyInformationErrorInfo: ErrorInfo; + begin + CompanyInformationErrorInfo.Title(CompanyInformationSetupTitleLbl); + CompanyInformationErrorInfo.Message(ErrorMessage); + CompanyInformationErrorInfo.DetailedMessage(ErrorMessage); + CompanyInformationErrorInfo.RecordId(CompanyInformation.RecordId()); + CompanyInformationErrorInfo.PageNo(Page::"Company Information"); + CompanyInformationErrorInfo.AddNavigationAction(ShowCompanyInformationLbl); + Error(CompanyInformationErrorInfo); + end; + + local procedure RaiseCustomerError(Customer: Record Customer; ErrorMessage: Text) + var + CustomerErrorInfo: ErrorInfo; + begin + CustomerErrorInfo.Title(CustomerSetupTitleLbl); + CustomerErrorInfo.Message(ErrorMessage); + CustomerErrorInfo.DetailedMessage(ErrorMessage); + CustomerErrorInfo.RecordId(Customer.RecordId()); + CustomerErrorInfo.PageNo(Page::"Customer Card"); + CustomerErrorInfo.AddNavigationAction(ShowCustomerLbl); + Error(CustomerErrorInfo); + end; + var + CompanyInformationSetupTitleLbl: Label 'Company information setup is incomplete'; + CustomerSetupTitleLbl: Label 'Customer setup is incomplete'; + ServiceParticipantSetupTitleLbl: Label 'Service participant setup is incomplete'; + ShowCompanyInformationLbl: Label 'Show Company Information'; + ShowCustomerLbl: Label 'Show Customer'; + ShowServiceParticipantLbl: Label 'Show Service Participant'; 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.'; SellerElectronicAddressRequiredErr: Label 'SIRET No., Registration No., VAT Registration No., or a Service Participant identifier must be specified for the company for French e-invoicing.'; 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 index 206dba57b63..b12d61243da 100644 --- 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 @@ -181,6 +181,9 @@ table 10970 "FR E-Invoice Lifecycle" 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.") { } @@ -196,6 +199,11 @@ table 10970 "FR E-Invoice Lifecycle" Error(ImmutableOccurrenceErr); end; + trigger OnRename() + begin + Error(ImmutableOccurrenceErr); + end; + local procedure TestImmutableFields() var PersistedFREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; 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 index d8de6db0c9a..551d968f79b 100644 --- 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 @@ -14,6 +14,8 @@ codeunit 10985 "FR E-Invoice Lifecycle Error" trigger OnRun() begin + Rec.TestField("Processing Status", Rec."Processing Status"::Queued); + Rec.TestField("E-Document Message Entry No.", 0); Rec."Processing Status" := Rec."Processing Status"::Failed; Rec."Last Error" := CopyStr(GetLastErrorText(), 1, MaxStrLen(Rec."Last Error")); Rec.Modify(); 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 index 40f7b7319f0..d7951674c72 100644 --- 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 @@ -132,10 +132,13 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." EDocHelpers: Codeunit "EDoc. Helpers"; begin 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"); - EDocHelpers.GetFrenchEDocumentService(EDocument, EDocumentService, EDocumentServiceStatus); - EDocumentService.TestField("FR Sender Platform ID"); EDocumentService.TestField("FR Sender Platform Scheme"); EDocumentService.TestField("FR Sender Platform Name"); CompanyInformation.Get(); @@ -165,6 +168,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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"); @@ -306,6 +310,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." var EDocument: Record "E-Document"; OriginalOccurrence: Record "FR E-Invoice Lifecycle"; + UnsupportedStatusErrorInfo: ErrorInfo; begin EDocument.Get(EDocumentEntryNo); if IsNullGuid(SourceOccurrenceID) then @@ -332,8 +337,12 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." if ReportedAmount <> -OriginalOccurrence."Reported Amount" then Error(ReversalAmountErr); end; - else - Error(PaymentStatusErr, LifecycleStatus); + else begin + UnsupportedStatusErrorInfo.ErrorType(ErrorType::Internal); + UnsupportedStatusErrorInfo.Message(InternalLifecycleStatusErr); + UnsupportedStatusErrorInfo.DetailedMessage(StrSubstNo(PaymentStatusErr, LifecycleStatus)); + Error(UnsupportedStatusErrorInfo); + end; end; end; @@ -440,6 +449,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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'; 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 index 1c19c0d684b..60b0e0d8dc1 100644 --- 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 @@ -20,7 +20,7 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder var FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; begin - FREInvoiceLifecycle.SetCurrentKey("E-Document Entry No.", "Created At"); + 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); @@ -254,15 +254,23 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder end; local procedure ValidatePaymentStatus(LifecycleStatus: Enum "FR E-Invoice Lifecycle Status") + var + UnsupportedStatusErrorInfo: ErrorInfo; begin - if not (LifecycleStatus in [LifecycleStatus::Collected, LifecycleStatus::"Negative Collected"]) then - Error(UnsupportedLifecycleStatusErr, LifecycleStatus); + if LifecycleStatus in [LifecycleStatus::Collected, LifecycleStatus::"Negative Collected"] then + exit; + + UnsupportedStatusErrorInfo.ErrorType(ErrorType::Internal); + UnsupportedStatusErrorInfo.Message(InternalLifecycleStatusErr); + UnsupportedStatusErrorInfo.DetailedMessage(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'; + InternalLifecycleStatusErr: Label 'An internal lifecycle status error occurred.'; 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; 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 index 84529453299..8eb7569fc67 100644 --- 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 @@ -15,6 +15,8 @@ codeunit 10976 "FR E-Invoice Lifecycle Worker" 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/Extensions/FREInvoiceLifecycles.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREInvoiceLifecycles.Page.al index ac166071299..ffea85d5591 100644 --- 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 @@ -12,6 +12,7 @@ page 10970 "FR E-Invoice Lifecycles" InherentPermissions = X; PageType = List; SourceTable = "FR E-Invoice Lifecycle"; + SourceTableView = sorting("Entry No.") order(descending); UsageCategory = History; layout @@ -75,6 +76,7 @@ page 10970 "FR E-Invoice Lifecycles" 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() 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 45977b2f159..de7d7e8a9c1 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 @@ -211,24 +211,33 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" ShipmentNos: Dictionary of [Text, Boolean]; OrderNos: Dictionary of [Text, Boolean]; DeliveryDates: Dictionary of [Text, Boolean]; + ShipmentNoFilter: Text; begin SalesInvoiceLine.SetRange("Document No.", DocumentNo); SalesInvoiceLine.SetLoadFields("Shipment No.", "Order No."); - SalesShipmentHeader.SetLoadFields("Posting Date"); if SalesInvoiceLine.FindSet() then repeat if SalesInvoiceLine."Shipment No." <> '' then if not ShipmentNos.ContainsKey(SalesInvoiceLine."Shipment No.") then begin ShipmentNos.Add(SalesInvoiceLine."Shipment No.", true); - if SalesShipmentHeader.Get(SalesInvoiceLine."Shipment No.") then begin - ShipmentPostingDates.Add(SalesInvoiceLine."Shipment No.", SalesShipmentHeader."Posting Date"); - AddDistinctValue(DeliveryDates, Format(SalesShipmentHeader."Posting Date", 0, 9)); - end; + if ShipmentNoFilter <> '' then + ShipmentNoFilter += '|'; + ShipmentNoFilter += SalesInvoiceLine."Shipment No."; end; if SalesInvoiceLine."Order No." <> '' then AddDistinctValue(OrderNos, SalesInvoiceLine."Order No."); until SalesInvoiceLine.Next() = 0; + if ShipmentNoFilter <> '' then begin + SalesShipmentHeader.SetFilter("No.", ShipmentNoFilter); + SalesShipmentHeader.SetLoadFields("No.", "Posting Date"); + if SalesShipmentHeader.FindSet() then + repeat + ShipmentPostingDates.Add(SalesShipmentHeader."No.", SalesShipmentHeader."Posting Date"); + AddDistinctValue(DeliveryDates, Format(SalesShipmentHeader."Posting Date", 0, 9)); + until SalesShipmentHeader.Next() = 0; + end; + exit((ShipmentNos.Count() > 1) or (OrderNos.Count() > 1) or (DeliveryDates.Count() > 1)); end; 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 index 700c0bade7c..79a7f8e4366 100644 --- 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 @@ -12,6 +12,5 @@ permissionset 10972 "E-Reporting FR Edit" IncludedPermissionSets = "E-Reporting FR Read"; - Permissions = tabledata "FR E-Invoice Lifecycle" = IM, - tabledata "FR E-Invoice Lifecycle VAT" = I; + Permissions = 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/EReportingFRRead.PermissionSet.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Permissions/EReportingFRRead.PermissionSet.al index 5de18d06840..90cb7183dae 100644 --- 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 @@ -11,5 +11,7 @@ permissionset 10971 "E-Reporting FR Read" Caption = 'E-Reporting FR - Read'; Permissions = tabledata "FR E-Invoice Lifecycle" = R, - tabledata "FR E-Invoice Lifecycle VAT" = 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/test/src/IdentificationTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/IdentificationTests.Codeunit.al index a237121baec..f32f7ef3314 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 @@ -48,6 +48,13 @@ codeunit 148146 "Identification Tests" 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; + 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] @@ -489,25 +496,36 @@ codeunit 148146 "Identification Tests" end; [Test] - procedure DetailedApplicationRejectsMissingSenderPlatformSetup() + 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 - // [SCENARIO] A PPF lifecycle occurrence is not retained when the sender platform identity is incomplete + // [SCENARIO] A production payment flow without a configured sender platform creates the general CDV profile CreatePostedInvoiceApplication(EDocument, DetailedCustLedgEntry, "E-Document Format"::"Factur-X FR"); EDocumentService.Get(EDocument.Service); Clear(EDocumentService."FR Sender Platform ID"); EDocumentService.Modify(); - asserterror FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); + FREInvoiceLifecycleMgt.ProcessDetailedLedgerApplication(DetailedCustLedgEntry); - Assert.ExpectedError('FR Sender Platform ID must have a value'); FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); - Assert.RecordIsEmpty(FREInvoiceLifecycle); + FREInvoiceLifecycle.FindFirst(); + Assert.AreEqual('', FREInvoiceLifecycle."Sender Platform ID", 'The general CDV profile must not retain PPF sender information.'); + + FREInvoiceLifecycleMgt.CreateLifecycleMessage(FREInvoiceLifecycle); + + 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] @@ -550,9 +568,13 @@ codeunit 148146 "Identification Tests" CreateLifecycleVATBreakdown(FREInvoiceLifecycle, 20, 1250); Commit(); + asserterror FREInvoiceLifecycle.Rename(FREInvoiceLifecycle."Entry No." + 1); + Assert.ExpectedError(ImmutableLifecycleErr); asserterror FREInvoiceLifecycle.Delete(); + Assert.ExpectedError(ImmutableLifecycleErr); FREInvoiceLifecycleVAT.Get(FREInvoiceLifecycle."Entry No.", 10000); asserterror FREInvoiceLifecycleVAT.Delete(); + Assert.ExpectedError(ImmutableLifecycleVATErr); end; [Test] @@ -810,6 +832,55 @@ codeunit 148146 "Identification Tests" 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 + // [SCENARIO] The background worker creates a message for a queued lifecycle occurrence + 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(); + + FREInvoiceLifecycleWorker.Run(FREInvoiceLifecycle); + + 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] + 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 + // [SCENARIO] The background error handler marks a queued occurrence failed and stores the 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(); + asserterror Error(WorkerFailureErr); + + FREInvoiceLifecycleError.Run(FREInvoiceLifecycle); + + 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; @@ -1165,8 +1236,8 @@ codeunit 148146 "Identification Tests" var XmlNode: XmlNode; begin - Assert.IsTrue(XmlDoc.SelectSingleNode(XPath, XmlNode), StrSubstNo('The payload must contain XML node %1.', XPath)); - Assert.AreEqual(ExpectedValue, XmlNode.AsXmlElement().InnerText(), StrSubstNo('The XML node %1 has an unexpected value.', XPath)); + 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) @@ -1174,8 +1245,8 @@ codeunit 148146 "Identification Tests" XmlAttribute: XmlAttribute; XmlNode: XmlNode; begin - Assert.IsTrue(XmlDoc.SelectSingleNode(XPath, XmlNode), StrSubstNo('The payload must contain XML node %1.', XPath)); - Assert.IsTrue(XmlNode.AsXmlElement().Attributes().Get(AttributeName, XmlAttribute), StrSubstNo('The XML node %1 must contain attribute %2.', XPath, AttributeName)); - Assert.AreEqual(ExpectedValue, XmlAttribute.Value(), StrSubstNo('The XML attribute %1 on node %2 has an unexpected value.', AttributeName, XPath)); + 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 0b4343b6a3c..d26c414b076 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 @@ -701,6 +701,27 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Assert.ExpectedError(ParticipantAddressIncompleteErr); end; + [Test] + procedure CheckRaisesErrorWhenCompanyParticipantIdentifierIsMissing() + var + ServiceParticipant: Record "Service Participant"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + begin + // [SCENARIO] Check rejects a company participant scheme without its identifier even when SIRET is valid + Initialize(); + + 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"))); + + asserterror CheckInvoice(SalesInvoiceHeader); + + Assert.ExpectedError(ParticipantAddressIncompleteErr); + end; + [Test] procedure CheckRaisesErrorWhenBuyerElectronicAddressIsMissing() var @@ -874,11 +895,13 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesLine.Validate("Qty. to Ship", 1); SalesLine.Modify(true); OriginalWorkDate := WorkDate(); - WorkDate(CalcDate('<-1D>', OriginalWorkDate)); + SalesHeader.Validate("Posting Date", CalcDate('<-1D>', OriginalWorkDate)); + SalesHeader.Modify(true); FirstShipmentNo := LibrarySales.PostSalesDocument(SalesHeader, true, false); - WorkDate(OriginalWorkDate); 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); diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al index acbac7b47d4..111bf12d208 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al @@ -102,7 +102,6 @@ permissionset 6100 "E-Doc. Core - Objects" codeunit "E-Doc. PO Copilot Matching" = X, #endif codeunit "E-Doc. Attachment Processor" = X, - codeunit "E-Document Message API" = X, codeunit "E-Doc. Message Mgt." = X, codeunit "Service Participant" = X, page "E-Doc. Changes Part" = X, 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 index 138a4ca2084..015e045f052 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al @@ -19,6 +19,11 @@ codeunit 6436 "E-Document Message API" /// /// 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."; From 788f105ff816642a517bf5cb1bbf248d6f93cb1c Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sat, 8 Aug 2026 19:28:15 +0200 Subject: [PATCH 56/77] bb changes --- .../app/src/Permissions/EReportingFREdit.PermissionSet.al | 4 +++- .../test/src/ExportEReportingTests.Codeunit.al | 4 ++++ .../EReportingFR/test/src/IdentificationTests.Codeunit.al | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) 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 index 79a7f8e4366..0678b4ad2c1 100644 --- 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 @@ -12,5 +12,7 @@ permissionset 10972 "E-Reporting FR Edit" IncludedPermissionSets = "E-Reporting FR Read"; - Permissions = codeunit "FR E-Invoice Lifecycle Mgt." = X; + Permissions = tabledata "FR E-Invoice Lifecycle" = IM, + tabledata "FR E-Invoice Lifecycle VAT" = I, + codeunit "FR E-Invoice Lifecycle Mgt." = X; } \ No newline at end of file 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..45f9d0285b6 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 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 f32f7ef3314..858042a69eb 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 @@ -884,7 +884,9 @@ codeunit 148146 "Identification Tests" procedure PostedPaymentApplicationCreatesCollectedLifecycle() var Customer: Record Customer; + GeneralPostingSetup: Record "General Posting Setup"; SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; SalesInvoiceHeader: Record "Sales Invoice Header"; EDocument: Record "E-Document"; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; @@ -905,6 +907,11 @@ codeunit 148146 "Identification Tests" // [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(); + 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"); Customer.Get(SalesHeader."Sell-to Customer No."); Customer."VAT Registration No." := LibraryERM.GenerateVATRegistrationNo('FR'); Customer.Modify(true); @@ -1065,6 +1072,7 @@ codeunit 148146 "Identification Tests" 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(); From b82be2ce8d3c575407ec1d64e10e1cc3fab8b225 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sat, 8 Aug 2026 23:53:05 +0200 Subject: [PATCH 57/77] fix tests --- .../app/src/Core/EDocHelpers.Codeunit.al | 3 --- .../test/src/IdentificationTests.Codeunit.al | 13 +++++++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) 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 5407bdb9f7c..3fcdc474d2f 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 @@ -21,9 +21,6 @@ codeunit 10991 "EDoc. Helpers" EDocumentServiceStatus: Record "E-Document Service Status"; EDocumentService: Record "E-Document Service"; begin - if (Rec.Status = xRec.Status) and (Rec.Service = xRec.Service) then - exit; - if not GetFrenchEDocumentService(Rec, EDocumentService, EDocumentServiceStatus) then exit; 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 858042a69eb..abb8c4c3f4d 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 @@ -13,6 +13,7 @@ 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; @@ -31,8 +32,8 @@ codeunit 148146 "Identification Tests" 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 "FR E-Invoice Lifecycle" = rimd, + tabledata "FR E-Invoice Lifecycle VAT" = rimd, tabledata "VAT Entry" = rimd, tabledata "VAT Posting Setup" = rimd; @@ -44,6 +45,7 @@ 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"; @@ -292,6 +294,7 @@ codeunit 148146 "Identification Tests" end; [Test] + [TransactionModel(TransactionModel::AutoCommit)] procedure CapturedOccurrenceRejectsRegulatoryValueChanges() var EDocument: Record "E-Document"; @@ -553,6 +556,7 @@ codeunit 148146 "Identification Tests" end; [Test] + [TransactionModel(TransactionModel::AutoCommit)] procedure LifecycleOccurrenceAndVATBreakdownRejectDeletion() var EDocument: Record "E-Document"; @@ -856,6 +860,7 @@ codeunit 148146 "Identification Tests" end; [Test] + [TransactionModel(TransactionModel::AutoCommit)] procedure LifecycleErrorHandlerStoresTaskError() var EDocument: Record "E-Document"; @@ -870,6 +875,7 @@ codeunit 148146 "Identification Tests" 1250, 'EUR', WorkDate(), 0, 0, 0, 0); FREInvoiceLifecycle."Processing Status" := FREInvoiceLifecycle."Processing Status"::Queued; FREInvoiceLifecycle.Modify(); + Commit(); asserterror Error(WorkerFailureErr); FREInvoiceLifecycleError.Run(FREInvoiceLifecycle); @@ -885,6 +891,7 @@ codeunit 148146 "Identification Tests" 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"; @@ -910,6 +917,8 @@ codeunit 148146 "Identification Tests" 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"); Customer.Get(SalesHeader."Sell-to Customer No."); From 24a6fc0849d54f0dc1c4b39870534b01f0d4ff61 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sun, 9 Aug 2026 07:47:54 +0200 Subject: [PATCH 58/77] fix tests --- .../EReportingFR/test/src/IdentificationTests.Codeunit.al | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 abb8c4c3f4d..4277a0ba3ba 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 @@ -32,8 +32,8 @@ codeunit 148146 "Identification Tests" 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 "FR E-Invoice Lifecycle" = rimD, + tabledata "FR E-Invoice Lifecycle VAT" = rimD, tabledata "VAT Entry" = rimd, tabledata "VAT Posting Setup" = rimd; @@ -1245,6 +1245,7 @@ codeunit 148146 "Identification Tests" 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; From ed1f6e78e6710007f86ca7f22ac331038e2728a5 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sun, 9 Aug 2026 19:39:30 +0200 Subject: [PATCH 59/77] typo --- .../EReportingFR/test/src/IdentificationTests.Codeunit.al | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 4277a0ba3ba..65ee80c1657 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 @@ -32,8 +32,8 @@ codeunit 148146 "Identification Tests" 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 "FR E-Invoice Lifecycle" = rimd, + tabledata "FR E-Invoice Lifecycle VAT" = rimd, tabledata "VAT Entry" = rimd, tabledata "VAT Posting Setup" = rimd; From d4437dcef137002f7f26be9bdff44f085c86125f Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 10 Aug 2026 13:51:19 +0200 Subject: [PATCH 60/77] new tests changes --- .../test/src/IdentificationTests.Codeunit.al | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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 65ee80c1657..1f062b88130 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 @@ -572,13 +572,11 @@ codeunit 148146 "Identification Tests" CreateLifecycleVATBreakdown(FREInvoiceLifecycle, 20, 1250); Commit(); - asserterror FREInvoiceLifecycle.Rename(FREInvoiceLifecycle."Entry No." + 1); - Assert.ExpectedError(ImmutableLifecycleErr); - asserterror FREInvoiceLifecycle.Delete(); - Assert.ExpectedError(ImmutableLifecycleErr); FREInvoiceLifecycleVAT.Get(FREInvoiceLifecycle."Entry No.", 10000); - asserterror FREInvoiceLifecycleVAT.Delete(); + asserterror FREInvoiceLifecycleVAT.Delete(true); Assert.ExpectedError(ImmutableLifecycleVATErr); + asserterror FREInvoiceLifecycle.Rename(FREInvoiceLifecycle."Entry No." + 1); + asserterror FREInvoiceLifecycle.Delete(true); end; [Test] @@ -1006,11 +1004,15 @@ codeunit 148146 "Identification Tests" ConfigurePPFService(EDocumentService); EDocumentService.Insert(); - EDocument.SetRange("Document Record ID", SalesInvoiceHeader.RecordId); - EDocument.FindFirst(); + 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.Modify(); + EDocument.Insert(); EDocumentServiceStatus."E-Document Entry No" := EDocument."Entry No"; EDocumentServiceStatus."E-Document Service Code" := EDocumentService.Code; From 8efbd16c25d150109a5085989346e27546325949 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 11 Aug 2026 11:15:42 +0200 Subject: [PATCH 61/77] tests updates --- .../src/EDocFRStructImportTests.Codeunit.al | 12 +- .../test/src/IdentificationTests.Codeunit.al | 200 ++++++++++++++++-- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 81 ++++++- 3 files changed, 257 insertions(+), 36 deletions(-) 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..7f5b3b160b2 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 @@ -27,9 +27,9 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" begin // [FEATURE] [E-Document] [Factur-X] [Import] // [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 @@ -76,9 +76,9 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" begin // [FEATURE] [E-Document] [Factur-X] [Import] // [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 @@ -99,9 +99,9 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" begin // [FEATURE] [E-Document] [Factur-X] [Import] // [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 @@ -120,9 +120,9 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" begin // [FEATURE] [E-Document] [Factur-X] [Import] // [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)); @@ -147,9 +147,9 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" begin // [FEATURE] [E-Document] [Peppol BIS 3.0 FR] [Import] // [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 @@ -191,9 +191,9 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" begin // [FEATURE] [E-Document] [Peppol BIS 3.0 FR] [Import] // [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 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 1f062b88130..3d96ea0d62d 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 @@ -67,6 +67,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(); @@ -79,7 +80,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(); @@ -93,6 +93,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(); @@ -105,7 +106,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(); @@ -115,50 +115,38 @@ 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(); - - // Cleanup - CompanyInformation.Get(); - CompanyInformation."SIRET No." := OriginalSIRETNo; - CompanyInformation.Modify(); end; [Test] @@ -169,14 +157,20 @@ codeunit 148146 "Identification Tests" 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.'); @@ -189,21 +183,21 @@ codeunit 148146 "Identification Tests" var EDocument: Record "E-Document"; FREInvoiceLifecycle: Record "FR E-Invoice Lifecycle"; - GeneralLedgerSetup: Record "General Ledger Setup"; FREInvoiceLifecycleMgt: Codeunit "FR E-Invoice Lifecycle Mgt."; begin + // [FEATURE] [AI test] // [SCENARIO] A local-currency payment occurrence keeps the Business Central blank currency representation - GeneralLedgerSetup.Get(); - if GeneralLedgerSetup."LCY Code" = '' then begin - GeneralLedgerSetup."LCY Code" := 'EUR'; - GeneralLedgerSetup.Modify(true); - end; + 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; @@ -216,17 +210,23 @@ codeunit 148146 "Identification Tests" 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; @@ -237,17 +237,23 @@ codeunit 148146 "Identification Tests" 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; @@ -259,16 +265,22 @@ codeunit 148146 "Identification Tests" 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; @@ -280,16 +292,22 @@ codeunit 148146 "Identification Tests" 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; @@ -301,22 +319,30 @@ codeunit 148146 "Identification Tests" 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; @@ -328,11 +354,17 @@ codeunit 148146 "Identification Tests" 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.'); @@ -359,14 +391,20 @@ codeunit 148146 "Identification Tests" 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); @@ -403,12 +441,18 @@ codeunit 148146 "Identification Tests" 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."); @@ -427,12 +471,18 @@ codeunit 148146 "Identification Tests" 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."); @@ -453,7 +503,11 @@ codeunit 148146 "Identification Tests" 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; @@ -464,8 +518,10 @@ codeunit 148146 "Identification Tests" 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."); @@ -486,13 +542,19 @@ codeunit 148146 "Identification Tests" 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); @@ -511,7 +573,11 @@ codeunit 148146 "Identification Tests" 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"); @@ -523,8 +589,10 @@ codeunit 148146 "Identification Tests" 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); @@ -541,15 +609,21 @@ codeunit 148146 "Identification Tests" 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); @@ -564,7 +638,11 @@ codeunit 148146 "Identification Tests" 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(), @@ -572,9 +650,14 @@ codeunit 148146 "Identification Tests" 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 or deleted asserterror FREInvoiceLifecycle.Rename(FREInvoiceLifecycle."Entry No." + 1); asserterror FREInvoiceLifecycle.Delete(true); end; @@ -587,11 +670,17 @@ codeunit 148146 "Identification Tests" 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); + // [THEN] No French lifecycle occurrence is created FREInvoiceLifecycle.SetRange("E-Document Entry No.", EDocument."Entry No"); Assert.RecordIsEmpty(FREInvoiceLifecycle); end; @@ -605,7 +694,11 @@ codeunit 148146 "Identification Tests" 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"); @@ -613,8 +706,10 @@ codeunit 148146 "Identification Tests" 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.'); @@ -628,12 +723,18 @@ codeunit 148146 "Identification Tests" 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(); @@ -649,12 +750,18 @@ codeunit 148146 "Identification Tests" 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; @@ -671,15 +778,21 @@ codeunit 148146 "Identification Tests" 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(); @@ -718,7 +831,11 @@ codeunit 148146 "Identification Tests" 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( @@ -726,8 +843,10 @@ codeunit 148146 "Identification Tests" 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); @@ -756,7 +875,11 @@ codeunit 148146 "Identification Tests" 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( @@ -766,8 +889,10 @@ codeunit 148146 "Identification Tests" 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; @@ -786,7 +911,11 @@ codeunit 148146 "Identification Tests" 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( @@ -798,8 +927,10 @@ codeunit 148146 "Identification Tests" -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); @@ -818,7 +949,11 @@ codeunit 148146 "Identification Tests" 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(), @@ -827,8 +962,10 @@ codeunit 148146 "Identification Tests" 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; @@ -841,7 +978,11 @@ codeunit 148146 "Identification Tests" 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(), @@ -850,8 +991,10 @@ codeunit 148146 "Identification Tests" 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.'); @@ -866,7 +1009,11 @@ codeunit 148146 "Identification Tests" 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(), @@ -876,8 +1023,10 @@ codeunit 148146 "Identification Tests" Commit(); asserterror Error(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"); @@ -906,6 +1055,8 @@ codeunit 148146 "Identification Tests" // [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(); @@ -969,10 +1120,15 @@ codeunit 148146 "Identification Tests" 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.Validate("Registration No.", '123456789'); + CompanyInformation.Validate("SIRET No.", '12345678901234'); + CompanyInformation.Modify(true); if IsInitialized then exit; LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Identification Tests"); 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 d26c414b076..021ba64b185 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 @@ -299,9 +299,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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.'; @@ -320,8 +322,10 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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; @@ -331,13 +335,17 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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; @@ -350,9 +358,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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; @@ -363,9 +373,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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')); @@ -429,9 +441,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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"; @@ -441,8 +455,10 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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; @@ -453,13 +469,17 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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."); @@ -479,13 +499,17 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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."); @@ -504,13 +528,17 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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; @@ -546,8 +574,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); @@ -559,7 +587,6 @@ 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); @@ -573,9 +600,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" XmlDoc: XmlDocument; EndpointId: Text[200]; begin + // [FEATURE] [AI test] // [SCENARIO] A service-specific company participant overrides the company endpoint fallbacks Initialize(); + // [GIVEN] A posted invoice and a company service participant EndpointId := CompanyInformation."Registration No."; ServiceParticipant.Service := EDocumentService.Code; ServiceParticipant."Participant Type" := ServiceParticipant."Participant Type"::Company; @@ -584,9 +613,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" ServiceParticipant.Insert(); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + // [WHEN] The invoice is checked and exported CheckInvoice(SalesInvoiceHeader); ExportInvoice(SalesInvoiceHeader, XmlDoc); + // [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')); @@ -606,8 +637,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); @@ -619,7 +650,6 @@ 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); @@ -631,6 +661,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceHeader: Record "Sales Invoice Header"; XmlDoc: XmlDocument; begin + // [FEATURE] [AI test] // [SCENARIO] Customer VAT registration number is used when the electronic address is blank Initialize(); @@ -657,17 +688,21 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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')); @@ -686,9 +721,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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; @@ -696,8 +733,10 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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; @@ -707,9 +746,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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; @@ -717,8 +758,10 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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; @@ -728,15 +771,19 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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(StrSubstNo(BuyerElectronicAddressRequiredErr, CustomerNo)); end; @@ -746,14 +793,18 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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(StrSubstNo(BuyerElectronicAddressSchemeRequiredErr, CustomerNo)); end; @@ -764,9 +815,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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; @@ -776,8 +829,10 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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; @@ -788,9 +843,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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; @@ -800,8 +857,10 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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 @@ -815,12 +874,17 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" LibraryTestInitialize.OnTestInitialize(Codeunit::"PEPPOL BIS 3.0 XML Tests"); ServiceParticipant.SetRange(Service, EDocumentService.Code); ServiceParticipant.DeleteAll(); - InitializeCompanyIdentity(); - if IsInitialized then + if IsInitialized then begin + InitializeCompanyIdentity(); + Commit(); exit; + end; LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"PEPPOL BIS 3.0 XML Tests"); - LibraryERM.CreateCountryRegion(CountryRegion); + if not CountryRegion.Get('FR') then begin + CountryRegion.Code := 'FR'; + CountryRegion.Insert(); + end; CountryRegion.Validate("ISO Code", 'FR'); CountryRegion.Modify(true); @@ -829,7 +893,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" CompanyInformation.Address := '123 Rue de Paris'; CompanyInformation.City := 'Paris'; CompanyInformation."Post Code" := '75001'; - CompanyInformation."Country/Region Code" := CountryRegion.Code; CompanyInformation.Validate(IBAN, 'FR1420041010050500013M02606'); CompanyInformation.Validate("SWIFT Code", 'CCBPFRPPVER'); CompanyInformation.Validate("Bank Branch No.", '20041'); @@ -847,6 +910,7 @@ 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(); @@ -856,6 +920,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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'); From fad76f7bfa5ebf787b17c66e85c12b13e2fc7669 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 11 Aug 2026 16:05:49 +0200 Subject: [PATCH 62/77] PR review comments --- .../Core/FREInvoiceLifecycleMsg.Codeunit.al | 14 +++++++-- .../EDocumentFacturXHandler.Codeunit.al | 22 ++++++++++---- .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 29 ++++++++----------- .../EReportingFREdit.PermissionSet.al | 4 +-- .../src/EDocFRStructImportTests.Codeunit.al | 21 ++++++++++++++ .../test/src/IdentificationTests.Codeunit.al | 10 ++++++- .../Message/EDocumentMessageAPI.Codeunit.al | 2 ++ 7 files changed, 75 insertions(+), 27 deletions(-) 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 index 60b0e0d8dc1..3db5dbedfb8 100644 --- 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 @@ -19,13 +19,18 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder 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 - Error(NoCapturedOccurrenceErr, EDocument."Entry No"); + 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; @@ -260,6 +265,9 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder 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(InternalLifecycleStatusErr); UnsupportedStatusErrorInfo.DetailedMessage(StrSubstNo(UnsupportedLifecycleStatusErr, LifecycleStatus)); @@ -271,6 +279,8 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder 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'; InternalLifecycleStatusErr: Label 'An internal lifecycle status error occurred.'; + 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; 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 d6ffa2cdfa5..3b70817c11e 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 @@ -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[4]; + begin + PdfInStream.ReadText(Signature, MaxStrLen(Signature)); + exit(Signature = '%PDF'); end; [TryFunction] 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 de7d7e8a9c1..5cf01b2ff40 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 @@ -83,7 +83,8 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" CompanyInformation.Get(); TempBlob.CreateInStream(InStr, TextEncoding::UTF8); - XmlDocument.ReadFrom(InStr, XmlDoc); + if not XmlDocument.ReadFrom(InStr, XmlDoc) then + Error(FrenchPostProcessingXmlErr); InitNamespaceManager(NamespaceMgr, XmlDoc); @@ -208,35 +209,28 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" var SalesInvoiceLine: Record "Sales Invoice Line"; SalesShipmentHeader: Record "Sales Shipment Header"; - ShipmentNos: Dictionary of [Text, Boolean]; + ShipmentNos: Dictionary of [Code[20], Boolean]; OrderNos: Dictionary of [Text, Boolean]; DeliveryDates: Dictionary of [Text, Boolean]; - ShipmentNoFilter: Text; + 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 begin + if not ShipmentNos.ContainsKey(SalesInvoiceLine."Shipment No.") then ShipmentNos.Add(SalesInvoiceLine."Shipment No.", true); - if ShipmentNoFilter <> '' then - ShipmentNoFilter += '|'; - ShipmentNoFilter += SalesInvoiceLine."Shipment No."; - end; if SalesInvoiceLine."Order No." <> '' then AddDistinctValue(OrderNos, SalesInvoiceLine."Order No."); until SalesInvoiceLine.Next() = 0; - if ShipmentNoFilter <> '' then begin - SalesShipmentHeader.SetFilter("No.", ShipmentNoFilter); - SalesShipmentHeader.SetLoadFields("No.", "Posting Date"); - if SalesShipmentHeader.FindSet() then - repeat - ShipmentPostingDates.Add(SalesShipmentHeader."No.", SalesShipmentHeader."Posting Date"); - AddDistinctValue(DeliveryDates, Format(SalesShipmentHeader."Posting Date", 0, 9)); - until SalesShipmentHeader.Next() = 0; - end; + 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; @@ -614,4 +608,5 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" 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 index 0678b4ad2c1..97a0ce1aaeb 100644 --- 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 @@ -12,7 +12,7 @@ permissionset 10972 "E-Reporting FR Edit" IncludedPermissionSets = "E-Reporting FR Read"; - Permissions = tabledata "FR E-Invoice Lifecycle" = IM, - tabledata "FR E-Invoice Lifecycle VAT" = I, + Permissions = tabledata "FR E-Invoice Lifecycle" = im, + tabledata "FR E-Invoice Lifecycle VAT" = i, codeunit "FR E-Invoice Lifecycle Mgt." = X; } \ No newline at end of file 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 7f5b3b160b2..b173538c292 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 @@ -14,6 +14,7 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" FacturXCreditMemoTok: Label 'facturx/facturx-creditmemo-0.xml', Locked = true; PeppolBIS30FRInvoiceTok: Label 'peppolfr/peppol-bis-fr-invoice-0.xml', Locked = true; UnsupportedXmlTok: Label '', Locked = true; + PdfWithoutEmbeddedInvoiceTok: Label '%PDF-1.7', Locked = true; #region Factur-X [Test] @@ -111,6 +112,26 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" Assert.ExpectedError('Unsupported XML root element'); end; + [Test] + procedure FacturXPdfWithoutEmbeddedInvoiceFails() + var + EDocument: Record "E-Document"; + EDocumentFacturXHandler: Codeunit "E-Document Factur-X Handler"; + begin + // [FEATURE] [E-Document] [Factur-X] [Import] + // [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 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 3d96ea0d62d..96f07417983 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 @@ -657,9 +657,17 @@ codeunit 148146 "Identification Tests" // [THEN] The deletion is rejected Assert.ExpectedError(ImmutableLifecycleVATErr); - // [WHEN] The lifecycle occurrence is renamed or deleted + // [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(ImmutableLifecycleErr); end; [Test] 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 index 015e045f052..bda1575bc71 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al @@ -34,6 +34,8 @@ codeunit 6436 "E-Document Message API" /// /// 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."; From e61151f680fcfb9b5e9da33d7642a6745bf88726 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 11 Aug 2026 17:04:30 +0200 Subject: [PATCH 63/77] PR comments fixes --- .../app/src/Core/EDocHelpers.Codeunit.al | 8 +-- .../app/src/Core/FREInvoiceLifecycle.Table.al | 1 + .../Core/FREInvoiceLifecycleError.Codeunit.al | 11 ++- .../Core/FREInvoiceLifecycleMgt.Codeunit.al | 1 + .../FREInvoiceLifecycleWorker.Codeunit.al | 1 - .../EReportingFREdit.PermissionSet.al | 2 + .../src/ExportEReportingTests.Codeunit.al | 23 ++++++ .../test/src/IdentificationTests.Codeunit.al | 40 +++++++++++ .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 72 +++++++++++++++++-- .../Message/EDocumentMessageAPI.Codeunit.al | 1 - 10 files changed, 148 insertions(+), 12 deletions(-) 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 3fcdc474d2f..37a9ef8a7c5 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 @@ -162,13 +162,13 @@ codeunit 10991 "EDoc. Helpers" if Customer."FR Electronic Address" <> '' then begin if Customer."FR Elec. Address Scheme" = Customer."FR Elec. Address Scheme"::" " then - RaiseCustomerError(Customer, StrSubstNo(BuyerElectronicAddressSchemeRequiredErr, Customer."No.")); + RaiseCustomerError(Customer, BuyerElectronicAddressSchemeRequiredErr); exit; end; if Customer."VAT Registration No." <> '' then exit; - RaiseCustomerError(Customer, StrSubstNo(BuyerElectronicAddressRequiredErr, Customer."No.")); + RaiseCustomerError(Customer, BuyerElectronicAddressRequiredErr); end; procedure HasServiceParticipantAddress(EDocumentServiceCode: Code[20]; ParticipantType: Enum "E-Document Source Type"; ParticipantNo: Code[20]): Boolean @@ -241,8 +241,8 @@ codeunit 10991 "EDoc. Helpers" 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.'; SellerElectronicAddressRequiredErr: Label 'SIRET No., Registration No., VAT Registration No., or a Service Participant identifier must be specified for the company for French e-invoicing.'; - BuyerElectronicAddressRequiredErr: Label 'Electronic Address, VAT Registration No., or a Service Participant identifier must be specified for Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.'; - BuyerElectronicAddressSchemeRequiredErr: Label 'Electronic Address Scheme must be specified for Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.'; + BuyerElectronicAddressRequiredErr: Label 'Electronic Address, VAT Registration No., or a Service Participant identifier must be specified for the customer for French e-invoicing.'; + BuyerElectronicAddressSchemeRequiredErr: Label 'Electronic Address Scheme must be specified for the customer for French e-invoicing.'; SellerCountryCodeRequiredErr: Label 'Country/Region Code must be specified in Company Information for French e-invoicing.'; 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/FREInvoiceLifecycle.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycle.Table.al index b12d61243da..f781fa6c4a5 100644 --- 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 @@ -98,6 +98,7 @@ table 10970 "FR E-Invoice Lifecycle" { Caption = 'E-Document Message Entry No.'; DataClassification = SystemMetadata; + TableRelation = "E-Document Message"."Entry No."; ToolTip = 'Specifies the related E-Document message entry.'; } field(13; "Processing Status"; Enum "FR E-Invoice Lifecycle Proc.") 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 index 551d968f79b..380552f4de7 100644 --- 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 @@ -8,7 +8,6 @@ codeunit 10985 "FR E-Invoice Lifecycle Error" { Access = Internal; InherentEntitlements = X; - InherentPermissions = X; Permissions = tabledata "FR E-Invoice Lifecycle" = m; TableNo = "FR E-Invoice Lifecycle"; @@ -16,8 +15,16 @@ codeunit 10985 "FR E-Invoice Lifecycle Error" 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::ExtensionPublisher, + 'Category', LifecycleTelemetryCategoryTok, 'ErrorCallStack', GetLastErrorCallStack()); Rec."Processing Status" := Rec."Processing Status"::Failed; Rec."Last Error" := CopyStr(GetLastErrorText(), 1, MaxStrLen(Rec."Last Error")); Rec.Modify(); end; -} \ No newline at end of file + + var + LifecycleWorkerFailedTelemetryMsg: Label 'French e-invoice lifecycle message creation failed.', 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 index d7951674c72..0caaf6e5b87 100644 --- 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 @@ -131,6 +131,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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; 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 index 8eb7569fc67..223982b5c32 100644 --- 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 @@ -8,7 +8,6 @@ codeunit 10976 "FR E-Invoice Lifecycle Worker" { Access = Internal; InherentEntitlements = X; - InherentPermissions = X; TableNo = "FR E-Invoice Lifecycle"; trigger OnRun() 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 index 97a0ce1aaeb..722c3a5af28 100644 --- 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 @@ -14,5 +14,7 @@ permissionset 10972 "E-Reporting FR Edit" 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/test/src/ExportEReportingTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/ExportEReportingTests.Codeunit.al index 45f9d0285b6..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 @@ -835,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/IdentificationTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/IdentificationTests.Codeunit.al index 96f07417983..54a69fe3f70 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 @@ -52,6 +52,7 @@ codeunit 148146 "Identification Tests" EDocHelpers: Codeunit "EDoc. Helpers"; ImmutableLifecycleErr: Label 'The regulatory identity and values of a French electronic invoice lifecycle occurrence cannot be changed.', Locked = true; ImmutableLifecycleVATErr: Label 'A French electronic invoice lifecycle VAT breakdown cannot be changed.', Locked = true; + UnsupportedSenderPlatformSchemeErr: Label 'The sender platform scheme must be 0238.', 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; @@ -311,6 +312,28 @@ codeunit 148146 "Identification Tests" 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() @@ -346,6 +369,22 @@ codeunit 148146 "Identification Tests" Assert.ExpectedError('The regulatory identity and values of a French electronic invoice lifecycle occurrence cannot be changed.'); end; + [Test] + procedure SenderPlatformSchemeRejectsUnsupportedValue() + var + EDocumentService: Record "E-Document Service"; + begin + // [FEATURE] [AI test] + // [SCENARIO] French lifecycle setup accepts only the supported sender platform identifier scheme + Initialize(); + + // [WHEN] An unsupported sender platform scheme is entered + asserterror EDocumentService.Validate("FR Sender Platform Scheme", '0009'); + + // [THEN] The unsupported scheme is rejected + Assert.ExpectedError(UnsupportedSenderPlatformSchemeErr); + end; + [Test] procedure DetailedApplicationCapturesCollectedForFREInvoice() var @@ -1030,6 +1069,7 @@ codeunit 148146 "Identification Tests" FREInvoiceLifecycle.Modify(); Commit(); asserterror Error(WorkerFailureErr); + Assert.ExpectedError(WorkerFailureErr); // [WHEN] The lifecycle error handler runs FREInvoiceLifecycleError.Run(FREInvoiceLifecycle); 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 021ba64b185..638e6d117f9 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 @@ -11,6 +11,7 @@ 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; @@ -41,12 +42,13 @@ 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 Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.', Locked = true; - BuyerElectronicAddressSchemeRequiredErr: Label 'Electronic Address Scheme must be specified for Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.', Locked = true; + 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; IsInitialized: Boolean; @@ -349,6 +351,46 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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 @@ -784,7 +826,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" asserterror CheckInvoice(SalesInvoiceHeader); // [THEN] A buyer electronic address error is raised - Assert.ExpectedError(StrSubstNo(BuyerElectronicAddressRequiredErr, CustomerNo)); + Assert.ExpectedError(BuyerElectronicAddressRequiredErr); end; [Test] @@ -805,7 +847,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" asserterror CheckInvoice(SalesInvoiceHeader); // [THEN] A buyer electronic address scheme error is raised - Assert.ExpectedError(StrSubstNo(BuyerElectronicAddressSchemeRequiredErr, CustomerNo)); + Assert.ExpectedError(BuyerElectronicAddressSchemeRequiredErr); end; [Test] @@ -935,6 +977,28 @@ 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 + 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); + 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]; 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 index bda1575bc71..c90504b4ae2 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al @@ -14,7 +14,6 @@ codeunit 6436 "E-Document Message API" { Access = Public; InherentEntitlements = X; - InherentPermissions = X; /// /// Creates an E-Document message with an explicit direction and stores its payload. From 7136c8eae598945ea924b61df00b6e8d1ae51df3 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 11 Aug 2026 17:11:15 +0200 Subject: [PATCH 64/77] compile errs --- .../EReportingFR/app/src/Core/FREInvoiceLifecycle.Table.al | 1 - .../src/Formats/FacturX/EDocumentFacturXHandler.Codeunit.al | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) 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 index f781fa6c4a5..b12d61243da 100644 --- 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 @@ -98,7 +98,6 @@ table 10970 "FR E-Invoice Lifecycle" { Caption = 'E-Document Message Entry No.'; DataClassification = SystemMetadata; - TableRelation = "E-Document Message"."Entry No."; ToolTip = 'Specifies the related E-Document message entry.'; } field(13; "Processing Status"; Enum "FR E-Invoice Lifecycle Proc.") 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 3b70817c11e..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 @@ -190,9 +190,9 @@ codeunit 10986 "E-Document Factur-X Handler" implements IStructuredFormatReader, local procedure IsPdf(PdfInStream: InStream): Boolean var - Signature: Text[4]; + Signature: Text; begin - PdfInStream.ReadText(Signature, MaxStrLen(Signature)); + PdfInStream.Read(Signature, 4); exit(Signature = '%PDF'); end; From 651c790a4cc1ed4224bc7fff31216c8d5e6c0054 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 12 Aug 2026 10:13:13 +0200 Subject: [PATCH 65/77] new tests --- .../peppolfr/peppol-bis-fr-creditmemo-0.xml | 67 ++++++++ .../src/EDocFRStructImportTests.Codeunit.al | 150 +++++++++++++++++- .../test/src/IdentificationTests.Codeunit.al | 13 +- 3 files changed, 216 insertions(+), 14 deletions(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/test/.resources/peppolfr/peppol-bis-fr-creditmemo-0.xml 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 b173538c292..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,8 +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] @@ -26,7 +37,7 @@ 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(); @@ -75,7 +86,7 @@ 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(); @@ -98,7 +109,7 @@ 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(); @@ -112,13 +123,53 @@ 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] [E-Document] [Factur-X] [Import] + // [FEATURE] [AI test] // [SCENARIO] Reading a PDF without an embedded Factur-X invoice fails with a clear error Initialize(); @@ -139,7 +190,7 @@ 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(); @@ -166,7 +217,7 @@ 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(); @@ -204,13 +255,46 @@ 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(); @@ -223,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() @@ -249,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/IdentificationTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/IdentificationTests.Codeunit.al index 54a69fe3f70..6eedce6570d 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 @@ -52,7 +52,6 @@ codeunit 148146 "Identification Tests" EDocHelpers: Codeunit "EDoc. Helpers"; ImmutableLifecycleErr: Label 'The regulatory identity and values of a French electronic invoice lifecycle occurrence cannot be changed.', Locked = true; ImmutableLifecycleVATErr: Label 'A French electronic invoice lifecycle VAT breakdown cannot be changed.', Locked = true; - UnsupportedSenderPlatformSchemeErr: Label 'The sender platform scheme must be 0238.', 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; @@ -370,19 +369,19 @@ codeunit 148146 "Identification Tests" end; [Test] - procedure SenderPlatformSchemeRejectsUnsupportedValue() + procedure SenderPlatformSchemeDefaultsTo0238() var EDocumentService: Record "E-Document Service"; begin // [FEATURE] [AI test] - // [SCENARIO] French lifecycle setup accepts only the supported sender platform identifier scheme + // [SCENARIO] French lifecycle setup defaults the sender platform identifier scheme to 0238 Initialize(); - // [WHEN] An unsupported sender platform scheme is entered - asserterror EDocumentService.Validate("FR Sender Platform Scheme", '0009'); + // [WHEN] A new E-Document Service is initialized + EDocumentService.Init(); - // [THEN] The unsupported scheme is rejected - Assert.ExpectedError(UnsupportedSenderPlatformSchemeErr); + // [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] From c3889fa5be6f1e69481d865110e95cb46e8792a3 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 12 Aug 2026 10:20:07 +0200 Subject: [PATCH 66/77] currency fix --- .../EReportingFR/test/src/IdentificationTests.Codeunit.al | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 6eedce6570d..e4192963999 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 @@ -1181,10 +1181,8 @@ codeunit 148146 "Identification Tests" LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Identification Tests"); GeneralLedgerSetup.Get(); - if GeneralLedgerSetup."LCY Code" = '' then begin - GeneralLedgerSetup."LCY Code" := 'EUR'; - GeneralLedgerSetup.Modify(true); - end; + GeneralLedgerSetup."LCY Code" := 'EUR'; + GeneralLedgerSetup.Modify(true); LibraryUtility.UpdateSetupNoSeriesCode( DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Invoice Nos.")); From f16546d23dc6c7c6c03ae5088640675499141333 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Wed, 12 Aug 2026 18:08:21 +0200 Subject: [PATCH 67/77] fix --- .../test/src/FacturXCIIXMLTests.Codeunit.al | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 bc222b81e8d..b50b488799c 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 @@ -23,7 +23,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 @@ -1086,6 +1087,7 @@ 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"); @@ -1105,6 +1107,7 @@ codeunit 148148 "Factur-X CII XML Tests" Customer: Record Customer; begin LibrarySales.CreateCustomer(Customer); + 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'; @@ -1115,6 +1118,19 @@ codeunit 148148 "Factur-X CII XML Tests" exit(Customer."No."); end; + local procedure EnsureVATBusinessPostingGroupExists(VATBusPostingGroupCode: Code[20]) + var + VATBusinessPostingGroup: Record "VAT Business Posting Group"; + begin + 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") var SalesInvoiceHeader: Record "Sales Invoice Header"; From 1d6b65bbf89ac6924a5df8f7174e18b4ac128e35 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 13 Aug 2026 08:45:49 +0200 Subject: [PATCH 68/77] cleantag --- .../app/src/Extensions/EReportingCustomerCard.PageExt.al | 8 ++++++++ 1 file changed, 8 insertions(+) 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 } } } From 785dbaf306c452dde07991b0bc2a995ed30a99b5 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 13 Aug 2026 09:30:41 +0200 Subject: [PATCH 69/77] fix --- .../test/src/FacturXCIIXMLTests.Codeunit.al | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) 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 b50b488799c..632e1a29e71 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; @@ -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); @@ -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(); From 12e77fb89278d80278f9f36783edccbc65187970 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 13 Aug 2026 22:26:08 +0200 Subject: [PATCH 70/77] fixes --- .../EReportingFR/app/src/Core/EDocHelpers.Codeunit.al | 10 ++++++++++ .../test/src/IdentificationTests.Codeunit.al | 5 +++++ .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 8 ++++++++ 3 files changed, 23 insertions(+) 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 37a9ef8a7c5..3e128e31d43 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 @@ -8,6 +8,7 @@ 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" { @@ -15,6 +16,15 @@ codeunit 10991 "EDoc. Helpers" 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 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 e4192963999..365b825975c 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 @@ -1117,6 +1117,11 @@ codeunit 148146 "Identification Tests" 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); 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 638e6d117f9..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 @@ -979,6 +979,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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"; @@ -992,6 +993,13 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" 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); From e3afb8704c154921acbf5f74ddbf1a7a90f49ce2 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 14 Aug 2026 21:48:19 +0200 Subject: [PATCH 71/77] new PR comments --- .../EReportingFR/app/src/Core/FREInvoiceLifecycle.Table.al | 5 +++-- .../app/src/Core/FREInvoiceLifecycleError.Codeunit.al | 4 ++-- .../app/src/Extensions/EReportingEDocuments.PageExt.al | 1 + .../app/src/Extensions/FREInvoiceLifecycles.Page.al | 1 + .../app/src/Extensions/FRServiceParticipant.TableExt.al | 2 +- .../App/src/Processing/Message/EDocumentMessageType.Enum.al | 1 + 6 files changed, 9 insertions(+), 5 deletions(-) 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 index b12d61243da..e5d1fdc7c5a 100644 --- 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 @@ -98,6 +98,7 @@ table 10970 "FR E-Invoice Lifecycle" { Caption = 'E-Document Message Entry No.'; DataClassification = SystemMetadata; + TableRelation = "E-Document Message"."Entry No."; ToolTip = 'Specifies the related E-Document message entry.'; } field(13; "Processing Status"; Enum "FR E-Invoice Lifecycle Proc.") @@ -139,7 +140,7 @@ table 10970 "FR E-Invoice Lifecycle" field(19; "Sender Platform Scheme"; Code[4]) { Caption = 'Sender Platform Scheme'; - DataClassification = CustomerContent; + DataClassification = SystemMetadata; ToolTip = 'Specifies the identifier scheme of the sender platform.'; } field(20; "Sender Platform Name"; Text[100]) @@ -157,7 +158,7 @@ table 10970 "FR E-Invoice Lifecycle" field(22; "Invoice Issuer Scheme"; Code[4]) { Caption = 'Invoice Issuer Scheme'; - DataClassification = CustomerContent; + DataClassification = SystemMetadata; ToolTip = 'Specifies the identifier scheme of the invoice issuer.'; } field(23; "Invoice Issuer Name"; Text[100]) 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 index 380552f4de7..5bd3a3e7c80 100644 --- 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 @@ -17,10 +17,10 @@ codeunit 10985 "FR E-Invoice Lifecycle Error" Rec.TestField("E-Document Message Entry No.", 0); Session.LogMessage( '0000TDQ', LifecycleWorkerFailedTelemetryMsg, Verbosity::Error, - DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, + DataClassification::SystemMetadata, TelemetryScope::All, 'Category', LifecycleTelemetryCategoryTok, 'ErrorCallStack', GetLastErrorCallStack()); Rec."Processing Status" := Rec."Processing Status"::Failed; - Rec."Last Error" := CopyStr(GetLastErrorText(), 1, MaxStrLen(Rec."Last Error")); + Rec."Last Error" := CopyStr(GetLastErrorText(true), 1, MaxStrLen(Rec."Last Error")); Rec.Modify(); end; 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 e760b97b0eb..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 @@ -35,6 +35,7 @@ pageextension 10974 "E-Reporting E-Documents" extends "E-Documents" 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/FREInvoiceLifecycles.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREInvoiceLifecycles.Page.al index ffea85d5591..5ef88e526b7 100644 --- 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 @@ -73,6 +73,7 @@ page 10970 "FR E-Invoice Lifecycles" { action(RetryMessageCreation) { + AccessByPermission = tabledata "FR E-Invoice Lifecycle" = M; ApplicationArea = Basic, Suite; Caption = 'Retry Message Creation'; Enabled = Rec."Processing Status" = Rec."Processing Status"::Failed; 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 index 2e56852eb75..25f0deb86c2 100644 --- 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 @@ -13,7 +13,7 @@ tableextension 10974 "FR Service Participant" extends "Service Participant" field(10970; "FR Identifier Scheme"; Enum "Electronic Address Scheme") { Caption = 'French Identifier Scheme'; - DataClassification = CustomerContent; + DataClassification = SystemMetadata; InitValue = " "; ToolTip = 'Specifies the electronic address scheme used for French electronic invoicing.'; } 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) { From 4a343364ad0c081da19671be0b6d6d891f2cecf1 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 14 Aug 2026 22:28:03 +0200 Subject: [PATCH 72/77] new --- .../app/src/Core/FREInvoiceLifecycle.Table.al | 3 +- .../Core/FREInvoiceLifecycleMgt.Codeunit.al | 42 ++++++++++++++----- .../Core/FREInvoiceLifecycleMsg.Codeunit.al | 12 +++--- .../EReportingEDocService.TableExt.al | 6 +-- .../Extensions/FREInvoiceLifecycles.Page.al | 2 +- .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 8 +++- .../test/src/FacturXCIIXMLTests.Codeunit.al | 6 +-- .../test/src/IdentificationTests.Codeunit.al | 3 +- 8 files changed, 56 insertions(+), 26 deletions(-) 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 index e5d1fdc7c5a..488fed1c92e 100644 --- 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 @@ -197,7 +197,7 @@ table 10970 "FR E-Invoice Lifecycle" trigger OnDelete() begin - Error(ImmutableOccurrenceErr); + Error(DeleteOccurrenceErr); end; trigger OnRename() @@ -234,5 +234,6 @@ table 10970 "FR E-Invoice Lifecycle" 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/FREInvoiceLifecycleMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleMgt.Codeunit.al index 0caaf6e5b87..ae651544cf3 100644 --- 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 @@ -195,6 +195,8 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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")); @@ -202,7 +204,9 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." if VATEntry."Source Currency Code" = '' then exit(-(VATEntry.Base + VATEntry.Amount)); - Error(VATEntryCurrencyErr, VATEntry."Entry No.", CurrencyCode); + 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) @@ -298,6 +302,9 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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") @@ -315,33 +322,33 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." begin EDocument.Get(EDocumentEntryNo); if IsNullGuid(SourceOccurrenceID) then - Error(SourceOccurrenceIDErr); + RaiseInternalError(SourceOccurrenceIDErr); if EventDate = 0D then - Error(EventDateErr); + RaiseInternalError(EventDateErr); case LifecycleStatus of LifecycleStatus::Collected: begin if ReportedAmount <= 0 then - Error(CollectedAmountErr); + RaiseInternalError(CollectedAmountErr); if OriginalOccurrenceEntryNo <> 0 then - Error(CollectedOriginalOccurrenceErr); + RaiseInternalError(CollectedOriginalOccurrenceErr); end; LifecycleStatus::"Negative Collected": begin if ReportedAmount >= 0 then - Error(NegativeCollectedAmountErr); + RaiseInternalError(NegativeCollectedAmountErr); if not OriginalOccurrence.Get(OriginalOccurrenceEntryNo) then - Error(OriginalOccurrenceErr); + RaiseInternalError(OriginalOccurrenceErr); OriginalOccurrence.TestField("E-Document Entry No.", EDocumentEntryNo); OriginalOccurrence.TestField("Lifecycle Status", OriginalOccurrence."Lifecycle Status"::Collected); if ReportedAmount <> -OriginalOccurrence."Reported Amount" then - Error(ReversalAmountErr); + RaiseInternalError(ReversalAmountErr); end; else begin UnsupportedStatusErrorInfo.ErrorType(ErrorType::Internal); - UnsupportedStatusErrorInfo.Message(InternalLifecycleStatusErr); - UnsupportedStatusErrorInfo.DetailedMessage(StrSubstNo(PaymentStatusErr, LifecycleStatus)); + UnsupportedStatusErrorInfo.Message(StrSubstNo(PaymentStatusErr, LifecycleStatus)); + UnsupportedStatusErrorInfo.DetailedMessage(InternalLifecycleStatusErr); Error(UnsupportedStatusErrorInfo); end; end; @@ -375,6 +382,18 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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 RaiseInternalError(ErrorMessage: Text) + var + InternalErrorInfo: ErrorInfo; + begin + InternalErrorInfo.ErrorType(ErrorType::Internal); + InternalErrorInfo.Message(ErrorMessage); + Error(InternalErrorInfo); end; local procedure ResolveCurrencyCode(CurrencyCode: Code[10]): Code[10] @@ -455,5 +474,8 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." 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 index 3db5dbedfb8..01cbdb2e16b 100644 --- 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 @@ -155,12 +155,16 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder 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 - Error(VATBreakdownErr, 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); @@ -269,8 +273,7 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder '0000TDP', StrSubstNo(UnsupportedLifecycleStatusErr, LifecycleStatus), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', LifecycleTelemetryCategoryTok); UnsupportedStatusErrorInfo.ErrorType(ErrorType::Internal); - UnsupportedStatusErrorInfo.Message(InternalLifecycleStatusErr); - UnsupportedStatusErrorInfo.DetailedMessage(StrSubstNo(UnsupportedLifecycleStatusErr, LifecycleStatus)); + UnsupportedStatusErrorInfo.Message(StrSubstNo(UnsupportedLifecycleStatusErr, LifecycleStatus)); Error(UnsupportedStatusErrorInfo); end; @@ -278,7 +281,6 @@ codeunit 10975 "FR E-Invoice Lifecycle Msg." implements IEDocMessageBuilder 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'; - InternalLifecycleStatusErr: Label 'An internal lifecycle status error occurred.'; InternalLifecycleStateErr: Label 'An internal lifecycle processing error occurred.'; LifecycleTelemetryCategoryTok: Label 'French E-Invoice Lifecycle', Locked = true; LifecycleMessageNameTok: Label 'Invoice lifecycle collected status', Locked = true; 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 index aa32b4423fb..ee1bbdf1b77 100644 --- 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 @@ -13,20 +13,20 @@ tableextension 10977 "E-Reporting E-Doc. Service" extends "E-Document Service" field(10970; "FR Sender Platform ID"; Text[50]) { Caption = 'FR Sender Platform ID'; - DataClassification = CustomerContent; + 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 = CustomerContent; + 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 = CustomerContent; + DataClassification = SystemMetadata; ToolTip = 'Specifies the name of the French approved platform that sends lifecycle messages.'; } } 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 index 5ef88e526b7..5255f747254 100644 --- 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 @@ -12,7 +12,7 @@ page 10970 "FR E-Invoice Lifecycles" InherentPermissions = X; PageType = List; SourceTable = "FR E-Invoice Lifecycle"; - SourceTableView = sorting("Entry No.") order(descending); + SourceTableView = sorting("E-Document Entry No.", "Created At") order(descending); UsageCategory = History; layout 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 5cf01b2ff40..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 @@ -78,13 +78,17 @@ 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); - if not XmlDocument.ReadFrom(InStr, XmlDoc) then - Error(FrenchPostProcessingXmlErr); + if not XmlDocument.ReadFrom(InStr, XmlDoc) then begin + FrenchPostProcessingErrorInfo.ErrorType(ErrorType::Internal); + FrenchPostProcessingErrorInfo.Message(FrenchPostProcessingXmlErr); + Error(FrenchPostProcessingErrorInfo); + end; InitNamespaceManager(NamespaceMgr, XmlDoc); 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 632e1a29e71..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 @@ -469,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; 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 365b825975c..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 @@ -51,6 +51,7 @@ codeunit 148146 "Identification Tests" 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; @@ -705,7 +706,7 @@ codeunit 148146 "Identification Tests" asserterror FREInvoiceLifecycle.Delete(true); // [THEN] The deletion is rejected - Assert.ExpectedError(ImmutableLifecycleErr); + Assert.ExpectedError(DeleteLifecycleErr); end; [Test] From 991cdd734bcc21bed51aacf30bf653448fed53b1 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 14 Aug 2026 22:37:20 +0200 Subject: [PATCH 73/77] more fixes --- .../app/src/Core/EDocHelpers.Codeunit.al | 62 ++++++++++++------- .../Core/FREInvoiceLifecycleError.Codeunit.al | 5 ++ .../FRServiceParticipant.TableExt.al | 2 +- 3 files changed, 44 insertions(+), 25 deletions(-) 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 3e128e31d43..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 @@ -105,7 +105,9 @@ codeunit 10991 "EDoc. Helpers" begin CompanyInformation.Get(); if CompanyInformation."Registration No." = '' then - RaiseCompanyInformationError(CompanyInformation, SIRENRequiredErr); + RaiseCompanyInformationError( + CompanyInformation, + StrSubstNo(SIRENRequiredErr, CompanyInformation.FieldCaption("Registration No."), CompanyInformation.TableCaption())); end; procedure CheckSIRETNotEmpty() @@ -114,12 +116,15 @@ codeunit 10991 "EDoc. Helpers" begin CompanyInformation.Get(); if CompanyInformation."SIRET No." = '' then - RaiseCompanyInformationError(CompanyInformation, 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; @@ -132,7 +137,12 @@ codeunit 10991 "EDoc. Helpers" if CompanyInformation.GetVATRegistrationNumber() <> '' then exit; - RaiseCompanyInformationError(CompanyInformation, SellerElectronicAddressRequiredErr); + RaiseCompanyInformationError( + CompanyInformation, + StrSubstNo( + SellerElectronicAddressRequiredErr, CompanyInformation.FieldCaption("SIRET No."), + CompanyInformation.FieldCaption("Registration No."), CompanyInformation.FieldCaption("VAT Registration No."), + ServiceParticipant.TableCaption())); end; procedure CheckSellerCountryCode() @@ -141,7 +151,9 @@ codeunit 10991 "EDoc. Helpers" begin CompanyInformation.Get(); if CompanyInformation."Country/Region Code" = '' then - RaiseCompanyInformationError(CompanyInformation, SellerCountryCodeRequiredErr); + RaiseCompanyInformationError( + CompanyInformation, + StrSubstNo(SellerCountryCodeRequiredErr, CompanyInformation.FieldCaption("Country/Region Code"), CompanyInformation.TableCaption())); end; procedure CheckBuyerElectronicAddress(var SourceDocumentHeader: RecordRef) @@ -152,6 +164,7 @@ codeunit 10991 "EDoc. Helpers" 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]; @@ -172,13 +185,18 @@ codeunit 10991 "EDoc. Helpers" if Customer."FR Electronic Address" <> '' then begin if Customer."FR Elec. Address Scheme" = Customer."FR Elec. Address Scheme"::" " then - RaiseCustomerError(Customer, BuyerElectronicAddressSchemeRequiredErr); + RaiseCustomerError( + Customer, StrSubstNo(BuyerElectronicAddressSchemeRequiredErr, Customer.FieldCaption("FR Elec. Address Scheme"))); exit; end; if Customer."VAT Registration No." <> '' then exit; - RaiseCustomerError(Customer, BuyerElectronicAddressRequiredErr); + 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 @@ -202,13 +220,13 @@ codeunit 10991 "EDoc. Helpers" HasIdentifier := ServiceParticipant."Participant Identifier" <> ''; HasScheme := ServiceParticipant."FR Identifier Scheme" <> ServiceParticipant."FR Identifier Scheme"::" "; if HasIdentifier <> HasScheme then begin - ParticipantAddressErrorInfo.Title(ServiceParticipantSetupTitleLbl); + 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(ShowServiceParticipantLbl); + ParticipantAddressErrorInfo.AddNavigationAction(StrSubstNo(ShowRecordLbl, ServiceParticipant.TableCaption())); Error(ParticipantAddressErrorInfo); end; @@ -219,12 +237,12 @@ codeunit 10991 "EDoc. Helpers" var CompanyInformationErrorInfo: ErrorInfo; begin - CompanyInformationErrorInfo.Title(CompanyInformationSetupTitleLbl); + CompanyInformationErrorInfo.Title(StrSubstNo(SetupIncompleteTitleLbl, CompanyInformation.TableCaption())); CompanyInformationErrorInfo.Message(ErrorMessage); CompanyInformationErrorInfo.DetailedMessage(ErrorMessage); CompanyInformationErrorInfo.RecordId(CompanyInformation.RecordId()); CompanyInformationErrorInfo.PageNo(Page::"Company Information"); - CompanyInformationErrorInfo.AddNavigationAction(ShowCompanyInformationLbl); + CompanyInformationErrorInfo.AddNavigationAction(StrSubstNo(ShowRecordLbl, CompanyInformation.TableCaption())); Error(CompanyInformationErrorInfo); end; @@ -232,27 +250,23 @@ codeunit 10991 "EDoc. Helpers" var CustomerErrorInfo: ErrorInfo; begin - CustomerErrorInfo.Title(CustomerSetupTitleLbl); + CustomerErrorInfo.Title(StrSubstNo(SetupIncompleteTitleLbl, Customer.TableCaption())); CustomerErrorInfo.Message(ErrorMessage); CustomerErrorInfo.DetailedMessage(ErrorMessage); CustomerErrorInfo.RecordId(Customer.RecordId()); CustomerErrorInfo.PageNo(Page::"Customer Card"); - CustomerErrorInfo.AddNavigationAction(ShowCustomerLbl); + CustomerErrorInfo.AddNavigationAction(StrSubstNo(ShowRecordLbl, Customer.TableCaption())); Error(CustomerErrorInfo); end; var - CompanyInformationSetupTitleLbl: Label 'Company information setup is incomplete'; - CustomerSetupTitleLbl: Label 'Customer setup is incomplete'; - ServiceParticipantSetupTitleLbl: Label 'Service participant setup is incomplete'; - ShowCompanyInformationLbl: Label 'Show Company Information'; - ShowCustomerLbl: Label 'Show Customer'; - ShowServiceParticipantLbl: Label 'Show Service Participant'; - 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.'; - SellerElectronicAddressRequiredErr: Label 'SIRET No., Registration No., VAT Registration No., or a Service Participant identifier must be specified for the company for French e-invoicing.'; - BuyerElectronicAddressRequiredErr: Label 'Electronic Address, VAT Registration No., or a Service Participant identifier must be specified for the customer for French e-invoicing.'; - BuyerElectronicAddressSchemeRequiredErr: Label 'Electronic Address Scheme must be specified for the customer for French e-invoicing.'; - 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/FREInvoiceLifecycleError.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceLifecycleError.Codeunit.al index 5bd3a3e7c80..3282c8508c4 100644 --- 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 @@ -18,6 +18,10 @@ codeunit 10985 "FR E-Invoice Lifecycle Error" 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")); @@ -26,5 +30,6 @@ codeunit 10985 "FR E-Invoice Lifecycle Error" 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/Extensions/FRServiceParticipant.TableExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FRServiceParticipant.TableExt.al index 25f0deb86c2..2e56852eb75 100644 --- 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 @@ -13,7 +13,7 @@ tableextension 10974 "FR Service Participant" extends "Service Participant" field(10970; "FR Identifier Scheme"; Enum "Electronic Address Scheme") { Caption = 'French Identifier Scheme'; - DataClassification = SystemMetadata; + DataClassification = CustomerContent; InitValue = " "; ToolTip = 'Specifies the electronic address scheme used for French electronic invoicing.'; } From 6a2ead0d861bcc643061512c151544e9bb8aae3a Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 14 Aug 2026 23:15:59 +0200 Subject: [PATCH 74/77] fix object ID --- .../App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index c90504b4ae2..60c5089b3a0 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al @@ -10,7 +10,7 @@ using System.Utilities; /// /// Provides public operations for E-Document messages. /// -codeunit 6436 "E-Document Message API" +codeunit 6532 "E-Document Message API" { Access = Public; InherentEntitlements = X; From e53bb79c1ffa56809ea8f244ca4475602f298821 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sat, 15 Aug 2026 09:39:55 +0200 Subject: [PATCH 75/77] removed tablerelations --- .../EReportingFR/app/src/Core/FREInvoiceLifecycle.Table.al | 1 - 1 file changed, 1 deletion(-) 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 index 488fed1c92e..f0fb8ac7347 100644 --- 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 @@ -98,7 +98,6 @@ table 10970 "FR E-Invoice Lifecycle" { Caption = 'E-Document Message Entry No.'; DataClassification = SystemMetadata; - TableRelation = "E-Document Message"."Entry No."; ToolTip = 'Specifies the related E-Document message entry.'; } field(13; "Processing Status"; Enum "FR E-Invoice Lifecycle Proc.") From 96b43833325fb09c1ae23d3937fbad183e3ca858 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sat, 15 Aug 2026 10:49:55 +0200 Subject: [PATCH 76/77] currency changes --- .../app/src/Core/FREInvoiceLifecycleMgt.Codeunit.al | 7 ++++--- .../app/src/Core/FREInvoiceLifecycleVAT.Table.al | 8 ++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) 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 index ae651544cf3..db8540e44fd 100644 --- 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 @@ -242,7 +242,7 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." RemainingAmount -= AllocatedAmount; end; - InsertVATBreakdown(FREInvoiceLifecycleVAT, FREInvoiceLifecycle."Entry No.", LineNo, VATRate, AllocatedAmount); + InsertVATBreakdown(FREInvoiceLifecycleVAT, FREInvoiceLifecycle."Entry No.", LineNo, VATRate, AllocatedAmount, CurrencyCode); end; end; @@ -271,17 +271,18 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." repeat InsertVATBreakdown( ReversalLifecycleVAT, FREInvoiceLifecycle."Entry No.", OriginalLifecycleVAT."Line No.", OriginalLifecycleVAT."VAT %", - -OriginalLifecycleVAT."Reported Amount"); + -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) + 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; 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 index e7270c3540e..41198fe826f 100644 --- 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 @@ -28,6 +28,7 @@ table 10971 "FR E-Invoice Lifecycle VAT" } field(3; "VAT %"; Decimal) { + AutoFormatType = 0; Caption = 'VAT %'; DataClassification = CustomerContent; DecimalPlaces = 0 : 5; @@ -35,11 +36,18 @@ table 10971 "FR E-Invoice Lifecycle VAT" } 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 From 3d4bab766e30cb60db050d0e64d9c07cd023c56f Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sat, 15 Aug 2026 23:38:40 +0200 Subject: [PATCH 77/77] error changed --- .../Core/FREInvoiceLifecycleMgt.Codeunit.al | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) 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 index db8540e44fd..8fe51a6bf57 100644 --- 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 @@ -323,28 +323,28 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." begin EDocument.Get(EDocumentEntryNo); if IsNullGuid(SourceOccurrenceID) then - RaiseInternalError(SourceOccurrenceIDErr); + RaiseClientError(SourceOccurrenceIDErr); if EventDate = 0D then - RaiseInternalError(EventDateErr); + RaiseClientError(EventDateErr); case LifecycleStatus of LifecycleStatus::Collected: begin if ReportedAmount <= 0 then - RaiseInternalError(CollectedAmountErr); + RaiseClientError(CollectedAmountErr); if OriginalOccurrenceEntryNo <> 0 then - RaiseInternalError(CollectedOriginalOccurrenceErr); + RaiseClientError(CollectedOriginalOccurrenceErr); end; LifecycleStatus::"Negative Collected": begin if ReportedAmount >= 0 then - RaiseInternalError(NegativeCollectedAmountErr); + RaiseClientError(NegativeCollectedAmountErr); if not OriginalOccurrence.Get(OriginalOccurrenceEntryNo) then - RaiseInternalError(OriginalOccurrenceErr); + RaiseClientError(OriginalOccurrenceErr); OriginalOccurrence.TestField("E-Document Entry No.", EDocumentEntryNo); OriginalOccurrence.TestField("Lifecycle Status", OriginalOccurrence."Lifecycle Status"::Collected); if ReportedAmount <> -OriginalOccurrence."Reported Amount" then - RaiseInternalError(ReversalAmountErr); + RaiseClientError(ReversalAmountErr); end; else begin UnsupportedStatusErrorInfo.ErrorType(ErrorType::Internal); @@ -388,13 +388,13 @@ codeunit 10971 "FR E-Invoice Lifecycle Mgt." DataClassification::SystemMetadata, TelemetryScope::All, 'Category', LifecycleTelemetryCategoryTok); end; - local procedure RaiseInternalError(ErrorMessage: Text) + local procedure RaiseClientError(ErrorMessage: Text) var - InternalErrorInfo: ErrorInfo; + ClientErrorInfo: ErrorInfo; begin - InternalErrorInfo.ErrorType(ErrorType::Internal); - InternalErrorInfo.Message(ErrorMessage); - Error(InternalErrorInfo); + ClientErrorInfo.ErrorType(ErrorType::Client); + ClientErrorInfo.Message(ErrorMessage); + Error(ClientErrorInfo); end; local procedure ResolveCurrencyCode(CurrencyCode: Code[10]): Code[10]