The Docentric Emailing Multiple Invoices (DEMI) solution has been greatly improved comparing to the solution described in this article. Learn more >>
If you need to send out a single email to every customer each month, and this email needs to have all the customer-belonging invoices for the current billing period attached, check out this article that will describe such a solution for Dynamics 365 for Finance and Operations.
You can also download and use the described solution as is, or you can adjust it to your needs.
The requirements
Let’s say that you have to post each of sales orders separately, which means that one sales order always produces exactly one invoice. This is not a problem by itself - you need to turn off the Summary order functionality, by selecting None for Summary update for on the Posting invoice form. The challenge that may arise is how to distribute all the created invoices at the end of the month to each customer.
Let’s assume that the requirement is the following: sending a single email at the end of each month (i.e. for the current billing period) to every customer that one or more sales orders (i.e. invoices) are created for in this month. So, a customer should receive a single email each month, with all his invoices attached. Such email needs to have a nicely designed body with information from the current billing period.
On the other hand, if your goal is to email multiple sales orders of the same customer posted as a single invoice (so called Summary invoice), this is the standard functionality provided OOTB. Learn more >>
The solution
The solution described here consists of two phases:
- Post each sales order as a separate invoice and print it to Azure blob storage, using the Docentric File print destination. Store the location of the printed output file in Invoice journal. Store also information about the current billing period.
- Fetch all printed invoice documents for the selected billing period for each of the customers and email them in the single email, using the selected Email template from Organization administration -> Setup -> Email templates.
The both steps can be run in batch.
Extending Invoice journal
We will extend the CustInvoiceJour table (and the Invoice journal form) with the following fields:
In the Print management setup for the Customer invoice report we will select the Docentric File print destination, and set up Saving to Azure blob storage settings as shown on the pictures below. Docentric print destinations are shipped with Docentric AX Free Edition that is completely free of charge, and you can download and use it right away.
The next step is to introduce an event handler DocReportRunDelegates_reportExecutionEnd for the reportExecutionEnd delegate defined on the DocReportRunDelegates class, which is invoked each time when printing a report to Docentric File print destination completes. In this method, only for invoices, we will store Azure blob path of the printed invoice document and the billing period inferred from the printing invoice date to the corresponding CustInvoiceJour record.
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
class DocSalesInvoiceReportExecutionEndHandler { [SubscribesTo(classStr(DocReportRunDelegates), delegateStr(DocReportRunDelegates, reportExecutionEnd))] public static void DocReportRunDelegates_reportExecutionEnd(DocReportExecutionInfo _docReportExecutionInfo, DocEventHandlerResult _result) { // Fill the data only if the File print destination is used. DocPrintReportSettings printReportSettings = _docReportExecutionInfo.parmPrintReportSettings(); DocPrintDestination targetDestination = printReportSettings.parmPrintDestination(); if (targetDestination != DocPrintDestination::File) { return; } DocPrintDestSettingsFile filePrintDestSettings = _docReportExecutionInfo.parmFilePrintDestSettings(); DocPrintReportToFileExecutionInfo filePrintDestExecutionInfo = _docReportExecutionInfo.parmPrintToFileExecutionInfo(); // Fill the data only if output file is saved to Azure blob storage successfully. if (!filePrintDestSettings.parmSaveToAzureBlobStorage()) { return; } if (!filePrintDestExecutionInfo.parmSaveToAzureBlobStorageSuccess()) { return; } // Fill the data only for SalesInvoice and FreeTextInvoice reports. str reportId = printReportSettings.parmReportId(); RecId custInvoiceJourRecId; if (reportId == ssrsReportStr(SalesInvoice, Report)) { SalesInvoiceContract contract = printReportSettings.parmSrsReportContract().parmRdpContract(); custInvoiceJourRecId = contract.parmRecordId(); } else if (reportId == ssrsReportStr(FreeTextInvoice, Report)) { FreeTextInvoiceContract contract = printReportSettings.parmSrsReportContract().parmRdpContract(); custInvoiceJourRecId = contract.parmCustInvoiceJourRecId(); } else { return; } if (custInvoiceJourRecId == 0) { return; } // Don't fill data for proforma scenario. // FreeTextInvoice proforma: custInvoiceJour record for custInvoiceJourRecId exists and custInvoiceJour.IsProForma = true // SalesInvoice proforma: custInvoiceJour record for custInvoiceJourRecId does not exist CustInvoiceJour custInvoiceJour = CustInvoiceJour::findRecId(custInvoiceJourRecId); if (custInvoiceJour.RecId == 0 || custInvoiceJour.Proforma == NoYes::Yes) { return; } // Calculate billing period. int billingMonth; int billingYear; if (custInvoiceJour.InvoiceDate) { billingMonth = mthOfYr(custInvoiceJour.InvoiceDate); billingYear = year(custInvoiceJour.InvoiceDate); } else { date currentDate = DateTimeUtil::date( DateTimeUtil::applyTimeZoneOffset(DateTimeUtil::getSystemDateTime(), DateTimeUtil::getUserPreferredTimeZone())); billingMonth = mthOfYr(currentDate); billingYear = year(currentDate); } // Fill and save the data. ttsbegin; custInvoiceJour.selectForUpdate(true); custInvoiceJour.PrintedToFile_DC = true; custInvoiceJour.OutputFilename_DC = filePrintDestExecutionInfo.parmReportOutputFilename(); custInvoiceJour.AzureContainerName_DC = filePrintDestSettings.parmAzureBsContainerName(); custInvoiceJour.AzureBlobName_DC = filePrintDestExecutionInfo.parmSaveToAzureBsBlobName(); custInvoiceJour.BillingMonth_DC = System.String::Format("{0:00}", billingMonth); custInvoiceJour.BillingYear_DC = int2Str(billingYear); custInvoiceJour.doUpdate(); ttscommit; } } |
You can also use Microsoft's Azure storage explorer to browse and manage these printed invoice files if needed.
Post and print sales orders
So, when we post and print a sales order, the extended fields on CustInvoiceJour table get filled as you can see on the pictures below. Also, if you have already posted some sales orders with the different target print destination settings, you can invoke the printing of the corresponding invoices again, by using the Print management from the Invoices journal form, in order to get the new DocReportRunDelegates_reportExecutionEnd event handler executed.
After an invoice document is printed and store on Azure blob storage, you can also download it by clicking the new Download button on the Invoice journal form.
Creating the email template for emailing multiple invoices
In order to send an arbitrary email using a predefined email subject and body, we will use Organization administration -> Setup -> Email templates.
Docentric AX Free Edition for Dynamics 365 for Finance and Operations also improves this Email templates functionality so you can edit email templates using a superior HTML editor with custom placeholders. We will first create an email template handler class to define some custom placeholders (e.g. customer contact name, customer account ID, billing period) we are going to use in our emails when emailing customer invoices.
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
class DocEmailInvoicesHandler extends DocEmailTemplateHandlerBase { // Placeholders. public const str PlaceholderCustomerName = 'CustName'; public const str PlaceholderCustomerContactName = 'CustContactName'; public const str PlaceholderCustomerAccountNum = 'CustAccountNum'; public const str PlaceholderBillingPeriodYear = 'BillingYear'; public const str PlaceholderBillingPeriodMonth = 'BillingMonth'; public const str PlaceholderBillingPeriodDateFrom = 'BillingDateFrom'; public const str PlaceholderBillingPeriodDateTo = 'BillingDateTo'; public ClassDescription description() { return 'Emailing multiple invoices email handler class'; } /**********************************************************************************/ /* Defining Custom Placeholders */ /**********************************************************************************/ protected void defineCustomPlaceholders(Map _customPlaceholderDefinitionMap) { // Placeholders for sales order summary (i.e. header/footer) _customPlaceholderDefinitionMap.insert(DocEmailInvoicesHandler::PlaceholderCustomerName, 'EMI - Customer name'); _customPlaceholderDefinitionMap.insert(DocEmailInvoicesHandler::PlaceholderCustomerContactName, 'EMI - Contact name'); _customPlaceholderDefinitionMap.insert(DocEmailInvoicesHandler::PlaceholderCustomerAccountNum, 'EMI - Account number'); _customPlaceholderDefinitionMap.insert(DocEmailInvoicesHandler::PlaceholderBillingPeriodMonth, 'EMI - Billing Month'); _customPlaceholderDefinitionMap.insert(DocEmailInvoicesHandler::PlaceholderBillingPeriodDateFrom, 'EMI - Billing From'); _customPlaceholderDefinitionMap.insert(DocEmailInvoicesHandler::PlaceholderBillingPeriodDateTo, 'EMI - Billing To'); } /**********************************************************************************/ /* Supplying values for Custom Placeholders */ /**********************************************************************************/ public static Map createAndFillMappingsFromOutside(CustTable _custTable, str _billingYear, str _billingMonth, LanguageId _languageId = currentUserLanguage()) { Map mappings = new Map(Types::String, Types::String); // Map(PlaceholderName (str) -> PlaceholderValue (str)) mappings.insert(DocEmailInvoicesHandler::PlaceholderCustomerName, _custTable.name()); mappings.insert(DocEmailInvoicesHandler::PlaceholderCustomerAccountNum, _custTable.AccountNum); str custContactPersonName = ContactPerson::name(_custTable.ContactPersonId); if (!custContactPersonName) { custContactPersonName = 'Sir/Madam'; } mappings.insert(DocEmailInvoicesHandler::PlaceholderCustomerContactName, custContactPersonName); mappings.insert(DocEmailInvoicesHandler::PlaceholderBillingPeriodMonth, _billingYear); mappings.insert(DocEmailInvoicesHandler::PlaceholderBillingPeriodYear, _billingMonth); date billingDateFrom = mkDate(1, str2Int(_billingMonth), str2Int(_billingYear)); date billingDateTo = mkDate(dayOfMth(endmth(billingDateFrom)), str2Int(_billingMonth), str2Int(_billingYear)); str billingDateFromStr = DocEmailInvoicesHandler::formatDatetimeData(billingDateFrom, _languageId); str billingDateToStr = DocEmailInvoicesHandler::formatDatetimeData(billingDateTo, _languageId); mappings.insert(DocEmailInvoicesHandler::PlaceholderBillingPeriodDateFrom, billingDateFromStr); mappings.insert(DocEmailInvoicesHandler::PlaceholderBillingPeriodDateTo, billingDateToStr); return mappings; } } |
Monthly emailing multiple invoices in the single email
This is the second phase of the solution. We have our monthly invoices posted and printed to Azure blob storage, and now we are going to create a functionality which will email them in the single email for each customer.
For that purpose we will create a dialog to select some parameters such as billing period, customer contact purpose or email recipients, etc. We will also include a combo box to select the MultiInv email template we have created in the previous step.
Note that this dialog uses the SysLastValue framework to store user choice for the form parameters so he or she doesn’t have to fill them each time the form is open.
Learn how to use the SysLastValue framework with this form >>
Email invoices in batch
Also, there is an option to run the whole operation in batch. You will notice a disabled checkbox Batch processing on the Email multiple invoices form, which is a signal whether the emailing invoices will be performed in batch when you click the OK button. You can turn it on or off on the Batch processing dialog that is shown when the Batch button is clicked. On this form you can schedule and set up a batch job for emailing invoices.
Learn in detail on how to add the Batch button to this form >>
Sending emails with invoices
When the OK button is clicked the DocEmailInvoicesManager::sendInvoices() method is called which first filters all CustInvoiceJour records by the selected billing period and groups them by customers. Afterwards, for each customer it fetches all the printed invoices stored on Azure blob storage and sends them as the attachments of the single email. The email is sent to all email addresses which have been resolved from the input parameters such as Purpose, Use primary contact or Send to.
Read how customer’s contact purpose and primary email can be resolved >>
After an email is sent, the information such as Sent via Email (NoYes::Yes), Email sent on (a timestamp), Sent to (resolved To email addresses), etc. are stored in each of the corresponding CustInvoiceJour records. In case that an email failed to be sent, Emailing error message is written to the corresponding CustInvoiceJour records.
Take a look at the method (a code snippet) for sending an email to the provided customer, with the attached invoices that are fetched from multiple CustInvoiceJour records selected by this customer and the provided billing period.
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 |
public static int sendEmailForCustomer(str _emailingInvoicesID, str _billingYear, str _billingMonth, str _purpose, boolean _usePrimaryContact, str _toEmailList, str _emailId, str _custAccountNum, boolean _sendAlreadySentInvoices) { ... CustInvoiceJour custInvoiceJour; CustTable custTable = CustTable::find(_custAccountNum); Map reportsToSend = new Map(Types::String, Types::Container); // Collect all invoices for the selected customer and the selected billing period. while select custInvoiceJour where custInvoiceJour.PrintedToFile_DC == true && (_sendAlreadySentInvoices == true || custInvoiceJour.SentViaEmail_DC == false) && custInvoiceJour.BillingMonth_DC == _billingMonth && custInvoiceJour.BillingYear_DC == _billingYear && custInvoiceJour.InvoiceAccount == _custAccountNum { str azureContainer = custInvoiceJour.AzureContainerName_DC; str azureBlob = custInvoiceJour.AzureBlobName_DC; container reportContent; using (System.IO.MemoryStream outputFileContent = DocAzureBlobHelper::getBlobContent(azureContainer, azureBlob)) { reportContent = DocGlobalHelper::convertMemoryStreamToContainer(outputFileContent); } reportsToSend.insert(custInvoiceJour.OutputFilename_DC, reportContent); } ... int invoicesToSendCount = reportsToSend.elements(); // Send the email with the collected invoices. errorMsg = DocEmailInvoicesManager::sendEmail(_billingYear, _billingMonth, resolvedToEmailList, _emailId, custTable, languageId, reportsToSend); if (errorMsg != '') { errorMsg = strFmt('Failed to send the email with the %1 invoices. Reason: %2', invoicesToSendCount, errorMsg); invoicesToSendCount = -1; } return invoicesToSendCount; } |
Solution improvements
There are several options to improve this solution. For example, if you have on-premises environments, you may prefer to use Docentric File print destination to save invoices to File system instead of Azure Blob storage. You can even use placeholders to create smartly named folders and sub-folders for each billing period and/or customer, in order to have nicely organized printed invoices on your shared network folders.
You can also add the Cc and Bcc fields for sending emails, and use Number sequence with the Emailing invoices ID parameter.
What happened in the meantime
This article describes the first version of the solution for sending multiple customer invoices in a single email, which are selected by Billing period. It was based on the RunBaseBatch framework and the logic that 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 selected also by Invoice date. This solution was based on the SysOperation framework, allowing also Email distributor batch to be used for emailing. See the related 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 >>