Tag Archives: X++
How a Leading North American Commercial Vehicle Manufacturer Bridged the Legacy PO Cancellation Gap in Dynamics 365 Finance & Operations Using DMF and X++
01Summary A leading North American manufacturer of heavy-duty commercial vehicles moved three years of procurement history from a legacy ERP into Dynamics 365 Finance and Operations, and roughly 4,000 of those Purchase Orders were already cancelled in the source system. The Data Management Framework imported them as records without complaint, but DMF cannot run a PO cancellation, so those orders landed in D365 F&O as confirmed supply and the first test Master Planning run returned net requirements of zero for hundreds of items the assembly line actually needed. CloudFronts extended the DMF staging entity with a cancel flag and added a post-import X++ batch job that reads the flag and calls the same cancellation API behind the Cancel button on the Purchase Order form. All 4,000 cancelled POs now hold the correct state in D365 F&O, MRP plans against real supply, and a reconciliation log gives procurement a line by line count to check against legacy. Table of Contents 01Summary→ 02About the Customer→ 03Business Challenges→ 04Solution Overview→ 05Technical Approach→ 06Business Impact / Key Takeaways→ 07Conclusion / Final Thoughts→ 08Call to Action / Connect With Us→ 02About the Customer Our customer is one of North America’s leading manufacturers of heavy-duty commercial vehicles, operating an extensive network of manufacturing and logistics facilities. As part of its supply chain operations, the organization manages high-volume production, component manufacturing, and distribution processes that require seamless integration across enterprise systems. 03Business Challenges The customer builds heavy-duty commercial vehicles across a network of manufacturing and logistics facilities, running high-volume component production that feeds a fixed assembly schedule. Master Planning was a go-live dependency, not a phase two item. If MRP could not be trusted on the first run, the build schedule could not be trusted either. Uncancelled POs are phantom supply D365 F&O nets demand, meaning sales orders, production orders and forecasts, against supply, meaning on-hand inventory, Purchase Orders and planned orders. Every confirmed PO counts as expected incoming material. Import 4,000 cancelled legacy POs as confirmed orders and the planning engine sees 4,000 supply lines that no vendor will ever ship. The arithmetic is unforgiving. Net requirement is demand minus supply, so inflating supply with dead orders drives net requirement to zero or below. MRP then raises nothing. The warehouse stays empty and the line stops. ⚠One PO can suppress an entire item A single uncancelled PO for a high-value component, an engine assembly, a transmission housing or a steel frame, suppresses MRP planning for that item across every production order that needs it. At 4,000 phantom POs, the whole planning output stops being usable. Where the existing pipeline stopped Legacy system exports PO line recordsEvery Purchase Order, active or cancelled, leaves the legacy ERP as a flat file record. DMF imports the records into D365 F&OPO lines land in PurchTable and PurchLine through the standard Purchase Order Lines data entity. Backend automation approves and confirms POs starting with NThe automation picks up everything matching that number series prefix and pushes it through approval and confirmation. That pipeline handled active orders well. The cancelled ones broke it in four separate ways. No flag in the file. The legacy export mixed active and cancelled POs, and the DMF entity carried no field to mark which ones needed cancelling in D365 F&O. The automation confirmed the wrong orders. It ran on every PO matching the number series, so orders cancelled in legacy were approved and confirmed into live supply commitments. Manual cancellation cost weeks. The UI cancels one PO at a time. The team sized 4,000 POs at three to four weeks of clicking, and MRP could not run reliably until that cleanup finished. No reconciliation record. Even a manual pass would leave no structured log proving each cancellation completed, and no way to match the cancelled count between legacy and D365 F&O. Why the cancelled POs could not simply be dropped from scope MRP accuracy was the reason this work became non-negotiable, but three other requirements pointed the same way. Audit and compliance. Procurement auditors trace why a purchase was raised, who approved it and why it was reversed. Three years of history with the cancellations missing is a history with holes in it. Vendor performance analysis. Cancellation patterns are where lead time failures and specification mismatches show up. Migrate only active and completed POs and procurement loses the evidence it negotiates with. Financial reconciliation. Cancelled POs that carried prepayments, accruals or commitments have to exist in D365 F&O so finance can close out historical transactions without opening the legacy system again. 04Solution Overview Before the fix makes sense, one thing about D365 F&O has to be clear. Cancelling a Purchase Order is not a data update. It is a business operation, and the status field is the last thing it touches, not the first. What the Cancel button actually does Validation checks. The system tests eligibility. Has a product receipt posted against any line? Has an invoice been matched? Are registrations pending? Any of these blocks the cancellation or narrows it to specific lines. Workflow recall. If Change Management is enabled, the approval workflow is recalled so the PO leaves its approved state through the proper channel instead of a status override. Accounting distribution reversal. Encumbrances, pre-encumbrances and budget reservations created at confirmation are reversed or voided. Inventory marking cleanup. Marking references between PO lines and sales or production order lines are removed. MRP exclusion. The PO drops out of Master Planning calculations and stops counting as incoming supply. For this migration, that was the step that mattered. Status update. Only once all of the above completes does the PO status become Cancelled. The Purchase Order Lines DMF entity writes field values into PurchTable and PurchLine. It does not invoke any of those six steps. Push a status value of Cancelled through DMF and you get one of three outcomes, a validation failure, a silently ignored value, or a written status with none of the underlying logic behind it. That third outcome is the dangerous … Continue reading How a Leading North American Commercial Vehicle Manufacturer Bridged the Legacy PO Cancellation Gap in Dynamics 365 Finance & Operations Using DMF and X++
Sales return order line registration in D365FO and AX 2012
Introduction: Sales return order line registration in X++ Details: Consider SalesReturnOrderRegisterLine is the table where records are with SalesLine referenced. If we are required to register serialized items than salesReturnOrderRegisterLine.inventSerialId will be considered and if item referred with batch then salesReturnOrderRegisterLine.inventBatchId will be considered. public static void inventoryRegistration(SalesId _salesId) { SalesReturnOrderRegisterLine salesReturnOrderRegisterLine; SalesLine salesLine; TmpInventTransWMS tmpInventTransWMS; InventTransWMS_Register inventTransWMS_Register; InventDim inventDim; InventTrans inventTrans; VendPackingSlipTrans vendPackingSlipTransLoc; InventTransOrigin inventTransOrigin; while select salesReturnOrderRegisterLine where salesReturnOrderRegisterLine.SalesId == _salesId { salesLine = SalesReturnOrderRegister::updateDispositionCode(salesReturnOrderRegisterLine.InventTransId); inventTransWMS_Register = InventTransWMS_Register::newStandard(tmpInventTransWMS); select firstonly inventTrans where inventTrans.StatusReceipt == StatusReceipt::Ordered exists join inventTransOrigin where inventTransOrigin.RecId == inventTrans.InventTransOrigin && inventTransOrigin.InventTransId == salesLine.InventTransId && inventTransOrigin.ReferenceId == _salesId; tmpInventTransWMS.clear(); tmpInventTransWMS.initFromInventTrans(inventTrans); tmpInventTransWMS.InventQty = salesReturnOrderRegisterLine.SalesQty; tmpInventTransWMS.LineNum = int642int(salesLine.LineNum); inventDim = salesLine.inventDim(); inventDim.inventSerialId = salesReturnOrderRegisterLine.inventSerialId; inventDim.inventBatchId = salesReturnOrderRegisterLine.inventBatchId; tmpInventTransWMS.InventDimId = InventDim::findOrCreate(inventDim).inventDimId; tmpInventTransWMS.ItemId = salesLine.ItemId; inventTransWMS_Register.writeTmpInventTransWMS(tmpInventTransWMS, inventTrans, InventDim::find(tmpInventTransWMS.InventDimId)); if (!inventTransWMS_Register.updateInvent(salesLine)) { throw error(“Error during sales return order registration”); } } } Consider SalesReturnOrderRegister is the class which has below static methods public static SalesLine updateDispositionCode(InventTransId _inventTransId) { SalesLine salesLine = SalesLine::findInventTransId(_inventTransId, true); ReturnDispositionCode returnDispositionCode; SalesReturnOrderRegister::runPreReturnOrderRegisterLine(salesLine); salesLine.ReturnDispositionCodeId = returnDispositionCode.DispositionCodeId; salesLine.update(); return salesLine; } public static void runPreReturnOrderRegisterLine(SalesLine _salesLine) { InventTransOriginId salesLineInventTransOriginId; InventTransOriginId reservationLineInventTransOriginId; SalesLine reservationLine; ReturnDispositionCode returnDispositionCode; if (_salesLine.ReturnStatus == ReturnStatusLine::Awaiting) { if (!_salesLine.ReturnAllowReservation && _salesLine.isStocked()) { SalesLine::changeReturnOrderType(_salesLine.InventTransId); _salesLine = SalesLine::findInventTransId(_salesLine.InventTransId, true); } select firstOnly returnDispositionCode where returnDispositionCode.DispositionAction == DispositionAction::Credit; _salesLine.ReturnDispositionCodeId = returnDispositionCode.DispositionCodeId; _salesLine.update(); } else if (_salesLine.ReturnStatus == ReturnStatusLine::Registered) { select forupdate firstonly reservationLine where reservationLine.InventRefTransId == _salesLine.InventTransId; if (reservationLine || _salesLine.qtyMarked()) { if ((_salesLine.returnDispositionCode().DispositionAction == DispositionAction::ReplaceScrap || _salesLine.returnDispositionCode().DispositionAction == DispositionAction::ReturnToCust || _salesLine.returnDispositionCode().DispositionAction == DispositionAction::Scrap)) { if (reservationLine.SalesQty == reservationLine.RemainSalesPhysical) { reservationLineInventTransOriginId = InventTransOriginSalesLine::findInventTransOriginId(reservationLine.DataAreaId, reservationLine.InventTransId); salesLineInventTransOriginId = InventTransOriginSalesLine::findInventTransOriginId(_salesLine.DataAreaId, _salesLine.InventTransId); InventTransOrigin::deleteMarking(salesLineInventTransOriginId, reservationLineInventTransOriginId, -_salesLine.QtyOrdered); reservationLine.delete(); } } else { throw error(“@SYS332911”); } } } } Thanks for reading and stay connected with us for more updates!!! Jagdish Solanki | Senior Technical Consultant | CloudFronts Business Empowering Solutions Team “Solving Complex Business Challenges with Microsoft Dynamics 365 & Power Platform”
Create Item Requirement from Item Forecast using X++ in D365 Operations
Introduction: In this blog article, we will see how we can create Item Requirement on insertion of Item Forecast using code. Steps: Create Extension Class for ForecastSales Table that is Item Forecast and using CoC for post insert() method we will initialize Item Requirement. We will call a new class which is created in Step 2. public void insert() { next insert(); SalesType salesType = SalesType::ItemReq; CFSCreateItemReqFrmItemForecast createItemReq = new CFSCreateItemReqFrmItemForecast(); createItemReq.initParameters(this); createItemReq.copyToSalesLine(SalesType); } Create a new class that will initialize values and insert record in Item Requirement form. class CFSCreateItemReqFrmItemForecast { ForecastSales forecastSales; } In the new class create a method initParameter. This method will initialize ForecastSales object. void initParameters(ForecastSales _forecastSales) { forecastSales = _forecastSales; } Create another method ‘copytoSalesLine’. It will validate the record and call other methods to copy values to SalesLine Table. public void copyToSalesLine(SalesType _salesType) { ProjTable projTable = ProjTable::find(forecastSales.ProjId); if (_salesType == SalesType::ItemReq) { if (!ProjStatusType::construct(projTable).validateWriteItemRequirement()) { throw error(“@SYS18447”); } } else { if (!ProjStatusType::construct(projTable).validateWriteSalesLine()) { throw error(“@SYS18447”); } } SalesLine salesLine = this.initializeSalesLine(_salesType, forecastSales, projTable); salesLine.createLine(false, // Validation false, // Init from SalesTable true, // Init from InventTable true, // Calc invent Qty false, // Search markup – copied from salesQuotationline false, // Search price – copied from salesQuotationline false, // Check reservation true); // Skip creditlimit check this.updateSalesLine(salesLine, forecastSales); salesLine.update(); } Create a new method ‘initializeSalesLine’. It is called from copyToSalesLine(). protected SalesLine initializeSalesLine(SalesType _salesType, ForecastSales _forecastSales, ProjTable _projTable) { SalesLine salesLine; salesLine.SalesType = _salesType; salesLine.initValue(); salesLine.setInventDimId(_forecastSales.InventDimId); salesLine.ItemId = _forecastSales.ItemId; salesLine.SalesQty = _forecastSales.SalesQty; salesLine.SalesUnit = _forecastSales.SalesUnitId; salesLine.ProjId = _forecastSales.ProjId; salesLine.ActivityNumber = _forecastSales.ActivityNumber; salesLine.CurrencyCode = _forecastSales.Currency; salesLine.initFromProjTable(_projTable, false); return salesLine; } Create a new method updateSalesLine(). It is called from copyToSalesLine() method. protected void updateSalesLine(SalesLine _salesLine, ForecastSales _forecastSales) { _salesLine.DefaultDimension = _salesLine.copyDimension(_forecastSales.DefaultDimension); _salesLine.ProjLinePropertyId = _forecastSales.ProjLinePropertyId; _salesLine.TaxGroup = _forecastSales.TaxGroupId; _salesLine.TaxItemGroup = _forecastSales.TaxItemGroupId; _salesLine.ProjCategoryId = _forecastSales.ProjCategoryId; _salesLine.CostPrice = _forecastSales.CostPrice; _salesLine.SalesPrice = _forecastSales.SalesPrice; _salesLine.LinePercent = _forecastSales.DiscPercent; _salesLine.LineDisc = _forecastSales.DiscAmount; _salesLine.LineAmount = 0; _salesLine.LineAmount = _salesLine.calcLineAmount(); SalesLineType_ItemReq::setSalesLineReceiptDate(_salesLine); }
Post Ledger Journal using X++ in D365 Operations
Introduction: In this blog article, we will see how we can post the journal by using code. How to do? Create a new method and write below code. In this code you declare object of Class ‘LedgerJournalCheckPost’. This class will use journal buffer and post it. public void postJournal(LedgerJournalTable ledgerJournalTable) { LedgerJournalCheckPost jourPost; jourPost = LedgerJournalCheckPost::newLedgerJournalTable(ledgerJournalTable, NoYes::Yes); jourPost.runOperation(); }
Create Fixed Asset Journal using X++
In this blog article, we will see how we can create Fixed Asset Journal using X++. Write below code to create Journal Header in LedgerJournalTable Table and Lines record in LedgerJournalTrans and LedgerJournalTrans_Asset Tables. public void createFixedAssetJournal() { LedgerJournalTable ledgerJournalTable; LedgerJournalTrans ledgerJournalTrans; LedgerJournalTrans_Asset ledgerJournalTrans_Asset; Assettable assetTable; ledgerJournalTable.initValue(); ledgerJournalTable.JournalNum = JournalTableData::newTable(ledgerJournalTable).nextJournalId(); ledgerJournalTable.Posted = NoYes::No; ledgerJournalTable.JournalName = ‘ACQUI’; ledgerJournalTable.JournalType = LedgerJournalType::Assets; ledgerJournalTable.initFromLedgerJournalName(ledgerJournalTable.JournalName); ledgerJournalTable.insert(); ledgerjournalTrans.initValue(); ledgerJournalTrans.CurrencyCode = Ledger::accountingCurrency(CompanyInfo::find().RecId); ledgerJournalTrans.AccountType = LedgerJournalACType::FixedAssets; ledgerJournalTrans.TransactionType = LedgerTransType::FixedAssets; ledgerJournalTrans.Approved = NoYes::Yes; ledgerJournalTrans.Approver = HcmWorker::userId2Worker(curuserid()); ledgerJournalTrans.LineNum = LedgerJournalTrans::lastLineNum(ledgerJournalTrans.JournalNum) + 1; ledgerJournalTrans.TransDate = DateTimeUtil::getSystemDate(DateTimeUtil::getUserPreferredTimeZone()); ledgerJournalTrans.LedgerDimension = LedgerDynamicAccountHelper::getDynamicAccountFromAccountNumber(AssetTable.AssetId,LedgerJournalACType::FixedAssets); ledgerJournalTrans.accountName(); ledgerJournalTrans.AmountCurDebit = ’45’; ledgerJournalTrans.OffsetAccountType = LedgerJournalACType::Ledger; ledgerJournalTrans.OffsetLedgerDimension = ledgerJournalTrans.getOffsetLedgerDimensionForLedgerType(AssetLedgerAccounts::assetOffsetLedgerDimension(AssetTable.AssetId, AssetTable.assetBookCurrent().BookId, AssetTransType::Acquisition),curExt()); ledgerJournalTrans.insert(); ledgerJournalTrans_Asset.initValue(); ledgerJournalTrans_Asset.RefRecId = ledgerJournalTrans.RecId; ledgerJournalTrans_Asset.AssetId = assetTable.assetId; ledgerJournalTrans_Asset.TransType = AssetTransTypeJournal::Acquisition; ledgerJournalTrans_Asset.BookId = AssetTable.assetBookCurrent().BookId; ledgerJournalTrans_asset.insert(); ttsbegin; LedgerJournalTable.selectForUpdate(true); LedgerJournalTable.numOfLines = LedgerJournalTable.numOfLines(); LedgerJournalTable.update(); ttscommit; }
Create Fixed Asset using X++
In this blog article, we will see how we can create a Fixed Asset using code based on AssetGroupID. Add below code in class to create a Fixed Asset, public void createFixedAsset(AssetGroupId assetGroupId) { AssetTable assetTable; NumberSeq numberSeq; assetTable.initValue(); assetTable.assetGroup = AssetGroupId; //Initialize Number Seq for Asset Id numberSeq = assetTable.initAssetNumberSeq(assetTable.Assetgroup); AssetTable.AssetId = NumberSeq.num(); //Initialize other fields based on Asset Group assetTable.initFromAssetGroupId(assetTable.Assetgroup); //Initialize name and Name Alias fron Item Id assetTable.Name = InventTable::find(‘ITM1104’).itemName(); assetTable.NameAlias = assetTable.Name; //Validate and Insert Fixed Asset. On insertion asset book is also created assetTable.validateWrite(); assetTable.insert(); }
Fetch Hierarchical data for Product Category in Dynamics 365 Operations
For today’s modern day business that needs customer satisfaction, scalability and digital intelligence, dynamics 365 finance and operations is a complete ERP solution and is one of the most trusted software in the world without any doubt. This ERP solution helps you to innovate your products and processes so that client’s expectations can be met on time and your business can survive well in the cut-throat competition. It also gives visibility to your business across customer sales and service, marketing system and connected distribution. It simplifies production floor management, speeds up product introduction and offers flexibility in delivery alternatives. When it comes to the impact on your finance, you can gain immediate financial insights, drive corporate strategy and growth and through efficient collection management, decrease debts considerably. Introduction: In this blog article, we will see how we can fetch hierarchical data using X++. How to fetch? We will take a scenario where we will pass a category hierarchy and will fetch all categories of that hierarchy and its child category. public class ProductCategoryHierarchy { EcoResCategory category; public void ParentCategory() { while select category where category.CategoryHierarchy == “Brands” { //code this.getChildrenCategory(category.RecId); } } /// <summary> /// get categories of child product /// </summary> public void getChildrenCategory(EcoResCategoryId ParentCategory) { while select category where category.ParentCategory == ParentCategory { //code this.getChildrenCategory(category.RecId); } } }
