Status: Accepted, stated in the project's written engineering rules. This record covers the rule that a person or an organisation exists once in Cluster ERP, as a party (Party), and that customer, supplier, owned organisation, and employee are roles of that party, shown as facets on one card. The module is described on parties module (parties).
Context¶
Customers, suppliers, owned organisations, and employees live in different modules: crm, procurement, and owned_organisations. Without a common record the same company or person would be typed and maintained in several places. The project's written engineering rules state that everything is a party: one party card with role facets instead of separate cards, roles declared with FacetMeta and synchronised with PartyRoleSet, and no module importing the parties module. Further rationale is not determined from code.
Decision¶
- The party is the canonical record. Its flags
is_customer,is_supplier, andis_employeesay which roles it plays (src/cluster_erp/modules/parties/parties/models.py:33-35). - Each role record holds a
party_id. Its metadata declaresIdentityMeta, so the web app opens the party card instead of a separate role card (src/cluster_erp/core/metadata.py:304-313). Seven entity metadata objects declare it (counted withgrep -rn "identity=IdentityMeta"). - The party card lists the roles as
FacetMetaentries, each loaded by a plain query filter onparty_id(src/cluster_erp/core/metadata.py:357-377,src/cluster_erp/modules/parties/parties/metadata.py:550-722). - A role module that links or unlinks a party emits
PartyRoleSet; the parties module sets the matching flag (src/cluster_erp/core/contracts.py:47-55).
The sequence diagram shows a supplier being linked to a party.
sequenceDiagram participant Sup as procurement SupplierService participant Bus as EventBus participant Par as parties Module participant DB as Party row Sup->>Sup: create Supplier with party_id Sup->>Bus: emit PartyRoleSet(role supplier, active true) Bus->>Par: _handle_party_role_set Par->>DB: set is_supplier and updated_at
In prose: after the supplier row is flushed, the service emits PartyRoleSet with the party id, the role, and active=True (src/cluster_erp/modules/procurement/suppliers/services.py:130-145). The parties module subscribed to that event (src/cluster_erp/modules/parties/module.py:130). Its handler maps the role to the flag column and updates the party in its own session (src/cluster_erp/modules/parties/module.py:168-192).
Consequences¶
- The party card shows five facets: customer (
crm), supplier (procurement), owned organisation, employee, and assignment (owned_organisations). The assignment facet appears only whenis_employeeis set (src/cluster_erp/modules/parties/parties/metadata.py:551,src/cluster_erp/modules/parties/parties/metadata.py:629,src/cluster_erp/modules/parties/parties/metadata.py:680,src/cluster_erp/modules/parties/parties/metadata.py:700,src/cluster_erp/modules/parties/parties/metadata.py:718-722). - Creating a party with roles in one step is application-layer orchestration in
server/routes/party_composite.py, declared to the client bycomposite_create(src/cluster_erp/server/routes/party_composite.py:1-10,src/cluster_erp/modules/parties/parties/metadata.py:319). - Adding a role to an existing party is also application-layer orchestration (
src/cluster_erp/server/routes/party_roles.py:1-10). - A start-up pass reconciles the flags with the facets that exist, as a safety net for missed events (
src/cluster_erp/server/app.py:1002-1008). - The flag update goes through the ORM: the handler loads the party, sets the flag and commits, so change history records the change and
updated_atrefreshes byonupdate(src/cluster_erp/modules/parties/module.py:184-193).
Where the code enforces it¶
| Mechanism | What it checks | Source |
|---|---|---|
test_guided_party_creation_is_declared_in_metadata |
Guided creation offers exactly the roles customer, supplier, and employee, each with its flag field and data groups |
tests/test_metadata_driven_rules.py:51-60 |
tests/test_party_form_contract.py |
The party form's metadata, schemas, error codes, and translations agree | tests/test_party_form_contract.py:1-6 |
| Module boundary test | No role module imports the parties module | tests/test_architecture_boundaries.py:46-57 |
_sync_party_role_flags |
Flags match existing facets after every start | src/cluster_erp/server/app.py:1002-1008 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.