Instructions for agents working on this brick
Built from the sales-order module spec (erp-method-l2c modules/sales-order, version 2). Read erp-platform's
BRICKS.md before changing anything — the rules and why each one exists. The spec is signed: a change goes through the
spec first, then the code.
Where a change goes
| You are changing… | It goes in |
|---|---|
| a business rule | domain/ (sales_order.py, gates.py) — and its test in tests/test_rules.py |
a readiness gate {gate, met, reason, fix} | domain/gates.py only — the refusal and the screen's answer are the same list |
| who may do something, or which rows they see | application/use_cases.py — the permission, _can_see(), _sees_prices(); never a role's name |
| a new action for people and agents | a new use case in application/use_cases.py — a route AND a tool |
| how another module is reached | infrastructure/seams.py only (a port per neighbour, a real adapter, tests use fakes) |
| "today" | infrastructure/business_time.py (one small read, so it can move to the platform) |
| the Odoo migration | application/odoo_migration.py (plan, series mark, reconcile) + import_odoo_orders, reconcile_odoo_orders |
| a column or table | infrastructure/tables.py, then regenerate schema.sql and migrations/ (additive only), tests/test_schema.py keeps them equal |
The rules that must not be undone
- Lines are priced only by product's
find_price(priced_on = today) and keep itspriced_from; tax only by the tax module'scompute. A line from an accepted quote keeps the quote's price (priced_from= the quote line); only an AMENDED line is re-priced. A confirmed order's lines, prices and tax never change except by a revision. - The order asks, invoicing owns the invoice. Requests are keyed
so:{sales_order_id}:{request_no}, schedules{sales_order_id}|{line_no}; this brick never writes anar_table. Invoiced quantity = the larger of what invoicing issued and what we already asked for, so nothing is asked twice. - Fulfilment and invoice status are derived on read, never stored. Deliveries are append-only; a correction is a negative row against the original, never below what is invoiced.
- Nothing dated starts today or earlier (orders, settings, customer policy, deliveries in the future); a correction is a new row from a later date. Only the migration may write a past date.
- The controls (
confirm_on_acceptance,amendment_needs_approval,lock_done_orders,business_timezone, AI step modes) areso:settings:controls(set_so_control);so:settings:writeis refused on them. Turning an AI step on is the owner's recorded consent. - AI steps only suggest. Output lands in
so_suggestion; a person'saccept_suggestionis the only way it reaches an order. The brick never calls a model; tests never do. - Machine permissions (
so:order:intake|suggest|finish|import,so:number_series:claim) are exact grants; no wildcard reaches them. Those use cases aretool=Falseexcept the migration. - A row the caller may not see is a 404; a fulfiller sees confirmed orders and never a price (projected in
_view). - No read-your-writes inside an open transaction (DataK3): use cases work counts out from the aggregate, not by querying
back. Numbers (
so_number_series) and orders save in one transaction at the versions they were loaded at. - The Odoo migration is idempotent on
sales_order_id = odoo:<company>:<name>; history (done, cancelled) is read-only and asks invoicing for nothing; an unmapped record on an open order stops the run before anything is written.
Before you commit
pytest -q --tb=short # rules, stories, scenarios, access, both doors, adapters, migration, schema
ruff check src tests
erp-compose check tests/tier1.compose.yaml --strict --roles src/brick_sales_order/roles.yaml