Task: Pair code doesn't recompute when base/quote currency changes

Table of Contents

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_code never 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_code that 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), mirroring is_key semantics for a non-key identity field.
  • core.py: replace the single key_widget computation with a list of all locked fields (is_key or immutable), each carrying its widget name and type (so the template can pick setReadOnly vs. setEnabled per widget kind — line_edit/text_edit vs. combo).
  • Template ores.cpp.qt.detail_dialog_impl.org: loop over that list in setCreateMode() instead of the single hardcoded key-widget line; rename the update...FromUi() gate from is_key to a combined is_locked_after_create (is_key OR immutable) so immutable fields also only write back during create.
  • currency_pair.org: mark base_currency=/=quote_currency as immutable: true.
  • Duplicate pair_code on create: since editing is now blocked entirely, this only matters at insert time — pair_code is 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:

Emacs 29.3 (Org mode 9.6.15)