ZuloOne
English
Sign in
Menu

06 · Production — production orders, BOM, routing, and WIP

Updated

Solution layer. Depends on Common, Inventory, Costing, HR (shop operator rate), and Sales. Production turns arbitrary raw materials into arbitrary finished goods by a bill of materials. The workshop layer — routings, work centers, and labor accounting — combines with the BOM and the order into a full production contour.

← General specification

Purpose#

BillOfMaterials is the BOM dictionary: the composition of any product, with multi-level expansion. Routing is the route dictionary: the sequence of operations at work centers. ProductionOrder is the production order document: in subtypes Draft/Released/Finished it reserves components, consumes them, accumulates work in progress, and receives the output at actual cost (materials plus assigned labor).

What is created (object map)#

Kind Name Purpose
Model Production module model, Solution(2) layer
NumberSequence BillOfMaterialsSeq, ProductionOrderSeq numbering
EDT RefBillOfMaterials, RefRouting, RefWorkCenter references
Dictionary BillOfMaterials (+ child BomComponent), Routing (+ child RoutingOperation), WorkCenter, ProductionSettings (singleton) BOMs, routings, work centers, module settings
TablePart ProductionOrderComponents, ProductionOrderOperations required components and order operations
Document ProductionOrder production order (Draft→Released→Finished)
Register WorkInProgress quantity of product in work
Service IBomService, IRoutingService, IWorkCenterLoadService, ILaborCostService, IProductionComponentAvailability BOM expansion, route stamping/load, labor cost, available component balance
Script tx ProductionDraftReserveTx/ProductionConsumeTx/ProductionWipTx/ProductionOutputTx, commands ExpandBom/ReleaseProduction/FinishProduction/ConfirmOperations/ScheduleProduction, order event handler postings, transitions, invariants: auto-expansion, availability/labor/trade-profile guards, output costing
Menu menu.json module menu
— reused — Item/Qty/Stock/ReservedStock/services (Inventory), ItemCostFifo/InventoryValue (Costing), Employee (HR) not created here

Total: 1 model, 2 number sequences, 3 EDTs, 4 dictionaries (+2 child dictionaries), 2 table parts, 1 document, 1 register, 5 services, scripts, and menu.

model.json#

Solution(2) layer. Dependencies: Common (≥1.10.0), Costing (≥1.0.0), Inventory (≥1.0.0), HR (≥1.9.0), Sales (≥1.55.0).

Number sequences#

BillOfMaterialsSeq, ProductionOrderSeq. Routing and WorkCenter use the default sequence and do not declare their own.

EDT#

RefItem/Qty/RefStoreCell come from Inventory; RefEmployee comes from HR. The model owns RefBillOfMaterials, RefRouting, and RefWorkCenter.

Dictionaries#

BillOfMaterials#

| Name String(160) required · Product→Item required · OutputQty Qty required |

  • The BOM applies to a batch, not to one unit. OutputQty is how many products one recipe run outputs. Expansion is centralized in IBomService.
  • Composition is the child dictionary BomComponent, shown as the list on the BOM card. The first live (not disabled) record for a Product wins. This is the same “first line wins” rule as Routing.

BomComponent#

Child BOM lines. FK Bom→BillOfMaterials.

| Component→Item required · QtyPer Qty required (consumption for OutputQty output units) · Unit optional (empty means the component base unit) · Explode Boolean optional |

  • Explode controls multi-level BOM expansion. false (default) requires the component from stock as-is: one-pass expansion. true expands the component's own BOM and merges its leaves into the total demand, so an in-house semi-finished product does not need a separate receipt line in stock. A cycle, where a product refers to itself through Explode, raises an exception during expansion instead of looping silently.

Routing#

A routing is the operation recipe for a product. It uses the same “first live record wins” semantics as BOM.

| Name String(160) required · Product→Item required · IsDisabled Boolean optional |

  • Composition is the child dictionary RoutingOperation. Operation order is Sequence.

RoutingOperation#

One routing step. Sequence is unique inside the routing.

| Routing→Routing required · Sequence Integer required · Name String(160) required · WorkCenter→WorkCenter optional · SetupMinutes Integer optional · RunMinutesPerUnit Decimal(18,4) optional |

WorkCenter#

A work center is the section or machine where an operation runs. This version has no physical capacity planning; capacity is reference data only.

| Name String(160) required · IsDisabled Boolean optional · HourlyRate Decimal(18,4) optional — hourly rate used when the marked operation does not name an employee; the employee position rate always wins · CapacityMinutesPerDay Decimal(18,4) optional — 0 means no ceiling is declared; it affects only the load report, not release |

ProductionSettings#

Singleton module settings dictionary (isSingleton, isCached).

| AutoExpandBom Boolean optional — a new order expands the BOM and stamps the route by itself on creation (see Business logic and invariants); without the flag, only the ExpandBom command does it · DefaultOutputLocation→StoreCell optional — default output cell when the order does not specify one |

Documents#

ProductionOrder#

  • Header: Product→Item (required) · Quantity Qty (required, unit is Unit, normalized into BaseQuantity) · Location→StoreCell (required) — component consumption cell and WIP accumulation cell · OutputLocation→StoreCell (optional) — output receipt cell. Empty means Location; if ProductionSettings has a default cell, a new order stamps it. Components and WIP always stay on Location. OutputLocation moves only the finished product.
  • TP-Components (ProductionOrderComponents): Component→Item (required) · QtyRequired Qty (required, unit is Unit, normalized into BaseQuantity) — demand for one line. Filled by ExpandBom or entered manually.
  • TP-Operations (ProductionOrderOperations): Sequence Integer (required) · Name String(160) (required) · WorkCenter→WorkCenter optional · SetupMinutes/RunMinutes Decimal optional (planned time from the routing) · Employee→Employee optional (who actually did the operation) · ScheduledOn DateTime optional (planned day from ScheduleProduction) · ActualMinutes Decimal optional and CompletedOn DateTime optional. The marker for “completed” is CompletedOn, not ActualMinutes: an instant zero-minute operation is legitimate and cannot be distinguished from an unmarked operation by minutes alone.
  • Subtypes: Draft (initial) → Released → Finished (isReadOnly). The user transition is command-only: ExpandBom is available on Draft, and ReleaseProduction and FinishProduction move the subtype. Programmatic code uses document.Subtype/SetSubtypeAsync in services and tests.
  • Postings by subtype: a subtype can have any number of tx scripts, ordered by executionOrder. The semantics are Mix: postings do not accumulate; the target subtype writes its own set again. Therefore ProductionConsumeTx is not duplicated on the Released→Finished transition.
    • Draft — ProductionDraftReserveTx: +ReservedStock for the demand from Components, with analytics Item/Cell=Location. Nothing is reserved if Location is empty or the line has zero quantity or no component.
    • Released — ProductionConsumeTx (−Stock for each component on Location), then ProductionWipTx (+WorkInProgress for Product/Location, full Quantity). The Draft reserve is removed by Mix: Released does not contain it.
    • Finished — ProductionConsumeTx with the same set as Released (the components are written again, not twice: Mix has already annulled the Released consumption), then ProductionOutputTx (+Stock for Product/OutputLocation, full Quantity). WorkInProgress is not written in this subtype: the Released→Finished transition removes it completely.

Registers#

WorkInProgress#

Quantity of product “in work”: Released writes it, Finished removes it (Mix). It stores quantity only, not cost. The FIFO cost of components has already left stock in ProductionConsumeTx; this register does not store it.

{
  "kind": "Register",
  "object": {
    "caption": { "en": "Work in progress", "ru": "Незавершённое производство" },
    "registerEngineType": "Standard",
    "isDoubleEntry": false, "allowNegativeBalance": true, "useBalanceTable": true,
    "metaId": "36549e8e-59a3-4a9e-af3d-86aaea866768", "name": "WorkInProgress",
    "modelId": "<GUID Model.Production>", "layerId": 2
  },
  "resources": [ { "fieldName": "Qty", "isOperational": true } ],
  "analytics": [
    { "name": "WorkInProgress-Cell", "isRequired": true },
    { "name": "WorkInProgress-Item", "isRequired": true }
  ]
}

allowNegativeBalance:true means an order can technically be closed after WIP is already exhausted, for example after manual intervention or a rerun. The register does not block this; it records what happened.

Services#

IBomService#

ExpandByProductAsync(product, qty) → Dictionary<Guid, decimal> returns the component demand in the component storage unit. Formula per line: need = QtyPer × (qty / OutputQty). If the line has Unit, it converts to the base unit through IItemQuantityConverter. A line with Explode=true recursively expands the component BOM and merges leaf components into the total demand. A cycle, where the product refers to itself through exploded components, raises an exception instead of hanging. This is the single expansion point used by the ExpandBom command and by order auto-expansion.

IRoutingService#

StampOperationsAsync(orderId) → number of stamped lines. It takes the first live (!IsDisabled) Routing record for the order Product, clears Operations, and copies RoutingOperation steps by Sequence, recalculating RunMinutes = RunMinutesPerUnit × qty for the actual order quantity (BaseQuantity). A product without a route leaves Operations empty. That is not an error: a route is not required to release the order.

Technical nuance: the search uses IDictionaryManager.GetRecordsAsync (the expression path), not typed IDictionaryManager<T>. The typed path round-trips the record through JSON and loses Guid columns, so Product/Routing would not match in memory.

IWorkCenterLoadService#

QueuedMinutesAsync(workCenter, onDate, exceptOrder), CapacityAsync(workCenter), ScheduleAsync(orderId), OverloadWarningAsync(orderId) (string?, null means no warning). The queue includes only operations of Released orders without CompletedOn. A marked operation leaves the queue immediately even if the order as a whole is not finished. ScheduleAsync sets ScheduledOn by Sequence and remaining work-center capacity: the first day that fits the capacity, with empty ScheduledOn defaulting to the order DocumentDate.

Capacity on a day is CapacityOnAsync(workCenter, day) (decimal?): one machine's minutes (MachineMinutesOnAsync) × Machines (empty or 0 = 1). One machine's minutes come from the center's ShiftCalendar — the weekday's MondayMinutes … SundayMinutes, overridden by a ShiftCalendarException on that date (one per calendar and date; ShiftCalendarExceptionEventHandler refuses a second) — or, with no calendar, from CapacityMinutesPerDay every day. With a calendar, 0 means the day is CLOSED: ScheduleAsync skips it, and an operation that does not fit one machine's minutes goes to the first open day. OverloadWarningAsync compares each day's load with that day's capacity and also names any operation longer than one machine's shift.

Machines are planned one by one: ScheduleAsync writes ProductionOrderOperations.MachineNo, the first machine with room that day. Other orders' released operations are read per machine; one without a number (an older order) counts against the least loaded machine. The warning also names a machine that overflows on its own while the day's total fits.

ReplanReleasedAsync(fromDate) (command ReplanShop, on Draft and Released) replans all Released orders together: by Priority (descending), RequiredBy (ascending, empty last), DocumentDate, ID; each from max(fromDate, order date); capacity shared only among the orders of the run; confirmed operations keep their day. It returns the late orders (finish after RequiredBy). ProductionOrderEventHandler writes an empty RequiredBy as 1901-01-01, the agreed empty date.

With no calendar, CapacityMinutesPerDay=0 means no ceiling is declared, so OverloadWarningAsync never warns for this center. Overload is a warning, not a refusal. Work-center capacity is a plan, not a physical law: overtime and a second shift are valid. For that reason ReleaseProduction asks for the warning after release; otherwise it would have to decide whether the order is already in the queue.

ILaborCostService#

OrderLaborCostAsync(orderId) → decimal: sum for marked order operations (ActualMinutes × rate). RateAsync(employee, workCenter) gives priority to the named employee's position rate (HR.Position.HourlyRate) over the WorkCenter.HourlyRate; if neither exists, the rate is zero.

ProblemAsync(orderId) → string? (null means posting is allowed) is one predicate, two doors. Negative minutes and fact/route desynchronization are checked here and only here. The same predicate is called by the posting guard (ProductionOrderEventHandler.OnBeforePostAsync) and by every cost estimate, so a negative actual-minute value cannot silently reduce output cost through any path.

IProductionComponentAvailability#

AvailableForOrderAsync(orderId, cell, item) → decimal: how much of the component the order can still take from the consumption cell. It is physical Stock, plus what this same order has already consumed (Finished does not refuse itself after Released), minus ReservedStock of other documents. The order's own draft reserve is not deducted; Mix removes it in the same posting. Used by the posting guard and by ReleaseProduction and FinishProduction before the transition.

Commands#

ExpandBom (“Expand bill of materials”, on Draft) replaces Components with demand from IBomService and stamps Operations through IRoutingService. It refuses if product or quantity is empty, or if the product has no BOM.

ReleaseProduction (“Release to shop”, Draft→Released) requires non-empty Components (hint: click “Expand bill of materials”), positive quantity on every line, and enough component stock at Location (guarded by IProductionComponentAvailability). After the transition it asks IWorkCenterLoadService about work-center overload and shows a warning without blocking.

FinishProduction (“Finish production”, Released→Finished) runs the same availability checks as ReleaseProduction. Components are not “issued” by the previous Released check; the order checks its own consumption cell again here.

ConfirmOperations (“Confirm operations”) closes all still-unmarked (CompletedOn == null) Operations lines by the standard time (SetupMinutes + RunMinutes, calculated when the route was stamped). Repeating the button does not overwrite already marked lines: neither time nor employee. If actual time differs from standard, edit the line manually; subtype Released does not lock the line.

ScheduleProduction (“Schedule operations”) assigns ScheduledOn by Sequence and capacity balance through IWorkCenterLoadService. It is only a plan and does not change the subtype.

Business logic and invariants#

BOM expansion is for a batch and can be multi-level. OutputQty is the output of one recipe run, and line consumption is QtyPer × (Quantity / OutputQty). Explode=true recursively expands the component BOM and merges leaves; Explode=false (default) is one-pass expansion. IBomService owns the formula; handlers do not copy it.

Draft reserves, Released consumes. After the BOM is expanded, the draft reserves components in ReservedStock without touching physical stock. Another document formally sees them as occupied, but can still compete for physical Stock. The reserve is removed automatically by Mix when the order moves to Released, where components are consumed from Stock and WIP is created.

Availability is checked on every transition, not once. Both ReleaseProduction and FinishProduction recheck IProductionComponentAvailability. The order cannot “issue” the same component to itself twice: its own consumption is counted in availability, and other documents' reserves are deducted from the total stock.

The trade profile can disable production completely. OnBeforeSaveAsync and OnBeforePostAsync ask ITradeProfileService.ProductionBlockReasonAsync() (Common). A tenant in the buy-sell (BuySell) profile cannot save or post production orders; the message tells the user to change the profile in common settings.

BOM auto-expansion is a setting, not default behavior. ProductionSettings.AutoExpandBom turns on BOM expansion and route stamping on order creation (OnAfterSaveAsync), only while Components is still empty. A manual component list wins over the setting. Without the flag, the ExpandBom command performs the same action.

Output cost is actual, not average. The costing driver has already consumed components from ItemCostFifo. Negative amounts are exactly what left stock for consumption. On OnAfterPostAsync, the order reads those negative amounts of the same document. It does not recompute an average: average cost diverges from FIFO across batches with different prices, and by the time of the event the balance is already after consumption, not before it. The order adds assigned labor and opens a new ItemCostFifo layer plus an InventoryValue movement for the finished product, symmetric to how ReceiptCostTx/ReceiptFifoTx open the receipt layer in Purchasing. An order with no marked operation has labor cost 0; existing orders from before routings keep their behavior.

Labor without a mark does not exist. The sign that an operation is done is CompletedOn, not non-zero minutes. An instant operation with 0 minutes is valid and is distinguished from an unmarked operation only by the completion date.

Internal operation means no tax. There is no seller or buyer, so the tax engine is not called.

Components and WIP live on the consumption cell; output can go elsewhere. OutputLocation moves only the finished product. If it is empty, the order uses Location, or the module default output cell for a new order.

Example#

BOM Bread (product “Bread”, OutputQty = 10, so one recipe run outputs 10 loaves): Flour QtyPer = 5 kg, Water QtyPer = 3 l, Yeast QtyPer = 0.2 kg; all three have Explode = false. An order for Quantity = 20 loaves gives batches = 20 / 10 = 2 and demand: Flour = 10, Water = 6, Yeast = 0.4. Consumption cell is Cell-1; output cell is empty, so it is also Cell-1. Before the order, Cell-1 has Flour = 50, Water = 100, Yeast = 1.0.

Subtype Action Stock (Flour/Water/Yeast, Cell-1) Stock (Bread, Cell-1) ReservedStock (F/W/Y, Cell-1) WorkInProgress (Bread, Cell-1)
— before order 50 / 100 / 1.0 0 0 / 0 / 0 0
Draft ExpandBom expands Components; ProductionDraftReserveTx reserves 50 / 100 / 1.0 (unchanged) 0 10 / 6 / 0.4 0
Released ReleaseProduction: reserve removed by Mix, ProductionConsumeTx consumes, ProductionWipTx creates WIP 40 / 94 / 0.6 0 0 / 0 / 0 20
Finished FinishProduction: ProductionConsumeTx again with the same list, not twice; ProductionOutputTx receives output; Mix removes WIP 40 / 94 / 0.6 (unchanged from Released) 20 0 / 0 / 0 0

Output cost (OnAfterPostAsync on Finished) with consumption prices Flour = 2.00/kg, Water = 0.50/l, Yeast = 25.00/kg. These are the costs that ItemCostFifo already wrote off through the costing driver for −10/−6/−0.4.

Component Calculation Amount
Materials (from ItemCostFifo, negative amounts of this document) 10×2.00 + 6×0.50 + 0.4×25.00 33.00
Labor (ILaborCostService.OrderLaborCostAsync, 1 marked operation: SetupMinutes=10 + RunMinutes=2×20=40, work-center rate 60.00/hour = 1.00/min) 50 min × 1.00 50.00
New ItemCostFifo/InventoryValue layer for Bread Qty=20 83.00 (4.15/pc)

General-ledger reflection is the order's own: ProductionOrderEventHandler writes Dr WIP / Cr Inventory on Released (the cost CostingIssue took off the layers) and Dr Inventory / Cr LaborAbsorbed on Finished. The Released leg leaves with the flip to Finished; materials out and product in are neutral on the one inventory account. See Who writes the general ledger.

The Production group → Dictionaries (BillOfMaterials, Routing, WorkCenter) · Documents (ProductionOrder) · Registers (WorkInProgress) · Settings (ProductionSettings).

  • Outgoing: Inventory (Stock, ReservedStock, IStoreCellService/IItemQuantityConverter), Costing (ItemCostFifo, InventoryValue), HR (Employee, position rate in ILaborCostService), Common (ITradeProfileService).
  • Ledger: Accounting (IGeneralLedgerService) and Costing (IInventoryCostGLService) — the order's WIP and labour legs.

Objects#

Generated from workspace metadata by node tools/gen-reference.mjs; edit the metadata, not this block.

Dictionaries#

BillOfMaterials — Bills of materials

Field Caption Type Required
ID Code String(50) Yes
Name Name String(128) Yes
OutputQty Output qty Qty Yes
Product Product → Item Yes

BomComponent — BOM components

Field Caption Type Required
Bom Bill of materials → BillOfMaterials Yes
Component Component → Item Yes
Explode Explode Boolean No
ID Code String(50) Yes
QtyPer Qty per unit Qty Yes
Unit Qty unit → UnitOfMeasure No

ProductionSettings — Production settings

Field Caption Type Required
AutoExpandBom Auto-expand BOM Boolean No
DefaultOutputLocation Default output location → StoreCell No
DefaultOutputLocationCode Default output location code String(128) No
ID ID String(50) Yes

Routing — Routings

Operation recipe for a product. First live row wins, same as BOM. Operations are the detail list.

Field Caption Type Required
ID Code String(50) Yes
IsDisabled Disabled Boolean No
Name Name String(160) Yes
Product Product → Item Yes

RoutingOperation — Routing operations

One step of a routing. Sequence is unique per routing.

Field Caption Type Required
ID Code String(50) Yes
Name Name String(160) Yes
Routing Routing → Routing Yes
RunMinutesPerUnit Run minutes / unit Decimal(18,4) No
Sequence Sequence Integer Yes
SetupMinutes Setup minutes Integer No
WorkCenter Work center → WorkCenter No

ShiftCalendar — Shift calendars

A working week for work centers: minutes per weekday for one machine. Dated overrides (holidays, overtime days) are in ShiftCalendarException. A work center with a calendar takes its daily capacity from it times its number of machines; without one, CapacityMinutesPerDay applies every day.

Field Caption Type Required
FridayMinutes Friday, min Integer No
ID Code String(50) Yes
MondayMinutes Monday, min Integer No
Name Name String(256) Yes
SaturdayMinutes Saturday, min Integer No
SundayMinutes Sunday, min Integer No
ThursdayMinutes Thursday, min Integer No
TuesdayMinutes Tuesday, min Integer No
WednesdayMinutes Wednesday, min Integer No

ShiftCalendarException — Calendar exceptions

A dated override of a shift calendar: a holiday (0 minutes) or a different working day (overtime Saturday, short pre-holiday shift). One per calendar and date; it wins over the weekday.

Field Caption Type Required
Date Date DateTime Yes
ID Code String(50) Yes
Minutes Minutes Integer No
Note Note String(256) No
ShiftCalendar Shift calendar → ShiftCalendar Yes

WorkCenter — Work centers

Where an operation runs. v1 is a name on the route; no capacity.

Field Caption Type Required
CapacityMinutesPerDay Capacity, min/day Decimal(18,4) No
HourlyRate Hourly rate Decimal(18,4) No
ID ID String(50) Yes
IsDisabled Disabled Boolean No
Machines Machines Integer No
Name Name String(160) Yes
ShiftCalendar Shift calendar → ShiftCalendar No

Documents#

ProductionOrder — Production order

Header fields:

Field Caption Type Required
BaseQuantity Qty in base unit Qty No
ID Number String(50) Yes
Location Location → StoreCell Yes
OutputLocation Output cell → StoreCell No
Priority Priority Integer No
Product Product → Item Yes
Quantity Quantity Qty Yes
RequiredBy Need by DateTime No
Unit Unit → UnitOfMeasure No

Subtypes:

Subtype Caption
Draft Draft
Released Released
Finished Finished

Commands:

Command Caption Target subtype
ReleaseProduction Release to shop —
ConfirmOperations Confirm operations —
ExpandBom Expand bill of materials —
FinishProduction Finish production —
ScheduleProduction Schedule operations —
ReplanShop Replan shop —

Registers#

WorkInProgress — Work in progress

Resources:

Resource Caption Type Required
Qty Qty Qty —

Analytics:

Analytic Caption Type Required
WorkInProgress-Cell WorkInProgress-Cell — Yes
WorkInProgress-Item WorkInProgress-Item — Yes

Table parts#

ProductionOrderComponents — Components

Used by: ProductionOrder.

Field Caption Type Required
BaseQuantity Qty in base unit Qty No
Component Component → Item Yes
QtyRequired Qty required Qty Yes
Unit Unit → UnitOfMeasure No

ProductionOrderOperations — Operations

Used by: ProductionOrder.

Field Caption Type Required
ActualMinutes Actual minutes Decimal(18,4) No
CompletedOn Completed on DateTime No
Employee Operator → Employee No
MachineNo Machine Integer No
Name Name String(160) Yes
RunMinutes Run minutes Decimal(18,4) No
ScheduledOn Scheduled on DateTime No
Sequence Sequence Integer Yes
SetupMinutes Setup minutes Integer No
WorkCenter Work center → WorkCenter No
Was this page helpful?