Microsoft Introduces a New AI-Powered Operating Model for Customer Experience in Dynamics 365

Microsoft has announced a new AI-powered operating model for customer experience in Dynamics 365, bringing together customer service, contact center operations, workforce engagement management (WEM), and AI capabilities within a unified platform.

 

The announcement reflects a shift in how organizations deliver customer service as AI becomes more integrated into business operations. Customer service leaders are increasingly responsible not only for managing human teams, but also for overseeing AI agents that participate in service delivery. This creates new requirements for visibility, coordination, and performance management across both human and digital workers.

 

A New Operating Model for Customer Experience

 

According to Microsoft’s 2026 Work Trend Index, organizations are entering a new phase of AI adoption in which AI agents become part of the workforce and contribute to business processes alongside employees.

 

To support this transition, Microsoft is expanding Dynamics 365 with capabilities that help organizations coordinate people and AI within a single operating model. At the core is a unified platform built on a single data model that connects Customer Service, Contact Center, Workforce Engagement Management, and AI capabilities. This provides a consistent view of customer interactions, workforce performance, and operational data across the service lifecycle.

 

The approach is designed to reduce fragmentation across customer service operations and support more informed decision-making using real-time operational insights.

 

Workforce Engagement Management in Dynamics 365

 

Microsoft is embedding Workforce Engagement Management directly into Dynamics 365 Customer Service and Dynamics 365 Contact Center, reducing reliance on separate tools and enabling a more unified approach to service operations.

 

The integrated WEM capabilities include demand forecasting, scheduling, adherence monitoring, intraday shift swapping, shift bidding, and time recording. These capabilities allow organizations to manage workforce operations directly within Dynamics 365 while maintaining a unified operational view.

 

Microsoft also notes that organizations can continue using existing third-party workforce management solutions through supported integrations, enabling a phased modernization approach.

 

Three Key Capability Areas

 

The announcement is structured around three main capability areas:

 

1. Embedded Workforce Engagement Management

 

Workforce Engagement Management is now integrated into Dynamics 365 Customer Service and Dynamics 365 Contact Center, enabling workforce planning and execution within the same environment as customer service operations.

 

2. AI Coaching and Operational Insights

 

AI-powered coaching provides real-time recommendations and performance insights to support employee development and help improve service quality. It also enables supervisors to identify operational issues based on live data.

 

3. Real-Time Wallboards

 

Microsoft is introducing real-time wallboards in Dynamics 365 Contact Center, providing supervisors with live visibility into service levels, backlog, and operational performance to support real-time operational awareness.

 

Together, these capabilities support connected planning, execution, and monitoring across customer service operations.

 

Managing Human Agents and AI Agents Together

 

A key element of the new operating model is the ability to manage both human service representatives and AI agents within the same environment.

 

As AI agents take on defined service tasks, human agents are able to focus on more complex customer interactions. Supervisors gain a consolidated view of performance across both human and AI contributors, supported by operational data and AI-driven insights.

 

Microsoft emphasizes that supervisors remain in control of how AI is applied within customer service workflows, using defined rules and operational guidance to shape system behavior.

 

Supporting Customer Service Operations

 

With these updates, Microsoft is positioning Dynamics 365 as a connected platform for customer service operations, bringing together customer service, contact center, workforce engagement management, and AI capabilities in a single environment.

 

By unifying these capabilities, organizations can reduce fragmentation across tools and processes and improve visibility across workforce planning, service execution, and operational performance.

 

The announcement represents another step in Microsoft’s broader strategy to embed AI across Dynamics 365 and support organizations as customer service operations continue to evolve.

What’s New in Microsoft Dynamics 365 Finance

The latest Microsoft Dynamics 365 Finance update brings key improvements, including features that are new, changed or enhanced, designed to make financial processes faster, more accurate, and easier to manage. All this will be available on the following schedule:

 

  • Preview of release: October 2025
  • General availability of release (self-update): December 2025
  • General availability of release (auto-update): February 2026

 

New or Changed Features

 

A number of new and changed features have been introduced to simplify finance workflows and enhance overall efficiency.

 

Accounts Receivable

 

  • Faster Global Transaction Page— Optimized performance for large datasets, allowing quicker lookups and smoother navigation.
  • Structured QR-Bill Addresses (Switzerland)— Supports structured “S-type” addresses for both biller and debtor, improving accuracy and compliance in Swiss billing.

 

Fixed Assets

 

  • Stable Acquisition Proposals— Recurring batch jobs for asset acquisitions now continue even if older journals are deleted, preventing interruptions.
  • Consistent Asset Naming— Asset names from purchase orders remain intact; new details are added without overwriting existing data.
  • Accurate Transaction Timing— Inventory sold transactions now post at the correct time for cleaner and more reliable financial tracking.
  • (Preview) Intercompany Asset Transfers— A new data entity streamlines transfers between legal entities, helping organizations automate asset management.

 

Subscription Billing

 

  • Optimized Batch Processing— Reduced a significant amount of chattiness in batch process.
  • Faster Invoice Posting— Billing schedule invoices now post using multithreaded batch processing, allowing multiple invoices to be processed in parallel for significantly faster execution.
  • (Preview) Parallel Deferral Recognition— Enhanced the subscription billing deferral recognition batch process by processing deferral data in parallel, allowing each run to complete more efficiently.

 

Enhanced Features

 

Several improvements have been made to refine existing processes, making daily finance tasks smoother and more reliable.

 

Accounts Receivable

 

  • Free Text Invoice— Corrects posted invoice totals to include all line amounts, ensuring consistent reporting.
  • Case Management Visibility— Invoice and Invoice line details information now appear on the Associations FastTab on My cases.

 

Credit & Collections

 

  • Interest Note Flexibility— Users can now view and select the option for the Due Date and the Terms of Payment when posting a customer interest note.
  • Smarter Collection Emails— Email template on the collection process automation uses the variables on the Subject line.
  • Faster Interest Note Creation— Uses top-picking optimization when creating customer interest notes, to speed up processing for large transaction volumes.

 

General Ledger

 

  • Account Reconciliation Agent (Production Preview)— Now provides suggested actions for In ledger not in subledger and In subledger not in ledger exception types.
  • User ID Visibility in SettlementsThe Ledger Settlement Inquiry page now shows the user ID that did the settlement for full audit transparency.
  • Ledger Settlement Performance— The Ledger settlements page has been updated to improve the performance, although customizations may need to be updated to work with the new page.

 

Subscription Billing

 

  • Automatic Discount Groups— Discount group setup in trade agreement for item prices now automatically applies to the billing schedule when created for the corresponding customer record.
  • (Preview) Deferral COGS Dashboard— Dashboard monitors for visibility into deferral adjustment errors with rerun capability.
  • (Preview) Process Completion Notifications— A notification message is displayed when the asynchronous process finishes so that the user is notified.
  • Consistent Credit Note Exchange Rates— At the termination of billing schedule, a credit note is created against the invoices (sales orders) with the same exchange rate used at the time of invoicing.

 

Take the Next Step with AXM365

 

Ready to unlock the full potential of Microsoft Dynamics 365 Finance?

 

AXM365 helps businesses implement, optimize, and modernize their ERP systems, ensuring finance and operations teams work smarter, not harder.

Unified Pricing Management (UPM) in Dynamics 365 Supply Chain Management

Unified Pricing Management (UPM) is a comprehensive solution that enables businesses to efficiently set, manage, and apply pricing rules across various sales channels.

 

Designed for flexibility and scalability, UPM allows organizations to maintain consistent pricing strategies while adapting to market demands and customer needs.

 

Key Features of UPM

 

  • Centralized Pricing Control – Businesses can define pricing structures in a single place, ensuring consistency across different sales and order processes.

 

  • Flexible Pricing Rules – UPM supports tiered pricing, volume discounts, and promotional offers, allowing for dynamic price adjustments.

 

  • Omnichannel Compatibility – The system seamlessly integrates with multiple sales channels, including e-commerce, retail, and B2B transactions.

 

  • Advanced Discounting Mechanisms – Users can configure complex discounting scenarios based on customer groups, product categories, or purchase volumes.

 

  • Scalability for Business Growth – Whether managing simple price lists or advanced pricing strategies, UPM adapts to growing business needs.

Benefits of UPM

 

  • Ensures consistent and transparent pricing policies;
  • Reduces manual pricing errors and discrepancies;
  • Enhances customer satisfaction through accurate and competitive pricing;
  • Streamlines pricing updates across the organization.

 

UPM is a powerful tool for businesses looking to optimize pricing strategies while maintaining flexibility in a competitive market.

 

Pricing Components in Unified Pricing Management

 

Unified Pricing Management (UPM) determines pricing based on several key components:

 

  • Price Attributes – These provide a flexible way to define pricing factors by using data from customers, products, sales order headers, and sales order lines. Since they integrate with the customer and product attribute framework, they are highly customizable. Additionally, extension points are available for further modifications.

 

  • Price Component Codes – These serve as the foundational elements of the pricing structure by grouping related price attributes. When setting up price and discount rules, each rule is assigned to a specific price component code.

 

  • Price Structures – These define the order in which price component codes are applied. A company can use a single price structure or multiple structures based on different order attributes. The embedded pricing logic ensures that a base price is established before any adjustments are applied (Base Price + Adjustments = Final Selling Price).

 

  • Concurrency Modes – These determine how the final price is calculated when multiple pricing rules apply to the same price component code, ensuring accurate price determination. 

 

In summary, Unified Pricing Management (UPM) in Dynamics 365 Supply Chain Management provides a structured and flexible approach to pricing.

 

By leveraging price attributes, component codes, structured pricing logic, and concurrency modes, businesses can ensure consistency, transparency, and adaptability in their pricing strategies.

 

This comprehensive system not only streamlines pricing operations but also enhances accuracy and competitiveness across multiple sales channels.

 

Running X++ Scripts and Jobs in an Environment with 0 Downtime

This post is about exploring the new Microsoft feature that allows you to run X++ jobs on PROD and non-PROD environments without release and with 0 downtime.

 

In 10.0.25 you can run simple X++ scripts on a production environment without any downtime. This feature lets you run custom X++ scripts without having to go through Dynamics LCS or suspend your system. Therefore, you can correct minor data inconsistencies without causing any disruptive downtime.

 

 

To be able to use this feature you need to create a deployable package with only one runnable class in it.

 

To do this, you can create a new model and reference the models which object you will use in the X++ script. Then create the runnable class and generate a deployable package from Visual Studio only with your new model that has only one runnable X++ class.

 

 

After generating the deployable package you can go to System Administration – Periodic tasks – Database – Custom scripts. Here you can upload your deployable package.

 

 

If your deployable package has not consisted of only one runnable job you won’t be able to upload it and a warning message will be displayed. After successfully uploading the deployable package, another user (different from the one who uploaded it) must Approve the script in the process workflow.

 

After the approval, you should follow the workflow and Run a test.

 

 

If the test log is okay and is without any errors, you need to Accept the test log and then you will be able to Run the script.

 

 

After the execution of the script is completed, the log will be updated and you need to end the workflow by confirming that the purpose is resolved, the purpose is unresolved, or abandon.

 

 

This is a helpful feature by Microsoft if you need to do some small data corrections or delete dates from certain tables. This way you don’t have to raise a Microsoft ticket to do small data corrections on PROD DB or connect to the ACC or other T2 environments databases.


Also, runnable classes with dialogs can be imported here not just simple data correction scripts.

 

But please be careful with running scripts like this!

 

______________________________________________________________________________________________________________

 

We hope that this blog post is helpful! If you are interested to join our team, check out our careers page, we have open positions for Microsoft Dynamics enthusiasts in every field. 

Export Multiple CSV Files in One Go

One of our colleagues recently had a requirement for having a dialog form with multiple toggle buttons to export several different files.

 

So, the standard procedure for exporting to CSV in D365 is:

 

  • to create a ‘CommaStreamIo’ object,
  • fill it with data
  • and then send a file to the user (File::SendFileToUser(stream, filename)) which file then gets downloaded from the browser.

 

However, the issue with this approach appears when we try to export multiple files with the same function, i.e. ‘File::SendFileToUser(stream, filename)’. In such a case, only the last file gets downloaded.

 

The solution to this is to use temporary storage, get the link from it and open it in a new tab.

 

 

We hope that this blog post is helpful, especially if you have found yourself in some simmliar situation.

 

______________________________________________________________________________________________________________________________

 

If you are interested to join our team, check out our careers page, we have open positions for Microsoft Dynamics enthusiasts in every field.

Send a File to the SFTP Server

Knowledge sharing is one of the basis and an everyday practice between our team members. So, we decided to share another fix to a challenge one of our colleagues encountered in his everyday tasks as a requirement for the client. Hopefully, this blog post will help you as well. 

 

Recently we got a client requirement to replace FTP upload for specific files with SFTP. After our team member has done some research, he found this post which was followed mostly. However, he wants to add a few things that he had to figure out during development.

 

First, you’ll need to create a DLL which you will use to connect and upload the file to SFTP. Prior to that, you need to install two DLLs for referencing through the NuGet packages for the solution:

 

 

If you don’t have the NuGet packages initially you can download them from:

 

 

Go to settings of NuGet manager and add the new folder with the two new packages:

 

 

It is very important first to install SSH.NET and then Renci.SshNet.Async. After installation without errors you should see them in the list of installed packages:

 

 

After you are done with the prerequisites you can create a new project of type C# class library in the same solution as the D365 project:

 

 

Add the following code for the C# class:

 

using System;
using System.IO;
using System.Net;
using System.Collections.Generic;
using Renci.SshNet;
using System.Text;

namespace SFTPFileTransfer
{
    public class SFTPConnect
    {
        public static string uploadSFTPFile(string host,
        string username,
        string password,
        System.IO.Stream sourceFile,
        string destinationPath,
        int port,
        string fileName,
        string privateKeyFilePath = "")
        {
            string successStr = "Fail";
            List<AuthenticationMethod> methods;

            /*It depends if the private key file is present for authentication. If the SFTP is key secured then the private key file has to be passed.*/
            if (privateKeyFilePath != "")
            {
                            var privateKeyFile = new PrivateKeyFile(privateKeyFilePath);
                            methods = new List<AuthenticationMethod>
                {
                    new PasswordAuthenticationMethod(username, password),
                    new PrivateKeyAuthenticationMethod(username, privateKeyFile)
                };
            }
            else
            {
                methods = new List<AuthenticationMethod>
                {
                    new PasswordAuthenticationMethod(username, password)
                };
            }
            try
            {
                var connectionInfo = new ConnectionInfo(host, port, username, methods.ToArray());
                using (SftpClient sftpclient = new SftpClient(connectionInfo))
                {
                    sftpclient.Connect();
                    sftpclient.ChangeDirectory(destinationPath.Trim());
                    sourceFile.Position = 0;
                    sftpclient.BufferSize = 8 * 1024;
                    sftpclient.UploadFile(sourceFile, fileName);
                }
                successStr = "Pass";
            }
            catch (WebException e)
            {
                successStr = "Fail";
            }
            return successStr;
        }

    }

}  

 

Rebuild the solution and you will now have the DLL file in the project directory\bin\Debug folder. Copy it to AosService\PackagesLocalDirectory\<your model>\bin folder.

 

Now to the X++ code, you should have parameters for all the needed fields:

 

Username: username of SFTP

 

Password: password for SFTP (container field to be filled using edit method or any encryption method), for simplification in this demo I’ll use plain text

 

HostName: this can be an ip address or <domain>.orgs.in

 

Port no: 22 by default for SFTP

 

Destination folder: /upload by default while an SFTP is created. This can vary but the input should start with “/” which depicts the folder in SFTP where the file will be dropped

 

//Upload to SFTP
        System.IO.StreamReader  reader;
        System.IO.Stream        requestStream;
        System.Byte[]           bytes;
        System.Text.Encoding    utf8;
        System.IO.Stream        stream = io.getStream();
        stream.Position = 0;

        UserId sftpUserName = companyInfo.SFTPUserName;
        str sftpHostURL = companyInfo.EDISFTPHost;
        int sftpPortNum = companyInfo.EDISFTPPortNum? companyInfo. SFTPPortNum: 22; //22 is default port for SFTP
        FilePath sftpDestinationFilePath = companyInfo.FileDestinationPath;

        if(sftpDestinationFilePath != "")
        {
            reader = new  System.IO.StreamReader(stream);
            utf8 = System.Text.Encoding::get_UTF8();
            bytes = utf8.GetBytes( reader.ReadToEnd() );
        
            reader.Close();
            try
            {
                
                Password sftpPassword = companyInfo.SFTPPW;
                str success = SFTPFileTransfer.SFTPConnect::uploadSFTPFile(sftpHostURL, sftpUserName, sftpPassword, stream, sftpDestinationFilePath, sftpPortNum, fileName);
                if(success == 'Pass')
                {
                    Info(strFmt("%1 File successfuly sent to SFTP ", fileName));
                }
                else
                {
                    Error("Error in sending file to SFTP due to incorrect Host/Port/User Credential");
                }
            }
            catch
            {
                Info(infolog.text());
            }
        }
        else
        {
            reader = new  System.IO.StreamReader(stream);
            
            File::SendStringAsFileToUser(reader.ReadToEnd(),fileName);
        }
        CodeAccessPermission::revertAssert();

 

That is all the development, you can test the solution if you have an SFTP host to send files to.

 

Let us know if you have any questions, and don’t forget if you are interested in a potential career opportunity in our company, check out the careers page for our open vacancies.

Block Invoicing Price Updates on Intercompany Sales Orders

It could happen that inventory adjustment journals are created after an intercompany sales order is created with picking list generated and packing slip posted for the same item in the order and different price.

 

This by it self doesn’t create a problem until the IC order has to be invoiced which updates the order lines with the latest price adjustments.

 

As the intercompany rules require to keep the same price from creating the orders to invoicing in order to prevent the update (actually re-update) we need to extend class ‘IntercompanySalesAtCostPriceFormLetterHandler’ and method ‘onSalesLineSelected’.

 

Before CoC we need to keep the previous value and then after it we will update the sales line with the original value.

 

[ExtensionOf(classStr(IntercompanySalesAtCostPriceFormLetterHandler))]
final class IntercompanySalesAtCostPriceFormLetterHandler_Extension
{
public void onSalesLineSelected(SalesLine _salesLine)
{    
SalesPrice          origSalesPrice = _salesLine.SalesPrice;
SalesLineAmount     origLineAmount = _salesLine.LineAmount;

    next onSalesLineSelected(_salesLine);

    //revert the price to the original price
    if (origSalesPrice)
    {
        ttsbegin;
        SalesLine salesLineLocal = SalesLine::findRecId(_salesLine.RecId, true);
        salesLineLocal.SalesPrice = origSalesPrice;
        salesLineLocal.LineAmount = origLineAmount;
        salesLineLocal.update();
        ttscommit;
    }
}

public void onSalesLinesSelectionStarted()
{
    next onSalesLinesSelectionStarted();
}

public void onSalesLinesSelectionFinished()
{
    next onSalesLinesSelectionFinished();
}
}

 

Just have in mind that you need to extend methods ‘onSalesLinesSelectionStarted’ and ‘onSalesLinesSelectionFinished’ as well as they must reside in the same transaction as ‘onSalesLineSelected’.

First Only for Data Entity Data Sources

You might have had a requirement to provide only the last record by CreatedDateTime field from a custom data entity, but you couldn’t accomplish it with what would be obvious solutions to the problem:

 

  • Using query as data source on the data entity;
  • Not possible to use query data source on data entity;
  • Using view built out of query as data entity data source:
  • For some reason the view omits the First Only setting set on the query data source and it returns all the records from the query.

 

So the solution would be to use view as data entity data source and create a computed column with ranking based on the CreatedDateTime field from the data source from which we want to select the first only record.

 

 

 

Add the following method for the computed column in the code of the View:

 

public class WorkflowTrackingView extends common

{ public static server str compColRankByCreatedDateTime()

{

str ret;

DictView dv =new DictView(tableNum(WorkflowTrackingView)); ret = 'RANK () OVER (ORDER BY '+dv.computedColumnString('WorkflowTrackingTable', 'CreatedDateTime')+' DESC)';

return ret;

}

}

 

The next step would be to use this new field on the data entity as a range with value of 1 as according to the computed column our latest record would be of rank 1.

 

 

 

And with that, you have a first only function on a data entity.

 

Electronic Reporting in D365: Replacing Existing Field with a Financial Dimension in an Outgoing Report Structured as Zip of Text Files

ER is a tool that you can use to configure formats for both incoming and outgoing electronic documents in accordance with the legal requirements of various countries/regions. ER lets you manage these formats during their lifecycle.

 

For example, you can adopt new regulatory requirements, and generate business documents in the required format to electronically exchange information with government bodies, banks, and other parties.

 

In this blog, general overview of ER components will be covered, together with a tutorial presenting a way how to add financial dimension to an existing electronic report.

 

Electronic Reporting Components

 

Electronic reporting (ER) supports the following types of components:

 

  • Data model
  • Model mapping
  • Format
  • Metadata

Data Model Component


A data model component is an abstract representation of a data structure. It describes a specific business domain area in enough detail to satisfy the reporting requirements for that domain. A data model component consists of the following parts:

 

  • Data Model: A set of domain-specific business entities and a hierarchically structured definition of relations between those entities.

 

  • Model Mapping: Linking between individual elements of the data model and selected application data sources, specifying the data flow of the model itself.

 

The business entity of a data model is represented as a container, or record. Business entity properties are represented as data items, or fields. Each data item has a unique name, label, description, and value.

 

The value of each data item can be designed so that it’s recognized as a string, integer, real, date, enumeration (enum), or Boolean value. Additionally, the data item can be another record or record list.

 

Model Mapping Component

 

Model mapping links application data sources to individual elements of a data model that specify, at runtime, the data flow, and rules for entering business data in a data model component. It can use different data types as data sources for a data model.

 

These data types include tables, data entities, methods, and enums. It supports user input parameters that can be defined as data sources for a data model when some data must be specified at runtime (for example, which financial dimension to be included). Also filtering, sorting and other operations are supported, but only for outgoing documents.

 

Format Component

 

A format component is the scheme of the reporting output that is generated at runtime. A scheme consists of a format that defines the structure and content of the generated electronic document, data sources, format mapping as a set of bindings and a format validation as a set of configurable rules that control report generation at runtime.

 

For example, there might be a rule that stops output generation of a vendor’s payments and throws an exception when specific attributes of the selected vendor are missing, such as the bank account number.

 

Created reporting output can be as individuals file in various formats, such as text, XML, Microsoft Word document, or worksheet. But there is also a possibility of creating multiple files separately and encapsulating them into zip file. The following illustration shows how the data flows in outgoing report.

 

 

Problem Description

 

In France each company has to deliver so-called FEC report to the government. It is an export of several entities having a specific format. The format is very strict, that means when it is not correct, the declaration will not be approved.

 

The FEC report is available in F&O as part of the electronic reporting functionality, a localization made for France. However, the export of the ledger transactions is based upon the Main account.

 

In France you need a specific local chart of account. Imagine that this account is stored in a custom-made financial dimension called: FRACOA. The main goal is to replace main account values with FRACOA values.

 

To be more precise, the account number (CompteNum) should be replaced with the dimension’s value, while account description (CompteLib) should be replaced with the dimension’s description.

 

 

 

The report’s output format is a ZIP file containing 5 text
documents. Only the values in the first document called
1_FEC.txt need to be replaced. 

 

 

 

Solution

 

Because the ER already exists and it’s part of Microsoft standard configurations library, we do not want to create a new model, but rather to create a duplicate of the existing one. In that way, we can do modifications on it.

 

Modifications of an existing electronic report can be done only on a duplicated version of it. You are not allowed to change or modify existing standardized configurations.

 

 

The FEC report is located under the configuration named
‘Data export model’. As we can see, it consists of a model
definition, model mapping and two report designs (German
and French localization).

 

 

If you are missing the configuration for your report, then first you should go to Workspaces > Electronic reporting > from Configuration providers select Microsoft and click on Repositories > select Global and click Open > find the desired model and click Import/Import all (depending on if you would like to import everything or just particular configuration).

 

If we take a look at the design of the data model, we can see that it has quite a complex structure unlike ones that are usually used. Fields that will be included in the final report are rather dynamically structured and, in the model, we just define some fundamental nodes so they can fetch the data.

 

In this model, all fields are implicitly defined, contrary to most of the models where nodes (fields) are explicitly defined. The Tables node defines files from the zip, while in Records is defined the structure of records in one file.

 

If we expand one of the (*)Fields node, we can only see some type of rules to check what type is the value in the field, to cast it in the appropriate type and some additional metadata for the field.

 

 

This means that the model is pretty conserved and made to be generic. We do not need to change anything here. Thus, there is no need of duplicating the data model. Instead, we need to duplicate the model mapping and do our changes there.

 

Duplicating the Data Model Mapping

 

  • Make sure you are located in Workspaces > Electronic reporting > Reporting configurations > locate and expand the desired model in the tree, in our case it is ‘Data export model’

 

  • Select the model mapping, in our case it is ‘French FEC model mapping’ and click on Create configuration

 

  • In the drop dialog, for the New field select ‘Derive from Name: French FEC model mapping, Microsoft’. In the Name field, type the name for the mapping ‘AXM – French FEC model mapping’. In the Description field, you can set a description, but it is not mandatory. Click Create configuration.

 

Map the  Model to Use Financial Dimensions

 

  • In the tree, select the newly created model mapping ‘AXM – French FEC model mapping’ and click on Designer in the action pane.
  • Since we are creating a duplicate instead of new mapping, in the grid there should already be a record for the mapping – copy of the original one. Select it and click Designer.
  • In the Model mapping designer form, it can be seen that there are already some data sources defined. Those data sources are used for the current implementation of the report. First thing we need to do is to add the data source for using financial dimensions for this report.

 

  • In the tree, select ‘Functions\Financial dimensions details’ and click Add root. This data source specifies how the scope of financial dimensions will be defined for any report that will use this model as a data source.


  • In the Name field, type a value. Select Yes in the Ask for dimensions field (select Yes to allow the user to select dimensions at run-time on the User dialog form. If set to No, all financial dimensions of the current instance will be used by default).

 

  • In the Financial dimensions selection field, select ‘Legal entity’ (select All to allow the user to select desire dimensions for the current instance in the Lookup field. Select Legal entity to allow the user to select dimensions for the company in the Lookup field. Select Dimension to allow the user to select dimensions using a single dimension set.

 

  • Select Yes in the Ask for main account field (set ‘Ask for main account’ to Yes to allow users to select the main account as part of the list of dimensions. If set to No, the main account will not be included to the list of dimensions and the ‘Is main account mandatory’ option is enabled. If “Is main account mandatory’ is set to Yes, include the main account in the list of dimensions regardless of the user’s selection).

 

  • Click OK.

 

  • The newly created data source should be visible among other data sources in the middle section of the form.


 

  • If we take a closer look at data sources and data model, we can see that Tables node in the data model is bonded to the TblGroup data source. This data source is actually a calculated field that serves as a bridge between the model and the data source named TblGroup1. We need to find the source of data providing and in this case that is the data source ‘FEC files with details(TblGroup1)’. This data source is defined using specific function (Dynamics.AX.Application.ERModelGDPdUFunction). By expanding it, we cannot add fields or change mapping because its structure is generic too.



If you cannot see details about data sources binding and inheritance, click on Show details button in the main action pane.



 

  • Select FEC files with details(TblGroup1) and click on Edit button in the ribbon.

 

  • A new dialog will appear showing data source parameters. In addition, there is a button named Editor which allows us to enter the editing mode for making changes in the structure of the report. Click on Editor.


 

  • Table Metadata form is opened. This form is divided into three parts:

 

    • Tables (most left section) where all files are being present. In this grid the name of the table (file) is being shown together with a path relating to a particular data source.

 

    • Fields (middle section) where all fields related to a selected table from the left grid are being shown. In this grid, the name of the field is being shown together with a path relating to a particular node in a data source.

 

    • Data sources (right section) where all active data sources for the desired model are being shown. Above the grid, there is an Add button. Using this button, we can find the field we want in any active data source and add it in the desired table.





If you are not able to see all sections, zoom out the browser settings.

 

  • Select the desired table in the left section. In our case, because we need the change to be applied only to the 1_FEC.txt file, we select the table 1_FEC.

 

  • In the middle section, select fields you would like to replace. In this case, we select CompteNum and CompteLib. Click Delete.


 

  • In the right section, expand the general journal entry where financial dimensions are stored. In this case, we expand $GeneralJournalForGDPdUView data source. If you are not sure which data source to use, take the one that other fields from the same table are mapped from.

 

  • Expand and select ‘$GeneralJournalForGDPdUView/getGeneralJournalAccountEntry() /LedgerDimension.Dimension/Main account and dimensions/Value/Code’.

 

  • Click Add.


 

  • Expand and select ‘$GeneralJournalForGDPdUView/getGeneralJournalAccountEntry() /LedgerDimension.Dimension/Main account and dimensions/Value/Description’.

 

  • Click Add.

 

  • New fields should be visible somewhere in the grid, under their original names. Click on the Name field with value ‘Code’ and change it to ‘CompteNum’. Change the value ‘Description’ to ‘CompteLib’.

 

  • Using the Move up and Move down buttons, you can change the order of the fields in the grid. This is important because in same manner the fields will be shown in the output file.


  • Save the changes. Click Close.

 

  • Save the changes again and click Close.

 

  • From the configuration tree, select the desired model mapping, in this case it is ‘AXM – French FEC model mapping’. Select the Draft version and click Change status.

 

  • From the drop dialog, select Complete. If it’s the first time you are modifying the mapping. Now you will get two records in the gird, one that is Completed and one that is in Draft. After every change, if we want it to be reflected in the output, we need to set the latest draft version to Completed.

 

  • In order the report to take our custom-made mapping, we need to set it as default. This way, the system will know what mapping to choose when running the report. Make sure the correct mapping is still selected.

 

  • Click Edit and set the control Default model mapping to Yes.

 

Designing the Report

 

If we open the format designer to see the structure of the report, we can notice that it is generic, same as the model itself. This means that no changes need to be done here as well.

 

The advantage of using this approach for modeling reports is that the structure of the report is partially conserved, but if you do need to do some small changes, you only need to do changes into the mapping part.

 

 

Running the Report

 

Depending how this report is set to be ran in standard environment, you can test it from there, or you can navigate to the configurations form and:

 

  • Expand the model we created mapping for and select the format that already exists, in this case it is ‘French FEC audit file’ and click Run.

  • In the Dimensions field, choose which dimension you would like to be shown in the output report instead of the main account number and click Select (if FRACOA dimension is chosen, then we will get its values in the report, otherwise if the MainAccount is chosen, then we will obtain the original report from before.

 

  • This implementation gives flexibility in the way of execution. Note that the drop dialog to choose a dimension is not available in the original configuration of the report. This is customization that was implemented using this tutorial).

 

  • After clicking the OK button, a zip file is being generated and downloaded to the local computer. The downloaded zip file has a structure similar to this:




  • We can open the first text file and compare it with a previously downloaded version of the original report.

 

 

 

We can notice that the main account values are indeed replaced with FRACOA values for all entries. No changes were made to other 4 files so they will keep the same structure and data.

 

Following this tutorial, you should be able to do a modification on any report you need to. The most important thing is to understand the logic behind components of electronic reports and their binding. In a situation where the model and the format have explicit structure and mapping from data sources is done directly, creating the desired mapping has a little bit more work to be done. You just need to stick to the following sequence:

 

Create duplicate of a model > Create the node you would like to be shown in the output (make sure you create it under the correct parent node) > Create duplicate of the model mapping > If the data source where your field comes from is not present, create it > Bind the data source field and the newly created node in the model > Create a duplicate of the format and add the node there too > Map the node from the model with the node in the format.

 

No need to thanks us, we are glad to help you. So, if you are in need of someone to upgrade your systems from Microsoft Dynamics AX to Dynamics 365, we are open to future collaboration. Reach out to us here.

 

And if you are interested to be part of our team, check out our careers page we have open job vacancies all the time!

Application Insights with D365 F&O

What is Application Insights?

 

Application Insights, a feature of Azure Monitor, is an extensible Application Performance Management (APM) service for developers and DevOps professionals. Use it to monitor your live applications. It will automatically detect performance anomalies, and includes powerful analytics tools to help you diagnose issues and to understand what users do with your app.

 

It’s designed to help you continuously improve performance and usability. It works for apps on a wide variety of platforms including .NET, Node.js, Java, and Python hosted on-premises, hybrid, or any public cloud. It integrates with your DevOps process and has connection points to a variety of development tools. It can monitor and analyze telemetry from mobile apps by integrating with Visual Studio App Center.

 

 

Application Insights can be used to log:

 

  1. Events Azure Application Insights Telemetry Data Model – Event Telemetry – Azure Monitor | Microsoft Docs
  2. Exceptions Azure Application Insights Exception Telemetry Data model – Azure Monitor | Microsoft Docs
  3. Requests Data model for request telemetry – Azure Application Insights – Azure Monitor | Microsoft Docs
  4. Metrics Data model for metric telemetry – Azure Application Insights – Azure Monitor | Microsoft Docs
  5. Traces Azure Application Insights Data Model – Trace Telemetry – Azure Monitor | Microsoft Docs

 

How to create Application Insights Create Application Insights | Microsoft Docs

 

Something more about the Application insights Application Insights Overview | Microsoft Docs

 

How We Can Connect D365 with Azure Application Insights

 

Open your environment in the browser and navigate to the System Administration -> Setup -> Monitoring and telemetry parameters form.

 

 

From here, you can enable the different telemetry types that are currently supported. More will be added over time. Each type has the Dynamics terminology followed by the Application Insights terminology in parenthesis. This will help you know where to locate it in Application Insights as well as do further learning on docs.microsoft.com.

 

 

Setup your environments, by copying the LCS Environment ID into the setup screen and mapping it to an Environment Type:

 

 

Thereafter, you can map environment types to Application Insights destinations:

 

 

The instrumentation key is retrieved from your Application Insights resource within Azure

 

 

As you copy your Finance and Operations database between environments, it will instantly pick up the new Application Insights configuration and automatically send telemetry to the correct location. If you copy your database to an environment which is not configured, then the environment will gracefully do nothing, and no telemetry will be sent.

 

D365 Standard Class Used for Application Insights

 

  • SysGlobalTelemetry class has methods to log text to Azure Application Insights.

 

We can use these methods to log metrics for Interfaces to see for how much time the file is exported and uploaded to blob.

 

    • logMetrics() – Tracks how long, on average, a compute node takes to download the required text file.
    • logEvents() – Tracks interesting events to capture.
    • global_pre_error() – logs error.

 

Example:

 

 

Creating Custom C# dll

 

Application Insights is a very popular (and FREE) approach to monitoring your websites and desktop applications. It works best with websites hosted by IIS, however it can be built into any product.

 

Create a custom C# dll to implement the methods you need to consume the Microsoft.ApplicationInsights.dll to make it easier to use from X++.

 

The handler DLL will enable to:

 

  • Set context properties like user, operation, version & custom global properties;
  • Instance and track telemetry for Events, Exceptions, Requests, Metrics, Traces;
  • Add custom properties & custom metrics related with Events, Exceptions, Requests, Metrics, Traces.

 

Let’s start by building our own C# library which uses the latest NuGet package for AppInsights:

 

 

After you have your C# library setup and the NuGet package installed, we can start developing class that can handle our events.
Here is the example of the code from my C# class. This code is only to instance and track telemetry for Exceptions and Requests.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.DependencyCollector;
using Microsoft.ApplicationInsights.Web;

namespace AppInsightsHandler
{
    public class AppInsightsHandler
    {
        
        private TelemetryClient 	Client;

        private RequestTelemetry 	requestTelemetry;
        private ExceptionTelemetry 	exceptionTelemetry;

        string objectType;

        List<KeyValuePair<string, string>> properties = new List<KeyValuePair<string, string>>();
        List<KeyValuePair<string, string>> ContextGlobalProperties = new List<KeyValuePair<string, string>>();
        List<KeyValuePair<string, Double>> metrics = new List<KeyValuePair<string, Double>>();

        public void instantiateClient(string _instrumentationKey, string _objectType)
        {

            if (string.IsNullOrEmpty(_instrumentationKey))
            {
                throw new ArgumentNullException(nameof(_instrumentationKey), "Instrumentation key is required.");
            }
            if (string.IsNullOrEmpty(_objectType))
            {
                throw new ArgumentNullException(nameof(_objectType), "Object type is required.");
            }
            objectType = _objectType;
            TelemetryConfiguration config = new TelemetryConfiguration(_instrumentationKey);
            Client = new TelemetryClient(config);
            switch (objectType)
            {
                case "ExceptionTelemetry":
                    exceptionTelemetry = new ExceptionTelemetry();
                    break;
                case "RequestTelemetry":
                    requestTelemetry = new RequestTelemetry();
                    break;
            }

        }

        public void addProperty(string _key, string _value)
        {
            KeyValuePair<string, string> property = new KeyValuePair<string, string>(_key, _value);
            if (!properties.Exists(item => item.Key == _key))
            {
                properties.Add(property);
            }
        }

        public void addContextGlobalProperty(string _key, string _value)
        {
            KeyValuePair<string, string> property = new KeyValuePair<string, string>(_key, _value);
            if (!ContextGlobalProperties.Exists(item => item.Key == _key))
            {
                ContextGlobalProperties.Add(property);
            }
        }
        public void addMetrics(string _key, Double _value)
        {
            KeyValuePair<string, Double> metric = new KeyValuePair<string, Double>(_key, _value);
            if (!metrics.Exists(item => item.Key == _key))
            {
                metrics.Add(metric);
            }
        }

        public void setContextUserInfo(string _accountId, string _authenticatedUserId, string _userId = "", string _userAgent = "")
        {
            switch (objectType)
            {
                case "ExceptionTelemetry":
                    exceptionTelemetry.Context.User.AccountId = _accountId;
                    exceptionTelemetry.Context.User.AuthenticatedUserId = _authenticatedUserId;
                    exceptionTelemetry.Context.User.Id = _userId;
                    exceptionTelemetry.Context.User.UserAgent = _userAgent;
                    break;
                case "RequestTelemetry":
                    requestTelemetry.Context.User.AccountId = _accountId;
                    requestTelemetry.Context.User.AuthenticatedUserId = _authenticatedUserId;
                    requestTelemetry.Context.User.Id = _userId;
                    requestTelemetry.Context.User.UserAgent = _userAgent;
                    break;
            }
        }
        public void setContextOperation(string _operationId, string _operationName, string _parentId = "")
        {
            switch (objectType)
            {
                case "ExceptionTelemetry":
                    exceptionTelemetry.Context.Operation.Id = _operationId;
                    exceptionTelemetry.Context.Operation.Name = _operationName;
                    exceptionTelemetry.Context.Operation.ParentId = _parentId;
                    break;
                case "RequestTelemetry":
                    requestTelemetry.Context.Operation.Id = _operationId;
                    requestTelemetry.Context.Operation.Name = _operationName;
                    requestTelemetry.Context.Operation.ParentId = _parentId;
                    break;
            }
        }
        public void setContextComponentVersion(string _version)
        {
            switch (objectType)
            {
                case "ExceptionTelemetry":
                    exceptionTelemetry.Context.Component.Version = _version;
                    break;
                case "RequestTelemetry":
                    requestTelemetry.Context.Component.Version = _version;
                    break;
            }
        }

        public void setContextGlobalProperties()
        {
            foreach (KeyValuePair<string, string> element in ContextGlobalProperties)
            {
                
                switch (objectType)
                {
                    case "ExceptionTelemetry":
                        exceptionTelemetry.Context.GlobalProperties.Add(element);
                        break;
                    case "RequestTelemetry":
                        requestTelemetry.Context.GlobalProperties.Add(element);
                        break;
                }
            }
        }
       
        public void setsysExceptionTelemetryValues(Exception _exception, string _message, string _problemId, int _severityLevel)
        {
            if (exceptionTelemetry != null)
            {

                exceptionTelemetry.Exception = _exception;
                exceptionTelemetry.Message = _message;
                exceptionTelemetry.ProblemId = _problemId;
                switch (_severityLevel)
                {
                    case 4:
                        exceptionTelemetry.SeverityLevel = SeverityLevel.Critical;
                        break;
                    default:
                        exceptionTelemetry.SeverityLevel = SeverityLevel.Information;
                        break;
                }
                exceptionTelemetry.Timestamp = DateTimeOffset.UtcNow;
                foreach (KeyValuePair<string, string> element in properties)
                {
                    exceptionTelemetry.Properties.Add(element);
                }
                foreach (KeyValuePair<string, Double> element in metrics)
                {
                    exceptionTelemetry.Metrics.Add(element);
                }

            }
        }
  public void TrackTelemetry()
        {
            if (Client != null && objectType != "")
            {
                switch (objectType)
                {
                    case "ExceptionTelemetry":
                        Client.TrackException(exceptionTelemetry);
                        break;
                    case "RequestTelemetry":
                        Client.TrackRequest(requestTelemetry);
                        break;
                }
                Client.Flush();
            }
        }
        public void setExceptionTelemetryValues(string _exceptionMessage, string _exceptionSource, string _message, string _problemId, int _severityLevel)
        {
            if (exceptionTelemetry != null)
            {
                Exception e = new Exception(_exceptionMessage);
                e.Source = _exceptionSource;
                exceptionTelemetry.Exception = e;
                exceptionTelemetry.Message = _message;
                exceptionTelemetry.ProblemId = _problemId;
                switch (_severityLevel)
                {
                    case 0:
                        exceptionTelemetry.SeverityLevel = SeverityLevel.Verbose;
                        break;
                    case 1:
                        exceptionTelemetry.SeverityLevel = SeverityLevel.Information;
                        break;
                    case 2:
                        exceptionTelemetry.SeverityLevel = SeverityLevel.Warning;
                        break;
                    case 3:
                        exceptionTelemetry.SeverityLevel = SeverityLevel.Error;
                        break;
                    case 4:
                        exceptionTelemetry.SeverityLevel = SeverityLevel.Critical;
                        break;
                    default:
                        exceptionTelemetry.SeverityLevel = SeverityLevel.Information;
                        break;
                }
                exceptionTelemetry.Timestamp = DateTimeOffset.UtcNow;
                foreach (KeyValuePair<string, string> element in properties)
                {
                    exceptionTelemetry.Properties.Add(element);
                }
                foreach (KeyValuePair<string, Double> element in metrics)
                {
                    exceptionTelemetry.Metrics.Add(element);
                }

            }
        }
    
        public void setRequestTelemetryValues(string _requestName, string _requestId, Boolean _success, string _responseCode, string _source, string _uriString, Double _duration)
        {
            if (requestTelemetry != null)
            {
                requestTelemetry.Name = _requestName;
                requestTelemetry.Timestamp = DateTimeOffset.UtcNow;
                requestTelemetry.Success = _success;
                requestTelemetry.Id = _requestId;
                requestTelemetry.ResponseCode = _responseCode;
                requestTelemetry.Source = _source;
                requestTelemetry.Duration = TimeSpan.FromSeconds(_duration);
                if (_uriString != "")
                {
                    Uri uri = new Uri(_uriString);
                    requestTelemetry.Url = uri;
                }

                foreach (KeyValuePair<string, string> element in properties)
                {
                    requestTelemetry.Properties.Add(element);
                }
                foreach (KeyValuePair<string, Double> element in metrics)
                {
                    requestTelemetry.Metrics.Add(element);
                }
            }
        }

    }
}

 

After that create an X++ project in your solution and add a reference to your C# library:

 

 

Thereafter, let’s create an X++ class which makes use of our telemetry library:

 

 

Let us know if this helped you, and if you are interested to work with Microsoft Dynamics 365 you can check out our careers page for the open job positions. 

 

Processing an Excel File in Batch Using the FileUpload Functionality Together with SysOperationFramework

Sometimes in D365 things happen which are either human or system error, which will leave you with faulty data in your system.

 

That kind of thing happened some time ago for one of our clients, when a daily batch job removed all the loads for the purchase orders which had lines who have ‘Registered’ quantities, and left them with more than 3000 purchase orders for which they couldn’t create and post a product receipt journal, because they didn’t have the load associated with them, and going with the logic of creating a new batch job for posting product receipts for quantity = ‘Registered’ would’ve broke their workflow.

 

So, they exported the purchase orders that they wanted to update, and after seeing that the update should be done for more than 3000 orders, I knew that I should extend the SysOperationFramework, so the update could be done in batch processing on the server, and the users wouldn’t experience any performance issues on the interface itself.

 

When talking about extending the SysOperationFramework parts, you probably already know that we will have the well-known structure of classes in order to design our batch process: the controller class, responsible for running the operation and modifying the contract, the contract class, responsible for passing parameters and values from the user to the logic that processes operation, the service class, responsible for providing the logic which is being executed (in our example, iterating through the Excel file, getting the purchase orders IDs and posting a product receipt for them), and the User Interface builder (UIBuilder) class, responsible for the actual looks and event handlers of the dialog that is presented to the user.

 

Let’s have a look at what our code changes will look like in the D365 user interface, before scheduling the actual batch job:

 

 

Analyzing this dialog, we will see that it looks like every other batch dialog, with only one parameter: a file.

 

Below, we will see the code which is taking care of the design and functionality for this batch process.

 

The Controller Class:

 

/// <summary>
/// Controller class for creating & posting PO packing slips
/// </summary>
class AXMPostPOReceiptsController extends SysOperationServiceController
{
	/// <summary>
    /// Creates new instance of <c>AXMPostPOReceiptsController</c>
    /// </summary>
    public void new()
    {
        super();
     
        this.parmClassName(classStr(AXMPostPOReceiptsService));
        this.parmMethodName(methodStr(AXMPostPOReceiptsService, processOperation));
     
        this.parmDialogCaption("Create packing slips");
    }
	
	/// <summary>
    /// Sets the caption of the job
    /// </summary>
    /// <returns>Caption of the job</returns>
    public ClassDescription caption()
    {
        return "Create packing slips";
    }
	
	/// <summary>
    /// Method which is run while calling the corresponding menu item
    /// </summary>
    /// <param name = "args">Arguments passed from the menu item</param>
    public static void main(Args args)
    {
        AXMPostPOReceiptsController controller;
     
        controller = new AXMPostPOReceiptsController();
        controller.startOperation();
    }
}

 

Looking at the code for the AXMPostPOReceiptsController, we see that it has 3 methods, of which 2 are overridden: new and caption. Above every method there is a summary explanation of what that method is doing. We can see that in the new method, we are initializing a new instance of the controller class and are defining the processing class and method (the service class), in our case, AXMPostPOReceiptsService class.

 

Please don’t be confused about the caption and the dialog caption for the batch job, ‘Create packing slips’, we know that sometimes we also refer to the product receipts as packing slips.

 

The Data Contract Class:

 

/// <summary>
/// Data contract class for creating & posting PO packing slips
/// </summary>
[   DataContract,
    SysOperationContractProcessing(classStr(AXMPostPOReceiptsUIBuilder))
]
class AXMPostPOReceiptsDataContract
{
    container       storageResult;

	/// <summary>
    /// Parameter method which holds values of the packed variables from <c>FileUploadTemporaryStorageResult</c> class
    /// </summary>
    /// <param name = "_storageResult">Packed instance of <c>FileUploadTemporaryStorageResult</c> class</param>
    /// <returns>Container with packed values</returns>
    [DataMemberAttribute('StorageResult')]
    public container parmStorageResult(container _storageResult =  storageResult)
    {
        storageResult = _storageResult;
        return storageResult;
    }
}
    

 

As said, the data contract class holds the parameters that user wants to pass to the logic which is being executed. In our case, we have one parameter that we’re passing, storageResult of type container. As explained in the summary, this variable holds the values of the packed variables of the FileUploadTemporaryStorageResult class instance, which in fact holds the upload result of the FileUpload control.

 

As for now, the only thing that you need to know is that FileUploadTemporaryStorageResult implements SysPackable, which means that we can do serialization on it, like pack and unpack the variables defined in the #CurrentList for the #CurrentVersion macro.

 

Note: Please visit the File upload control page, where you can find documentation on this control.

 

The UI Builder Class:

 

/// <summary>
/// UI Builder class for posting purchase orders packing slips
/// </summary>
class AXMPostPOReceiptsUIBuilder extends SysOperationUIBuilder
{
    private str                 availableTypes = ".csv, .xlsx";
    private const str           OkButtonName = 'CommandButton';
    private const str           FileUploadName = 'FileUpload';
	
    AXMPostPOReceiptsDataContract   contract;

	/// <summary>
    /// Overriden the <c>postBuild</c> method to add a <c>FileUpload</c> control
    /// </summary>
    public void postBuild()
    {
        DialogGroup      dialogGroup;
        FormBuildControl formBuildControl;
        FileUploadBuild  dialogFileUpload;

        super();

        contract = this.dataContractObject();
        
        dialogGroup = dialog.addGroup("File path");
        formBuildControl = dialog.formBuildDesign().control(dialogGroup.name());
       
        dialogFileUpload = formBuildControl.addControlEx(classstr(FileUpload), FileUploadName);
        dialogFileUpload.style(FileUploadStyle::MinimalWithFilename);
        dialogFileUpload.baseFileUploadStrategyClassName(classstr(FileUploadTemporaryStorageStrategy));
        dialogFileUpload.fileTypesAccepted(availableTypes);
        dialogFileUpload.fileNameLabel("@SYS308842");
    }

	/// <summary>
    /// Subscribes events to the dialog form
    /// </summary>
    /// <param name = "_formRun">The instance of the dialog form</param>
    private void dialogEventsSubscribe(FormRun _formRun)
    {
        FileUpload fileUpload = _formRun.control(_formRun.controlId(FileUploadName));
        fileUpload.notifyUploadCompleted += eventhandler(this.uploadCompleted);
        fileUpload.notifyUploadAttemptStarted += eventhandler(this.uploadStarted);
        _formRun.onClosing += eventhandler(this.dialogClosing);
    }
	
	/// <summary>
    /// Executes logic for unsubscribing the registered events on the form
    /// </summary>
    /// <param name = "sender"></param>
    /// <param name = "e"></param>
    [SuppressBPWarningAttribute('BPParameterNotUsed', 'This is event parameter not required to use')]
    private void dialogClosing(xFormRun sender, FormEventArgs e)
    {
        this.dialogEventsUnsubscribe(sender as FormRun);
    }
	
	/// <summary>
    /// Unsubscribes events from the dialog form
    /// </summary>
    /// <param name = "_formRun">The instance of the dialog form</param>
    private void dialogEventsUnsubscribe(FormRun _formRun)
    {
        FileUpload fileUpload = _formRun.control(_formRun.controlId(FileUploadName));
        fileUpload.notifyUploadCompleted -= eventhandler(this.uploadCompleted);
        fileUpload.notifyUploadAttemptStarted -= eventhandler(this.uploadStarted);
        _formRun.onClosing -= eventhandler(this.dialogClosing);
    }
	
    /// <summary>
    /// Executes additional logic once the upload of the file is completed
    /// </summary>
    protected void uploadCompleted()
    {
        var formRun = this.dialog().dialogForm().formRun();
        FileUpload fileUpload = formRun.control(formRun.controlId(FileUploadName));
        FileUploadTemporaryStorageResult uploadResult = fileUpload.getFileUploadResult();

        if (uploadResult != null && uploadResult.getUploadStatus())
        {
            contract.parmStorageResult(uploadResult.pack());
        }

        this.setDialogOkButtonEnabled(formRun, true);
    }
	
	/// <summary>
    /// Additional logic which is executed once the upload of the file has started
    /// </summary>
    private void uploadStarted()
    {
        var formRun = this.dialog().dialogForm().formRun();
        this.setDialogOkButtonEnabled(formRun, false);
    }

    /// <summary>
    /// Enables/Disables the OK button of the dialog
    /// </summary>
    /// <param name = "_formRun">The instance of the dialog form</param>
    /// <param name = "_isEnabled">Should the OK button be enabled?</param>
    protected void setDialogOkButtonEnabled(FormRun _formRun, boolean _isEnabled)
    {
        FormControl okButtonControl = _formRun.control(_formRun.controlId(OkButtonName));
        if (okButtonControl)
        {
            okButtonControl.enabled(_isEnabled);
        }
    }

    /// <summary>
    /// Override of the <c>postRun</c> method in order to add events subscriptions
    /// </summary>
    public void postRun()
    {
        super();

        FormRun formRun = this.dialog().dialogForm().formRun();
        this.dialogEventsSubscribe(formRun);

        this.setDialogOkButtonEnabled(formRun, false);
    }

}

    

We can see that in this class we have more methods and more logic. You can also notice that we have event handlers, which should run the specified code once the event is started or completed. Analyzing the code, first we can see that the method postBuild has been overridden. Here, we’re constructing the actual look of the dialog. We can see that first we’re creating a group for the file path, and after that we’re adding the FileUpload control, we’re setting the style and the file upload strategy (you can see the documentation in the referenced documentation for File upload). We can also set the available types that user will be able to choose and upload. You can see that the availableTypes is actually string variable with file extensions defined.

 

After we’re done with adding controls to the dialog, it’s time to make the form responsive, and that’s when the event handlers come into action. As for every event, first we need to do a subscription to a certain event, and after we’re finished, we unsubscribe from that event. So, next we will have a look at method postRun which is called once the dialog is built and ran.

 

In this method, we see that we’re taking instance of the current dialog (FormRun), we’re making event subscriptions for that FormRun instance and we’re calling the method setDialogOkButtonEnabled which as the name suggests, it’s either enabling/disabling the ‘OK’ button, which is actually submitting the form displayed on the dialog. The solution here is only enabling the ‘OK’ button once we have successfully uploaded a file.

 

Now, let’s review the logic for subscribing and unsubscribing to an event. Looking at method dialogEventsSubscribe, we see that we’re retrieving the FileUpload control that we’ve created in postBuild method, and we’re adding (subscribing) to events for once the upload has attempted to start and once the upload is completed. The event handlers for these events are respectively methods uploadStarted and uploadCompleted. After that, we’re assigning an event handler for the onClosing event of the FormRun, which is basically calling another method, dialogEventsUnsubscribe, which is basically subtracting (unsubscribing) the event handlers from the events.

 

Looking at the event handlers, we can see that event handler uploadStarted doesn’t do anything special, but only disables the dialog ‘OK’ button. However, the other event handler, uploadCompleted, we’re once again retrieving the FileUpload, but this time, because the upload is completed, it means that we can take the upload result and store it somewhere (or do additional logic with it).

 

After taking the result of the file upload, as explained before, I am packing the result and sending it as a parameter to our contract class, so I can later use it in our service class, where all the logic for creating and posting the product receipts is located.

 

The Service Class

 

/// <summary>
/// Service class which is executing the logic for creating & posting PO packing slips
/// </summary>
class AXMPostPOReceiptsService extends SysOperationServiceBase
{
    #File
    container               currentLine;
    CommaTextStreamIo       localStream;
    PurchId                 currentPurchId;
    PurchTable              purchTable;

    /// <summary>
    /// The actual logic of the process
    /// </summary>
    /// <param name = "_contract">Instance of the <c>AXMPostPOReceiptsDataContract</c> class</param>
    public void processOperation(AXMPostPOReceiptsDataContract _contract)
    {
        if (_contract.parmStorageResult() != conNull())
        {
            FileUploadTemporaryStorageResult fileUploadResult = new FileUploadTemporaryStorageResult();

            fileUploadResult.unpack(_contract.parmStorageResult());

            if (fileUploadResult != null)
            {
                try 
                {
                    localStream = CommaTextStreamIo::constructForRead(File::UseFileFromURL(fileUploadResult.getDownloadUrl()));

                    if (localStream.status() != IO_Status::Ok)
                    {
                        throw error(strfmt('Is not possible to open the file. Error %1',enum2str(localStream.status())));
                    }
   
                    localStream.inFieldDelimiter("\,");
                    localStream.inRecordDelimiter("\n");
   
                    currentLine = localStream.read();
   
                    while(currentLine)
                    {
                        currentPurchId = conPeek(currentLine, 1);

                        purchTable = PurchTable::find(currentPurchId);

                        if (purchTable)
                        {
                            Num psNumber = strFmt("%1_PS", currentPurchId);

                            PurchFormLetter     purchFormLetter = PurchFormLetter::construct(DocumentStatus::PackingSlip);
                            purchFormLetter.update(purchTable, psNumber, systemDateGet(), PurchUpdate::Recorded, AccountOrder::None, NoYes::No, NoYes::No);
                        }
            
                        currentLine = localStream.read();
                    }
                }
                catch (Exception::Error)
                {
                    error("@RET433");
                }
            }
        }
    }
}

 

Since we’re only doing a creation and posting of a product receipt for a certain purchase orders, the logic here will be pretty simple. We can see that we have once method, processOperation, which is taking an instance of the data contract class. With the help of the data contract, we can transfer the packed values of the file upload result from the UI builder to the service class.

 

Once we have unpacked the result, we’re constructing an IO stream, to which we’re using a file from URL, and that URL is generated with the help of FileUploadTemporaryStorageResult based on the unpacked values. I have wrapped all of the logic in try – catch block, and the code iterates trough the file that the user has uploaded previously, and it’s getting column values (in my case, it’s getting only the values from the first column, since I don’t have additional columns).

 

Once the value is taking, code is checking whether a purchase order exists for that purchase ID, and if it does, it proceeds with creating and posting the product receipt with the help of the PurchFormLetter class, and as you can see the parameters for the update are a buffer of the purchase table, a number for the packing slip (which in this case is generated manually, but that can be done via number sequences too), the date of the posting, the update parameter (in this case, only ‘Registered’ lines will be included), the account order set to ‘None’, and the settings for the proforma and whether the packing slip should be printed or not (both options set to ‘No’).

 

This was everything for processing a file in batch using the SysOperationFramework that you should know for starters. I really hope that I have well explained the bits of code which were presented.

 

Please note that there is also another way to solve this, and that would be to create a data entity and a table in which you will place your values (for this example, purchase order IDs), and also a batch classes and a query against the newly created table, so you can process the data that you have implemented. Both solutions work well, and although the data entity solution is more extendible, the one with the file upload is quicker and less painful.

What Is Performance Testing and Why Do You Need One?

Digital transformation is not an option for a long time ago, and many businesses are doing it. Only entering into the digital world is not sufficient anymore, you also need to build a culture of continuous improvements, that should help you to maintain the leadership position by meeting customer and business demands.

 

On one hand you have your business, which is fully automated with high expectations on stability, reliability, and capability of their applications towards achieving their goals. Any downtime or malfunction might lead to huge losses.

 

On the other hand, you have your smart and digitally aware customers that expect responsive systems, around-the-clock availability, immediate / on-time order fulfillment, quick invoicing, and zero errors. And these are the most common drivers of progress that shouldn’t be disappointed.

 

How Can Performance Testing Help You in Achieving Your Strategic Goals?

 

Performance testing is a powerful tool that applied against a variety of use case scenarios provides valuable insights to identify a company’s digital infrastructure performance risks.

 

Companies can and must be proactive in the identification of those risks that might damage their reputation, cause revenue and/or opportunity losses, but also in their mitigation, by devoting a certain amount of resources to perform performance testing and act upon the results. 

 

When Is the Best Time to Run Performance Testing?

 

Our recommendation is to perform performance testing:

 

Before Going Live

 

Once the solution is stable from a functionality perspective, it’s time to run performance testing to uncover what needs to be improved before they go live. 

 

Before Deploying Major Changes  

 

Any change in the system is potentially dangerous with certain risks associated with it and must be approached carefully. Especially when you have major phase releases, performance testing is a mandatory step to verify the existing and new functionalities from the perspective of performance and scalability.

 

Periodical Verification that System Can Handle Peaks

 

It is common that the load on a productive system is not constant over time, and there are certain events that can trigger business activities to cause meaningful fluctuations in the load. Such events can be caused by business behavior to handle certain tasks at a certain time of a day or month, legal requirements, or some special days such as Christmas or Black Friday that dictate consumer behavior that directly or indirectly challenges the scalability of business applications. The peak load from such a usage pattern can be significantly higher than the average load. 

 

How Can Axapta Masters Help You?

 

Axapta Masters has years of technical and business expertise working with Microsoft Dynamics solutions across many industries. We are leveraging that experience to help our customers to build an adapted approach based on their business needs. Our approach is unique and is consist of two major directions, that joined together are a guarantee for success: 

 

  • 24/7 production monitoring, and  
  • Performance Testing using a non-production environment. 

 

24/7 Production Monitoring

 

24/7 monitoring over a period of time is a crucial activity that helps us to capture valuable system metrics from a production environment, that are available only at that particular moment, without the possibility to retrieve them afterward.

 

Metrics such as AOS, SQL utilization, head-blockers, slow queries, batch job executions, integration points and exceptions are captured mainly in real-time, collected, and organized into a daily or weekly report. 

 

 

Performance Testing

 

Performance testing itself is a complex process that involves many steps to be successfully performed to get the desired outcome. 

Capture Requirements 

 

Discuss with business and IT users to understand exact requirements, day-to-day processes, and known issues, to design representative test cases with a focus on specific areas of interest. 

 

Test Environment

 

Usually a non-production environment.

 

Identify Test Scenarios

 

Work closely with the business and IT users to identify and agree on exact test scenarios taking into consideration multiple factors such as current system load, known issues/challenges, painful areas, and expected growth in the future. 

 

Plan and Design Tests

 

Create and agree on the schedule, test data, the specific configuration of the environment, and the team that will perform the testing.

 

Configure Test Environment

 

Our engineers will inspect the environment prior to testing, to assess its health and apply certain configuration changes required to support the testing and capture test results.

 

Test Execution

 

Execute test cases, monitor the executions, environment utilization metrics, and capture the statistics and trace log files. 

 

If you have more questions or you are interested in a performance review testing, contact us here and schedule a meeting!

 

And if you are interested in potential career opportunities, check out our careers page.

Error During Platform Update – “UpdateRetailSelfService.ps1 failed”

Starting the process of digital transformation for your company it’s not easy. In one of our previous blog posts, we shared how to install a deployable package like platform update/D365 version, and one of the errors we encountered in the Sync AX Database step.

 

As many of you know during the installation of the platform update you might encounter various errors. This time we would like to share a quick fix for the following error: “UpdateRetailSelfService.ps1 failed”

 

 

For this error try to start the Azure Storage Emulator on your D365 environment. As soon as you get the confirmation that the storage emulator is running you can click on the resume in LCS or rerun the failed step in your already opened cmd:

 

AXUpdateInstaller.exe execute -runbookid=”runbook1″ -rerunstep=52

 

 

Now the error is fixed.

 

No need to thanks us, we are glad to help you. So, if you are in need of someone to upgrade your systems from Microsoft Dynamics AX to Dynamics 365, we are open to future collaboration. Reach out to us here.

 

And if you are interested to be part of our team, check out our careers page we have open job vacancies all the time!

Sync AX Database Error – Installing Platform Update Deployable Package

If you are reading this blog post, you have encountered the same issues as us, so don’t worry you are not alone in this process.

 

For you who are for the first time here, you should check our previous blog post, learn how to install a deployable package like platform update/D365 version, and start the digital transformation of your company.

 


 

During the installation of the platform update package on your environment, you might get an error in the Sync AX Database step. Although there might be different errors when this step is executed the most common is the following (usually if you perform the installation on a local VM):

 

 

“The running command stopped because the preference variable “ErrorActionPreference” or common parameter is set to Stop: System.Management.Automation.RuntimeException: An exception of type System.Net.WebException occurred when making an http request to: http://127.0.0.1:80/ReportServer. Refer to the log file for more details.”

 

The error can easily be resolved following the steps below.

 

1. Open the SQL Reporting Services Configuration Manager (as an administrator).

 

 

2. Before clicking on the connect button check if the “Server Name” is the same as Computer Name.

 

 

3. The next step would be starting the SSRS server. If the server is already up and running please stop it and then again click on the start button.

 

 

4. As soon as you get a confirmation that the Report server is up and running you can continue with the installation of the deployable package using the command:

 

AXUpdateInstaller.exe execute -runbookid=”<name of the runbook>” -rerunstep=<step id where the update failed>

 

In our case this would be:

AXUpdateInstaller.exe execute -runbookid=”runbook1″ -rerunstep=25

 

Another error you can encounter in the process is “UpdateRetailSelfService.ps1 failed”, and here you can find our solution for it.

 

If you are in need of someone to upgrade your systems from Microsoft Dynamics AX to Dynamics 365, we are open to future collaboration. Reach out to us here.

 

And if you are interested in potential career opportunities, check out our careers page.

Installing a Deployable Package – Platform Update D365 Version

The digital transformation of your company will fundamentally change how you operate and deliver value to the end customer. No matter the industry you are in if you are using an older version of Dynamics, you should consider updating to Dynamics 365 version.

 

So, in this blog post, we are sharing the steps for installing a deployable package like platform update/D365version.

 

  1. Download the deployable package from LCS.
  2. Customize the necessary files for the update.
  3. Execute the commands using the axupdateinstaler.exe.

 

Before we jump into the process of installing a deployable package we would like to point out that you’ll need to:

 

1.1. Download the deployable package from LCS. When the file is downloaded check the general properties and make sure that they are like in the photo. (if there is a checkbox “unlock” make sure it’s marked)

 

 

1.2. Unzip the file in a new location. For example, C:\package. This should be done because of the length of the file path (just avoid using downloads or temp and MAKE SURE your folder doesn’t have space characters)

 

1.3. The package should look like this:

 

 

2.1. In that folder find the file: DefaultTopologyData.xml Right click and edit. Leave the notepad window open.

 

 

2.2  In the same folder open cmd as an administrator

 

 

2.3. Execute the command: AXUpdateInstaller.exe list

 

 

2.4. Copy all of the services you’ve got from the cmd command into the DefaultTopologyData.xml file ( <string>XXX</string> ) and save the file.

 

<ServiceModelList>

<string>AOSService</string>

<string>DevToolsService</string>

<string>ALMService</string>

<string>BIService</string>

<string>DIXFService</string>

<string>MROneBox</string>

<string>PayrollTaxModule</string>

<string>PerfSDK</string>

<string>ReportingService</string>

<string>RetailCloudPos</string>

<string>RetailHQConfiguration</string>

<string>RetailSDK</string>

<string>RetailSelfService</string>

<string>RetailServer</string>

<string>RetailStorefront</string>

<string>SCMSelfService</string>

<!– DefaultTopologyData are only for dev vhd, which we only support AOS update–>

<!– Create a new topology data for your Cloud AX topology–>

</ServiceModelList>

 

3.1. On the currently running cmd windows execute this command:

 

AXUpdateInstaller.exe generate -runbookid=”runbook1″ -topologyfile=”DefaultTopologyData.xml” -servicemodelfile=”DefaultServiceModelData.xml” -runbookfile=”runbook1.xml”

 

(note: the “runbook1” and “runbook1.xml” are just an example, type your text as you want)

 

This command would generate the runbook that will provide step-by-step instructions for updating the environment.

 

3.2. Next, execute the following command:

 

AXUpdateInstaller.exe import -runbookfile=”runbook1.xml”

 

3.3. And the last step is:

AXUpdateInstaller.exe execute -runbookid=”runbook1″

After successful deployment, the cmd will look like this (the number of steps may vary but you need to get the last step completed and get a clean command line)

 

 

After all the steps are done the D365 should have the desired platform update/ version.

 

*Note: If any of the steps fail you can use this command to rerun the deployment from the step that failed after troubleshooting it:

 

AXUpdateInstaller.exe execute -runbookid=”runbook1″ -rerunstep=25

 

If you are in need of someone to upgrade your systems from Microsoft Dynamics AX to Dynamics 365, we are open to future collaboration.

 

Reach out to us here, and if you are interested in potential career opportunities, check out our careers page.

AZCOPY for Easier and Faster Download of Files

Those of you who are familiar with the struggle of waiting hours for downloading a file from LCS (either for the Platform Update zip files or the Database backups from the Asset Library) can now benefit from the AzCopy app.

 

First things first and that means downloading the azcopy application which can be found here.

 

After downloading it, extract the zip file into the desired location (in my case is the “C:\” Drive).

 

 

 

 

Now, to use it we need to open cmd from the folder where you’ve extracted the zip file (or open cmd and navigate to that folder).

 

 

The command that we are going to use is:

 

azcopy copy “<Source Destination>” “<Desired/Target Destination>” –recursive=true

 

Based on the command above we can see that we have two parameters which be changed based on our activity.

 

As an example, I am trying to download the PlatformUpdate package to my local VM and for that first, I’ve logged in to the LCS and picked up the desirable package from the AssetLibrary but only to get the download link which I’ll use in the command.

 

 

Now equipped with what we need we can modify the command to suit us.

 

*NOTE: Please look at the end of the second parameter. You can notice that I’ve named my file as “pu10_16.zip”. If you don’t specify the name and especially the type of file your downloading, the azopy will download it as type “file” and you don’t want that cause it will be of no use for you.

 

azcopy copy “https://uswedpl1catalog.blob.core.windows.net/product-navproductname/6567e2af-004a-4f7b-91a1-d41fcca4e095/NavProductName-22-10-6ffd92c1-b99d-4d62-9a2c-4f3278adc44d-6567e2af-004a-4f7b-91a1-d41fcca4e095?sv=2015-12-11&sr=b&sig=yTbGuo9taddUO%2FtpUzrr65XZkTE58O5rA9PEhU9dVjc%3D&se=2021-01-29T09%3A37%3A05Z&sp=r” “C:\Platform_Update\pu10_16.zip” –recursive=true

 

 

You can see that my 5GB were downloaded in less than 20 minutes (way faster than using the regular download). Satisfied with the results I can now continue with deploying the platfrom_update package to my local D365 VM.

If you need to do the same there is another blog regarding the installation of platform update packages on the following link.

 

If you are interested in optimizing/improving your business processes, making the experience while using the application a whole lot easier in the long run, having the data secured in the cloud at the same time, and in need for someone to upgrade your systems from Microsoft Dynamics AX to Dynamics 365, we are open for future collaboration.

 

Reach out to us here, and if you are interested in potential career opportunities, check out our careers page.

Data Upgrade from Microsoft Dynamics AX2012 R2 / AX2012 R3 to Dynamics 365 for Finance and Operations

Upgrading Microsoft Dynamics AX2012 to Dynamics 365 Finance and Operations brings the digital transformation in most companies by giving them the ability to use significant new features and capabilities.

 

Recently, we have added another successfully completed data upgrade project for one of our clients in our portfolio. If you are looking for some help while upgrading from AX2012 R2 or Ax 2012 R3 to Dynamics 365 then you are at the right place.

 

This process contains several phases:

 

  • Preparing the database in the AX2012 environment
  • Installing a deployable package on the D365 development environment
  • Migrating upgraded database from D365 one-box to sandbox environment

 

 

 

AX2012 Procedure

 

The process starts in the AX 2012 environment where you have to prepare the ax2012 database for upgrading to Dynamics 365. Microsoft has put together a pre-upgrade checklist that has to be installed in Dynamics AX 2012. The checklist ensures you have updated information in your database for the AX 2012 to D365 upgrade.

 

 

After the installation of the hotfix, run the sysCheckList classes to prepare the database. After executing the process in the AX2012 environment we must make a backup from this database and transfer it to the D365 development environment. For transferring databases between environments, we will use Microsoft Azure Storage.

 

D365 Procedure

 

Most of the errors must be resolved in this process while installing the data upgrade deployable package.

 

The process on D365 development environment is following:

 

  1. Restore and rename the database.
  2. Install deployable package.
  3. Made another back up from this database and restore it to prepare it for Tier2 environment.
  4. Export a bacpac file for transferring

 

 

Restore the Database

 

After restoring the database on D365 environment we need to rename the names of the databases. Rename the original AxDB to AxDB_Orig and then rename the newly restored database to AxDB.

 

Install a Deployable Package

 

Download data upgrade deployable package from Shared asset library from Software deployable package, find the data upgrade package that corresponds to your upgrade.

 

If you’re upgrading from AX 2012, the package name starts with AX2012DataUpgrade-x-x-x where x-x-x is the application release. Select the package that corresponds to the release you are upgrading to. After downloading extract it and install it.

 

Make a Back-up to Prepare the Database for Tier 2

 

After the installation of the deployable package completes, we need to make back up from this database and restore it again in this environment. We will do this because we will delete some objects from it to prepare for the sandbox environment.

 

Do the following steps in SQL management studio against the newly imported database:

 

  • Update sysglobalconfiguration table

 

  • Drop procedure XU_DisableEnableNonClusteredIndexes and drop following users: axdbadminaxdeployuseraxmrruntimeuseraxretaildatasyncuseraxretailruntimeuser

 

  • remove NT users as these are not supported in Azure SQL Database

 

  • remove any AX 2012 RTM model store procedures that still exist

 

  • If you receive a message that you cannot delete users because they own a schema, then check which schema the user owns. Either change the ownership to another user (for example to do) or drop the schema if it does not contain any objects.

 

  • drop all views in the current database because some refresh the tempDB, which is not a supported action in Azure SQL Databases

 

  • Drop MaintainShipCarrierRole procedure because it contains a tempDB reference that is not supported in Azure SQL Database

 

  • Export a bacpac file for transferring 

 

After all this, the last step on the development environment is to export a bacpac file that should be imported in a sandbox environment. Go to the following location and execute the following command for exporting the file

 

A Process on the D365 Sandbox Environment

 

After transferring the database, we need to do the following steps:

 

  • Import the database;
  • Create users and prepare the database;
  • Rename the database;
  • Full database synchronization.

 

Import the Database

 

Import the database into the sandbox environment.

 

Create Users on the Newly Imported Database

 

Create users that were dropped in the preparation procedure. The passwords for the users must be copied from the LCS from the environment where we are importing the database. After users are created, alter the database with the required configuration.

 

Rename the Database

 

Full Database Synchronization  

 

Run full database synchronization and after it completes successfully you are ready to use the environment.

 

If you are interested in optimizing/improving your business processes, making the experience while using the application a whole lot easier in the long run, having the data secured in the cloud at the same time, and in need for someone to upgrade your systems from Microsoft Dynamics AX to Dynamics 365, we are open for future collaboration.

 

Reach out to us here, and if you are interested in potential career opportunities, check out our careers page.