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
205 lines
6.3 KiB
Python
205 lines
6.3 KiB
Python
"""Service Order Blueprint: workshop Kanban management.
|
|
|
|
Prefix: /pos/api/service-orders
|
|
"""
|
|
|
|
from flask import Blueprint, request, jsonify, g
|
|
from middleware import require_auth
|
|
from tenant_db import get_tenant_conn
|
|
from services.service_order_engine import (
|
|
create_service_order, get_service_order, list_service_orders,
|
|
update_status, add_item, update_item, remove_item,
|
|
add_labor, update_labor, remove_labor,
|
|
update_service_order, get_kanban_summary,
|
|
)
|
|
|
|
service_order_bp = Blueprint('service_orders', __name__, url_prefix='/pos/api/service-orders')
|
|
|
|
|
|
@service_order_bp.route('', methods=['GET'])
|
|
@require_auth()
|
|
def list_orders():
|
|
status = request.args.get('status')
|
|
priority = request.args.get('priority')
|
|
customer_id = request.args.get('customer_id', type=int)
|
|
employee_id = request.args.get('employee_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_service_orders(
|
|
conn, status=status, branch_id=g.branch_id,
|
|
customer_id=customer_id, priority=priority,
|
|
employee_id=employee_id, page=page, per_page=per_page
|
|
)
|
|
return jsonify(result)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@service_order_bp.route('', methods=['POST'])
|
|
@require_auth()
|
|
def create_order():
|
|
data = request.get_json() or {}
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
result = create_service_order(conn, {
|
|
'tenant_id': g.tenant_id,
|
|
'branch_id': data.get('branch_id', g.branch_id),
|
|
'customer_id': data.get('customer_id'),
|
|
'vehicle_id': data.get('vehicle_id'),
|
|
'priority': data.get('priority', 'normal'),
|
|
'reception_notes': data.get('reception_notes'),
|
|
'estimated_cost': data.get('estimated_cost'),
|
|
'estimated_completion': data.get('estimated_completion'),
|
|
'employee_id': data.get('employee_id'),
|
|
'mileage_in': data.get('mileage_in'),
|
|
'fuel_level': data.get('fuel_level'),
|
|
'created_by': getattr(g, 'employee_id', None),
|
|
})
|
|
return jsonify(result), 201
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@service_order_bp.route('/<int:so_id>', methods=['GET'])
|
|
@require_auth()
|
|
def get_order(so_id):
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
order = get_service_order(conn, so_id)
|
|
if not order:
|
|
return jsonify({'error': 'Service order not found'}), 404
|
|
return jsonify(order)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@service_order_bp.route('/<int:so_id>', methods=['PUT'])
|
|
@require_auth()
|
|
def update_order(so_id):
|
|
data = request.get_json() or {}
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
ok = update_service_order(conn, so_id, data)
|
|
if not ok:
|
|
return jsonify({'error': 'No fields to update'}), 400
|
|
return jsonify({'message': 'Service order updated'})
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@service_order_bp.route('/<int:so_id>/status', methods=['PUT'])
|
|
@require_auth()
|
|
def change_status(so_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_status(
|
|
conn, so_id, new_status,
|
|
changed_by=getattr(g, 'employee_id', None),
|
|
notes=data.get('notes'),
|
|
)
|
|
return jsonify(result)
|
|
except ValueError as e:
|
|
return jsonify({'error': str(e)}), 400
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
# ─── Items (Parts) ─────────────────────────────
|
|
|
|
@service_order_bp.route('/<int:so_id>/items', methods=['POST'])
|
|
@require_auth()
|
|
def add_order_item(so_id):
|
|
data = request.get_json() or {}
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
item_id = add_item(conn, so_id, data)
|
|
return jsonify({'id': item_id, 'message': 'Item added'}), 201
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@service_order_bp.route('/items/<int:item_id>', methods=['PUT'])
|
|
@require_auth()
|
|
def update_order_item(item_id):
|
|
data = request.get_json() or {}
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
ok = update_item(conn, item_id, data)
|
|
if not ok:
|
|
return jsonify({'error': 'No fields to update'}), 400
|
|
return jsonify({'message': 'Item updated'})
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@service_order_bp.route('/items/<int:item_id>', methods=['DELETE'])
|
|
@require_auth()
|
|
def delete_order_item(item_id):
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
remove_item(conn, item_id)
|
|
return jsonify({'message': 'Item removed'})
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
# ─── Labor ─────────────────────────────
|
|
|
|
@service_order_bp.route('/<int:so_id>/labor', methods=['POST'])
|
|
@require_auth()
|
|
def add_order_labor(so_id):
|
|
data = request.get_json() or {}
|
|
if not data.get('description'):
|
|
return jsonify({'error': 'description is required'}), 400
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
labor_id = add_labor(conn, so_id, data)
|
|
return jsonify({'id': labor_id, 'message': 'Labor added'}), 201
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@service_order_bp.route('/labor/<int:labor_id>', methods=['PUT'])
|
|
@require_auth()
|
|
def update_order_labor(labor_id):
|
|
data = request.get_json() or {}
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
ok = update_labor(conn, labor_id, data)
|
|
if not ok:
|
|
return jsonify({'error': 'No fields to update'}), 400
|
|
return jsonify({'message': 'Labor updated'})
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@service_order_bp.route('/labor/<int:labor_id>', methods=['DELETE'])
|
|
@require_auth()
|
|
def delete_order_labor(labor_id):
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
remove_labor(conn, labor_id)
|
|
return jsonify({'message': 'Labor removed'})
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
# ─── Kanban Summary ─────────────────────────────
|
|
|
|
@service_order_bp.route('/kanban/summary', methods=['GET'])
|
|
@require_auth()
|
|
def kanban_summary():
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
summary = get_kanban_summary(conn, branch_id=g.branch_id)
|
|
return jsonify(summary)
|
|
finally:
|
|
conn.close()
|