Workstation Capacity & Downtime Scheduling
How a workstation's operating cost is assembled from named cost components, how that cost propagates (and sometimes doesn't) into planning documents, and why downtime logging and capacity scheduling are two unconnected mechanisms despite sharing a workstation
1. Requirements
1.1 Functional requirements
- Define a Workstation — a physical production resource — with its own working-hour windows, a holiday calendar, a parallel-job capacity, and an hourly operating rate assembled from named cost components rather than typed in as one number.
- Let many workstations share one reusable operating-cost profile (a Workstation Type), and let a shop-floor operation be scheduled against a type of station rather than one specific station, so capacity planning can pick whichever matching station is actually free.
- Break a workstation’s hourly rate into named components (e.g., separate cost drivers), each mappable to its own expense ledger account per legal entity, so actual accrued operating cost can be posted component-by-component rather than as one lump sum.
- Log downtime against a workstation — a time window, an operator, and a reason drawn from a fixed list — for later analysis.
- Provide a live, visual Plant Floor view scoped to a warehouse: workstation status and color, the next queued Job Cards per station with their time logs, and raw-material availability for each queued Job Card.
1.2 Non-functional requirements
- Cost derivation, not manual entry: a workstation’s hourly rate is always the sum of its own cost-component rows — it is never a value someone sets directly, and it recomputes on every save.
- Live status broadcast: a workstation’s status change should reach an open Plant Floor board in real time, without the viewer polling or reloading.
- One-time template copy, not a live binding: adopting a Workstation Type’s cost profile onto a specific Workstation is a snapshot at the moment of adoption, not a subscription to future changes on the type.
1.3 Constraints
- Component-level cost tracking exists specifically to let actual operating cost be posted into distinct expense accounts, not merely to organize the rate on the Workstation form.
- Downtime logging and shop-floor capacity scheduling are not the same mechanism and do not interact in this codebase — see §3.2.
2. High-Level Design
2.1 Component diagram
2.2 Data flow — a workstation’s rate change and where it does (and does not) propagate
3. Deep Dive
3.1 Data model
Workstation — the physical resource: production_capacity (how many Job Cards it can run in parallel — the figure the shop-floor execution document’s overlap check reads directly), holiday_list, working_hours (a table of non-overlapping start/end windows, validated against each other on save and summed into total_working_hours), workstation_costs, and a status (Production/Off/Idle/Problem/Maintenance/Setup) that a disabled workstation is forced into Off. An optional workstation_type link, when changed, triggers a one-time copy of that type’s cost rows onto this workstation; an optional plant_floor link enables the live-status broadcast described below.
Workstation Type — a reusable cost profile: its own workstation_costs table and derived hour_rate, computed the identical way. Beyond serving as a copy source for individual workstations, a shop-floor operation can name a Workstation Type directly instead of one physical workstation, letting the capacity-planning search (documented in the shop-floor execution design) pick whichever workstation of that type has a free slot.
Workstation Cost — one row, on either a Workstation or a Workstation Type: an operating_component reference and a per-hour operating_cost. Duplicate components on the same parent are rejected. The sum of these rows is the only source of hour_rate — there is no path to set the hourly rate directly on either parent document.
Workstation Operating Component — a named cost category (e.g., a distinct operating-cost driver), holding a per-legal-entity table of Workstation Operating Component Account rows, each mapping the component to a specific expense ledger account for one company. This is the mechanism that lets a Work Order’s accrued operating cost be split and posted component-by-component into distinct expense accounts at actual-consumption time, rather than as one undifferentiated operating-cost line — the same component name drives both the planning-side rate (via Workstation Cost) and the posting-side account resolution (via this table).
Downtime Entry — a standalone log: workstation, operator, stop_reason (a fixed list of causes), from_time/to_time, and a downtime duration computed as the difference between them in minutes. This is the entire mechanism — nothing else in the codebase reads a Downtime Entry, reacts to one being created, or feeds one back into capacity planning, workstation status, or Job Card scheduling. It exists solely to be logged and later analyzed in a separate report outside this document’s scope.
Plant Floor — a warehouse-scoped visualization surface, not itself a scheduling mechanism: it surfaces each of its workstations’ live status and color, the next ten non-completed, non-subcontracted Job Cards queued against a given workstation (with their time logs), and — for each of those Job Cards — whether the required raw materials are actually available, checked against live warehouse stock and adjusted for whichever warehouse a “skip transfer, backflush from work-in-progress” Job Card would actually draw from. It also exposes a generic stock-move helper and a small per-warehouse stock dashboard (projected/actual/reserved quantities) alongside the workstation board — a convenience bolted onto the same page rather than a mechanism this document’s scheduling logic depends on.
3.2 Algorithm — where a rate change propagates, and the two unconnected holiday mechanisms
Editing a workstation’s cost only pushes forward into a Routing, and only there. Saving a Workstation with a changed hour_rate triggers one specific, narrow side effect: every operation row on a Routing that names this workstation has its own hour_rate rewritten to match. Nothing else is touched. A BOM’s own operation row — even one originally copied wholesale from that same Routing, as the costing-engine document describes — keeps whatever rate it captured at copy time; a Work Order’s or Job Card’s own operation row is likewise its own independent snapshot, taken when that record was created. This is the same “trust the copy, not a live reference” pattern that governs Routing-to-BOM copying and Workstation-Type-to-Workstation cost adoption elsewhere in this module — here it draws a sharp, easy-to-miss line: some downstream copies of a workstation’s rate update live, and most do not.
Two independent holiday/working-hours mechanisms exist, and only one of them runs. The mechanism actually wired into capacity-planning Job Card scheduling (described in the shop-floor execution design) calls a single method on the Workstation document that shifts a proposed start date forward, day by day, past any date on the holiday calendar — nothing more elaborate than that, and no working-hours check happens at that call site beyond the day-level holiday skip. A second, entirely separate set of functions in this same module — checking whether an operation’s duration fits inside any of a workstation’s declared working-hour windows at all, and raising a dedicated error naming the workstation if a holiday falls inside a proposed time range — is fully implemented, including its own error classes, but has no caller anywhere in the codebase outside its own test file. Both mechanisms are real, working code; only one of them is reachable from any production code path.
3.3 API/interaction contract (illustrative)
POST /workstations/{name}/set-data-based-on-workstation-type
→ one-time copy of the linked Workstation Type's cost rows onto this workstation
GET /workstations?plant_floor={floor}
→ live status, color, and image for every enabled workstation on that floor
GET /workstations/{name}/job-cards
→ next 10 queued, non-subcontracted Job Cards, with time logs
GET /job-cards/{name}/raw-materials
→ required items with live stock-based availability flag
POST /plant-floors/{name}/make-stock-entry
{ item_code, qty, from_warehouse, to_warehouse, purpose }
→ a generic ad-hoc stock move, independent of any Work Order
3.4 Error handling
- Overlapping working-hour windows on the same workstation are rejected at save time, naming the conflicting rows.
- Duplicate operating components on the same Workstation or Workstation Type are rejected outright.
- A dedicated “operation longer than any working-hour window” error and a dedicated “workstation closed for a holiday in this range” error both exist and are fully implemented — but, per §3.2, neither is invoked from any live code path, so in practice a Job Card’s actual time window is never rejected on either ground; only the day-level holiday skip inside capacity-planning scheduling has any real effect.
4. Scale and Reliability
- Cost propagation is intentionally shallow, not lazy. Because only Routing operation rows are rewritten on a workstation rate change, the cost of that update is small and bounded regardless of how many BOMs, Work Orders, or Job Cards have ever copied that workstation’s rate — but the trade-off is that every one of those copies is a snapshot whose accuracy decays the moment the source workstation’s cost changes, with nothing to detect or flag the drift.
- The live-status broadcast is per-workstation, not batched — each status change publishes its own event scoped to the linked Plant Floor, so a floor with many workstations changing status in quick succession sends one message per change rather than one summarized update.
- Plant Floor’s Job Card and stock queries are capped, not paginated for scale — ten Job Cards per workstation query, twenty stock rows per warehouse dashboard page — adequate for a live board a human is watching, not designed as a bulk reporting interface.
5. Trade-off Analysis
| Decision | Trade-off |
|---|---|
| Hourly rate derived from named cost components rather than entered directly | Enables component-level cost posting into distinct expense accounts at actual-consumption time, at the cost of the rate being un-settable directly — every rate change must go through editing the component rows. |
| Workstation rate change propagates live only to Routing, not to BOM/Work Order/Job Card copies | Keeps the update cheap and bounded, at the cost of silent staleness everywhere else a rate was ever copied — there is no signal distinguishing a current copy from a stale one. |
| Downtime Entry as a pure log with no live interaction with scheduling or status | Simple to implement and safe to ignore if unused, at the cost of the module’s own naming (“Downtime Scheduling”) implying a live effect on capacity planning that the code does not provide. |
| Two independently implemented holiday/working-hours checks, only one of them wired into a live call path | The unused mechanism is more rigorous (a real working-hours-fit check, dedicated errors) than the one that actually runs (a bare day-level holiday skip) — whichever engineer eventually wires it up gets a head start, but until then the more careful validation simply never executes. |
| Workstation Type as both a cost-template source and a first-class schedulable target (a pool of interchangeable stations) | One record serves two different roles cleanly, at the cost of a reader needing to know which role applies in a given context — “this workstation’s type” (template) versus “any workstation of this type” (schedulable pool) look identical on the field. |
6. What to Revisit as the System Grows
- Either wire up or remove the unused working-hours/holiday validation functions — leaving fully-implemented, never-called validation code in place is a latent trap for a future change that assumes it runs.
- Add a staleness signal for copied operating rates (Routing-to-BOM, Workstation-Type-to-Workstation, or a workstation’s own rate versus its already-created Work Order/Job Card operation rows), so a rate change is at least visible as “some downstream copies may be out of date” rather than silently invisible.
- Connect Downtime Entry to capacity planning, if the intent behind its name is ever meant to be realized — today logging downtime has no effect on any schedule, available-slot search, or workstation status.
- Reconsider the fixed ten-Job-Card / twenty-row caps on Plant Floor queries if the board is ever used for something closer to reporting than a live, glanceable status view.