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:
131
pos/blueprints/logistics_bp.py
Normal file
131
pos/blueprints/logistics_bp.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user