angee.sequence.models
Sequence: gapless, period-resetting document numbering.
A :class:Sequence is a named counter with a formatting template; a :class:SequenceCounter holds the current value for one (sequence, period) pair. Consumers seed their Sequence rows and compose scope into the key — the framework stays scope-agnostic ("<addon>.<document>/<company_sqid>" is a consumer convention, not a sequence concern).
Allocation protocol (Sequence.objects.next_value), executed exactly:
- resolve the
Sequenceby key (fail fast —Sequence.DoesNotExist); - compute the period key from
period_resetand the draw date; SELECT … FOR UPDATEthe counter row (:meth:AngeeQuerySet.lock_if_supported);- if it is absent,
INSERT … ON CONFLICT DO NOTHING(bulk_create(ignore_conflicts=True)) then re-SELECT … FOR UPDATE— the winner and the loser of the first-draw race both end up locking the one row; - increment the counter and format the value through the template.
The counter reads and writes run under system_context (framework-owned bookkeeping the actor never touches directly); next_value never opens its own transaction, so it must be called inside the caller's transaction: rollback releases the row lock and never burns a number, and concurrent posts serialize on the lock.
Backend scope of the gapless guarantee. Gaplessness is a PostgreSQL fact, resting on SELECT … FOR UPDATE. lock_if_supported is a no-op on backends without row locks (the SQLite dev floor), so the same code runs there without the lock and the invariant is not guaranteed. The concurrency test is PostgreSQL-marked accordingly.
PeriodReset
class PeriodReset(models.TextChoices)When a sequence restarts its counter at 1.
SequenceManager
class SequenceManager(AngeeManager)Draws and previews formatted numbers from a keyed sequence.
next_value
def next_value(key: str, *, on_date: date | None = None) -> strReserve and return the next formatted number for key.
Runs the module's allocation protocol under system_context: resolves the sequence (raising :class:Sequence.DoesNotExist when the key is unknown), draws the locked counter for the resolved period, and formats the incremented value. Call this inside the caller's transaction (see the module docstring): the row lock is held until that transaction commits, so a rollback returns the number and concurrent draws serialize.
preview_next
def preview_next(key: str, *, on_date: date | None = None) -> str | NoneReturn the number next_value would draw, without reserving it.
A non-locking, non-reserving peek for draft forms: returns None unless the sequence exists and has preview_enabled. It is advisory — a concurrent post may take the previewed number before the draft is saved, so a stale preview is correct behaviour, not a bug. Nothing may treat the result as a reservation; use :meth:next_value for the authoritative, fail-fast draw.
Sequence
class Sequence(AngeeDataModel)A named counter with a formatting template and reset period.
The key is the stable lookup handle consumers draw against; scope (a company, a journal) is composed into the key by the consumer, keeping the sequence framework itself scope-agnostic. template is a str.format pattern over {prefix}, {year}, {month} and {number} (e.g. "INV/{year}/{number:05d}").
Meta
class Meta()Django model options for a sequence.
__str__
def __str__() -> strReturn the sequence key for Django displays.
period_key
def period_key(on_date: date) -> strReturn the counter partition key for on_date under this reset.
"" when the counter never resets, "2026" for a yearly reset, and "2026-07" for a monthly reset.
format_number
def format_number(value: int, on_date: date) -> strRender value for on_date through this sequence's template.
SequenceCounterManager
class SequenceCounterManager(AngeeManager)Owns the locked counter row for one (sequence, period) pair.
Not a GraphQL surface: counter rows are internal bookkeeping the caller never addresses directly. Callers reach these methods through :class:SequenceManager, which brackets them in system_context.
draw
def draw(sequence: models.Model, period: str) -> intLock, create-if-absent, increment and return this counter's value.
Steps 3–5 of the allocation protocol. lock_if_supported applies SELECT … FOR UPDATE where the backend supports it (a no-op on the SQLite floor). On the first draw of a new period the locked read finds nothing to lock, so both racers INSERT … ON CONFLICT DO NOTHING and re-read under the lock — landing on the one row and serializing there.
peek
def peek(sequence: models.Model, period: str) -> intReturn the current value for this pair without locking, 0 if none.
SequenceCounter
class SequenceCounter(AngeeModel)Current value for one sequence within one reset period.
Internal to the sequence addon — no sqid, no GraphQL surface, no REBAC resource type. The (sequence, period) uniqueness is the row-lock target and the ON CONFLICT arbiter of the first-draw race.
Meta
class Meta()Django model options for a sequence counter.
__str__
def __str__() -> strReturn a readable label for Django displays.
SequenceRole
The sequence/role anchor: its const admin arm resolves a platform admin as an effective sequence manager. See :func:angee.base.models.role_anchor.