Translate

Saturday, October 10, 2026

Automating IRN Error Monitoring and Email Notifications in Dynamics 365 Finance & Operations Using X++


 1. Introduction

In Microsoft Dynamics 365 Finance & Operations (D365FO), electronic invoicing plays an important role in invoice processing and GST compliance. During e-invoice processing, IRN (Invoice Reference Number) generation may fail because of validation errors, incorrect invoice information, or integration issues.

When multiple invoices are processed daily, manually checking every failed IRN transaction can be time-consuming for Finance and technical teams.

To simplify this process, we can develop an X++ batch class that automatically identifies failed IRN transactions, generates an Excel report containing invoice details and error messages, and sends the report to configured users through email.

In this blog, we will learn how to implement this functionality using RunBaseBatch, X++ queries, EPPlus Excel generation, and email notification classes in D365FO.

2. Business Requirement

The requirement is to automate IRN error monitoring for Project Invoices and Customer Invoices.

The solution should:

  • Identify invoices for which IRN generation has failed.

  • Retrieve invoice information and associated error messages.

  • Filter error records based on the current fiscal year.

  • Generate an Excel report containing invoice and error details.

  • Retrieve To and CC email addresses from a configuration table.

  • Send the Excel report as an email attachment.

  • Support execution through a batch job.

3. Technical Design

The solution is implemented using the following components.

ComponentPurpose
MyAutoMailForIRNErrorLogMain batch class
RunBaseBatchSupports batch execution
MyEInvoiceTableStores e-invoice processing information and error details
ProjInvoiceJourRetrieves project invoice information
CustInvoiceJourRetrieves customer invoice information
CustTableRetrieves customer account information
DirPartyTableRetrieves customer name
MyEinvoiceErrorAutoMailToCCStores configured email recipients
ExcelPackageCreates the Excel workbook
SysMailerMessageBuilderBuilds the email message
SysMailerExchangeSends the email

The custom tables, fields, and enums listed above must exist in your D365FO environment.

4. Complete X++ Implementation

Create a new class named MyAutoMailForIRNErrorLog in your D365FO project.

The following implementation is based on the supplied source code. Verify the custom object names and compile the class in your development environment before deployment.

final class MyAutoMailForIRNErrorLog extends RunBaseBatch

{

#define.CurrentVersion(1)

 

public static void main(Args _args)

{

    MyAutoMailForIRNErrorLog autoMail =

        new MyAutoMailForIRNErrorLog();

 

    if (autoMail.prompt())

    {

        autoMail.run();

    }

}

 

public ClassDescription caption()

{

    return "@MY:IRNErrorAutoMail";

}

 

public container pack()

{

    return [#CurrentVersion];

}

 

public boolean canGoBatch()

{

    return true;

}

 

public boolean unpack(container packedClass)

{

    return true;

}

 

public boolean runsImpersonated()

{

    return true;

}

 

public void run()

{

    this.sendMailIRNError();

}

 

public str mailBody()

{

    System.Text.StringBuilder htmlTable;

 

    htmlTable = new System.Text.StringBuilder();

 

    htmlTable.Append("<html>");

    htmlTable.Append("<head>");

    htmlTable.Append("<style>");

    htmlTable.Append(

        "table, th, td { border: 1px solid black; border-collapse: collapse; }");

    htmlTable.Append("</style>");

    htmlTable.Append("</head>");

    htmlTable.Append("<body>");

 

    htmlTable.Append("<p>Dear Team,</p>");

 

    htmlTable.Append(

        strFmt(

            "Please note the Error log for IRN not generated cases for date %1",

            date2Str(

                today(),

                321,

                DateDay::Digits2,

                DateSeparator::Hyphen,

                DateMonth::Digits2,

                DateSeparator::Hyphen,

                DateYear::Digits4)));

 

    htmlTable.Append("<br/><br/>");

    htmlTable.Append("Regards");

    htmlTable.Append("<br/>");

    htmlTable.Append("D365 Notifications");

 

    htmlTable.Append("</body>");

    htmlTable.Append("</html>");

 

    return htmlTable.ToString();

}

 

public void sendMailIRNError()

{

    SysMailerMessageBuilder mailer =

        new SysMailerMessageBuilder();

 

    SysMailerExchange smtp =

        new SysMailerExchange();

 

    ProjInvoiceJour projInvoiceJour;

    MyEInvoiceTable eInvoiceTable;

    CustInvoiceJour custInvoiceJour;

    DirPartyTable dirPartyTable;

    CustTable custTable;

 

    MyEinvoiceErrorAutoMailToCC einvoiceErrorAutoMailToCC;

 

    RowNumber row = 1;

 

    MyEmailIds to, cc;

 

    str fileName = strFmt(

        "IRN Error log for %1",

        today());

 

    boolean sendMail = false;

 

    System.IO.MemoryStream memoryStream =

        new System.IO.MemoryStream();

 

    FiscalCalendarRecId fiscalCalendar =

        Ledger::fiscalCalendar(CompanyInfo::current());

 

    date startDate, endDate;

 

    // Retrieve configured To and CC recipients.

    while select einvoiceErrorAutoMailToCC

    {

        if (einvoiceErrorAutoMailToCC.EmailType ==

            MyEmailType::To)

        {

            if (to == "")

            {

                to = einvoiceErrorAutoMailToCC.EmailId;

            }

            else

            {

                to += ";" +

                    einvoiceErrorAutoMailToCC.EmailId;

            }

        }

        else

        {

            if (cc == "")

            {

                cc = einvoiceErrorAutoMailToCC.EmailId;

            }

            else

            {

                cc += ";" +

                    einvoiceErrorAutoMailToCC.EmailId;

            }

        }

    }

 

    // Configure email subject and body.

    mailer.setSubject(

        strFmt(

            strUpr(curExt()) +

            ": IRN Error Log for %1.",

            date2Str(

                today(),

                321,

                DateDay::Digits2,

                DateSeparator::Hyphen,

                DateMonth::Digits2,

                DateSeparator::Hyphen,

                DateYear::Digits4)));

 

    mailer.setFrom(

        ProjParameters::find().IRNErrorLogMail_My);

 

    mailer.setBody(this.mailBody());

 

    mailer.addTo(to);

    mailer.addCc(cc);

 

    // Determine the current fiscal year.

    endDate =

        FiscalCalendarYear::findYearByCalendarDate(

            fiscalCalendar,

            today()).EndDate;

 

    startDate =

        FiscalCalendarYear::findYearByCalendarDate(

            fiscalCalendar,

            today()).StartDate;

// Create Excel workbook in memory.

using (var package =

    new OfficeOpenXml.ExcelPackage(memoryStream))

{

    var worksheets =

        package.get_Workbook().get_Worksheets();

 

    var worksheet = worksheets.Add("@SYS16445");

    var cells = worksheet.get_Cells();

 

    var cell = cells.get_Item(1, 1);

    cell.set_Value("Proposal Id");

 

    cell = cells.get_Item(1, 2);

    cell.set_Value("Invoice Id");

 

    cell = cells.get_Item(1, 3);

    cell.set_Value("Supply Type Code");

 

    cell = cells.get_Item(1, 4);

    cell.set_Value("Date");

 

    cell = cells.get_Item(1, 5);

    cell.set_Value("Invoice Account");

 

    cell = cells.get_Item(1, 6);

    cell.set_Value("Name");

 

    cell = cells.get_Item(1, 7);

    cell.set_Value("Sales Currency");

 

    cell = cells.get_Item(1, 8);

    cell.set_Value("Invoice Amount");

 

    cell = cells.get_Item(1, 9);

    cell.set_Value("Due Date");

 

    cell = cells.get_Item(1, 10);

    cell.set_Value("Error Log");

 

    // Process project invoice errors.

    while select ProposalId,

                 InvoiceAmount,

                 InvoiceDate,

                 CurrencyId,

                 InvoiceAccount,

                 ProjInvoiceId,

                 DueDate

        from projInvoiceJour

        join SupplyTypeCode,

             ErrorLog,

             RecId

        from eInvoiceTable

        where eInvoiceTable.projInvoiceJour ==

              projInvoiceJour.RecId

           && eInvoiceTable.IsError ==

              MyEInvoiceNoYes::Y

           && eInvoiceTable.DocumentDate <= endDate

           && eInvoiceTable.DocumentDate >= startDate

    {

        select Name from dirPartyTable

            exists join custTable

            where custTable.Party ==

                  dirPartyTable.RecId

               && custTable.AccountNum ==

                  projInvoiceJour.InvoiceAccount;

 

        row++;

 

        cell = cells.get_Item(row, 1);

        cell.set_Value(projInvoiceJour.ProposalId);

 

        cell = cells.get_Item(row, 2);

        cell.set_Value(projInvoiceJour.ProjInvoiceId);

 

        cell = cells.get_Item(row, 3);

        cell.set_Value(

            enum2Str(eInvoiceTable.SupplyTypeCode));

 

        cell = cells.get_Item(row, 4);

        cell.set_Value(

            date2Str(

                projInvoiceJour.InvoiceDate,

                321,

                DateDay::Digits2,

                DateSeparator::Hyphen,

                DateMonth::Digits2,

                DateSeparator::Hyphen,

                DateYear::Digits4));

 

        cell = cells.get_Item(row, 5);

        cell.set_Value(projInvoiceJour.InvoiceAccount);

 

        cell = cells.get_Item(row, 6);

        cell.set_Value(dirPartyTable.Name);

 

        cell = cells.get_Item(row, 7);

        cell.set_Value(projInvoiceJour.CurrencyId);

 

        cell = cells.get_Item(row, 8);

        cell.set_Value(

            int2Str(projInvoiceJour.InvoiceAmount));

 

        cell = cells.get_Item(row, 9);

        cell.set_Value(

            date2Str(

                projInvoiceJour.DueDate,

                321,

                DateDay::Digits2,

                DateSeparator::Hyphen,

                DateMonth::Digits2,

                DateSeparator::Hyphen,

                DateYear::Digits4));

 

        cell = cells.get_Item(row, 10);

        cell.set_Value(eInvoiceTable.ErrorLog);

 

        sendMail = true;

    }

            // Process customer invoice errors.

            while select InvoiceAmount,

                         InvoiceDate,

                         CurrencyCode,

                         InvoiceAccount,

                         InvoiceId,

                         DueDate

                from custInvoiceJour

                join SupplyTypeCode,

                     ErrorLog,

                     RecId

                from eInvoiceTable

                where eInvoiceTable.CustInvoiceJour ==

                      custInvoiceJour.RecId

                   && eInvoiceTable.IsError ==

                      MyEInvoiceNoYes::Y

                   && eInvoiceTable.DocumentDate <= endDate

                   && eInvoiceTable.DocumentDate >= startDate

            {

                select Name from dirPartyTable

                    exists join custTable

                    where custTable.Party ==

                          dirPartyTable.RecId

                       && custTable.AccountNum ==

                          custInvoiceJour.InvoiceAccount;

 

                row++;

 

                // Proposal Id is not applicable to this invoice.

                cell = cells.get_Item(row, 2);

                cell.set_Value(custInvoiceJour.InvoiceId);

 

                cell = cells.get_Item(row, 3);

                cell.set_Value(

                    enum2Str(eInvoiceTable.SupplyTypeCode));

 

                cell = cells.get_Item(row, 4);

                cell.set_Value(

                    date2Str(

                        custInvoiceJour.InvoiceDate,

                        321,

                        DateDay::Digits2,

                        DateSeparator::Hyphen,

                        DateMonth::Digits2,

                        DateSeparator::Hyphen,

                        DateYear::Digits4));

 

                cell = cells.get_Item(row, 5);

                cell.set_Value(custInvoiceJour.InvoiceAccount);

 

                cell = cells.get_Item(row, 6);

                cell.set_Value(dirPartyTable.Name);

 

                cell = cells.get_Item(row, 7);

                cell.set_Value(custInvoiceJour.CurrencyCode);

 

                cell = cells.get_Item(row, 8);

                cell.set_Value(

                    int2Str(custInvoiceJour.InvoiceAmount));

 

                cell = cells.get_Item(row, 9);

                cell.set_Value(

                    date2Str(

                        custInvoiceJour.DueDate,

                        321,

                        DateDay::Digits2,

                        DateSeparator::Hyphen,

                        DateMonth::Digits2,

                        DateSeparator::Hyphen,

                        DateYear::Digits4));

 

                cell = cells.get_Item(row, 10);

                cell.set_Value(eInvoiceTable.ErrorLog);

 

                sendMail = true;

            }

 

            package.Save();

        }

 

        // Send the report only if failed invoices exist.

        if (sendMail)

        {

            memoryStream.Seek(

                0,

                System.IO.SeekOrigin::Begin);

 

            mailer.addAttachment(

                memoryStream,

                fileName + ".xlsx",

                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

 

            try

            {

                smtp.sendNonInteractive(

                    mailer.getMessage());

            }

            catch (Exception::CLRError)

            {

                error(

                    CLRInterop::getLastException().toString());

            }

        }

    }

}


5. Explanation of the Important Methods

main()

The main() method creates the class instance and prompts the user to run the batch process.

pack() and unpack()

These methods support serialization of batch parameters. The current implementation has no additional user-defined parameters, so pack() stores the version and unpack() accepts the packed container.

canGoBatch()

Returning true allows the class to be configured for batch execution.

runsImpersonated()

This method returns true. Review the security implications and execution identity requirements for your environment before using this behavior in production.

mailBody()

This method creates the HTML body of the notification email. It includes a greeting, reporting date, and signature.

sendMailIRNError()

This is the main processing method. It retrieves recipients, determines the fiscal-year date range, queries invoice errors, generates the workbook, and sends the email when error records are found.

6. How the Invoice Queries Work

Project Invoice Query

The first query retrieves project invoice information from ProjInvoiceJour and joins it with MyEInvoiceTable.

The query checks:

  • The e-invoice record is associated with the project invoice journal.

  • IsError is equal to MyEInvoiceNoYes::Y.

  • DocumentDate falls between the fiscal-year start and end dates.

The result is written to the Excel worksheet.

Customer Invoice Query

The second query retrieves customer invoice information from CustInvoiceJour and joins it with MyEInvoiceTable.

It applies the same error and fiscal-year filters, but uses the customer invoice relationship.

Both queries populate invoice details and the associated error message.

7. Excel Report Structure

The report includes the following columns:

ColumnDescription
Proposal IdProject invoice proposal reference
Invoice IdInvoice identifier
Supply Type CodeE-invoice supply type
DateInvoice date
Invoice AccountCustomer account
NameCustomer name
Sales CurrencyCurrency code
Invoice AmountInvoice amount
Due DateInvoice due date
Error LogError details returned by the e-invoice process

The workbook is created in memory using System.IO.MemoryStream and OfficeOpenXml.ExcelPackage. The resulting workbook is attached to the notification email.

8. Email Notification Process

The email configuration is retrieved from MyEinvoiceErrorAutoMailToCC.

The EmailType field determines whether an address is added to the To or CC list.

The subject includes the current company identifier and date. The email body is generated by mailBody(), and the Excel report is attached using addAttachment().

The email is sent through SysMailerExchange.sendNonInteractive().

The sendMail flag ensures that the email is sent only when at least one failed transaction is found. Consequently, this implementation does not send a notification when no matching errors are found.

9. Execution Flow

The overall process follows these steps:

  1. Start the batch class.

  2. Retrieve configured email recipients.

  3. Determine the current fiscal-year start and end dates.

  4. Query failed project invoice IRN transactions.

  5. Query failed customer invoice IRN transactions.

  6. Populate the Excel worksheet.

  7. Save the workbook to a memory stream.

  8. Check whether any error records were found.

  9. Attach the Excel workbook to the email.

  10. Send the notification email.

10. Configuration and Deployment

Before executing the class, confirm the following configuration:

  • The custom e-invoice tables and fields are available in the model.

  • The error status enum values match the implementation.

  • The To and CC configuration table contains valid email addresses.

  • The sender email field is correctly defined in ProjParameters.

  • Email delivery is configured and tested in the environment.

  • The current legal entity has a valid fiscal calendar.

  • The required batch execution permissions are available.

To execute the class manually, run it through its menu item or development entry point. To automate execution, expose it through the appropriate menu item and configure a recurring batch job in D365FO.

11. Testing Scenarios

Test the following scenarios before deploying the solution.

Scenario 1: Project invoice IRN failure

Create or identify a project invoice with a corresponding error record. Verify that the invoice appears in the Excel report.

Scenario 2: Customer invoice IRN failure

Identify a customer invoice with a corresponding error record. Verify that its invoice details and error message appear in the report.

Scenario 3: No error records

Run the batch when no matching error records exist. Verify that no notification email is sent.

Scenario 4: Multiple recipients

Configure multiple To and CC recipients. Verify that the email reaches the expected users.

Scenario 5: Fiscal-year filtering

Create or identify error records with document dates inside and outside the current fiscal year. Verify that only records within the selected date range are included.

Scenario 6: Email delivery failure

Test the behavior when the email infrastructure rejects or cannot deliver the message. Verify that failures are logged and can be investigated.

12. Best Practices and Improvements

The supplied implementation provides the core workflow, but several areas should be reviewed before production use.

  • Invoice amount precision: int2Str() converts the invoice amount to an integer string and may lose decimal precision. Use a suitable decimal conversion or write a numeric value to Excel.

  • Recipient validation: Validate the To list before sending. An empty recipient list should be handled explicitly.

  • Batch serialization: Implement proper version validation in unpack() if the class later introduces parameters.

  • Error handling: The current code handles Exception::CLRError around email sending. Add suitable logging and handling for other failures, including workbook generation.

  • Resource management: Confirm that the memory stream and Excel package are disposed of correctly under both successful and exceptional execution.

  • Column consistency: The Proposal Id field is not populated for customer invoices, so the Invoice Id remains in column 2. Consider making the report layout explicit for both invoice types.

  • Recipient configuration: Filter configuration records by the appropriate company or business scope if recipients differ between legal entities.

  • Date filtering: Confirm whether the business requirement is to report the entire fiscal year or only newly failed transactions. The current implementation filters by fiscal year.

  • Duplicate notifications: If the batch runs frequently, consider tracking previously reported errors to avoid repeatedly emailing the same records.

13. Benefits

Automating IRN error monitoring provides several operational benefits:

  • Reduces manual monitoring effort.

  • Provides centralized visibility into failed e-invoice transactions.

  • Helps Finance and GST teams identify issues faster.

  • Distributes error details through a familiar Excel format.

  • Supports scheduled background processing.

  • Makes email recipients configurable without hardcoding individual addresses.

14. Conclusion

Automated IRN Error Monitoring is a practical example of how X++ batch processing can improve operational visibility in Microsoft Dynamics 365 Finance & Operations.

By combining RunBaseBatch, invoice journal queries, e-invoice error records, Excel generation, and email notification, the MyAutoMailForIRNErrorLog class provides a structured way to report failed IRN transactions to business users.

With proper validation, error logging, recipient configuration, and production testing, this approach can serve as a foundation for more comprehensive e-invoice monitoring solutions in D365FO.