Status: Accepted, stated in the project's written engineering rules. This record covers the rule that a document line, a stock row, a bill of materials, a reservation, or a movement never names a bare product: it names the product, one of its models, and one of that model's versions. The shared helpers live in src/cluster_erp/core/product_breakdown.py; the stock side is explained on product breakdown and stock conversion.
Context¶
Stock is kept per product version, so a reference to a product alone is ambiguous. The project's written engineering rules make the three-level breakdown mandatory across the whole ERP and forbid a silent fallback of the form "use the product when there is no model or version". The helper module states the intent: the ERP is "deterministic about what a product reference means" (src/cluster_erp/core/product_breakdown.py:1-9). Further rationale is not determined from code.
Decision¶
- If a line has a product, it must have a model and a version; if it has no product, it has neither (
src/cluster_erp/core/product_breakdown.py:94-118). - The model must belong to the product and the version to the model. Because that check needs the products module, it is a
ValidateProductBreakdownquery over the EventBus (src/cluster_erp/core/product_breakdown.py:120-147,src/cluster_erp/core/contracts.py:218-226). - Forms offer a dependent picker: the model list is filtered by the chosen product and the version list by the chosen model (
src/cluster_erp/core/product_breakdown.py:31-83).
The sequence diagram shows the check on a document line.
sequenceDiagram participant Svc as Module service participant Core as ensure_product_breakdown participant Bus as EventBus participant PS as products_and_services Svc->>Core: product_id, product_model_id, product_version_id Core->>Core: all empty, or all three present? Core->>Bus: query ValidateProductBreakdown Bus->>PS: _handle_validate_breakdown PS-->>Core: ok, or an error code Core-->>Svc: return, or raise AppError 422
In prose: the service calls ensure_product_breakdown. A model or version without a product, or a product without both, raises AppError with HTTP 422 at once. Otherwise the helper sends the query; the products module answers with ok or with PRODUCT_MODEL_PRODUCT_MISMATCH or PRODUCT_VERSION_MODEL_MISMATCH, and a mismatch raises 422 (src/cluster_erp/core/product_breakdown.py:103-147, src/cluster_erp/modules/products_and_services/module.py:131).
Consequences¶
- Stock levels, goods movements, usage decisions, reservations, and project stock loans declare
product_model_idandproduct_version_idas non-null columns (src/cluster_erp/modules/inventory/stock/models.py:54-63,src/cluster_erp/modules/inventory/stock/models.py:144-153,src/cluster_erp/modules/inventory/stock/models.py:289-297,src/cluster_erp/modules/inventory/stock/models.py:355-364,src/cluster_erp/modules/inventory/stock/models.py:445-453). - The live-row unique index of stock levels includes the model and the version (
src/cluster_erp/modules/inventory/stock/models.py:28-46). - 49 service calls in 23 files under
src/cluster_erp/modulesrun the check, across BOM, inventory, procurement, production, projects, quotations, sales orders, nesting, and fleet (counted withgrep -rn "ensure_product_breakdown("). - Flows that create documents without a person choosing, such as MRP, ask for the product's default model and version through
GetProductDefaultBreakdownand record them explicitly (src/cluster_erp/core/contracts.py:229-235). The default is written on the document; it is not a silent fallback at read time.
Where the code enforces it¶
| Mechanism | What it checks | Source |
|---|---|---|
ensure_product_breakdown |
Presence and consistency of the three ids on every write | src/cluster_erp/core/product_breakdown.py:86-147 |
| Non-null breakdown columns | Stock rows cannot be stored without model and version | src/cluster_erp/modules/inventory/stock/models.py:54-63 |
_e2e_product_model_versions.mjs |
The product form, the model card, and the version list follow the chain in the browser | web-client/_e2e_product_model_versions.mjs:1-4 |
Not determined from code: a pytest test that calls ensure_product_breakdown directly. None was found in tests/ (searched for product_breakdown).
Related pages¶
- Decision records
- Product breakdown and stock conversion
- Products and services module (products_and_services)
- Inventory module (inventory)
Verified against commit 92f8051 on 2026-11-13.