Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Business modules

Explanation

Party unification

How one party card carries the customer, supplier, employee, and owned-organisation roles as facets, kept in step over the EventBus.

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).
  • IdentityMeta on 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).
  • FacetMeta entries 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, and is_employee on the party follow the role records through the PartyRoleSet event 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_id asks EnsurePartyForOrganisation; the parties module 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, the parties module 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_parties creates 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 FacetMeta and PartyRoleSet, 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 outside modules/ 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_employee flag 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).
  • PartyUpdate accepts 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_delete soft-deletes rows without emitting PartyRoleSet (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

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13