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:
@@ -6,6 +6,7 @@ from rich.table import Table
|
||||
from domain import (
|
||||
Account,
|
||||
Asset,
|
||||
CurrencyConverter,
|
||||
ExchangeRate,
|
||||
Liability,
|
||||
RecurringCashflow,
|
||||
@@ -132,7 +133,8 @@ def account_list() -> None:
|
||||
table.add_column(t("table.accounts_config.col.currency"))
|
||||
table.add_column(t("table.accounts_config.col.balance"), justify="right")
|
||||
for a in model.accounts:
|
||||
table.add_row(str(a.id)[:8], a.name, a.currency, f"{a.balance:,.2f}")
|
||||
balance_str = CurrencyConverter.format_money(a.balance, a.currency)
|
||||
table.add_row(str(a.id)[:8], a.name, a.currency, balance_str)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@@ -182,8 +184,9 @@ def transaction_add(
|
||||
model.transactions.append(txn)
|
||||
save_model(model)
|
||||
kind = t("cmd.config.transaction_add.kind.income") if amount >= 0 else t("cmd.config.transaction_add.kind.expense")
|
||||
amount_str = CurrencyConverter.format_money(abs(amount), _account_currency(model, account_id))
|
||||
ok = t("global.ok")
|
||||
msg = t("cmd.config.transaction_add.ok", kind=kind, amount=abs(amount), account=account)
|
||||
msg = t("cmd.config.transaction_add.ok", kind=kind, amount_str=amount_str, account=account)
|
||||
console.print(f"[green]{ok}[/green] {msg}")
|
||||
|
||||
|
||||
@@ -203,9 +206,12 @@ def transaction_list() -> None:
|
||||
table.add_column(t("table.transactions.col.description"))
|
||||
for tx in model.transactions:
|
||||
account_name = _account_name(model, tx.account)
|
||||
amount_str = CurrencyConverter.format_money(
|
||||
tx.amount, _account_currency(model, tx.account)
|
||||
)
|
||||
table.add_row(
|
||||
str(tx.id)[:8], tx.date, account_name, tx.category,
|
||||
f"{tx.amount:,.2f}", tx.description,
|
||||
amount_str, tx.description,
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
@@ -245,8 +251,9 @@ def recurring_add(
|
||||
)
|
||||
model.recurring.append(rc)
|
||||
save_model(model)
|
||||
amount_str = CurrencyConverter.format_money(abs(amount), model.base_currency)
|
||||
ok = t("global.ok")
|
||||
msg = t("cmd.config.recurring_add.ok", amount=abs(amount), frequency=frequency)
|
||||
msg = t("cmd.config.recurring_add.ok", amount_str=amount_str, frequency=frequency)
|
||||
console.print(f"[green]{ok}[/green] {msg}")
|
||||
|
||||
|
||||
@@ -265,7 +272,8 @@ def recurring_list() -> None:
|
||||
table.add_column(t("table.recurring.col.amount"), justify="right")
|
||||
table.add_column(t("table.recurring.col.category"))
|
||||
for r in model.recurring:
|
||||
row = (str(r.id)[:8], r.start_date, r.end_date, r.frequency, f"{r.amount:,.2f}", r.category)
|
||||
amount_str = CurrencyConverter.format_money(r.amount, model.base_currency)
|
||||
row = (str(r.id)[:8], r.start_date, r.end_date, r.frequency, amount_str, r.category)
|
||||
table.add_row(*row)
|
||||
console.print(table)
|
||||
|
||||
@@ -316,7 +324,8 @@ def asset_list() -> None:
|
||||
table.add_column(t("table.assets.col.value"), justify="right")
|
||||
table.add_column(t("table.assets.col.growth"), justify="right")
|
||||
for a in model.assets:
|
||||
table.add_row(str(a.id)[:8], a.name, f"{a.value:,.2f}", f"{a.growth_rate:.1f}%")
|
||||
value_str = CurrencyConverter.format_money(a.value, model.base_currency)
|
||||
table.add_row(str(a.id)[:8], a.name, value_str, f"{a.growth_rate:.1f}%")
|
||||
console.print(table)
|
||||
|
||||
|
||||
@@ -371,8 +380,9 @@ def liability_list() -> None:
|
||||
table.add_column(t("table.liabilities.col.rate"), justify="right")
|
||||
table.add_column(t("table.liabilities.col.payment"), justify="right")
|
||||
for li in model.liabilities:
|
||||
b, i, p = f"{li.balance:,.2f}", f"{li.interest:.1f}%", f"{li.payment:,.2f}"
|
||||
row = (str(li.id)[:8], li.name, b, i, p)
|
||||
balance_str = CurrencyConverter.format_money(li.balance, model.base_currency)
|
||||
payment_str = CurrencyConverter.format_money(li.payment, model.base_currency)
|
||||
row = (str(li.id)[:8], li.name, balance_str, f"{li.interest:.1f}%", payment_str)
|
||||
table.add_row(*row)
|
||||
console.print(table)
|
||||
|
||||
@@ -411,3 +421,10 @@ def _account_name(model, account_id: str) -> str:
|
||||
if str(a.id) == account_id:
|
||||
return a.name
|
||||
return account_id[:8]
|
||||
|
||||
|
||||
def _account_currency(model, account_id: str) -> str:
|
||||
for a in model.accounts:
|
||||
if str(a.id) == account_id:
|
||||
return a.currency
|
||||
return model.base_currency
|
||||
|
||||
@@ -31,7 +31,7 @@ def info() -> None:
|
||||
tbl.add_column(t("table.accounts.col.currency"))
|
||||
tbl.add_column(t("table.accounts.col.balance"), justify="right")
|
||||
for a in model.accounts:
|
||||
tbl.add_row(a.name, a.currency, f"{a.balance:,.2f}")
|
||||
tbl.add_row(a.name, a.currency, CurrencyConverter.format_money(a.balance, a.currency))
|
||||
console.print(tbl)
|
||||
|
||||
if model.transactions:
|
||||
@@ -41,7 +41,8 @@ def info() -> None:
|
||||
tbl.add_column(t("table.transactions.col.amount"), justify="right")
|
||||
tbl.add_column(t("table.transactions.col.description"))
|
||||
for tx in model.transactions[:10]:
|
||||
tbl.add_row(tx.date, tx.category, f"{tx.amount:,.2f}", tx.description)
|
||||
amount_str = CurrencyConverter.format_money(tx.amount, model.base_currency)
|
||||
tbl.add_row(tx.date, tx.category, amount_str, tx.description)
|
||||
if len(model.transactions) > 10:
|
||||
console.print(tbl)
|
||||
console.print(t("cmd.info.more", count=len(model.transactions) - 10))
|
||||
@@ -54,7 +55,8 @@ def info() -> None:
|
||||
tbl.add_column(t("table.recurring.col.amount"), justify="right")
|
||||
tbl.add_column(t("table.recurring.col.category"))
|
||||
for r in model.recurring:
|
||||
tbl.add_row(r.frequency, f"{r.amount:,.2f}", r.category)
|
||||
amount_str = CurrencyConverter.format_money(r.amount, model.base_currency)
|
||||
tbl.add_row(r.frequency, amount_str, r.category)
|
||||
console.print(tbl)
|
||||
|
||||
if model.assets:
|
||||
@@ -63,7 +65,8 @@ def info() -> None:
|
||||
tbl.add_column(t("table.assets.col.value"), justify="right")
|
||||
tbl.add_column(t("table.assets.col.growth"), justify="right")
|
||||
for a in model.assets:
|
||||
tbl.add_row(a.name, f"{a.value:,.2f}", f"{a.growth_rate:.1f}%")
|
||||
value_str = CurrencyConverter.format_money(a.value, model.base_currency)
|
||||
tbl.add_row(a.name, value_str, f"{a.growth_rate:.1f}%")
|
||||
console.print(tbl)
|
||||
|
||||
if model.liabilities:
|
||||
@@ -73,5 +76,7 @@ def info() -> None:
|
||||
tbl.add_column(t("table.liabilities.col.rate"), justify="right")
|
||||
tbl.add_column(t("table.liabilities.col.payment"), justify="right")
|
||||
for li in model.liabilities:
|
||||
tbl.add_row(li.name, f"{li.balance:,.2f}", f"{li.interest:.1f}%", f"{li.payment:,.2f}")
|
||||
balance_str = CurrencyConverter.format_money(li.balance, model.base_currency)
|
||||
payment_str = CurrencyConverter.format_money(li.payment, model.base_currency)
|
||||
tbl.add_row(li.name, balance_str, f"{li.interest:.1f}%", payment_str)
|
||||
console.print(tbl)
|
||||
|
||||
@@ -190,7 +190,7 @@ _r("cmd.config.transaction_add.opt.amount", "Сумма (доход/расход
|
||||
_r("cmd.config.transaction_add.opt.category", "Категория")
|
||||
_r("cmd.config.transaction_add.opt.description", "Описание")
|
||||
_r("cmd.config.transaction_add.opt.date", "Дата (YYYY-MM-DD)")
|
||||
_r("cmd.config.transaction_add.ok", "Транзакция '{kind}' на {amount:.2f} (счёт: {account})")
|
||||
_r("cmd.config.transaction_add.ok", "Транзакция '{kind}' на {amount_str} (счёт: {account})")
|
||||
_r("cmd.config.transaction_add.kind.income", "доход")
|
||||
_r("cmd.config.transaction_add.kind.expense", "расход")
|
||||
_r("cmd.config.transaction_add.err", "Счёт '{account}' не найден")
|
||||
@@ -209,7 +209,7 @@ _r("cmd.config.recurring_add.opt.category", "Категория")
|
||||
_r("cmd.config.recurring_add.opt.frequency", "Периодичность")
|
||||
_r("cmd.config.recurring_add.opt.start", "Дата начала (YYYY-MM-DD)")
|
||||
_r("cmd.config.recurring_add.opt.end", "Дата окончания (YYYY-MM-DD)")
|
||||
_r("cmd.config.recurring_add.ok", "Регулярный платёж на {amount:.2f} ({frequency})")
|
||||
_r("cmd.config.recurring_add.ok", "Регулярный платёж на {amount_str} ({frequency})")
|
||||
|
||||
_r("cmd.config.recurring_list.help", "Список регулярных платежей")
|
||||
_r("cmd.config.recurring_list.empty", "Регулярные платежи не добавлены")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user