The products_and_services module in src/cluster_erp/modules/products_and_services/ is the material master. It holds products and services with their planning, stock-size, and costing fields, the mandatory product, model, and version breakdown, a category tree, vendor sources per breakdown, and the units of measure. Every other module that names a product asks this module over the EventBus. The shared breakdown rule is explained on product breakdown and stock conversion.
At a glance¶
| Item | Value | Source |
|---|---|---|
| Package | src/cluster_erp/modules/products_and_services/ |
folder |
| Module class | Module |
src/cluster_erp/modules/products_and_services/module.py:18 |
name / version |
products_and_services / 0.1.0 |
src/cluster_erp/modules/products_and_services/module.py:19-25 |
| API prefix | /api/v1/products_and_services (ModuleBase.api_prefix) |
src/cluster_erp/core/module_base.py:37-38 |
| Sub-modules | 6: products, product_models, product_versions, categories, vendors, settings |
folder listing |
| Tables | 9 (__tablename__ count in the package) |
src/cluster_erp/modules/products_and_services/models.py |
| Entities in metadata | 7: product, product_model, product_version, product_vendor, category, unit_category, unit |
src/cluster_erp/modules/products_and_services/module.py:998-1006 |
| Routes | 55 route decorators in the package | grep for @router. |
| Web app presence | Catalogue app products |
src/cluster_erp/modules/app_store/catalog.json (entry products) |
| Desktop presence | Generic widgets only; no folder in src/cluster_erp/desktop/modules/ |
folder |
What the module does¶
- Keeps products and services with a material type, four capability flags, a status, units, physical data, stock-size and make-or-buy data, planning parameters, costing fields, and trade data (
src/cluster_erp/modules/products_and_services/products/models.py:20-351). - Gives every product at least one model and every model at least one version, so that stock and documents can always post against a full breakdown (
src/cluster_erp/modules/products_and_services/product_versions/models.py:16-19). - Links a product to several categories of a category tree (
src/cluster_erp/modules/products_and_services/products/models.py:395-417,src/cluster_erp/modules/products_and_services/categories/models.py:11-30). - Records vendor sources per product, model, version, and vendor party (
src/cluster_erp/modules/products_and_services/vendors/models.py:14-63). - Holds unit categories and units with conversion factors and seeds a default set at start-up (
src/cluster_erp/modules/products_and_services/module.py:31-50).
Sub-modules¶
| Sub-module | Purpose | Main entities | Router | Source |
|---|---|---|---|---|
products |
Products and services, category links, service facet, photo | Product, ProductServiceAttributes, ProductCategoryLink |
/products |
src/cluster_erp/modules/products_and_services/products/routes.py:32 |
product_models |
Models of a product, photo | ProductModel |
/models |
src/cluster_erp/modules/products_and_services/product_models/routes.py:25 |
product_versions |
Versions of a model | ProductVersion |
/versions |
src/cluster_erp/modules/products_and_services/product_versions/routes.py:22 |
categories |
Category tree | ProductCategory |
/categories |
src/cluster_erp/modules/products_and_services/categories/routes.py:22 |
vendors |
Vendor sources | ProductVendor |
/vendors |
src/cluster_erp/modules/products_and_services/vendors/routes.py:24 |
settings |
Unit categories and units | UnitCategory, UnitOfMeasure |
/settings |
src/cluster_erp/modules/products_and_services/settings/routes.py:29 |
Router paths are relative to the module prefix; all six routers sit under one module router that depends on the app entitlement (src/cluster_erp/modules/products_and_services/module.py:75-83).
Data model¶
The product, model, and version breakdown¶
The diagram shows the breakdown tables and the rows that point at them.
erDiagram
Product ||--|{ ProductModel : "product_id"
ProductModel ||--|{ ProductVersion : "product_model_id"
Product ||--o{ ProductCategoryLink : "product_id"
ProductCategory ||--o{ ProductCategoryLink : "category_id"
Product ||--o| ProductServiceAttributes : "product_id"
ProductVersion ||--o{ ProductVendor : "product_version_id"
UnitCategory ||--|{ UnitOfMeasure : "category_id"
UnitOfMeasure ||--o{ Product : "base_unit_id"In prose: a product has one or more models and a model has one or more versions. Exactly one live model per product and exactly one live version per model carry is_default, enforced by partial unique indexes (src/cluster_erp/modules/products_and_services/product_models/models.py:32-38, src/cluster_erp/modules/products_and_services/product_versions/models.py:35-41). A version name is unique within its model (src/cluster_erp/modules/products_and_services/product_versions/models.py:42-49). A product has category links and, when it is a service, one service facet row. A vendor source names a product, a model, a version, and a vendor party (src/cluster_erp/modules/products_and_services/vendors/models.py:16-49). Units belong to a unit category, and a product points at its base, purchase, sales, weight, volume, and dimension units (src/cluster_erp/modules/products_and_services/products/models.py:103-138).
Entities¶
| Entity | Table | Key fields | Notes | Source |
|---|---|---|---|---|
Product |
products_products |
code, name, sku, barcode, material_type, category_id, is_purchasable, is_sellable, is_manufacturable, is_stockable, status, stock_form, sourcing_preference, standard_cost, extra |
sku and barcode unique among live rows |
src/cluster_erp/modules/products_and_services/products/models.py:20-351 |
ProductServiceAttributes |
products_service_attributes |
product_id, billing_method, rate_basis, standard_rate, currency_code, duration_value, is_billable |
one live row per product | src/cluster_erp/modules/products_and_services/products/models.py:353-392 |
ProductCategoryLink |
products_product_category_links |
product_id, category_id |
unique pair among live rows | src/cluster_erp/modules/products_and_services/products/models.py:395-417 |
ProductModel |
products_models |
code, product_id, name, sku, barcode, attributes, is_default, photo_attachment_id |
one live default per product | src/cluster_erp/modules/products_and_services/product_models/models.py:17-66 |
ProductVersion |
products_model_versions |
code, product_model_id, name, sku, barcode, attributes, is_default |
one live default per model | src/cluster_erp/modules/products_and_services/product_versions/models.py:16-63 |
ProductCategory |
products_categories |
code, name, parent_id, sort_order |
self-referencing tree | src/cluster_erp/modules/products_and_services/categories/models.py:11-30 |
ProductVendor |
products_product_vendors |
product_id, product_model_id, product_version_id, party_id, vendor_part_number, lead_time_days, unit_price, currency, is_preferred, valid_from, valid_to |
unique per breakdown and party among live rows | src/cluster_erp/modules/products_and_services/vendors/models.py:14-63 |
UnitCategory |
products_unit_categories |
code, name, translations |
code and name unique among live rows | src/cluster_erp/modules/products_and_services/settings/models.py:13-39 |
UnitOfMeasure |
products_units_of_measure |
code, category_id, name, symbol, is_base_unit, conversion_factor, sort_order |
symbol unique among live rows; factor Numeric(24, 6) |
src/cluster_erp/modules/products_and_services/settings/models.py:42-79 |
Money and quantity columns are Numeric; costs and vendor prices use Numeric(18, 4) (src/cluster_erp/modules/products_and_services/products/models.py:313-314, src/cluster_erp/modules/products_and_services/vendors/models.py:56). Decimal values are serialised as strings (src/cluster_erp/modules/products_and_services/products/schemas.py:149-151, src/cluster_erp/modules/products_and_services/products/schemas.py:478-481). See money and Decimal.
Screens and metadata¶
get_metadata declares a sidebar group Products & Services (Turkish Ürünler ve Hizmetler) with three sections: Products (a list), Categories (a tree), and Settings with the tabs unit_category and unit (src/cluster_erp/modules/products_and_services/module.py:954-997). The product detail has the tabs General, Service, Physical Properties, Inventory & Planning, Production & Costing, Vendors, Models, Bill of Materials, and Stock (src/cluster_erp/modules/products_and_services/products/metadata.py:222-295). The Service tab shows only for the material type service, and the physical, inventory, production, and stock tabs hide for it. The Bill of Materials tab shows only for manufacturable products and reads the bom module's list, and Stock reads the inventory module's list (src/cluster_erp/modules/products_and_services/products/metadata.py:230-294). The create form collects the first model's name, SKU, and barcode and the first version's name (src/cluster_erp/modules/products_and_services/products/metadata.py:381-424). Products and models have photos (src/cluster_erp/modules/products_and_services/products/metadata.py:118, src/cluster_erp/modules/products_and_services/products/metadata.py:1328). The category tree uses GET /categories/tree (src/cluster_erp/modules/products_and_services/categories/metadata.py:26-32).
Status and actions¶
Product statuses are draft, active, blocked, and obsolete (src/cluster_erp/modules/products_and_services/products/schemas.py:24-28). The status is a form field, and a change emits ProductStatusChanged (src/cluster_erp/modules/products_and_services/products/services.py:590-598). The module declares one ActionMeta.
| Action | Label | Condition | Endpoint | Permission | Source |
|---|---|---|---|---|---|
seed_defaults (unit category) |
Seed Defaults | none (global) | POST /settings/unit-categories/seed-defaults |
unit_category.seed |
src/cluster_erp/modules/products_and_services/settings/metadata.py:29-37, src/cluster_erp/modules/products_and_services/settings/routes.py:84-91 |
Business rules¶
- Each material type has default capability flags, used when the payload leaves a flag out (
src/cluster_erp/modules/products_and_services/products/schemas.py:34-89,src/cluster_erp/modules/products_and_services/products/services.py:296-312). - Hard constraints: a service is neither stockable nor manufacturable, a consumable is not stockable, and a raw material is not manufacturable; a violation answers
PRODUCT_CAPABILITY_VIOLATION(src/cluster_erp/modules/products_and_services/products/schemas.py:91-95,src/cluster_erp/modules/products_and_services/products/services.py:671-683). - A product that is not stockable has its batch, serial, shelf-life, safety-stock, and reorder-point fields cleared (
src/cluster_erp/modules/products_and_services/products/services.py:48-55,src/cluster_erp/modules/products_and_services/products/services.py:316-318). - SKU and barcode must be free among live products (
src/cluster_erp/modules/products_and_services/products/services.py:272-292). - Creating a product creates its default model, named after the product when no name is given, and the model creates its default version, named
1by default (src/cluster_erp/modules/products_and_services/products/services.py:376-395,src/cluster_erp/modules/products_and_services/product_models/services.py:103-112,src/cluster_erp/modules/products_and_services/product_models/schemas.py:22). - The first model of a product and the first version of a model become the default; a new default takes the flag from the old one (
src/cluster_erp/modules/products_and_services/product_models/services.py:78-99,src/cluster_erp/modules/products_and_services/product_versions/services.py:72-90). - The default model or version cannot be demoted, deactivated, or deleted; the answer is
PRODUCT_MODEL_DEFAULT_PROTECTEDorPRODUCT_VERSION_DEFAULT_PROTECTEDwith HTTP 409 (src/cluster_erp/modules/products_and_services/product_models/services.py:115-160,src/cluster_erp/modules/products_and_services/product_versions/services.py:92-140). - A product that is a live component of an active BOM version cannot be deleted; the module asks the
bommodule withGetPartUsageCountsand answersPRODUCT_IN_ACTIVE_BOM(src/cluster_erp/modules/products_and_services/products/services.py:604-626). - Deleting a product also soft-deletes its category links and service facet with the same timestamp; reactivating it restores them (
src/cluster_erp/modules/products_and_services/products/services.py:628-660,src/cluster_erp/modules/products_and_services/module.py:165-236). - Every category named on a product must be live; the first one becomes the denormalised
category_id(src/cluster_erp/modules/products_and_services/products/services.py:151-206). - A vendor source checks that the product and the party exist, validates the breakdown, and refuses a second live source for the same breakdown and party with HTTP 409 (
src/cluster_erp/modules/products_and_services/vendors/services.py:94-143). - Unit conversion works only within one unit category: value times the source factor, divided by the target factor (
src/cluster_erp/modules/products_and_services/settings/services.py:237-247). The seed has 13 unit categories (src/cluster_erp/modules/products_and_services/settings/seed_data.py, entries with acodekey). - Codes come from the yearly sequences
product,product_model,product_version,product_category, andunit_of_measure(src/cluster_erp/modules/products_and_services/products/services.py:325,src/cluster_erp/modules/products_and_services/product_models/services.py:79,src/cluster_erp/modules/products_and_services/product_versions/services.py:73).
Events and queries¶
The module consumes 19 contracts and produces 8, counted as on the parties module page. The full list of producers and consumers is on the contracts reference.
| Direction | Contract class | Handler or emitter | Source |
|---|---|---|---|
| emits | ProductCreated, ServiceCreated |
ProductService.create |
src/cluster_erp/modules/products_and_services/products/services.py:407-424 |
| emits | ProductUpdated, ProductStatusChanged |
ProductService.update |
src/cluster_erp/modules/products_and_services/products/services.py:582-598 |
| emits | ProductDeleted |
ProductService.delete |
src/cluster_erp/modules/products_and_services/products/services.py:652-659 |
| queries | GetPartUsageCounts, GetProductById, GetPartyById |
product delete; vendor create | src/cluster_erp/modules/products_and_services/products/services.py:609-616, src/cluster_erp/modules/products_and_services/vendors/services.py:96-111 |
| answers query | ValidateProductBreakdown, GetProductDefaultBreakdown, GetBulkProductDefaultBreakdowns |
_handle_validate_breakdown, _handle_default_breakdown, _handle_bulk_default_breakdowns |
src/cluster_erp/modules/products_and_services/module.py:131-138, src/cluster_erp/modules/products_and_services/module.py:759-916 |
| answers query | GetProductById |
_handle_get_product |
src/cluster_erp/modules/products_and_services/module.py:108, src/cluster_erp/modules/products_and_services/module.py:238-317 |
| answers query | ConvertQuantity, GetUnitOfMeasure, FindUnitOfMeasure |
unit handlers | src/cluster_erp/modules/products_and_services/module.py:109-115 |
| answers query | IsProductManufacturable, IsProductPurchasable, IsProductSellable, GetProductsByMaterialType |
flag handlers | src/cluster_erp/modules/products_and_services/module.py:116-119 |
| answers query | GetProductVendors, GetBulkProductVendors, GetProductPlanningData, SetProductPlanningClassification, ApplyProductBufferZones |
planning handlers | src/cluster_erp/modules/products_and_services/module.py:120-130 |
| answers query | CreateScaffoldIntermediate |
_handle_create_scaffold_intermediate |
src/cluster_erp/modules/products_and_services/module.py:133-135, src/cluster_erp/modules/products_and_services/module.py:806-875 |
| subscribes | RecordActivated, DescribeAnalyticsFacts |
_on_record_activated, describe_analytics_facts |
src/cluster_erp/modules/products_and_services/module.py:107, src/cluster_erp/modules/products_and_services/module.py:142-147 |
CreateScaffoldIntermediate creates, or reuses by SKU, a stockable semi-finished product that is not manufacturable, for the output of a routing operation (src/cluster_erp/modules/products_and_services/module.py:806-875). It is used by the routing scaffold described on the BOM module page.
Permissions¶
| Permission key | Used by | Source |
|---|---|---|
product.read, .create, .update, .delete, .bulk_delete |
product and photo routes | src/cluster_erp/modules/products_and_services/products/routes.py:55-266 |
product_model.read, .create, .update, .delete, .bulk_delete |
model and photo routes | src/cluster_erp/modules/products_and_services/product_models/routes.py:32-199 |
product_version.read, .create, .update, .delete, .bulk_delete |
version routes | src/cluster_erp/modules/products_and_services/product_versions/routes.py:29-101 |
category.read, .create, .update, .delete, .bulk_delete |
category routes | src/cluster_erp/modules/products_and_services/categories/routes.py:28-97 |
product_vendor.read, .create, .update, .delete, .bulk_delete |
vendor routes | src/cluster_erp/modules/products_and_services/vendors/routes.py:34-127 |
unit_category.*, unit.* |
unit routes | src/cluster_erp/modules/products_and_services/settings/routes.py:59-265 |
Privacy, retention, and history¶
The module stores no personal data of its own and has no privacy_handlers.py. A vendor source keeps the vendor party's display name in vendor_name_cache (src/cluster_erp/modules/products_and_services/vendors/models.py:51, src/cluster_erp/modules/products_and_services/vendors/services.py:145-148). Changes are captured by change history.
Analytics facts¶
analytics_facts.py declares one fact set, product, over products_products, with the dimensions material type, status, stock form, sourcing preference, ABC indicator, active, and created date, and the measures product_count and average_lead_time_days (src/cluster_erp/modules/products_and_services/analytics_facts.py:21-87).
Settings and reference data¶
Unit categories and units are reference data in the database. At every start the module seeds the default set from UNIT_SEED_DATA (src/cluster_erp/modules/products_and_services/module.py:37-50, src/cluster_erp/modules/products_and_services/settings/services.py:86-150). The Seed Defaults action runs the same seeding on demand.
Tests¶
Found by a grep for modules.products_and_services in tests/ and by file name in web-client/.
| Kind | File | Covers |
|---|---|---|
| pytest | tests/test_bom_bugfix_batch_2026_10_08.py |
product metadata, unit seed data, and unit pickers used by the BOM forms (10 tests) |
| e2e | web-client/_e2e_products.mjs |
the products app: list, category tree, detail, settings |
| e2e | web-client/_e2e_products_service.mjs |
the service facet, multiple categories, and the type-aware form |
| e2e | web-client/_e2e_product_model_versions.mjs |
the product, model, and version chain in the web app |
| e2e | web-client/_e2e_make_or_buy_stock.mjs |
stock size, make-or-buy, and the stock material breakdown |
Discrepancies¶
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| product | Product |
An item or service of the material master. | Ürün | src/cluster_erp/modules/products_and_services/products/models.py:20 |
| product model | ProductModel |
A variant of a product; every product has a default one. | Ürün modeli | src/cluster_erp/modules/products_and_services/product_models/models.py:17 |
| product version | ProductVersion |
A version of a model; stock is kept per product, model, and version. | Ürün versiyonu | src/cluster_erp/modules/products_and_services/product_versions/models.py:16 |
| material type | material_type |
The kind of product, which sets default capability flags. | Malzeme türü | src/cluster_erp/modules/products_and_services/products/schemas.py:12-21 |
| vendor source | ProductVendor |
A vendor party that supplies one product breakdown, with price and lead time. | Tedarikçi kaynağı | src/cluster_erp/modules/products_and_services/vendors/models.py:14 |
| unit of measure | UnitOfMeasure |
A unit with a conversion factor inside its unit category. | Ölçü birimi | src/cluster_erp/modules/products_and_services/settings/models.py:42 |
Related pages¶
- Modules index
- Product breakdown and stock conversion
- Decision record on the product, model, and version breakdown
- BOM module
- Inventory module
- API reference for this module
- Related material, historical, not verified: the in-tree note
src/cluster_erp/modules/products_and_services/docs/how-to.md. It does not mention product versions and uses a fielddefault_unit_id, which the product model does not have (src/cluster_erp/modules/products_and_services/products/models.py:103-116).
Verified against commit 92f8051 on 2026-11-13.