Every invoice, purchase order, and quotation number in Odoo comes from the same mechanism: ir.sequence, a small model with an outsized number of configuration options most modules never touch beyond a prefix. Two of those options — implementation and use_date_range — decide real behavior worth understanding before an auditor or accountant asks why invoice numbers have gaps.
The short version: the default sequence implementation is fast (built on a native PostgreSQL SEQUENCE) but can leave permanent gaps if a transaction using it rolls back — the number was already consumed, and it's never reused. A "No gap" implementation exists specifically to guarantee consecutive numbers with no exceptions, at the cost of real table-level locking and reduced concurrency.
The basic shape
self.env['ir.sequence'].next_by_code('sale.order')
# or, for a sequence not tied to a model's default:
self.env['ir.sequence'].next_by_code('my_module.custom_sequence')
next_by_code() looks up the ir.sequence record whose code field matches the string passed in, and returns the next formatted value — prefix, zero-padded number, suffix, all applied automatically based on that record’s own configuration. The fields doing the actual work:
| Field | What it controls |
|---|---|
code | The identifier next_by_code() looks up — must match exactly. |
prefix / suffix | Fixed text before/after the number (supports date placeholders like %(year)s). |
padding | Zero-pads the number to a fixed width — padding: 4 turns 7 into 0007. |
number_next | The starting value. |
number_increment | The step size between consecutive values. |
implementation | 'standard' or 'no_gap' — covered below, the one with real behavioral consequences. |
standard vs no_gap: a real, deliberate tradeoff
The standard implementation is built on a native PostgreSQL SEQUENCE object — extremely fast, safe under heavy concurrency, and the sensible default for the overwhelming majority of use cases. Its real tradeoff: because a PostgreSQL sequence advances independently of the surrounding transaction, a number it hands out is consumed permanently even if the transaction that requested it later rolls back — a quotation number that gets allocated and then the create fails for an unrelated reason leaves a real, permanent gap in the sequence, one that will never be reused.
The no_gap implementation exists specifically to prevent this: it uses real table-level locking instead of a database sequence, guaranteeing genuinely consecutive numbers with zero gaps even across a rollback — at the direct cost of serializing concurrent access to that counter, since every request for the next number has to wait for the lock. For a high-volume model under real concurrent load, that’s a meaningful throughput cost; for something like a legally-numbered invoice sequence in most jurisdictions where regulators expect zero gaps, it’s frequently the entire point.
Watch out: this isn't a setting to leave at whatever a module's default happens to be without a deliberate choice — if a sequence's numbers show up on a legal document (an invoice number an accountant reconciles against, in particular), confirm which implementation it actually uses before assuming "no gaps" is guaranteed. The standard implementation's occasional gap after a rollback is an accepted, documented tradeoff, not a bug — but it's a genuinely wrong assumption to carry into a context where gapless numbering is a compliance requirement.
Resetting per period: use_date_range
A sequence with use_date_range enabled maintains a genuinely separate counter per date range instead of one continuously incrementing number — the practical result being that the first invoice of a new year (or month, depending on configuration) restarts at the configured starting value regardless of how high the previous period’s counter reached, producing the familiar INV/2027/0001 reset-per-year pattern rather than an ever-climbing raw integer.
Quick reference
| Need | Setting |
|---|---|
| Fast, high-concurrency, gaps acceptable on rollback | implementation: standard (the default) |
| Legally/compliance-mandated gapless numbering | implementation: no_gap |
| Reset numbering each year/period | use_date_range: True |
| A fixed-width, zero-padded number | padding set to the desired width |
Frequently asked questions
Can I switch an existing sequence from standard to no_gap later?
The setting can be changed on the record, but doing so doesn't retroactively fill in any gaps that already exist from before the switch — it only changes behavior going forward, so this is a decision worth making before real numbers have already been issued under the old setting.
Does no_gap guarantee sequential numbers even under concurrent requests?
Yes — that's precisely what the table-level locking accomplishes: concurrent requests for the next number are serialized rather than allowed to interleave, which is also exactly why it's slower under load than the standard implementation.
What happens if two different modules define a sequence with the same code?
next_by_code() resolves to whichever record actually matches that code — the same kind of naming collision risk covered in the CSV import article's discussion of external ids, and worth avoiding the same way: scope custom sequence codes to your own module's naming convention rather than a generic string.
Does number_increment need to be 1?
No — it can be any step size; a value of 1 is simply the overwhelmingly common case for a human-facing reference number.
Is padding purely cosmetic?
Functionally, yes — it only affects the formatted string's zero-padding, not the underlying numeric value or how it increments; changing padding on an existing sequence doesn't affect numbers already issued, only how future ones are formatted.
Further reading
- ORM API — official Odoo 18.0 developer documentation, for the broader model/field reference
ir.sequencesits alongside.
And on this site: The Odoo CSV Import Trick That Makes Re-Importing the Same File Safe for the same naming-collision discipline applied to external ids, and The manifest.py File, Key by Key for where a sequence’s own XML data record belongs in a module’s data list.