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

132 lines
4.4 KiB
Python

"""Logistics Blueprint: shipments, couriers, tracking.
Prefix: /pos/api/logistics
"""
from flask import Blueprint, request, jsonify, g
from middleware import require_auth
from tenant_db import get_tenant_conn
from services.logistics_engine import (
create_shipment, get_shipment, list_shipments, update_shipment_status,
get_couriers, add_courier,
)
logistics_bp = Blueprint('logistics', __name__, url_prefix='/pos/api/logistics')
@logistics_bp.route('/shipments', methods=['GET'])
@require_auth()
def list_all_shipments():
status = request.args.get('status')
courier_id = request.args.get('courier_id', type=int)
related_type = request.args.get('related_type')
related_id = request.args.get('related_id', type=int)
page = int(request.args.get('page', 1))
per_page = min(int(request.args.get('per_page', 50)), 200)
conn = get_tenant_conn(g.tenant_id)
try:
result = list_shipments(
conn, g.tenant_id, status=status, courier_id=courier_id,
related_type=related_type, related_id=related_id,
page=page, per_page=per_page,
)
return jsonify(result)
finally:
conn.close()
@logistics_bp.route('/shipments', methods=['POST'])
@require_auth()
def create_new_shipment():
data = request.get_json() or {}
conn = get_tenant_conn(g.tenant_id)
try:
result = create_shipment(conn, {
'tenant_id': g.tenant_id,
'branch_id': data.get('branch_id', g.branch_id),
'shipment_type': data.get('shipment_type', 'outbound'),
'related_type': data.get('related_type'),
'related_id': data.get('related_id'),
'courier_id': data.get('courier_id'),
'tracking_number': data.get('tracking_number'),
'origin_address': data.get('origin_address'),
'destination_address': data.get('destination_address'),
'recipient_name': data.get('recipient_name'),
'recipient_phone': data.get('recipient_phone'),
'estimated_delivery': data.get('estimated_delivery'),
'shipping_cost': data.get('shipping_cost'),
'weight_kg': data.get('weight_kg'),
'dimensions_cm': data.get('dimensions_cm'),
'notes': data.get('notes'),
'created_by': getattr(g, 'employee_id', None),
})
return jsonify(result), 201
finally:
conn.close()
@logistics_bp.route('/shipments/<int:shipment_id>', methods=['GET'])
@require_auth()
def get_shipment_detail(shipment_id):
conn = get_tenant_conn(g.tenant_id)
try:
shipment = get_shipment(conn, shipment_id)
if not shipment:
return jsonify({'error': 'Shipment not found'}), 404
return jsonify(shipment)
finally:
conn.close()
@logistics_bp.route('/shipments/<int:shipment_id>/status', methods=['PUT'])
@require_auth()
def update_status_endpoint(shipment_id):
data = request.get_json() or {}
new_status = data.get('status')
if not new_status:
return jsonify({'error': 'status is required'}), 400
conn = get_tenant_conn(g.tenant_id)
try:
result = update_shipment_status(
conn, shipment_id, new_status,
location=data.get('location'),
description=data.get('description'),
raw_response=data.get('raw_response'),
)
return jsonify(result)
except ValueError as e:
return jsonify({'error': str(e)}), 400
finally:
conn.close()
@logistics_bp.route('/couriers', methods=['GET'])
@require_auth()
def list_couriers():
conn = get_tenant_conn(g.tenant_id)
try:
couriers = get_couriers(conn, g.tenant_id)
return jsonify({'couriers': couriers})
finally:
conn.close()
@logistics_bp.route('/couriers', methods=['POST'])
@require_auth()
def create_courier():
data = request.get_json() or {}
if not data.get('name') or not data.get('code'):
return jsonify({'error': 'name and code are required'}), 400
conn = get_tenant_conn(g.tenant_id)
try:
cid = add_courier(
conn, g.tenant_id, data['name'], data['code'],
tracking_url_template=data.get('tracking_url_template'),
api_endpoint=data.get('api_endpoint'),
is_active=data.get('is_active', True),
)
return jsonify({'id': cid, 'message': 'Courier created'}), 201
finally:
conn.close()