Error "Duplicate transaction id found." while inserting/pushing records in D365 F&O

Error "Duplicate transaction id found." while inserting/pushing records in D365 F&O

While working in Dynamics 365 Finance & Operations, you might encounter the error:

"Duplicate transaction id found."

This typically occurs when you're inserting records in a loop with ttsbegin/ttscommit inside the loop, like in a data push or migration routine. The issue is triggered by reusing the same table buffer object across multiple tts transactions, which causes the system to treat the inserts as part of a conflicting transaction scope.

Cause:

  • Reusing the same table buffer (ForecastSales, CustInvoiceJour, etc.) across multiple iterations with internal commits.
  • Each insert expects a fresh transaction scope and unique transaction ID.

Solution:

  • Move ttsbegin/ttscommit outside the loop.
  • Declare a new buffer inside the loop to ensure clean state for each iteration.

Before (Problematic code):

class ABC_ForecastSalesPushService extends SysOperationServiceBase
{

public void pushDemandForecasts()
{

ABC_ForecastSalesExtended forecastExt;
ForecastSales              forecastSales;
InventDim                  inventDim;
int                        insertedCount = 0;

while select forUpdate forecastExt
where forecastExt.CreatedBy == curUserId()
&& forecastExt.IsTransferred == NoYes::No
{

forecastSales.ModelId           = forecastExt.ModelId;
forecastSales.StartDate         = forecastExt.StartDate;
forecastSales.ProjId            = forecastExt.ProjId;
forecastSales.CustAccountId     = forecastExt.CustAccountId;
forecastSales.CustGroupId       = forecastExt.CustGroupId;
forecastSales.ItemGroupId       = forecastExt.ItemGroupId;
forecastSales.ItemId            = forecastExt.ItemId;

inventDim.configId              = forecastExt.configId;
inventDim.InventColorId         = forecastExt.InventColorId;
inventDim.InventSizeId          = forecastExt.InventSizeId;
inventDim.InventStyleId         = forecastExt.InventStyleId;
inventDim.InventSiteId          = forecastExt.InventSiteId;
inventDim.InventLocationId      = forecastExt.InventLocationId;
inventDim.InventBatchId         = forecastExt.InventBatchId;
inventDim.InventSerialId        = forecastExt.InventSerialId;
inventDim.InventStatusId        = forecastExt.InventStatusId;
inventDim.InventVersionId       = forecastExt.InventVersionId;

forecastSales.InventDimId       = InventDim::findOrCreate(inventDim).InventDimId;
forecastSales.ItemAllocateId    = forecastExt.ItemAllocateId;
forecastSales.SalesQty          = forecastExt.SalesQty;
forecastSales.SalesUnitId       = forecastExt.SalesUnitId;
forecastSales.Amount            = forecastExt.Amount;
forecastSales.SalesPrice        = forecastExt.SalesPrice;
forecastSales.Currency          = forecastExt.SalesCurrency;
forecastSales.InventQty         = forecastExt.InventQty;

ttsbegin;
forecastSales.insert();
insertedCount++; // Increment counter

forecastExt.IsTransferred = NoYes::Yes;
forecastExt.update();
ttscommit;
}

info(strFmt("Total records pushed: %1", insertedCount));
}
}        

ttsbegin; while select ... { forecastSales.insert(); // Reusing the same buffer ttscommit; }

Output:

Article content

✅ After (Fixed code):

class ABC_ForecastSalesPushService extends SysOperationServiceBase
{

public void pushDemandForecasts()
{

ABC_ForecastSalesExtended forecastExt;
int insertedCount = 0;

ttsbegin;
while select forUpdate forecastExt
where forecastExt.CreatedBy == curUserId()
&& forecastExt.IsTransferred == NoYes::No
{

ForecastSales forecastSales; // <== Re-declare inside the loop
InventDim inventDim;

forecastSales.ModelId           = forecastExt.ModelId;
forecastSales.StartDate         = forecastExt.StartDate;
forecastSales.ProjId            = forecastExt.ProjId;
forecastSales.CustAccountId     = forecastExt.CustAccountId;
forecastSales.CustGroupId       = forecastExt.CustGroupId;
forecastSales.ItemGroupId       = forecastExt.ItemGroupId;
forecastSales.ItemId            = forecastExt.ItemId;

inventDim.configId              = forecastExt.configId;
inventDim.InventColorId         = forecastExt.InventColorId;
inventDim.InventSizeId          = forecastExt.InventSizeId;
inventDim.InventStyleId         = forecastExt.InventStyleId;
inventDim.InventSiteId          = forecastExt.InventSiteId;
inventDim.InventLocationId      = forecastExt.InventLocationId;
inventDim.InventBatchId         = forecastExt.InventBatchId;
inventDim.InventSerialId        = forecastExt.InventSerialId;
inventDim.InventStatusId        = forecastExt.InventStatusId;
inventDim.InventVersionId       = forecastExt.InventVersionId;

forecastSales.InventDimId       = InventDim::findOrCreate(inventDim).InventDimId;
forecastSales.ItemAllocateId    = forecastExt.ItemAllocateId;
forecastSales.SalesQty          = forecastExt.SalesQty;
forecastSales.SalesUnitId       = forecastExt.SalesUnitId;
forecastSales.Amount            = forecastExt.Amount;
forecastSales.SalesPrice        = forecastExt.SalesPrice;
forecastSales.Currency          = forecastExt.SalesCurrency;
forecastSales.InventQty         = forecastExt.InventQty;

forecastSales.insert();
insertedCount++;

forecastExt.IsTransferred = NoYes::Yes;
forecastExt.update();
}
ttscommit;

info(strFmt("Total records pushed: %1", insertedCount));

}
}        

ttsbegin; while select ... { ForecastSales forecastSales; // Declare fresh buffer each loop forecastSales.insert(); } ttscommit;

Output:

Article content

Tip:

Avoid calling construct() on table buffers—it’s for classes. Instead, simply re-declare the buffer within the loop.

This simple fix can save hours of debugging, especially in integration or bulk data push scenarios.

#D365FO #Xpp #Dynamics365 #ERP #Troubleshooting #DataMigration

Enjoy the code!

Babar Salman.


Ulrike Duregger

D365 F&SCM Consultant & Developer | Finance & SCM | X++ | Multilingual

2mo

Thanks for sharing!

Thomas LOËB

Architect/Developer | D365 F&O, AX, D365 CE, Power Platform, Azure, C#, .NET

2mo

I'd also add that ttsbegin and ttscommit could be left inside the loop depending on the volume of data being inserted or updated (and having the "IsTransferred" filtering in your case) or else you could also make use of RecordInsertList when the scenario allows it ;) https://learn.microsoft.com/en-us/dotnet/api/dynamics.ax.application.recordinsertlist?view=dyn-finops-dotnet

To view or add a comment, sign in

Others also viewed

Explore topics