Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// ------------------------------------------------------------------------------------------------
// 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;

table 10970 "FR E-Invoice Message"
{
Caption = 'FR E-Invoice Message';
DataClassification = CustomerContent;
InherentEntitlements = X;
InherentPermissions = X;
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; Type; Enum "FR E-Invoice Message Type")
{
Caption = 'Type';
DataClassification = SystemMetadata;
}
field(4; "Source Occurrence ID"; Guid)
{
Caption = 'Source Occurrence ID';
DataClassification = SystemMetadata;
}
field(5; "Original Entry No."; Integer)
{
Caption = 'Original Entry No.';
DataClassification = SystemMetadata;
TableRelation = "FR E-Invoice Message"."Entry No.";
}
field(6; Amount; Decimal)
{
AutoFormatExpression = Rec."Currency Code";
AutoFormatType = 1;
Caption = 'Amount';
DataClassification = CustomerContent;
}
field(7; "Currency Code"; Code[10])
{
Caption = 'Currency Code';
DataClassification = CustomerContent;
}
field(8; "Event Date"; Date)
{
Caption = 'Event Date';
DataClassification = CustomerContent;
}
field(9; "Detailed Ledger Entry No."; Integer)
{
Caption = 'Detailed Ledger Entry No.';
DataClassification = SystemMetadata;
}
field(10; "Reason Code"; Code[20])
{
Caption = 'Reason Code';
DataClassification = CustomerContent;
}
field(11; "Reason Description"; Text[500])
{
Caption = 'Reason Description';
DataClassification = CustomerContent;
}
field(12; "E-Document Message Entry No."; Integer)
Comment thread
djukicmilica marked this conversation as resolved.
{
Caption = 'E-Document Message Entry No.';
DataClassification = SystemMetadata;
}
Comment thread
djukicmilica marked this conversation as resolved.
field(13; "Created At"; DateTime)
{
Caption = 'Created At';
DataClassification = SystemMetadata;
}
}

keys
{
key(PK; "Entry No.")
{
Clustered = true;
}
key(Occurrence; "E-Document Entry No.", "Source Occurrence ID", Type)
{
Unique = true;
}
key(DetailedLedgerEntry; Type, "Detailed Ledger Entry No.")
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// ------------------------------------------------------------------------------------------------
// 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.GeneralLedger.Setup;
using System.Utilities;

codeunit 10976 "FR E-Invoice Message Builder"
{
Access = Internal;
InherentEntitlements = X;
InherentPermissions = X;

procedure BuildMessage(EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; var TempBlob: Codeunit "Temp Blob")
var
XmlDoc: XmlDocument;
RootElement: XmlElement;
AcknowledgementElement: XmlElement;
ReferenceElement: XmlElement;
StatusElement: XmlElement;
AmountElement: XmlElement;
OutStream: OutStream;
begin
XmlDoc := XmlDocument.Create();
XmlDoc.SetDeclaration(XmlDeclaration.Create('1.0', 'UTF-8', 'no'));
RootElement := XmlElement.Create('CrossDomainAcknowledgementAndResponse', RsmNamespaceTok);
RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('ram', RamNamespaceTok));
RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('rsm', RsmNamespaceTok));
RootElement.Add(XmlElement.Create('ExchangedDocument', RsmNamespaceTok,
XmlElement.Create('ID', RamNamespaceTok, Format(FREInvoiceMessage."Source Occurrence ID"))));

AcknowledgementElement := XmlElement.Create('AcknowledgementDocument', RsmNamespaceTok);
ReferenceElement := XmlElement.Create('ReferenceReferencedDocument', RamNamespaceTok);
ReferenceElement.Add(XmlElement.Create('IssuerAssignedID', RamNamespaceTok, EDocument."Document No."));
ReferenceElement.Add(XmlElement.Create('StatusCode', RamNamespaceTok, InvoiceReferenceStatusCodeTok));
if FREInvoiceMessage.Type = FREInvoiceMessage.Type::Refused then begin
ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, RefusedStatusCodeTok));
ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, RefusedStatusNameTok));
StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok);
StatusElement.Add(XmlElement.Create('ReasonCode', RamNamespaceTok, FREInvoiceMessage."Reason Code"));
StatusElement.Add(XmlElement.Create('Reason', RamNamespaceTok, FREInvoiceMessage."Reason Description"));
ReferenceElement.Add(StatusElement);
end else begin
ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, CollectedStatusCodeTok));
ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, CollectedStatusNameTok));
StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok);
StatusElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, CollectedAmountTypeCodeTok));
AmountElement := XmlElement.Create('ValueAmount', RamNamespaceTok, Format(FREInvoiceMessage.Amount, 0, 9));
AmountElement.Add(XmlAttribute.Create('currencyID', ResolveCurrencyCode(FREInvoiceMessage."Currency Code")));
StatusElement.Add(AmountElement);
ReferenceElement.Add(StatusElement);
end;
AcknowledgementElement.Add(ReferenceElement);
RootElement.Add(AcknowledgementElement);
XmlDoc.Add(RootElement);

TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8);
XmlDoc.WriteTo(OutStream);
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;

var
RsmNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:CrossDomainAcknowledgementAndResponse:100', Locked = true;
RamNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', Locked = true;
InvoiceReferenceStatusCodeTok: Label '47', Locked = true;
CollectedStatusCodeTok: Label '212', Locked = true;
CollectedStatusNameTok: Label 'Encaissée', Locked = true;
CollectedAmountTypeCodeTok: Label 'MEN', Locked = true;
RefusedStatusCodeTok: Label '210', Locked = true;
RefusedStatusNameTok: Label 'Refusée', Locked = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace Microsoft.eServices.EDocument.Formats;

using Microsoft.eServices.EDocument;
using Microsoft.eServices.EDocument.Processing.Message;
using Microsoft.Sales.Receivables;
using System.Utilities;

codeunit 10975 "FR E-Invoice Message Mgt."
{
Access = Internal;
InherentEntitlements = X;
InherentPermissions = X;

internal procedure RefuseInvoice(EDocument: Record "E-Document"; ReasonCode: Code[20]; ReasonDescription: Text[500])
var
FREInvoiceMessage: Record "FR E-Invoice Message";
begin
EDocument.TestField(Direction, EDocument.Direction::Incoming);
EDocument.TestField("Document Type", EDocument."Document Type"::"Purchase Invoice");
EDocument.TestField(Service);
if ReasonCode = '' then
Error(ReasonCodeRequiredErr);
if ReasonDescription = '' then
Comment thread
djukicmilica marked this conversation as resolved.
Outdated
Error(ReasonDescriptionRequiredErr);

FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No");
FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Refused);
if not FREInvoiceMessage.IsEmpty() then
Error(AlreadyRefusedErr, EDocument."Document No.");

CreateAndSendMessage(EDocument, FREInvoiceMessage.Type::Refused, CreateGuid(), 0, '', Today(), 0, 0, ReasonCode, ReasonDescription);
end;

internal procedure ProcessApplication(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry")
var
EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt.";
begin
EDocPaymentOccurrenceMgt.ProcessApplication(DetailedCustLedgEntry);
end;

internal procedure ProcessUnapplication(OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry")
var
EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt.";
begin
EDocPaymentOccurrenceMgt.ProcessUnapplication(OldDetailedCustLedgEntry, NewDetailedCustLedgEntry);
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. Payment Occurrence Mgt.", 'OnAfterCreatePaymentOccurrence', '', false, false)]
local procedure OnAfterCreatePaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence")
var
CollectedMessage: Record "FR E-Invoice Message";
EDocument: Record "E-Document";
OriginalOccurrence: Record "E-Doc. Payment Occurrence";
begin
EDocument.Get(EDocPaymentOccurrence."E-Document Entry No.");
if not IsEligibleFrenchEDocument(EDocument) then
exit;

if EDocPaymentOccurrence.Type = EDocPaymentOccurrence.Type::Applied then begin
CreateAndSendMessage(
EDocument, "FR E-Invoice Message Type"::Collected, EDocPaymentOccurrence."Source Occurrence ID",
EDocPaymentOccurrence.Amount, EDocPaymentOccurrence."Currency Code", EDocPaymentOccurrence."Event Date",
EDocPaymentOccurrence."Detailed Ledger Entry No.", 0, '', '');
exit;
end;

if not OriginalOccurrence.Get(EDocPaymentOccurrence."Original Occurrence Entry No.") then
exit;
CollectedMessage.SetRange("E-Document Entry No.", EDocument."Entry No");
CollectedMessage.SetRange(Type, CollectedMessage.Type::Collected);
CollectedMessage.SetRange("Source Occurrence ID", OriginalOccurrence."Source Occurrence ID");
if not CollectedMessage.FindFirst() then
exit;

CreateAndSendMessage(
EDocument, "FR E-Invoice Message Type"::"Negative Collected", EDocPaymentOccurrence."Source Occurrence ID",
EDocPaymentOccurrence.Amount, EDocPaymentOccurrence."Currency Code", EDocPaymentOccurrence."Event Date",
EDocPaymentOccurrence."Detailed Ledger Entry No.", CollectedMessage."Entry No.", '', '');
end;

local procedure CreateAndSendMessage(EDocument: Record "E-Document"; MessageType: Enum "FR E-Invoice Message Type"; SourceOccurrenceID: Guid; Amount: Decimal; CurrencyCode: Code[10]; EventDate: Date; DetailedLedgerEntryNo: Integer; OriginalEntryNo: Integer; ReasonCode: Code[20]; ReasonDescription: Text[500])
var
FREInvoiceMessage: Record "FR E-Invoice Message";
EDocumentMessageAPI: Codeunit "E-Document Message API";
FREInvoiceMessageBuilder: Codeunit "FR E-Invoice Message Builder";
TempBlob: Codeunit "Temp Blob";
begin
FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Performance}$

CreateAndSendMessage uses FindFirst() only to decide whether a matching FR E-Invoice Message already exists, then discards the row. IsEmpty() answers the same existence question without materializing a record.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

        FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No");
        FREInvoiceMessage.SetRange("Source Occurrence ID", SourceOccurrenceID);
        FREInvoiceMessage.SetRange(Type, MessageType);
        if not FREInvoiceMessage.IsEmpty() then
            exit;

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4

FREInvoiceMessage.SetRange("Source Occurrence ID", SourceOccurrenceID);
FREInvoiceMessage.SetRange(Type, MessageType);
if FREInvoiceMessage.FindFirst() then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Performance}$

This lookup uses FindFirst() only to test whether a matching FR E-Invoice Message already exists, but the record is discarded. IsEmpty() expresses the existence check directly and avoids materializing a row unnecessarily.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

        if not FREInvoiceMessage.IsEmpty() then

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.34.4

exit;

FREInvoiceMessage.Init();
FREInvoiceMessage."E-Document Entry No." := EDocument."Entry No";
FREInvoiceMessage.Type := MessageType;
FREInvoiceMessage."Source Occurrence ID" := SourceOccurrenceID;
FREInvoiceMessage."Original Entry No." := OriginalEntryNo;
FREInvoiceMessage.Amount := Amount;
FREInvoiceMessage."Currency Code" := CurrencyCode;
FREInvoiceMessage."Event Date" := EventDate;
FREInvoiceMessage."Detailed Ledger Entry No." := DetailedLedgerEntryNo;
FREInvoiceMessage."Reason Code" := ReasonCode;
FREInvoiceMessage."Reason Description" := ReasonDescription;
FREInvoiceMessage."Created At" := CurrentDateTime();
FREInvoiceMessage.Insert();

FREInvoiceMessageBuilder.BuildMessage(EDocument, FREInvoiceMessage, TempBlob);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Interfaces}$

The new French lifecycle flows queue outgoing messages in production, but this PR adds no production IMessageSender implementation or Service Integration mapping for them. The only explicit IMessageSender mapping added here is the test-only FR Message Mock, so real EDocumentService."Service Integration V2" values still resolve to E-Doc. Msg. Transport Default, which raises "does not support sending E-Document messages" when Accept/Refuse or payment-triggered sends run. Add a real production IMessageSender implementation and map the relevant service integration value(s), or keep these paths disabled until a sender exists.

Agent judgement — not directly backed by a BCQuality knowledge article.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4

FREInvoiceMessage."E-Document Message Entry No." := EDocumentMessageAPI.CreateMessage(
EDocument, "E-Document Message Type"::"FR Invoice Lifecycle", GetResponseType(MessageType), TempBlob);
FREInvoiceMessage.Modify();
EDocumentMessageAPI.QueueMessage(FREInvoiceMessage."E-Document Message Entry No.");
end;

local procedure IsEligibleFrenchEDocument(EDocument: Record "E-Document"): Boolean
var
EDocumentService: Record "E-Document Service";
EDocumentServiceStatus: Record "E-Document Service Status";
begin
if not EDocumentService.Get(EDocument.Service) then
exit(false);
if not (EDocumentService."Document Format" in [EDocumentService."Document Format"::"Peppol BIS 3.0 FR", EDocumentService."Document Format"::"Factur-X FR"]) then
exit(false);
if not EDocumentServiceStatus.Get(EDocument."Entry No", EDocument.Service) then
exit(false);
exit(EDocumentServiceStatus.Status in [EDocumentServiceStatus.Status::Approved, EDocumentServiceStatus.Status::Cleared]);
end;

local procedure GetResponseType(MessageType: Enum "FR E-Invoice Message Type"): Enum "E-Doc. Response Type"
begin
if MessageType = MessageType::Refused then
exit("E-Doc. Response Type"::Refused);
exit("E-Doc. Response Type"::None);
end;

var
ReasonCodeRequiredErr: Label 'A refusal reason code is required.';
ReasonDescriptionRequiredErr: Label 'A refusal reason description is required.';
AlreadyRefusedErr: Label 'Invoice %1 has already been refused.', Comment = '%1 = invoice number';
}
Original file line number Diff line number Diff line change
@@ -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 10970 "FR E-Invoice Message Type"
{
Access = Public;

value(0; Collected)
{
Caption = 'Collected';
}
value(1; "Negative Collected")
{
Caption = 'Negative Collected';
}
value(2; Refused)
{
Caption = 'Refused';
}
value(3; Submitted)
{
Caption = 'Submitted';
}
value(4; "Technical Rejected")
{
Caption = 'Technical Rejected';
}
value(5; Accepted)
{
Caption = 'Accepted';
}
}
Loading
Loading