dodil/erp-brick-product

src/brick_product/application/process.yaml

# 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'