Task: Party/counterparty Create dialog cannot set parent_party_id
Table of Contents
This page documents a task in the Commission: party, counterparty, and party_status story. It captures the goal, current status, acceptance, and any notes or results.
Goal
Found while re-running the party/counterparty Qt UI scenarios: Create
fails for any party/counterparty beyond the tenant's first, with
Repository error: Executing INSERT failed: ERROR: duplicate key
value violates unique constraint "parties_root_party_uniq_idx" (the
DB allows exactly one root party – parent_party_id IS NULL – per
tenant, non-System category). parent_party_id is never exposed as a
Detail field in party.org=/=counterparty.org's Qt facet (same
precedent as book's parent_portfolio_id, set programmatically by the
host rather than user-picked), and PartyHierarchyTab=/
=CounterpartyHierarchyTab are read-only tree displays with no way to
assign a parent either. Net effect: today there is genuinely no UI
path to create a second party or counterparty at all.
Add a nullable/optional "Parent Party" (resp. "Parent Counterparty") dynamic combo to the Create/Edit dialog – self-referencing FK, allow-blank for root – populated from the existing party/counterparty list, excluding the entity's own id when editing (to prevent self-parenting) and, ideally, its own descendants (to prevent cycles; confirm whether the server already validates this or if it needs a DB-side check too).
This is new template territory: there's no existing self-referencing
nullable dynamic-combo precedent in the codegen templates yet (the
combo_allow_blank facet exists for plain lookup dynamic combos, but
not for "combo sourced from the same entity, excluding self/
descendants"). Scope whether to generalize this as a facet other
self-referencing hierarchical entities could reuse, or keep it
party/counterparty-specific for now given no other entity currently
needs it.
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Commission: party, counterparty, and party_status |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-07-15 |
Acceptance
- Party and counterparty Create/Edit dialogs gain a required "Parent Party"/"Parent Counterparty" combo, sourced from the existing entity list.
- Creating a second (non-root) party/counterparty with a parent selected succeeds; Save stays disabled (consistent with every other required field) until a parent is chosen.
- Self-parenting is impossible by construction (the new entity's id doesn't exist yet when the combo populates on Create). Descendant- cycle prevention on Edit is explicitly out of scope – see Notes.
- Builds cleanly; manually verified live for both party and counterparty.
Plan
Simplified scope per discussion: the combo is required, not blank-allowed – "you must supply a valid parent party" to create one. This sidesteps the harder "root party via UI" case entirely (root is provisioned via GLEIF publish, never manually).
Found the existing dynamic_combo facet mechanism already generalizes
to a self-referencing UUID-valued combo almost for free:
is_uuid=/=combo_code_fieldalready drive a UUID-keyed dynamic combo correctly (used for e.g. locked key fields) – just never combined withcombo_domain_typepointing at the same entity before.required_dynamic_combo_fieldsalready generates thecurrentIndex() >0= check invalidateInput()generically – no new validation machinery needed, just mark the fieldis_required.- Gap found: the non-locked
is_dynamic_combobranches ofupdateUiFromX()=/=updateXFromUi()(tangled fromores.cpp.qt.detail_dialog_impl.org) only handle plainstd::string-valued combos, notis_uuid=/=is_optional_uuid. Sinceparent_party_idisstd::optional<boost::uuids::uuid>(nullable at the DB/domain level – the root party genuinely has none), extended both branches to handleis_optional_uuid(parse/round-trip throughstd::optional, unset on an empty combo selection). - Self-parenting is naturally impossible on Create (the new party's id doesn't exist yet when the combo is populated, so it can't appear in its own parent list); descendant-cycle prevention on Edit is out of scope for this pass (Edit's parent-reassignment isn't exercised by the failing scenario) – captured as a note below if picked up later.
Steps:
- Add
fetch_parties=/=fetch_counterpartiestoLookupFetcher(same shape asfetch_party_id_schemesetc., reusing the existingget_parties_request=/=get_counterparties_requestwith a large limit – matchesPartyChildEntityTables's existinglimit=1000precedent rather than building a searchable/paginated combo). - Extend the
is_dynamic_combobranches inores.cpp.qt.detail_dialog_impl.orgforis_optional_uuid. - Add
parent_party_idas a requireddynamic_comboDetail field toparty.org=/=counterparty.org's Qt facet (self-referencingcombo_domain_type). - Regenerate, build, manually verify Create end-to-end for both.
Notes
Descendant-cycle prevention on Edit (reassigning an existing party's parent to one of its own descendants) is not handled by this task – only Create is in scope. If Edit-time parent reassignment is exposed later, revisit whether the server needs a cycle check (client-side combo filtering alone can't prevent it once any client can call save() directly).
Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario, run
through the QA Validation Runner panel) that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
| Verify parent party/counterparty picker on Create | PASSED | All 6 steps PASS: required-combo presence, disabled-Save-when-unset, successful second-party/counterparty Create, edit round-trip, and cleanup. |
PRs
| PR | Title |
|---|---|
| #1594 | [codegen,qt] Add required parent picker to party/counterparty Create |
Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | Self-parenting not prevented on Edit; hangs the recursive hierarchy CTE (self-referencing row, UNION ALL never dedupes) | PartyDetailDialog.cpp, CounterpartyDetailDialog.cpp | Fixed | Added a generic exclude_if predicate to populateDynamicCombo (DynamicComboSetup.hpp) plus an is_self_referencing_combo codegen flag (core.py) so the template emits the exclusion only for genuinely self-referencing combos. Commit 88aa06e77. |
| 2 | Parent picker sorted by version, not alphabetically |
ores.refdata.party.org, ores.refdata.counterparty.org | Declined | Already documented as a deliberate tradeoff in the PR description – populateDynamicCombo's sort_key_of is int-typed and neither entity has a more meaningful int ordinal; not worth widening a widely-shared generic helper's signature for one combo. |
| 3 | Save may not stay disabled by default once the async fetch completes (QComboBox auto-selects index 0) | PartyDetailDialog.cpp, CounterpartyDetailDialog.cpp | Declined | Pre-existing, shared behavior across 5+ already-shipped entities' required dynamic combos (crm_driver_pair, crm_enabled_derived_pair, tenor_convention x2, business_centre, instrument_code) – not a regression this PR introduces, and fixing it would change runtime behavior for all of them without a full re-verification pass. Out of scope here. |
| 4 | counterparty.hpp include not alphabetically ordered |
LookupFetcher.hpp | Fixed | Commit 88aa06e77. |
Result
Verified PASSED live against BARCLAYS PLC (scenario
709B8639-638A-48D5-876C-A460F29B1BCB): both Party and Counterparty
Create dialogs gained a required Parent combo, Save correctly stays
disabled with no selection, a second party/counterparty was created
successfully with a parent selected, and reopening it for edit
correctly round-trips the selected parent. This closes the last
blocker on the party/counterparty commissioning story's manual Create
flow.
Implementation reused nearly all existing codegen machinery
(is_uuid=/=combo_code_field, required_dynamic_combo_fields) –
the only real gap was is_optional_uuid support in the non-locked
is_dynamic_combo template branches, plus a latent bug the extension
surfaced (the item-to-combo-value extractor was gated on the wrong
field's uuid-ness). Fixed at the ores.cpp.qt.detail_dialog_impl.org
tangle source, not the generated output. Zero regression verified via
--diff roundtrip across all 9 other entities using
combo_domain_type.