PartyModelMixin & Model Integration
Developer guide for the Enezaframework party system. Part of the Party System series.
Table of Contents
- Overview
- The Three Requirements
- MRO Ordering
- How Party Creation Works
get_party_defaults()- Related Name
- Complete Model Example
- Model Field Conventions
- 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.
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
- Inherit
PartyModelMixinfirst. It must appear beforeEnezaMasterin the base-class list. - Implement
party_category. A property returning aPartyCategoryvalue ('PRS'or'ORG'). - Optionally override
get_party_defaults(). Customize thedisplay_name(and any otherPartyfield). Callsuper()to keep the auto-includedentityandparty_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 <|-- CustomerMRO Ordering
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 wheninstance.partyis stillNone. - If
party_categoryisNone, 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:
{
'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:
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_namefalls back tostr(self). Because__str__often readsself.party.display_name, a brand-new instance can return an empty string before the party exists. Supplying an explicitdisplay_nameinget_party_defaults()avoids that.
Related Name
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:
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
# 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:
# myapp/models/__init__.py
from .customer import Customer
Never include
partyinFieldcolumns. 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:
- One field per line. Never declare multiple fields on the same line.
- Always set
verbose_name. Every field must include averbose_namekeyword argument. - 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.
customer_type = models.CharField(
max_length=5,
choices=PartyCategory.choices,
verbose_name="Customer Type",
)