dodil/erp-brick-sales-order

DODIL ERP brick: sales order

AGENTS.md

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 ruledomain/ (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 seeapplication/use_cases.py — the permission, _can_see(), _sees_prices(); never a role's name
a new action for people and agentsa new use case in application/use_cases.py — a route AND a tool
how another module is reachedinfrastructure/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 migrationapplication/odoo_migration.py (plan, series mark, reconcile) + import_odoo_orders, reconcile_odoo_orders
a column or tableinfrastructure/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 its priced_from; tax only by the tax module's compute. 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 an ar_ 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) are so:settings:controls (set_so_control); so:settings:write is 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's accept_suggestion is 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 are tool=False except 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