Most modifications made to standard AX fall under the following categories:
We will demonstrate this in the form of a series of modifications, further integrating our fleet management application with standard AX. To demonstrate this, we will associate our haulier table with the vendor table so that we can identify a haulier as a supplier. Vehicles tracked by our Fleet Management System might be supplied by external hauliers. In order to track this, we require the following:
This first part seems easy enough; we just need to drag the EDT for the supplier account into our vehicle table. To work out which EDT we should use, a good trick is to do the following:
VendTable.AccountNum
. Open the AOT and then the properties for this field. The ExtendedDataType is VendAccount
.VendAccount
in the AOT and drag it into our ConFMSVehicleTable
vehicle table.To mark a vendor as a vehicle service provider, the most obvious change would be to add a flag to the VendTable
table. The problem here is that over time, with the addition of other similar requirements, we could acquire many checkboxes. This increases the chances of errors due to incorrect data setup; the easier we make the setup of maintenance of the master, the better.
We will instead try to use an existing functionality, and there are several possibilities that we are going to consider. The first two are explained here:
TableGroupAll
pattern or provide the ability to store a query that allows the user to define the criteria of a vehicle service provider.An example of the TableGroupAll
pattern can be found by going to Accounts payable | Setup | Vendor posting profiles. In order to determine whether a supplier matches, code must be written; we can't simply use the criteria in a query.
You can see the query method by navigating to Warehouse management | Setup | Labor standard | Labor standards. Here, we have two buttons, Select items and Select locations, that allow the user to associate items and locations with a labor standard using a user-defined query. Both of these methods will be covered in the Exception handling section in Chapter 10, Advanced Development Techniques.
However, it turns out that no modification is needed to do this; Dynamics AX provides this ability in the form of the procurement catalogue.
The procurement catalogue feature allows vendors to be "tagged" as being approved to provide goods and services from one or more categories. The catalogue is a hierarchy of item categories, whereby if the vendor is tagged with a high-level category, then it is also expected to be able to supply items from a more specialist category.
Our requirement almost exactly matches the purpose of the procurement catalogue. This demonstrates the importance of broad system knowledge—so that we can identify when a requirement does and does not require significant modifications. It is common that, as we start work, we notice that the system can already perform the task. AX is a highly functional system, and a consultant or key user can miss this. We can also see that changes are requested that could cause a performance issue or conflict with the way AX is designed, ricking regression on when updates are applied. We should push back to the consultant or key user and discuss the correct action. Everyone involved will want the footprint of our modifications to be as small as possible and, where a modification is necessary, be sympathetic to AX's design.
To use the procurement catalogue, we simply choose a procurement category to represent a vehicle service provider and then tag the vendors as needed. We will still require a system parameter to know which category identifies vehicle service providers, and we will also need to write a lookup so that we allow only suitable suppliers to the vehicles.
First, we will configure a vendor to be a vehicle supplier. In the USMF
company of the Contoso demo dataset, the procurement category contains a VEHICLES
category. We will assign this to US-102
and Tailspin Parts
:
Our requirement is to be able to mark the purchase orders as vehicle service orders, which we will do by checking whether the category to which the supplier is assigned is at or below the VEHICLES
category. Therefore, we need to add a system parameter that will define which category identifies vehicle service providers. We will use this parameter to identify purchase orders as vehicle service orders, so we will add it to the Vendor parameters form. To do this, follow these steps:
We need to know which EDT to use in the same way as we did for the VendAcount
EDT. Navigate to USMF | Accounts payable | Common | All vendors, select the General tab, and click on Categories.
Right-click on the Category field and select Personalization. This tells us that the form is DirPartyEcoResCategory
and the field is VendCategory.Category
.
The EDT we need is EcoResCategoryId
. Drag this onto the field list of the VendParameters
table. As usual, click on Yes when prompted to add a foreign key relation.
ConFMSServiceCategoryId
. Descriptive field names are critical for getting maintainable code. Set Label to Vehicle service category.'RelatedTableRole' conflicts with another 'RelatedTableRole' on relation EcoResCategory on table VendParameters.
EcoResCategory1
. We should make this obvious with a proper name. Rename this to ConFMSServiceCategory
. This won't fix the error but a correct name is very important.EcoResCategory
relation. Enter ConFMSServiceCategory
in this property.VendParameters
form, you will notice a suitable field group to use—Vendor
. Drag your new field into this field group.VendParameters
form. The drop-down list for the new field isn't very useful; it's just a flat list of categories. We need a nice tree view lookup.The
ConFMSServiceCategoryId
field is a record ID, so in order to make this usable for the user, we have to show data that the field's relation references. Dynamics AX provides a special control for this, called ReferenceGroup
.
By adding the field to the Vendor
field group, AX will automatically create the control on the forms that reference this field group. Since the record ID field has a primary-key-based foreign key relation, AX will create a ReferenceGroup
control.
In the AOT, navigate to Forms | VendParameters and then look at the properties of the Vendor_ConFMSServiceCategoryId
control. This is in Designs | Design | Tab | GeneralBody | Vendor.
The control has a property, called ReferenceField, that has a value for the ConFMSServiceCategoryId
reference field, and also one for ReplacementFieldGroup. This references a field group on the referenced table in order to replace the record ID field with one or more fields from the reference table.
It defaults to AutoIdentification
, in this case showing the Name
field. You can see the fields added from the field group by expanding the control.
The ReferenceGroup
form controls are different from other bound form controls in that they allow us to see fields from the referenced table. They do this by creating a reference data source as an outer join. This happens behind the scenes and is constructed using the table relation.
To use a custom lookup with reference group fields, we override the lookupReference
method. The structure of this method is as follows:
public Common lookupReference(FormReferenceControl _formReferenceControl)
By investigating how Microsoft made the lookup work on the DirPartyEcoResCategory
form, we see that they overrode the lookupReference
and resolveReference
methods. Since the category hierarchy is a complicated structure, Microsoft has provided a helper class for us.
For the lookup, the key method is EcoResCategory::lookupCategoryHierarchy
. This requires a form control and a hierarchy to select from. Dynamics AX has the ability to have many types of hierarchies. We want the user to see those in the procurement category. This is done by locating the EcoResCategoryHierarchy
for the EcoResCategoryNamedHierarchyRole::Procurement
role.
To write the lookup method, follow these steps:
VendParameters
form, so we drag this from the AOT to the Forms node of our project.VendParameters
form, go to Data Sources | VendParameters | Fields | ConFMSServiceCategoryId | Methods.public Common lookupReference(FormReferenceControl _formReferenceControl) { Common category; EcoResCategoryHierarchy hierarchy; EcoResCategoryHierarchyRole role; select firstonly * from hierarchy join role where hierarchy.RecId == role.CategoryHierarchy && role.NamedCategoryHierarchyRole == EcoResCategoryNamedHierarchyRole::Procurement; category = EcoResCategory::lookupCategoryHierarchy( _formReferenceControl, hierarchy); return category; }
The resolveReference
method is triggered if we enter a category manually. If we enter the VEHICLES
category, we want to ensure that it finds a reference within the procurement hierarchy.
The code is actually similar; Microsoft provides a helper function to resolve the reference, and we have to supply the form control and the EcoResHierarchy
record for the procurement role. The code we need is as follows:
public Common resolveReference(FormReferenceControl _formReferenceControl) { Common category; EcoResCategoryHierarchy hierarchy; EcoResCategoryHierarchyRole role; select firstonly * from hierarchy join role where hierarchy.RecId == role.CategoryHierarchy && role.NamedCategoryHierarchyRole == EcoResCategoryNamedHierarchyRole::Procurement; category = EcoResCategory::resolveCategoryHierarchy( _formReferenceControl, hierarchy); return category.RecId ? category : null; }
The last line introduces a new command—the inline if
statement. This is similar to C#, and is defined as follows:
<boolean condition> ? <statement if true> : <statement if false>
We can now enter values directly and be sure that the reference is resolved and the correct value is set in the table.
Most of the time, the lookups we would create are based on string controls, such as the item lookup on the sales order line. A lookup on form control is performed by the control's lookup
method. When a form control is bound to a data source, it calls the data source field's lookup
method. This is where we would make the change to alter the lookup.
The following example is from the InventTable
.lookupItem table:
public client static void lookupItem(FormStringControl _ctrl) { SysTableLookup sysTableLookup = SysTableLookup::newParameters( tableNum(InventTable),_ctrl); Query query = new Query(); QueryBuildDataSource queryBuildDataSource = query.addDataSource(tableNum(InventTable)); sysTableLookup.addLookupField(fieldNum(InventTable,ItemId)); sysTableLookup.addLookupMethod(tableMethodStr( InventTable,defaultProductName)); sysTableLookup.addLookupMethod(tableMethodStr( InventTable,itemGroupId)); sysTableLookup.addLookupField(fieldNum( InventTable,NameAlias)); sysTableLookup.addLookupField(fieldNum(InventTable,ItemType)); sysTableLookup.addSelectionField(fieldNum( InventTable,Product)); sysTableLookup.parmQuery(query); sysTableLookup.performFormLookup(); }
A lookup
method is broken down as follows:
SysTableLookup
class with a primary data source and a form control.SysTableLookup
object.SysTableLookup
object and tell it to perform the lookup.To use this code, we would simply override the data source field's lookup method, as follows:
public void lookup(FormControl _formControl, str _filterStr) { InventTable::lookupItem(_formControl); }
You may notice that we aren't handling a return from the lookup
method. The lookup
class updates the form control for us if the user selects a record.
Using the procurement category not only provides the user with a method of identifying vendors as vehicle service providers, but also works with a standard process. We also have a minimal footprint on standard code.
This does present one problem, however; it is difficult to determine whether the vendor is a vehicle service provider. In order to solve this, VendTable
should have a method that returns information on whether it is a vehicle service provider or not. This method sits naturally on the vehicle table, so it can be used wherever necessary.
We will write this method as a display method, so we can add it to a form or info part, if desired. Since we intend to use field groups wherever possible, we will create an EDT for the return type; in this way, a label will be added automatically.
Create the ConFMSIsVehicleServiceProvider
EDT as a type enum, extending NoYesId
and with the Vehicle service provider
label.
Using personalization on the Vendor Categories form, we can determine that the list of categories a supplier is associated with is in the VendCategory
table. We need a way to work out whether this category is a descendant of the Vehicle service provider category in vendor parameters.
Since this structure is complicated, we would expect a helper function to exist. The way to find such a function would be to first look at the EcoResCategory
table, and then look for a class that provides this functionality. The EcoResCategory
table has a method called getAscendants
. This returns a set of records that are parents of the current category record.
We should test this first, to check whether it works for us. We will write a job that shows all ascendants of the Cars
category. We are expecting to see VEHICLES
and CORP PROCUREMENT CATEGORIES
.
Since the getAscendants
method returns a set of records, we will use a new method to iterate through them. The job is as follows:
static void ConFMSTestCategoryHierarchy(Args _args) { EcoResCategory ascendants, child; select child where child.Name == "Cars"; ascendants = child.getAscendants(false); while(ascendants.RecId != 0) { info(ascendants.Name); next ascendants; } }
The next
command will select the next record in the set of records returned by the getAscendants
method, and the while
command will execute as long as there is an active record in ascendants. The result should look like what is shown in the following screenshot:
Now that we know that the method works, we can write our display
method. Create a new method on the VendTable table and write the following lines of code:
public display ConFMSIsVehicleServiceProvider conFMSDispIsVehicleServiceProvider() { EcoResCategoryHierarchy hierarchy; EcoResCategoryHierarchyRole role; VendCategory vendCategories; EcoResCategory ascendantCategories, category; VendParameters parm; EcoResCategoryId serviceCategoryId; parm = VendParameters::find(); serviceCategoryId = parm.ConFMSServiceCategoryId; // if there is not category, return No now. if(serviceCategoryId == 0) { return NoYes::No; } select firstonly * from hierarchy join role where hierarchy.RecId == role.CategoryHierarchy && role.NamedCategoryHierarchyRole == EcoResCategoryNamedHierarchyRole::Procurement; // if a procurement hierarchy has not been // configured, return No now if(hierarchy.RecId == 0) { return NoYes::No; } while select vendCategories where vendCategories.VendorAccount == this.AccountNum && vendCategories.VendorDataArea == this.dataAreaId join category where category.RecId == vendCategories.Category && category.CategoryHierarchy == hierarchy.RecId { // Check the current level if(category.RecId == serviceCategoryId) { return NoYes::Yes; } // get all parent category records for the // current category ascendantCategories = category.getAscendants(); while(ascendantCategories.RecId != 0) { if(ascendantCategories.RecId == serviceCategoryId) { return NoYes::Yes; } } } return NoYes::No; }
Since the EDT extends NoYesId
, we return its base enum value of NoYes::No
or NoYes::Yes
.
Our organization needs us to provide a method to allow reporting on purchase orders for vehicle service providers. The solution we have decided upon is to record against a purchase order whether or not the order was a vehicle service order. The order should default as a vehicle service order if the supplier is a vehicle service provider, and allow the user to uncheck this flag. Orders that are not for vehicle service providers should not be allowed to have this flag set.
This provides the opportunity to demonstrate event handling, which allows us to hook into AX functionality with minimum footprint. We will also cover some of the tables used in the purchase order process.
First, the requirement to "record against a purchase order" is there to highlight common mistakes. Although fields on a purchase order can be used for reporting purposes, they shouldn't be. They should only be used to help manage the order-to-invoice process. Once invoiced, the purchase order can be deleted, and many organizations routinely delete invoiced purchase orders, either at the point of invoice or after a period of time.
To be used for reporting, we need this information to be stored somewhere more suitable, and this depends on why we need it. If it is for statistical reporting, it should be on the invoice journal record. Dynamics AX follows standard naming conventions for both sales and purchase orders, and the documents they generate fall under the same framework—FormLetter
.
Each time a document, such as a purchase order invoice, is posted, it creates a set of records in a journal set. This allows the document to be reproduced as it was when first printed, and allows reprint on the invoice even if the order has been deleted.
These tables follow a naming convention: <module><document>
followed by Jour
for the header and Trans
for the lines. The prefix for vendor documents is Vend
and for customer documents is Cust
. The following table shows the standard journal tables:
Module |
Document |
Journal tables |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
In our case, it is statistical reporting, so this should go against VendInvoiceJour
. This was determined using the Personalization form from the Invoice journal form.
The first step is to flag the order as a vehicle service order and allow the user to uncheck this. We will do this by adding a field to PurchTable
, but we will handle the various events using event handlers.
To do this, follow these steps:
ConFMSVehicleServiceOrder
of the enum type. It extends NoYesId
. Set the label to Vehicle service order
.PurchTable
table into your project.PurchTable
.Administration
field group.ConFMSPurchVehicleOrderHandler
.This method means that we will have one or more event subscriptions, which is perfect in our situation—it is a new module and allows it to be installed without any dependency on other customer elements. Sometimes, when making more generalized modifications, it is more convenient to have one class that handles the table or class, for example, ConPurchTableHandler
.
initFromVendTable
and validateField
table methods, as we need to default our new fields and verify that it isn't set for vendors who aren't vehicle service providers. Right-click on the new class and go to New | Pre-post event handler.initFromVendTableEventHandler
.VendTable
record being passed to it. On inspection, we notice that the method has a _vendTable
parameter, which we will use. The following lines of code will get the VendTable
record, check whether the vendor is a vehicle service provider, and set our new field accordingly:public static void initFromVendTableEventHandler(XppPrePostArgs _args) { VendTable vendTable; PurchTable purchTable = _args.getThis(); if(!_args.existsArg("_vendTable")) throw error(strFmt("@SYS22828", staticMethodStr( ConFMSPurchVehicleOrderHandler, initFromVendTableEventHandler))); vendTable = _args.getArg("_vendTable"); purchTable.ConFMSVehicleServiceOrder = NoYes::No; if(vendTable.conFMSDispIsVehicleServiceProvider() == NoYes::Yes) { purchTable.ConFMSVehicleServiceOrder = NoYes::Yes; } }
validateField
method on PurchTable
so that we allow the field to be set only if the supplier is a vehicle service supplier. This time, we need to read the _fieldToCheck
parameter and alter the return value of the method. This is done by the following lines of code:public static void validateFieldEventHandler(XppPrePostArgs _args) { VendTable vendTable; PurchTable purchTable = _args.getThis(); RefFieldId fieldToCheck; boolean ret = _args.getReturnValue(); // no point checking further if the validation // has already failed. if(!ret) return; if(!_args.existsArg("_fieldIdToCheck")) { throw error(strFmt("@SYS22828", staticMethodStr(ConFMSPurchVehicleOrderHandler, initFromVendTableEventHandler))); } fieldToCheck = _args.existsArg("_fieldIdToCheck"); switch(fieldToCheck) { case fieldNum(PurchTable, ConFMSVehicleServiceOrder): vendTable = purchTable.vendTable_OrderAccount(); if(vendTable.conFMSDispIsVehicleServiceProvider() && purchTable.ConFMSVehicleServiceOrder == NoYes::Yes) { ret = checkFailed(strFmt( "Supplier %1 is not a vehicle service supplier", purchTable.OrderAccount)); } break; } if(!ret) { _args.setReturnValue(ret); } }
initFromVendTable
and validateField
methods.PurchTable
table, expand Methods, right-click on the initFromVendTable
method, and select New Event Handler Subscription.validateField
and configure it as follows:validateField
method would have the final say on the return value.We should test this now by creating a new order for a supplier that is a vehicle service provider, and one order for a supplier that isn't.
The next stage is to add our field to the VendInvoiceJour
table and set the field based on the PurchTable
record that it is created from. This is done by the following steps:
VendInvoiceJour
table into the project. Then add a new field from the EDT just as you did for PurchTable
.initFromPurchTable
.initFromPurchTable
method, we will add an event handler on the ConFMSPurchVehicleOrderHandler
class, and create a new pre-post event handler method called vendJourInitFromPurchTableEventHandler
.VendInvoiceJour
record and set our new field based on the method's _purchTable
parameter, which is done by the following lines of code:public static void vendJourInitFromPTEventHandler(XppPrePostArgs _args) { PurchTable purchTable; VendInvoiceJour vendInvoiceJour = _args.getThis(); if(!_args.existsArg("_purchTable")) { throw error(strFmt("@SYS22828", staticMethodStr(ConFMSPurchVehicleOrderHandler, vendJourInitFromPurchTableEventHandler))); } purchTable = _args.getArg("_purchTable"); vendInvoiceJour.ConFMSVehicleServiceOrder = purchTable.ConFMSVehicleServiceOrder; }
VendInvoiceJour.initFromPurchTable
method as before.You may notice on occasion that the drop-down list for the method is empty. AX uses a pattern to recognize event handler methods, and sometimes, this fails to execute correctly. This can be due to the method name length or compilation errors in the code. You can manually paste the method name, as the important parameter is AOTLink
, which is set by the system based on the Class
and Method
parameters.
3.16.79.147