T8: ISO 4217 per-currency decimals + format_money()

- Add CURRENCY_DECIMALS dict (JPY=0, BHD/KWD/OMR=3, rest=2) and DEFAULT_CURRENCY_DECIMALS=2
- Add CurrencyConverter.format_money() staticmethod with ROUND_HALF_UP; symbol-prefix for known, code-prefix for unknown
- Replace 14 :,.2f literals across services.py / info.py / config_cmd.py with format_money() calls
- Refactor 2 i18n templates (transaction_add.ok, recurring_add.ok): {amount:.2f} -> {amount_str}; callers pass pre-formatted string
- New _account_currency() helper for transaction currency lookup
- Extend test_currency.py: 8 new tests (JPY/BHD/USD/CHF/negative/zero/float-int/lowercase)
- 75/75 tests pass; 0 :,.2f literals remain in infrastructure/cli/
This commit is contained in:
2026-10-09 19:24:47 +03:00
parent 9175d5c76b
commit d58547e8f8
6 changed files with 136 additions and 19 deletions
+8 -4
View File
@@ -54,7 +54,11 @@ def convert_value(
return converter.convert(amount, from_curr, to_curr)
def fmt_amount(amount: Decimal | float, currency: str, symbol: str) -> str:
if isinstance(amount, Decimal):
return f"{symbol}{amount:,.2f}"
return f"{symbol}{amount:,.2f}"
def fmt_amount(amount: Decimal | float, currency: str, symbol: str = "") -> str:
"""Format money via the canonical display rule.
`symbol` is retained as a positional parameter for backward compatibility with
existing callers (compare.py / forecast.py / scenario.py / whatif.py); the
symbol/layout decision is centralised in `CurrencyConverter.format_money`.
"""
return CurrencyConverter.format_money(amount, currency)