Base Models & Model Types

Developer guide for the Enezaframework. Part of the architecture series.

Table of Contents

  1. Overview
  2. Base Model Reference
  3. Choosing a Base Model
  4. The Entity Model
  5. How to Add a Model
  6. Model Field Conventions
  7. Related Guides

Overview

Enezaframework provides abstract base models that carry the fields and behaviors every business record needs: primary key, timestamps, audit user tracking, history, and (for most types) an entity foreign key for multi-tenant separation. Application models inherit from one of these instead of models.Model directly.

All base models are abstract; inheriting from them does not create a table by itself. The framework uses simple_history for change tracking.

erDiagram
    EnezaModel ||--o{ EnezaMaster : "abstract parent"
    EnezaModel ||--o{ EnezaMasterDetails : "abstract parent"
    EnezaModel ||--o{ EnezaGeneral : "abstract parent"
    EnezaModel ||--o{ EnezaDocument : "abstract parent"
    EnezaModel ||--o{ EnezaDocumentLines : "abstract parent"
    EnezaModel ||--o{ EnezaTransactionalTable : "abstract parent"
    EnezaModel ||--o{ EnezaSetup : "abstract parent"
    EnezaModel ||--o{ EnezaParameter : "abstract parent"
    EnezaModel ||--o{ Entity : "concrete parent"

Base Model Reference

EnezaModel

The most fundamental base model. Defined in enezaframework/models/enezaModel.py.

Field Type Notes
id UUIDField Primary key, generated automatically.
created_at DateTimeField auto_now_add=True.
updated_at DateTimeField auto_now=True.
created_by ForeignKey to enezacommon.User Set on first save.
modified_by ForeignKey to enezacommon.User Updated on every save.
history HistoricalRecords simple_history tracking.

Note: the audit field is modified_by, not updated_by. EnezaModel does not define entity or is_active. Each concrete base class adds its own entity foreign key. Do not assume is_active exists on a model unless that model defines it.

Convenience helpers on EnezaModel:

python
class MyModel(EnezaModel):
    ...

MyModel.search("acme", "name", "code")   # case-insensitive search across fields
instance.get_age()                        # timedelta since created_at
instance.was_updated_recently(days=1)     # bool

EnezaMaster

class EnezaMaster(SequenceMixin, EnezaModel) — from enezaframework/models/enezamaster.py.

  • Adds entity (ForeignKey to Entity, on_delete=PROTECT, nullable).
  • Includes SequenceMixin, so it can generate sequential business numbers (for example CUST0001, VEND0002).
  • Intended for master records referenced by transactions: Customer, Vendor, Item, BankAccount.
  • Entity specific: yes.
  • Custom manager: EnezaManager.

EnezaMasterDetails

class EnezaMasterDetails(EnezaModel) — from enezaframework/models/enezamaster.py.

  • Adds entity.
  • Intended for one-to-one detail tables linked to a master record, keeping the main table lean.
  • Entity specific: yes.

EnezaGeneral

class EnezaGeneral(EnezaModel) — from enezaframework/models/enezamaster.py.

  • Adds entity.
  • A general-purpose abstract base for records that are neither master data, documents, nor setup.

EnezaDocument

class EnezaDocument(SequenceMixin, EnezaModel) — from enezaframework/models/enezadocuments.py.

  • Adds entity and document numbering via SequenceMixin.
  • Intended for the header of transactional documents such as SalesInvoiceHeader, PurchaseOrderHeader, JournalBatch.
  • Entity specific: yes.

EnezaDocumentLines

class EnezaDocumentLines(EnezaModel) — from enezaframework/models/enezadocuments.py.

  • Adds entity.
  • Intended for line items belonging to a document header, typically one-to-many via a ForeignKey back to the header.
  • Entity specific: yes.

EnezaTransactionalTable

class EnezaTransactionalTable(EnezaModel) — from enezaframework/models/enezabtransactional.py.

  • Adds entity only. It does not include SequenceMixin.
  • Use it for a transactional header when you do not want framework-managed numbering; use EnezaDocument when you do.

EnezaSetup

class EnezaSetup(EnezaModel) — from enezaframework/models/enezacsetups.py.

  • Adds entity.
  • Intended for configuration/setup records that control system behavior: LedgerSetup, PaymentTerms, PostingGroup, Currency.
  • Entity specific: often yes, some setups may be global.

EnezaParameter

class EnezaParameter(EnezaModel) — from enezaframework/models/enezacsetups.py.

  • Adds entity.
  • Intended for single-instance parameter models (typically one record per entity), often edited through a parameter view. See CRUD Base Views.

EnezaMasterDetail

class EnezaMasterDetail(EnezaModel) exists at enezaframework/models/enezamasterdetail.py but is not re-exported from enezaframework/models/__init__.py, so from enezaframework.models import EnezaMasterDetail fails on the package root. Prefer EnezaMasterDetails (re-exported) unless you import the module directly.

The architecture reference describes a framework signal that auto-creates the detail row for EnezaMasterDetail children. Verify this signal in the current source before depending on it; do not rely on an external row being created automatically without confirming the receiver exists.


Choosing a Base Model

flowchart TD
    START[What kind of record?] --> SETUP{Configuration<br/>or settings?}
    SETUP -->|Single instance per entity| PARAM[EnezaParameter]
    SETUP -->|Many setup rows| ESETUP[EnezaSetup]
    SETUP -->|No| DOC{Part of a document?}
    DOC -->|Header, needs numbering| EDOC[EnezaDocument]
    DOC -->|Header, no numbering| ETRAN[EnezaTransactionalTable]
    DOC -->|Line items| ELINES[EnezaDocumentLines]
    DOC -->|No| MASTER{Master data with<br/>a business number?}
    MASTER -->|Yes| EMASTER[EnezaMaster]
    MASTER -->|One-to-one detail| EDETAILS[EnezaMasterDetails]
    MASTER -->|No| EGEN[EnezaGeneral]

The Entity Model

Entity is the central model representing a legal entity (company, organization, or individual) for which records are kept. Most records link to an Entity, which is how the ERP separates data per company/branch.

  • Defined in enezaframework/models/Entity.py (framework, not the common app).
  • It is a concrete model: class Entity(PartyModelMixin, EnezaModel).
  • Database table: entity.
  • Key fields include name, company_code, full_legal_name, slug, logo, document_logo, tax_registration_number, default_currency, and tax-related fields.

Because Entity inherits from EnezaModel, it uses the same UUID primary key and audit/history fields as everything else.


How to Add a Model

  1. Create a file in the app's models/ package, for example accountpayable/models/vendortable.py.
  2. Import the base model from enezaframework.models and inherit from it.
  3. Declare fields following the project field conventions below.
  4. Re-export the model in models/__init__.py.
  5. Generate and apply migrations.
python
# accountpayable/models/vendortable.py
from django.db import models
from enezaframework.models import EnezaMaster
from enezacommon.models import VendorType


class VendorTable(EnezaMaster):
    vendorno = models.CharField(
        max_length=50,
        unique=True,
        verbose_name="Vendor No.",
    )
    searchname = models.CharField(
        max_length=255,
        verbose_name="Search Name",
    )
    vendortype = models.CharField(
        max_length=50,
        choices=VendorType.choices,
        verbose_name="Vendor Type",
    )
    vendorgroup = models.ForeignKey(
        "VendorGroup",
        on_delete=models.PROTECT,
        verbose_name="Vendor Group",
    )
    blocked = models.BooleanField(
        default=False,
        verbose_name="Blocked",
    )

    class Meta:
        verbose_name = "Vendor"
        verbose_name_plural = "Vendors"

    def __str__(self):
        return self.searchname
python
# accountpayable/models/__init__.py
from .vendortable import VendorTable
from .vendorgroup import VendorGroup
from .vendortabledetails import VendorTableDetails
bash
python manage.py makemigrations accountpayable
python manage.py migrate

Next, add the views in CRUD Base Views and the navigation entries in Menu & Navigation Configuration.


Model Field Conventions

These conventions are mandatory for this project:

  1. One field per line. Never declare multiple fields on the same line.
  2. Always set verbose_name. Every field must include a verbose_name keyword argument.
  3. Multi-line fields close on their own line. When a field declaration spans multiple lines (for example a ForeignKey with related_name, on_delete, help_text), the closing parenthesis goes on its own line, aligned with the start of the field declaration.
python
    vendorgroup = models.ForeignKey(
        "VendorGroup",
        on_delete=models.PROTECT,
        related_name="vendors",
        verbose_name="Vendor Group",
    )