Skip to main content

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 classMGCLatticeProdOrderReportedFinishedBusinessEvent
Contract classMGCLatticeProdOrderReportedFinishedBusinessEventContract
CategoryProduction control
ModuleModuleAxapta::ProductionControl
Label IDsProdReportFinishedBusinessEventName / ProdReportFinishedBusinessEventDescription
TriggerProdUpdReportFinished.run() and ProdJournalCheckPostProd.run() -> ProdStatus transitions into ReportedFinished

Trigger / Hook​

  • Extension classes: MGCLatticeProdUpdReportFinished_Class_Extension, decorated with [ExtensionOf(classStr(ProdUpdReportFinished))], and MGCLatticeProdJournalCheckPostProd_Class_Extension, decorated with [ExtensionOf(classStr(ProdJournalCheckPostProd))]. Both send through MGCLatticeProdOrderReportedFinishedBusinessEvent::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 the ProdTable.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 in ProdUpdReportFinished.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. After next run(), each order is re-read and the event is sent when its status changed into ProdStatus::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 a MGCLatticeProdReportFinishedEventScope for the order. After it returns, the order is re-read and the event is sent when its status changed into ProdStatus::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 ProdStatus directly) 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 fieldDisplay labelType / EDTSourceNotes
legalEntityLegal EntityDataAreaIdProdTable.DataAreaIdCompany of the production order.
productionOrderIdProduction Order IDProdIdProdTable.ProdIdProduction order number.
itemIdItem NumberItemIdProdTable.ItemIdItem being produced.
itemSearchNameItem Search NamestrInventTable::find(ProdTable.ItemId).NameAliasThe item's search name, not its product name.
productionStatusProduction Statusintenum2int(ProdTable.ProdStatus)ProdStatus as an integer; always 5 (ReportedFinished) for this event.
productionStatusNameProduction Status Namestrenum2Symbol(enumNum(ProdStatus), enum2int(ProdTable.ProdStatus))ProdStatus symbol; always ReportedFinished for this event.
reportedFinishedGoodQuantityReported As Finished Good QuantityInventQtyProdTable.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.
reportedFinishedErrorQuantityReported As Finished Error QuantityInventQtyProdTable.reportedFinishedError()The order's total error (scrap) quantity reported as finished, in unitId, counted the same way.
unitIdInventory UnitUnitOfMeasureSymbolInventTable.inventUnitId() of the produced itemUnit of both quantities: the produced item's inventory unit.
crossReferencesCross referencesList of MGCLatticeCrossReferenceEventContractMGCLatticeCrossReferenceEventHelper::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).