Using SequenceMixin in Models

Developer guide for the Enezaframework sequence generation. Part of the Sequence Generation series.

Table of Contents

  1. Overview
  2. Choose the Right Base Model
  3. Required Attributes
  4. Basic Model
  5. Single Sequence Field per Model
  6. Overriding generate_sequence()
  7. Inheritance Rules
  8. Related Guides

Overview

SequenceMixin is already part of EnezaMaster and EnezaDocument. Adding sequence generation to a model means choosing one of those bases, declaring a nullable field, and setting two class attributes. You do not import or inherit the mixin directly.


Choose the Right Base Model

Base model Sequence support Use for
EnezaMaster Yes Master data: Customer, Vendor, Product, Employee.
EnezaDocument Yes Documents: Invoice, Purchase Order, Journal.
EnezaGeneral No Records that need no business number.

See Base Models & Model Types for the full base-model reference.


Required Attributes

Attribute Purpose
sequence_field_name The field that stores the generated number.
document_name The document type looked up in SequenceConfig. Must match a configured value.

The sequence field must be nullable (null=True, blank=True) because it is populated on first save.

flowchart TD
    M["Your model"] --> A{"Inherits EnezaMaster<br/>or EnezaDocument?"}
    A -->|No| X["No sequence generation"]
    A -->|Yes| B["Define sequence_field_name"]
    B --> C["Define document_name"]
    C --> D{"Active SequenceConfig<br/>for document_name?"}
    D -->|No| E["ValueError on save"]
    D -->|Yes| F["Field populated on first save"]

Basic Model

python
# myapp/models/invoice.py
from django.db import models
from enezaframework.models import EnezaDocument
from myapp.choices import ProjectDocumentTypes


class Invoice(EnezaDocument):
    invoice_number = models.CharField(
        max_length=50,
        null=True,
        blank=True,
        verbose_name="Invoice Number",
    )
    customer = models.ForeignKey(
        "crm.Customer",
        on_delete=models.CASCADE,
        verbose_name="Customer",
    )
    amount = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        verbose_name="Amount",
    )
    date = models.DateField(
        verbose_name="Date",
    )

    sequence_field_name = 'invoice_number'
    document_name = ProjectDocumentTypes.INVOICE

    class Meta:
        verbose_name = "Invoice"
        verbose_name_plural = "Invoices"

At save time, SequenceMixin.save() fills invoice_number only when it is empty on a new record.


Single Sequence Field per Model

The standard mixin manages one sequence field per model, identified by sequence_field_name:

  • Only one field can be auto-managed.
  • sequence_field_name points at a single field name.
  • The save() hook only checks and sets that one field.

Recommended approach: use a single sequence field per model. For related sequential data, prefer separate models or computed fields.

If you truly need a second sequence, override save() and generate it after the primary sequence. The mixin's save() calls super().save(), so a subclass override must call super().save() to let the primary sequence run, then generate the secondary one.

python
class ComplexDocument(EnezaMaster):
    primary_seq = models.CharField(
        max_length=50,
        null=True,
        blank=True,
        verbose_name="Primary Sequence",
    )
    secondary_seq = models.CharField(
        max_length=50,
        null=True,
        blank=True,
        verbose_name="Secondary Sequence",
    )

    sequence_field_name = 'primary_seq'
    document_name = ProjectDocumentTypes.COMPLEX_DOC

    def save(self, *args, **kwargs):
        is_new = self._state.adding

        # Let SequenceMixin handle primary_seq.
        super().save(*args, **kwargs)

        # Generate the secondary sequence after the first save.
        if is_new and not self.secondary_seq:
            secondary_config = SequenceConfig.objects.filter(
                documents=ProjectDocumentTypes.SECONDARY_DOC,
                active=True,
            ).first()
            if secondary_config:
                formatted = f"{secondary_config.nextnumber:0{secondary_config.number_length}d}"
                self.secondary_seq = (
                    f"{secondary_config.prefix}{secondary_config.delimiter}{formatted}"
                )
                secondary_config.nextnumber += 1
                secondary_config.save()
                super().save(update_fields=['secondary_seq'])

Warnings: this pattern is harder to reason about under concurrency (two configure rows, two UPDATEs), and the increment is not atomic. Prefer a single sequence field when possible.


Overriding generate_sequence()

Every model that includes the mixin can override generate_sequence() for custom formatting. The save() hook still calls it when the field is empty on a new object.

python
class YearlyInvoice(EnezaMaster):
    invoice_number = models.CharField(
        max_length=50,
        null=True,
        blank=True,
        verbose_name="Invoice Number",
    )
    year = models.IntegerField(
        verbose_name="Year",
    )

    sequence_field_name = 'invoice_number'
    document_name = ProjectDocumentTypes.YEARLY_INVOICE

    def generate_sequence(self):
        config = SequenceConfig.objects.filter(
            documents=self.document_name,
            active=True,
        ).first()
        if not config:
            raise ValueError("No active sequence configuration for yearly invoices.")
        number = f"{config.nextnumber:0{config.number_length}d}"
        config.nextnumber += 1
        config.save()
        return f"{config.prefix}-{self.year}-{number}"

Keep the config increment in the override if you want it to advance, or delegate to super().generate_sequence() and reformat.


Inheritance Rules

Correct:

python
class Product(EnezaMaster):
    product_code = models.CharField(
        max_length=50,
        null=True,
        blank=True,
        verbose_name="Product Code",
    )
    name = models.CharField(
        max_length=200,
        verbose_name="Name",
    )

    sequence_field_name = 'product_code'
    document_name = ProjectDocumentTypes.PRODUCT

Incorrect:

python
class Product(SequenceMixin, EnezaMaster):   # Don't double-inherit
    ...

EnezaMaster and EnezaDocument already include SequenceMixin; adding it again risks MRO problems.