Skip to content

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 rules
  • sequence.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

  1. Duplicate Numbers
  2. Check unique constraints on number fields
  3. Implement retry logic in create methods
  4. Verify sequence configuration

  5. Missing Sequences

  6. Create default sequences for each document type
  7. Check company-specific sequence setup
  8. Verify sequence type matches model usage

  9. Wrong Number Format

  10. Review prefix pattern syntax
  11. Check context variables passed to sequence
  12. 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