Write product configuration model attributes to BOM and ProdBOM

pctn

https://www.youtube.com/watch?v=BaHYbDQJB_A

The product configuration model in Dynamics 365 for Finance and Operations is a great tool dynamically generate production orders including bill of materials (ProdBOM) and routes. In the product configuration model, the user is free to define attributes of different types, and use these attributes for calculations and conditions within the model. However, I had to come up with a solution to write the attribute value from the production configuration wizard to the generated ProdBOM. Here is an example code how this can be done:

Data type, table and form extensions

  1. Create a new string EDT and call it ERPPCStringAttribute
  2. Create table extensions for the BOM and ProdBOM and add a new field based on the ERPPCStringAttribute EDT
  3. Create a form extension for the ProdBOM and add the new field in the grid (Tab > Overview > Grid)

Extend the PCBOMLineDetails form

This is a tricky part. In the form for the BOM line details in the product configuration model, create a section for the attribute that looks the same like all the others. Here is a screenshot from Visual Studio:

image

You need to create a group, hat contains another group, with a checkbox and group with a string edit field and a radio button. Make sure to name the elements like shown in the screenshot.  If you are not sure, compare it with the other existing groups e.g. the SubContractor. You will need to compare the properties of each element with the corresponding properties of an existing element to make it look similar. 

image

Extend PCBOMLineDetails Form UI logic

You cannot overwrite methods on UI elements in form extensions e.g. clicked() . Therefore you have to implement the logic for the UI elements in a separate class. Create a new class ERPPCBomLineDetailsEventHandler and implement the following UI logic for clicked(), modified() and lookup()

class ERPPCBomLineDetailsEventHandler
{
   
    [FormControlEventHandler(formControlStr(PCBOMLineDetails, 
                             AllocateStringAttribute),
                             FormControlEventType::Clicked)]
    public static void AllocateAttributeString1_OnClicked(FormControl sender,
                                                     FormControlEventArgs e)
    {
        FormCheckBoxControl chx = sender;

        FormControl allocationGroup_AttributeString1 =
                sender.formRun().design().controlName(formControlStr(
                                             PCBOMLineDetails,
                                             allocationGroup_StringAttribute)
                                                       );
        FormControl attributeString1_Allocation =
                 sender.formRun().design().controlName(formControlStr(
                                             PCBOMLineDetails,
                                             StringAttribute_Allocation)
                                                       );

        PCModelingLibrary::templateSetEnabledStatus(
                                            (chx.value() == NoYes::Yes), 
                                            allocationGroup_AttributeString1,
                                            attributeString1_Allocation
                                                   );
    }

    [FormControlEventHandler(formControlStr(PCBOMLineDetails, 
                             StringAttribute_Allocation),
                             FormControlEventType::Modified)]
    public static void AttributeString1_Allocation_OnModified(
                                                      FormControl sender,
                                                      FormControlEventArgs e)
    {
        FormRadioControl radio = sender;
        FormStringControl attributeString1 =
                      sender.formRun().design().controlName(formControlStr(
                                                            PCBOMLineDetails,
                                                            StringAttribute)
                                                             );

        str label = attributeString1.labelText();

        attributeString1.text(”);

        if (radio.selection() == PCAllocation::Value)
        {
            attributeString1.extendedDataType(
                                        extendedTypeNum(ERPPCStringAttribute)
                                              );
        }
        else
        {
            attributeString1.extendedDataType(
                                        extendedTypeNum(ERPPCStringAttribute)
                                              );
            attributeString1.label(label);
        }
    }

     
    [FormControlEventHandler(formControlStr(PCBOMLineDetails, 
                             StringAttribute),
                             FormControlEventType::Lookup)]
    public static void AttributeString1_OnLookup(FormControl sender,
                                                 FormControlEventArgs e)
    {
        Object PCBOMLineDetails = sender.formRun();
        PCClass component = PCBOMLineDetails.component();
        FormRadioControl radio = 
           
sender.formRun().design().controlName(formControlStr(
                                                  PCBOMLineDetails,
                                                  StringAttribute_Allocation)
                                                 
);

        if(radio.selection() == PCAllocation::Attribute)
        {
            PCModelingLibrary::attributeLookup(sender, component);
        }       
    }

}

Extend the PCBOMLineDetails class-behind

Like many forms in Dynamics 365 for Finance and Operations the form has a class-behind that implements the business logic. You need to extend this class in order to deal with the newly created  Attribute group. Create a new class ERPPCBomLineDetails_Extension and impelement the following logic:

[ExtensionOf(formStr(PCBOMLineDetails))]
final class ERPPCBomLineDetails_Extension
{
    public PCClass component()
    {
        return component;
    }

    [ExtensionOf(FormMethodStr(PCBOMLineDetails,loadAllocations))]
    public void loadAllocations()
    {
        PCTemplateAttributeBinding      templateAttributeBinding;
        PCTemplateAttribute             fieldReference;


        PCTemplateAttributeBinding    findBindingByIDs(TableId _tableId, 
                                                       FieldId _fieldId)
        {
            PCTemplateAttributeBinding binding;

            fieldReference =
                 templateFind.findTemplateAttributeByTableIdAndFieldId(
                                                                _tableId,
                                                                _fieldId
                                                                );
           
            select  firstonly binding
            where   binding.TemplateAttribute == fieldReference.RecId
            &&      binding.TemplateComponent == templateComponent.RecId;

            return binding;
        }

        next loadAllocations();

        templateAttributeBinding = findBindingByIDs(tableNum(BOM), 
                                                    fieldNum(BOM,
                                                    ERPPCStringAttribute));


        PCModelingLibrary::templateLoadStringAllocation(component,
                                            templateAttributeBinding,
                                            StringAttribute,
                                            AllocateStringAttribute,
                                            StringAttribute_Allocation,
                                            AllocationGroup_StringAttribute);


    }

    [ExtensionOf(FormMethodStr(PCBOMLineDetails,updateRadioControls))]
    public void updateRadioControls()
    {
        next updateRadioControls();

        PCModelingLibrary::templateSetEnabledStatus(
                            (AllocateStringAttribute.value() == NoYes::Yes), 
                            AllocationGroup_StringAttribute,
                            StringAttribute_Allocation);

    }

    [ExtensionOf(FormMethodStr(PCBOMLineDetails,writeAllocations))]
    public void writeAllocations()
    {
        PCTemplateAttribute             fieldReference;

        next writeAllocations();

        if ((AllocateStringAttribute.value() == NoYes::Yes))
        {
            fieldReference = 
                   templateFind.findTemplateAttributeByTableIdAndFieldId(
                                      tableNum(BOM),
                                      fieldNum(BOM, ERPPCStringAttribute));


            PCModelingLibrary::templateSaveStringAllocation(component, 
                                               templateComponent,
                                               fieldReference,
                                               StringAttribute,
                                               StringAttribute_Allocation);
        }
    }

}

Extend the product configuration model framework

Implement the following classes to extend the product configuration model framework in Dynamics 365 Finance and Operations:

PcAdaptorBOMLine class:

[ExtensionOf(classStr(PcAdaptorBOMLine))]
final class ERPPcAdaptorBOMLine_Extension
{
    public ERPPCStringAttribute parmWANPCStringAttribute(
            ERPPCStringAttribute _stringAttribute = bom.ERPPCStringAttribute)
    {
        EcoResTextValue value;

        value.TextValue = _stringAttribute;
        this.fieldAssignment(bom.TableId,
                             fieldnum(BOM, ERPPCStringAttribute),
                             value);

        bom.ERPPCStringAttribute = _stringAttribute;                   
        return bom.ERPPCStringAttribute;
    }

}

PCGenerateBOMLine

[ExtensionOf(classStr(PCGenerateBOMLine))]
final class ERPPCGenerateBOMLine_Extension
{
    protected void setupBOM(
        BOMId       _bomId,
        InventDim   _inventDim,
        InventTable _inventTable,
        boolean     _isProduction
    )
    {
        next setupBom(_bomId,_inventDim,_inventTable,_isProduction);

        bom.ERPPCStringAttribute = templateFind.getBindingValueAsString(
                                       tableNum(BOM),
                                       fieldNum(BOM,ERPPCStringAttribute));
    }

}

Re-Initialize the framework template records

By default the framework does not recognize to create template records for the newly added attribute field. Therefore you have to delete the existing templates and trigger the framework to reinitialize. Be aware, this might harm your existing models!

PCTemplateInitialize class:

[ExtensionOf(classStr(PcTemplateInitialize))]
final class ERPPCTemplateInitialize_Extension
{
public static void main(Args _args)
{
PCTemplate tableTemplate;
PcTemplateInitialize init = PcTemplateInitialize::construct();
delete_from tableTemplate;
init.run();
}
protected void createTemplatesForBOM()
{
next createTemplatesForBom();
PCTemplate tableTemplate;
select firstonly tableTemplate where
tableTemplate.ReferencedTableId == tableNum(BOM);
this.createFieldTemplate(tableTemplate,fieldNum(BOM,
ERPPCStringAttribute ));
}
}

Start this class from Class Runner or add a menu item to call it by hand.

Test your Implementation

  1. Create a new product configuration model
  2. Add a new attribute “notes” of type string
  3. Add a BOM line to the model
  4. Open the BOM line details and assign the notes attribute to the string attribute 1
    image
  5. Save the model
  6. Create and approve the model version
  7. Create a new production order
  8. Use the configuration wizard to provide a text value for the notes attribute
    image
  9. Create the production order
  10. Verify that you can see the attribute value in the ProdBOM of your production order
    image

Send SSRS Report as Email from Report Viewer in Dynamics 365 FO

A very common task required by Dynamics 365 Finance and Operations clients is to send a report directly from the report viewer. This can be achieved with a view lines of Code. Here is video on Youtube how it works:

image
https://www.youtube.com/watch?v=0yQ8wvzzJSg

To achieve this create an extension of the SRSReportViewerForm, add a “Send Email” button. Create a OnClicked Event Handler in a Class.

[FormControlEventHandler(formControlStr(SrsReportViewerForm,
FormButtonControl1), FormControlEventType::Clicked)]
public static void FormButtonControl1_OnClicked(FormControl sender, FormControlEventArgs e)
{
SrsReportViewerControl ct = sender.formRun().design(0).controlName('ReportViewerControl');
object viewerForm = ct.formRun();
SrsReportRunController ctr= viewerForm.controller();
Microsoft.Dynamics.AX.Framework.Reporting.Shared.ReportingService.ParameterValue[] parameterValueArray;
Map reportParametersMap;

SRSReportRunService srsReportRunService = new SrsReportRunService();
srsReportRunService.getReportDataContract(ctr.parmreportcontract().parmReportName());
srsReportRunService.preRunReport(ctr.parmreportcontract());
reportParametersMap = srsReportRunService.createParamMapFromContract(ctr.parmReportContract());
parameterValueArray = SrsReportRunUtil::getParameterValueArray(reportParametersMap);str fileName = ctr.parmReportName()+ ".pdf";

SRSPrintDestinationSettings settings;
settings = ctr.parmReportContract().parmPrintSettings();
settings.printMediumType(SRSPrintMediumType::File);
settings.fileName(fileName);
settings.fileFormat(SRSReportFileFormat::PDF);
SRSProxy proxy = SRSProxy::constructWithConfiguration(SRSConfiguration::getDefaultServerConfiguration());

System.Byte[] reportBytes = proxy.renderReportToByteArray(ctr.parmreportcontract().parmreportpath(),
parameterValueArray,settings.fileFormat(),settings.deviceinfo());

if(reportBytes.Length > 0)
{
SysMailerMessageBuilder messageBuilder = new SysMailerMessageBuilder();
messageBuilder.setSubject(fileName);
messageBuilder.addAttachment(new System.IO.MemoryStream(reportBytes),fileName);
SysMailerFactory::sendInteractive(messageBuilder.getMessage());
}
}

Product Configuration Model in Dynamics 365 Finance and Operations

This video explains how you can use the product configuration model in D365 FO to model a heater. The heater can have a length between 0.5 m – 4,5 m, can have a certain color and provide an analog handle, a touch panel or a remote control. Touch panel and remote control is only available for heaters > 1m . A heater contains of heating units (4x 1m), requires connector modules between each 4 heating units and an enclosed. The product configuration model is used to define these requirements and constrains, provide a wizard for production configuration and generates the correct BOM.

image

Feature-Based reuse in the ERP domain: An industrial case study

Enterprise Resource Planning (ERP) system vendors need to customize their products according to the domain-specific requirements of their customers. Systematic reuse of features and related ERP product customizations would improve software quality and save implementation time. In our previous research, we have developed a tool-based approach supporting feature-based reuse of ERP product customizations. Our tool environment automatically infers reusable features from requirements and their associated implementation artifacts. Furthermore, it allows domain experts to search for features based on natural language requirement descriptions representing the needs of new customers. Matching features can be automatically deployed to a new ERP product. In this paper, we present an industrial evaluation of our tool-based approach conducted together with an Austrian small- to medium-sized enterprise. A domain expert used our approach to identify matching features for 20 randomly selected requirements for new customer products and identified matching features for 17 of the 20 requirements. We compared the time needed to identify and deploy the candidate features with the time required to implement the requirements from scratch. We found that, in total, over 60% implementation time can be saved by applying our reuse approach presented in this case study.

The actual paper regarding reusing of ERP customizations across multiple instances for Dynamics AX has been presented at the 22nd International System and Software Product Line Conference 2018 in Gothenburg (SE). The paper is available at ACM

Dynamic filtered lookup fields in Dynamics AX 2009 report dialog

I recently faced a customer requirement in Dynamics AX 2009 where a customer needs two lookups in report dialog. Depending what was selected on the first lookup (InventLocation),  the content of the second lookup should be filtered (WMSLocationId). Michael has already documented in his blog how to overwrite a lookup in dialog.  In order to override the lookup methods a RunBaseReport class is needed to call the the report.

class WarehouseReportRun extends RunBaseReport
{
Dialog dlg;

    DialogField fieldInventLocationId;
DialogField fieldWMSLocationId;

    InventLocationId    inventLocationId;
WMSLocationId       wmsLocationId;
}

Overwrite the dialog and getFromDialog methods as usual

public Object dialog(DialogRunbase dialog, boolean forceOnClient)
{
dlg = super(dialog, forceOnClient);
fieldInventLocationId = dlg.addField(typeId(InventLocationId));
fieldWMSLocationId = dlg.addField(typeId(WMSLocationId));

    return dlg;
}

 

public boolean getFromDialog()
{
boolean ret;

    ret = super();

    inventLocationId = fieldInventLocationId.value();
wmsLocationId = fieldWMSLocationid.value();

    return ret;
}

Create two parm methods for the lookup variables

public InventLocationId parmInventLocationId(
InventLocationId _inventLocationId = inventLocationId)
{
;
inventLocationId = _inventLocationId;

    return inventLocationId;
}

 

public WMSLocationId parmWmsLocationId(
WMSLocationId _wmsLocationId = wmsLocationId)
{
;
wmsLocationId = _wmsLocationId;

    return wmsLocationId;
}

Implement the abstract method lastValueElementName and provide the name of the report to start. In my case the report is called WarehouseReport.

public identifiername lastValueElementName()
{
return reportStr(WarehouseReport);
}

Create a menu item and start the class. Right click the lookup fields and from the setup form note the name of the lookup fields. In my case the system generated name for the first lookup field is Fld6_1 (InventLocation) and the name for the second is Fld7_1 (WMSLocation)

Find the dynamically generated field name in the report dialog class

According to michaels blog overwrite the dialogPostRun method. Here you can defined that you want to overwrite methods and link an object with the overwritten methods.

public void dialogPostRun(DialogRunbase dialog)
{
super(dialog);

    dialog.dialogForm().formRun().controlMethodOverload(true);
dialog.dialogForm().formRun().controlMethodOverloadObject(this);
}

Next implement the code for the lookup on the second lookup field in the dialog. In my case it will only show WMSLocations for the selected InventLocation in the first lookup.

void Fld7_1_lookup()
{
FormStringControl ctr;
SysTableLookup lookup;
Query q;
QueryBuildDataSource qbds;
QueryBuildRange qr;
;

    q = new Query();
qbds = q.addDataSource(tableNum(WMSLocation));
qr = qbds.addRange(fieldNum(WMSLocation,InventLocationId));
qr.value(fieldInventLocationId.value());

ctr = dlg.formRun().controlCallingMethod();
lookup = SysTableLookup::newParameters(tableNum(WMSLocation),ctr);
lookup.addLookupfield(fieldNum(WMSLocation,WMSLocationId),true);
lookup.addLookupfield(fieldNum(WMSLocation,InventLocationId));
lookup.addLookupfield(fieldNum(WMSLocation,checkText));
lookup.parmQuery(q);
lookup.performFormLookup();
}

Test the class. It will only show WMSLocations for the selected InventLocation.

The values in  the second lookup is filted by the value of the first lookup

In the last step overwrite the init method in the report and set the range according to the values from the lookup fields. In my report I have a InventSum datasource linked with an InventDim datasource. I use the parm methods to set the InventDim ranges on the InventLocation and WMSLocation

public void init()
{
QueryBuildDataSource qbds;
QueryBuildRange rinv;
QueryBuildRange rwms;
WarehouseReportRun wrr = element.args().caller();
;

super();

    qbds = this.query().dataSourceTable(tableNum(InventDim));

    rinv = qbds.addRange(fieldNum(InventDim,InventLocationId));
rinv.value(wrr.parmInventLocationId());

rwms = qbds.addRange(fieldNum(InventDim,WMSLocationId));
rwms.value(wrr.parmWMSLocationId());
}

The report works as required and shows only data from InventLocation 22 and WMSLocation 01-01-01-1

Dynamics AX 2009 report with dynamic lookup filter

Configure PowerBI on Dynamics 365 FO developer VM

I’ve created a video tutorial how to configure PowerBI on a stand alone Dynamics 365 Finance and Operations developer VM

Graphical representation of warehouse usage in Dynamics AX

A often recurring requirement is a graphical inventory overview showing the usage of locations. There are many ways how to implement such a solution. One simple way is to use data shapes in Visio and link them to Dynamics Ax data.

graphical inventory usage

Visio

Since every warehouse is different, you have to create a separate Visio drawing for each one. Visio provides you with good standard shapes to draw a floor plan. In this example I am using a simple drawing of a warehouse with one door and 12 locations. In my example a square in Visio represents a WMSLocation in Dynamics AX. Create one Visio file per warehouse and save it on a file share.

Warehouse floor plan

Data

Next create a view for each warehouse on the Dynamics AX database. Ask you DBA to secure access to the view for your users. Here is an example SQL code I am using to fetch data from Location 11 and 12 ( Contoso Demo Data)

SELECT
w.INVENTLOCATIONID, w.WMSLOCATIONID, w.DATAAREAID, w.VOLUME, COALESCE (l.CURRENTVOLUMEADJUSTED, 0) AS CURRENTVOLUMEADJUSTED, w.VOLUME – COALESCE (l.CURRENTVOLUMEADJUSTED,0) AS FreeTotal, (w.VOLUME – COALESCE (l.CURRENTVOLUMEADJUSTED, 0)) * 100 / w.VOLUME AS FreePercent
FROM           
dbo.WMSLOCATION AS w
LEFT OUTER JOIN
dbo.WMSLOCATIONLOAD AS l
ON
w.INVENTLOCATIONID = l.INVENTLOCATIONID AND
w.WMSLOCATIONID = l.WMSLOCATIONID AND
w.DATAAREAID = l.WMSLOCATIONDATAAREAID
WHERE
(w.VOLUME > 0) AND
(w.DATAAREAID = ‘USMF’) AND
(w.INVENTLOCATIONID = ’11’) OR (w.INVENTLOCATIONID = ’12’)

Link SQL Data to Visio shapes

In the Visio main menu go to the Data tab and Link Data with Shapes. Go through the wizard and connect to your SQL Server and the view you have just created. This will open the External Data window showing the results from the SQL query.

Load Dynamics AX data in Visio

In the Visio drawing panel select the first square that represents a location. Right click on a record in the external data grid and select Link to selected shape. A chain symbol will appear next to the record, showing you that this record is new linked to a shape in your drawing.

Right click on the shape that is now linked to a record in the external data. In the shapes context menu go to shape data and open edit data graphic. Here you can add and format the column values from the record to the shape. In my case I’ve formatted the InventLocationId as Header and the FreePercent as progress bar.

image

Once you have formatted one shape you can copy & paste it multiple times. You only need to selected the copy and then right click on the corresponding row in the external data and link to shape. This will update the shape data with the values from the new record.

View in Dynamics AX

Finally some work is needed in Dynamics AX to view the Visio drawing. At the InventLocation table add a new field using the FileName extended data type. Add the field in the InventLocation form, so you can provide a Visio file path for each InventLocation.

Visio floor plan for invent location

Create a new form and add an ActiveX control. Use the Microsoft Visio Document class.

image

Overwrite the init() method of the form to load the Visio document.

public void init()
{
    InventLocation inventLocation;
    super();

    if(element.args() &&
       element.args().record() &&
       element.args().record().TableId == tableNum(inventLocation))
    {
        inventLocation = element.args().record();
        if(inventLocation.ERPFilenameOpen != "")
        {
            Visio.Load(inventLocation.ERPFilenameOpen);
        }
    }
}

Create a display menu item for the form and add it to the InventLocation form. Make sure to set the InventLocation as data source for the menu item. Now if you have created a Visio document and provided the file path in the InventLocation record, by clicking on the menu item you can see a graphical representation of your warehouse.

Open Invent Location floor plan from Dynamics AX