Access D365 FinOps Services with OAuth 2.0

So before we jump in how to, we need to understand some basics how OAuth2 works.

well basically OAuth2 is one of the authentication out there, the purpose is to is validate and authenticate with Azure AD, permission granted or not to access the apps. if granted, it will generate “TOKEN”.

now we heading to D365FinOps , in D365 FinOps we have users and all of its security roles, so basically user that can call D365FinOps services (example OData or Custom Services) via OAuth 2.0 should be only legitimate D365 FinOps users.

There is some grant_type for OAuth 2.0 that works with D365 FinOps :
1. Authorization Code : when user access or call some services, they need to provide login information . (works with D365 FinOps, just provide user and password when microsoft login window popup) , then TOKEN generated.
ref : https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow

2. Password Credentials : the user and password credentials is hardcoded in the call, so no popup window login, then TOKEN generated afterward.
ref : https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc

other grant_type seems not working wih D365 FinOps because cant provide legitimate D365 FinOps User in the call parameter.

Now let us try shall we ??
the summary for the tutorial (will update in next blog post) :

1. Setup Apps Registrations in Azure AD and Setup in D365 FinOps
2. Generate Oauth2 Token using grant_type Authorization Code
3. Generate Oauth2 Token using grant_type Password Credentials
4. Testing OAuth2 Token to call OData Services
5. Create Simple D365 FinOps custom services
6. Testing Oauth2 Token to call D365 FinOps custom services.

~fanddy

Create Invoice PO base on Packing Slip/Product Receipt by X++

This code will created pending invoice base on VendPackingSlipJour data and posting that’s pending invoice at the end of code.

PurchFormLetter             purchFormLetter;

    PurchTable                  purchTable;
    PurchLine                   purchLine;

    VendPackingSlipJour         vendPackingSlipJour;
    VendPackingSlipTrans        vendPackingSlipTrans;

    VendInvoiceInfoTable        vendInvoiceInfoTable;
    VendInvoiceInfoLine         vendInvoiceInfoLine;
    VendInvoiceInfoSubTable     vendInvoiceInfoSubTable;
    VendInvoiceInfoSubLine      vendInvoiceInfoSubLine;

    PurchId                     purchId;
    PackingSlipId               packingSlipId;
    ;

    purchId = '9001-PO-16-000745';
    packingSlipId = '9001-PO-16-000745-2';

    ttsBegin;
    purchTable = PurchTable::find(purchId);

    while select vendPackingSlipJour
        where vendPackingSlipJour.PurchId == purchTable.PurchId
        && vendPackingSlipJour.PackingSlipId == packingSlipId
    {

        //Generate Pending Invoice Header
        vendInvoiceInfoTable.clear();
        vendInvoiceInfoTable.initValue();
        vendInvoiceInfoTable.initFromPurchTable(purchTable);

        vendInvoiceInfoTable.DocumentOrigin  = DocumentOrigin::Manual;
        vendInvoiceInfoTable.CurrencyCode = purchTable.CurrencyCode;
        vendInvoiceInfoTable.DeliveryName = purchTable.DeliveryName;
        vendInvoiceInfoTable.Num = "INV-" + vendPackingSlipJour.PackingSlipId; //add invoice number in here
        vendInvoiceInfoTable.PurchName = purchTable.PurchName;
        vendInvoiceInfoTable.VendInvoiceSaveStatus = VendInvoiceSaveStatus::Pending;
        vendInvoiceInfoTable.DocumentDate = systemDateGet();
        vendInvoiceInfoTable.LastMatchVariance = LastMatchVarianceOptions::OK;
        vendInvoiceInfoTable.ParmJobStatus = ParmJobStatus::Waiting;
        vendInvoiceInfoTable.Approved = NoYes::Yes;
        vendInvoiceInfoTable.Approver = DirPersonUser::currentWorker();

        vendInvoiceInfoTable.DefaultDimension = vendInvoiceInfoTable.copyDimension(purchTable.DefaultDimension);

        vendInvoiceInfoTable.defaultField(fieldNum(VendInvoiceInfoTable,RemittanceLocation),null,purchTable);
        vendInvoiceInfoTable.defaultField(fieldNum(VendInvoiceInfoTable,FixedDueDate),null,purchTable);
        vendInvoiceInfoTable.defaultField(fieldNum(VendInvoiceInfoTable,ExchRate),null,purchTable);
        vendInvoiceInfoTable.defaultField(fieldNum(VendInvoiceInfoTable,TransDate),null,purchTable);
        vendInvoiceInfoTable.defaultField(fieldNum(VendInvoiceInfoTable,PaymMode),null,purchTable);

        vendInvoiceInfoTable.insert();

        //Generate Vend Invoice Info reference
        if(vendInvoiceInfoTable)
        {
            vendInvoiceInfoSubTable.clear();
            vendInvoiceInfoSubTable.initValue();
            vendInvoiceInfoSubTable.defaultRow();

            vendInvoiceInfoSubTable.ParmId = vendInvoiceInfoTable.ParmId;
            vendInvoiceInfoSubTable.OrigPurchId = vendInvoiceInfoTable.PurchId;
            vendInvoiceInfoSubTable.PurchName = vendInvoiceInfoTable.PurchName;
            vendInvoiceInfoSubTable.TableRefId = vendInvoiceInfoTable.TableRefId;

            vendInvoiceInfoSubTable.insert();
        }

        //select all packing slip line
        while select vendPackingSlipTrans
            where vendPackingSlipTrans.PackingSlipId == vendPackingSlipJour.PackingSlipId
            && vendPackingSlipTrans.VendPackingSlipJour == vendPackingSlipJour.RecId
        {
            //Generate Pending Invoice Line
            purchLine = vendPackingSlipTrans.purchLine();
            vendInvoiceInfoLine.clear();
            vendInvoiceInfoLine.initValue();
            vendInvoiceInfoLine.defaultRow(null,purchLine);
            vendInvoiceInfoLine.initFromPurchLine(purchLine);

            vendInvoiceInfoLine.DeliveryName = vendInvoiceInfoTable.DeliveryName;
            vendInvoiceInfoLine.ParmId = vendInvoiceInfoTable.ParmId;
            vendInvoiceInfoLine.TableRefId = vendInvoiceInfoTable.TableRefId;
            vendInvoiceInfoLine.currencyCode = vendInvoiceInfoTable.CurrencyCode;
            vendInvoiceInfoLine.LineNum = any2int(purchLine.LineNumber);

            vendInvoiceInfoLine.InvoiceAccount = vendInvoiceInfoTable.InvoiceAccount;
            vendInvoiceInfoLine.InventDimId = vendPackingSlipTrans.InventDimId;
            vendInvoiceInfoLine.OrderAccount  = vendInvoiceInfoTable.OrderAccount;
            vendInvoiceInfoLine.ItemId = vendPackingSlipTrans.ItemId;
            vendInvoiceInfoLine.InventTransId = vendPackingSlipTrans.InventTransId;

            vendInvoiceInfoLine.DocumentOrigin = DocumentOrigin::Manual;

            vendInvoiceInfoLine.ReceiveNow = vendPackingSlipTrans.Qty;
            vendInvoiceInfoLine.RemainBefore = vendPackingSlipTrans.Qty;
            vendInvoiceInfoLine.RemainBeforeInvent = vendPackingSlipTrans.Qty;

            vendInvoiceInfoLine.PurchPrice = purchLine.PurchPrice;
            vendInvoiceInfoLine.InventNow = vendPackingSlipTrans.Qty;
            vendInvoiceInfoLine.LineAmount = (purchLine.LineAmount / purchLine.QtyOrdered) * vendPackingSlipTrans.Qty;

            vendInvoiceInfoLine.DefaultDimension = purchLine.DefaultDimension;

            vendInvoiceInfoLine.insert();

            //Generate Vend Invoice Info reference from packing slip
            if(vendInvoiceInfoLine.RecId)
            {
                vendInvoiceInfoSubLine.clear();
                vendInvoiceInfoSubLine.initValue();
                vendInvoiceInfoSubLine.defaultRow();
                vendInvoiceInfoSubLine.ParmId = vendInvoiceInfoTable.ParmId;
                vendInvoiceInfoSubLine.LineRefRecId = vendInvoiceInfoLine.RecId;
                vendInvoiceInfoSubLine.ReceiveNow = vendPackingSlipTrans.Qty;
                vendInvoiceInfoSubLine.InventNow = vendPackingSlipTrans.Qty;
                vendInvoiceInfoSubLine.JournalRefRecId = vendPackingSlipTrans.RecId;
                vendInvoiceInfoSubLine.JournalRefTableId = vendPackingSlipTrans.TableId;
                vendInvoiceInfoSubLine.DocumentId = vendPackingSlipTrans.PackingSlipId;
                vendInvoiceInfoSubLine.insert();
            }
        }

        //Posting pending invoice invoice
        purchFormLetter = PurchFormLetter_Invoice::newFromSavedInvoice(vendInvoiceInfoTable);
        purchFormLetter.update(vendInvoiceInfoTable.purchTable(),vendInvoiceInfoTable.Num);
    }

    ttsCommit;

    info(strFmt('Purchase Order %1 invoiced',purchTable.PurchId));

Get tax amount through x++

static void TIDgetTax(Args _args)
{
    TaxAmount           tax;
    CustInvoiceTrans    CustInvoiceTrans;
    ;

    CustInvoiceTrans =  CustInvoiceTrans::findRecId(5637155209);
    tax = Tax::calcTaxAmount(CustInvoiceTrans.TaxGroup, CustInvoiceTrans.TaxItemGroup, CustInvoiceTrans.InvoiceDate,
                            CustInvoiceTrans.CurrencyCode, CustInvoiceTrans.LineAmount, TaxModuleType::FreeTxtInvoice);

    info(strfmt('%1', tax));
}

Only foreign key constraints are allowed on this table.

Best Practice Error

Only foreign key constraints are allowed on this table

Capture

How to rectify this best practice error.

For Custom Table

  1. Export table to XPO
  2. Edit it with text editor, and change
    • from: EnforceFKRelation 1
    • to: EnforceFKRelation 0
  3. delete table in AX, synchronize
  4. import  xpo file

Capture

For Standard  Table

Some custom relation table may have some error on standard table, we can fix this  best practice error using same trick as above, but for this one lets check on XPO file and change REFERENCETYPE value.

  1. Export table to XPO
  2. Edit it with text editor, and change
    • from: REFERENCETYPE  NORMAL
    • to: REFERENCETYPE PKFK
  3. delete table in AX, synchronize
  4. import  xpo file

Capture.

Starting Dynamics AX for Retail (POS Development)

One of the most challenging on dynamics AX is to develop POS on AX Retail module.

The Dynamics AX for Retail POS application was designed base on .Net Application using C# language so it’s little bit odd for regular AX technical who always develop using  X++, so since the POS is not using standard AX MorpX so we need to prepare to develop POS.

  1. Visual Studio for development tools.
  2. Dev Express License (Optional for better touch GUI)
  3. Retail SDK (POS Source Code)

Let’s  jump   into number 3. I assume you already have visual studio on dev server.

You can found Retail SDK on Dynamics AX Installation.

Installation

After installation finished you can found the source code  on document folder.

Source Code

For very first time lets  focus at the two folder on the POS Plug-ins, there is Services and Triggers because almost all POS function source code is on that folder.

Services: Services are actually .Net assemblies. POS implements many of the features as services using interfaces and can be modified using Visual Studio. POS loads these services at run time by calling the interfaces. So whenever you modify or extend the standard service keep the assembly name same as original so that POS can recognize it and call it at run time.

Triggers: Triggers are called before and after the operations. There are two types of triggers, Pre-triggers and Post-triggers. Pre-triggers provide a way of validation before a certain operation is executed. Post-triggers are used to respond to an operation after it has finished. You can modify the triggers same way as services.

*I use Dynamics AX 2012 R3 CU8 on this tutorial, also work for AX 2012 Feature Pack, AX 2012 R2, and AX 2o12 R3.

Create Custom Script

So first lets open the Triggers folder and double click Triggers.sln file, you will show all the triggers source code now expand on visual studio, lets pick our first function to modify.

For this tutorial we will add some function when POS starting, so lets’s open ApplicationTriggers Project and open  ApplicationTriggers.cs , at this class you will found the some of methods, this methods will be calling when POS staring, for example  ApplicationStart, ApplicationStop, PreLogon, PostLogon and etc.

Try to add some code at ApplicationStart method, i assume this method will be calling when POS application starting, lets add some simple script at here, let’s try add some dialog box.

 public void ApplicationStart()
{
     //Our Custom Script
     //<begin>
     MessageBox.Show("POS aplication starting....");
     //<end>

     string source = "IApplicationTriggers.ApplicationStart";
     string value = "Application has started";
     LSRetailPosis.ApplicationLog.Log(source, value, LSRetailPosis.LogTraceLevel.Debug);
     LSRetailPosis.ApplicationLog.WriteAuditEntry(source, value);
     ...
     ...
}

To make the script running on the POS we need to build the project in to dll and copy the dll on POS folder.

Try to run your POS, and the message box will be showing when POS starting.

Deploy Custom Script/dll

You should copy all  custom *.dll files on the Extensions folder at Retail POS Folder. Don’t replace the original ones.

We can using two different Extensions Folder on POS .

1. Retail POS\Extensions

Source Code2. Retail POS\Services\Extensions

Source Code

Always use the first one for custom Triggers *.dll, and second one for Services *.dll to make it easy to remember and maintenance.

Lets Starting To Debug POS Custom Code.

For debug the code, we can change some properties on the visual studio project,  use our first sample project, right click on that project and chose Properties.

First open Build  Tab on Properties UI and select the Output Path because this is Triggers so i  select “C:\Program Files (x86)\Microsoft Dynamics AX\60\Retail POS\Extensions\” Folder.

Source Code

And than open Debug tab, at the Start Action group select Start external program and select your POS.exe file.

Source Code

just put your break point on your custom script, and run the project using F5 or Start Button. Lets see the magic happen.

*Note: You can copy all POS Folder to your own folder to make the  original POS Files safety.

Running Batch Job from X++ example code

static void TID_ExecuteBatchJobDistribution(Args _args)
{
    BatchHeader header;
    SysRecurrenceData sysRecurrenceData;
    Batch batch;
    BatchJob batchJob;
    RetailConnScheduleRunner _RetailConnScheduleRunner; // Class extends RunBaseBatch
    BatchInfo processBatchInfo;
    BatchRetries noOfRetriesOnFailure = 4;
    ;

    // Setup the RunBaseBatch Job
    header = BatchHeader::construct();
    _RetailConnScheduleRunner = new RetailConnScheduleRunner();
    processBatchInfo = _RetailConnScheduleRunner.batchInfo();
    processBatchInfo.parmRetriesOnFailure(noOfRetriesOnFailure);
    processBatchInfo.parmCaption(_RetailConnSchedule.Name); // Description Batch Job
    processBatchInfo.parmGroupId('RTL'); // Batch Gorup
    processBatchInfo.parmBatchExecute(NoYes::Yes);
    header.addTask(_RetailConnScheduleRunner);

    // Set the recurrence data
    sysRecurrenceData = SysRecurrence::defaultRecurrence();
    SysRecurrence::setRecurrenceStartDateTime(sysRecurrenceData, DateTimeUtil::addSeconds(DateTimeUtil::utcNow(), 20)); // Set range of recurrence
    SysRecurrence::setRecurrenceNoEnd(sysRecurrenceData);
    SysRecurrence::setRecurrenceUnit(sysRecurrenceData, SysRecurrenceUnit::Minute); // Set reccurence pattern
    header.parmRecurrenceData(sysRecurrenceData);
    // Set the batch alert configurations
    header.parmAlerts(NoYes::No, NoYes::Yes, NoYes::No, NoYes::Yes, NoYes::Yes);
    header.save();

    // Update the frequency to run the job to every two minutes
    ttsbegin;
    select forupdate batchJob
    join batch
    where batchJob.RecId == batch.BatchJobId
    && batch.ClassNumber == classnum(RetailConnScheduleRunner);

    sysRecurrenceData = batchJob.RecurrenceData;
    sysRecurrenceData = conpoke(sysRecurrenceData, 8, [10]);
    batchJob.RecurrenceData = sysRecurrenceData;
    batchJob.update();
    ttscommit;
}

Step by step deploying AX ListPage form to Enterprise portal

Listpage type form on ax can be deploy directly to enterprise portal.

And steps below show you how to do it :
1. Do incremental CIL first before deploy
2. On menu item display, right click on it and select deploy to EP and select module.
Capture

3. After deploy, AX will create two web object on AOT, WebUrls (Web -> Web Menu Items -> Web URLs) and Web Page Definition (Web -> Web Files -> Web Page Definition)
Capture

4. On Web Page Definitions properties “Module”, add submodule if you want to add this listpage on subModule on Enterprise portal , then right click on it select Deploy element.
Capture

5. On Web URLs properties URL , add submodule same as page definition (optional), then right click on it and select import page
Capture

6. Restart IIS
https://hellodax.com/2015/05/22/restart-iis-command-prompt/

7. Clear AOT cache
Capture

8. To check the newly added custom webcontrol, just copy paste your Default Enterprise portal URLS (System Administration -> Setup -> Enterprise portal -> Websites) and webcontrol urls (from Web Urls Properties) to web browser.

default enterprise portal urls :
Capture

full urls on web browser :
Capture

another guide to add to quick launch sidebar :
https://hellodax.com/2015/05/22/add-new-custom-webparts-data-grid-to-enterprise-portal-quick-launch-sidebar-tutorial/

Step by step to deploy newly added WebControl to Enterprise Portal AX 2012

Deploying newly added webcontrol is much complicated than deploying SSRS.
I hope microsoft will simplify it on the next version of AX.

note :
this guide below is only for newly addded webcontrols.
if you already done the steps below, and want to update your webpage by using webcontrol, just right click on web controls AOT, and select deploy. It will replace the old web page with the new web page.

here is the steps for deploying webcontrols :

1. deploy the webcontrol on aot (Web -> Web Files -> Web Controls) , right click and choose “Deploy”
Capture

2. then deploy Web Content Managed on aot (Web -> Web Content -> Managed) , and choose the module
Capture

3. after you deploy web content managed , you will get two new object on AOT,
Web Urls (Web -> Web Menu Items -> URLs), and Web Page Definition (Web -> Web Files -> Page Definitions) , Right click on web page definitions , select “Deploy Elements”, right clicks on web urls, and select “Import Pages”

Web page definitions
Capture

Web Ulrs
Capture

4. Restart di IIS with IISRESET on your sharepoint server , see the reference post below
https://hellodax.com/2015/05/22/restart-iis-command-prompt/

5. Clear cache for making sure the enterprise portal is displaying the newly added webparts. (on some cases you must generate Incremental CIL too)
Capture

6. To check the newly added custom webcontrol, just copy paste your Default Enterprise portal URLS (System Administration -> Setup -> Enterprise portal -> Websites) and webcontrol urls (from Web Urls Properties) to web browser.

default enterprise portal urls :
Capture

web control urls :
Capture

full urls on web browser :
Capture

another guide to add to quick launch sidebar :
https://hellodax.com/2015/05/22/add-new-custom-webparts-data-grid-to-enterprise-portal-quick-launch-sidebar-tutorial/

Useful powershell command for export and install AXModel

Export axmodel layer example :
Export-AXModel -Model CUS Model -config MicrosoftDynamicsAXUAT -file CUS.axmodel
Capture2

Import axmodel with replace and create parent example :
Install-AXModel -File CUS.axmodel -Conflict Overwrite -Config MicrosoftDynamicsAXUAT -createparents
Capture

reference link :
http://axbloggerblog.blogspot.co.id/p/powershell-commands.html

do full compile, full sync db, full CIL and incremental. Re-deploy website EP if you are customize the EP

Visualize with ax data to ASP calendar COLOR highlight example code on Enterprise Portal C#

i’ll explain the code below on another day .
when we remove the highlights on calender with clicks, it will remove the data on ax
and do the same when we add highlights on calendar.

Preview :
Capture

on design

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="HRSUploadTimeSheet.ascx.cs" Inherits="HRSUploadTimeSheet" %>
<dynamics:AxDataSource ID="hrsScheduleRequestWorkerTmpDS" runat="server" 
    DataSetName="HRSScheduleRequestWorkerTmpDS" 
    ProviderView="HRSScheduleRequestWorkerTmp">
</dynamics:AxDataSource>
<dynamics:AxDataSource ID="HRSJmgProfileDayDS" runat="server" 
    DataSetName="HRSJmgProfileDayDS" 
    ProviderView="JmgProfileDay">
</dynamics:AxDataSource>

<dynamics:AxForm ID="AxForm1" runat="server">
    <dynamics:AxMultiColumn ID="AxMultiColumn1" runat="server">
        <dynamics:AxColumn ID="AxColumn1" runat="server">
            <dynamics:AxGridView ID="AxGridView1" runat="server" BodyHeight="" 
                DataKeyNames="ProfileId" DataMember="HRSScheduleRequestWorkerTmp" 
                DataSetCachingKey="09d6aa10-4163-4e45-9074-8449211996ae" 
                DataSourceID="hrsScheduleRequestWorkerTmpDS" EnableModelValidation="True" 
                AllowPaging="False" 
                onselectedindexchanged="AxGridView1_SelectedIndexChanged" 
                ShowFilter="False">
                <Columns>
                    <asp:TemplateField ConvertEmptyStringToNull="False" 
                        HeaderText="<%$ AxLabel:@SYS303656 %>" SortExpression="PersonnelNumber">
                        <ItemTemplate>
                            <asp:Label ID="PersonnelNumber" runat="server" Text='<%# Bind("PersonnelNumber") %>'></asp:Label>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <dynamics:AxBoundField DataField="Name" DataSet="HRSScheduleRequestWorkerTmpDS" 
                        DataSetView="HRSScheduleRequestWorkerTmp" SortExpression="Name">
                    </dynamics:AxBoundField>
                    <asp:TemplateField ConvertEmptyStringToNull="False" 
                        HeaderText="<%$ AxLabel:@SYS7607 %>" SortExpression="ProfileId">
                        <ItemTemplate>
                            <asp:Label ID="ProfileId" runat="server" Text='<%# Bind("ProfileId") %>'></asp:Label>
                        </ItemTemplate>
                    </asp:TemplateField>
                </Columns>
            </dynamics:AxGridView>
        </dynamics:AxColumn>

         <dynamics:AxColumn ID="AxColumn2" runat="server">
            <asp:Calendar ID="Calendar1" runat="server" 
                                    OnPreRender="Calendar1_PreRender" 
                                    OnSelectionChanged="Calendar1_SelectionChanged" 
                                    ondayrender="Calendar1_DayRender" Height="226px" 
                    Width="427px">
            </asp:Calendar>
            <dynamics:AxGridView ID="AxGridView2" runat="server" BodyHeight="" 
                DataKeyNames="RecId" DataMember="JmgProfileDay" 
                DataSetCachingKey="a1fd011a-2d33-4680-a45d-c70b2dcaf863" 
                DataSourceID="HRSJmgProfileDayDS" EnableModelValidation="True"
                ondatabound="AxGridView2_databound" ShowFilter="False" AllowPaging="False"
                >
                <Columns>
                    <asp:TemplateField ConvertEmptyStringToNull="False" 
                        HeaderText="<%$ AxLabel:@SYS67221 %>" SortExpression="Color">
                        <ItemTemplate>
                            <asp:Label ID="Color" runat="server" Text='<%# Bind("Color") %>' Visible="false"></asp:Label>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField ConvertEmptyStringToNull="False" 
                        HeaderText="<%$ AxLabel:@SYS7607 %>" SortExpression="Profile">
                        <ItemTemplate>
                            <asp:Label ID="Profile" runat="server" Text='<%# Bind("Profile") %>'></asp:Label>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField ConvertEmptyStringToNull="False" HeaderText="Profile Key" 
                        SortExpression="HRSProfleKey">
                        <ItemTemplate>
                            <asp:Label ID="ProfileKey" runat="server" Text='<%# Bind("HRSProfleKey") %>'></asp:Label>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <dynamics:AxBoundField DataField="StartWorkTime" DataSet="HRSJmgProfileDayDS" 
                        DataSetView="JmgProfileDay" SortExpression="StartWorkTime">
                    </dynamics:AxBoundField>
                    <dynamics:AxBoundField DataField="EndWorkTime" DataSet="HRSJmgProfileDayDS" 
                        DataSetView="JmgProfileDay" SortExpression="EndWorkTime">
                    </dynamics:AxBoundField>
                </Columns>
            </dynamics:AxGridView>

        </dynamics:AxColumn>
    </dynamics:AxMultiColumn>
</dynamics:AxForm>

.cs file

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.DataVisualization.Charting;
using System.Web.UI.WebControls;
using Microsoft.Dynamics.Framework.Portal.UI.WebControls;
using Microsoft.Dynamics.Framework.Portal.UI.WebControls.WebParts;
using System.IO;
using System.Data.OleDb;
using System.Data;
using Proxy = Microsoft.Dynamics.Framework.BusinessConnector.Proxy;
using Microsoft.Dynamics.Framework.BusinessConnector.Session;
using System.Drawing;
using Microsoft.Dynamics.AX.Framework.Portal.Data;
using Microsoft.Dynamics.Framework.BusinessConnector.Adapter;


public partial class HRSUploadTimeSheet : System.Web.UI.UserControl
{
    private string profileId { get; set; }
    private string scheduleRequestId { get; set; }
    private string personnelNumber { get; set; }
    private Boolean canEdit { get; set; }

    Dictionary<DateTime, int> dict = new Dictionary<DateTime, int>();

    public Dictionary<DateTime, int> SelectedDates
    {
        get
        {
            if (ViewState["Dates"] == null)
            {
                dict.Add(DateTime.MaxValue.AddDays(-2), 0);
                ViewState["Dates"] = dict;
            }

            return (Dictionary<DateTime, int>)ViewState["Dates"];
        }
        set
        {
            ViewState["Dates"] = value;
        }
    }

    private ISession AxSession
    {
        get
        {
            AxBaseWebPart webPart = AxBaseWebPart.GetWebpart(this);
            return webPart == null ? null : webPart.Session;
        }
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        IAxaptaRecordAdapter currentRecord = AxBaseWebPart.GetWebpart(this).ExternalRecord;
        if (currentRecord != null)
        {
            string status = currentRecord.GetField("HRSRequestStatus").ToString();
            if (status != "0")
            {
                canEdit = false;
            }
            else
            {
                canEdit = true;
            }
            scheduleRequestId = currentRecord.GetField("ScheduleReqestId").ToString();
        }
        if (!IsPostBack)
        {
            getProfileIdAndPersonnelNumber(true);
            getCalendarDataFromAX();
        }
      
    }

    #region grid
    protected void AxGridView2_databound(object sender, EventArgs e)
    {
        for (int i = 0; i <= AxGridView2.Rows.Count - 1; i++)
        {
            Label colorTxt = (Label)AxGridView2.Rows[i].FindControl("Color");
            Color myColor = Color.FromArgb(Convert.ToInt32(colorTxt.Text));
            AxGridView2.Rows[i].Cells[0].BackColor = myColor;
        }

        getProfileIdAndPersonnelNumber(true);
        getCalendarDataFromAX();
        FilterRows();
   
    }

    protected void AxGridView1_SelectedIndexChanged(object sender, EventArgs e)
    {
        getProfileIdAndPersonnelNumber();
        getCalendarDataFromAX();
        FilterRows();

    }

    protected void getProfileIdAndPersonnelNumber(Boolean isInit = false)
    {
        Label profileIdLbl;
        Label PersonnelNumberLbl;

        if (isInit)
        {
            profileIdLbl = (Label)AxGridView1.Rows[0].FindControl("ProfileId");
            PersonnelNumberLbl = (Label)AxGridView1.Rows[0].FindControl("PersonnelNumber");
        }
        else
        {
            profileIdLbl = (Label)AxGridView1.Rows[AxGridView1.SelectedIndex].FindControl("ProfileId");
            PersonnelNumberLbl = (Label)AxGridView1.Rows[AxGridView1.SelectedIndex].FindControl("PersonnelNumber");
        }
        profileId = profileIdLbl.Text;
        personnelNumber = PersonnelNumberLbl.Text;
    }

    protected void getCalendarDataFromAX()
    {
        SelectedDates.Clear();
        SelectedDates.Add(DateTime.MaxValue.AddDays(-2), 0);

        DateTime keyDate = System.DateTime.Now;
        int valueInt = 0;
        Microsoft.Dynamics.AX.ManagedInterop.Container axContainer;

        axContainer = (Microsoft.Dynamics.AX.ManagedInterop.Container)this.AxSession.AxaptaAdapter.CallStaticRecordMethod("HRSScheduleRequestWorkerTmp", "getDataScheduleByWorker", personnelNumber, scheduleRequestId);

        for (int i = 1; i <= axContainer.Count; i++)
        {
            if (i % 2 != 0)
            {
                keyDate = Convert.ToDateTime(axContainer.get_Item(i));
            }
            else
            {
                valueInt = Convert.ToInt32(axContainer.get_Item(i));
                if (SelectedDates.ContainsKey(keyDate))
                {
                    SelectedDates.Remove(keyDate);
                }

                SelectedDates.Add(keyDate, valueInt);

            }
        }

        ViewState["Dates"] = SelectedDates;
    }

    protected void ShowRows()
    {
        // Display all the rows
        foreach (GridViewRow row in AxGridView2.Rows)
        {
            row.Visible = true;
        }
    }

    protected void FilterRows()
    {
        ShowRows();
        string colProfileId;
        // Loop through the rows and apply the filter
        int i = 0;
        foreach (GridViewRow row1 in AxGridView2.Rows)
        {
            Label lblProfile = (Label)AxGridView2.Rows[i].FindControl("Profile");
            // Get the filter column values
            colProfileId = lblProfile.Text;

            // If the name, visit and discharge fields are not equal to "All" and not equal to the filter value then the row is hidden.
            if (colProfileId != profileId)
            {
                row1.Visible = false;
            }
            i++;
        }

    }

    #endregion

    #region calendar
    protected void Calendar1_PreRender(object sender, EventArgs e)
    {
        // Reset Selected Dates 
        Calendar1.SelectedDates.Clear();
        // Select previously Selected Dates 

        foreach (var dt in SelectedDates)
        {
            Calendar1.SelectedDates.Add(dt.Key);
            Color myColor = Color.FromArgb(Convert.ToInt32(dt.Value));
            Calendar1.SelectedDayStyle.BackColor = myColor;
        }

    }

    protected void Calendar1_SelectionChanged(object sender, EventArgs e)
    {
        if (canEdit)
        {
            getProfileIdAndPersonnelNumber();
            //Check if selected Date is in the saved list 
            // Remove the Selected Date from the saved list 
            if (SelectedDates.ContainsKey(Calendar1.SelectedDate))
            {
                SelectedDates.Remove(Calendar1.SelectedDate);
                //delete record in ax , call custom table static method
                this.AxSession.AxaptaAdapter.CallStaticRecordMethod("HRSScheduleRequestWorkerTmp", "deleteScheduleRequestWorker", personnelNumber, scheduleRequestId, Calendar1.SelectedDate.Date);
            }

            else
            {
                Label colorTxt = (Label)AxGridView2.Rows[AxGridView2.SelectedIndex].FindControl("Color");
                Label profileKey = (Label)AxGridView2.Rows[AxGridView2.SelectedIndex].FindControl("ProfileKey");
                SelectedDates.Add(Calendar1.SelectedDate, Convert.ToInt32(colorTxt.Text));
                //insert to ax , call custom table static method
                this.AxSession.AxaptaAdapter.CallStaticRecordMethod("HRSScheduleRequestWorkerTmp", "insertScheduleRequestWorker", personnelNumber, scheduleRequestId, Calendar1.SelectedDate.Date, profileId, profileKey.Text);
            }

            ViewState["Dates"] = SelectedDates;
        }
    }

    protected void Calendar1_DayRender(object sender, DayRenderEventArgs e)
    {
        if (e.Day.IsSelected == true)
        {
            int color = SelectedDates[e.Day.Date];
            Color myColor = Color.FromArgb(color);
            e.Cell.BackColor = myColor;
        }
    }

    #endregion
}

add new default dimension (financial dimension) on new table and form example tutorial

This example tutorial will guide you to add new default dim on table and forms.

1. Add new field with type Int64 and set the properties.
Capture

2. Add relation on table relation and set the properties.
Capture

3. on class declaration form , add code below :

public class FormRun extends ObjectRun
{
    DimensionDefaultingController   dimensionDefaultingController;
}

4. add tab page Financial Dimension on form design , and set properties like img below
Capture

5. on TabFinancialDimensions , override method pageActivated

public void pageActivated()
{
    dimensionDefaultingController.pageActivated();

    super();
}

6. on init form, like code below :

public void init()
{
    super();
    dimensionDefaultingController = DimensionDefaultingController::constructInTabWithValues(true, true, true, 0, this, TabFinancialDimensions, "@SYS138487", curext());
    dimensionDefaultingController.parmAttributeValueSetDataSource(HRSJobPowerPlan_ds, fieldStr(HRSJobPowerPlan, DefaultDimension));
}

7. finnaly, on datasource form method override method :
on active()

public int active()
{
    int ret;

    ret = super();
    dimensionDefaultingController.activated();

    return ret;
}

on write()

public void write()
{
    dimensionDefaultingController.writing();
    super();
}

on delete()

public void delete()
{
    dimensionDefaultingController.deleted();
    super();
}

8. preview example form:
Capture

Join ItemName from (EcoResProductTranslation ) and search name from InventTable to OnHand Form for search matter

it is common for user to search item based on name. but currently on AX 2012, onHand form can’t provide search based on item name or search name. It will take a little modification from technical side.

example how to do it :

1. you can join directly on form datasource or create view like example (i’m using the view that already created for another purpose)
Capture

2. on InventSumTable add relations like this.
Capture

3. then go to form onHand (InventOnHandItem), and add the view to datasource
Capture

4. open class InventDimCtrl_Frm_OnHand and modify method modifyQuery , on class declaration parameter, add new parameter FormDataSource with default null parameter. then define new variable QueryBuildDataSource.
Capture

5. on the last line of the method modifyQuery , add code like example :

if(_tidInventLookUp)
{
    qbsInventLookup = query.dataSourceName(_tidInventLookUp.name());

    //filter current company language
    qbr = SysQuery::findOrCreateRange(qbsInventLookup, fieldnum(TIDInventLookup, LanguageId));
    qbr.value(queryValue(CompanyInfo::languageId()));

    qbsInventLookup.addGroupByField(fieldNum(TIDInventLookup,Name));
    qbsInventLookup.addGroupByField(fieldNum(TIDInventLookup,NameAlias));

}

5. then back to the form, on datasource inventSUm, on executeQuery , modify the code from

void executeQuery()
{
    element.inventDimSetupObject().modifyQuery(inventSum_DS,inventDim_DS);

    super();
}

to

void executeQuery()
{
    element.inventDimSetupObject().modifyQuery(inventSum_DS,inventDim_DS,TIDInventLookUp_ds);

    super();
}

6. drag the field from newly add datasource on form to the grid :
Capture

7. Voila done, example result, :
Capture

note: remember to GROUPBY on code if you add new field to the form, or the field on form will show “UNRETRIEVED”

Add new custom webparts (Data Grid) to Enterprise Portal Quick Launch sidebar tutorial

This is step by step to create custom webparts and added it to Enterprise Portal (EP)
1. at Visual Studio, add new EP VS Project
Capture

2. Right click at the solution project, and click add new item, select Microsoft Dynamics Ax -> EP User Control with form and grid.
Capture

3. Go to AOT, add new data set , then add datasource, link it to your table , do compile and do INCREMENTAL CIL.
Capture

4. Go back to VS, refresh schema for the datasource, and select your newly created datasource
Capture

5. Make sure your column etc are inside axForm, fill the properties DataKeyNames,DataMember and DataSourcesID
Capture

6. at gridview, select datasource, and edit colomn, select field you want to show on the grid
Capture

7. right click on VS solution project, and choose “Add your_webParts to AOT”
Capture

8. go back to AOT, on Web -> Web Content -> Managed , find your webcontrol, right click on it, and Deploy to EP, then choose which module this webcontrol belongs to. After you deploy it , AX will create an URL Menu Item automatically at (Web -> Web Menu Items -> URLs) and Page Definition (Web -> Web Files -> Page Definition
Capture

9. If your menu is on submenu of the module, example Home\EmployeeServices\ManageTime , just change it on properties “Web Module” at Page Definition. and change the url on on web url properties
Capture
Capture

10. The next step is we want to add newly web parts to EP Quick Launch. Find the page you want to customize on “Web Modules” on AOT, on the properties, look at value of properties “Quick Launch”
Capture

10. Go to Web -> Web Menus , find the Quick Launch, and drag and drop the newly add url menu item to Quick Launch Menu, then deploy the EP from System Admin -> Setup -> Enterprise Portal -> Deployment
Capture

11. go to Web -> Web Modules . just right click and deploy on the module you customize.
Capture

12. Last, restart IIS for refreshing the EP with newest customize, with command prompt see post :
https://hellodax.com/2015/05/22/restart-iis-command-prompt/
Capture

13. Preview
Capture

Restart IIS command prompt

To restart IIS using the IISReset command-line utility
1.From the Start menu, click Run.
2.In the Open box, type cmd, and click OK.
3.At the command prompt, type iisreset /noforce computername, and press ENTER.
4.IIS attempts to stop all services before restarting. The IISReset command-line utility waits up to one minute for all services to stop. If the services cannot be stopped within one minute, all IIS services are terminated, and IIS restarts.

example :
Capture

source :
https://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/IIS/003ed2fe-6339-4919-b577-6aa965994a9b.mspx?mfr=true

Custom Number Sequence Reference Example

1. Create new field as picture below

Capture1

2. Go to the form, right click and choose restore to reload new field we added before. On the field method, add code below
 

public Common lookupReference(FormReferenceControl _formReferenceControl)
{
    return NumberSequenceTable::lookupReference(_formReferenceControl);
}

 
Capture2

3. Add reference group for the field at the form as picture below , and you are done.

Capture3

4. Just run the form for testing.

Capture4

Speeding up Sales Agreement Release Order process

When we create SO from sales agreement, we will face a problem that occur on standard version of AX 2012 if number of sales agreement record are thousands and we only want to choose let’s say 2 or 3 line from sales agreement line to Sales Line.
 
we can choose records from agreement line by fill the quantity, and the problem is standard AX still will loop all records no matter the quantity is fill or not. and all thosands will going through process that not needed.
 
Class : SalesAutoCreate\Create

void  create()
{
    #OCCRetryCount

    try
    {
        setprefix("@SYS55110");

        ttsbegin;

        while (this.recordExist())
        {

            this.setCust();

            setprefix(#PreFixField(CustTable,AccountNum));

            this.setSalesTable();

            this.setSalesLine();

            setprefix(#PreFixField(SalesLine,ItemId));

            this.nextRecord();
        }

        this.endUpdate();

        ttscommit;
    }

    catch (Exception::Deadlock)
    {
        retry;
    }

    catch (Exception::UpdateConflict)
    {
        if (appl.ttsLevel() == 0)
        {
            if (xSession::currentRetryCount() >= #RetryNum)
            {
                throw Exception::UpdateConflictNotRecovered;
            }
            else
            {
                retry;
            }
        }
        else
        {
            throw Exception::UpdateConflict;
        }
    }

}

i tried debug and i found the issue that made sales agreement release order process become so slow. and made a modification as below.
Class : SalesAutoCreate_ReleaseFromAgreement\New

protected void new(Common  _releaseOrderLine,
                   Object  _callBackClass     = null,
                   Common  _releaseOrderTable = null)
{
    //modify by fanddy
    //purpose : speeding up release agreement from BMP
    SalesCreateReleaseOrderLineTmp  TIDSalesCreateReleaseOrderLineTmp;

    releaseOrderLine     = _releaseOrderLine  as SalesCreateReleaseOrderLineTmp;
    delete_from releaseOrderLine where releaseOrderLine.SalesQty < 1;
    //end modify fanddy

    releaseOrderTable    = _releaseOrderTable as SalesCreateReleaseOrderTableTmp;
    this.firstRecord();

    createFromAgreementLine     = AgreementLineQuantityCommitment::find(releaseOrderLine.AgreementLineQuantityCommitment, false);
    createFromSalesAgreement    = SalesAgreementHeader::find(createFromAgreementLine.Agreement ? createFromAgreementLine.Agreement : releaseOrderTable.AgreementHeader);

    currentSalesId       = '';
    firstRecord          = true;

    super(_releaseOrderLine,_callBackClass);
    // <GEERU>
    countryRegion_RU = SysCountryRegionCode::isLegalEntityInCountryRegion([#isoRU]);
    // </GEERU>

 

Voila !! Now your when you create an SO with 2-3 SO lines originated from thousands line data of sales agreement, the process has been boosted !!

using “update_record” instead of “while select forupdate”

description on MSDN :
“This means that certain tasks may have improved performance by using the power of the SQL server.”

replace code below for a better perfomance.

MyTable myTableBuffer;
;
while select forUpdate mytableBuffer
{
mytableBuffer.field1 = field * 1.10;
}

with


MyTable myTableBuffer;
;
update_recordset myTableBuffer
setting field1 = field1 * 1.10;

source : http://msdn.microsoft.com/en-us/library/aa674382.aspx

one PurchReq Line multiple PO

got client that really what one PR line can posting to multiple.

class RequisitionReleaseStrategy\Main

add this code below

//AIFD
if(cekProses)
{
    ttsBegin;
    for (purchReqLine = fds.getFirst(processMarkedRows) ? fds.getFirst(processMarkedRows) : _args.record(); purchReqLine; purchReqLine = fds.getNext())
    {
        select forUpdate purchReqLineLoc where purchReqLineLoc.RecId==purchReqLine.RecId;
        purchReqLineLoc.PurchLineCreated = false;
        purchReqLineLoc.RequisitionStatus = PurchReqRequisitionStatus::Approved;
        if(purchReqLineLoc)
        {
            purchReqLineLoc.update();
        }
    }
    ttsCommit;
}

fix bugs Remove Sales Line when the sales Line already pick,pack,or invoiced on AX2012

i don’t know this is a bug come from our own customize or bugs originated from AX 2012.
anyway we have to fix this. Our client found this bug and requesting to add some validation and disable Remove Button on sales line when already pick.

so how i did this :

go to form SalesTable,
locate button Remove , and set AutoDeclaration to Yes, so we can call this button and disable or enable it via code.
look for method “active” in datasource SalesLine.

add this code below :

//AIFD fix bug remove button
    InventTrans     inventTransLoc;
    boolean         cekStatus;
    WMSOrderTrans   wmsOrderTrans;
  //END


    //add this code before return ret;
    cekStatus = true;
    //cek status dari inventtrans
    while select inventTransLoc   where inventTransLoc.InventTransOrigin==
    InventTransOrigin::findByInventTransId(SalesLine.InventTransId).RecId
    {
        if(inventTransLoc.StatusIssue != StatusIssue::OnOrder)
        {
            cekStatus = false;
            break;
        }
    }

    //cek status dari picking list
    while select wmsOrderTrans where   wmsOrderTrans.inventTransId==SalesLine.InventTransId
    {
        cekStatus = false;
        break;
    }


    if( salesLine.SalesStatus == SalesStatus::Backorder &amp;&amp; cekStatus)
    {
        LineStripDelete.enabled(true);
    }
    else
    {
       LineStripDelete.enabled(false);
    }
    
    //END

voila… happy Daxing !!

add Department Name to grid form PurchReqTableListPage

On table purchReqTable,

create new method and add this

display public server Name aiDepartment()
{
    return HcmWorker::find(this.Originator).primaryDepartmentName();
}

on form PurchReqTableListPage, right click and choose Restore (to restore DataSource).
and add string to Grid, set datasource and method on properties.

voila !! done..

happy Daxing… !!

Weight Detection PortComm

//Get PortComm number that already set from .txt file on local drive.
private str getPortComm()
{
    #file
    str getCOMport;
    boolean cekPortExist;
    AsciiIo file;
    Container data;
    int i;
    System.IO.FileInfo fileInfo;
    ;

    new InteropPermission(InteropKind::ClrInterop).assert();
    fileInfo = new System.IO.FileInfo(@”C:\portcomm.txt”);
    if(fileInfo.get_Exists())
    {
        file = new AsciiIo(@”C:\portcomm.txt”,#io_read);
        file.inFieldDelimiter(‘\t’);
        file.inRecordDelimiter(‘\n’);

        if(file)
        {
            while(file.status() == IO_Status::Ok)
            {  
                data = file.read();
                for(i = 1; i <= conlen(data); i++)
                {
                    getCOMport = any2str(conpeek(data, i));
                }
            }
        }

    }

    return strLTrim(getCOMport);
}

————————————

//Get Weight from PortComm
private real AI_getWeight()
{
    System.String netstring;
    System.String sample;

    int WeightInReal;
    int tryCount;
    str value;
    str value2;
    str portComm;
    System.IO.Ports.SerialPort port3;
    real weightFinal;

    tryCount = 0;

    portComm = this.getPortComm();
    if(portComm)
    {
        port3 = new System.IO.Ports.SerialPort();
        value = “0”;
        port3.set_PortName(portComm);
        port3.set_BaudRate(9600);
        port3.set_Parity(System.IO.Ports.Parity::None);
        port3.set_DataBits(8);
        port3.set_StopBits(System.IO.Ports.StopBits::One);
        port3.set_Handshake(System.IO.Ports.Handshake::None);

        try
        {
            port3.Open();
            while(tryCount<1)
            {
                WeightInReal = sleep(500);
                netstring = port3.ReadExisting();
                value = netstring;
                value2 = value;
                tryCount++;
            }
            port3.Close();
            port3.Dispose();
        }
        catch(Exception::CLRError)
        {
            info(“Wrong Port Comm Number”);
        }

        value2 = strReplace(value,”=”,””);
        value2 = strReplace(value2,”-“,””);
        value2 = strLTrim(value2);
        value2 = subStr(value2,1,6);
        weightFinal = str2num(value2);
    }
    else
    {
        info(“Check file portcomm.txt at C:\””);
    }

    if(!weightFinal)
    {
        info(“Please check your device, clarify it already turned on”);
    }
    return weightFinal;
}

—————————

on method button clicked, override.. some example here for you

void clicked()
{
    real _value;
    InventTable inventTableLoc;
    super();
    _value = this.AI_getWeight();
    
    Select * from inventTableLoc where inventTableLoc.ItemId == ProdJournalProd.ItemId;
    if(_value)
    {
        if(inventTableLoc.AISatuanPrimer!=”Kg”)
        {
            ProdJournalProd.AISatuanTertier = _value;
            ProdJournalProd_AISatuanTertier.enabled(False);
        }
        else
        {
            ProdJournalProd.AISatuanTertier =_value;
            ProdJournalProd_AISatuanTertier.enabled(True);
        }
    }
    ProdJournalProd_ds.executeQuery();
}

Multi Print Report based on Selected Items on Grid

void clicked()
{
    //AIFD_PrintBarcode
    AI_PrintBarcodeController aiBarcode = new AI_PrintBarcodeController();
    AI_PrintBarcodeContract aiContract = new AI_PrintBarcodeContract();
    ProdJournalProd prodJournalProdLoc;

    super();

    prodJournalProdLoc = ProdJournalProd_ds.getFirst(true);

    while (prodJournalProdLoc)
    {
        aiBarcode.parmReportName(ssrsReportStr(AI_PrintBarcoderReport,Report));
        aiBarcode.parmShowDialog(false);
        aiContract.parmRecordId(prodJournalProdLoc.RecId);
        aiContract.parmDocumentTitle(“x”);
        aiBarcode.parmReportContract().parmRdpContract(aiContract);
        aiBarcode.startOperation();

        prodJournalProdLoc = ProdJournalProd_ds.getNext();
    }
    //END
}

Print SSRS Barcode in AX 2012

Hello there,
just working on project that need to print somekind of barcode label for Production’s Serial Number.

you need to convert your string to Barcode string :

__________________________________________________________

public BarCodeString ShowBarcode(str _text)
{
  Barcode barcode;

  barcode = Barcode::construct(BarcodeType::Code128);
  barcode.string(true, _text);
  barcode.encode();

  return barcode.barcodeStr();
}

_________________________________________________________

then do this on Visual Studio 😀

Untitled

 

result :

Untitled

Happy DAXing !!