The Docentric Emailing Multiple Invoices (DEMI) solution has been greatly improved comparing to the solution described in this article. Learn more >>
Does it happen in your type of business that some customers have multiple sales orders in short period of time, for example in one day? During the D365FO batch invoicing, if invoices are sent via email, such customers would get multiple emails in the same day. They will probably ask you to send them only one email with all invoices. This article describes a solution to this situation, emailing multiple invoices in one email from D365FO.
A while ago we published an article about the solution to the similar requirement: sending one email with all invoices generated for a customer during a certain billing period (let’s call it "billing-period" solution). Article and downloadable solution were welcomed by our users, whose feedback gave us an insight to their real-life requirements, such as the one covered in the article you are reading ("non-billing-period" solution).
You can download the solution that supports both the requirements from this and the previous article. Feel free to use it as is or adjust it to your needs.
Business process description
We want to base the solution on the standard D365FO invoicing, let D365FO post and generate the invoices, which we will store on a known location and later execute a separate procedure that will collect and send them in the single email.
We can roughly divide the process into two steps:
Step 1: Post and print the selected sales orders to Azure Blob storage.
- The step is performed as the standard D365FO batch or interactive invoicing.
- Docentric File print destination is used for printing. It is configured to save each generated invoice as a separate file on Azure Blob storage. This is a standard feature of Docentric AX Free Edition, and no customization is required.
- Location of the printed output file on Azure Blob storage is automatically stored on the Invoice journal. This feature has been implemented in the "billing-period" solution, as an extension of standard invoice posting. We will re-use it for the "non-billing-period" solution.
Step 2: Collect and email the printed invoices.
- The step is performed as batch or interactive procedure, which is implemented as part of this, "non-billing-period" solution.
- The procedure fetches all files to be sent for the selected customers. This is different from the "billing-period" solution, where we fetch the files based on the invoice date that matches the selected billing period.
- Finally, procedure sends the fetched files in a single email to each customer.
Reused elements of the "billing-period" solution
As written above, the first step, posting and printing, is the same in both "billing-period" and "non-billing-period" solution. It is explained in detail in the previous article, but let’s repeat the most important points.
Invoice journal extension
We extended the CustInvoiceJour table and Invoice journal form with fields that allow us to save the information about the posted document location and to track if it was emailed.
DocReportRunDelegates_reportExecutionEnd event handler
We implemented an event handler that is invoked when the printing of a report to Docentric File print destination is completed. In this event handler, if the report being executed is invoice, we store the information about the Azure blob path of the printed invoice document. You can check the previous article for more details about the implementation.
Docentric File print destination setup
Print management for Sales Invoice report should be configured as Docentric File print destination. Besides the mandatory information for Output filename and format, we also enable the Save to Azure storage option and enter the values for Blob container and Blob path:
While posting, we will print invoices by using Print management. The above settings will be applied, and the printed file will be saved on Azure Blob storage based on the configured Azure blob path (Blob container + Blob path + File name). The exact path will be saved on the invoice journal and will be used in the emailing step.
Email body editor and placeholders
If you are already using Docentric AX Free or Full Edition, you are probably familiar with the feature-rich emailing capabilities it offers, such as Docentric Email print destination with the email body editor and placeholders, as well as email processing, unresolved email tokens handling, etc. You can seamlessly use them when printing a report to Docentric Email print destination, but for the solution described in this article we are developing a custom emailing procedure. Therefore, our improved emailing won’t work OOTB, so we need to additionally support the emailing features of our choice.
Email body editor and some placeholders are a must-have features, therefore we will support them by first implementing the DocEmailInvoicesHandler class with a few custom placeholders and then by using the Docentric improved Email templates where we will point to the previously implemented class.
That was an overview of the reused features from the "billing-period" solution. Let’s move on to what is new with the "non-billing-period" solution.
Emailing the multiple invoices for selected customers in a single mail
Main use-case for this solution is:
- For the selected customers periodically send one email message with all invoices created since the previous email was sent.
Additional requirements are:
- Ability to resend the already sent invoices.
- Selection how the multiple invoices will be attached to the email: as separate files, as a .zip or as a merged PDF file.
- Option to send through email processing, i.e. Batch email sending status.
- Possibility to select the email template for the email message.
- Ability to use the email tokens or specific email address for the email recipient.
- Possibility to execute interactively or in batch.
This is the dialog where these options can be specified:
The above dialog offers a selectable query for specifying the customers. For each customer returned from the query a procedure will:
- Fetch all invoice documents that are stored on Azure Blob storage and have not been sent (unless Send already sent invoices flag is set).
- Create one email message based on the specified email template and email recipient address. Resolve the placeholders if they are used in the email template body.
- Add the fetched invoice files as separate or merged/zipped file.
- Send it immediately or store it in the Batch email sending status table, leaving to Email distributor batch to handle the outgoing emails.
- Save the success or error information on the invoice journal.
The procedure can be started from the Invoice journal form > Invoice > Emailing invoices > Email without billing period (see image below) or from the Accounts receivable > Periodic tasks > Email multiple invoices without billing period menu item.
Implementation
The solution is implemented through SysOperation Framework. It is a well-documented D365FO framework, so we won’t bother you with all its implementation details. The important stuff happens in the service class DocEmailInvoicesService, in its processOperation() method, where for each customer returned from the query we call the sendCustomerEmail() method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public void processOperation(DocEmailInvoicesContract _contract) { // Input any business process logic that needs to be scheduled in batch. // Use this service to loop through the selection of query or a controller overide // and call the distributor. Distributor is only responsable for sending emails to customer, // it isn't linked to contract or customer. This allows for the future flexibility // to split emailing into batch per customer for big loads. // Initiate dedicated clas for performing the operation. DocEmailCustInvoicesDistributor distributor = DocEmailCustInvoicesDistributor::constructFromContract(_contract); CustTable custTable; // Generate QueryRunObject based on selection criteria . QueryRun queryRun = new QueryRun(_contract.getQuery()); // Loop trough the query and send emails. while (queryRun.next()) { custTable = queryRun.get(tableNum(CustTable)); // Call distributor action. // In multi thread scenario we would initiate a different object and split it. distributor.sendCustomerEmail(custTable); } } |
The sendCustomerEmail() method encapsulates the whole logic for one customer. The snippet below gives a high-level overview of that logic: it first resolves the recipient email addresses, then it prepares and sends the email message and finally updates the invoice journal with the success/error status:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
str errorMsg; // Resolve the To email addresses. str resolvedToEmailList; boolean isError; // Call to framework helper class to get the emailing list [resolvedToEmailList, isError] = DocEmailInvoicesManager::resolveToEmailList(docContactPurpose, docUsePrimaryContact, docToEmailList, _custTable); if (isError) { errorMsg = strFmt("@DocEmailMultiInv:DocEmaiFailed", invoicesToSendCount, resolvedToEmailList); invoicesToSendCount = -1; } else { // Send the email with the collected invoices. errorMsg = DocEmailInvoicesManager::sendEmailAttachmentTypeNoBilling(docAttachInvoice, resolvedToEmailList, docEmailIdTemplate, _custTable, languageId, reportsToSend, docEmailProcessing); if (errorMsg != '') { errorMsg = strFmt("@DocEmailMultiInv:DocEmaiFailed", invoicesToSendCount, errorMsg); invoicesToSendCount = -1; } } // Update all involved CustInvoiceJour records. this.updateCustInvoiceJourRecords(_custTable, resolvedToEmailList, errorMsg); |
Preparing and sending the email message happens in the DocEmailInvoicesManager::sendEmailAttachmentTypeNoBilling() method. Its first interesting part is combining all invoices for one customer, based on the option user selected for the attachments (separate files, .zip file or merged PDF file):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// logic to combine the found invoices, based on the selected option for the attachment // (separate files, .zip, merged PDF) switch (_docAttachInvoice) { case DocAttachInvoice::SeparateFiles: while (reportsToSendEnum.moveNext()) { if (firstAttachmentName == '') { firstAttachmentName = reportsToSendEnum.currentKey(); firstAttachmentContent = reportsToSendEnum.currentValue(); } else { str attachmentName = reportsToSendEnum.currentKey(); container attachmentContent = reportsToSendEnum.currentValue(); additionalAttachments = additionalAttachments + [[attachmentName, attachmentContent]]; } } if (firstAttachmentName == '' || firstAttachmentContent == conNull()) { return strFmt("@DocEmailMultiInv:DocInvoiceNotFound", _custTable.AccountNum); } break; case DocAttachInvoice::MergedPDF: // Merged PDF firstAttachmentName = 'MergedInvoices.pdf'; firstAttachmentContent = DocEmailFilesHelper::downloadMergedPDF(_reportsToSend); break; case DocAttachInvoice::ZipFile: firstAttachmentName = 'ZipInvoices.zip'; firstAttachmentContent = DocEmailFilesHelper::downloadZipAttachments(_reportsToSend); break; } |
Next, if user selected sending through email processing, then a docEmailProcessingParams object should be populated (it will be given as a parameter to the actual email sending method DocEmailTemplateManager::sendMail()):
1 2 3 4 5 6 7 8 9 10 11 12 |
// Set how email is sent, through email processing or in a synchronous way. // In case of email processing, we need to set the context. DocEmailProcessingParams docEmailProcessingParams = null; if (_docEmailProcessing == NoYes::Yes) { // Send to the system queue, docEmailProcessingParams is needed docEmailProcessingParams = new DocEmailProcessingParams(); docEmailProcessingParams.contextInformation('Email multiple invoices'); docEmailProcessingParams.documentId('Multiple'); docEmailProcessingParams.accountType(DocAccountRole::Customer); docEmailProcessingParams.accountNum(_custTable.AccountNum); } |
Finally, the email message is created based on the selected email template (_emailId parameter) and sent:
1 2 3 4 5 6 |
try { // Send email based on template with the given emailId. Set the sending options (Email batch table or standard) DocEmailTemplateManager::sendMail(_emailId, _languageId, _toEmailList, placeholderMappings, null, conNull(), '', '', firstAttachmentName, firstAttachmentContent, additionalAttachments, null, _docEmailProcessing, docEmailProcessingParams); } |
Resulting email
Below is the resulting email for one customer. It is stored in the Batch email sending status form, because the Use email processing option was selected:
With Docentric AX Free or Full version you can Download message directly from this form and inspect it:
Ideas for improvements
- The DocEmailInvoicesHandler class could support more placeholders, for example a list of all invoice IDs, sum of all invoice amounts, first due date, etc.
- Query could be modified to support the selection of invoices (for example, based on Due date, Amount, Invoice Id, etc.).
- The Cc and Bcc fields can be introduced. Currently, only the To field is supported.
What happened in the meantime
The first version of the solution for sending multiple customer invoices in a single email, which are selected by Billing period, was based on the RunBaseBatch framework and it’s explained in the previous article. The solution collects invoice documents from Azure storage, stored there during posting & printing via Docentric File print destination. The collected invoices are then sent to the customer via single outgoing email based on the selected Organization email template.
Wishing to achieve greater flexibility, we extended the solution to enable emailing multiple invoices also selected by Invoice date. This solution was based on the SysOperation framework, allowing also Email distributor batch to be used for emailing, and it’s described in this article.
We called this solution Docentric Emailing Multiple Invoices - DEMI Version 3.0.
Learn about DEMI Version 3.0 >>
You can download DEMI Version 3.0 below. Please note that we do not provide download of the previous versions.
Resources for DEMI Version 3.0
Download the solution project >>
Download the solution model >>