fix(audit): corrige errores criticos y mayores, mejora UX/accesibilidad y optimiza rendimiento
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- Arregla @require_auth, permisos, race conditions, locks de caja/stock
- Elimina N+1 en layaway, flotilla, dashboard y global_invoice
- Asegura folios atomicos para CFDI, ordenes de servicio y polizas
- Protege client_secret de MercadoLibre en backend
- Conecta botones/filtros de config, customers, accounting e invoicing
- Mejora accesibilidad (labels/aria-label) y estados de carga/vacio
- Limpia accounting.js obsoleto y consolida accounting.v9.js
- Actualiza cache busting a v32 y Service Worker a v32
- Documenta todo en docs/AUDIT_Y_MEJORAS_2026-06-15.md

Tests: 35 passed
This commit is contained in:
2026-06-29 23:54:58 +00:00
parent 59a4893e84
commit 2bdeb2973a
61 changed files with 2879 additions and 706 deletions

View File

@@ -33,12 +33,15 @@ service_order_bp = Blueprint('service_orders', __name__, url_prefix='/pos/api/se
@service_order_bp.route('', methods=['GET'])
@require_auth()
@require_auth('workshop.view')
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)
delivery_method = request.args.get('delivery_method')
is_direct = request.args.get('is_direct', type=lambda v: v.lower() == 'true') if 'is_direct' in request.args else None
q = request.args.get('q')
page = int(request.args.get('page', 1))
per_page = min(int(request.args.get('per_page', 50)), 200)
@@ -47,15 +50,53 @@ def list_orders():
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
employee_id=employee_id, delivery_method=delivery_method,
is_direct=is_direct, q=q, page=page, per_page=per_page
)
return jsonify(result)
finally:
conn.close()
@service_order_bp.route('/from-pos', methods=['POST'])
@require_auth('pos.sell')
def create_order_from_pos():
"""Create a service order directly from the POS cart (quotation)."""
data = request.get_json() or {}
items = data.get('items', [])
estimated_cost = sum(
(it.get('unit_price') or 0) * (it.get('quantity') or 1)
for it in items
)
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': estimated_cost or None,
'estimated_completion': data.get('estimated_completion'),
'employee_id': data.get('employee_id'),
'mileage_in': data.get('mileage_in'),
'created_by': getattr(g, 'employee_id', None),
'delivery_method': data.get('delivery_method'),
'courier_id': data.get('courier_id'),
'is_direct': data.get('is_direct', False),
})
so_id = result['service_order_id']
for item in items:
add_item(conn, so_id, item)
order = get_service_order(conn, so_id)
return jsonify(order), 201
finally:
conn.close()
@service_order_bp.route('', methods=['POST'])
@require_auth()
@require_auth('workshop.edit')
def create_order():
data = request.get_json() or {}
conn = get_tenant_conn(g.tenant_id)
@@ -73,6 +114,9 @@ def create_order():
'mileage_in': data.get('mileage_in'),
'fuel_level': data.get('fuel_level'),
'created_by': getattr(g, 'employee_id', None),
'delivery_method': data.get('delivery_method'),
'courier_id': data.get('courier_id'),
'is_direct': data.get('is_direct', False),
})
return jsonify(result), 201
finally:
@@ -80,7 +124,7 @@ def create_order():
@service_order_bp.route('/<int:so_id>', methods=['GET'])
@require_auth()
@require_auth('workshop.view')
def get_order(so_id):
conn = get_tenant_conn(g.tenant_id)
try:
@@ -93,7 +137,7 @@ def get_order(so_id):
@service_order_bp.route('/<int:so_id>', methods=['PUT'])
@require_auth()
@require_auth('workshop.edit')
def update_order(so_id):
data = request.get_json() or {}
conn = get_tenant_conn(g.tenant_id)
@@ -107,7 +151,7 @@ def update_order(so_id):
@service_order_bp.route('/<int:so_id>/status', methods=['PUT'])
@require_auth()
@require_auth('workshop.edit')
def change_status(so_id):
data = request.get_json() or {}
new_status = data.get('status')
@@ -130,7 +174,7 @@ def change_status(so_id):
# ─── Items (Parts) ─────────────────────────────
@service_order_bp.route('/<int:so_id>/items', methods=['POST'])
@require_auth()
@require_auth('workshop.edit')
def add_order_item(so_id):
data = request.get_json() or {}
conn = get_tenant_conn(g.tenant_id)
@@ -142,7 +186,7 @@ def add_order_item(so_id):
@service_order_bp.route('/items/<int:item_id>', methods=['PUT'])
@require_auth()
@require_auth('workshop.edit')
def update_order_item(item_id):
data = request.get_json() or {}
conn = get_tenant_conn(g.tenant_id)
@@ -156,7 +200,7 @@ def update_order_item(item_id):
@service_order_bp.route('/items/<int:item_id>', methods=['DELETE'])
@require_auth()
@require_auth('workshop.edit')
def delete_order_item(item_id):
conn = get_tenant_conn(g.tenant_id)
try:
@@ -169,7 +213,7 @@ def delete_order_item(item_id):
# ─── Labor ─────────────────────────────
@service_order_bp.route('/<int:so_id>/labor', methods=['POST'])
@require_auth()
@require_auth('workshop.edit')
def add_order_labor(so_id):
data = request.get_json() or {}
if not data.get('description'):
@@ -183,7 +227,7 @@ def add_order_labor(so_id):
@service_order_bp.route('/labor/<int:labor_id>', methods=['PUT'])
@require_auth()
@require_auth('workshop.edit')
def update_order_labor(labor_id):
data = request.get_json() or {}
conn = get_tenant_conn(g.tenant_id)
@@ -197,7 +241,7 @@ def update_order_labor(labor_id):
@service_order_bp.route('/labor/<int:labor_id>', methods=['DELETE'])
@require_auth()
@require_auth('workshop.edit')
def delete_order_labor(labor_id):
conn = get_tenant_conn(g.tenant_id)
try:
@@ -210,7 +254,7 @@ def delete_order_labor(labor_id):
# ─── Kanban Summary ─────────────────────────────
@service_order_bp.route('/kanban/summary', methods=['GET'])
@require_auth()
@require_auth('workshop.view')
def kanban_summary():
conn = get_tenant_conn(g.tenant_id)
try:
@@ -224,7 +268,7 @@ def kanban_summary():
@service_order_bp.route('/<int:so_id>/items/<int:item_id>/reserve', methods=['POST'])
@require_auth()
@require_auth('workshop.edit')
def reserve_order_item(so_id, item_id):
"""Reserve inventory for a service order item."""
conn = get_tenant_conn(g.tenant_id)
@@ -238,7 +282,7 @@ def reserve_order_item(so_id, item_id):
@service_order_bp.route('/<int:so_id>/items/<int:item_id>/release', methods=['POST'])
@require_auth()
@require_auth('workshop.edit')
def release_order_item(so_id, item_id):
"""Release a previous inventory reservation."""
conn = get_tenant_conn(g.tenant_id)
@@ -294,7 +338,7 @@ def convert_order_to_sale(so_id):
@service_order_bp.route('/<int:so_id>/assign-mechanic', methods=['PUT'])
@require_auth()
@require_auth('workshop.edit')
def assign_mechanic_endpoint(so_id):
"""Assign a mechanic/technician to a service order."""
data = request.get_json() or {}
@@ -316,7 +360,7 @@ def assign_mechanic_endpoint(so_id):
@service_order_bp.route('/service-catalog', methods=['GET'])
@require_auth()
@require_auth('workshop.view')
def list_catalog():
"""List reusable labor/service concepts."""
active_only = request.args.get('active_only', 'true').lower() != 'false'
@@ -329,7 +373,7 @@ def list_catalog():
@service_order_bp.route('/service-catalog', methods=['POST'])
@require_auth()
@require_auth('workshop.edit')
def create_catalog_item():
"""Create a reusable labor concept."""
data = request.get_json() or {}
@@ -345,7 +389,7 @@ def create_catalog_item():
@service_order_bp.route('/service-catalog/<int:item_id>', methods=['PUT'])
@require_auth()
@require_auth('workshop.edit')
def update_catalog_item(item_id):
"""Update a reusable labor concept."""
data = request.get_json() or {}
@@ -360,7 +404,7 @@ def update_catalog_item(item_id):
@service_order_bp.route('/service-catalog/<int:item_id>', methods=['DELETE'])
@require_auth()
@require_auth('workshop.edit')
def delete_catalog_item(item_id):
"""Soft-delete a reusable labor concept."""
conn = get_tenant_conn(g.tenant_id)
@@ -375,7 +419,7 @@ def delete_catalog_item(item_id):
@service_order_bp.route('/<int:so_id>/print', methods=['POST'])
@require_auth()
@require_auth('workshop.view')
def print_service_order_ticket(so_id):
"""Generate a printable ticket for a service order.