In Cluster ERP a customer, a supplier, an employee, and an owned organisation are not separate cards. Each is a role record in its own module that points at one party (Party) of the parties module, and the web app opens the party card for all of them. This page explains the identity redirect, the role sections on the party card, the role flags, guided creation, and role enabling. The party fields themselves are listed on the parties sub-module.
In short¶
- A role record carries a
party_id: the CRM customer, the procurement supplier, the employee, the assignment, and the owned organisation (src/cluster_erp/modules/crm/customers/models.py:23,src/cluster_erp/modules/procurement/suppliers/models.py:42,src/cluster_erp/modules/owned_organisations/employees/models.py:43,src/cluster_erp/modules/owned_organisations/organisations/models.py:41). IdentityMetaon a role entity tells the web app to open the party card instead of a separate role card (src/cluster_erp/core/metadata.py:304-313).FacetMetaentries on the party metadata show each role's data as sections of the party card (src/cluster_erp/core/metadata.py:357-377,src/cluster_erp/modules/parties/parties/metadata.py:550-763).- The flags
is_customer,is_supplier, andis_employeeon the party follow the role records through thePartyRoleSetevent and a start-up reconciliation (src/cluster_erp/modules/parties/module.py:168-195,src/cluster_erp/server/app.py:1002-1055). - Creating a party with roles, and adding a role later, run in the server layer, which calls several module services in one request transaction (
src/cluster_erp/server/routes/party_composite.py:1-10,src/cluster_erp/server/routes/party_roles.py:1-15).
How it works¶
The model¶
The diagram shows which role records point at a party and which module owns each.
flowchart LR Party["Party (parties)"] Customer["Customer (crm)"] -->|party_id| Party Supplier["Supplier (procurement)"] -->|party_id| Party Employee["Employee (owned_organisations)"] -->|party_id| Party Assignment["Assignment (owned_organisations)"] -->|party_id| Party Org["OwnedOrganisation (owned_organisations)"] -->|party_id| Party Assignment -->|organisation_id| Org Assignment -->|employee_id| Employee
In prose: five role records point at the party with a party_id column. An assignment also points at the employee record and at the owned organisation where the person works (src/cluster_erp/modules/owned_organisations/employees/models.py:69-102). None of these columns is a database foreign key across modules; the employee model states that the manager and party ids are bare UUIDs so that the module boundary stays clean (src/cluster_erp/modules/owned_organisations/employees/models.py:59-61).
Identity redirect¶
IdentityMeta(field, module, entity) names the local column that holds the party id. When it is set, the web app opens the party card instead of the role card (src/cluster_erp/core/metadata.py:304-313). Five entities declare it: customer (src/cluster_erp/modules/crm/customers/metadata.py:30), supplier (src/cluster_erp/modules/procurement/suppliers/metadata.py:27), organisation, employee, and assignment (src/cluster_erp/modules/owned_organisations/organisations/metadata.py:53, src/cluster_erp/modules/owned_organisations/organisations/metadata.py:355, src/cluster_erp/modules/owned_organisations/organisations/metadata.py:590). The intranet directory also declares it (src/cluster_erp/modules/intranet/directory/metadata.py:30).
Role sections on the party card¶
FacetMeta describes a role that a party can play. The web app loads the role records with a plain list query filtered by link_field equal to the party id, and renders the declared sections (src/cluster_erp/core/metadata.py:357-377). The party metadata declares five facets:
| Facet | Sections | Gate | Source |
|---|---|---|---|
crm / customer |
Commercial form; Customer Parts table; RFQ, quotation, and order history tables | none | src/cluster_erp/modules/parties/parties/metadata.py:551-628 |
procurement / supplier |
Supplier form; purchasing RFQ, quotation, and order history tables | none | src/cluster_erp/modules/parties/parties/metadata.py:629-679 |
owned_organisations / organisation |
Departments tree; Employees table of assignments | none | src/cluster_erp/modules/parties/parties/metadata.py:680-698 |
owned_organisations / employee |
Employment form | none | src/cluster_erp/modules/parties/parties/metadata.py:700-713 |
owned_organisations / assignment |
Assignments table across organisations | requires_flag="is_employee" |
src/cluster_erp/modules/parties/parties/metadata.py:718-762 |
The rendering rules are on generic detail.
Role flags¶
The party list filters by role through three booleans on the party (src/cluster_erp/modules/parties/parties/services.py:53-61). Each role module emits PartyRoleSet(party_id, role, active) when it links or unlinks a role record (src/cluster_erp/core/contracts.py:48-55): the customer service (src/cluster_erp/modules/crm/customers/services.py:134-144), the supplier service (src/cluster_erp/modules/procurement/suppliers/services.py:136-145), and the employee service (src/cluster_erp/modules/owned_organisations/employees/services.py:88-101, src/cluster_erp/modules/owned_organisations/employees/services.py:125-140). The parties module subscribes and updates the matching column in its own session (src/cluster_erp/modules/parties/module.py:168-195). At every start, _sync_party_role_flags recomputes the three flags from the live role records and writes only the rows that change (src/cluster_erp/server/app.py:1002-1055).
Guided creation¶
POST /api/v1/parties/composite takes one payload with the party, the list of roles, and one block per role (src/cluster_erp/server/routes/party_composite.py:40-51). The party metadata points the create form at this endpoint through composite_create (src/cluster_erp/modules/parties/parties/metadata.py:319-329).
sequenceDiagram participant C as Client form participant R as create_party_composite participant P as PartyService participant F as Role services C->>R: party, roles, role blocks R->>P: create(party) P-->>R: party R->>F: CustomerService / SupplierService / EmployeeService / AssignmentService .create F-->>R: role ids R-->>C: party_id and role ids
In prose: the route creates the party first (src/cluster_erp/server/routes/party_composite.py:80). For each chosen role it fills party_id, defaults legal_name and tax_no from the party for customer and supplier, validates the block, and calls the role service (src/cluster_erp/server/routes/party_composite.py:84-126). For an employee, fields gated by a permission are dropped for a caller without that permission (src/cluster_erp/server/routes/party_composite.py:114-120). A first assignment block must name the organisation, position, employment type, and start date, or the call fails with PARTY_ASSIGNMENT_INCOMPLETE (src/cluster_erp/server/routes/party_composite.py:127-145). The request session commits everything together or rolls everything back (src/cluster_erp/server/routes/party_composite.py:4-9).
Adding a role later¶
POST /api/v1/parties/{party_id}/roles/customer, /roles/supplier, and /roles/employee add a role to an existing party (src/cluster_erp/server/routes/party_roles.py:58-123). The party card offers them as the actions Enable Customer Role, Enable Supplier Role, and Enable Employee Role, each shown only while the matching flag is false (src/cluster_erp/modules/parties/parties/metadata.py:393-438). Removing a role is the role record's own deactivation; there is no route to remove a role from the party side (src/cluster_erp/server/routes/party_roles.py:12-14).
Name and tax id follow the party¶
When a party's display name or tax id changes, PartyUpdated carries the new values (src/cluster_erp/modules/parties/parties/services.py:201-212). The crm module copies them into the customer's legal_name and tax_no (src/cluster_erp/modules/crm/module.py:115-135), and the procurement module subscribes to the same event (src/cluster_erp/modules/procurement/module.py:122).
Deactivation follows the party¶
When a party is deleted, PartyDeleted is emitted (src/cluster_erp/modules/parties/parties/services.py:216-233). The owned_organisations module soft-deletes the person's employee record with the reason party_deactivated, and restores exactly those rows when the party is reactivated (src/cluster_erp/modules/owned_organisations/module.py:348-420). The crm and procurement modules also subscribe to PartyDeleted (src/cluster_erp/modules/crm/module.py:96, src/cluster_erp/modules/procurement/module.py:125).
Parties created on behalf of other modules¶
- An owned organisation created without a
party_idasksEnsurePartyForOrganisation; thepartiesmodule returns a live party with the same display name or creates an organisation party (src/cluster_erp/modules/owned_organisations/organisations/services.py:74-94,src/cluster_erp/modules/parties/module.py:324-357). - On
CustomerCreated, thepartiesmodule finds or creates an organisation party with the customer's legal name (src/cluster_erp/modules/parties/module.py:445-487). - At start-up,
_sync_customers_to_partiescreates a party for every live customer without one, matched by name, and links it (src/cluster_erp/server/app.py:951-999).
Why it is built this way¶
- The project's written engineering rules state that everything is a party: customer, organisation, employee, and supplier are one party card with role facets, roles go through
FacetMetaandPartyRoleSet, and modules do not import the party module directly. - The composite and role routes state that they are server-level orchestration so that modules never import one another (
src/cluster_erp/server/routes/party_composite.py:4-9,src/cluster_erp/server/routes/party_roles.py:4-10). - The start-up reconciliation states that it fixes anything a handler missed, for example a role record created against a party that was not yet committed (
src/cluster_erp/server/app.py:1003-1008).
Rules and boundaries¶
- Modules communicate about parties only through contracts. A test checks that no module imports another, except the permission guards of
auth(tests/test_architecture_boundaries.py:1-11,tests/test_architecture_boundaries.py:46). The server routes for guided creation and role enabling sit outsidemodules/and import the module services directly (src/cluster_erp/server/routes/party_composite.py:61-73). - One live employee record per party (
src/cluster_erp/modules/owned_organisations/employees/models.py:33-40), one live owned organisation per party (src/cluster_erp/modules/owned_organisations/organisations/models.py:28-35), and one live supplier per party (src/cluster_erp/modules/procurement/suppliers/models.py:34-36). - The assignments facet appears only on a party whose
is_employeeflag is true (src/cluster_erp/modules/parties/parties/metadata.py:722).
Interaction with other parts¶
| Module | Role record | Contracts used | Page |
|---|---|---|---|
crm |
Customer |
emits PartyRoleSet, CustomerCreated; subscribes PartyUpdated, PartyDeleted |
CRM module |
procurement |
Supplier |
emits PartyRoleSet; subscribes PartyUpdated, PartyDeleted |
Procurement module |
owned_organisations |
Employee, Assignment, OwnedOrganisation |
emits PartyRoleSet; queries EnsurePartyForOrganisation; subscribes PartyDeleted, RecordActivated |
Owned organisations module |
All producers and consumers are on the contracts reference.
Limits and open points¶
EnsurePartyForOrganisation,CustomerCreated, and the start-up customer sync match an existing party by display name only (src/cluster_erp/modules/parties/module.py:338-347,src/cluster_erp/modules/parties/module.py:462-470,src/cluster_erp/server/app.py:969-976).PartyUpdateaccepts the three role flags (src/cluster_erp/modules/parties/parties/schemas.py:88-90), so a direct update can set a flag without a role record. The next start-up reconciliation sets it back to the role records' state (src/cluster_erp/server/app.py:1035-1051).EmployeeService.bulk_deletesoft-deletes rows without emittingPartyRoleSet(src/cluster_erp/modules/owned_organisations/employees/services.py:142-145); the flag is corrected by the next start-up reconciliation.
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| facet | FacetMeta |
A role a party can play, shown as a section of the party card. | rol bölümü | src/cluster_erp/core/metadata.py:357 |
| identity redirect | IdentityMeta |
Metadata that makes a role entity open the party card. | Kimlik yönlendirmesi | src/cluster_erp/core/metadata.py:304 |
| guided creation | composite_create |
Creating a party and its chosen roles in one request. | Rehberli oluşturma | src/cluster_erp/modules/parties/parties/metadata.py:319 |
| role event | PartyRoleSet |
The event a role module emits when it links or unlinks a role record. | Rol olayı | src/cluster_erp/core/contracts.py:48 |
Related pages¶
- Parties module
- Parties sub-module
- Owned organisations module
- Decision record on party unification
- Event bus
- Generic detail
Verified against commit 92f8051 on 2026-11-13.