Skip to content

POS Applet — SA-Governed Retail Sales

Status: Draft
Date: 2026-06-12
ADR: 0005 - V3 POS Applet Design


Overview

The POS Applet extends the V3 association-model pattern to Odoo's Point of Sale module. Unlike B2B sales (sale.order), POS collapses the entire sales process into a single checkout step. The POS Applet governs these transactions through functionality-scoped Serviced Accounts.

B2B vs POS Comparison

Step B2B (sale.order) POS (pos.order)
Customer Selected upfront Identified at checkout (phone/card)
Products Added to quote Added to cart
Pricing Pricelist on SO Pricelist on POS config, SA-scoped
Inventory Separate delivery order Auto-decremented from POS location
Invoice Explicit create step Auto-generated on validation
Payment Separate reconciliation One-step at checkout
Receipt Optional Auto-printed/emailed
SA context From customer From cashier's membership

Core Principle

SA = Functionality Boundary

In POS, the Serviced Account represents what type of products or services are being sold — not who the customer is or where the store is located.

Retail Shop SA  →  physical goods (fans, batteries, solar systems)
Swap Station SA →  battery swap services
Supermarket SA  →  combined retail (multiple product families)

Architecture

Association Models

ov.sa_pos_config (NEW)
├── account_id  → SA                # Functionality (Retail, Swap, etc.)
├── config_id   → pos.config        # POS terminal/store config
└── association_kind → binding/access

ov.sa_pricelist (NEW)
├── account_id  → SA                # Functionality that uses this pricelist
├── pricelist_id → product.pricelist
└── association_kind → binding/access

ov.sa_pos_order (EXISTS, 0 records)
├── account_id  → SA                # Cashier's active functionality SA
├── pos_order_id → pos.order        # The POS transaction (NOT sale.order)
└── association_kind → binding

ov.actor_sa_pos_order (EXISTS, 0 records)
├── assignment_id → ov.sa_pos_order # Follows V3 subset rule
├── actor_id       → res.partner    # The cashier
└── state / is_primary

Key Difference from B2B

B2B: SA context comes from the CUSTOMER
     ov.sa_sale_order → account_id = customer's governing SA

POS: SA context comes from the CASHIER'S MEMBERSHIP
     ov.sa_pos_order  → account_id = cashier's active functionality SA

This means:

  • A cashier at a combined shop+swap location selects their role at shift start
  • Retail sales go to Retail SA; swap transactions go to Swap SA
  • The same customer can shop at both without being pre-assigned
  • New customers are auto-admitted to the SA at checkout

Transaction Flow

Shift Start

Cashier logs into POS
  → System determines available SAs (via cashier's ov.membership)
  → Cashier selects functionality SA for the shift
    (auto-selected if only one membership)
  → UI filters:
    • Pricelists  → ov.sa_pricelist for selected SA
    • Customers   → not filtered (search all)

Checkout

1. Customer identified (phone / service card)
2. Auto-admission: if new customer, system creates ov.sa_customer
3. Cart: products added, serials scanned
4. Payment: M-Pesa / cash / card (standard POS)
5. Validate (one-click):
   • pos.order created
   • ov.sa_pos_order created (account_id = cashier's SA)
   • ov.actor_sa_pos_order created (actor_id = cashier)
   • Invoice auto-created
   • Stock decremented
   • Receipt printed

Customer Auto-Admission

Customers are always served. At checkout:

  • Lookup by phone or card
  • Check ov.sa_customer for this partner + this SA
  • If no record exists → auto-create with association_kind = 'binding'
  • The act of purchase constitutes admission to the SA's functionality scope

Implementation Status

Model Status Action Needed
ov.sa_pos_config ❌ Not found Create (V3 canonical structure)
ov.sa_pricelist ❌ Not found Create (general-purpose, not POS-specific)
ov.sa_pos_order ✅ Exists Add ir.rule on pos.order to activate
ov.actor_sa_pos_order ✅ Exists No action needed

Open Questions

  1. Cashier UI: Does the POS front-end need a custom SA selector, or can the cashier switch Odoo company/user context?
  2. Pricelist filtering: How does ov.sa_pricelist feed into the POS pricelist dropdown — via ir.rule or server-side compute?
  3. Customer search scope: Should the POS partner search be restricted to customers visible through ov.sa_customer, or search all partners?

References