Sequences (Record Numbering)
A document’s reference — an invoice number, an order reference — is derived from the
documents that already exist, not from a stored counter. A Sequence record defines the
shape of the number (prefix, suffix, padding, when it resets); the records themselves are
the record of which numbers have been used.
A model declares which of its columns are numbered, and the engine fills them.
Not the view ordering field. This page is about numbering records. It is unrelated to the integer
sequencefield used to order rows in list/kanban views — that one is just a sort key.
Declaring a Numbered Column
Section titled “Declaring a Numbered Column”Put _numbering on the model, keyed by the column it numbers:
class SaleOrder(Model): _unique_together = [("company", "name")] _numbering = { "name": { "when": "create", "sequence": "sale_order_sequence", "date": "date", }, }That is the whole integration — there is no call to make. create fills the column when it
arrives empty.
The keys
Section titled “The keys”| Key | Meaning |
|---|---|
when | "create" fills the column as the row is written. "assign" leaves it empty until the model asks, by calling await self.assign_number("<column>"). |
sequence | The identifier of a Sequence — one series shared by every record. |
sequence_path | A path to a Sequence held on the record ("journal__numbering_sequence"), for numbering that varies per record. |
date | The field whose date selects the period for a resetting sequence. None uses today. |
when and date are both required, date included when the answer is None; and exactly
one of sequence / sequence_path must be named.
Nothing here defaults, because a default is only safe when getting it wrong fails visibly
and neither of these does. A document that should be numbered at posting but is filled at
create quietly spends a number on every abandoned draft — the numbering develops gaps, nothing
errors, and gapless numbering is a legal requirement in many jurisdictions. A date naming a
field that does not exist reads as None, which is the legal way of saying this series never
resets — so the numbering silently stops restarting each year.
Every one of these is checked when the model loads, not when a document is finally
numbered: a spec that names a column, a date field or a sequence_path the model does not
have refuses to compose.
There is no scope key — see below.
Numbering at Posting
Section titled “Numbering at Posting”A document that earns its number later declares when: "assign" and asks for it at the moment
it earns it:
class FinancialDocument(Model): _unique_together = [("company", "number")] _numbering = { "number": { "when": "assign", "sequence_path": "journal__numbering_sequence", "date": "date", }, }
async def post(self): await self.assign_number("number") ...assign_number does nothing when the column already holds a value, so re-posting a cancelled
document keeps the number it was issued. Until then the column is empty — not a "Draft"
placeholder. A placeholder is a value, so every draft in a company shares it and no unique
constraint can be applied; an empty column says what is true, and repeated NULLs never
collide.
The Scope Is the Unique Constraint
Section titled “The Scope Is the Unique Constraint”The boundary the numbering is isolated by is derived, not declared: it is the other
members of the _unique_together tuple the numbered column appears in.
_unique_together = [("company", "number")] # ← numbers per company_numbering = {"number": {...}} # ← says nothing about scopeA column under no unique constraint numbers once for the whole install.
It is derived because it is not a second fact. A scope FINER than the constraint is a
collision waiting to happen, and this is the trap worth understanding: an invoice numbers
from its journal’s series, so isolating it per journal looks right. It is not. Two
journals may be pointed at one Sequence — a second bank account reusing the payment series
is an ordinary thing to want — and each would then read only its own documents, take the
same value, and the second to post would fail on the constraint, which never heard about
journals. A scope coarser than the constraint is safe but skips numbers. So the only correct
value is the constraint’s own boundary, which means declaring it separately is writing one
value twice and hoping — and nothing catches the day the two disagree, because each reads
plausibly on its own.
The scope is not what separates one series from another — the prefix is. Numbering reads the highest value already carrying this period’s prefix, so two journals with different sequences stay independent inside one company scope without being told to. The numbering lock is keyed the same way, on the scope and the prefix — and never on the model, so an invoice and a bill never wait for each other while two journals genuinely sharing a series do.
A numbered column covered by two unique constraints has no single boundary, and the model refuses to compose rather than picking one.
A conditional series
Section titled “A conditional series”When the choice of series is a decision about the document rather than a path, override
_numbering_sequence. A credit note numbers from its journal’s reversal series:
async def _numbering_sequence(cls, field_name, spec, record=None): if field_name == "number": doc_type = await cls.numbering_field(record, "type") journal = await cls.numbering_field(record, "journal") if journal is not None and doc_type in ("Credit Note", "Debit Note"): await journal.fetch_related("reversal_numbering_sequence", "numbering_sequence") return journal.reversal_numbering_sequence or journal.numbering_sequence return await super()._numbering_sequence(field_name, spec, record)Read the record’s fields through await cls.numbering_field(record, "<field>"). The
override is called from two places with two different shapes — at create the row does not
exist yet, so it is handed the vals; at assign_number it is handed the saved record — and
that accessor resolves either, fetching a relation off a saved record for you. Reaching for
the attribute directly works in one case and not the other.
One Series, One Table
Section titled “One Series, One Table”A series belongs to one column on one model. Never copy a number a record earned into another table: the copy is stored under that table’s own unique constraint, so one value ends up policed in two places and “which numbers has this series spent?” no longer has a single table to ask.
Invoicing is the worked example. Three kinds of record, three kinds of series:
| Record | Numbers from |
|---|---|
| A document (invoice, credit note, bill, debit note) | its document series — INV-, CN-, BILL-, DN- |
| A payment | the payment series — PAY- |
| A journal entry | its own journal’s series — SJE-, PJE-, BJE-, CJE-, MJE- |
An invoice’s ledger entry therefore carries its own number and names the invoice in its
reference; it is not a second copy of INV-00042. That is what lets a register’s shift
close, a manual adjustment and an invoice all be posted into one journal without any of
them spending another’s numbers.
Why derivation still reads more than one table
Section titled “Why derivation still reads more than one table”_highest_numbered looks at every model naming the same series and isolated by the same
scope, not just the caller’s own. That is not a licence to share a series — it covers values that reached a
second table anyway: rows written before the rule above, a data import, an API client
carrying its own references. Such a row is invisible to a single-table derivation, so the
next record takes a number that already exists and the write fails — every time, since a
derivation recomputes the same value on each retry.
Extending Another Module’s Numbering
Section titled “Extending Another Module’s Numbering”_numbering merges across __inherit__, keyed by column, so a module adds a number without
disturbing the ones already there — the same contribute-don’t-assign rule the view layer uses.
class FinancialDocumentXX(Model): __inherit__ = "FinancialDocument" # Adds a second numbered column; the document's own `number` is untouched. _numbering = { "fiscal_number": {"when": "assign", "sequence": "xx_fiscal_sequence", "date": "date"}, }Remove one by naming it — the column stops being filled, and whatever declared it becomes your responsibility:
_numbering = {"number": {"disabled": True}}Why Numbers Are Derived, Not Counted
Section titled “Why Numbers Are Derived, Not Counted”A stored counter is a second opinion about which numbers have been used, and it can be wrong. Anything that writes a number without going through it — a data import, a restore, an API client carrying its own references — leaves it behind, and every document numbered afterwards collides with one that already exists. The save fails, and keeps failing, until an administrator finds and edits the counter.
Deriving removes the failure rather than recovering from it. A document arriving as
SO-2026-00500 simply means the next one is 00501.
The one counter that remains
Section titled “The one counter that remains”Sequence still carries a next_number, and await Sequence.get_next("<identifier>")
still hands out values from it. That path is for generated names — a lot code, a serial
number — not document numbers. The difference is what the value means: a lot code is a label
minted on demand, so a gap in the run says nothing and there is no scope to derive within. A
document number is a claim about a sequence of documents, which is why it is read back off
them.
Never point both at one column. A column filled by _numbering and also written from
get_next has two sources disagreeing about what has been issued, and the counter re-issues
a number the documents already hold the moment the derived side moves ahead of it.
Pattern Tokens
Section titled “Pattern Tokens”The prefix and suffix are Python percent-format patterns. The following tokens are
substituted from the date you pass (or now). A pattern with no tokens is used verbatim.
| Token | Meaning | Example |
|---|---|---|
%(year)s | 4-digit year | 2026 |
%(y)s | 2-digit year | 26 |
%(month)s | Zero-padded month | 06 |
%(day)s | Zero-padded day of month | 25 |
%(doy)s | Day of year | 176 |
%(woy)s | Week of year | 25 |
%(weekday)s | Weekday number (0 = Sunday) | 4 |
%(h24)s | Hour (24h) | 14 |
%(h12)s | Hour (12h) | 02 |
%(min)s | Minute | 09 |
%(sec)s | Second | 30 |
The final value is prefix + zero-padded-number + suffix.
Zero-Padded Size
Section titled “Zero-Padded Size”size controls the zero-padding of the numeric portion (default 5). With size: 5, the
forty-second document renders as 00042. So a sequence with prefix: "INV-" and
size: 5 produces INV-00042; one with prefix: "PKG-%(year)s-" produces PKG-2026-00042.
Per-Period Resets
Section titled “Per-Period Resets”reset_frequency controls when numbering starts again from 1. Choices: Never, Daily,
Weekly, Monthly, Yearly (default Never).
The reset is expressed through the prefix, not a counter: numbering looks for the highest existing value carrying this period’s prefix, so a new period simply finds nothing and starts at 1. Combine a reset with a matching date token so the visible number and the period line up:
prefix: "INV-%(year)s-"reset_frequency: Yearlygiving INV-2026-00001, INV-2026-00002, …, then INV-2027-00001 in the new year.
Which period applies comes from the date key of the model’s _numbering entry — the field
whose date the document is numbered by. With date: None, the period is today’s.
Seeding a Sequence (YAML)
Section titled “Seeding a Sequence (YAML)”Ship data_type: Sequence records in your module’s data/ directory.
- data_type: Sequence identifier: package_sequence name: Package Sequence prefix: PKG-%(year)s- size: 5
- data_type: Sequence identifier: serial_number_sequence name: Serial Number Sequence prefix: "SN-" size: 5Fields
Section titled “Fields”| Field | Type | Meaning |
|---|---|---|
identifier | Char | Unique handle named by a model’s _numbering (snake_case). |
name | Char | Human-readable label (required). |
prefix | Char | Pattern prepended to the number. |
suffix | Char | Pattern appended to the number. |
size | Integer | Zero-padding width of the number (default 5). |
reset_frequency | Selection | Never / Daily / Weekly / Monthly / Yearly (default Never). |
model | ManyToOne (ModelRegistry) | Optional. The model this sequence numbers (informational/grouping). |
next_number | Integer | The counter used by get_next for generated names only — ignored by _numbering. |
For a numbered column, a Sequence defines the shape of the number. It does not track
which numbers have been issued — the records do.
A Series Chosen From the Record
Section titled “A Series Chosen From the Record”Because sequence_path follows a relation, the series can be chosen from the record rather
than fixed.
That is how an invoice numbers from its journal’s series while staying scoped to its company:
# One numbering series per journal, created per company at install.- data_type: Sequence identifier: customer_invoice_seq name: Customer Invoices prefix: INV-%(year)s- size: 5 reset_frequency: YearlyThe journal points at it, and format: "journal__numbering_sequence" follows that link. To
give one company — or one journal — its own series, point it at a different Sequence; no
code change. Two journals may share one, and then they share one run of numbers, which is
what pointing them at the same series means.