Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- 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:
@@ -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 %} · 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>
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user