Files
Autoparts-DB/pos/blueprints/supplier_bp.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

224 lines
7.3 KiB
Python

"""Supplier and purchase order blueprint.
Endpoints (all under /pos/api):
GET/POST /suppliers
GET/PUT /suppliers/<id>
GET /suppliers/<id>/purchase-orders
POST /purchase-orders
GET /purchase-orders
GET /purchase-orders/<id>
PUT /purchase-orders/<id>/send
PUT /purchase-orders/<id>/receive
PUT /purchase-orders/<id>/cancel
"""
from flask import Blueprint, request, jsonify, g
from middleware import require_auth
from tenant_db import get_tenant_conn
from services.supplier_engine import (
create_supplier, update_supplier, get_supplier, list_suppliers,
create_po, send_po, receive_po, cancel_po, get_po, list_pos,
)
supplier_bp = Blueprint('supplier', __name__, url_prefix='/pos/api')
# ── SUPPLIERS ──────────────────────────────────────────────────────────────
@supplier_bp.route('/suppliers', methods=['GET'])
@require_auth('inventory.view')
def get_suppliers():
"""List suppliers."""
conn = get_tenant_conn(g.tenant_id)
active_only = request.args.get('active_only', 'true').lower() == 'true'
limit = request.args.get('limit', 100, type=int)
offset = request.args.get('offset', 0, type=int)
data = list_suppliers(conn, active_only=active_only, limit=limit, offset=offset)
conn.close()
return jsonify({'data': data})
@supplier_bp.route('/suppliers', methods=['POST'])
@require_auth('inventory.edit')
def post_supplier():
"""Create a supplier."""
data = request.get_json() or {}
if not data.get('name'):
return jsonify({'error': 'name is required'}), 400
conn = get_tenant_conn(g.tenant_id)
try:
supplier_id = create_supplier(conn, data)
conn.commit()
conn.close()
return jsonify({'id': supplier_id, 'message': 'Supplier created'}), 201
except Exception as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 500
@supplier_bp.route('/suppliers/<int:supplier_id>', methods=['GET'])
@require_auth('inventory.view')
def get_supplier_detail(supplier_id):
"""Get supplier by ID."""
conn = get_tenant_conn(g.tenant_id)
supplier = get_supplier(conn, supplier_id)
conn.close()
if not supplier:
return jsonify({'error': 'Supplier not found'}), 404
return jsonify(supplier)
@supplier_bp.route('/suppliers/<int:supplier_id>', methods=['PUT'])
@require_auth('inventory.edit')
def put_supplier(supplier_id):
"""Update supplier."""
data = request.get_json() or {}
conn = get_tenant_conn(g.tenant_id)
try:
updated = update_supplier(conn, supplier_id, data)
conn.commit()
conn.close()
if not updated:
return jsonify({'error': 'Supplier not found or no changes'}), 404
return jsonify({'message': 'Supplier updated'})
except Exception as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 500
@supplier_bp.route('/suppliers/<int:supplier_id>/purchase-orders', methods=['GET'])
@require_auth('inventory.view')
def get_supplier_pos(supplier_id):
"""List POs for a supplier."""
conn = get_tenant_conn(g.tenant_id)
status = request.args.get('status')
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
data = list_pos(conn, status=status, supplier_id=supplier_id, limit=limit, offset=offset)
conn.close()
return jsonify({'data': data})
# ── PURCHASE ORDERS ────────────────────────────────────────────────────────
@supplier_bp.route('/purchase-orders', methods=['POST'])
@require_auth('inventory.edit')
def post_purchase_order():
"""Create a purchase order."""
data = request.get_json() or {}
if not data.get('items'):
return jsonify({'error': 'items are required'}), 400
conn = get_tenant_conn(g.tenant_id)
try:
result = create_po(conn, data, branch_id=g.branch_id, employee_id=g.employee_id)
conn.commit()
conn.close()
return jsonify(result), 201
except ValueError as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 400
except Exception as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 500
@supplier_bp.route('/purchase-orders', methods=['GET'])
@require_auth('inventory.view')
def get_purchase_orders():
"""List purchase orders."""
conn = get_tenant_conn(g.tenant_id)
status = request.args.get('status')
supplier_id = request.args.get('supplier_id', type=int)
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
data = list_pos(conn, status=status, supplier_id=supplier_id, limit=limit, offset=offset)
conn.close()
return jsonify({'data': data})
@supplier_bp.route('/purchase-orders/<int:po_id>', methods=['GET'])
@require_auth('inventory.view')
def get_purchase_order(po_id):
"""Get PO detail with items."""
conn = get_tenant_conn(g.tenant_id)
po = get_po(conn, po_id)
conn.close()
if not po:
return jsonify({'error': 'Purchase order not found'}), 404
return jsonify(po)
@supplier_bp.route('/purchase-orders/<int:po_id>/send', methods=['PUT'])
@require_auth('inventory.edit')
def put_send_po(po_id):
"""Mark PO as sent."""
conn = get_tenant_conn(g.tenant_id)
try:
ok = send_po(conn, po_id)
conn.commit()
conn.close()
if not ok:
return jsonify({'error': 'PO not found or not in draft status'}), 400
return jsonify({'message': 'PO marked as sent'})
except Exception as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 500
@supplier_bp.route('/purchase-orders/<int:po_id>/receive', methods=['PUT'])
@require_auth('inventory.edit')
def put_receive_po(po_id):
"""Receive items from a PO."""
data = request.get_json() or {}
received_items = data.get('items', [])
if not received_items:
return jsonify({'error': 'items are required'}), 400
conn = get_tenant_conn(g.tenant_id)
try:
result = receive_po(
conn, po_id, received_items,
supplier_invoice=data.get('supplier_invoice'),
notes=data.get('notes')
)
conn.commit()
conn.close()
return jsonify(result)
except ValueError as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 400
except Exception as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 500
@supplier_bp.route('/purchase-orders/<int:po_id>/cancel', methods=['PUT'])
@require_auth('inventory.edit')
def put_cancel_po(po_id):
"""Cancel a PO."""
data = request.get_json() or {}
reason = data.get('reason', '')
conn = get_tenant_conn(g.tenant_id)
try:
cancel_po(conn, po_id, reason)
conn.commit()
conn.close()
return jsonify({'message': 'PO cancelled'})
except ValueError as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 400
except Exception as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 500