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
This commit is contained in:
223
pos/blueprints/supplier_bp.py
Normal file
223
pos/blueprints/supplier_bp.py
Normal file
@@ -0,0 +1,223 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user