Work Order & Job Card Shop-Floor Execution
How a Bill of Materials becomes a schedulable production run, how each operation is tracked as its own Job Card, and how the two feed a shared stock-movement mechanism to consume raw materials and produce finished goods
1. Requirements
1.1 Functional requirements
- Raise a Work Order — a commitment to manufacture a quantity of one item — from a Bill of Materials, optionally scoped to a specific sales order, a subcontracting inward order, or a sub-assembly line inside a production plan, and copy that BOM’s raw-material list (optionally exploded across every nested level) and operation sequence onto it.
- Split a Work Order’s operations into one or more Job Cards — a unit of shop-floor work scoped to one operation and a batch-sized slice of the total quantity — and, when capacity planning is enabled, schedule each Job Card into a concrete time slot on a workstation, respecting that workstation’s working hours and holidays.
- Enforce that operations execute in their declared sequence: a Job Card for a later-sequence operation cannot log more completed quantity than earlier-sequence operations have already completed.
- Move raw materials into a work-in-progress location, consume them, and post finished output, all through one shared, purpose-tagged stock-movement mechanism — with distinct purposes for transferring materials in, converting them into finished/semi-finished goods, and reversing a manufacture (disassembly).
- Support two materially different execution shapes: a pooled mode, where the whole Work Order shares one work-in-progress warehouse and one running “how much is done” figure, and a staged mode (semi-finished-goods tracking), where each operation is its own mini production step with its own input/output warehouses, feeding the next operation directly.
- Track actual time spent per employee per operation (time logs), separately from any capacity-planning projection of when that time is expected to occur (scheduled time logs) — the two are independent tables serving different purposes.
- Let a Job Card correct a defect found in an already-completed operation via a dedicated corrective Job Card, whose cost is tracked separately from normal operation cost and only optionally folded into finished-good valuation.
- Reserve stock against a Work Order — both the raw materials it will consume and, when it is fulfilling a sales order, sub-assembly requirement, or subcontracting inward order, the finished good it will produce — and keep that reservation’s transferred/consumed figures current as real stock movements happen.
1.2 Non-functional requirements
- Status as a derived fact, not a stored decision: a Work Order’s status is recomputed from its own stock-movement history and operation state on nearly every update, rather than being a value some single action sets once and forgets.
- Overproduction bounded, not forbidden: producing or transferring slightly more than planned is allowed up to a configurable percentage allowance, not blocked outright, since real shop floors rarely hit a planned quantity exactly.
- Non-blocking capacity search: scheduling a Job Card into a workstation’s calendar must fail fast with a specific error (naming the configured search horizon) rather than searching indefinitely for a free slot.
- Reversibility of a specific manufacture, not just cancellation of the whole run: disassembling output must be trackable against the specific manufacture event that produced it, net of whatever has already been disassembled from that same event.
- Auditable time and quantity: every completed-quantity and time figure must be traceable to a specific stock movement, time log, or Job Card — a Work Order’s aggregate figures are always a computed sum over that evidence, never a number typed in directly.
1.3 Constraints
- A Work Order’s “additional items” and “secondary items” views (non-stock costs, co-products/by-products/scrap) are not stored rows — they are computed on read, either from the BOM’s own declared secondary/non-stock lines or from the actual stock movement that already ran, and cannot be edited as if they were a real child table.
- Job Card generation and capacity scheduling only run automatically when a Work Order is submitted with a Bill of Materials that has operations; a Work Order with no operations skips Job Cards entirely and treats “transfer material” and “manufacture” as two plain stock movements against the whole order.
- The two transfer scopes — “against the whole Work Order” and “against each Job Card individually” — are mutually exclusive per Work Order and change which formula computes “how much has been transferred so far.”
- Subcontracted operations and the purchase order they can generate are a real, working seam in this mechanism, but the subcontracting lifecycle itself is out of scope for this document.
2. High-Level Design
2.1 Component diagram
2.2 Data flow — from submitting a Work Order to a scheduled Job Card
Because each operation’s planned start chains off the previous operation’s planned end (operations sharing one sequence id start together instead of one after another), a Work Order with several parallel-capable operations schedules them concurrently rather than strictly serially — but a capacity failure on any one operation aborts the whole submission, since Job Cards for later operations depend on the timing already assigned to earlier ones.
3. Deep Dive
3.1 Data model
Work Order — the root record: the item and quantity to manufacture, its Bill of Materials, warehouses (work-in-progress, finished-good, source, scrap), a skip_transfer flag (bypass the separate material-transfer step and consume raw materials directly at manufacture time), use_multi_level_bom (explode required items and operations across every nested sub-assembly rather than just the top level), transfer_material_against (Work Order — one pooled transfer figure — or Job Card — each Job Card tracks its own), and track_semi_finished_goods (the staged execution mode, inherited from the BOM). Carries running totals (produced_qty, material_transferred_for_manufacturing, process_loss_qty, disassembled_qty) that are always recomputed from stock-movement history, never typed in directly.
Work Order Item — one required raw-material line: required_qty, transferred_qty, consumed_qty, returned_qty, stock_reserved_qty, an optional operation/operation_row_id tag (which operation consumes this line, used by the staged execution mode and by Job-Card-scoped transfer), and an is_additional_item flag for a line added on the fly during an actual transfer that wasn’t in the original BOM-derived list (this can happen when the raw-material-quantity validation setting is relaxed).
Work Order Operation — one manufacturing step copied from the BOM’s operations: planned and actual start/end times, planned_operating_cost/actual_operating_cost, completed_qty/pending_qty/process_loss_qty, and — in staged mode — its own source_warehouse/wip_warehouse/fg_warehouse triplet. Status (Pending/Work in Progress/Completed) is derived purely from completed-plus-process-loss quantity against the Work Order’s own quantity (with the overproduction allowance as slack), never set directly.
Work Order Additional Item (the “secondary items” / “non-stock items” views) — not a stored child table. Its document class stubs out insert/update/delete entirely; the rows a user sees are assembled live, either by scaling the BOM’s own secondary-item and non-stock-item lines to the Work Order’s quantity, or — once a finished-good stock movement has actually run — by reading that movement’s own recorded co-product/by-product/scrap lines back out. It exists purely as a read projection, not an editable record.
Job Card — one unit of shop-floor work: work_order, operation/operation_id (which Work Order Operation this is), for_quantity (this Job Card’s own batch-sized slice), workstation/workstation_type, wip_warehouse/source_warehouse/target_warehouse, and status (below). is_corrective_job_card plus for_job_card/for_operation mark a corrective card. finished_good/semi_fg_bom are populated only in staged execution mode, where this one Job Card is itself a mini production step with its own BOM-derived output.
Job Card Item — a required raw-material line scoped to this specific Job Card (populated only when the Work Order transfers against Job Cards, or tracks semi-finished goods): required_qty, transferred_qty, consumed_qty.
Job Card Operation (the live Sub-Operation status breakdown — distinct from the Operation master’s own reusable Sub-Operation list) — one row per sub-step the parent Operation master declares, mirrored onto this Job Card and kept in its own Pending/Work In Progress/Complete/Pause status, driven by which sub-step each time log entry names. When any of these exist, the Job Card’s own total_completed_qty becomes the minimum completed quantity across all of them — a multi-step operation is only as done as its slowest sub-step.
Job Card Scheduled Time — a projected time slot from the capacity-planning walk described in §2.2/§3.2: from_time/to_time/time_in_mins. Purely a forecast — it drives expected_start_date/expected_end_date and capacity-conflict checking, and is never itself converted into a real time log.
Job Card Time Log — an actual worked interval: employee, from_time/to_time, completed_qty for that interval. Sums into total_time_in_mins/total_completed_qty on the Job Card, which in turn feed the parent Work Order Operation’s actual_operation_time/completed_qty. Overlap checking treats two time logs on the same workstation as conflicting unless the workstation’s own production_capacity (how many jobs it can run in parallel) is high enough to absorb them, and separately refuses to let one employee hold two concurrently-open Job Cards on different workstations.
Job Card Secondary Item — populated only in staged execution mode, mirroring the semi-finished-goods BOM’s own co-product/by-product/scrap lines (scaled to this Job Card’s quantity, net of process loss) so the Job Card’s own semi-finished manufacture movement can record them.
3.2 Algorithm — status derivation, batch-splitting, and the two execution modes
Work Order status is recomputed, not stored as a decision. On (almost) every touch, the status is derived fresh: Draft/Cancelled follow docstatus directly; otherwise the order starts at Not Started and is promoted to In Process the moment any material has been transferred (or the order skips transfer, or any operation has moved off Pending), and promoted to Completed once produced_qty + process_loss_qty reaches the ordered quantity. Stopped and Closed are the two states that survive this recomputation instead of being overwritten by it — once set, the order stays there until an explicit unstop/reopen action. Only when the order would otherwise read Not Started and stock is being reserved does the status branch into Stock Reserved/Stock Partially Reserved, comparing each required line’s reserved quantity against what it needs.
“How much has been transferred” is not one formula — it depends on the transfer scope. With transfer_material_against = "Job Card", the figure is deliberately not a sum: it is the minimum, across every operation, of that operation’s own completed-plus-process-loss quantity — a strict reflection of sequential flow, since the slowest operation caps what the whole order can be said to have “moved forward.” With whole-order transfer, the figure comes from actually summing transfer stock-movement quantities; but when those movements were generated from a pick list (which posts a zero fg_completed_qty on the transfer voucher rather than a per-line total), the code instead takes the minimum fraction of required-quantity actually transferred across every raw-material line, scaled by the order quantity — so a line that’s only half-transferred caps the reported progress at half, even if every other line is complete.
Job Card generation splits by batch size, not evenly. For each operation, in sequence order, the remaining quantity is repeatedly sliced by that operation’s batch size (which itself only applies if the underlying Operation master is flagged to size Job Cards by batch — otherwise a single Job Card takes the operation’s full quantity) until nothing remains; each slice becomes one Job Card. If capacity planning is enabled, each new Job Card’s time slot is computed by chaining off the previous operation’s planned end time (or the Work Order’s own planned start, for the first operation), walking forward through the target workstation’s declared working-hour windows and skipping holidays, splitting the required time across multiple days if it doesn’t fit in one window. A slot that would land beyond a configured number of days from the Work Order’s planned start raises a capacity error and aborts the whole submission — this is the one point where the two “High” documents in this plan (this one and the workstation/capacity document) meet directly.
Sequence enforcement happens at two different layers. On the Work Order itself, validate_operations_sequence only checks shape: sequence ids must start at 1 and every subsequent one must equal the running value or exactly one more than it (letting several operations share an id to represent work that can run in parallel). On the Job Card, a second and stricter check runs at save time: before a Job Card for a given sequence id can record more completed quantity, every operation at a lower sequence id must already show that same cumulative completed quantity — not just a Completed status flag, an actual quantity comparison — so a later stage cannot race ahead of an earlier one even by a fractional amount.
Two execution modes compute “what a Job Card produces” differently. In the ordinary (pooled) mode, operations do not themselves produce or consume stock — only the Work Order’s own whole-order Manufacture movement does, at the very end, consuming everything from one work-in-progress warehouse and producing the finished good. In staged (semi-finished-goods) mode, completing a Job Card can itself trigger its own semi-finished manufacture movement (via a dedicated action), consuming that operation’s own required items from its own source warehouse and producing semi-finished output — using the BOM’s is_final_finished_good operation and finished_good item — into the next operation’s source warehouse, except for the last operation, whose output lands in the Work Order’s real finished-good warehouse. Each stage is thus a self-contained miniature production step, chained by warehouse, rather than one big consumption/production event at the end.
Corrective cost is additive, not blended into normal operation cost. Every corrective Job Card for a Work Order contributes total_time_in_mins / 60 * hour_rate to a single corrective_operation_cost figure, recomputed in full from every corrective Job Card each time one changes; this figure only enters the Work Order’s total_operating_cost (and, from there, potentially the finished good’s valuation) when a Manufacturing Settings toggle explicitly says corrective cost should be folded in — otherwise it is tracked but never costed into the product.
3.3 API/interaction contract (illustrative)
POST /work-orders { bom_no, item, qty, sales_order? }
→ copies required items (exploded, if multi-level) and operations from the BOM
POST /work-orders/{name}/submit
→ creates Job Cards per operation (batch-split, capacity-scheduled)
→ reserves stock if reserve_stock is set
POST /work-orders/{name}/stock-entry
{ purpose: "Material Transfer for Manufacture" | "Manufacture" | "Disassemble", qty? }
→ returns a pre-filled Shop-Floor Stock Voucher scoped to this Work Order
POST /job-cards/{name}/start-timer { employees }
POST /job-cards/{name}/pause-job { end_time }
POST /job-cards/{name}/resume-job { start_time }
POST /job-cards/{name}/complete { qty, pending_qty, process_loss_qty, end_time, auto_submit? }
→ closes the open time log, sets completed/pending/process-loss quantities
→ auto_submit also submits the Job Card and, in staged mode, posts its
own semi-finished Manufacture movement
POST /job-cards/{name}/make-corrective { operation, for_operation }
→ creates a linked corrective Job Card, excluded from normal sequencing
3.4 Error handling
- Overproduction is bounded, not forbidden: producing, transferring, or reporting Job Card quantity beyond the ordered amount plus a configured allowance percentage raises a specific error naming the ceiling, at three independent checkpoints (against the sales order, against the Work Order’s own planned quantity, and against a single operation’s Job Card total).
- Capacity exhaustion: a Job Card that cannot be scheduled within the configured planning horizon raises a capacity error identifying the operation and the horizon setting, aborting the whole Work Order submission rather than leaving some operations scheduled and others not.
- Sequence violations: a Job Card that tries to log completed quantity ahead of an earlier-sequence operation’s own completed quantity is rejected outright, naming both operations.
- Cross-linking mismatches: a Job Card’s stored operation reference must match the Work Order Operation row it claims to belong to, or the save is rejected — this guards against a stale or manually-edited link pointing at the wrong step.
- Cancellation guard: a Work Order cannot be cancelled while any submitted stock voucher still references it, and a
StoppedWork Order must be explicitly unstopped before it can be cancelled at all. - Quality gate at Job Card submission: when both the BOM and the specific operation require inspection, a missing or rejected Quality Inspection blocks (or, depending on a Stock Settings toggle, only warns on) submission.
- Over-transfer on a Job Card’s own items: transferring more than a Job Card’s required quantity for a line is rejected unless a Manufacturing Settings toggle explicitly permits excess transfer.
- Closing blocked by live work: a Work Order cannot be closed while any of its Job Cards are still
Work In Progress.
4. Scale and Reliability
- Status is cheap to get wrong and cheap to fix, by design. Because status is a pure function of current stock-movement and operation data rather than a stored decision, a bug in one status transition self-heals the next time anything touches the record — at the cost of recomputing it on nearly every save, including from unrelated child-table updates.
- Job Card generation is synchronous with Work Order submission. For an order with many operations and a small batch size, submission itself does the batch-splitting, Job Card creation, and (if enabled) the capacity-planning search inline — there is no background queue here, unlike the bulk reposting machinery on the inventory side; a Work Order with an unusually large operation count or very fine batch sizing pays that cost synchronously at submit time.
- The capacity search is bounded by a configured horizon, not by workstation count. Walking forward through one workstation’s working hours to find a slot is a linear scan bounded by the configured number of days, independent of how many other Job Cards exist elsewhere in the system — but it is a per-operation, per-submission search with no caching of “known busy” windows across different Work Orders being submitted around the same time.
- Two independent “how much is done” formulas must stay conceptually distinct. Because Job-Card-scoped transfer uses a minimum-across-operations formula while whole-order transfer uses a summed or minimum-fraction formula, any future change to one must be checked against the other separately — they are not two configurations of one shared calculation.
- Corrective cost recomputes in full from scratch on every corrective Job Card change. This is cheap while the number of corrective cards per Work Order stays small (the normal case — genuine rework is the exception, not the rule); it would not scale gracefully if corrective cards became a routine, high-volume part of the process.
5. Trade-off Analysis
| Decision | Trade-off |
|---|---|
| Status recomputed fresh from evidence rather than stored as an explicit transition | Self-healing and hard to get permanently wrong, at the cost of recomputing it on nearly every touch and needing careful reasoning about which states (Stopped, Closed) are deliberately exempted from the recomputation. |
| Two mutually exclusive transfer scopes (whole-order vs. per-Job-Card), each with its own “progress” formula | Lets a simple shop floor use one pooled figure while a sequential, multi-station floor gets an honest minimum-across-operations figure — at the cost of two formulas that must each be understood and maintained on their own terms. |
| Job Card batch-splitting and capacity scheduling run synchronously at Work Order submission | Simple to reason about (submission either fully succeeds or fails with a specific error) at the cost of a large operation count or fine batch size making submission itself slow, with no partial/background completion path. |
| “Additional items” and “secondary items” as computed, non-persisted views rather than a real child table | Always consistent with whatever the BOM or the actual stock movement currently says, at the cost of behaving unlike every other child table on the document — no independent edit, insert, or delete is possible, which is easy to miss when scripting against the Work Order. |
| Staged (semi-finished-goods) execution as a distinct mode layered onto the same Work Order/Job Card shape, rather than a separate document type | Reuses the whole scheduling, sequencing, and status mechanism for a materially different execution style (each operation its own mini production step), at the cost of many methods across both documents branching on track_semi_finished_goods to know which behavior applies. |
| Corrective cost tracked separately and only optionally folded into valuation | Keeps normal operation cost clean of rework noise by default, at the cost of two Work Orders with identical rework history reporting different total costs purely based on a global settings toggle. |
| Sequence enforcement duplicated at two layers (shape check on the Work Order, quantity check on the Job Card) | Catches structurally invalid sequences early and race-ahead quantity violations late, at the cost of two independent places encoding “what a valid sequence means” that could drift out of agreement. |
6. What to Revisit as the System Grows
- Move Job Card generation and capacity scheduling to a background job once Work Orders with large operation counts or fine batch sizes make synchronous submission noticeably slow — today’s design trades that risk for submission-time certainty.
- Unify the two “how much is done” formulas (minimum-across-operations for Job-Card-scoped transfer; summed-or-minimum-fraction for whole-order transfer) behind one shared abstraction if a third transfer scope is ever added, so a future mode does not need a third bespoke formula.
- Give the capacity search cross-submission awareness (a shared, cacheable view of “which workstation windows are already claimed”) if many Work Orders are commonly submitted in close succession against the same limited set of workstations.
- Reconsider corrective cost’s full-recompute-on-every-change approach if rework becomes routine enough that the number of corrective Job Cards per Work Order stops being small.
- A visible marker for “additional items” being a computed view, surfaced wherever the Work Order is scripted or integrated against, since its read-only, non-persisted nature is not obvious from the shape of the data alone.