Cash Management Domain - Models Documentation
Source: api/nextango/apps/cash_management/models.py
Last Updated: 2026-07-09
Overview
The Cash Management models define the shift and drawer-count layer used for cash-handling audit and reconciliation. Both models are plain django.db.models.Model subclasses; neither syncs with Sanity.
| Model | DB Table | Purpose |
|---|---|---|
CashierShift | cash_management_cashier_shift | A time window during which one employee operates one register at one store |
DrawerCount | cash_management_drawer_count | A physical cash count event tied to a shift |
CashierShift
File: models.py:7-81
DB table: cash_management_cashier_shift
Inherits: django.db.models.Model
Anchors all cash events and drawer counts to a named employee-register-store-time tuple. terminal_id mirrors pos_gateway.TerminalSession.terminal_id as a plain string rather than a foreign key, so this app carries no dependency on pos_gateway.
Fields
| Field | Type | Constraints | Description |
|---|---|---|---|
shift_id | CharField(100) | unique, indexed, not editable | Generated on first save: SHIFT-<YYYYMMDD>-<8-char hex> |
store | ForeignKey(stores.Store) | PROTECT, related_name='cashier_shifts' | Store the shift belongs to |
employee | ForeignKey(users.User) | PROTECT, related_name='cashier_shifts' | Employee operating the register during this shift |
terminal_id | CharField(100) | indexed | String identifier of the physical register, matches TerminalSession.terminal_id |
status | CharField(20) | choices, default=active, indexed | Shift lifecycle status, see below |
started_at | DateTimeField | default=now | When the shift began |
ended_at | DateTimeField | null, blank | When the shift ended |
opening_float | DecimalField(10,2) | default=0.00 | Starting cash amount placed in the drawer |
closing_submitted | DecimalField(10,2) | null, blank | Amount the employee counted at close, populated by the blind-count workflow |
closing_expected | DecimalField(10,2) | null, blank | System-computed expected close amount |
discrepancy | DecimalField(10,2) | null, blank | Difference between closing_submitted and closing_expected |
reviewed_by | ForeignKey(users.User) | SET_NULL, null, blank, related_name='reviewed_shifts' | Manager who reviewed the shift close |
reviewed_at | DateTimeField | null, blank | When the review occurred |
notes | TextField | blank | Free-text notes |
created_at | DateTimeField | auto_now_add | Record creation timestamp |
updated_at | DateTimeField | auto_now | Record last-modified timestamp |
Status Choices
| Value | Display |
|---|---|
active | Active |
closing | Pending Close Review |
closed | Closed |
abandoned | Abandoned |
Indexes
(terminal_id, status)(store, started_at)(employee, started_at)
Ordering
Default ordering: ['-started_at'] (most recent shift first)
shift_id generation
shift_id is assigned on first save if not already set, in the form SHIFT-<YYYYMMDD>-<8-character uppercase hex>. The date segment reflects the save time, and the hex segment is derived from a UUID4.
__str__
Returns "Shift <shift_id>" followed by the employee and the store name.
DrawerCount
File: models.py:84-133
DB table: cash_management_drawer_count
Inherits: django.db.models.Model
Records a physical cash count event tied to a CashierShift.
Fields
| Field | Type | Constraints | Description |
|---|---|---|---|
shift | ForeignKey(CashierShift) | PROTECT, related_name='drawer_counts' | Shift this count belongs to |
count_type | CharField(20) | choices | Which point in the shift this count represents, see below |
submitted_by | ForeignKey(users.User) | PROTECT, related_name='submitted_drawer_counts' | Employee who submitted the count |
submitted_amount | DecimalField(10,2) | required | Physical cash amount counted |
cash_breakdown | JSONField | default={} | Denomination-level breakdown of the counted cash |
expected_amount | DecimalField(10,2) | null, blank | System-expected amount. For a closing count, this is left null until after submitted_amount is received, enforcing a blind count |
discrepancy | DecimalField(10,2) | null, blank | Difference between submitted_amount and expected_amount once both are known |
witnessed_by | ForeignKey(users.User) | SET_NULL, null, blank, related_name='witnessed_drawer_counts' | Second employee who witnessed the count, if any |
witnessed_at | DateTimeField | null, blank | When the witness signed off |
submitted_at | DateTimeField | auto_now_add | When the count was submitted |
Count Type Choices
| Value | Display |
|---|---|
open | Opening Count |
mid_shift | Mid-Shift Count |
close | Closing Count |
Ordering
Default ordering: ['-submitted_at'] (most recent count first)
__str__
Returns the count type display, the shift's shift_id, and the submitted dollar amount.
Model Relationships
CashierShift
└─ drawer_counts (reverse FK) → DrawerCount (many)
CashierShift
└─ cash_drawer_events (reverse FK) → transactions.CashDrawerEvent (many, nullable FK)
Cascade rules:
- Deleting a
CashierShiftis blocked (PROTECT) if it hasDrawerCountrecords - Deleting a
StoreorUserreferenced asCashierShift.storeorCashierShift.employeeis blocked (PROTECT) while shifts reference it - Deleting the
Userreferenced asCashierShift.reviewed_byorDrawerCount.witnessed_bysets that field to null (SET_NULL) - Deleting a
CashierShiftreferenced bytransactions.CashDrawerEvent.shiftsets that field to null (SET_NULL), the event record is not deleted