PartyModelMixin & Model Integration

Developer guide for the Enezaframework party system. Part of the Party System series.

Table of Contents

  1. Overview
  2. The Three Requirements
  3. MRO Ordering
  4. How Party Creation Works
  5. get_party_defaults()
  6. Related Name
  7. Complete Model Example
  8. Model Field Conventions
  9. Related Guides

Overview

PartyModelMixin is an abstract Django model (enezaframework/models/ezaparty.py:5). Adding it to a business model contributes a party foreign key to the shared Party model and opts the model into automatic party creation and detail-page inlines.

python
from enezaframework.models import EnezaMaster, PartyModelMixin

class Customer(PartyModelMixin, EnezaMaster):
    ...

That single line replaces a manually declared party = ForeignKey(...) and all the wiring that went with it.


The Three Requirements

  1. Inherit PartyModelMixin first. It must appear before EnezaMaster in the base-class list.
  2. Implement party_category. A property returning a PartyCategory value ('PRS' or 'ORG').
  3. Optionally override get_party_defaults(). Customize the display_name (and any other Party field). Call super() to keep the auto-included entity and party_type.
classDiagram
    class PartyModelMixin {
        party: ForeignKey
        party_category = None
        party_related_name = None
        +get_party_defaults() dict
        +_resolve_party_related_name() str
    }
    class EnezaMaster
    class Customer {
        customer_no
        customer_type
        +party_category str
        +get_party_defaults() dict
    }
    PartyModelMixin <|-- Customer : first base
    EnezaMaster <|-- Customer

MRO Ordering

python
class YourModel(PartyModelMixin, EnezaMaster):   # PartyModelMixin FIRST

Django resolves abstract-model fields through the method resolution order. Putting PartyModelMixin first makes the party field resolve consistently. If the base models are reversed, the contributed field can be shadowed or the model's field set can be surprising — keep the mixin first.


How Party Creation Works

The mixin only supplies the field and defaults; creation is done by a signal.

flowchart TD
    SAVE["YourModel.save()"] --> SIG["auto_create_party_for_party_model<br/>enezaframework/signals.py:9"]
    SIG --> CHECK{"created and<br/>party is None and<br/>party_category set?"}
    CHECK -->|No| SKIP["Return without creating"]
    CHECK -->|Yes| CREATE["Party.objects.create(**get_party_defaults())"]
    CREATE --> LINK["instance.party = party<br/>instance.save(update_fields=['party'])"]
    LINK --> PROFILE["platform signal creates<br/>Person or Business"]
  • The receiver only acts on create (created=True) and only when instance.party is still None.
  • If party_category is None, the receiver logs a warning and skips creation. A model with the mixin but no category is treated as intentionally non-party.

See Party Types & Profiles for the profile side.


get_party_defaults()

The base implementation (enezaframework/models/ezaparty.py:21-30) returns:

python
{
    'party_type': self.party_category,
    'display_name': str(self),
    'is_active': True,
    # 'entity': <instance.entity>  # auto-included when the model has one
}

Override it to improve the display name or mirror a business flag:

python
def get_party_defaults(self):
    defaults = super().get_party_defaults()
    defaults['display_name'] = self.customer_no or self.name or 'Customer'
    return defaults

Always call super() first. The base class reads entity off the instance and includes it, which is required for entity-scoped data.

display_name falls back to str(self). Because __str__ often reads self.party.display_name, a brand-new instance can return an empty string before the party exists. Supplying an explicit display_name in get_party_defaults() avoids that.


The party FK uses related_name='%(class)s_party', which resolves to <modelname>_party (for example customer_party). Override party_related_name on the model only when you need a custom reverse accessor:

python
party_related_name = 'billing_customers'

_resolve_party_related_name() (ezaparty.py:32-34) returns cls.party_related_name or '%(class)s_party'. This is internal; do not call it from application code.


Complete Model Example

python
# myapp/models/customer.py
from django.db import models
from enezaframework.models import EnezaMaster, PartyModelMixin
from enezacommon.models.choices import PartyCategory


class Customer(PartyModelMixin, EnezaMaster):
    customer_no = models.CharField(
        max_length=50,
        verbose_name="Customer No.",
    )
    customer_type = models.CharField(
        max_length=5,
        choices=PartyCategory.choices,
        verbose_name="Customer Type",
    )
    name = models.CharField(
        max_length=255,
        verbose_name="Name",
    )
    email = models.CharField(
        max_length=255,
        blank=True,
        null=True,
        verbose_name="Email",
    )

    sequence_field_name = 'customer_no'
    document_name = 'CUSTOMER'

    @property
    def party_category(self):
        return self.customer_type

    def get_party_defaults(self):
        defaults = super().get_party_defaults()
        defaults['display_name'] = self.name or self.customer_no or 'Customer'
        return defaults

    def __str__(self):
        if self.party and self.party.display_name:
            return self.party.display_name
        return self.customer_no or ''

    class Meta:
        db_table = 'customer'
        verbose_name = "Customer"
        verbose_name_plural = "Customers"

Re-export the model so it can be imported from the package root:

python
# myapp/models/__init__.py
from .customer import Customer

Never include party in Fieldcolumns. The framework hides it from forms automatically; declaring it manually can cause a duplicate or unexpected form field.


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, the closing parenthesis goes on its own line, aligned with the start of the field declaration.
python
    customer_type = models.CharField(
        max_length=5,
        choices=PartyCategory.choices,
        verbose_name="Customer Type",
    )