Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos
- F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado
- F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment
- F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings)
- F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant
- Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas
- Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant)
- Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant
- Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
This commit is contained in:
2026-10-12 00:57:31 +00:00
parent f2d4558b80
commit a386ca5b4a
165 changed files with 31313 additions and 2203 deletions

View File

@@ -3,6 +3,7 @@
Endpoints (all under /pos/api):
GET/POST /warranties
GET /warranties/<id>
GET /warranties/<id>/print (HTML)
GET /customers/<id>/warranties
POST /warranty-claims
GET /warranty-claims
@@ -11,7 +12,7 @@ Endpoints (all under /pos/api):
PUT /warranty-claims/<id>/close
"""
from flask import Blueprint, request, jsonify, g
from flask import Blueprint, request, jsonify, g, render_template_string
from middleware import require_auth
from tenant_db import get_tenant_conn
from services.warranty_engine import (
@@ -43,7 +44,10 @@ def get_warranties():
def post_warranty():
"""Register a warranty (usually called at sale time)."""
data = request.get_json() or {}
required = ['sale_id', 'sale_item_id', 'inventory_id', 'customer_id', 'warranty_months']
required = ['sale_id', 'sale_item_id', 'inventory_id', 'customer_id']
# warranty_months is required unless a day-based coverage is given (used parts)
if 'warranty_months' not in data and 'warranty_days' not in data:
required.append('warranty_months')
missing = [f for f in required if f not in data]
if missing:
return jsonify({'error': f'Missing fields: {missing}'}), 400
@@ -56,11 +60,12 @@ def post_warranty():
sale_item_id=data['sale_item_id'],
inventory_id=data['inventory_id'],
customer_id=data['customer_id'],
warranty_months=int(data['warranty_months']),
warranty_months=int(data.get('warranty_months') or 0),
supplier_id=data.get('supplier_id'),
part_number=data.get('part_number'),
name=data.get('name'),
notes=data.get('notes'),
warranty_days=int(data['warranty_days']) if data.get('warranty_days') else None,
)
conn.commit()
conn.close()
@@ -87,6 +92,49 @@ def get_warranty_detail(warranty_id):
return jsonify(w)
@warranty_bp.route('/warranties/<int:warranty_id>/print', methods=['GET'])
@require_auth('inventory.view')
def print_warranty(warranty_id):
"""Printable warranty receipt (HTML).
Serves both month-based and Yonke day-based coverage so a used-part buyer
gets a physical voucher with the coverage window.
"""
conn = get_tenant_conn(g.tenant_id)
try:
w = get_warranty(conn, warranty_id)
if not w:
return jsonify({'error': 'Warranty not found'}), 404
business = {'name': 'NEXUS AUTOPARTS', 'rfc': '', 'address': '', 'phone': ''}
try:
cur = conn.cursor()
cur.execute("""
SELECT 'name' AS k, value FROM tenant_config
WHERE key IN ('tenant_nombre', 'receipt_store_name')
UNION ALL SELECT 'rfc', value FROM tenant_config
WHERE key IN ('tenant_rfc', 'receipt_rfc')
UNION ALL SELECT 'address', value FROM tenant_config
WHERE key IN ('tenant_direccion', 'receipt_address')
UNION ALL SELECT 'phone', value FROM tenant_config
WHERE key IN ('tenant_telefono', 'receipt_phone')
""")
for k, v in cur.fetchall():
if v:
business[k] = v
cur.close()
except Exception:
pass
finally:
conn.close()
coverage = (f"{w['warranty_days']} días" if w.get('warranty_days')
else f"{w.get('warranty_months') or 0} meses")
html = render_template_string(
WARRANTY_PRINT_TEMPLATE, w=w, business=business, coverage=coverage
)
return html, 200, {'Content-Type': 'text/html; charset=utf-8'}
@warranty_bp.route('/customers/<int:customer_id>/warranties', methods=['GET'])
@require_auth('inventory.view')
def get_customer_warranties(customer_id):
@@ -206,3 +254,68 @@ def put_close_claim(claim_id):
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 500
# ── Printable warranty receipt ─────────────────────────────────────────────
WARRANTY_PRINT_TEMPLATE = """<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Garantia #{{ w.id }}</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f3f4f6;color:#111;padding:16px;line-height:1.5}
.card{max-width:640px;margin:0 auto;background:#fff;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,.08);overflow:hidden}
.header{background:linear-gradient(135deg,#1f2937,#374151);color:#fff;padding:24px;text-align:center}
.header h1{font-size:20px;font-weight:700;margin-bottom:4px}
.header p{font-size:13px;opacity:.85}
.body{padding:24px}
.meta{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:20px;font-size:13px;color:#4b5563}
.meta div{background:#f9fafb;padding:10px 12px;border-radius:8px}
.meta strong{color:#111;display:block;font-size:11px;text-transform:uppercase;letter-spacing:.4px;margin-bottom:2px}
.badge{display:inline-block;padding:4px 10px;border-radius:999px;font-size:11px;font-weight:700;text-transform:uppercase}
.badge-active{background:#d1fae5;color:#065f46}
.badge-expired,.badge-claimed,.badge-void{background:#fee2e2;color:#991b1b}
.terms{font-size:12px;color:#6b7280;border-top:1px solid #e5e7eb;padding-top:14px}
.footer{text-align:center;padding:14px;font-size:12px;color:#9ca3af}
.actions{padding:0 24px 20px;text-align:center}
.btn{display:inline-block;padding:12px 22px;border-radius:10px;border:none;background:#1f2937;color:#fff;font-size:14px;font-weight:700;cursor:pointer}
@media print{body{background:#fff;padding:0}.card{box-shadow:none}.actions{display:none}}
</style>
</head>
<body>
<div class="card">
<div class="header">
<h1>Comprobante de Garantia</h1>
<p>{{ business.name }}{% if business.rfc %} &middot; RFC {{ business.rfc }}{% endif %}</p>
</div>
<div class="body">
<div class="meta">
<div><strong>Folio</strong>G-{{ w.id }}</div>
<div><strong>Estado</strong><span class="badge badge-{{ w.status }}">{{ w.status }}</span></div>
<div><strong>Cliente</strong>{{ w.customer_name or 'Publico general' }}</div>
<div><strong>Venta</strong>#{{ w.sale_id or '—' }}</div>
<div><strong>Pieza</strong>{{ w.name or '—' }}</div>
<div><strong>No. parte</strong>{{ w.part_number or '—' }}</div>
<div><strong>Inicio</strong>{{ w.start_date }}</div>
<div><strong>Vence</strong>{{ w.end_date }}</div>
<div><strong>Cobertura</strong>{{ coverage }}</div>
</div>
<div class="terms">
<p>Esta garantia ampara defectos de funcionamiento de la pieza por {{ coverage }} a partir de
la fecha de compra. Presente este comprobante junto con la pieza para cualquier reclamacion.</p>
{% if w.notes %}<p>Notas: {{ w.notes }}</p>{% endif %}
{% if business.address %}<p>{{ business.address }}</p>{% endif %}
{% if business.phone %}<p>Tel: {{ business.phone }}</p>{% endif %}
</div>
</div>
<div class="actions">
<button class="btn" onclick="window.print()">Imprimir</button>
</div>
<div class="footer">{{ business.name }} — Comprobante generado el {{ w.created_at[:10] }}</div>
</div>
</body>
</html>
"""