Thursday, 28 May 2026

How to Get Trade Agreement Line Discount Percentage by Item and Date in D365FO Using X++

 


In Microsoft Dynamics 365 Finance and Operations (D365FO), trade agreements are used to manage prices and discounts for customers, vendors, and items. Frequently, developers need to retrieve the applicable line discount percentage for a specific item, customer, and date without creating a sales order.

The recommended approach is to use the PriceDisc framework, which is the standard pricing and discount engine used throughout D365FO. Microsoft identifies the PriceDisc class as the search engine for prices and discounts, while PriceDiscParameters acts as the container for all pricing criteria.

This article explains a practical X++ implementation that retrieves the line discount percentage from trade agreements.

Complete Method

server static DiscPct getLineDiscountPct( ItemId _itemId, CustAccount _custAccount, TransDate _priceDate = systemDateGet())

{

    InventTable             inventTable;

    InventDim               inventDim;

    PriceDisc               priceDisc;

    PriceDiscParameters     priceDiscParameters = PriceDiscParameters::construct();

    DiscPct                 lineDiscPct;

    boolean                 discExist;

    inventTable = InventTable::find(_itemId);

    if (!inventTable)

        return 0;

    // Fix blank date

    if (_priceDate == dateNull())

    { _priceDate = today();

    }

    // Build parameters

    priceDiscParameters.parmModuleType(ModuleInventPurchSales::Sales);

    priceDiscParameters.parmItemId(inventTable.ItemId);

    priceDiscParameters.parmInventDim(InventDim::findOrCreate(inventDim));

    priceDiscParameters.parmUnitID(inventTable.inventTableModuleSales().UnitId);

    priceDiscParameters.parmPriceDiscDate(_priceDate);

    priceDiscParameters.parmQty(1);

    priceDiscParameters.parmAccountNum(_custAccount);

    priceDiscParameters.parmCurrencyCode(

        Ledger::reportingCurrency(CompanyInfo::current()));

    priceDisc = PriceDisc::newFromPriceDiscParameters(priceDiscParameters);

    discExist = priceDisc.findLineDisc('', '');

    if (discExist)

    {        lineDiscPct = priceDisc.lineDiscPct();

    }

    else    {

        lineDiscPct = 0;

    }

    return lineDiscPct;

}


Understanding the Logic

The method determines the line discount percentage that would be applied to an item for a customer on a specific date based on existing trade agreements.

Input Parameters

ItemId      _itemId
CustAccount _custAccount
TransDate _priceDate
ParameterPurpose
_itemIdItem to evaluate
_custAccountCustomer account
_priceDateDate used for trade agreement validity

Example:

DiscPct discount =
MyPricingHelper::getLineDiscountPct(
"A0001",
"US-001",
today());

The method returns the matching trade agreement discount percentage.


Step 1: Retrieve Item Information

inventTable = InventTable::find(_itemId);

The first step retrieves the item record from InventTable.

if (!inventTable)
return 0;

If the item does not exist, there is no reason to continue searching for discounts.


Step 2: Handle Empty Date Values

if (_priceDate == dateNull())
{
_priceDate = today();
}

Trade agreements are date-sensitive.

Every trade agreement contains:

  • From Date
  • To Date

If the caller passes a blank date, the method defaults to today's date.

This ensures the pricing engine evaluates currently active agreements.


Step 3: Create PriceDiscParameters

Microsoft recommends using PriceDiscParameters to pass all pricing and discount criteria into the pricing engine. The PriceDisc class then performs the actual trade agreement search.

PriceDiscParameters priceDiscParameters =
PriceDiscParameters::construct();

This object acts as the search request.


Step 4: Specify Sales Module

priceDiscParameters.parmModuleType(
ModuleInventPurchSales::Sales);

This tells the framework:

Search Sales trade agreements.

Possible values include:

ModuleInventPurchSales::Sales
ModuleInventPurchSales::Purch

Because we are evaluating customer discounts, Sales is appropriate.


Step 5: Set Item

priceDiscParameters.parmItemId(
inventTable.ItemId);

The pricing engine needs to know which item is being evaluated.

The engine will check trade agreement records that match:

  • Specific item
  • Item group
  • All items

depending on the setup.


Step 6: Set Inventory Dimensions

priceDiscParameters.parmInventDim(
InventDim::findOrCreate(inventDim));

This supplies inventory dimensions.

Examples include:

  • Site
  • Warehouse
  • Color
  • Size
  • Style

In your code:

InventDim inventDim;

is blank.

Therefore:

InventDim::findOrCreate(inventDim)

creates an "empty" dimension combination.

This means discounts are searched without dimension-specific restrictions.

If your trade agreements depend on site or warehouse, you should populate these values.

Example:

inventDim.InventSiteId = "1";
inventDim.InventLocationId = "11";

Step 7: Set Sales Unit

priceDiscParameters.parmUnitID(
inventTable.inventTableModuleSales().UnitId);

Trade agreements can be defined per unit.

Example:

UnitDiscount
EA10%
BOX15%

The pricing engine therefore requires the sales unit.


Step 8: Set Pricing Date

priceDiscParameters.parmPriceDiscDate(
_priceDate);

This is one of the most important parameters.

The engine uses it to validate:

From Date <= Price Date <= To Date

Only active agreements are considered.


Step 9: Set Quantity

priceDiscParameters.parmQty(1);

Trade agreements often contain quantity breaks.

Example:

QtyDiscount
1+5%
10+10%
100+20%

By supplying:

parmQty(1)

you are asking:

What discount applies when purchasing one unit?

If quantity-based discounts are important, pass the actual quantity.


Step 10: Set Customer Account

priceDiscParameters.parmAccountNum(
_custAccount);

The pricing engine evaluates customer-specific agreements.

Possible trade agreement relations include:

  • Specific Customer
  • Customer Group
  • All Customers

This parameter enables the framework to resolve the appropriate discount hierarchy.


Step 11: Set Currency

priceDiscParameters.parmCurrencyCode(
Ledger::reportingCurrency(
CompanyInfo::current()));

Currency can affect price and discount searches.

The framework expects a currency code during evaluation.


Step 12: Create the PriceDisc Engine

priceDisc =
PriceDisc::newFromPriceDiscParameters(
priceDiscParameters);

At this point all search criteria have been prepared.

The PriceDisc class now becomes the engine responsible for finding applicable prices and discounts. Microsoft describes PriceDisc as the core search engine used for price and discount determination.


Step 13: Search for Line Discount

discExist = priceDisc.findLineDisc('', '');

This is the most important statement in the method.

The framework searches trade agreements for a matching line discount.

Internally it evaluates:

  • Customer
  • Customer group
  • Item
  • Item group
  • Date validity
  • Quantity
  • Unit
  • Currency

and returns:

true

if a matching discount is found.


Step 14: Retrieve Discount Percentage

if (discExist)
{
lineDiscPct = priceDisc.lineDiscPct();
}

After a successful search:

priceDisc.lineDiscPct()

returns the actual discount percentage.

Example:

Trade Agreement = 15%

Result:

15.00

Step 15: Handle No Match

else
{
lineDiscPct = 0;
}

If no agreement exists:

0

is returned.

This prevents null or unexpected values.


Example Scenario

Assume the following trade agreement exists:

CustomerItemFrom DateTo DateDiscount
US-001A000101-Jan-202531-Dec-202612%

Call:

DiscPct pct =
getLineDiscountPct(
"A0001",
"US-001",
today());

Result:

12.00

Potential Improvements

1. Pass Actual Quantity

Instead of:

parmQty(1);

consider:

parmQty(_qty);

This ensures quantity-based trade agreements are correctly evaluated.


2. Pass Actual Currency

Instead of reporting currency:

Ledger::reportingCurrency(...)

consider using the customer's sales currency:

CustTable::find(_custAccount).Currency

This better reflects real sales order pricing behavior.


3. Pass Actual Inventory Dimensions

If discounts depend on:

  • Site
  • Warehouse
  • Configuration
  • Size
  • Color

populate the InventDim record before creating the pricing request.


Why Use PriceDisc Instead of Querying Trade Agreement Tables Directly?

Many developers try to read PriceDiscTable or PriceDiscAdmTrans directly.

This is not recommended because the framework already handles:

  • Customer hierarchy
  • Customer groups
  • Item groups
  • Quantity breaks
  • Date validity
  • Unit conversions
  • Currency handling
  • Search priorities

Using PriceDisc ensures your logic matches standard D365FO sales order pricing behavior. Microsoft specifically positions PriceDisc as the framework responsible for price and discount determination throughout the application.

Conclusion

The method shown above is a clean and framework-compliant way to retrieve a trade agreement line discount percentage in D365FO. By constructing a PriceDiscParameters object and passing it into the PriceDisc engine, you leverage the same pricing logic used by sales orders and other standard processes.

The key flow is:

  1. Validate item and date.
  2. Build PriceDiscParameters.
  3. Populate customer, item, quantity, unit, currency, and date.
  4. Instantiate PriceDisc.
  5. Call findLineDisc().
  6. Retrieve the result using lineDiscPct().

This approach guarantees that your custom code remains aligned with the standard D365FO pricing framework and future platform enhancements.

Wednesday, 6 May 2026

How to Delete an SSRS Report from SQL Server Reporting Services (SSRS) in D365FO

 In Microsoft Dynamics 365 Finance & Operations, SSRS reports are deployed to the SSRS server and stored in the ReportServer database. While reports are typically managed through Visual Studio and deployment pipelines, there are situations where you may need to manually remove a report directly from the SSRS database.

This article walks through a safe and structured way to delete an SSRS report from SQL Server Reporting Services.

⚠️ Important Considerations

Before proceeding:

  • Always take a backup of the ReportServer database.
  • Avoid deleting records directly in production unless absolutely necessary.
  • Ensure the report is not required by any active processes.
  • Prefer standard deployment cleanup where possible.

Step 1: Connect to the ReportServer Database


  1. Open SQL Server Management Studio (SSMS).
  2. Connect to your SQL Server instance.
  3. Locate the ReportServer database (commonly named ReportServer or DynamicsAXReportServer).
  4. Right-click the database → New Query.

Step 2: Identify the Report in the Catalog Table

All SSRS reports are stored in the Catalog table.

Use the following query to locate your report:

SELECT * FROM Catalog WHERE Name LIKE '%<ReportName>%'

Example

SELECT *
FROM Catalog
WHERE Name LIKE '%SalesInvoice%'

This query returns:

  • Report name
  • ItemID (important for dependencies)
  • Path and metadata

Step 3: Check for Dependencies in DataSource Table

Before deleting the report, check whether it has related entries in dependent tables such as DataSource.

SELECT *

FROM DataSource AS t

WHERE t.ItemID IN ('<ItemID1>', '<ItemID2>')

The ItemID values come from the results of the Catalog query.

Step 4: Delete Dependent Records

If records exist in the DataSource table, delete them first to avoid orphaned references.

DELETE t

FROM DataSource AS t

WHERE t.ItemID IN ('<ItemID1>', '<ItemID2>')

You can also delete a single dependency:

DELETE t

FROM DataSource AS t

WHERE t.ItemID = '<ItemID>'

Step 5: Delete Report from Catalog

Once dependencies are removed, delete the report from the Catalog table.

DELETE FROM Catalog

WHERE Name IN ('ReportName_Version1', 'ReportName_Version2')

Example

DELETE FROM Catalog

WHERE Name = 'SalesInvoice'

Step 6: Validate Cleanup

Run the following again to confirm deletion:

SELECT *

FROM Catalog

WHERE Name LIKE '%SalesInvoice%'

Also re-check dependencies:

SELECT *

FROM DataSource

WHERE ItemID = '<ItemID>'

Best Practices

  • Use naming conventions to easily identify reports.
  • Avoid direct DB deletion during active deployments.
  • Use SSRS Web Portal or deployment tools when possible.
  • Document any manual changes for audit purposes.

When Should You Use This Approach?

This method is useful when:

  • A report is stuck or partially deployed
  • Duplicate reports exist
  • Deployment cleanup failed
  • You need to remove legacy artifacts quickly 

Final Thoughts

Directly modifying the SSRS ReportServer database should always be a last resort. However, when used carefully, this approach gives you precise control over report cleanup and helps resolve deployment inconsistencies in D365FO environments.

If you find yourself doing this often, it may be worth reviewing your deployment pipeline or report lifecycle management process.

Friday, 16 January 2026

Generate CAR report

 Generate CAR Report:

1. CMD as Admin

2. CD K:\AosService\PackagesLocalDirectory\bin

3. Run below command

K:\AosService\PackagesLocalDirectory\bin\xppbp.exe -packagesroot="K:\AosService\PackagesLocalDirectory" -metadata="K:\AosService\PackagesLocalDirectory" -all -model="MW" -xmlLog=C:\temp\BPCheckLogcd.xml -module="MW" -car=c:\temp\CAReport.xlsx

Sunday, 16 November 2025

Automating Credit Card Transaction File Transformation Using Azure Logic Apps and D365 Finance & Operations

 

Modern ERP environments increasingly depend on reliable, automated integration processes. After recent platform updates, certain internal transformation capabilities within Dynamics 365 Finance & Operations (D365FO) are restricted—especially those related to transforming flat‑file formats (TXT/CSV) using XSLT inside the application. 

This article presents a generic, scalable approach using Azure Logic Apps to transform incoming transaction files (TXT → XML) before they are imported into D365FO.

## 🌐 1. Overview

Many organizations receive credit card transaction files or similar flat‑file datasets from banking partners or external systems. After system updates, internal transformation using XSLT inside D365FO may no longer be supported, prompting the need for external transformation tools.

 **Solution:** 

Use **Azure Logic Apps + Integration Account** to handle external TXT‑to‑XML transformation, store processed XML files in SharePoint (or Azure Storage), and finally import them into D365FO using Data Management (DMF).

## 🔄 2. High-Level Architecture

 Below is the generic architecture flow:

```

[External System / Bank]

          |

        (TXT)

          |

      [SharePoint / Storage]

          |

   Trigger → Logic App

          |

   XSLT Mapping (Integration Account)

          |

       (XML Output)

          |

 [Processed Folder in SharePoint]

          |

      DMF Import to D365FO

## 🛠️ 3. End‑to‑End Process Flow

Logic App Implementation

3.1 Prerequisites / Access you need

  1. Azure subscription with permission to create resources (Resource Group, Logic App, Integration Account, Storage Account).
  2. Resource Group where you will deploy resources.
  3. Storage Account with at least one container:
    • incoming (where CSV is uploaded)
    • output (where .xslt is saved)
  4. Integration Account (Enterprise Integration) (required for Flat File decoding and for Maps/XSLT). You must be owner or contributor to create and link it.
  5. Permissions / credentials:
    • Contributor / Logic App Operator on Logic App resource.
    • Storage Blob Data Contributor or access keys (to let Logic App create/read blobs). You can use:
      • Managed Identity for the Logic App + RBAC on the Storage account (recommended), or
      • Storage account connection using storage account key / SAS.

 

The Logic App includes the following steps:

A. Create resources/ use existing

  1. Sign into Azure Portal.
  2. Create a Resource Group (if not present).
  3. Create an Integration Account:
    • Search "Integration Account" in the portal -> Create.
    • SKU: Standard (or depending on your needs).
    • Link it later to your Logic App (Standard or Consumption) in the Logic App settings.
  4. Upload map (XSLT) to Integration Account:
    • In Integration Account -> Maps -> Add -> Upload your .xslt file (choose type Map).
    • Use XSLT generated form D365FO DMF (Import project)àDownload.

B. Create the Logic App (Standard recommended)

  1. Create a Logic App (Standard):
    • Resource type: Logic App (Standard)
    • Plan: either create a new App Service environment or use default single-tenant plan.
    • On creation, in Workflow settings you can link the Integration Account that you created.
    • After creation, open the Logic App in Designer.

C. Build the workflow (Designer) — exact actions & settings

In the Logic App designer, create a Workflow and add these actions

  1. When a file is created (properties only) — Trigger on new file creation in SharePoint.

1.  {

2.    "type": "ApiConnection",

3.    "inputs": {

4.      "host": {

5.        "connection": {

6.          "referenceName": "sharepointonline-1"

7.        }

8.      },

9.      "method": "get",

10.     "path": "/datasets/@{encodeURIComponent(encodeURIComponent('https://yoursite.sharepoint.com/sites/d365/dev'))}/tables/@{encodeURIComponent(encodeURIComponent('cfcxxx696-e5a4-4c28-a2a7-60d7ff06fa1'))}/onnewfileitems",

11.     "queries": {

12.       "folderPath": "/AMEX Credit Card Transactions"

13.     }

14.   },

15.   "recurrence": {

16.     "frequency": "Minute",

17.     "interval": 2

18.   },

19.   "splitOn": "@triggerBody()?['value']"

20. }

 

  1. Initialize variable — To hold file content temporarily.
    • Name: Filecontent
    • Type: string
    • Code:

1.  {

2.    "type": "InitializeVariable",

3.    "inputs": {

4.      "variables": [

5.        {

6.          "name": "filecontent",

7.          "type": "string"

8.        }

9.      ]

10.   },

11.   "runAfter": {}

12. }

 

  1. Get file content — Retrieve the uploaded TXT file content.

1.  {

2.    "type": "ApiConnection",

3.    "inputs": {

4.      "host": {

5.        "connection": {

6.          "referenceName": "sharepointonline-1"

7.        }

8.      },

9.      "method": "get",

10.     "path": "/datasets/@{encodeURIComponent(encodeURIComponent('https://yoursite.sharepoint.com/sites/d365/dev'))}/files/@{encodeURIComponent(triggerBody()?['{Identifier}'])}/content",

11.     "queries": {

12.       "inferContentType": true

13.     }

14.   },

15.   "runAfter": {

16.     "Initialize_variable": [

17.       "Succeeded"

18.     ]

19.   }

20. }

 

  1. Set variable — Assign file content to variable.
    • Name: filecontent
    • Value: <Batch><![CDATA[
      ]]></Batch>
    • Code:

1.  {

2.    "type": "SetVariable",

3.    "inputs": {

4.      "name": "filecontent",

5.      "value": "<Batch><![CDATA[@{body('Get_file_content')}\n]]></Batch>"

6.    },

7.    "runAfter": {

8.      "Get_file_content": [

9.        "Succeeded"

10.     ]

11.   }

12. }

 

  1. Transform XML (1) — Apply the first XSLT transformation (TXT → XML).
    • Content: filecontent
    • Map Name: (select the Map(XSLT) which uploaded in step 4 of A.
    • Code:

1.  {

2.    "type": "Xslt",

3.    "inputs": {

4.      "content": "@variables('filecontent')",

5.      "integrationAccount": {

6.        "map": {

7.          "name": "ECLAMEXTXT2AX"

8.        }

9.      }

10.   },

11.   "runAfter": {

12.     "Set_variable": [

13.       "Succeeded"

14.     ]

15.   }

16. }

      1.  
  1. Transform XML (2) — Apply the second XSLT transformation (XML → FinOps XML).
    • Content: filecontent
    • Map Name: (select the Map(XSLT) which uploaded in step 4 of A.
    • Code:

1.  {

2.    "type": "Xslt",

3.    "inputs": {

4.      "content": "@variables('filecontent')",

5.      "integrationAccount": {

6.        "map": {

7.          "name": "ECLAMEXTXT2AX"

8.        }

9.      },

10.     "transformOptions": "GenerateTextOutput"

11.   },

12.   "runAfter": {

13.     "Transform_XML_1": [

14.       "Succeeded"

15.     ]

16.   }

17. }

 

  1. Create file — Store the transformed XML in the processed folder on SharePoint.

1.  {

2.    "type": "ApiConnection",

3.    "inputs": {

4.      "host": {

5.        "connection": {

6.          "referenceName": "sharepointonline-1"

7.        }

8.      },

9.      "method": "post",

10.     "body": "@body('Transform_XML')",

11.     "path": "/datasets/@{encodeURIComponent(encodeURIComponent('https://yoursite.sharepoint.com/sites/d365/dev'))}/files",

12.     "queries": {

13.       "folderPath": "/Operations/AMEX Credit Card Transactions/Processed",

14.       "name": "@{triggerBody()?['{Name}']}.xml",

15.       "queryParametersSingleEncoded": true

16.     }

17.   },

18.   "runAfter": {

19.     "Transform_XML": [

20.       "Succeeded"

21.     ]

22.   },

23.   "runtimeConfiguration": {

24.     "contentTransfer": {

25.       "transferMode": "Chunked"

26.     }

27.   }

28. }

 

A screenshot of a computer

AI-generated content may be incorrect.


How to Get Trade Agreement Line Discount Percentage by Item and Date in D365FO Using X++

  In Microsoft Dynamics 365 Finance and Operations (D365FO), trade agreements are used to manage prices and discounts for customers, vendor...