← Work · private · enterprise
Case study · Odoo 14
Petty-cash batch ops — utilization checks, multi-tier review, clean posting
Construction / enterprise Odoo: replace many one-off expense sheets with a holder-bound batch workflow so float limits, project/city review, mobile entry, and accounting stay aligned. Client details omitted.
Problem
Field spend sat on many unrelated expense sheets. Float utilization was hard to see, drafts could enter approval at the wrong time, and reviewers were not always the right project/city owners. Mobile and desktop drifted on limit checks. Accounting needed operating-unit–aware journals, not a single flat post. Reviewers also hit ACL / prefetch failures when the UI loaded sibling sheets in a batch.
Domain
- 01Holder
Petty-cash float owner. One open batch at a time; ceiling enforced before new spend.
- 02Batch
Container for a small set of expense sheets. Tracks draft / submitted / paid / balance and utilization stage.
- 03Sheets & lines
Standard
hr.expense.sheetlinked to the batch. Lines carry project, operating unit, type, and amount. - 04Review tiers
Holder/manager for the sheet; project agreement managers (city-aware) for their lines; separate voucher path where needed.
Design decisions
- Utilization-gated submit — drafts only enter tier validation after batch usage hits the next milestone (with a near-cap escape so the last drafts are not stuck forever).
- One pure draft at a time — avoids parallel unfinished sheets fighting the float and the UI.
- Same rules on mobile and desktop — REST hub reuses batch limits, required fields, and submit preview so field entry cannot bypass the same rules.
- OU-split posting — when a sheet mixes operating units, post splits journals cleanly and consolidates holder credit lines.
Engineering patterns
01 KPI visibility vs record rules
Batch totals (draft / submitted / paid / balance) compute with sudo over related sheets and mapped() so a reviewer's narrower ACL does not skew utilization. The UI still respects line privacy for agreement managers.
# Anonymized pattern — not client source
sheets = batch.sudo().sheet_ids
draft = sum(sheets.mapped('total_draft_amount'))
# utilization = f(draft, submitted, paid, float_limit)
# submit allowed only when next threshold is met02 Sibling-safe reviewer ACL
Assigned reviewers need the reviewed sheet and other sheets in the same batch. Without that, loading batch.sheet_ids after sudo prefetch raised AccessError in production. Access is granted via record rules plus Python check_access_rule for same-batch siblings; tier reviews prefer a direct search over a stale related cache.
03 City-scoped line review + OU post
Project reviewers are resolved city-aware. Agreement-manager–only reviewers see filtered expense lines (including after approve/reject). On post, expenses group by operating unit when mixed; holder credits merge per account/partner on each journal.
Constraints & edges
- Single open batch per holder; zero-limit holders cannot receive sheets
- Required line fields enforced on save and submit; PDF required to enter validation
- Projects must resolve an agreement manager before submit (legacy bypass via config where needed)
- Under-review sheets: blocked writes stripped; reject blocks further approve
- Block repost while posted journals already exist for the sheet
API surface
Mobile JSON endpoints sit on the same domain rules: holder context, create draft, draft summary, submit preview / submit, paginated sheets, home dashboard, sheet detail (approvals + attachments), and related purchase documents. Auth is shared with the wider hub — not a one-off petty-cash API.
Outcome
Ops submit and review cash inside a holder-bound batch instead of chasing one-off sheets and spreadsheets. Float utilization drives when drafts may enter approval. Project/city reviewers see the right lines. Mobile and desktop share limits. Accounting posts split cleanly by operating unit. The ACL/prefetch edge cases that blocked reviewers in the batch UI are closed.
Source code stays private. Related notes: payroll case study, architecture, ORM example.