feat(pos): add multi-language i18n (#37) and multi-currency USD/MXN (#38)

- i18n.js with 130+ translation keys for es/en, loaded in all 11 templates
- sidebar.js uses t() for all nav labels, adds MX/US language toggle
- app-init.js role labels use i18n
- currency.py service with convert() and format_currency()
- config.py adds DEFAULT_CURRENCY and EXCHANGE_RATE_USD_MXN settings
- config_bp.py adds GET/PUT /pos/api/config/currency endpoints
- config.html adds currency/exchange-rate section (Section 8)
- config.js adds loadCurrency/saveCurrency with localStorage sync
- pos.js fmt() reads pos_currency from localStorage for USD/MXN display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 08:19:18 +00:00
parent e00dce7d5a
commit c1d0638b45
15 changed files with 554 additions and 24 deletions

55
pos/services/currency.py Normal file
View File

@@ -0,0 +1,55 @@
"""Multi-currency support for border refaccionarias.
Supports MXN and USD with configurable exchange rate.
"""
from config import DEFAULT_CURRENCY, EXCHANGE_RATE_USD_MXN
CURRENCIES = {
'MXN': {'symbol': '$', 'name': 'Peso Mexicano', 'name_en': 'Mexican Peso', 'decimals': 2},
'USD': {'symbol': 'US$', 'name': 'Dolar Estadounidense', 'name_en': 'US Dollar', 'decimals': 2},
}
def convert(amount, from_currency, to_currency, rate=None):
"""Convert an amount between currencies.
Args:
amount: The numeric amount to convert.
from_currency: Source currency code ('MXN' or 'USD').
to_currency: Target currency code ('MXN' or 'USD').
rate: Optional custom exchange rate (USD->MXN). Defaults to config value.
Returns:
The converted amount, rounded to 2 decimals.
"""
if from_currency == to_currency:
return amount
if rate is None:
rate = EXCHANGE_RATE_USD_MXN
if from_currency == 'USD' and to_currency == 'MXN':
return round(amount * rate, 2)
if from_currency == 'MXN' and to_currency == 'USD':
return round(amount / rate, 2)
return amount
def format_currency(amount, currency='MXN'):
"""Format an amount with the appropriate currency symbol.
Args:
amount: Numeric value.
currency: Currency code.
Returns:
Formatted string like '$1,234.56' or 'US$1,234.56'.
"""
info = CURRENCIES.get(currency, CURRENCIES['MXN'])
return f"{info['symbol']}{amount:,.{info['decimals']}f}"
def get_currency_info(code=None):
"""Return currency metadata dict. If code is None, return all."""
if code:
return CURRENCIES.get(code)
return CURRENCIES