-
Notifications
You must be signed in to change notification settings - Fork 441
[FR E-Reporting] Add payment and invoice lifecycle messages #10437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
08c3cc0
31b4bb0
dc09a12
31387e6
161edf4
53b2023
bcef50b
54fcc33
6cff425
6a85318
19a729b
7a454d7
dc578d2
c55c5d4
70ca478
80c8837
67d718e
05f4c34
f5b8567
62dd4e4
ad98e96
bb53620
2e6e4e3
e3a91c8
372c19e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
| { | ||
| Caption = 'E-Document Message Entry No.'; | ||
| DataClassification = SystemMetadata; | ||
| } | ||
|
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 | ||
|
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"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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() thenKnowledge: 👍 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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'; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.