Files
Autoparts-DB/pos/services/currency.py
Nexus Dev 9ff3dc4c8b FASE 4-5-6: Infraestructura, CRM, Service Orders, Notificaciones, Ahorro, Logistica, API Publica
FASE 4:
- Redis cache de stock con fallback graceful
- Multi-moneda (MXN/USD) con contabilidad en MXN
- Proveedores y ordenes de compra completo
- Meilisearch 1.5M+ partes indexadas
- Metabase KPIs con dashboard auto-generado

FASE 5:
- CRM mejorado: activities, tags, loyalty program, analytics
- Imagenes de partes: upload, resize, thumbnails WebP
- Ordenes de servicio Kanban: received->diagnosis->repair->ready->delivered
- Garantias/RMA, alertas de reorden, multi-sucursal
- Stubs BNPL (APLAZO) y ERP Sync (Aspel/Contpaqi)

FASE 6:
- Notificaciones automaticas: push/WhatsApp/email/in-app
- Reportes de ahorro vs retail_price
- Logistica + tracking: DHL, FedEx, Estafeta, 99min, Uber
- API Publica: API keys, rate limiting, catalog search

Migraciones: v1.9-v3.0
Tests: 93/93 pasando
Backup: nexus_backup_20260427_045859.tar.gz
2026-04-27 05:23:30 +00:00

152 lines
4.2 KiB
Python

"""Multi-currency support for border refaccionarias.
Supports MXN and USD with configurable exchange rate per tenant.
Rates are cached in Redis for 60 seconds to avoid repeated DB hits.
Business rule: inventory prices are ALWAYS in MXN (base currency).
Sales can be recorded in USD with conversion at checkout time.
Accounting and CFDI always use MXN.
"""
from decimal import Decimal, ROUND_HALF_UP
from config import DEFAULT_CURRENCY, EXCHANGE_RATE_USD_MXN
from services.redis_stock_cache import _get_redis
CURRENCIES = {
'MXN': {'symbol': '$', 'name': 'Peso Mexicano', 'name_en': 'Mexican Peso', 'decimals': 2},
'USD': {'symbol': 'US$', 'name': 'Dolar Estadounidense', 'name_en': 'US Dollar', 'decimals': 2},
}
# Cache TTL for exchange rates in Redis (seconds)
_RATE_TTL = 60
def _to_dec(val):
if val is None:
return Decimal('0')
return Decimal(str(val))
def get_exchange_rate(conn, from_currency, to_currency):
"""Get the exchange rate from tenant_config, with Redis cache.
Returns:
Decimal: rate such that amount * rate = converted amount
(e.g., USD->MXN returns ~17.5, MXN->USD returns ~0.057)
"""
if from_currency == to_currency:
return Decimal('1')
cache_key = f"nexus:rate:{from_currency}:{to_currency}"
# Try Redis first
r = _get_redis()
if r:
try:
cached = r.get(cache_key)
if cached:
return Decimal(str(cached))
except Exception:
pass
# Fallback: read from tenant_config DB
rate = None
if conn:
try:
cur = conn.cursor()
cur.execute(
"SELECT value FROM tenant_config WHERE key = 'exchange_rate_usd_mxn'"
)
row = cur.fetchone()
cur.close()
if row and row[0]:
rate = Decimal(str(row[0]))
except Exception:
pass
if rate is None:
rate = _to_dec(EXCHANGE_RATE_USD_MXN)
# Compute cross rate
if from_currency == 'USD' and to_currency == 'MXN':
result = rate
elif from_currency == 'MXN' and to_currency == 'USD':
result = (Decimal('1') / rate).quantize(Decimal('0.000001'), rounding=ROUND_HALF_UP)
else:
result = Decimal('1')
# Cache in Redis
if r:
try:
r.set(cache_key, str(result), ex=_RATE_TTL)
except Exception:
pass
return result
def convert(amount, from_currency, to_currency, rate=None, conn=None):
"""Convert an amount between currencies.
Args:
amount: numeric amount to convert.
from_currency: source currency code.
to_currency: target currency code.
rate: optional pre-computed rate (skips DB lookup).
conn: optional DB connection to look up tenant rate.
Returns:
float: converted amount rounded to 2 decimals.
"""
if from_currency == to_currency:
return float(amount)
if rate is None:
rate = get_exchange_rate(conn, from_currency, to_currency)
amt = _to_dec(amount)
rate_dec = _to_dec(rate)
result = (amt * rate_dec).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
return float(result)
def to_mxn(amount, currency, rate=None, conn=None):
"""Convert an amount to MXN (convenience wrapper)."""
return convert(amount, currency, 'MXN', rate=rate, conn=conn)
def from_mxn(amount, currency, rate=None, conn=None):
"""Convert an amount from MXN to target currency."""
return convert(amount, 'MXN', currency, rate=rate, conn=conn)
def format_currency(amount, currency='MXN'):
"""Format an amount with the appropriate currency symbol.
Returns:
str: e.g. '$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.copy()
def invalidate_rate_cache():
"""Clear all cached exchange rates from Redis."""
r = _get_redis()
if not r:
return
try:
keys = r.keys('nexus:rate:*')
if keys:
r.delete(*keys)
except Exception:
pass