In the previous article, Are you ready to collect more of your money?, we described how we can improve the dunning process, but now we want to focus on the technical part.
The article shows you how you can add the functionality of printing and sending a single collection letter, then adding a cherry on top by using the same solution for automation of the dunning process with mass printing and sending of Collection letters.
Printing and sending a single collection letter
In this solution, we assume that the invoices are already created in PDF format and stored in the journal tables or Print archive.
Before we dig into the technical part, let us see how the ending result of the solution will look like.
To start, please download this sample POC project first. Use Import an .axpp file in Visual Studio with the Finance and Operations (Dynamics 365) extension installed, as described here.
When you import the project into Visual Studio, the following tree structure appears in Solution Explorer.
Now let’s get started with a step by step tutorial.
Ten simple steps to create a working solution
We have built the POC project out of 10 simple steps. In the article, we are focusing on the CustCollectionJour related objects in AOT, to understand how things are running under the hood.
In the article, we explain how to create the POC project out of ten simple steps:
- Step 1: Enable the new functionality with a new subclass controller.
- Step 2: Create a menu item to extend the forms.
- Step 3: Extend forms with new menu item button.
- Step 4: Define new data types for options used on the extended dialog.
- Step 5: Create a new extension of the contract with new parameters.
- Step 6: Add new options to the print dialog (UI Builder).
- Step 7: Create a new DSP class with logic for the Email and other print destinations.
- Step 8: Register new DSP class to Docentric report setup.
- Step 9: Define & design how outgoing e-mails will look like.
- Step 10: Try the solution.
Learn more about how to install Docentric here.
Step 1: Enable the new functionality with a new subclass controller
When extending the existing UI functionality, we need to know in which cases we should do this (showing and hiding of extended print destination options). That is why we need to create a new subclass controller and implement the class static constructor and the static main() method. In this case, we can then check if the call has come from this controller.
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 |
/// <summary> /// The <c>DocCustCollectionJourController</c> class is the subclass of <c>CustCollectionJourController</c> for the /// <c>CustCollectionJour</c> SSRS report. It introduces additional report parametes and functionality for attaching overdue invoices. /// </summary> /// <remarks> /// Run this report from: /// <see href="https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=usmf&mi=CustCollectionLetterJournal">Collection letter journal</see> /// <see href="https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=usmf&mi=CustCollectionLetterNote">Review and process collection letters</see> /// <see href="https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=usmf&mi=Output%3ADocCustReport_collectionLetterAllWithAttachments">Collection letter report with attached invoices</see> /// </remarks> public class DocCustCollectionJourController extends CustCollectionJourController { /// <summary> /// The constructor of <c>DocCustCollectionJourController</c> class. /// </summary> /// <returns>An instance of <c>DocCustCollectionJourController</c></returns> public static DocCustCollectionJourController construct() { return new DocCustCollectionJourController(); } /// <summary> /// Main method of <c>DocCustCollectionJourController</c> class. /// </summary> /// <param name = "_args">Arguments passed to controller class.</param> public static void main(Args _args) { DocCustCollectionJourController controller = DocCustCollectionJourController::construct(); controller.parmReportName(PrintMgmtDocType::construct(PrintMgmtDocumentType::CustCollectionLetter).getDefaultReportFormat()); controller.parmArgs(_args); controller.startOperation(); } } |
Step 2: Create a menu item to extend the forms
Before we add a new menu item button to the UI form, we need to create an output menu item. We used the menu item for running the created controller subclass DocCustCollectionJourController described in Step 1.
Step 3: Extend forms with new menu item button
Now we are ready to develop form extensions. As the collection letter printing process is accessible from two forms (CustCollectionLetterJournal and CustCollectionLetterNote), we need to extend them both.
The important thing is to assign newly created menu item buttons to the form data source CustCollectionLetterJour and the correct output menu item DocPrintoutCollectionLetterWithAttachments, as you can see on the properties panel bellow.
Step 4: Define new data types for options used on the extended dialog
As we needed to control the printing and e-mail sending pipeline, we had to define a few new options. In our case, attaching overdue invoices, the source location of already created PDF documents, and error handling if source invoices are missing, a few new data types were added to the project.
Step 5: Create a new extension of the contract with new parameters
To be able to pass new print options to the Docentric pipeline and control what happens happen with the overdue invoice attachments, we need to create an extension of the existing contract class by using the ExtensionOf attribute and add additional parameters to it.
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 |
/// <summary> /// Extension class CustCollectionJourContract. /// NOTE: This is possible from January 2019 in so-called “4th extension wave” Platform update 23. /// SEE: https://community.dynamics.com/365/financeandoperations/b/d365fo-blog-of-er-consult/posts/extending-sysoperation-contracts-with-datamemberattribute /// </summary> [ExtensionOf(classStr(CustCollectionJourContract))] final class CustCollectionJourContract_DCL_Extension { #define.DocentricNonExistingGroup('DocentricNonExistingGroup') private NoYes attachOpenInvoices = NoYes::No; private DocCustCollectionJourAttachOpenInvoicesAs attachOpenInvoicesAs = DocCustCollectionJourAttachOpenInvoicesAs::MergedPDF; private DocCustCollectionJourOpenInvoiceSource openInvoiceSource = DocCustCollectionJourOpenInvoiceSource::Both; private DocCustCollectionJourEmailOpenInvoiceMissingAction openInvoiceMissingAction = DocCustCollectionJourEmailOpenInvoiceMissingAction::EmailWithWarning; [ DataMember('AttachOpenInvoices'), SysOperationLabel("@DocentricAX_WE_CLWA:CustCollectionJourAttachOpenInvoices"), SysOperationHelpText("@DocentricAX_WE_CLWA:CustCollectionJourAttachOpenInvoicesHelpText"), // Specifying non-existing group to create custom DialogField build process SysOperationGroupMember(#DocentricNonExistingGroup) ] public NoYes parmAttachOpenInvoices(NoYes _attachOpenInvoices = attachOpenInvoices) { attachOpenInvoices = _attachOpenInvoices; return attachOpenInvoices; } |
Keep in mind that in this case, you are writing an extension of the existing contract used on the existing dialog, and the newly added parameters are automatically shown as fields on the dialog using it. Therefore, we used a non-existing group on the UI to prevent the SrsReportDataContractUIBuilder from adding the new parameters as DialogField on the dialog (the fields are not visible when UI builder is building the dialog).
Step 6: Add new options to the print dialog (UI Builder)
Now the exciting part. How to display new fields on the dialog, but only when we are using newly added menu item buttons? As we have used our controller on the output menu item, we can control the build processes of the print dialog by checking if the controller used is DocCustCollectionJourController. But to do this, we must create an extension of the CustCollectionJourUIBuilder class.
1 2 3 4 5 6 7 8 9 10 |
/// <summary> /// Extension of <c>CustCollectionJourUIBuilder</c> to build a dialog with additional dialog groups and fields /// </summary> [ExtensionOf(classStr(CustCollectionJourUIBuilder))] final class CustCollectionJourUIBuilder_DCL_Extension { private DialogField attachOpenInvoicesField; private DialogField attachOpenInvoicesAsField; private DialogField openInvoiceSourceField; private DialogField openInvoiceMissingActionField; |
The tricky part here is that the existing CustCollectionJourContract contract class extended in Step 5 already has defined the dialog creation strategy.
1 2 3 4 5 6 7 8 9 10 11 |
[ DataContractAttribute, SysOperationContractProcessingAttribute(classStr(CustCollectionJourUIBuilder ), SysOperationDataContractProcessingMode::CreateUIBuilderForRootContractOnly), SysOperationGroupAttribute('PrintManagementGrp'," ",'1'), SysOperationGroupAttribute('DateGroup',"@SYS12608",'2') ] public class CustCollectionJourContract implements SysOperationValidatable, SysOperationInitializable { TransDate postingsUntil; |
How can we change this? Is it even possible?
In short, the answer is Yes. We can change the dialog by implementing build() and postBuild() methods by using Method Wrapping and CoC (Chain Of Command) principle.
But first, we need to add new DialogField to the dialog. For this purpose, we are using the build() method of CustCollectionJourUIBuilder.
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 |
/// <summary> /// Dialog build handler. Extending custom dialog /// </summary> public void build() { // initializing data contract object CustCollectionJourContract custCollectionJourContract = this.dataContractObject() as CustCollectionJourContract; next build(); // After the from build we need to add extended form options if (this.controller() is DocCustCollectionJourController) { // Get the main dialog group Dialog mainDialog = this.dialog(); // Adding dialog fields to override the behaviour of SysOperation // Adding PRINT OPTIONS group DialogGroup docPrintGroup = mainDialog.addGroup("@DocentricAX_WE_CLWA:CustCollectionJourPrintGroup", null, identifierStr('DocPrintOriginalInvoices')); docPrintGroup.columns(1); this.addDialogField(methodStr(CustCollectionJourContract, parmAttachOpenInvoices), custCollectionJourContract); // Adding SOURCE LOCATION INFORMATION group docPrintGroup = mainDialog.addGroup(null, null, identifierStr('DocPrintOriginalInvoicesSource')); docPrintGroup.columns(1); this.addDialogField(methodStr(CustCollectionJourContract, parmOpenInvoiceSource), custCollectionJourContract); // Adding EMAIL OPTIONS group docPrintGroup = mainDialog.addGroup("@DocentricAX_WE_CLWA:CustCollectionJourPrintGroupEmail", null, identifierStr('DocPrintOriginalInvoicesEmailOptions')); docPrintGroup.columns(2); this.addDialogField(methodStr(CustCollectionJourContract, parmAttachOpenInvoicesAs), custCollectionJourContract); this.addDialogField(methodStr(CustCollectionJourContract, parmOpenInvoiceMissingAction), custCollectionJourContract); } } |
Then we need to enable or disable other print options on the user interface if the option Attach overdue invoices is disabled.
In this case, we are also extending the behavior of newly added controls to the dialog, so we need to extend the postBuild() method. Therefore, we are registering an event to execute on the OnModified event handler, where you can change the visibility of the new dialog fields.
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 |
public void postBuild() { // initializing data contract object CustCollectionJourContract custCollectionJourContract = this.dataContractObject() as CustCollectionJourContract; custCollectionJourContract = this.dataContractObject() as CustCollectionJourContract; // Get the parameter controls from dialog attachOpenInvoicesField = this.bindInfo().getDialogField(custCollectionJourContract, methodStr(CustCollectionJourContract, parmAttachOpenInvoices)); attachOpenInvoicesAsField = this.bindInfo().getDialogField(custCollectionJourContract, methodStr(CustCollectionJourContract, parmAttachOpenInvoicesAs)); openInvoiceSourceField = this.bindInfo().getDialogField(custCollectionJourContract, methodStr(CustCollectionJourContract, parmOpenInvoiceSource)); openInvoiceMissingActionField = this.bindInfo().getDialogField(custCollectionJourContract, methodStr(CustCollectionJourContract, parmOpenInvoiceMissingAction)); // We are changing the form only if this was called from the following controller if (this.controller() is DocCustCollectionJourController) { this.EnableFields(); // register events attachOpenInvoicesField.registerOverrideMethod(methodStr(FormCheckBoxControl, modified), methodStr(CustCollectionJourUIBuilder, attachOpenInvoicesOnModified), this); } next postBuild(); } |
Step 7: Create a new DSP class with logic for the Email and other print destinations
Now we start modifying the output of print destinations. When you installed Docentric AX, you got replica designs of the original SSRS reports and DSP (Data Source Provider) class DocCustCollectionJourReportDSP.
In this case, extending of the DocCustCollectionJourReportDSP class is needed to handle our new functionality:
- If the selected print destination is Docentric Email, to attach overdue invoices as separate PDF files or Merged PDF file or a ZIP file containing overdue invoice PDFs.
- Otherwise, to download all overdue invoices as zipped PDF file.
In the extended DSP class, the main logic goes in the overrideReportRunSettings method, where we need to do the following:
- Check if attaching of overdue invoices is enabled by using parameters passed to the DSP class through the implemented contract in Step 5.
- Read all overdue invoices into memory from various locations depending on the invoice type.
- Create overdue invoice files depending on the print destination
As we now have all the PDF streams, the only thing left is to create the result. If the print destination is other than Email, a ZIP file with overdue invoices in PDF format is available for download. On the other hand, if the selected print destination is Email, then you can send overdue invoices as Separate files, Merged PDF or ZIP file containing PDF files.
1 2 3 4 5 6 7 8 9 10 11 |
void downloadAsZIP() { if (openInvoiceStreams.elements() > 0) { using(System.IO.Stream zipStream = DocDocumentHelper::documents2Zip(openInvoiceStreams, true)) { Filename destinationFilename = getDestinationFilename(' - Overdue invoices.zip'); DocFileMngHelper::sendFileToUser(zipStream, destinationFilename); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void attactToEmailAsZIP() { if (openInvoiceStreams.elements() > 0) { using(System.IO.Stream zipStream = DocDocumentHelper::documents2Zip(openInvoiceStreams, true)) { Filename destinationFilename = getDestinationFilename(' - Overdue invoices.zip'); _reportRunContext.emailPrintDestSettings().addAdditionalAttachment(destinationFilename, DocGlobalHelper::convertMemoryStreamToContainer(zipStream)); } } } |
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 |
void attachToEmailAsMergedPDF() { if (openInvoiceStreams.elements() > 0) { List docList = new List(Types::AnyType); ListEnumerator le = openInvoiceStreams.getEnumerator(); while (le.moveNext()) { Filename documentFileName; System.IO.MemoryStream documentStream; InvoiceId documentInvoiceId; [documentFileName, documentStream, documentInvoiceId] = le.current(); docList.addEnd(documentStream.ToArray()); } // Create the merged PDF file from the document contents saved in the list using (System.IO.MemoryStream mergedPDFStream = DocDocumentHelper::mergePdfDocuments(docList)) { Filename destinationFilename = getDestinationFilename(' - Overdue invoices.pdf'); _reportRunContext.emailPrintDestSettings().addAdditionalAttachment(destinationFilename, DocGlobalHelper::convertMemoryStreamToContainer(mergedPDFStream)); } } } |
Step 8: Register new DSP class to Docentric report setup
Before we start to register the newly created data source provider class, don’t forget to build the project and models. When the build process completes, you can assign the new DSP class to the Collection letter note report in Docentric report setup. You can do this by clicking in D365FO on Workspaces > Docentric AX > Reports > Report CustCollectionJour.Report > Data source and selecting the correct DSP class.
Step 9: Define & design how outgoing e-mails will look like
The only thing left is to define & design the outgoing e-mail. We can do this as we run the print action on the collection letter or in Docentric AX workspace under print management. In this case, we are using the standard print action.
Docentric AX supports dynamic placeholders of data used in the current collection letter. You can use them when you define & design an outgoing e-mail to fill some information automatically.
You can use this defined template for testing this solution.
- E-mail TO field template: @Invoice@;@@
- E-mail SUBJECT field template: @COMPANYNAME@: @CollLetterTitle@ - @CollLetterNum@ » Overdue payments on @DueDate@
- E-mail BODY field HTML template is available here for download.
Step 10: Try the solution
Now we are done, and you can try the solution at hand. For those of you who don’t have the time to try this out, you can watch this short video and see the demo in action.
Mass printing and sending
We have taken a look at how to create the solution for a single collection, but now let us focus on the mass printing of collection letters by using the same functionality as for printing a single collection letter. The only thing left is to create a new menu item inside the main menu of the D365 Modules > Credit and collections > Collection letter > Collection letter report with attached invoices. To extend the Credit and Collection menu, you need to create an extension of the exiting CreditAndCollection menu. Then you need to create an output menu item DocCustReport_collectionLetterAllWithAttachments and attach it to the DocCustCollectionJourController controller. Then you need to add the new menu to the extension of the CreditAndCollection menu.
And here is the result in D365FO of a newly added menu item.
Conclusion
The POC solution at hand will get you started. By using Docentric AX Free edition and simple modifications of existing D365 FO functionality, you can provide an automated way in the dunning process for printing and sending collection letters with attached overdue invoices.
note if you have Collection letter setup as ER report/Business Document and you are using the free version the print function will without any result and no error message will show.
reason for that: free version not support ER report/Business Documen, just full….
solution: revert to standard SSRS report or upgrade to full version
Hi Bence , are you was able to make that working with ER ? I understand i need to make right change in DSP class
Hi Marcin,
ER uses its own execution pipeline and ER destinations for printing. The example in this article uses the SSRS report execution pipeline in combination with Docentric APIs. To use ER, you need a Full Edition of Docentric AX, in which you can use ER data model and model mapping configurations and create Docentric report templates using the data from these configurations. The reports created in this way can be printed just like the SSRS reports and you can apply the solution from this article.