Extending e-documents functionality
The E-Documents Core module is created as an extension and built as a framework. Therefore, by default, there's no Document Format or Service Integration option. These details, and others, are components of localization apps, which cater to specific local requirements. This framework is intended to cover most requirements for the process of communication with electronic documents (e-documents). However, some parts are left for localization apps. The information in this article helps you add value to this module and use it for your own localization.
Develop a localization extension
To implement your localization on top of the E-Document Core application, you should complete the following steps.
Step 1 - Create and set up a new extension
Create a new extension and add the dependency to the E-Document Core application. In your app.json file, add the dependency on the E-Document Core extension:
"dependencies": [
{
"id": "e1d97edc-c239-46b4-8d84-6368bdf67c8b",
"name": "E-Document Core",
"publisher": "Microsoft",
"version": "23.0.0.0"
}
]
Step 2 - Implement the document interface
The E-Document Core interface comprises a collection of methods that are designed to streamline the export of Microsoft Dynamics 365 Business Central documents, such as sales invoices, into e-document blobs, based on predefined format specifications. The interface also facilitates the reverse process by enabling the import of documents from blobs back into Business Central.
This first step is to extend the enumeration (enum) and associate it with your implementation codeunit.
enumextension 50100 "EDocument Format Ext" extends "E-Document Format"
{
value(50100; "PEPPOL 3.x")
{
Implementation = "E-Document" = "PEPPOL EDocument";
}
}
The document interface is divided into two sections:
- Create an e-document from a Business Central document that can be sent to a designated endpoint: Check, Create, and CreateBatch.
- Receive a document from the endpoint: GetBasicInfo and PrepareDocument.
The following example shows how you can implement each method in the interface.
Check: Use the method to run a check on the release/post action of a document, to make sure that all fields that are required to submit the document are available.
procedure Check(var SourceDocumentHeader: RecordRef; EDocumentService: Record "E-Document Service"; EDocumentProcessingPhase: Enum "E-Document Processing Phase") var SalesHeader: Record "Sales Header"; begin Case SourceDocumentHeader.Number of Database::"Sales Header": begin SourceDocumentHeader.Field(SalesHeader.FieldNo("Customer Posting Group")).TestField(); SourceDocumentHeader.Field(SalesHeader.FieldNo("Posting Date")).TestField(); end; End;
You can also select to perform distinct checks, depending on the document processing phase.
procedure Check(var SourceDocumentHeader: RecordRef; EDocumentService: Record "E-Document Service"; EDocumentProcessingPhase: Enum "E-Document Processing Phase") var SalesHeader: Record "Sales Header"; begin Case SourceDocumentHeader.Number of Database::"Sales Header": case EDocumentProcessingPhase of EDocumentProcessingPhase::Release: begin SourceDocumentHeader.Field(SalesHeader.FieldNo("Customer Posting Group")).TestField(); SourceDocumentHeader.Field(SalesHeader.FieldNo("Posting Date")).TestField(); end; EDocumentProcessingPhase::Post: begin SourceDocumentHeader.Field(SalesHeader.FieldNo("Customer Posting Group")).TestField(); SourceDocumentHeader.Field(SalesHeader.FieldNo("Posting Date")).TestField(); SourceDocumentHeader.Field(SalesHeader.FieldNo("Bill-to Name")).TestField(); end; end; End; end;
Create: Create a blob that represents the posted document.
The core extension creates an "E-Document" record that has initial information. It automatically determines the type of the document, based on the source document. You can find the value in the Document Type field.
The Document Type field is automatically identified by the core extension, based on the source document. If you've introduced your custom document type, you must extend the E-Document Type enum and populate the E-Document Type field.
procedure Create(EDocumentService: Record "E-Document Service"; var EDocument: Record "E-Document"; var SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; var TempBlob: Codeunit "Temp Blob") var OutStr: OutStream; begin TempBlob.CreateOutStream(OutStr); case EDocument."Document Type" of EDocument."Document Type"::"Sales Invoice": GenerateInvoiceXMLFile(SourceDocumentHeader, OutStr); EDocument."Document Type"::"Sales Credit Memo": GenerateCrMemoXMLFile(SourceDocumentHeader, OutStr); end; end;
CreateBatch: Create a blob that represents a batch of posted documents.
The functionality resembles the Create method. It lets you iterate through a collection of e-documents and generate a single blob that collectively represents them.
procedure CreateBatch(EDocumentService: Record "E-Document Service"; var EDocuments: Record "E-Document"; var SourceDocumentHeaders: RecordRef; var SourceDocumentsLines: RecordRef; var TempBlob: Codeunit "Temp Blob") var OutStr: OutStream; begin TempBlob.CreateOutStream(OutStr); if EDocuments.FindSet() then repeat OutStr.WriteText(EDocuments."Document No."); until EDocuments.Next() = 0; end;
GetBasicInfo: Get the basic information of an e-document from the received blob.
procedure GetBasicInfo(var EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob") var XmlDoc: XmlDocument; DocInstr: InStream; NamespaceManager: XmlNamespaceManager; begin // Create an XML document from the blob TempBlob.CreateInStream(DocInstr); XmlDocument.ReadFrom(DocInstr, XmlDoc); // Parse the document to fill EDocument information EDocument."Bill-to/Pay-to No." := CopyStr(GetPEPPOLNode('//cac:InvoiceLine/cbc:ID', XmlDoc, NamespaceManager), 1, 20); EDocument."Bill-to/Pay-to Name" := CopyStr(GetPEPPOLNode('//cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name', XmlDoc, NamespaceManager), 1, 20); Evaluate(EDocument."Document Date", GetPEPPOLNode('//cbc:IssueDate', XmlDoc, NamespaceManager)); EDocument."Document Type" := EDocument."Document Type"::"Purchase Invoice"; end;
PrepareDocument: Create a document from the imported blob.
procedure PrepareDocument(var EDocument: Record "E-Document"; var CreatedDocumentHeader: RecordRef; var CreatedDocumentLines: RecordRef; var TempBlob: Codeunit "Temp Blob") var begin end;
Note
The Create and CreateBatch methods generate a blob that's stored in the log table. When you export the blob, the E-Document Core app exports it without a predefined file extension. If you want to specify the file extension, use the following event subscriber.
[EventSubscriber(ObjectType::Table, Database::"E-Document Log", 'OnBeforeExportDataStorage', '', false, false)] local procedure MyProcedure() begin end;
Step 3 - Implement the integration interface
The E-Document Core integration interface comprises a collection of methods that are designed to streamline the process of integrating with endpoints for submitting electronic documents.
First, you must extend the enum and associate it with your implementation codeunit.
enumextension 50101 "EDocument Integration Ext" extends "E-Document Integration"
{
value(50100; "Example Service")
{
Implementation = "E-Document Integration" = "Example Integration SVC";
}
}
The following example shows how you can implement each method in the interface.
Send: Send an e-document to an external service.
procedure Send(var EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob"; var IsAsync: Boolean; var HttpRequest: HttpRequestMessage; var HttpResponse: HttpResponseMessage) var // Record that hold integration setup ExampleIntegration: Record "Example - Test Integration"; HttpClient: HttpClient; Payload: Text; begin ExampleIntegration.Get(); Payload := EDocumentHelper.TempBlobToTxt(TempBlob); // Manipulate the payload and set the headers if needed HttpRequest.Content.WriteFrom(Payload); HttpRequest.Method := 'POST'; HttpRequest.SetRequestUri(ExampleIntegration."Sending Endpoint"); HttpClient.Send(HttpRequest, HttpResponse); // Parse the response if needed. end;
SendBatch: Send a batch of e-documents to an external service.
procedure SendBatch(var EDocuments: Record "E-Document"; var TempBlob: Codeunit "Temp Blob"; var IsAsync: Boolean; var HttpRequest: HttpRequestMessage; var HttpResponse: HttpResponseMessage) var // Record that hold integration setup ExampleIntegration: Record "Example - Test Integration"; HttpClient: HttpClient; Payload: Text; begin ExampleIntegration.Get(); Payload := EDocumentHelper.TempBlobToTxt(TempBlob); // Manipulate the payload and set the headers if needed HttpRequest.Content.WriteFrom(Payload); HttpRequest.Method := 'POST'; HttpRequest.SetRequestUri(ExampleIntegration."Sending Endpoint"); HttpClient.Send(HttpRequest, HttpResponse); // Parse the response if needed. end;
GetResponse: Get the response of an asynchronous send request.
procedure GetResponse(var EDocument: Record "E-Document"; var HttpRequest: HttpRequestMessage; var HttpResponse: HttpResponseMessage): Boolean var // Record that hold integration setup ExampleIntegration: Record "Example - Test Integration"; HttpClient: HttpClient; begin ExampleIntegration.Get(); // Manipulate the payload and set the headers if needed HttpRequest.Method := 'GET'; HttpRequest.SetRequestUri(ExampleIntegration."Get Response Endpoint"); HttpClient.Send(HttpRequest, HttpResponse); // Parse the response if needed. end;
GetApproval: Check whether a document is approved or rejected.
procedure GetApproval(var EDocument: Record "E-Document"; var HttpRequest: HttpRequestMessage; var HttpResponse: HttpResponseMessage): Boolean var // Record that hold integration setup ExampleIntegration: Record "Example - Test Integration"; HttpClient: HttpClient; begin ExampleIntegration.Get(); // Manipulate the payload and set the headers if needed HttpRequest.Method := 'GET'; HttpRequest.SetRequestUri(ExampleIntegration."Get Approval Endpoint"); HttpClient.Send(HttpRequest, HttpResponse); // Parse the response if needed. end;
Cancel: Send a cancel request for an e-document.
procedure Cancel(var EDocument: Record "E-Document"; var HttpRequest: HttpRequestMessage; var HttpResponse: HttpResponseMessage): Boolean var // Record that hold integration setup ExampleIntegration: Record "Example - Test Integration"; HttpClient: HttpClient; begin ExampleIntegration.Get(); // Manipulate the payload and set the headers if needed HttpRequest.Method := 'Delete'; HttpRequest.SetRequestUri(ExampleIntegration."Cancel Endpoint"); HttpClient.Send(HttpRequest, HttpResponse); // Parse the response if needed. end;
ReceiveDocument: Receive an e-document from an external service.
procedure ReceiveDocument(var TempBlob: Codeunit "Temp Blob"; var HttpRequest: HttpRequestMessage; var HttpResponse: HttpResponseMessage) var // Record that hold integration setup ExampleIntegration: Record "Example - Test Integration"; HttpClient: HttpClient; Result: Text; begin ExampleIntegration.Get(); HttpRequest.Method := 'GET'; HttpRequest.SetRequestUri(ExampleIntegration."Receiving Endpoint"); HttpClient.Send(HttpRequest, HttpResponse); HttpResponse.Content.ReadAs(Result); WriteToTempBlob(TempBlob, Result); end;
GetDocumentCountInBatch: Define how many received documents are in a batch import.
procedure GetDocumentCountInBatch(var TempBlob: Codeunit "Temp Blob"): Integer begin // Parse the TempBlob to find how many documents in the batch. exit(1); end;
GetIntegrationSetup: Define the integration setup of a service.
procedure GetIntegrationSetup(var SetupPage: Integer; var SetupTable: Integer) begin SetupPage := page::"Example - Test Integration"; SetupTable := Database::"Example - Test Integration"; end;
Step 4 - Implement the setup wizard
Create a setup wizard that guides users through the process of configuring e-documents by gathering all details that are required for a seamless integration with the service.
- Create the wizard page. This first page should introduce the feature.
- Create a second page that includes integration setup information, such as URL endpoints, user names and passwords, certificates, and schema URIs.
- On the third page, include setup information for the sending profiles.
- If your service will submit documents to the endpoint, use the next page to get consent from the user and enable HTTP outgoing calls for the document core extension and your localization.
Here's an example of a wizard.
page 6138 "Edocument Setup Wizard"
{
PageType = NavigatePage;
ApplicationArea = All;
Caption = 'E-Document setup wizard';
layout
{
area(Content)
{
group(MediaStandard)
{
Caption = '';
Editable = false;
Visible = TopBannerVisible;
field("MediaResourcesStandard Media Reference"; MediaResourcesStandard."Media Reference")
{
ApplicationArea = All;
Editable = false;
ShowCaption = false;
}
}
group(FirstPage)
{
Caption = '';
Visible = FirstStepVisible;
group("IntroductionGroup")
{
Caption = 'Welcome to Edocument service';
Visible = FirstStepVisible;
group(LearnMoreLinkGroup)
{
Caption = '';
field(CanLearnMore; YouCanLearnMoreTxt)
{
ApplicationArea = Basic, Suite;
ShowCaption = false;
Editable = false;
MultiLine = true;
}
}
}
}
group(SetupService)
{
Caption = '';
Visible = SetupServiceStepVisible;
field("Service Name"; EdocFormat.Code)
{
ApplicationArea = Basic, Suite;
Caption = 'Service Name';
}
field(Description; EdocFormat.Description)
{
ApplicationArea = Basic, Suite;
}
field("Document Format"; EdocFormat."Document Format")
{
ApplicationArea = Basic, Suite;
}
field("Service Integration"; EdocFormat."Service Integration")
{
ApplicationArea = Basic, Suite;
}
}
group(SetupSendingProfiles)
{
Caption = '';
Visible = SetupSendingProfilesStepVisible;
field(UseWithDefaultDocSendingProfile; UseWithDefaultDocSendingProfile)
{
ApplicationArea = Basic, Suite;
Caption = 'Use with default Sending Profile';
}
}
group(FinalPage)
{
Visible = FinalStepVisible;
group("That's it!")
{
Caption = 'That''s it!';
group(ChooseFinishGroup)
{
Caption = '';
Visible = true;
field(ChooseFinish; ChooseFinishTxt)
{
ApplicationArea = Basic, Suite;
ShowCaption = false;
Editable = false;
MultiLine = true;
}
}
}
}
}
}
actions
{
area(processing)
{
action(ActionBack)
{
ApplicationArea = Basic, Suite;
Caption = 'Back';
Enabled = BackActionEnabled;
Visible = BackActionEnabled;
Image = PreviousRecord;
InFooterBar = true;
trigger OnAction()
begin
NextStep(true);
end;
}
action(ActionNext)
{
ApplicationArea = Basic, Suite;
Caption = 'Next';
Enabled = NextActionEnabled;
Image = NextRecord;
InFooterBar = true;
trigger OnAction()
begin
NextStep(false);
end;
}
action(ActionFinishAndEnable)
{
ApplicationArea = Basic, Suite;
Caption = 'Finish';
Enabled = FinishActionEnabled;
Image = Approve;
InFooterBar = true;
trigger OnAction()
begin
FinishAndEnableAction();
end;
}
}
}
trigger OnInit()
begin
LoadTopBanners();
end;
trigger OnOpenPage()
begin
Step := Step::Start;
EnableControls();
end;
local procedure FinishAndEnableAction()
var
EDocumentHelper: codeunit "E-Document Helper";
begin
// Insert E-Document Services
// Insert Document Sending Profile
// Insert WorkFlows
// You can find detailed examples in codeunit "E-Document Workflow Setup"
// Enable EDocument Core extension to send http calls after getting user's consent
EDocumentHelper.AllowEDocumentCoreHttpCalls();
// Setup retention policy if needed
end;
local procedure LoadTopBanners()
begin
if MediaRepositoryStandard.Get('AssistedSetup-NoText-400px.png', Format(ClientTypeManagement.GetCurrentClientType())) and
MediaRepositoryDone.Get('AssistedSetupDone-NoText-400px.png', Format(ClientTypeManagement.GetCurrentClientType()))
then
if MediaResourcesStandard.Get(MediaRepositoryStandard."Media Resources Ref") and
MediaResourcesDone.Get(MediaRepositoryDone."Media Resources Ref")
then
TopBannerVisible := MediaResourcesDone."Media Reference".HasValue;
end;
local procedure NextStep(Backwards: Boolean)
begin
if Backwards then
Step -= 1
else
Step += 1;
EnableControls();
end;
local procedure EnableControls()
begin
ResetControls();
case Step of
Step::Start:
ShowStartStep();
Step::SetupService:
ShowSetupServiceStep();
Step::SetupSendingProfiles:
ShowSetupSendingProfilesStep();
Step::Finish:
ShowFinalStep();
END;
end;
local procedure ShowStartStep()
begin
FirstStepVisible := true;
BackActionEnabled := false;
end;
local procedure ShowSetupServiceStep()
begin
FirstStepVisible := false;
SetupServiceStepVisible := true;
SetupSendingProfilesStepVisible := false;
FinalStepVisible := false;
BackActionEnabled := true;
end;
local procedure ShowSetupSendingProfilesStep()
begin
FirstStepVisible := false;
SetupServiceStepVisible := false;
SetupSendingProfilesStepVisible := true;
FinalStepVisible := false;
BackActionEnabled := true;
end;
local procedure ShowFinalStep()
begin
FirstStepVisible := false;
SetupServiceStepVisible := false;
SetupSendingProfilesStepVisible := false;
FinalStepVisible := true;
FinishActionEnabled := true;
NextActionEnabled := false;
BackActionEnabled := true;
end;
local procedure ResetControls()
begin
BackActionEnabled := true;
NextActionEnabled := true;
FirstStepVisible := false;
SetupServiceStepVisible := false;
SetupSendingProfilesStepVisible := false;
FinalStepVisible := false;
end;
var
MediaRepositoryStandard: Record "Media Repository";
MediaRepositoryDone: Record "Media Repository";
MediaResourcesStandard: Record "Media Resources";
MediaResourcesDone: Record "Media Resources";
EdocFormat: Record "E-Document Service" temporary;
ClientTypeManagement: Codeunit "Client Type Management";
Step: Option Start,SetupService,SetupSendingProfiles,Finish;
TopBannerVisible: Boolean;
BackActionEnabled, NextActionEnabled, FinishActionEnabled : Boolean;
FirstStepVisible, SetupServiceStepVisible, SetupSendingProfilesStepVisible, FinalStepVisible : Boolean;
UseWithDefaultDocSendingProfile: Boolean;
YouCanLearnMoreTxt: Label 'This wizard helps you to setup a connection to an electronic invoicing setup.';
ChooseFinishTxt: Label 'Click ''Finish'' to insert the service connection. \You will still have to setup the settings for the endpoint and import/export mapping.';
}
Helper procedures
The following set of E-Document Helper codeunits is available. These codeunits consist of collections of utility methods that we highly recommend you use to build your localization app. These methods can help you with various tasks. For example, they can help you effortlessly log any error messages that are encountered.
- "E-Document Helper"
- "E-Document Error Helper"
- "E-Document Import Helper"
procedure Create(EDocumentService: Record "E-Document Service"; var EDocument: Record "E-Document"; var SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; var TempBlob: Codeunit "Temp Blob")
var
EDocumentHelper: Codeunit "E-Document Helper";
OutStr: OutStream;
begin
TempBlob.CreateOutStream(OutStr);
case EDocument."Document Type" of
EDocument."Document Type"::"Sales Invoice":
if not GenerateInvoiceXMLFile(SourceDocumentHeader, OutStr) then
EDocumeneHelper.LogSimpleErrorMessage(EDocument, 'Error <> happened while creating this document');
EDocument."Document Type"::"Sales Credit Memo":
if not GenerateCrMemoXMLFile(SourceDocumentHeader, OutStr) then
EDocumeneHelper.LogSimpleErrorMessage(EDocument, 'Error <> happened while creating this document');
end;
end;
Missing features
Do you believe that there are any essential features that could make the development of an e-document solution easier? If so, create the idea on aka.ms/BCIdeas, or start the article on aka.ms/BCYammer, and we'll get back to you.
See also
How to set up e-documents in Business Central
How to use e-documents in Business Central
Work with Business Central