The ERP confirmation contract¶
Generated from the contract models at build time, so this page and the software cannot drift apart. The same models are published as a JSON Schema — erp-confirmation.schema.json (https://docs.factorysemantics.com/reference/erp-confirmation.schema.json) — which your own tooling can validate against with no connection to this MES.
What this MES sends an ERP when work is done: one confirmation per completed operation, one completion per finished order. SAP-shaped on purpose — a confirmation per operation carrying quantities, cost centre, times and component consumption is what CO11N wants and what ERPNext's Job Cards and Stock Entries are built from — but shaped for SAP is not the same as tested against SAP, and no SAP has consumed one of these. Times are UTC without an offset marker. There is no money anywhere in this document: the MES reports quantities and time against a cost centre, and the ERP owns what they are worth.
Every field below says three things: what it means on the floor, where the MES gets it, and when it is null. Null is never a placeholder for zero. See the confirmation handoff for the folder these documents are written into and for fsmes erp validate.
Worked examples¶
Generated from a run of the demo plant, not written by hand.
| Example | JSON | B2MML | What it shows |
|---|---|---|---|
| An operation with scrap and consumed lots | operation-with-scrap-and-components.json |
operation-with-scrap-and-components.xml |
The fullest of the confirmations: 120 units into the mixer, 118 good, 2 scrapped, two raw lots consumed against the step, and the cost centre inherited from the line. This is the document an ERP posts labour, machine time and a back-flush against. |
| A clean operation | operation-clean.json |
operation-clean.xml |
The common case, and the one to read first: everything that reached the step came out of it good, nothing was scrapped, nothing was consumed. components is empty because nothing was booked - which is not the same as nothing having been used. |
| An order completion with a finished-goods lot | completion-with-a-lot.json |
completion-with-a-lot.xml |
The close of that order: 118 good against 120 ordered, 2 scrapped, and the lot the goods receipt is posted against. over_qty is 0 and is sent anyway. |
| An operation that ran past the order | operation-over-run.json |
operation-over-run.xml |
One machine count carried the line to 16 good against an order for 15, so wip_qty is -1: more came out of the step than went into it. It is reported, not clamped. Every unit the machine counted is booked, because the machine made them. |
| An order completion carrying an over-run | completion-with-an-over-run.json |
completion-with-an-over-run.xml |
The same order's close. 16 good against 15 ordered, and over_qty is 1 rather than leaving the ERP to notice that 16 of 15 is not a typo. |
| An order completion with no lot | completion-without-a-lot.json |
completion-without-a-lot.xml |
An order where everything scrapped. good_qty is 0, lot is null, and null is the honest value: there is no lot, and an ERP posting a receipt against one the MES never made is worse than a missing field. |
OperationConfirmation¶
One step of one order, confirmed the moment the step is done: quantities, the machine and its cost centre, the time the step was open, and every lot consumed at it. This is the document an ERP posts labour, machine time and consumption against.
| Field | Type | On the floor | Where the MES gets it | When it is null |
|---|---|---|---|---|
kind (optional) |
always operation_confirmation |
Which of the two documents this is; the first thing a reader should branch on. | Constant, written by the MES. | Never. |
message_key |
string | The idempotency key. A second file carrying a key already seen is the same fact arriving twice, not a second confirmation, and must be posted once. | Built from the order and the step: <order>:op<seq> for an operation, <order>:completion for the close. |
Never. |
order |
string | The order number the floor ran, as it appears on the dispatch list and the operator's screen. | The work order, which took it from the schedule the ERP sent. | Never. |
erp_reference (optional) |
string, or null | The ERP's own key for this order, carried back untouched so the ERP can match the confirmation without parsing the order code. | The erp_reference on the order, set when the ERP's schedule was imported. |
When the order was raised in the MES rather than sent by an ERP. There is no ERP key to carry, and inventing one would match the confirmation to the wrong document. |
material |
string | What the order makes: the finished-goods code the line is set up to produce. | The order's material. | Never. |
seq |
integer | The step number in the routing — 10, 20, 30 — the same numbering the operator sees on the screen. | The routing as it was when the order was created. Later routing edits never rewrite an order that has already run. | Never. |
operation |
string | What the step is called: Mix, Pack, Deburr. | The same routing snapshot as seq. |
Never. |
equipment (optional) |
string, or null | The machine that did the work — an ISA-95 work unit. | The machine the operation is assigned to. | When the step has no machine against it, such as a hand operation. Null, not the line's code: a bench is not a machine. |
work_center (optional) |
string, or null | The line the machine belongs to. | Walked up the equipment tree from the machine to the first work centre above it, however many cells sit in between. | When the machine hangs outside any line. That is a real state of the equipment tree, not a gap to be filled in. |
cost_center (optional) |
string, or null | The cost centre this work is accounted to — the account the ERP posts the hours and the consumption against. | The machine's own cost centre, or the first one found above it: cell, then line. Plants set it once per line and override only where accounting genuinely differs. | When nothing from the machine upwards carries one. Null is the honest answer: a cost centre guessed here posts a plant's labour to the wrong account, and nobody finds it for a month. |
input_qty |
number | How many units this step had to work with. | The order quantity at the first step; the previous step's good quantity at every step after it. | Never. |
good_qty |
number | Units this step made that passed. | What was booked against the step: machine counter deltas from OPC, or a person typing a count. | Never. Zero means the step made none; it never means nobody was watching. |
scrap_qty |
number | Units this step made that did not pass. | The same bookings as good_qty. |
Never. |
wip_qty |
number | What went into the step and has not come out of it either way: input_qty - good_qty - scrap_qty. A negative number is reported, not clamped — it means more came out of the step than went in, which really happens when one coalesced counter delta carries the line past the order, and hiding it behind a zero would be inventing production. |
Arithmetic on the three quantities above, and nothing else. | Never. |
setup_seconds (optional) |
number, or null | Planned set-up time for this step. Planned, not measured: the MES does not watch a fitter change a die. | The routing snapshot. | When the routing carries no set-up time. Null, not zero — 'no set-up planned' and 'nobody ever recorded one' are different facts, and only one of them means the ERP should post no set-up. |
machine_seconds (optional) |
number, or null | How long the step was actually open on the machine, in seconds. | Measured: completed_at - started_at. |
When the step has no start or no finish to subtract. Null rather than zero, because a step of unknown length is not a step that took no time. |
labour_seconds (optional) |
number, or null | Planned labour for what this step handled. Planned, not clocked — nobody badges on and off an operation in this MES. | The routing's labour seconds per unit multiplied by the units handled (good_qty + scrap_qty). |
When the routing carries no labour time per unit. |
started_at (optional) |
timestamp, or null | When the step started, UTC, with no offset marker — the MES's single timestamp convention. | The moment the operation was started, by an operator or by the first machine count arriving. | When the step was never started. |
completed_at (optional) |
timestamp, or null | When the step finished, on the same clock as started_at. |
The moment the operation was completed. | When the step is not finished. |
components (optional) |
list of ComponentUse | Every lot consumed at this step, one line each. | The consumption booked against the operation, by scanner or by hand. | Never null; an empty list means nothing was booked against the step. That is not the same as nothing having been used, and an ERP that back-flushes a bill of materials should treat an empty list as 'the MES has no record', not as 'no material was consumed'. |
OrderCompletion¶
The order-level close, sent once when the last step of the order is done: what was ordered, what came out good, what was scrapped, how far the line ran past the order, and the finished-goods lot if there is one.
| Field | Type | On the floor | Where the MES gets it | When it is null |
|---|---|---|---|---|
kind (optional) |
always order_completion |
Which of the two documents this is; the first thing a reader should branch on. | Constant, written by the MES. | Never. |
message_key |
string | The idempotency key. A second file carrying a key already seen is the same fact arriving twice, not a second confirmation, and must be posted once. | Built from the order and the step: <order>:op<seq> for an operation, <order>:completion for the close. |
Never. |
order |
string | The order number the floor ran, as it appears on the dispatch list and the operator's screen. | The work order, which took it from the schedule the ERP sent. | Never. |
erp_reference (optional) |
string, or null | The ERP's own key for this order, carried back untouched so the ERP can match the confirmation without parsing the order code. | The erp_reference on the order, set when the ERP's schedule was imported. |
When the order was raised in the MES rather than sent by an ERP. There is no ERP key to carry, and inventing one would match the confirmation to the wrong document. |
material |
string | What the order makes: the finished-goods code the line is set up to produce. | The order's material. | Never. |
ordered_qty |
number | What the ERP asked for. | The order's quantity, as imported from the schedule. | Never. |
good_qty |
number | What the order actually yielded — the good quantity of its last step, not the sum of every step. | The last operation's good quantity. | Never. |
scrap_qty |
number | Everything scrapped anywhere on the order, summed across the steps. | The sum of the operations' scrap. | Never. |
over_qty (optional) |
number | How far past the ordered quantity the line actually ran. Zero on an order that stopped where it was asked to — and sent anyway, because a reader who never sees the field cannot tell an order that did not over-run from one whose over-run was not reported. | max(good_qty - ordered_qty, 0). Derivable from the two numbers above, and stated because an ERP that posts a good quantity without noticing it exceeds the order is how a stock discrepancy nobody can explain starts. |
Never. |
lot (optional) |
string, or null | The finished-goods lot the order produced — what a goods receipt is posted against. | Named after the order: <order>-FG, created when the order completed. |
When the order yielded nothing good. There is no lot, and an ERP posting a receipt against a lot the MES never made is worse than a null. |
started_at (optional) |
timestamp, or null | When the order started on the floor, UTC, with no offset marker. | The moment its first step was started. | When the order never started. |
completed_at (optional) |
timestamp, or null | When the order finished, on the same clock as started_at. |
The moment its last step was completed. | When the order is not finished. |
ComponentUse¶
One lot consumed at one operation. Appears inside an operation confirmation.
| Field | Type | On the floor | Where the MES gets it | When it is null |
|---|---|---|---|---|
lot |
string | The lot number of what was consumed: the number on the pallet, the drum or the label. | The MES lot the consumption was booked against. | Never. |
material |
string | What that lot is. | The lot's material. | Never. |
quantity |
number | How much of that lot this step consumed, in the material's own unit — kilograms, litres, each. | The booked consumption. The unit itself is not in this document; it is the material master's, on both sides. | Never. |
equipment (optional) |
string, or null | The machine that consumed it. | The operation's machine. | When the operation has no machine against it. |
ProductionRequest¶
The inbound direction: an order arriving from the ERP. Included here because a plant checking its file exchange usually wants to check both directions with the same tool.
| Field | Type | On the floor | Where the MES gets it | When it is null |
|---|---|---|---|---|
code |
string | The order number the floor will see. | The ERP's order or job number, from the schedule file or the API payload. | Never; a request with no order code is rejected rather than given a number the MES invented. |
material |
string | What to make. | The ERP's material code, which must already exist in the MES's material master. | Never; a request with no material is rejected. |
quantity |
number | How many the ERP is asking for. | The schedule. | Never null. A file that omits it is read as 0 — the one place this contract fills a gap, and it fills it with the smallest number rather than a plausible one, so an order for nothing looks like an order for nothing. |
due_date (optional) |
timestamp, or null | When the plant promised it. | The ERP's requested finish date. | When the schedule carries none. The MES then sequences by priority alone rather than inventing a date the plant never promised. |
priority (optional) |
integer, or null | Which order the dispatch list puts first; lower runs earlier. | The ERP's priority, exactly as the schedule gives it. | When the schedule does not say - which for an ERPNext Work Order is always, because that doctype has no priority field. The MES then gives the order the priority this plant chose for orders carrying none ([erp] default_order_priority, 50 unless the plant says otherwise). Null here means the ERP was silent, not that the order has no priority once it is in the MES. |
erp_reference (optional) |
string, or null | The ERP's own key for this order, kept so every confirmation can carry it back. | The payload's erp_reference or id, falling back to the order code. |
Never in practice, because of that fallback. |