Production order reported as finished
Fires when a production order moves to the Reported as finished status (ProdStatus::ReportedFinished), whether a report-as-finished update or a report-as-finished journal posted on its own moves it there. The manufactured output has been physically completed and received into inventory, although the order hasn't been ended (financially closed) yet. Downstream systems can use this moment to release finished goods, notify warehouse or quality teams, or start packaging and shipping workflows. The payload carries the order's total good and error quantities reported as finished, with their unit.
| Event class | MGCLatticeProdOrderReportedFinishedBusinessEvent |
| Contract class | MGCLatticeProdOrderReportedFinishedBusinessEventContract |
| Category | Production control |
| Module | ModuleAxapta::ProductionControl |
| Label IDs | ProdReportFinishedBusinessEventName / ProdReportFinishedBusinessEventDescription |
| Trigger | ProdUpdReportFinished.run() and ProdJournalCheckPostProd.run() -> ProdStatus transitions into ReportedFinished |
Trigger / Hook
- Extension classes:
MGCLatticeProdUpdReportFinished_Class_Extension, decorated with[ExtensionOf(classStr(ProdUpdReportFinished))], andMGCLatticeProdJournalCheckPostProd_Class_Extension, decorated with[ExtensionOf(classStr(ProdJournalCheckPostProd))]. Both send throughMGCLatticeProdOrderReportedFinishedBusinessEvent::sendIfReportedFinished(). - CoC method signatures:
void run()(no parameters) on both. The first wraps the report-as-finished update for one production order; the second wraps the posting of a report-as-finished journal. Neither hooks theProdTable.update()table write. - Where the status changes: a discrete production order moves to Reported as finished when a report-as-finished journal line that ends the job (End job) is posted (
ProdJournalCheckPostProd.postTransLedger()). A process (batch) order moves there when such a journal is posted and no co-product or by-product remains to be reported (postVoucher()), or, during a report-as-finished update, right after that posting inProdUpdReportFinished.updateProdReceipt(). - Firing condition, journal posting: before
next run(), the production orders on the journal's lines and their current statuses are read, because posting can delete the lines. Afternext run(), each order is re-read and the event is sent when its status changed intoProdStatus::ReportedFinished. - Firing condition, report-as-finished update: before
next run(), the production order ID is read from the update's parameter record (this.parmParmBuffer().Id) and the order's current status from the database.next run()runs inside aMGCLatticeProdReportFinishedEventScopefor the order. After it returns, the order is re-read and the event is sent when its status changed intoProdStatus::ReportedFinished. - One event per order: while a report-as-finished update runs for an order, the journals it posts (its report-as-finished journal, and one that its route card posting generates) don't send the event for that order; the update sends it once, after all of them. If an update for an order were to run inside another update for the same order, only the outer one sends the event.
- Coverage: raised whichever process moves the order to Reported as finished. That includes the Report as finished action on the production order, report as finished from manufacturing execution registrations, the report-as-finished step that the production status framework runs when an order that hasn't been reported as finished is ended, report as finished from the warehouse mobile app (
ProdUpdReportFinished::reportFinished()), report-as-finished journals posted from the production journal page, report-as-finished journals that route card and job card postings generate and post, and journals posted on the warehouse execution hub (ProdJournalPostProdWESHub). - When it isn't raised: a partial report that doesn't end the job leaves the status unchanged and doesn't raise the event, and further reports for an order that is already Reported as finished don't raise it again. If the order's status is reset to Started and it is reported as finished again, the event is raised again. A status written without posting (for example, by custom code or an import that sets
ProdStatusdirectly) doesn't raise it. With deferred report-as-finished posting, the event is raised when the deferred posting runs, not when the worker reports. - Order relative to other events: when a route card posting generates and posts the report-as-finished journal, this event is sent before Production route card posted, and after Production pick list posted for a picking list posted by the same route card.
- Transaction: the event is sent after the update or the journal posting returns. A journal posted as part of a larger process (for example, a route card posting) is posted inside that process's transaction.
BusinessEventsBase.send()only writes to the commit log inside the current transaction and delivery is asynchronous post-commit, so an event raised inside a transaction that rolls back is never delivered. - Enablement gate: guarded by
BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeProdOrderReportedFinishedBusinessEvent)), so the event only sends when activated in the business events catalog.
// MGCLatticeProdUpdReportFinished_Class_Extension
void run()
{
// ParmBuffer.Id maps to ProdParmReportFinished.ProdId. Read it and the order's status before next: a
// deferred (manufacturing execution) posting deletes the parameter record during the run.
// The activation check is a cached lookup and comes first; an environment that has not activated the event
// pays no database read. The nesting scope is still entered so a nested update for the same order is
// recognized by the journal posting extension either way.
boolean eventEnabled = BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeProdOrderReportedFinishedBusinessEvent));
ProdId prodId = this.parmParmBuffer().Id;
ProdStatus statusBefore;
boolean ownsScope;
if (eventEnabled)
{
statusBefore = ProdTable::find(prodId).ProdStatus;
}
using (MGCLatticeProdReportFinishedEventScope scope = MGCLatticeProdReportFinishedEventScope::newForProdId(prodId))
{
// False only when this update runs nested inside another update for the same order; the outer one
// then raises the event.
ownsScope = scope.ownsProdId();
next run();
}
if (eventEnabled && ownsScope)
{
MGCLatticeProdOrderReportedFinishedBusinessEvent::sendIfReportedFinished(prodId, statusBefore);
}
}
// MGCLatticeProdJournalCheckPostProd_Class_Extension
void run()
{
JournalId journalId = this.parmJournalId();
boolean eventEnabled = BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeProdOrderReportedFinishedBusinessEvent));
Map statusBefore = new Map(Types::String, Types::Enum);
ProdJournalProd prodJournalProd;
if (eventEnabled && journalId)
{
while select ProdId from prodJournalProd
group by ProdId
where prodJournalProd.JournalId == journalId
{
statusBefore.insert(prodJournalProd.ProdId, ProdTable::find(prodJournalProd.ProdId).ProdStatus);
}
}
next run();
MapEnumerator enumerator = statusBefore.getEnumerator();
while (enumerator.moveNext())
{
ProdId prodId = enumerator.currentKey();
ProdStatus orderStatus = enumerator.currentValue();
if (!MGCLatticeProdReportFinishedEventScope::isActiveFor(prodId))
{
MGCLatticeProdOrderReportedFinishedBusinessEvent::sendIfReportedFinished(prodId, orderStatus);
}
}
}
// MGCLatticeProdOrderReportedFinishedBusinessEvent
public static void sendIfReportedFinished(ProdId _prodId, ProdStatus _statusBefore)
{
if (!_prodId || _statusBefore == ProdStatus::ReportedFinished)
{
return;
}
ProdTable prodTableAfter = ProdTable::find(_prodId);
if (prodTableAfter.RecId
&& MGCLatticeProdOrderReportedFinishedBusinessEvent::isReportedFinishedTransition(_statusBefore, prodTableAfter.ProdStatus)
&& BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeProdOrderReportedFinishedBusinessEvent)))
{
MGCLatticeProdOrderReportedFinishedBusinessEvent::newFromProdTable(prodTableAfter).send();
}
}
Payload Contract
One row per [DataMember], in declaration order. All sources are the production order re-read after the update or posting, 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 5 (ReportedFinished) for this event. |
productionStatusName | Production Status Name | str | enum2Symbol(enumNum(ProdStatus), enum2int(ProdTable.ProdStatus)) | ProdStatus symbol; always ReportedFinished 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 including the one that finished it, in unitId. 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": "MGCLatticeProdOrderReportedFinishedBusinessEvent",
"ControlNumber": 5637151790,
"EventId": "583A0154-D09D-4C50-ABD3-E83FFD454A89",
"EventTime": "\/Date(1790178117000)\/",
"EventTimeIso8601": "2026-09-23T15:41:57.9847951Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{3E5B7C1E-8F5A-9584-EF0E-6B5D8A15C0A2}",
"ParentContextRecordSubjects": [],
"eventSequence": 48190,
"correlationId": "Event:e0e94d44-0d7c-4ab6-8efe-121f4e134ea2",
"schemaVersion": 1,
"legalEntity": "usmf",
"productionOrderId": "P000108",
"itemId": "D0001",
"itemSearchName": "MidRangeSpeaker",
"productionStatus": 5,
"productionStatusName": "ReportedFinished",
"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 reported as finished") 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).