Wednesday, January 14, 2026

How to extend TradeDocumentReportController control to call your dynamic report design irrespective of print management setting setup.



The following code calls the SalesConfirmationReport but instead of using its design, it uses bespoke design set in branding. This can allow you to call any report design that you want.

The contract class is nothing but  parm methods for the journal recid and report name details for the dp class to fill in the write tmp tables. This is not a good design, so you dont have to follow it, but the below shows important methods that you need to override.

My biggest issue was overriding existing print management detail which can be done in the runPrintMgmt method.


/// <summary>

/// The <c>LSSalesConfirmController </c> class is the helper class for the associated SRS Report.

/// </summary>

public class LSSalesConfirmController extends TradeDocumentReportController implements BatchRetryable

{

    FormletterJournalPrint              formletterJournalPrint;

    CustConfirmJour                     custConfirmJour;

    /// <summary>

    /// Gets the sales confirmation document title.

    /// </summary>

    /// <returns>

    /// The document title.

    /// </returns>

    protected str documentTitle()

    {

        str documentTitle;


        if (printCopyOriginal == PrintCopyOriginal::Copy)

        {

            if (custConfirmJour.isProforma())

            {

                documentTitle = literalStr("@SYS70793");

            }

            else

            {

                documentTitle = literalStr("@SYS24432");

            }

        }

        else

        {

            if (custConfirmJour.isProforma())

            {

                documentTitle = literalStr("@SYS7845");

            }

            else

            {

                documentTitle = literalStr("@SYS12529");

            }

        }


        return documentTitle;

    }


    [Wrappable(false)]

    protected str documentIdentifier()

    {

        return custConfirmJour.ConfirmDocNum;

    }


    [Wrappable(false)]

    protected TableId reportExceptionContextTableId()

    {

        return custConfirmJour.TableId;

    }


    [Wrappable(false)]

    protected RecId reportExceptionContextRecordId()

    {

        return custConfirmJour.RecId;

    }


    protected boolean getFirstJournal()

    {

        return journalList.first(custConfirmJour);

    }


    protected boolean getNextJournal()

    {

        return journalList.next(custConfirmJour);

    }


    /// <summary>

    /// Returns the record ID of the journal record to be passed to the contract.

    /// </summary>

    /// <returns>

    /// The record ID of the journal record.

    /// </returns>

    protected RecId getRecordId()

    {

        return custConfirmJour.RecId;

    }


    /// <summary>

    /// Initializes the report and sets the print management.

    /// </summary>

    protected void initFormLetterReport()

    {

        printCopyOriginal = this.parmArgs().parmEnum();


        if (classIdGet(this.parmArgs().caller()) == classNum(SalesConfirmJournalPrint))

        {

            // Set the caller

            formletterJournalPrint = this.parmArgs().caller();

        }


        if (this.parmArgs().record())

        {

            // Get journal list from the selected record/s

            journalList = FormLetter::createJournalListCopy(this.parmArgs().record());

        }

        else

        {

            journalList = this.parmArgs().object();

        }


        formLetterReport = FormLetterReport::construct(PrintMgmtDocumentType::SalesOrderConfirmation);


        formLetterReport.parmPrintType(printCopyOriginal);

        

        if (formletterJournalPrint)

        {

            // Get the print settings

            formLetterReport.parmDefaultCopyPrintJobSettings(new SRSPrintDestinationSettings(formletterJournalPrint.parmPrinterSettingsFormLetterCopy()));

            formLetterReport.parmDefaultOriginalPrintJobSettings(new SRSPrintDestinationSettings(formletterJournalPrint.parmPrinterSettingsFormLetter()));

            formLetterReport.parmUsePrintMgmtDestinations(formletterJournalPrint.parmUsePrintManagement());

        }

        else if (printCopyOriginal == PrintCopyOriginal::OriginalPrint)

        {

            // Always use the print mgmt destinations when reprinting for the OriginalPrint case.

            formLetterReport.parmUsePrintMgmtDestinations(true);

        }


        super();

    }


    protected void runPrintMgmt()

    {

//if you know there will not be any print management setup for your report, you can create a default one here. In my case, we were setting it up for emails but wanted a dynamic report design so it is commented out.        

//SRSPrintDestinationSettings     printSettings = new SRSPrintDestinationSettings(formLetterReport.parmReportRun().parmReportRunController().parmReportContract().parmPrintSettings().pack());


        PrintMgmtPrintSettingDetail     printSettingDetail;

            

        journalList.first(custConfirmJour);


        do

        {

            formLetterReport.loadPrintSettings(custConfirmJour, custConfirmJour.salesTable(), custConfirmJour.LanguageId);

            

            if (formLetterReport.moveNextPrintSetting())

            {

                printSettingDetail = formLetterReport.getCurrentPrintSetting();

                printSettingDetail.parmReportFormatName(this.getReportName(custConfirmJour));

                //if you are creating the print settings value above, you need to pass it here.

                //printSettingDetail.parmPrintJobSettings(printSettings);


                formLetterReport.parmReportRun().loadSettingDetail(printSettingDetail);

            }


            CustLanguageId languageId = custConfirmJour.LanguageId;


            this.parmReportContract().parmRdlContract().parmLabelLanguageId(languageId);

            this.parmReportContract().parmRdlContract().parmLanguageId(languageId);

            this.outputReports();

        }

        while (journalList.next(custConfirmJour) && !this.parmCancelRun());

    }


    /// <summary>

    /// Sets the <c>SRSReportParameters</c> values in the report contract.

    /// </summary>

    protected void modifyContract()

    {

        LSOrderReportContract      LSOrderConfcontract = this.parmReportContract().parmRdpContract() as LSOrderReportContract;


        LSOrderConfcontract.parmTransactionRecId(custConfirmJour.RecId);


        if (LSOrderConfcontract

            && !LSOrderConfcontract.parmLSReportName()

            && this.parmReportName())

        {

            container cReport = str2con(this.parmReportName(), '.');

            str curReportName = any2Str(conPeek(cReport,1));

            str curReportDesignName = any2Str(conPeek(cReport,2));


            LSOrderConfcontract.parmLSReportName(curReportName);

            LSOrderConfcontract.parmLSReportDesignName(curReportDesignName);

            LSOrderConfcontract.parmCalledFromSO(true);

        }

            

    }


    protected void output()

    {

        formLetterReport.loadPrintSettings(custConfirmJour, SalesTable::find(custConfirmJour.SalesId), custConfirmJour.LanguageId);


        if (this.validateReportPrintJobSettingsEmailTo())

        {

            this.parmReportContract().parmRdlContract().parmLanguageId(custConfirmJour.LanguageId);

            this.parmReportContract().parmRdlContract().parmLabelLanguageId(custConfirmJour.LanguageId);

            super();

        }

    }


    private boolean validateReportPrintJobSettingsEmailTo()

    {

        boolean ret = true;


        PrintMgmtReportRun printMgmtReportRunLocal = formLetterReport.parmReportRun();


        if (printMgmtReportRunLocal.next())

        {

            SRSPrintDestinationSettings printSettings = printMgmtReportRunLocal.settingDetail().parmPrintJobSettings();

            if (printSettings.printMediumType() == SRSPrintMediumType::Email)

            {

                str emailTo = printSettings.emailTo();


                if (!emailTo)

                {

                    ret = checkFailed(strFmt("@SCM:SalesOrderEmailWasNotSent", custConfirmJour.SalesId));

                }

                else if (!SysEmailDistributor::validateEmail(emailTo))

                {

                    ret = checkFailed(strFmt("@SCM:SalesOrderEmailWasNotSentInvalidEmail", custConfirmJour.SalesId, emailTo));

                }

                printSettings.fileName(strFmt("Order confirmation %1", custConfirmJour.SalesId));

            }


            printMgmtReportRunLocal.reset();

        }


        

        if(this.getFirstJournal())

        {

            this.parmReportName(this.getReportName(custConfirmJour));

        }


        return ret;

    }


    /// <summary>

    /// Sets the values in the <c>CustConfirmJour</c> record.

    /// </summary>

    /// <param name="_common">

    /// A <c>CustConfirmJour</c> record.

    /// </param>

    protected void setDataContractRecord(Common _common)

    {

        custConfirmJour = args.record();

    }


    public static LSSalesConfirmController construct()

    {

        return new LSSalesConfirmController();

    }


    /// <summary>

    /// Initailzes the controller, sets the print management, assigns parameter values and runs the SRS report.

    /// </summary>

    /// <param name="_args">

    /// The <c>Args</c> object.

    /// </param>

    public static void main(Args _args)

    {

        SrsReportRunController formLetterController = LSSalesConfirmController::construct();


        LSSalesConfirmController controller = formLetterController;

        if(_args.record())

        {

            CustConfirmJour ccj = _args.record();

            controller.initArgs(_args, controller.getReportName(ccj));

        }


        if (classIdGet(_args.caller()) == classNum(SalesConfirmJournalPrint))

        {

            formLetterController.renderingCompleted += eventhandler(SalesConfirmJournalPrint::renderingCompleted);

        }

        

        SRSPrintDestinationSettings settings = controller.parmReportContract().parmPrintSettings();

        settings.printMediumType(SRSPrintMediumType::Screen);


        controller.parmReportContract().parmPrintSettings(settings);

        formLetterController.startOperation();

    }


    /// <summary>

    /// Specifies if the batch task is retryable for transient exceptions or not.

    /// </summary>

    /// <returns>

    /// If true is returned, the batch task is retryable, otherwise it is not.

    /// </returns>

    [Hookable(false)]

    final boolean isRetryable() 

    {

        return true;

    }


    public PrintMgmtReportFormatName getReportName(CustConfirmJour _custConfirmJour)

    {

        RetailStoreTable retailStoreTable = RetailStoreTable::find(_custConfirmJour.salesTable().retailSalesTable().RetailStoreId);

        CustTable        custTable        = CustTable::find(_custConfirmJour.OrderAccount);


        SysDocuBrandDetails brandDetails;


        str designType = custTable.custgroup == 'B2B' ?

            strFmt('%1%2*', enum2Symbol(enumNum(LSSalesDocumentType), enum2int(LSSalesDocumentType::Confirmation)), 'B2B') :

            strFmt('%1%2*', enum2Symbol(enumNum(LSSalesDocumentType), enum2int(LSSalesDocumentType::Confirmation)), 'B2C');


        select firstonly brandDetails

        where brandDetails.BrandId == retailStoreTable.LSBrandId

        && brandDetails.ReportName == 'LSOrderReport'

        && brandDetails.DesignName like designType;


        if (!brandDetails.RecId)

        {

            throw error(strFmt('No report design found for BrandId: %1, CustGroupId: %2, DocumentType: %3',

            retailStoreTable.LSBrandId,

            custTable.custgroup,

            enum2Symbol(enumNum(LSSalesDocumentType), enum2int(LSSalesDocumentType::Confirmation))));

        }

    

        return strFmt('%1.%2', brandDetails.ReportName, brandDetails.DesignName);

    }


}

Tuesday, October 7, 2025

UnionBranchNumber and the magic of unions in X++

IF you check out the view HCMCommplensationPlan, you will notice there is a method called plan type which is a view method

public class HcmCompensationPlan extends common
{
    public static str planType(int _unionBranchNumber)
    {
        HrmCompPlanType planType;
        switch (_unionBranchNumber)
        {
            case 1:
                planType = HrmCompPlanType::Fixed;
                break;
            case 2:
                planType = HrmCompPlanType::Variable;
                break;
        }
        return SysComputedColumn::returnLiteral(planType);
    }
}

The unionBranchNumber parameter here is basically the number of the datasource in the union join. 


The parameter automatically pics the first datasource as case 1 and the second datasource as case 2. This threw me off as I have not seen any documentation on this, but if you find some, do share.


Tuesday, August 26, 2025

The not equal to operator that you have never seen!

 So, I recently found out about this not equal to operator


i.e. this


And I think this is hilarious! I am not even sure where on the keyboard this option is, and how you are supposed to access this.

Thursday, August 14, 2025

Weird behaviour of setPrefix method in D365 FO. Do not use with if condition

 For some reason you cannot do 

{
    setPrefix("MESSAGE");
}
while()
{
    info("ChildMsg");
}


This means you cannot have setPrefix in an if condition or anything which needs curly brackets, the kernel calcualtes what to group based on the curly brackets and as soon as you add any extra, nothing breaks, but it stops doing what it is supposed to do. Could not find any documentation on it so added it here

Friday, May 9, 2025

Backup and restore print management settings

To quickly move print management settings between environments like UAT and FAT 

Pre-requisites

Environment:

  • At this time latest config is stored in UAT

Backup scope identified
Print management settings:

  • PRINTMGMTDOCINSTANCE
  • PRINTMGMTIDENTIFICATIONTEXT
  • PRINTMGMTREPORTFORMAT
  • PRINTMGMTSETTINGS

Work templates:

  • WHSWORKTEMPLATETABLE
  • WHSWORKTEMPLATELINE

Customer pool:

  • CUSTCOLLECTIONSPOOL

Export settings to a backup database
Login to T1 Dev VM 
Connect to local SQL Server
Create new database (or use database created in the previous backup)



On the new database click Import Data



Select data source



Select destination (local DB created above)


Select tables to be copied (e.g. PrintMgmt*)


If target tables to be truncated - use Edit mapping and select "Delete rows in destination table"



Repeat this for each table that needs to be truncated.

Run the package



Import settings from a backup
For this - export settings from the backup database (created when taking a backup using guide above) to the target environment AxDB database.

Use Export Data option



Select data source


Select destination


Select tables to be included


Select all and click Edit mappings and select delete rows in destination tables:


Run the package

Example summary:


Additional info
Some tables have read-only fields, for example SysRowVersion.
Set destination to <ignore> via Edit mappings option:


Tables affected:

  • WHSWORKTEMPLATETABLE
  • WHSWORKTEMPLATELINE
  • CUSTCOLLECTIONSPOOL

Monday, March 17, 2025

Things to think about for integrations

 Considerations and questions

  • Scope
    • The application interfaces for consideration 
    • To confirm the data being integrated

  • Transport and Broking
    • Is there an Enterprise Service Bus currently in use? Is this a standard that we should adopt
    • Where does the demarcation between connected systems lie?
    • Is Canonical Form being used?
    • Is there a standard transport mechanism for messages between systems? Can we use the Azure Service Bus?
    • Is there a standard used for systems to subscribe to messages of specific types?
    • Are messages lightweight, and how should their payloads be informed?

  •  Resilience
    • What status information should integrations return?
    • Are there standards we should adopt for service availability and recovery?
    • How should the connected systems recover from failure, regarding integrations?
    • Are outages captured anywhere?

  • Security
    • To describe current good practices that we should adhere to.
    • Confirm that we may use Azure Key Vault and Managed Identities as required.
    • To confirm levels of encryption for data in transit.
    • To confirm levels of encryption for data at rest.

  • Operation
    • For each set of data being moved, should this be synchronous/asynchronous?
    • If synchronous, what would be the trigger?
    • If asynchronous, what would be the frequency?
    • Where should error messages be surfaced?
    • Who will monitor and maintain the integrations?
    • What telemetry should be captured and how?
    • Will test systems for line-of-business applications be available?

Thursday, October 26, 2023

Inbound SOAP web service in Dynamics 365 Finance needs enum as non extensible

This was a weird one for me, If an inbound SOAP service in X++ expects an enum, such that the contract class defines in the following way


[DataContractAttribute]
public final class myContract 
{
    myEnum myEnum ;
    [DataMemberAttribute('myEnum')]
    public myEnum parmMyEnum (myEnum _myEnum = myEnum )
    {
        myEnum = _myEnum ;
        return myEnum ;
    }
}

Then you need to make sure that the extensible property on mEnum is set to false.

 

Otherwise you get the error in the following format
         <faultstring xml:lang="en-US">The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://tempuri.org:myContract . The InnerException message was 'Invalid enum value 'None' cannot be deserialized into type 'Dynamics.AX.Application.myEnum '. Ensure that the necessary enum values are present and are marked with EnumMemberAttribute attribute if the type has DataContractAttribute attribute.'.  Please see InnerException for more details.</faultstring>


In essence, the inbound services do not seem to allow extensible enums

In addition, to create a list type of contract classes, I have found this to be the cleanest way

[Newtonsoft.Json.JsonObjectAttribute(IsReference = false)]//Remove $id from Custom Web Service JSON
public class LOKInventoryServiceOutputContractList  
{
    protected List                    inventoryOutputContract;

    [DataMemberAttribute("InventoryList"),
    DataCollectionAttribute(Types::Class, classStr(LOKInventoryServiceOutputContract))]//the name of the class that will be in the list
    public List parmInventoryOutputContract(List _inventoryOutputContract = inventoryOutputContract)
    {
        inventoryOutputContract = _inventoryOutputContract;
        return inventoryOutputContract;
    }

}