Monday, November 18, 2019

Certificate issue in D365FO

We use virtual machines to perform development tasks which are provided as images by Microsoft run via Hyper-V on a local server. After the trail period expires the certificates will be expired. Here i have explained how to re activate the certificates

Error details

On your development machine you cannot open the application in the browser anymore and face an error message like
There is a problem with the server
Sorry, the server has encountered an error. It is either not available or it can't respond at this time. Please contact your system administrator.
If you check the event log using the Event Viewer you’ll find a warning message pointing to an ExpiredCertificateException there:
Process information: 
    Process ID: 14516 
    Process name: w3wp.exe 
    Account name: NT AUTHORITY\NETWORK SERVICE 
 
Exception information: 
    Exception type: ExpiredCertificateException 
    Exception message: Expired certificate for id 'C0E503DC8987D25B63897A7BE0B3E34BDCC89F41'.
   at Microsoft.Dynamics.AX.Configuration.CertificateHandler.LocalStoreCertificateHandler.GetCertificatesForId(String id)
etc.
Solution
Find Certificates
You can see the certificates that are relevant here using Manage computer certificates from Windows Start menu. Navigate to Certificates – Local Computer > Personal > Certificates.

In the column Expiration Date you can easily identify the ones that recently expired, in this case
  • DeploymentsOnebox.DaxRunnerTokenUserCertificate.pfx
  • DeploymentsOnebox.LcsClientCertificate.pfx
  • DeploymentsOnebox.MRClientCertificate.pfx
  • DeploymentsOnebox.SessionAuthenticationCertificate.pfx
Identify Thumbprint of Expired Certificate
Certificates get accessed by their thumbprint which is a 40-digit hexadecimal value. You can see it by double-clicking the certificate in the certificates viewer and open the Details tab.

Copy the thumprint values and make sure all letters are capital and remove all spaces.
example:43082FE50B4D02562C89EA728B2359C598E84886 
You can use any text editor or event VS, my preferred one for such operations is Notepad++. Make sure to run it as Administrator so you can save the files later without any issues. All three files we need are located in 

Clone the Certificate

Use PowerShell (and Run as Administrator, of course) to execute the following command (and make sure to replace the thumbprint with the one you just identified):
Set-Location -Path "cert:\LocalMachine\My"
$OldCert = (Get-ChildItem -Path 43082FE50B4D02562C89EA728B2363C598E84886)
New-SelfSignedCertificate -CloneCert $OldCert -NotAfter (Get-Date).AddMonths(999)
999 is the number of months the certificate will be valid until. Should be fine for quite some time.
The execution of this creates some output – copy and note the thumbprint of the newly created certificate. In the certificate manager you can see the clone (you might have to Refresh after a right click on the folder on the left).

Update References

The new thumprint values we need to update in web config, wif config and wif services config (take backup the files before modification). The files are available in the below path 

C:\AOSService\webroot:
  • web.config
  • wif.config
  • wif.services.config
Find the old thumprint 4(3082FE50B4D02562C89EA728B2359C598E84886) and replace new thumprint value which is generated.

Repeat this for all expired certificates. 

Reboot

Restart Batch, Management reporter, DMF, SQL and IIS services.
Restart the server 


Form event handlers in D365FO

Form datasource from xFormRun
[FormEventHandler(formStr(SomeForm), FormEventType::Initialized)]
public static void SomeForm_OnInitialized(xFormRun sender, FormEventArgs e)
{
    FormDataSource MyRandomTable_ds = sender.dataSource(formDataSourceStr(SomeForm, MyRandomTableDS));
    ...
}

Get FormRun from form datasource

[FormDataSourceEventHandler(formDataSourceStr(MyForm, MyRandomTableDS), FormDataSourceEventType::Written)]
public static void MyRandomTableDS_OnWritten(FormDataSource sender, FormDataSourceEventArgs e)
{
    FormRun formRun = sender.formRun() as FormRun;
    // you can even call custom methods (I think IntelliSense won't work though)
    formRun.myCustomMethod();
}

Get FormRun from form control

[FormControlEventHandler(formControlStr(MyForm, MyButton), FormControlEventType::Clicked)]
public static void MyButton_OnClicked(FormControl sender, FormControlEventArgs e)
{
   FormRun formRun = sender.formRun() as FormRun;
   formRun.myCustomMethod();
}

Access form control from xFormRun

[FormEventHandler(formStr(SomeForm), FormEventType::Initialized)]
public static void SomeForm_OnInitialized(xFormRun sender, FormEventArgs e)
{
    // set the control to invisible as an example
    sender.design().controlName(formControlStr(SomeForm, MyControl)).visible(false);
}

Get current record in form control event

[FormControlEventHandler(formControlStr(SomeForm, SomeButton), FormControlEventType::Clicked)]
public static void SomeButton_OnClicked(FormControl sender, FormControlEventArgs e)
{
    // as an example the datasource number is used for access; I perceive the formDataSourceStr as more robust
    SomeTable callerRec = sender.formRun().dataSource(1).cursor();
}

Convert Common and use DataEventArgs

[DataEventHandler(tableStr(AnyTable), DataEventType::ValidatedWrite)]
public static void InventLocation_onValidatedWrite(Common sender, DataEventArgs e)
{
    // convert Common to AnyTable
    AnyTable anyTable = sender;
    // the DataEventArgs actually are ValidateEventArgs and can be converted
    ValidateEventArgs validateEventArgs = e;
    // the ValidateEventArgs carry the validation result (so far)
    boolean ret = validateEventArgs.parmValidateResult();
    // the table has some additional validation logic and gives back the result
    ret = anyTable.doSomeAdditionalCustomValidation(ret);
    // provide the args with the validation result
    validateEventArgs.parmValidateResult(ret);
}

Use the onValidatedFieldValue event properly

[DataEventHandler(tableStr(SomeTable), DataEventType::ValidatedFieldValue)]
public static void SomeTable_onValidatedFieldValue(Common sender, DataEventArgs e)
{
    SomeTable someTable = sender;
    // the clue is to know that the DataEventArgs actually are ValidateFieldValueEventArgs and that you can get the field name from them
    ValidateFieldValueEventArgs validateEventArgs = e;
    boolean ret = validateEventArgs.parmValidateResult();
    FieldName fieldName = validateEventArgs.parmFieldName();
    switch (fieldName)
    {
        case fieldStr(SomeTable, SomeCustomField):
            ... do some magic
            break;
    }
    validateEventArgs.parmValidateResult(ret);
}

Use the MappedEntityToDataSource event

[DataEventHandler(tableStr(MyTableEntity), DataEventType::MappedEntityToDataSource)]
public static void MyTableEntity_onMappedEntityToDataSource(Common _sender, DataEventArgs _eventArgs)
{
    DataEntityContextEventArgs eventArgs = _eventArgs;
    MyTableEntity entity = _sender;
    if (eventArgs.parmEntityDataSourceContext().name() == dataEntityDataSourceStr(MyTableEntity, MyTable))
    {
        MyTable myTable = eventArgs.parmEntityDataSourceContext().getBuffer();
        ... do some magic with it
    }
}

Code to identify data type and generate the data template

The below code will help you to generate the data template with field data type in csv file

public static void main(Args _args)
{
DictTable dictTable = new DictTable(tableNum(HcmEmployeeEntity));
FieldId fieldId = dictTable.fieldNext(0);
DictField dictField;
DictType dictType;
DictEnum dictenum;
CommaIo file;
container line;
str help;
str helpdefined;
#define.filename(@”C:\Temp\employee.csv”)
#File
file = new CommaIo(#filename, #io_write);
if (!file || file.status() != IO_Status::Ok)
{
throw error(“File cannot be opened.”);
}
while(fieldId)
{
dictField = dictTable.fieldObject(fieldId);
line = [dictField.name(), enum2Str(dictField.baseType()), dictField.label(), enum2Str(dictField.mandatory()), dictField.displayLength(),dictField.stringLen()];
file.writeExp(line);
fieldId = dictTable.fieldNext(fieldId);
}
}

Batch number registration for sales return order.

Hi

The below code will be helpful to register the batch number during sales return order packing slip posting

public void registerInventory(RefRecId _recId, InventQty _qty, InventBatchId _batchId)
{   

       InventTransWMS_Register     inventTransWMS_register;
        TmpInventTransWMS            tmpInventTransWMS;
         InventDim                               inventDim;
        SalesLine                                 salesLine;
        InventTrans                              inventTranslocal

inventTransWMS_register         = inventTransWMS_register::newStandard(tmpInventTransWMS);

            salesLine                       = CustConfirmTrans::findRecId(_custConfirmTransRecId).salesLine();
            inventDim                                   = salesLine.inventDim();
            inventTranslocal                          = InventTrans::findTransId(salesLine.InventTransId, true);
           
            inventDim.inventBatchId            = _inventBatchId;
            inventDim                                    = inventDim::findOrCreate(inventDim);
            inventTranslocal.inventDimId     = inventDim.inventDimId;
                         
            tmpInventTransWMS.clear();
            tmpInventTransWMS.initFromInventTrans(inventTranslocal);
            tmpInventTransWMS.ItemId        = inventTranslocal.ItemId;
            tmpInventTransWMS.InventQty     = _qty;
            tmpInventTransWMS.insert();

            inventTransWMS_register.writeTmpInventTransWMS(tmpInventTransWMS,
                                                            inventTranslocal,
                                                            inventDim);
     
            inventTransWMS_register.updateInvent(inventTranslocal);
}

JumpRef method to call the form

public void jumpRef()
{
MenuFunction menuFunction;
super();
menuFunction = new MenuFunction(menuitemdisplaystr(CustTable), MenuItemType::Display);
menuFunction.run();
}

Hide the batch tab in Dialog window in D365FO

1. Create UI builder class
2. Override the dialogPostRun method in UI builder class

protected void dialogPostRun()
{

SysOperationDialog  sysOperationDialog;
DialogTabPage         batchTabPage;
FormRun                   formRun;

super();
sysOperationDialog  = this.dialog() as SysOperationDialog;
formRun                   = sysOperationDialog.formRun();
batchTabPage          = sysOperationDialog.batchDialogTabPage();
formRun.selectControl(dialogTabPage.control());

}

Open the form in Edit mode using x++

FormRun         formRun;
Args                 args;
MenuFunction  menuFunction;
args = new Args();
args.record(VendTable::find(‘1001’));
menuFunction = new MenuFunction(menuitemDisplayStr(VendTable), MenuItemType::Display);
if(menuFunction)
{
menuFunction.openMode(OpenMode::Edit);
formRun = menuFunction.create(args);
if(formRun)
formRun.run();
}