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
224 lines
7.3 KiB
Python
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
|