Production route card posted
Fires when a production route card journal is posted, recording the operation time and the good and error quantities reported for a production order's operations. The payload identifies the production order and its item, its status after posting, and lists each posted line with its operation, resource, hours and quantities. Downstream systems can use it for shop-floor progress tracking, labor and machine utilization, and scrap monitoring.
| Event class | MGCLatticeProdRouteCardPostedBusinessEvent |
| Contract class | MGCLatticeProdRouteCardPostedBusinessEventContract |
| Category | Production control |
| Module | ModuleAxapta::ProductionControl |
| Label IDs | ProdRouteCardPostedBusinessEventName / ProdRouteCardPostedBusinessEventDescription |
| Trigger | ProdJournalCheckPostRoute.run() -> the run posts the journal and JournalType == ProdJournalType::RouteCard (lines captured before posting) |
Trigger / Hook
- Extension class:
MGCLatticeProdJournalCheckPostRoute_Class_Extension, decorated with[ExtensionOf(classStr(ProdJournalCheckPostRoute))]. - CoC method signature:
void run()(no parameters; the wrapper mirrors the default visibility of the standardProdJournalCheckPostRoute.run()). This is the posting process for production route journals, not theProdJournalTable.update()table write. - Firing condition:
- Before
next run(), the journal's lines are read in line number order (MGCLatticeProdRouteCardPostedBusinessEvent::captureJournalLines()), because posting deletes them when the journal name is set to delete lines after posting. The journal's posted flag is read too. - After
next run(), the journal header is re-read withProdJournalTable::find(this.parmJournalId())and the production order withProdTable::find(prodJournalTable.ProdId). - The event is sent only when the production order is found (
prodTable.RecId), the journal type isProdJournalType::RouteCard, and the journal changed from not posted to posted during this run (Postedwas No before and is Yes after). Job card journals are excluded by the journal-type check. A journal that fails to post isn't marked as posted, so nothing is sent for it. The order is found through the journal header, so without a header nothing is sent. - Lines with errors that posting moved to a new journal instead of posting them (
transferJournalId()) are left out oflines.
- Before
- What
run()includes: the standardProdJournalCheckPostRoute.run()posts the route card and then posts any picking list and report-as-finished journals that the route card generated. The event is sent after all of these, so a picking list posted automatically by the route card raises Production pick list posted first, a report-as-finished journal that moves the order to Reported as finished raises Production order reported as finished first, andproductionStatusreflects the order after those postings. - Coverage: raised once per route card journal posted through
ProdJournalCheckPostRoute, whichever process posts it. That includes posting from the production journal page and route cards that other production processes post automatically, such as the start and report-as-finished updates and manufacturing execution registrations that are posted as route cards (registrations posted as job cards don't raise it). Reversing route cards are posted the same way (for example, when the status of a started order is reset), so the event is also raised for them, with negative line quantities. - Transaction:
JournalCheckPost.run()posts the route card inside its ownttsbegin/ttscommit, and the event is sent afterrun()returns. When the route card is posted as part of a larger update, that update's transaction is still open.BusinessEventsBase.send()only writes to the commit log inside the current transaction and delivery is asynchronous post-commit, so if that transaction rolls back, the event is never delivered. - Enablement gate: guarded by
BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeProdRouteCardPostedBusinessEvent)), so the event only sends when activated in the business events catalog. The lines are only read when the event is enabled.
void run()
{
JournalId journalId = this.parmJournalId();
boolean eventEnabled = BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeProdRouteCardPostedBusinessEvent));
NoYes postedBefore;
List capturedLines;
ProdJournalTable prodJournalTable;
ProdTable prodTable;
if (eventEnabled)
{
postedBefore = ProdJournalTable::find(journalId).Posted;
capturedLines = MGCLatticeProdRouteCardPostedBusinessEvent::captureJournalLines(journalId);
}
next run();
if (eventEnabled && postedBefore == NoYes::No)
{
prodJournalTable = ProdJournalTable::find(journalId);
prodTable = ProdTable::find(prodJournalTable.ProdId);
if (prodTable.RecId
&& prodJournalTable.Posted == NoYes::Yes
&& prodJournalTable.JournalType == ProdJournalType::RouteCard)
{
MGCLatticeProdRouteCardPostedBusinessEvent::newFromJournal(
prodTable,
journalId,
MGCLatticeProdRouteCardPostedBusinessEvent::postedLines(capturedLines, this.transferJournalId())).send();
}
}
}
Payload Contract
One row per [DataMember], in declaration order. The order fields come from the re-read ProdTable record; the lines come from the journal lines captured before posting. All are mapped in initialize(). Hours and quantities are reported per line, each with its operation. There is no journal total: the same units pass through every operation, so adding good quantities across operations would count them more than once.
| 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 the journal belongs to (the journal header's ProdId). |
journalId | Journal ID | ProdJournalId | ProdJournalTable.JournalId | The posted route card journal. |
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, read after posting (for example, 4 for StartedUp or 5 for ReportedFinished). |
productionStatusName | Production Status Name | str | enum2Symbol(enumNum(ProdStatus), enum2int(ProdTable.ProdStatus)) | ProdStatus symbol, read after posting. |
unitId | Inventory Unit | UnitOfMeasureSymbol | InventTable.inventUnitId() of the produced item | Unit of the good and error quantities on the lines: the produced item's inventory unit. |
lines | Lines | List of MGCLatticeProdRouteCardPostedLineContract | ProdJournalRoute lines of the journal | One entry per posted line, in line number order. See the table below. |
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. The keys are the production order's, not the journal's. |
Each entry in lines:
| JSON field | Display label | Type / EDT | Source | Notes |
|---|---|---|---|---|
lineNumber | Line Number | LineNum | ProdJournalRoute.LineNum | Journal line number. |
operationNumber | Operation Number | OprNum | ProdJournalRoute.OprNum | Route operation the line reports on. An operation appears once per line, so it can appear more than once (for example, for several resources or workers). |
operationId | Operation ID | RouteOprId | ProdJournalRoute.OprId | The operation. |
resourceId | Resource | WrkCtrIdBase | ProdJournalRoute.WrkCtrId | Resource the line was reported on. |
goodQuantity | Good Quantity | ProdReportedGood | ProdJournalRoute.QtyGood | Good quantity reported for the operation on this line, in unitId; 0 on a line that reports only time. Negative for a reversing route card. |
errorQuantity | Error Quantity | ProdReportedError | ProdJournalRoute.QtyError | Error (scrap) quantity reported for the operation on this line, in unitId. |
hours | Hours | ProdReportedHours | ProdJournalRoute.Hours | Hours reported on this line. |
isOperationFinished | Is Operation Finished | boolean | ProdJournalRoute.OprFinished == NoYes::Yes | true when the line reports the operation as finished. |
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": "MGCLatticeProdRouteCardPostedBusinessEvent",
"ControlNumber": 5637151722,
"EventId": "A584C41C-EC67-4C42-BDC9-AAC0A6B80B2E",
"EventTime": "\/Date(1790075059000)\/",
"EventTimeIso8601": "2026-09-22T11:04:19.7846509Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{534DE51B-8EDB-164A-6D8C-4344995E4E40}",
"ParentContextRecordSubjects": [],
"eventSequence": 48122,
"correlationId": "Event:01863c8b-b93e-4f52-80e1-4ec3cb7c06c3",
"schemaVersion": 1,
"legalEntity": "usmf",
"productionOrderId": "P000108",
"journalId": "01390",
"itemId": "D0001",
"itemSearchName": "MidRangeSpeaker",
"productionStatus": 4,
"productionStatusName": "StartedUp",
"unitId": "ea",
"lines": [
{
"lineNumber": 1.00,
"operationNumber": 10,
"operationId": "Assembly",
"resourceId": "1211",
"goodQuantity": 20.00,
"errorQuantity": 0.00,
"hours": 2.50,
"isOperationFinished": true
},
{
"lineNumber": 2.00,
"operationNumber": 20,
"operationId": "Testing",
"resourceId": "1225",
"goodQuantity": 19.00,
"errorQuantity": 1.00,
"hours": 1.25,
"isOperationFinished": false
}
],
"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 route card posted") 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).