dodil/erp-brick-pos
DODIL ERP brick: point of sale
src/brick_pos/application/process.yaml
# The brick's process: module.yaml's workflows (pos v1), copied as signed (pattern.md section 15). POS.declare_process serves
# it as BPMN 2.0 at GET /process/bpmn. A transition's key is the BPMN id and is frozen once signed; tests/test_process.py
# holds this file equal to the spec and the drawing equal to the signed hashes in erp-method signoff/pos.
id: pos
name: Point of sale (shops and tills)
version: 1
actors:
- id: cashier
name: Barista / cashier
does: opens their session with a float, rings up sales, takes payments and tips, refunds within their limit, gives discounts within their role's limit, counts the drawer at close
sees: their own open session and the receipts of their shop for refunds
permissions:
- pos:session:open
- pos:session:count
- pos:receipt:sell
- pos:receipt:refund
- pos:discount:apply
- pos:cash:move
- pos:voucher:sell
- pos:shop:read
- id: shift_lead
name: Shift lead / shop manager
does: approves cash differences, refunds and discounts above a cashier's limit, forces a session closed when a register has died, takes the cash bag to the bank, reads the shop's day
sees: everything in their shop
permissions:
- pos:shop:read
- pos:session:open
- pos:session:count
- pos:receipt:sell
- pos:receipt:refund
- pos:discount:apply
- pos:cash:move
- pos:voucher:sell
- pos:session:approve_difference
- pos:override:approve
- pos:session:force_close
- pos:deposit:prepare
- pos:report:read
note: 'never approves their own difference, refund or discount: matched on email AND sub'
- id: retail_ops
name: Retail operations / area manager
does: keeps shops, registers, the till layout, discount limits and recipes; reads every shop's days
sees: every shop
permissions:
- pos:shop:read
- pos:shop:write
- pos:register:write
- pos:layout:write
- pos:recipe:write
- pos:report:read
- pos:settings:write
- id: accountant
name: Retail accountant
does: checks each shop's day before it posts, decides tax mismatches and late receipts, reads the trail from a Z report to the journal, the stock moves and the bank
sees: every shop and day
permissions:
- pos:shop:read
- pos:report:read
- pos:day:review
- pos:day:post
- pos:migration:read
- id: owner
name: The owner (or whoever the owner names)
does: holds the controls on the people who handle cash (difference threshold, refund and discount limits, blind close, offline limits, fiscal profiles), signs the migration reconciliation
sees: everything
permissions:
- pos:shop:read
- pos:report:read
- pos:settings:controls
- pos:migration:read
- pos:migration:sign
note: a cashier, shift lead or accountant holding pos:settings:controls is a configuration error the access test refuses
- id: customer
name: The customer at the counter
external: true
does: pays, asks for a receipt or a VAT invoice, collects stamps
sees: their receipt
permissions: []
- id: system
name: The system
does: takes receipts from registers (online and offline), signs them through the shop's fiscal capability, sends stock consumption, VAT and the day's journal to the neighbours from pending rows, hands card settlements and cash bags to payments, marks days stale when something arrives late, loads an Odoo migration
sees: every shop, through its own service account
permissions:
- pos:receipt:ingest
- pos:fiscal:sign
- pos:outbox:dispatch
- pos:day:derive
- pos:number_series:claim
- pos:migration:load
- pos:migration:reconcile
note: each is an exact grant to pos's service client, never reached by a wildcard; the same client holds product:product:read, product:price_list:read, tax:document_tax:compute, gl:journal:post, gl:period:read, gl:company:read, inv:availability:read and bp:partner:read
workflows:
- entity: session
states:
- open
- counted
- closed
- force_closed
- posted
terminal:
- posted
transitions:
- key: open_session
name: Open the session with a float
from: none
to: open
by: cashier
gate: no open session on the register; the fiscal device ready where the profile needs one; the cashier employed today (hr, when installed); the float counted
effect: business date fixed (business_timezone and day_cutoff); register snapshot built (POS-30)
- key: record_cash_move
name: Record a safe drop or paid-out
from: open
to: open
by: cashier
gate: amount positive, NUMERIC(18,2); a paid-out above paid_out_limit approved by another person
- key: count_session
name: Count the drawer
from: open
to: counted
by: cashier
gate: every receipt synced (the register's last number equals the server's)
effect: difference = counted - expected; within the threshold it closes at once
- key: approve_difference
name: Approve the difference
from: counted
to: closed
by: shift_lead
approval: true
gate: the approver is not the counter (email AND sub)
requires: reason
- key: force_close_session
name: Force the session closed
from: open
to: force_closed
by: shift_lead
approval: true
requires: reason
effect: the claimed gap of receipt numbers recorded; late receipts accepted into this session only
- key: post_session
name: Posted with the shop day
from:
- closed
- force_closed
to: posted
by: system
gate: the shop day posted (POS-20)
readiness:
- transition: open_session
gate: one_open_session
reason: the register already has session {session_id} open
fix: close it first
- transition: open_session
gate: fiscal_device_ready
reason: the register's TSE or signature device is not registered
fix: 'Shops: register the device'
- transition: count_session
gate: all_synced
reason: 'the register holds receipts the server has not received: {from} to {to}'
fix: bring the register online, or a shift lead force-closes
- transition: approve_difference
gate: second_person
reason: you counted this session
fix: ask another shift lead
note: a session never reopens; a correction is a later document in a later session
- entity: receipt
states:
- open
- finalised
- quarantined
terminal:
- finalised
transitions:
- key: ring_up
name: Ring up lines
from: none
to: open
by: cashier
gate: the session open; the product on the snapshot
- key: take_payment
name: Take a payment line
from: open
to: open
by: cashier
gate: a card line authorised; a voucher line claimed; amounts NUMERIC(18,2)
- key: void_open_receipt
name: Void before finalising
from: open
to: none
by: cashier
effect: kept as a void record with its lines (fiscal law counts voids); card lines reversed on the terminal
- key: finalise_receipt
name: Finalise and print
from: open
to: finalised
by: cashier
gate: payment lines sum exactly to the gross; the fiscal signature returned
effect: receipt number claimed gapless per register; outbox rows for stock, tax, loyalty written in the same transaction
- key: quarantine_receipt
name: Quarantine a conflicting sync
from: none
to: quarantined
by: system
gate: a known receipt_id arrives with a different digest
- key: resolve_quarantine
name: Resolve the conflict
from: quarantined
to: finalised
by: accountant
requires: reason
effect: the accepted version is the receipt; the other kept as evidence; never two receipts
- entity: shop_day
states:
- open
- ready
- posted
- stale
- post_failed
transitions:
- key: derive_day_ready
name: Day ready
from: open
to: ready
by: system
gate: every session of the shop and date closed or force-closed; differences approved
- key: post_day
name: Post the day
from:
- ready
- post_failed
to: posted
by:
- system
- accountant
gate: tax agrees or the accountant accepted the printed amounts; the gl period open
effect: one summary journal (POS-20)
- key: fail_day_post
name: Posting refused
from: ready
to: post_failed
by: system
effect: gl's gate and reason kept
- key: mark_day_stale
name: A late receipt arrived
from: posted
to: stale
by: system
effect: supplement n+1 pending
- key: post_supplement
name: Post the supplement
from: stale
to: posted
by: system
effect: supplement journal for the difference only
readiness:
- transition: post_day
gate: tax_agrees
reason: 'receipts whose printed VAT differs from tax''s answer: named'
fix: 'Shop day: accept the printed amount with a reason'
- transition: post_day
gate: period_open
reason: the gl period of the business date is closed
fix: ask finance to post into the next open period or reopen
- transition: post_day
gate: reviewer_not_approver
reason: you approved a difference of this day
fix: ask another accountant
note: 'stale -> posted is the supplement, never a repost: the posted journal is not touched'
- entity: cash_deposit
states:
- sealed
- handed_over
- matched
terminal:
- matched
transitions:
- key: seal_bag
name: Seal the cash bag
from: none
to: sealed
by: shift_lead
gate: amount at most the safe's balance; bag number unused
- key: hand_over_bag
name: Hand the bag to payments
from: sealed
to: handed_over
by: system
effect: payments' expected deposit batch (POS-26)
- key: match_bag
name: Matched to the bank
from: handed_over
to: matched
by: system
gate: payments reports the batch matched
- entity: pos_migration_run
states:
- loaded
- reconciled
- signed
- discarded
terminal:
- signed
- discarded
transitions:
- key: load_pos_from_odoo
name: Load Odoo's sessions and orders
from: none
to: loaded
by: system
gate: gl, product and tax migrations signed; no open pos.session
- key: reconcile_pos_migration
name: Reconcile against Odoo
from: loaded
to: reconciled
by: system
- key: sign_pos_reconciliation
name: Sign the reconciliation
from: reconciled
to: signed
by: owner
approval: true
gate: every check equal or explained
- key: discard_pos_migration
name: Discard the run
from:
- loaded
- reconciled
to: discarded
by: owner
requires: reason