Skip to content

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:

  1. resolve the Sequence by key (fail fast — Sequence.DoesNotExist);
  2. compute the period key from period_reset and the draw date;
  3. SELECT … FOR UPDATE the counter row (:meth:AngeeQuerySet.lock_if_supported);
  4. 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;
  5. 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

python
class PeriodReset(models.TextChoices)

When a sequence restarts its counter at 1.

SequenceManager

python
class SequenceManager(AngeeManager)

Draws and previews formatted numbers from a keyed sequence.

next_value

python
def next_value(key: str, *, on_date: date | None = None) -> str

Reserve 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

python
def preview_next(key: str, *, on_date: date | None = None) -> str | None

Return 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

python
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

python
class Meta()

Django model options for a sequence.

__str__

python
def __str__() -> str

Return the sequence key for Django displays.

period_key

python
def period_key(on_date: date) -> str

Return 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

python
def format_number(value: int, on_date: date) -> str

Render value for on_date through this sequence's template.

SequenceCounterManager

python
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

python
def draw(sequence: models.Model, period: str) -> int

Lock, 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

python
def peek(sequence: models.Model, period: str) -> int

Return the current value for this pair without locking, 0 if none.

SequenceCounter

python
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

python
class Meta()

Django model options for a sequence counter.

__str__

python
def __str__() -> str

Return 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.

Released under the AGPL-3.0 License.