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