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

PowerBI finance report grouped by main accounts and cost center

A customer recently asked to create a PowerBI report in order to group and compare ledger postings. The report had to meet the following requirements:

  • Compare two time periods
  • Group postings by account and type (e.g. Assets)
  • Postings on a certain cost center need to be shown separately
  • Grouping of accounts and cost centers are defined by a key user in Excel
    e.g. 1001 – 1104 and 5001 – 5002 are “Assets”
  • Cost center groups are defined by using wildcard style
    e.g. 601500 – 61500 and Costcenter ?6?? is Development/Compliance

Dynamics AX ledger postings grouped by account and cost center

Report Data from Dynamics AX

First, we loaded the report data from Dynamics AX into PowerBI. The data contained the LedgerJournalTrans joined with the LedgerAccountView. The Dimenions were renamed to Department, CostCenter and CostUnit.

select
T.TRANSDATE, T.AMOUNTCURCREDIT, T.AMOUNTCURDEBIT, T.ACCOUNTNUM, V.ACCOUNTNAME, DIMENSION as DEPARTMENT, DIMENSION2_ as COSTCENTER, DIMENSION3_ as COSTUNIT
from LedgerJournalTrans as T
join LEDGERACCOUNTVIEW as V
on T.ACCOUNTNUM = V.ACCOUNTNUM
where ACCOUNTTYPE <= 2

Report Definition Data

Next we defined and loaded the report definition from Excel. We used to sheets, one for the definition of the two time frames and one for the group definition

date range for ledger postings

grouping definition for ledger postings

Like initially described the Accounts sheet defines the grouping of postings regarding their account number. For example in line 2 and 3 postings on  accounts 1101 .. 1104 and 5001 .. 5002 shall be grouped as “Assets”. Postings on accounts 601500 .. 606300 with a cost center where the 2nd character is 6 shall be grouped separately as “Development/Compliance” regardless if they are also part of the group “Expenses”.

Both Excel worksheets were loaded into PowerBI. The Accounts was modifed to replace an empty value in the Costcenter column with the text “%”. This was done to use the Costcenter value in a SQL statement with a Like clause (see section “Calling function from the account definition”).

Query Parameter

We added 3 parameters to the PowerBI called FromAccountNum, ToAccountNum and CostCenter. The default values are the smallest account number for the FromAccountNum, the largest account number for the ToAccountNum and the text % for the CostCenter.

PowerBI parameters

Next we changed the query of LedgerJournalTrans and added the parameter to the query. This can be done used “Edit query” on the data set and opening “Advanced Editor”

Parameters in PowerBI query

The new query text in the advanced editor looked like this

let
Source = Sql.Database(„localhost“, „DynamicsAx“, [Query=“select T.TRANSDATE, T.AMOUNTCURCREDIT,T.AMOUNTCURDEBIT, T.ACCOUNTNUM,V.ACCOUNTNAME,DIMENSION as DEPARTMENT,DIMENSION2_ as COSTCENTER,DIMENSION3_ as COSTUNIT #(lf)from LedgerJournalTrans as T#(lf)join LEDGERACCOUNTVIEW as V on T.ACCOUNTNUM = V.ACCOUNTNUM #(lf)where ACCOUNTTYPE <= 2 AND T.ACCOUNTNUM >= „&FromAccountNum&“ AND T.ACCOUNTNUM <= „&ToAccountNum&“ AND T.DIMENSION2_ like ‚“&CostCenter&“‚ #(lf)order by T.ACCOUNTNUM,T.TRANSDATE#(lf)“, CreateNavigationProperties=false])
in
Source

With the parameter in the query and the default values set to the parameters the dataset did not change. Next we added a new function to the LedgerJournalTrans. This can be done from the context menu of the query “Create Function”. PowerBI inspects the query statement and creates function parameter for each parameter in the query. In this case FromAccountNum, ToAccountNum and CostCenter.

Calling function from the Account Definitions

In PowerBI a function call can be used like a row wise join. A function can be added by using the used defined function button in the query editor. So we added the function call to the Accounts dataset, i.e. each account definition row fetches all postings from the LedgerJournalTrans with the corresponding accounts and costcenter.

PowerBI calling user defined function

The query parameter are mapped to the fields in the Accounts table.

Parameter values for used defined function

PowerBI will popup a warning that function calls can have a security impact. In the actual version (Mai 2018) PowerBI was quite annoying with security warnings and required a restart (close&open) to stop asking again and again. Finally, expand the Accounts table and the function call results by clicking on the Arrow Right-Left Button next to “GetPostings”. Per default PowerBI adds the fields from the function call result with the function name prefix e.g. GetPostings.Transdate, GetPostings.AmountCurCredit, etc.

As you can see below the Account definition 1101 – 1104 was expanded with all resulting rows from the LedgerJournalTrans that have an account between 1101 and 1104 and any Costcenter (%)

Expanding used defined function call results

Calculate Period Amounts

To get the amount values for each of the two periods, defined in the Daterange Excel sheet, we added 4 additional columns to the Accounts. A debit and credit column for period 1 and period 2.

Ledger postings in period

The code looks like this

CreditDate1 = IF(Accounts[GetPostings.TRANSDATE]>=FIRSTDATE(Daterange[FromDate1]);IF(Accounts[GetPostings.TRANSDATE]<=FIRSTDATE(Daterange[ToDate1]);Accounts[GetPostings.AMOUNTCURCREDIT];0);0)

 

DebitDate1 = IF(Accounts[GetPostings.TRANSDATE]>=FIRSTDATE(Daterange[FromDate1]);IF(Accounts[GetPostings.TRANSDATE]<=FIRSTDATE(Daterange[ToDate1]);Accounts[GetPostings.AMOUNTCURDEBIT];0);0)

 

CreditDate2 = IF(Accounts[GetPostings.TRANSDATE]>=FIRSTDATE(Daterange[FromDate2]);IF(Accounts[GetPostings.TRANSDATE]<=FIRSTDATE(Daterange[ToDate2]);Accounts[GetPostings.AMOUNTCURCREDIT];0);0)

 

DebitDate2 = IF(Accounts[GetPostings.TRANSDATE]>=FIRSTDATE(Daterange[FromDate2]);IF(Accounts[GetPostings.TRANSDATE]<=FIRSTDATE(Daterange[ToDate2]);Accounts[GetPostings.AMOUNTCURDEBIT];0);0)

Display results in matrix

Finally we used a matrix to display the values from the Accounts dataset and grouped it by Name and Accounts.

PowerBI matrix for ledger postings

Security considerations

Injecting range values in a query is not the best way to do this. A better way would be to refactor the LedgerJournalTrans query into a stored procedure and provide the FromAccountNum, ToAccountNum and Costcenter as parameter to the SP.

Print Customer and Item specific labels in Dynamics 365 FO

This is an example how to print customer and item specific labels in Dynamics 365 for Finance and Operations. The labels shall be printed directly within the sales order form at the sales line grid.

Configuration Table

Create a new table containing the following five fields. The table will hold the configuration which report and design to use for which combination of customer and item

Field Purpose
CustAccount Reference to the Customer
ItemId Reference to an Item
ReportName Name of the SSRS report to use
ReportDesign Name of the report design
IsDefault NoYes

Create Report Classes

Create a new class called ItemLabelContract, which will be the data contract class for the report. It contains the number of labels to be printed, the SalesId and the LineNum to reference the calling SalesLine.

[DataContractAttribute]
class ItemLabelContract
{
NumberOf numberOf;
SalesId salesId;
LineNum lineNum;

    [DataMemberAttribute]
public NumberOf parmNumberOf(NumberOf _numberOfLabels = numberOf)
{
numberOf = _numberOfLabels;
return numberOf;
}

    [DataMemberAttribute]
public SalesId parmSalesId(SalesId _salesId = SalesId)
{
SalesId = _salesId;
return SalesId;
}

    [DataMemberAttribute]
public LineNum parmLinNum(LineNum _lineNum = LineNum)
{
LineNum = _lineNum;
return LineNum;
}

}

Create a new controller class called ItemLabelContract. This class will be called from the menu item in the sales order form. It takes the SalesLine as parameter and decides which report and design to use.

class ItemLabelController extends SrsReportRunController
{
public static void main(Args _args)
{
ItemLabelSetup setup;
SalesLine salesLine = _args.record();

ItemLabelController ctrl = new ERPItemLabelController();
ctrl.parmArgs(_args);

        select firstonly setup where
setup.CustAccount == SalesLine.salesTable().CustAccount &&
setup.ItemId == SalesLine.ItemId;

if(setup.RecId == 0)
{
select firstonly setup where
setup.CustAccount == SalesLine.salesTable().CustAccount &&
setup.ItemId == „“;
}

        if(setup.RecId == 0)
{
select firstonly setup where setup.ItemId == SalesLine.ItemId &&
setup.CustAccount == „“;
}

        if(setup.RecId == 0)
{
select firstonly setup where setup.IsDefault == NoYes::Yes;
}

if(setup.RecId == 0)
{
error(„No report selected“);
}
else
{
str reportName = strFmt(„%1.%2″,
setup.ReportName,
setup.ReportDesign);

            ctrl.parmReportName(reportName);
ctrl.startOperation();
}

    }

    protected void prePromptModifyContract()
{
ItemLabelContract contract =
this.parmReportContract().parmRdpContract() as ItemLabelContract;

SalesLine salesLine = this.parmArgs().record();

        contract.parmSalesId(SalesLine.SalesId);
contract.parmLinNum(SalesLine.LineNum);
contract.parmNumberOf(SalesLine.SalesQty);

        super();
}

}

Create a temporary (InMemory) table called ItemLabelTmp for the report data set including the following five columns:

Field Purpose
ItemId Item Id
ItemName Name
ItemBarcode Barcode value
BarcodeString Encoded barcode string
VendName Name of the vendor, here “Contoso”

Create the report data provide class. Overwrite the prePromptModify method and populate the contract with the number of labels to print taken from the SalesQty, the SalesId and LineNum.

[SRSReportParameterAttribute(classStr(ItemLabelContract))]
class ItemLabelDP extends SrsReportDataProviderBase
{
ItemLabelTmp        itemLabelTmp;

    [SrsReportDataSetAttribute(‚ItemLabelTmp‘)]
public ItemLabelTmp getItemLabelTmp()
{
select * from itemLabelTmp;
return itemLabelTmp;
}

    public void processReport()
{
ItemLabelContract contract = this.parmDataContract()
as ItemLabelContract;
SalesLine salesLine;

        select firstonly SalesLine where
SalesLine.SalesId == contract.parmSalesId() &&
SalesLine.LineNum == contract.parmLinNum();

        BarCodeString bcstring = „“;
InventItemBarcode itemBarcode = InventItemBarcode::findItemId(
SalesLine.ItemId,false,false);

if(ItemBarCode.RecId > 0)
{
Barcode barcode = Barcode::construct(BarcodeSetup::find(
itemBarcode.barcodeSetupId).barcodeType);
barcode.string(true,ItemBarCode.itemBarCode,
BarcodeContentType::Item);
Barcode.encode();
bcstring = Barcode.barcodeStr();
}

        for(int i = 0; i < contract.parmNumberOf(); i++)
{
itemLabelTmp.clear();
itemLabelTmp.ItemId = SalesLine.ItemId;
itemLabelTmp.ItemName = SalesLine.itemName();
itemLabelTmp.ItemBarCode = SalesLine.BarCode;
itemLabelTmp.VendName = „Contoso“;
itemLabelTmp.BarCodeString = bcstring;
itemLabelTmp.insert();
}

    }

}

Create a report with multiple designs

Create a new report and add the report data provider class as source. Create at least two designs. In this example I’ve created two designs, a small and and  large label.

Small Item Label Design

Large Item Label Design

Create Menu Items and Forms

Create a new form using a the Simple List pattern to manipulate the configuration table form. Create a new display menu item for the form and add it e.g. to the accounts receivable module.

Configuration form in Dynamics 365 FO

Create a form extension for the SalesTable form. Create an output menu item form the ItemLabelController class and add it to SalesTable extension e.g. Main > TabPageDetails > DetailsTab > LineView > LineViewTab > LineViewLines > LinesActionPaneStrip > LineOverviewActionTab . Make sure to set the SalesLine as Datasource for the Menu Item.

Menu Item in SalesTable form

Test the labels

Create new sales orders and test the different configurations and labels. Here is a Youtube example.

Youtube Link for Dynamics 365 Finance and Operations Demo

Setup multiple developer VMs for Version Control with Team Services in Dynamics 365 for Finance and Operations

Here is a walkthrough how to connect two Dynamics 365 Finance and Operations developer VMs with VSTS.

Configure Azure AD and setup Visual Studio Team Services

Before you start, you have to add your developers to Azure Active Directory. If you have Azure AD Connect make sure the accounts have been synced to the Cloud. In my case I added two additional users to my Azure AD.

Configure Developer Accounts in Azure AD

Next logon to your Azure Portal using your Organization Account. Create a new service and search for “Team Services”. You should find Visual Studio Team Services (preview).

Create Visual Studio Team Services project in Azure Portal

Create a new instance of VSTS. The basic version of VSTS for 5 users is free. Make sure to use Team Foundation Server as Version Control system. You may choose any Methodology you like, but II ‘d recommend to go for CMMI compatible one.

Create Visual Studio Team Services project in Azure Portal

After the deployment succeeded, logon to your Visual Studio Team Services account, using  the URL https://<ACCOUNTNAME&gt;.visualstudio.com . There you see a new project. Open the project and add your developer users by clicking the (+) icon on the upper left “Members” pane. If everything is configured correctly, you should be able to add your AD users. In my example developer one and developer two.

Add developer accounts to Dynamics 365 FO project

Configure Developer VMs

If you are using the VHD Images from Microsoft, the first thing you should do is to rename the computer. If you don’t rename the VMs you will get errors when mapping the source control on multiple machines. In my case I rename the VMs to “devbox1” and “devbox2”. No domain is needed. Details here.

Rename Dynamics 365 FO developer VM

Configure first Developer Box

After the VM reboots, open Visual Studio 2015 in Admin mode. Depending on your licensing you may need to provide additional credentials to use your subscription. Don’t get confused if this may be your Microsoft ID (aka. Live ID) while you need your Organization Account to access VSTS. Zwinkerndes Smiley  From the menu bar select > Team > Manage Connections. Provide the URL for your VSTS account.

Connect to Visual Studio Team Services

After you have connected to VSTS select the project to work on. Next, from the Team Explorer open the Source Control explorer. Map the root folder to a folder on your developer VM.

Map Source Control Folder in Visual Studio

Afterwards use the source control explorer to create two new folders. One for Visual Studio Projects and one for metadata. This is where the D365 source code artifacts will be stored. Check in you pending changes. This will sync the folders to VSTS.

Map Dynamics 365 FO metadata folder

Now, in the Source Control Explorer open the dropdown Workspace and edit your workspace. Map the metadata folder to C:\AOSService\PackagesLocalDirectory.

Map Dynamics 365 FO metadata folder

From the menu bar > Dynamics 365 > Model Management > Create a new model. Give it a name and complete the wizard. This will ask you to create a new Dynamics X++ project.

Create new Dynamics 365 FO project

In the solution explorer, right click on the project and add to source control.

Check in to Source Control

Add a new element to the project, for example add a new String Extended Datatype called DMOCustomerName. In the solution explorer, right click the project and select build. After a few seconds you should see the console output “Build completed”. Check in all your pending changes.

Next, from the Team Explorer open the Source Control Explorer. You should see the model structure in the tree view. Right click on the metadata folder and select “Add items to folder”. Navigate to your model folder and there to the Descriptor folder. Add the Model Descriptor XML file. Unfortunately you have to do this manually, otherwise the second developer can sync the Folders and Extended Datatypes etc. but will not see the model in the AOT.

Add Dynamics 365 FO Model Descriptor File to Source Control

You can also inspect your code in VSTS

Dynamics 365 FO X++ Source Code

Configure second Developer Box

Make sure that the second VM is properly renamed. Open Visual Studio in Admin mode and connect to VSTS. Logon with the account of the second developer. Select the Dynamics 365 project and again in the Source Control Explorer map the metadata folder to C:\AOSService\ PackagesLocalDirectory. Checkout the latest version of the metadata folder.

Get Latest Version from metadata folder

This will create the model folder in the packages directory.

Model folder created in PackagesLocalDirectory

In Visual Studio open the Dynamics 365 Application Explorer. If the AOT is in classic mode, right click and switch to model view. Scroll down and you we will see the synchronized model and the its software artifacts.

Model in Dynamics 365 FO Application Explorer