# The brick's process: module.yaml's workflows, copied (pattern.md §15), spec v7. Served as BPMN 2.0 at GET /process/bpmn.
# A transition's key is frozen once signed: it is the BPMN id. tests/test_process compares this file to the spec.
id: product
name: Products & price lists
version: 7
actors:
- id: catalogue_manager
name: Whoever keeps the product list (operations, the owner)
does: adds products with their type, sets their unit, category, tax category and billing type, classifies them, archives what is no longer sold
sees: every product
permissions:
- product:product:read
- product:product:write
- product:product:archive
- product:category:write
- product:class:write
- product:settings:write
- id: planner
name: Whoever plans stock per site (the buyer, the warehouse manager)
does: sets per product and site whether it is bought or transferred, its lead times, reorder point, safety stock, min / max and lot size, and blocks a product at one site
sees: every product and its site data; never cost
permissions:
- product:product:read
- product:site:write
- id: pricing_manager
name: Whoever sets prices (the owner, a commercial manager)
does: keeps price lists and their rules, schedules price changes, assigns customers to price lists
sees: every product and price
permissions:
- product:product:read
- product:price_list:read
- product:price_list:write
- product:price_list:publish
- product:customer_price_list:write
- product:settings:write
- id: owner
name: The owner or a finance controller
does: sets the controls on pricing (whether publishing needs a second person, how far a price may move before it warns)
sees: every product and price
permissions:
- product:product:read
- product:price_list:read
- product:settings:controls
- id: sales_rep
name: Sales
does: looks up a product and the price a customer pays
sees: active products and the prices of published price lists; never cost
permissions:
- product:product:read
- product:price_list:read
- id: system
name: The system
does: resolves prices on read, checks for duplicates, suggests a new product's details, flags unusual prices, imports a catalogue
sees: everything, through its own service account
permissions:
- product:product:read
- product:price_list:read
- product:catalogue:import
- product:product:suggest
- product:price_list:check
- product:migration:read
- product:unit:mark_used
workflows:
- entity: product
states:
- draft
- active
- archived
terminal:
- archived
transitions:
- key: add_product
name: Add the product
from: none
to: draft
by: catalogue_manager
gate: the code is not in use; a product type is named, mandatory on every door (v5, gate type_named)
effect: the code is fixed from now on (typed, or claimed from the type's series); kind and stocked_default taken from the type
- key: change_product_type
name: Change the product type
from: draft
to: draft
by: catalogue_manager
requires: reason
gate: 'readiness: type_unused (no document or stock has used any unit of the product)'
effect: 'v5: kind, stocked_default, sellable, valuation_class and the required unit roles are COPIED again from the new type; the code is kept; from LEGACY it is allowed on an active, used product once, when the new type''s kind and stocked_default equal the product''s own (no change of behaviour)'
- key: ai_check_duplicate
name: AI checks for a likely duplicate (suggest)
from: draft
to: draft
by: system
gate: the duplicate check is on
effect: a likely duplicate is shown; nothing is merged
- key: ai_suggest_details
name: AI suggests category, unit and tax category (suggest)
from: draft
to: draft
by: system
gate: the field is empty and the step is on
effect: a suggestion the catalogue manager accepts or changes
- key: generate_variants
name: Generate the variants of a template
from: none
to: draft
by: catalogue_manager
gate: a template with at least one attribute and its values
effect: one draft product per missing combination; existing ones untouched
- key: set_units
name: Set the base unit and the other units
from: draft
to: draft
by: catalogue_manager
gate: every unit converts exactly to the base unit; every role names one of the product's units
effect: the roles chosen (sales, purchase, issue, production, warehouse, delivery); packaging levels, their GTINs (one main per unit), weights, volumes and dimensions
- key: complete_product
name: Complete unit, category, tax category and billing
from: draft
to: draft
by: catalogue_manager
- key: classify_product
name: Classify the product
from: draft
to: draft
by: catalogue_manager
gate: each value allowed by its characteristic (list, number with a unit of the right kind, date)
effect: class assignments and dated characteristic values; searchable by them
- key: activate_product
name: Activate the product
from: draft
to: active
by: catalogue_manager
gate: 'readiness: units_complete, type_requirements (the roles and fields its type asks for, v3), class_complete (every required characteristic of its classes, v3); tax category, billing type and invoicing policy set'
effect: offered to sales (where its type is sellable) and to purchasing (where purchasable)
- key: add_unit
name: Add a unit or a pack size
from: active
to: active
by: catalogue_manager
gate: a new unit code with an exact conversion; a conversion already used is never changed
effect: the new unit can be bought, sold or issued from now
- key: add_gtin
name: Add or retire a barcode on a unit
from: active
to: active
by: catalogue_manager
gate: a valid GS1 check digit; the GTIN on no other product or unit, retired ones included; on a template only while it has one product
effect: one main GTIN per unit, the others additional, on the SKU (or a one-product template); a retired GTIN still names its unit on old documents
- key: set_measures
name: Record or correct weights
volume and dimensions: null
from: active
to: active
by: catalogue_manager
gate: net weight not above gross; each measure's unit of the right kind (mass, volume, length); valid_from today or later (v3); a row for today is replaced only while no document has copied it
effect: 'a dated prod_unit_measure row per one of the unit, on the template (inherited) or the SKU (its own row wins); never frozen by use (v3): documents keep the copy they took'
- key: set_rounding
name: Set a role's unit
minimum and multiple: null
from: active
to: active
by: catalogue_manager
gate: the role's unit or another unit of the product; starts after today
effect: a dated unit_role row on the template (inherited) or the SKU (its own row wins); purchasing and sales-order gate on it from its valid_from; a used row is never edited
- key: reclassify_product
name: Change a classification value
from: active
to: active
by: catalogue_manager
gate: the value allowed; valid_from today or later
effect: a new dated value row; search answers by the value in force on the date asked
- key: set_site_data
name: Set the product's data for a site
from: active
to: active
by: planner
gate: 'readiness: site_row_after_today, site_quantities_consistent (safety stock <= reorder point <= max), supplier_site_valid (a transfer names another site of the same company), manufacturing_installed (v6: procurement make and planning mrp only when manufacturing is in the composition; was not_reserved_m7)'
effect: a dated prod_site row; inventory plans from it and sales-order and purchasing read its site status from its valid_from
- key: rename_product
name: Change name or description
from: active
to: active
by: catalogue_manager
effect: documents already raised keep their copy
- key: archive_product
name: Archive the product
from: active
to: archived
by: catalogue_manager
effect: no new lines; existing documents unchanged
- key: reactivate_product
name: Reactivate the product
from: archived
to: active
by: catalogue_manager
requires: reason
note: 'a product is never deleted once a document names it. Every per-unit row (conversion, GTIN, measures, role, rounding) is set on the template or the SKU and the SKU''s own row wins; a conversion, packaging level, decimals and GTIN are frozen once a document or stock uses them, an inherited template row included; measures, roles and rounding are dated rows instead (v3: measures are no longer frozen, a correction is a new row from today)'
readiness:
- transition: activate_product
gate: units_complete
reason: no base or sales unit, or a role names a unit the product does not have
fix: 'Products: units'
- transition: add_gtin
gate: gtin_check_digit
reason: the last digit is not the GS1 check digit of the others
fix: re-scan or re-type the barcode
- transition: add_gtin
gate: gtin_unique
reason: the GTIN already names another product or unit (retired GTINs included)
fix: check the barcode; a GTIN is never reused
- transition: set_measures
gate: net_not_above_gross
reason: the net weight is above the gross weight
fix: correct one of them
- transition: set_measures
gate: measure_unit_kind
reason: the weight unit is not a mass, the volume unit not a volume, or the dimension unit not a length
fix: 'pick a unit of that kind (Settings: units)'
- transition: set_measures
gate: measure_not_backdated
reason: a measure starts today at the earliest (in the business time zone)
fix: date it today or later
- transition: set_measures
gate: today_row_uncopied
reason: a document already copied today's measures for this unit
fix: date the correction tomorrow
- transition: add_product
gate: type_named
reason: no product type was named
fix: name one (TRADING, FINISHED, RAW, SERVICE, NONSTOCK, PACKAGING or your own); LEGACY only for a product that already existed
- transition: change_product_type
gate: type_unused
reason: a document or stock already used this product
fix: make a new product of the other type and archive this one
- transition: activate_product
gate: type_requirements
reason: the product's type asks for a role or field that is not set (e.g. TRADING needs a purchase unit)
fix: 'Products: units, or change the type while it is a draft'
- transition: activate_product
gate: class_complete
reason: a required characteristic of one of its classes has no value
fix: 'Products: classification'
- transition: set_site_data
gate: site_row_after_today
reason: site data starts tomorrow at the earliest
fix: pick a later date
- transition: set_site_data
gate: site_quantities_consistent
reason: safety stock above the reorder point, or the reorder point above the maximum
fix: correct the quantities
- transition: set_site_data
gate: supplier_site_valid
reason: a transfer must name another site of the same company
fix: pick the supplying site, or procurement buy
- transition: set_site_data
gate: manufacturing_installed
reason: procurement make and planning mrp need the production module, which this system does not have
fix: install manufacturing, or use buy or transfer, and reorder_point, min_max or manual
- transition: set_rounding
gate: starts_after_today
reason: a rounding rule starts tomorrow at the earliest
fix: pick a later date
- entity: price_list
states:
- draft
- published
- retired
terminal:
- retired
transitions:
- key: open_price_list
name: Open a price list
from: none
to: draft
by: pricing_manager
gate: a currency (ISO 4217); a base list in the same currency with the same VAT basis; rounding no finer than the currency's minor unit
effect: based on another list, or standalone
- key: set_prices
name: Set prices and rules
from: draft
to: draft
by: pricing_manager
gate: a pricing quantity is a whole number from 1 to 100000
effect: dated rows per product and unit, each a price per its pricing quantity (default 1), rules, and the list's default_from, all still editable; nothing visible to sales yet
- key: ai_price_check
name: AI flags unusual prices (suggest)
from: draft
to: draft
by: system
gate: the price check is on
effect: outliers against the base list and similar products are listed for the pricing manager
- key: publish_price_list
name: Publish the price list
from: draft
to: published
by: pricing_manager
approval: true
gate: every row and rule has a valid_from after today, or today while the currency has no published price yet; a default_from after today, or today for the first default list of a currency; the approver is not the author where the setting asks for one
effect: sales prices from it on each row's valid_from; from its default_from it is the currency's default list
- key: change_prices
name: Schedule a price change
from: published
to: published
by: pricing_manager
approval: true
gate: every new row starts after today
effect: new rows with a later valid_from; old rows are never edited
- key: assign_customer
name: Assign customers to the list
from: published
to: published
by: pricing_manager
gate: the assignment starts after today
effect: their prices in this currency resolve from this list from that date
- key: retire_price_list
name: Retire the price list
from: published
to: retired
by: pricing_manager
gate: not the default of its currency today or later, and not the base of another list or rule
effect: assigned customers fall back to the default list; prices for past dates still resolve from it
note: 'the CURRENT price is derived on read from the dated rows (PRD-11); nothing stores it. One default per currency on any date: the latest default_from on or before it. Nothing changes a price for a date already past'