Sequences & Auto-Numbering¶
Netforce provides a robust sequence system for automatically generating unique numbers for business documents like invoices, purchase orders, and stock movements. The system supports pattern templates, date variables, and multi-company isolation.
Overview¶
The sequence system consists of two main models:
sequence- Sequence definitions with patterns and rulessequence.running- Current running numbers for each sequence
Sequence Model¶
Basic Structure¶
class Sequence(Model):
_name = "sequence"
_fields = {
"name": fields.Char("Name", required=True),
"type": fields.Selection([
["cust_invoice", "Customer Invoice"],
["supp_invoice", "Supplier Invoice"],
["sale_order", "Sales Order"],
["purchase_order", "Purchase Order"],
["stock_move", "Stock Movement"],
# ... many more types
], "Type"),
"prefix": fields.Char("Prefix Pattern"),
"padding": fields.Integer("Number Padding"),
"running": fields.One2Many("sequence.running", "sequence_id", "Running Numbers"),
"company_id": fields.Many2One("company", "Company"),
"model_id": fields.Many2One("model", "Target Model"),
}
Pattern Variables¶
The prefix field supports dynamic variables:
| Variable | Description | Example |
|---|---|---|
%(Y)s |
4-digit year | 2023 |
%(y)s |
2-digit year | 23 |
%(m)s |
2-digit month | 01, 12 |
%(d)s |
2-digit day | 01, 31 |
%(H)s |
2-digit hour | 00, 23 |
%(bY)s |
Buddhist year (4-digit) | 2566 |
%(by)s |
Buddhist year (2-digit) | 66 |
%(contact_code)s |
Contact code from context | CUST001 |
%(categ_code)s |
Category code from context | ELEC |
Using Sequences in Models¶
1. Basic Integration¶
Add auto-numbering to your model's create method:
class Invoice(Model):
_name = "account.invoice"
_fields = {
"number": fields.Char("Number", required=True, search=True),
"type": fields.Selection([["out", "Customer"], ["in", "Supplier"]]),
# ... other fields
}
def _get_number(self, context={}):
"""Generate next number based on invoice type"""
inv_type = context.get("type", "out")
if inv_type == "out":
seq_type = "cust_invoice"
else:
seq_type = "supp_invoice"
seq_id = get_model("sequence").find_sequence(type=seq_type)
if seq_id:
return get_model("sequence").get_next_number(seq_id, context)
return None
_defaults = {
"number": _get_number,
}
def create(self, vals, context={}):
# Generate number if not provided
if not vals.get("number"):
vals["number"] = self._get_number(context)
return super().create(vals, context)
2. Context-Based Numbering¶
Pass context variables for dynamic patterns:
def create_invoice_for_customer(self, customer_id, lines):
"""Create invoice with customer-specific numbering"""
customer = get_model("contact").browse(customer_id)
# Create invoice with context
invoice_id = get_model("account.invoice").create({
"customer_id": customer_id,
"lines": lines,
}, context={
"contact_code": customer.code,
"sequence_params": {
"branch": customer.branch_code
}
})
return invoice_id
Sequence Configuration¶
1. Creating Sequences¶
Create sequences through code or UI:
# Create customer invoice sequence
seq_id = get_model("sequence").create({
"name": "Customer Invoice 2023",
"type": "cust_invoice",
"prefix": "INV%(Y)s-%(m)s-",
"padding": 4,
"company_id": 1
})
# This will generate numbers like: INV2023-01-0001, INV2023-01-0002, etc.
2. Advanced Patterns¶
Create complex numbering patterns:
# Customer-specific invoice numbering
{
"name": "Customer Invoice - Premium",
"type": "cust_invoice",
"prefix": "%(contact_code)s-INV%(y)s%(m)s-",
"padding": 3
}
# Generates: CUST001-INV2301-001, CUST001-INV2301-002
# Department-based numbering
{
"name": "Purchase Orders - IT Department",
"type": "purchase_order",
"prefix": "IT-PO%(Y)s-",
"padding": 5
}
# Generates: IT-PO2023-00001, IT-PO2023-00002
# Daily sequence reset
{
"name": "Daily Stock Movements",
"type": "stock_move",
"prefix": "SM%(Y)s%(m)s%(d)s-",
"padding": 3
}
# Generates: SM20230115-001, SM20230115-002 (resets daily)
Sequence Methods¶
Core Methods¶
# Find sequence by type
def find_sequence(type=None, name=None, model=None, context={}):
"""Find sequence by criteria"""
seq_id = get_model("sequence").find_sequence(type="cust_invoice")
return seq_id
# Get next number
def get_next_number(seq_id, context={}):
"""Generate next sequential number"""
number = get_model("sequence").get_next_number(seq_id, context={
"date": "2023-01-15",
"contact_code": "CUST001"
})
return number
# Increment sequence
def increment_number(seq_id, context={}):
"""Manually increment sequence counter"""
get_model("sequence").increment_number(seq_id)
# Reset sequence
def reset_sequence(seq_id, value=1):
"""Reset sequence to specific value"""
get_model("sequence").reset_sequence(seq_id, value)
Advanced Usage¶
def get_smart_number(self, document_type, context={}):
"""Intelligent sequence selection"""
company_id = get_active_company()
# Try company-specific sequence first
seq_id = get_model("sequence").find_sequence(
type=document_type,
context={"company_id": company_id}
)
if not seq_id:
# Fallback to default sequence
seq_id = get_model("sequence").find_sequence(type=document_type)
if not seq_id:
# Create default sequence if none exists
seq_id = self.create_default_sequence(document_type, company_id)
return get_model("sequence").get_next_number(seq_id, context)
def create_default_sequence(self, doc_type, company_id):
"""Create default sequence for document type"""
type_configs = {
"cust_invoice": {"prefix": "INV%(Y)s-", "padding": 4},
"purchase_order": {"prefix": "PO%(Y)s-", "padding": 4},
"sale_order": {"prefix": "SO%(Y)s-", "padding": 4},
}
config = type_configs.get(doc_type, {"prefix": "", "padding": 4})
return get_model("sequence").create({
"name": f"Default {doc_type.replace('_', ' ').title()}",
"type": doc_type,
"prefix": config["prefix"],
"padding": config["padding"],
"company_id": company_id
})
Running Numbers¶
Sequence Running Model¶
Tracks current numbers for each sequence variation:
class SequenceRunning(Model):
_name = "sequence.running"
_fields = {
"sequence_id": fields.Many2One("sequence", "Sequence"),
"prefix": fields.Char("Resolved Prefix"),
"number": fields.Integer("Current Number"),
"company_id": fields.Many2One("company", "Company"),
}
Manual Number Management¶
# Get current number for specific prefix
def get_current_number(seq_id, resolved_prefix):
running = get_model("sequence.running").search_browse([
["sequence_id", "=", seq_id],
["prefix", "=", resolved_prefix]
])
return running[0].number if running else 0
# Set specific number
def set_sequence_number(seq_id, prefix, number):
running = get_model("sequence.running").search([
["sequence_id", "=", seq_id],
["prefix", "=", prefix]
])
if running:
get_model("sequence.running").write(running, {"number": number})
else:
get_model("sequence.running").create({
"sequence_id": seq_id,
"prefix": prefix,
"number": number
})
Multi-Company Support¶
Company-Specific Sequences¶
class CompanyAwareModel(Model):
def _get_company_number(self, seq_type, context={}):
"""Get number with company isolation"""
company_id = get_active_company()
# Find company-specific sequence
seq_id = get_model("sequence").find_sequence(
type=seq_type,
context={"company_id": company_id}
)
if seq_id:
return get_model("sequence").get_next_number(seq_id, context)
return None
# Usage in model defaults
_defaults = {
"number": lambda self, context: self._get_company_number("sale_order", context),
}
Cross-Company Sequences¶
For shared sequences across companies:
def create_shared_sequence(seq_type, all_companies=True):
"""Create sequence shared across companies"""
return get_model("sequence").create({
"name": f"Shared {seq_type.replace('_', ' ').title()}",
"type": seq_type,
"prefix": "SHARED%(Y)s-",
"padding": 6,
"company_id": None if all_companies else get_active_company()
})
Error Handling¶
Duplicate Number Prevention¶
def create_with_retry(self, vals, context={}, max_retries=5):
"""Create record with duplicate number retry logic"""
for attempt in range(max_retries):
try:
# Generate new number each attempt
if not vals.get("number"):
vals["number"] = self._get_number(context)
return super().create(vals, context)
except database.IntegrityError as e:
if "unique" in str(e).lower() and attempt < max_retries - 1:
# Increment sequence and retry
seq_id = self._find_sequence_for_type(vals)
if seq_id:
get_model("sequence").increment_number(seq_id, context)
continue
raise
raise Exception(f"Failed to create unique number after {max_retries} attempts")
Sequence Validation¶
def validate_sequence_integrity(seq_id):
"""Validate sequence integrity"""
seq = get_model("sequence").browse(seq_id)
# Check for gaps in numbering
running_numbers = get_model("sequence.running").search_browse([
["sequence_id", "=", seq_id]
])
issues = []
for running in running_numbers:
# Check if prefix pattern matches
expected_prefix = seq.get_prefix(seq.prefix, {"date": "2023-01-01"})
if not running.prefix.startswith(expected_prefix.split("%(")[0]):
issues.append(f"Invalid prefix: {running.prefix}")
# Check for reasonable number ranges
if running.number > 999999:
issues.append(f"Sequence number too high: {running.number}")
return issues
Performance Optimization¶
Caching Sequences¶
# Cache frequently used sequences
_sequence_cache = {}
def get_cached_sequence(seq_type, company_id=None):
"""Get sequence with caching"""
cache_key = f"{seq_type}_{company_id or 'global'}"
if cache_key not in _sequence_cache:
seq_id = get_model("sequence").find_sequence(
type=seq_type,
context={"company_id": company_id} if company_id else {}
)
_sequence_cache[cache_key] = seq_id
return _sequence_cache[cache_key]
def clear_sequence_cache():
"""Clear sequence cache"""
global _sequence_cache
_sequence_cache = {}
Bulk Number Generation¶
def generate_bulk_numbers(seq_id, count, context={}):
"""Generate multiple sequential numbers efficiently"""
seq = get_model("sequence").browse(seq_id)
prefix = seq.get_prefix(seq.prefix or "", context)
# Get current number
running = get_model("sequence.running").search_browse([
["sequence_id", "=", seq_id],
["prefix", "=", prefix]
])
start_num = (running[0].number if running else 0) + 1
# Generate number list
padding = seq.padding or 4
numbers = []
for i in range(count):
num_str = str(start_num + i).zfill(padding)
numbers.append(f"{prefix}{num_str}")
# Update running number
new_number = start_num + count - 1
if running:
running[0].write({"number": new_number})
else:
get_model("sequence.running").create({
"sequence_id": seq_id,
"prefix": prefix,
"number": new_number
})
return numbers
Best Practices¶
1. Naming Conventions¶
# Good sequence naming
"Customer Invoice 2023"
"Purchase Order - IT Department"
"Stock Movement - Warehouse A"
# Avoid generic names
"Sequence 1"
"Default"
"Test"
2. Pattern Design¶
# Good patterns - readable and unique
"INV%(Y)s-%(m)s-" # INV2023-01-0001
"%(contact_code)s-SO" # CUST001-SO0001
"PO%(Y)s%(m)s%(d)s-" # PO20230115-001
# Avoid confusing patterns
"%(Y)s%(m)s%(d)s%(H)s" # 2023011514 (too compact)
"A%(y)sB%(m)sC" # A23B01C001 (meaningless)
3. Sequence Management¶
def setup_company_sequences(company_id):
"""Setup standard sequences for new company"""
standard_sequences = [
{"type": "cust_invoice", "prefix": "INV%(Y)s-"},
{"type": "supp_invoice", "prefix": "BILL%(Y)s-"},
{"type": "sale_order", "prefix": "SO%(Y)s-"},
{"type": "purchase_order", "prefix": "PO%(Y)s-"},
]
for seq_config in standard_sequences:
get_model("sequence").create({
"name": f"{seq_config['type'].replace('_', ' ').title()} - {company.name}",
"type": seq_config["type"],
"prefix": seq_config["prefix"],
"padding": 4,
"company_id": company_id
})
Troubleshooting¶
Common Issues¶
- Duplicate Numbers
- Check unique constraints on number fields
- Implement retry logic in create methods
-
Verify sequence configuration
-
Missing Sequences
- Create default sequences for each document type
- Check company-specific sequence setup
-
Verify sequence type matches model usage
-
Wrong Number Format
- Review prefix pattern syntax
- Check context variables passed to sequence
- Validate date/time in context
Debugging¶
def debug_sequence_generation(model_name, context={}):
"""Debug sequence number generation"""
print(f"Debugging sequence for {model_name}")
print(f"Context: {context}")
# Find applicable sequences
sequences = get_model("sequence").search_browse([
["model_id.name", "=", model_name]
])
for seq in sequences:
print(f"Sequence: {seq.name}")
print(f" Type: {seq.type}")
print(f" Prefix pattern: {seq.prefix}")
# Test prefix generation
test_prefix = seq.get_prefix(seq.prefix or "", context)
print(f" Resolved prefix: {test_prefix}")
# Check running numbers
running = seq.running
for r in running:
print(f" Running: {r.prefix} -> {r.number}")
Next Steps¶
- Learn about Multi-Company for company-specific sequences
- Explore Advanced Patterns for complex sequence logic
- Check Models for integration with model defaults
- Review Security for sequence access control