Sequence Generation Architecture
Developer guide for the Enezaframework sequence generation. Part of the Sequence Generation series.
Table of Contents
- Overview
- Component Map
- Base Models with Sequence Support
generate_sequence()save()Hook- Choice System
- Related Guides
Overview
Sequence generation is deliberately small: a mixin that overrides save(), a config model that stores the format and next number, and a pluggable choice system that limits which document types and modules are selectable. EnezaMaster and EnezaDocument already inherit the mixin, so most models need only two attributes.
Component Map
flowchart TD
subgraph Framework["enezaframework"]
MIXIN["SequenceMixin<br/>services/Mixin.py:4"]
GEN["generate_sequence()<br/>services/Mixin.py:5"]
SAVE["save() hook<br/>services/Mixin.py:30"]
CFG["SequenceConfig<br/>models/enezasequence.py:10"]
CHOICES["NumberSequencesDocument / Modules<br/>models/choices.py"]
LOADERS["get_number_sequences_choices()<br/>get_modules_choices()"]
end
subgraph Bases["Base models"]
EM["EnezaMaster(SequenceMixin, EnezaModel)"]
ED["EnezaDocument(SequenceMixin, EnezaModel)"]
EG["EnezaGeneral(EnezaModel)<br/>no sequence"]
end
subgraph App["Your model"]
MODEL["Customer / Invoice"]
end
MIXIN --> EM
MIXIN --> ED
MIXIN --> SAVE
MIXIN --> GEN
GEN --> CFG
CFG --> CHOICES
LOADERS --> CHOICES
EM --> MODEL
ED --> MODEL| Piece | Location | Role |
|---|---|---|
SequenceMixin |
enezaframework/services/Mixin.py:4 |
save() hook and generate_sequence(). |
generate_sequence() |
services/Mixin.py:5 |
Builds the next value from the active config. |
SequenceConfig |
enezaframework/models/enezasequence.py:10 |
Stores format and nextnumber. |
NumberSequencesDocument |
enezaframework/models/choices.py:5 |
Default document-type choices. |
Modules |
models/choices.py:93 |
Default module choices. |
get_number_sequences_choices() |
models/choices.py:70 |
Loads project document choices. |
get_modules_choices() |
models/choices.py:122 |
Loads project module choices. |
EnezaMaster |
enezaframework/models/enezamaster.py:8 |
SequenceMixin, EnezaModel. |
EnezaDocument |
enezaframework/models/enezadocuments.py:7 |
SequenceMixin, EnezaModel. |
EnezaGeneral |
enezaframework/models/enezamaster.py |
No sequence support. |
Base Models with Sequence Support
| Base model | Includes SequenceMixin |
Use |
|---|---|---|
EnezaMaster |
Yes | Master data: customers, vendors, products. |
EnezaDocument |
Yes | Documents: invoices, orders, journals. |
EnezaGeneral |
No | General records with no business number. |
EnezaMasterDetails |
No | One-to-one detail tables. |
EnezaSetup |
No | Configuration/setup records. |
EnezaTransactionalTable |
No | Transactional headers without framework numbering. |
Do not add
SequenceMixinexplicitly.EnezaMasterandEnezaDocumentalready include it, and double inheritance (class X(SequenceMixin, EnezaMaster)) risks MRO problems.
generate_sequence()
SequenceMixin.generate_sequence() (enezaframework/services/Mixin.py:5-28) reads the model's document_name, finds the active config, formats the number, increments the config, and returns the string.
def generate_sequence(self):
sequence_field_name = self.sequence_field_name
document_name = self.document_name
config = SequenceConfig.objects.filter(documents=document_name, active=True).first()
if not config:
raise ValueError(f"No Sequence configuration found for model '{self.__class__.__name__}'. Please set up the sequence configuration under System Administrator.")
next_number = config.nextnumber
formatted_number = f"{next_number:0{config.number_length}d}"
config.nextnumber += 1
config.save()
return f"{config.prefix}{config.delimiter}{formatted_number}"
flowchart TD
A["generate_sequence()"] --> B["read document_name"]
B --> C["filter SequenceConfig<br/>documents=X, active=True"]
C --> D{"config found?"}
D -->|No| E["raise ValueError"]
D -->|Yes| F["next_number = config.nextnumber"]
F --> G["formatted = zero-pad next_number<br/>to number_length"]
G --> H["config.nextnumber += 1; save"]
H --> I["return prefix + delimiter + formatted"]Notes:
- If no active config exists, it raises
ValueError— saving fails loudly rather than writing an empty number. - The number is zero-padded with
f"{next_number:0{config.number_length}d}"; if the number is wider thannumber_length, it is not truncated. config.save()is a separate UPDATE, so each generated sequence is one SELECT plus one UPDATE.
save() Hook
The mixin wraps the model's save() (services/Mixin.py:30-45):
def save(self, *args, **kwargs):
sequence_field_name = self.sequence_field_name
currentobject = self.__class__.objects.filter(pk=self.pk)
if not currentobject:
if not getattr(self, sequence_field_name, None):
sequence_value = self.generate_sequence()
if not sequence_value:
raise ValueError(
f"Failed to generate sequence for field '{sequence_field_name}' in model '{self.__class__.__name__}'."
)
setattr(self, sequence_field_name, sequence_value)
super().save(*args, **kwargs)
Behavior:
- A sequence is generated only when the object is new (no row for its PK) and the sequence field is empty.
- An explicit value on the field is preserved; the mixin never overwrites a supplied number.
- If generation returns empty, a
ValueErroris raised.
sequenceDiagram
participant App as Model.save()
participant Mix as SequenceMixin.save()
participant DB as SequenceConfig
participant Super as EnezaModel.save()
App->>Mix: save()
Mix->>Mix: objects.filter(pk=self.pk)
alt new and field empty
Mix->>DB: filter(documents, active=True).first()
DB-->>Mix: config
Mix->>DB: nextnumber += 1; save()
Mix->>Mix: setattr(field, prefix + delimiter + number)
end
Mix->>Super: super().save()Choice System
SequenceConfig.documents and SequenceConfig.modules use choices= from loader functions, not the classes directly:
get_number_sequences_choices()readssettings.MY_PROJECT_NUMBER_SEQUENCE_DOCUMENT, imports the class, and returns it; otherwise returnsNumberSequencesDocument(models/choices.py:70-90).get_modules_choices()readssettings.MY_PROJECT_MODULES, imports the class, and returns it; otherwise returnsModules(models/choices.py:122-149).
Both warn and fall back when the dotted path cannot be imported. See Sequence Document & Module Choices and Sequence Settings Configuration.