Sequence Generation Architecture

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

Table of Contents

  1. Overview
  2. Component Map
  3. Base Models with Sequence Support
  4. generate_sequence()
  5. save() Hook
  6. Choice System
  7. 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 SequenceMixin explicitly. EnezaMaster and EnezaDocument already 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.

python
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 than number_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):

python
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 ValueError is 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() reads settings.MY_PROJECT_NUMBER_SEQUENCE_DOCUMENT, imports the class, and returns it; otherwise returns NumberSequencesDocument (models/choices.py:70-90).
  • get_modules_choices() reads settings.MY_PROJECT_MODULES, imports the class, and returns it; otherwise returns Modules (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.