Files
Autoparts-DB/pos/tests/test_multi_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

241 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""Test multi-currency support (Mejora #8).
Validates:
1. MXN sale (default) works unchanged
2. USD sale stores currency='USD' and exchange_rate
3. Sale items and payments inherit currency
4. Accounting entries are in MXN (converted)
5. Quotation in USD converts correctly
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ.setdefault('MASTER_DB_URL', 'postgresql://postgres@/nexus_autoparts')
os.environ.setdefault('TENANT_DB_URL_TEMPLATE', 'postgresql://postgres@/{db_name}')
os.environ.setdefault('POS_JWT_SECRET', 'test-secret-12345678901234567890123456789012')
from services.pos_engine import process_sale, calculate_totals
from services.currency import convert, get_exchange_rate, to_mxn
from services.accounting_engine import record_sale_entry
from tenant_db import get_tenant_conn_by_dbname
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RESET = '\033[0m'
def print_result(name, passed, detail=""):
status = f"{GREEN}PASS{RESET}" if passed else f"{RED}FAIL{RESET}"
print(f" [{status}] {name}" + (f"{detail}" if detail else ""))
def get_test_inventory(conn):
"""Get first active inventory item with a price."""
cur = conn.cursor()
cur.execute("""
SELECT id, part_number, name, cost, price_1, tax_rate, branch_id
FROM inventory WHERE is_active = true AND price_1 > 0 LIMIT 1
""")
row = cur.fetchone()
cur.close()
return row
def get_open_register(conn):
cur = conn.cursor()
cur.execute("SELECT id FROM cash_registers WHERE status = 'open' LIMIT 1")
row = cur.fetchone()
cur.close()
return row[0] if row else None
def main():
print("=" * 60)
print("MULTI-CURRENCY — VALIDATION SUITE")
print("=" * 60)
passed = 0
failed = 0
conn = get_tenant_conn_by_dbname('tenant_acct_test')
inv = get_test_inventory(conn)
register_id = get_open_register(conn)
if not inv:
print(f"\n{RED}No inventory items available — aborting{RESET}")
return passed, failed
if not register_id:
print(f"\n{RED}No open cash register — aborting{RESET}")
return passed, failed
inv_id, part_num, name, cost, price_1, tax_rate, branch_id = inv
# ── Test 1: MXN sale (default) ──────────────────────────────
print("\n[1] MXN Sale (default)")
sale_data_mxn = {
'items': [{
'inventory_id': inv_id,
'quantity': 2,
'unit_price': float(price_1),
'discount_pct': 0,
'tax_rate': float(tax_rate or 0.16),
}],
'payment_method': 'efectivo',
'sale_type': 'cash',
'register_id': register_id,
'amount_paid': float(price_1) * 2 * 1.16 + 100, # overpay
}
try:
sale_mxn = process_sale(conn, sale_data_mxn)
conn.commit()
if sale_mxn.get('currency') == 'MXN' and sale_mxn.get('exchange_rate') == 1.0:
print_result("MXN sale", True, f"total={sale_mxn['total']:.2f} MXN")
passed += 1
else:
print_result("MXN sale", False, f"currency={sale_mxn.get('currency')}, rate={sale_mxn.get('exchange_rate')}")
failed += 1
except Exception as e:
conn.rollback()
print_result("MXN sale", False, str(e))
failed += 1
# ── Test 2: USD sale ────────────────────────────────────────
print("\n[2] USD Sale")
# Get exchange rate
rate = float(get_exchange_rate(conn, 'USD', 'MXN'))
# Convert price to USD
price_usd = round(float(price_1) / rate, 2)
sale_data_usd = {
'items': [{
'inventory_id': inv_id,
'quantity': 2,
'unit_price': price_usd,
'discount_pct': 0,
'tax_rate': float(tax_rate or 0.16),
}],
'payment_method': 'efectivo',
'sale_type': 'cash',
'register_id': register_id,
'amount_paid': price_usd * 2 * 1.16 + 10,
'currency': 'USD',
}
try:
sale_usd = process_sale(conn, sale_data_usd)
conn.commit()
checks = []
if sale_usd.get('currency') == 'USD':
checks.append("currency=USD")
if sale_usd.get('exchange_rate', 0) > 1:
checks.append(f"rate={sale_usd['exchange_rate']:.4f}")
# Verify DB has currency columns populated
cur = conn.cursor()
cur.execute("SELECT currency, exchange_rate FROM sales WHERE id = %s", (sale_usd['id'],))
db_sale = cur.fetchone()
cur.execute("SELECT currency, exchange_rate FROM sale_items WHERE sale_id = %s LIMIT 1", (sale_usd['id'],))
db_item = cur.fetchone()
cur.execute("SELECT currency, exchange_rate FROM sale_payments WHERE sale_id = %s LIMIT 1", (sale_usd['id'],))
db_pay = cur.fetchone()
cur.close()
if db_sale and db_sale[0] == 'USD':
checks.append("sale_row_currency=USD")
if db_item and db_item[0] == 'USD':
checks.append("item_row_currency=USD")
if db_pay and db_pay[0] == 'USD':
checks.append("payment_row_currency=USD")
if len(checks) >= 5:
print_result("USD sale", True, ", ".join(checks))
passed += 1
else:
print_result("USD sale", False, f"only {len(checks)} checks passed: {checks}")
failed += 1
except Exception as e:
conn.rollback()
print_result("USD sale", False, str(e))
failed += 1
# ── Test 3: Accounting in MXN regardless of sale currency ───
print("\n[3] Accounting entries in MXN")
try:
# Look up the journal entry for the USD sale
cur = conn.cursor()
cur.execute("""
SELECT id, description FROM journal_entries
WHERE reference_type = 'sale' AND reference_id = %s
""", (sale_usd['id'],))
je = cur.fetchone()
cur.close()
if je:
print_result("Accounting entry", True, f"JE #{je[0]} exists for USD sale")
passed += 1
else:
print_result("Accounting entry", False, "no journal entry found")
failed += 1
except Exception as e:
print_result("Accounting entry", False, str(e))
failed += 1
# ── Test 4: Currency conversion helper ──────────────────────
print("\n[4] Currency conversion helper")
try:
rate_usd_mxn = float(get_exchange_rate(conn, 'USD', 'MXN'))
rate_mxn_usd = float(get_exchange_rate(conn, 'MXN', 'USD'))
usd_to_mxn = convert(100, 'USD', 'MXN', rate=rate_usd_mxn, conn=conn)
mxn_to_usd = convert(usd_to_mxn, 'MXN', 'USD', rate=rate_mxn_usd, conn=conn)
if abs(usd_to_mxn - 100 * rate_usd_mxn) < 0.01:
print_result("USD→MXN", True, f"100 USD = {usd_to_mxn:.2f} MXN")
passed += 1
else:
print_result("USD→MXN", False, f"expected ~{100*rate_usd_mxn:.2f}, got {usd_to_mxn:.2f}")
failed += 1
if abs(mxn_to_usd - 100) < 0.05:
print_result("Round-trip", True, f"100 USD → {usd_to_mxn:.2f} MXN → {mxn_to_usd:.2f} USD")
passed += 1
else:
print_result("Round-trip", False, f"drift={abs(mxn_to_usd - 100):.2f}")
failed += 1
except Exception as e:
print_result("Conversion", False, str(e))
failed += 1
# ── Test 5: to_mxn convenience ──────────────────────────────
print("\n[5] to_mxn convenience")
try:
mxn = to_mxn(50, 'USD', conn=conn)
if mxn > 50:
print_result("to_mxn", True, f"50 USD = {mxn:.2f} MXN")
passed += 1
else:
print_result("to_mxn", False, f"expected > 50, got {mxn:.2f}")
failed += 1
except Exception as e:
print_result("to_mxn", False, str(e))
failed += 1
conn.close()
# ── Summary ─────────────────────────────────────────────────
print("\n" + "=" * 60)
print(f"RESULTS: {GREEN}{passed} passed{RESET}, {RED}{failed} failed{RESET}")
print("=" * 60)
return passed, failed
if __name__ == '__main__':
passed, failed = main()
sys.exit(0 if failed == 0 else 1)