Using SequenceMixin in Models
Developer guide for the Enezaframework sequence generation. Part of the Sequence Generation series.
Table of Contents
- Overview
- Choose the Right Base Model
- Required Attributes
- Basic Model
- Single Sequence Field per Model
- Overriding
generate_sequence() - Inheritance Rules
- 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
# 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_namepoints 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.
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.
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:
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:
class Product(SequenceMixin, EnezaMaster): # Don't double-inherit
...
EnezaMaster and EnezaDocument already include SequenceMixin; adding it again risks MRO problems.