Task: Pair code doesn't recompute when base/quote currency changes
This page documents a task in the Currency pair support in reference data story. It captures the goal, current status, acceptance, and any notes or results.
Goal
In the Currency Pair detail dialog, changing Base Currency or Quote
Currency does not update pair_code (still shows the pair's original
"BASE/QUOTE" value) — the key field is stale relative to the fields
that determine it.
Before implementing a fix, resolve the underlying design question:
currency_pair is currently keyed by pair_code itself (no
surrogate id), so editing base/quote in place either (a) recomputes
pair_code live and must reject a value that collides with an
existing pair, or (b) the entity should be keyed by a GUID instead,
with pair_code becoming a derived, uniquely-constrained display
column. (b) is the more standard shape for this codebase (most
entities use a UUID primary key) but is a bigger migration — decide
with the user before implementing.
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Currency pair support in reference data |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-07-08 |
Acceptance
- Design decision made and recorded: no GUID migration — Base and
Quote Currency are identity-defining and become read-only after
creation, so
pair_codenever needs to recompute or be re-validated post-creation. - Base Currency and Quote Currency are editable when creating a new Currency Pair, and locked (disabled) when editing an existing one.
- Creating a new Currency Pair with a
pair_codethat collides with an existing pair is rejected with a clear error (verify existing DB primary-key behaviour is reasonable; improve only if the resulting error is unclear).
Plan
Design decision (with user): no GUID migration. Base and Quote Currency are identity-defining properties of a currency_pair — once created, they (and therefore pair_code) never change. Make them read-only after creation instead of recomputing/re-validating pair_code live.
Investigated the existing lock mechanism before implementing:
pair_code (the is_key field) is already correctly locked in edit
mode — setCreateMode(false) calls
ui_->pairCodeEdit->setReadOnly(true) via a key_widget property
computed once in core.py (next(f for f in detail_fields if
is_key)), and update...FromUi() only writes the key field back to
the entity when createMode_ is true. No latent bug here — my
earlier hypothesis (a "phantom editable pair_code" bug) was wrong;
setReadOnly() just doesn't visually gray out a QLineEdit, so it
looks editable even though it isn't. What's actually missing:
base_currency=/=quote_currency (flagged_combo, not is_key) have
no lock at all — fully editable in edit mode, and their edits do
get saved, which is the real bug.
Generalizing rather than hand-scoping to this one entity:
- New detail-field property
:immutable:(bool), mirroringis_keysemantics for a non-key identity field. core.py: replace the singlekey_widgetcomputation with a list of all locked fields (is_keyorimmutable), each carrying its widget name and type (so the template can picksetReadOnlyvs.setEnabledper widget kind — line_edit/text_edit vs. combo).- Template
ores.cpp.qt.detail_dialog_impl.org: loop over that list insetCreateMode()instead of the single hardcoded key-widget line; rename theupdate...FromUi()gate fromis_keyto a combinedis_locked_after_create(is_key OR immutable) so immutable fields also only write back during create. currency_pair.org: markbase_currency=/=quote_currencyasimmutable: true.- Duplicate
pair_codeon create: since editing is now blocked entirely, this only matters at insert time —pair_codeis already the primary key, so a duplicate insert should already fail at the DB layer; verify the resulting error is reasonably clear rather than building new validation from scratch.
Notes
PRs
| PR | Title |
|---|---|
| #1476 | [refdata,qt,codegen] Pair code derives from base/quote, locked after create |
Review
| Comment summary | File | Decision | Notes |
|---|---|---|---|
Result
Shipped: Base and Quote Currency are now immutable identity fields
(new :immutable: detail-field property, generalized alongside
is_key into is_locked_after_create=/=locked_fields in codegen).
pair_code derives live from Base/Quote at create time and is always
read-only; Base/Quote themselves are fully editable at create and
locked — but visually normal, not grayed out — after save, via a new
WidgetUtils::set_combo_locked() helper. Base = Quote is rejected
at validation time. Settlement Currency toggles with Deliverable and
clears (rather than showing a stale value) via a new
=WidgetUtils::set_combo_unavailable() helper. All of this
entity-specific logic is wired through 10 new paste-block seams
added to the shared detail_dialog/mdi_window/controller templates
(documented in paste_blocks_in_codegen.org), so it survives
regeneration — verified regenerating currency_pair.org needs zero
manual re-application.
QA scenario (scenario_currency-pair-code-derivation) run to green
after several fix rounds, including two real regressions found and
fixed along the way: a crash in AdminPlugin::setup_menus() when
--open-scenario was combined with the Scenario Runner's earlier
switch to lazy window construction, and a stale-ccache-object link
failure (CountryController::notifyOpenDialogs undefined symbol).
Not resolved in this PR — filed as separate follow-up tasks instead, since each needs its own design/investigation:
- Creating a Currency Pair with an already-used pair_code silently
versions the existing row — the DB's create-vs-update-by-natural-key
pattern (used by every bitemporal entity, not currency_pair-specific)
means a colliding
pair_codeon create silently overwrites the existing row rather than erroring. - pair_code inline flag icon renders tiny regardless of
keyFlagIconSize() — the
QLineEdit-hosted flag doesn't respect icon-size the wayQComboBox=/=QTableViewicons do; two rounds of attempted fixes had no visible effect and were reverted per explicit instruction rather than keep adding unproven complexity. - Rename deliverable flag to non_deliverable — separately requested rename to the idiomatic FX trading term, out of scope for this task.