Production order ended
Fires when a production order is ended and moves to the Ended status (ProdStatus::Completed), the final stage of the production lifecycle. Ending settles the order's costs and closes it for further transactions. Downstream systems can use this moment to reconcile work in progress, release reserved capacity, or close the manufacturing job in an MES or ERP integration. The payload carries the order's total good and error quantities reported as finished, with their unit.
| Event class | MGCLatticeProdOrderEndedBusinessEvent |
| Contract class | MGCLatticeProdOrderEndedBusinessEventContract |
| Category | Production control |
| Module | ModuleAxapta::ProductionControl |
| Label IDs | ProdOrderEndedBusinessEventName / ProdOrderEndedBusinessEventDescription |
| Trigger | ProdUpdHistoricalCost.run() -> ProdStatus transitions into Completed |
Trigger / Hook
- Extension class:
MGCLatticeProdUpdHistoricalCost_Class_Extension, decorated with[ExtensionOf(classStr(ProdUpdHistoricalCost))]. - CoC method signature:
void run()(no parameters). This is the End update for one production order, not theProdTable.update()table write. - Firing condition: before
next run(), the production order ID is read from the update's parameter record (this.parmParmBuffer().Id) and the order's current status is read from the database. Afternext run(), the order is re-read and the event is sent only when the status changed intoProdStatus::Completed(origStatus != ProdStatus::Completed && prodTable.ProdStatus == ProdStatus::Completed). - When the status changes: the standard update (
updateProduction()) sets the order to Ended. Running End again for an order that is already Ended doesn't raise the event. Runs that don't end the order don't raise it either; for example, in legal entities that use the Russian work-in-progress calculation, a calculation run without End job leaves the status unchanged. - Quantities: the contract reads the order's report-as-finished totals after the End update, so they include a report as finished that the End update ran itself for an order that hadn't been reported as finished. See the Payload Contract.
- Transaction:
ProdUpdHistoricalCost.run()performs the End update inside its ownttsbegin/ttscommit, and the event is sent after it returns.BusinessEventsBase.send()only writes to the commit log inside the current transaction and delivery is asynchronous post-commit, so an event raised inside a larger transaction that rolls back is never delivered. - Enablement gate: guarded by
BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeProdOrderEndedBusinessEvent)), so the event only sends when activated in the business events catalog.
void run()
{
// The activation check is a cached lookup and comes first, so an environment that has not activated the
// event pays no database read. next run() is always called.
boolean eventEnabled = BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeProdOrderEndedBusinessEvent));
ProdId prodId;
ProdStatus origStatus;
// Capture the pre-status from the database BEFORE next so the event fires only on a genuine transition.
// The base class initializes its prodTable buffer inside run(), so read the persisted status using the
// production order id from the parm buffer.
if (eventEnabled)
{
prodId = this.parmParmBuffer().Id;
origStatus = ProdTable::find(prodId).ProdStatus;
}
next run();
if (eventEnabled)
{
// Re-read the committed record after next for the freshest status.
ProdTable prodTable = ProdTable::find(prodId);
if (origStatus != ProdStatus::Completed
&& prodTable.ProdStatus == ProdStatus::Completed)
{
MGCLatticeProdOrderEndedBusinessEvent::newFromProdTable(prodTable).send();
}
}
}
Payload Contract
One row per [DataMember], in declaration order. All sources are the production order re-read after the End update, mapped in initialize().
| JSON field | Display label | Type / EDT | Source | Notes |
|---|---|---|---|---|
legalEntity | Legal Entity | DataAreaId | ProdTable.DataAreaId | Company of the production order. |
productionOrderId | Production Order ID | ProdId | ProdTable.ProdId | Production order number. |
itemId | Item Number | ItemId | ProdTable.ItemId | Item being produced. |
itemSearchName | Item Search Name | str | InventTable::find(ProdTable.ItemId).NameAlias | The item's search name, not its product name. |
productionStatus | Production Status | int | enum2int(ProdTable.ProdStatus) | ProdStatus as an integer; always 7 (Completed) for this event. |
productionStatusName | Production Status Name | str | enum2Symbol(enumNum(ProdStatus), enum2int(ProdTable.ProdStatus)) | ProdStatus symbol; always Completed (shown as Ended in the client) for this event. |
reportedFinishedGoodQuantity | Reported As Finished Good Quantity | InventQty | ProdTable.reportedFinishedGood() | The order's total good quantity reported as finished, over all its report-as-finished postings, in unitId. This is what the order actually produced, not its scheduled quantity. Covers the order's main item only; co-products, by-products and catch-weight quantities aren't included. |
reportedFinishedErrorQuantity | Reported As Finished Error Quantity | InventQty | ProdTable.reportedFinishedError() | The order's total error (scrap) quantity reported as finished, in unitId, counted the same way. |
unitId | Inventory Unit | UnitOfMeasureSymbol | InventTable.inventUnitId() of the produced item | Unit of both quantities: the produced item's inventory unit. |
crossReferences | Cross references | List of MGCLatticeCrossReferenceEventContract | MGCLatticeCrossReferenceEventHelper::listFor(ProdTable) | Keys external systems hold for the production order: one { externalSystem, externalKey } object per external system, ordered by external system, historical keys included; [] when it has none. See Cross-reference keys. |
Every Lattice event also carries the Lattice ordering and correlation envelope, inherited from MGCLatticeBusinessEventContractBase: eventSequence (Int64 - increasing across the whole catalog and all companies, allocated when the event is raised inside the business transaction rather than when it commits; it orders the events of one business record, gaps are permanent and normal, and it must not be used to re-sequence the whole stream), correlationId (String, always Scope:Key - groups emissions that belong to one logical run; Event:<GUID> unique to this emission when the event belongs to no run) and schemaVersion (Int - the payload shape version; 1 for this contract). D365 F&O also adds its standard envelope fields to every business event payload: BusinessEventId, BusinessEventLegalEntity, ContextRecordSubject, ControlNumber, EventId, EventTime, EventTimeIso8601, InitiatingUserAADObjectId, MajorVersion, MinorVersion, and ParentContextRecordSubjects. Do not rely on the order of members in the delivered JSON.
Example Payload
{
"BusinessEventId": "MGCLatticeProdOrderEndedBusinessEvent",
"ControlNumber": 5637151836,
"EventId": "210E8689-C113-4F3F-825D-C213C50A25E2",
"EventTime": "\/Date(1790266808000)\/",
"EventTimeIso8601": "2026-09-24T16:20:08.3510331Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{278DDDDA-823B-F628-D7E4-85F2AB5E2B7B}",
"ParentContextRecordSubjects": [],
"eventSequence": 48236,
"correlationId": "Event:ad59110f-5296-4164-82f7-4ee592ba3a71",
"schemaVersion": 1,
"legalEntity": "usmf",
"productionOrderId": "P000108",
"itemId": "D0001",
"itemSearchName": "MidRangeSpeaker",
"productionStatus": 7,
"productionStatusName": "Completed",
"reportedFinishedGoodQuantity": 19.00,
"reportedFinishedErrorQuantity": 1.00,
"unitId": "ea",
"crossReferences": [
{
"externalSystem": "MES",
"externalKey": "WO-104522"
}
]
}
Activation
In the D365FO client, go to System administration > Setup > Business events > Business events catalog. Find this event by name ("Production order ended") or by filtering the Category column to Production control. Select it and use Activate to bind it to a configured endpoint (for example, an Azure Service Bus queue, Event Grid topic, or HTTPS webhook).