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
59 lines
1.8 KiB
Python
59 lines
1.8 KiB
Python
# /home/Autopartes/pos/services/audit.py
|
|
"""Audit logging service. INSERT-only, never update or delete."""
|
|
|
|
from flask import g
|
|
|
|
|
|
def log_action(conn, action, entity_type=None, entity_id=None,
|
|
old_value=None, new_value=None):
|
|
"""Insert an audit log entry using the current request context.
|
|
|
|
Args:
|
|
conn: psycopg2 connection to the tenant DB
|
|
action: SALE, CANCEL, PRICE_CHANGE, STOCK_ADJUST, LOGIN, DISCOUNT, etc.
|
|
entity_type: 'sale', 'inventory', 'customer', 'employee', etc.
|
|
entity_id: ID of the affected entity
|
|
old_value: dict of previous values (or None)
|
|
new_value: dict of new values (or None)
|
|
"""
|
|
import json
|
|
|
|
cur = conn.cursor()
|
|
cur.execute("""
|
|
INSERT INTO audit_log
|
|
(employee_id, action, entity_type, entity_id, old_value, new_value,
|
|
device_id, ip_address, branch_id)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
""", (
|
|
_safe_g('employee_id'),
|
|
action,
|
|
entity_type,
|
|
entity_id,
|
|
json.dumps(old_value) if old_value else None,
|
|
json.dumps(new_value) if new_value else None,
|
|
_safe_g('device_id'),
|
|
_get_client_ip(),
|
|
_safe_g('branch_id'),
|
|
))
|
|
# Don't commit here — let the caller control the transaction
|
|
|
|
|
|
|
|
def _safe_g(attr, default=None):
|
|
"""Safely read flask.g attribute outside of app context."""
|
|
try:
|
|
return getattr(g, attr, default)
|
|
except RuntimeError:
|
|
return default
|
|
|
|
|
|
def _get_client_ip():
|
|
"""Get client IP, handling proxies."""
|
|
try:
|
|
from flask import request
|
|
if request.headers.get('X-Forwarded-For'):
|
|
return request.headers['X-Forwarded-For'].split(',')[0].strip()
|
|
return request.remote_addr
|
|
except RuntimeError:
|
|
return None
|