fix(audit): corrige errores criticos y mayores, mejora UX/accesibilidad y optimiza rendimiento
- 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:
@@ -13,7 +13,7 @@ fleet_bp = Blueprint('fleet', __name__, url_prefix='/pos/api/fleet')
|
||||
# ─── Vehicles CRUD ─────────────────────────────
|
||||
|
||||
@fleet_bp.route('/vehicles', methods=['GET'])
|
||||
@require_auth()
|
||||
@require_auth('fleet.view')
|
||||
def list_vehicles():
|
||||
"""List fleet vehicles with pagination and search.
|
||||
|
||||
@@ -83,7 +83,7 @@ def list_vehicles():
|
||||
|
||||
|
||||
@fleet_bp.route('/vehicles/<int:vehicle_id>', methods=['GET'])
|
||||
@require_auth()
|
||||
@require_auth('fleet.view')
|
||||
def get_vehicle(vehicle_id):
|
||||
"""Vehicle detail with maintenance schedules and recent logs."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
@@ -155,8 +155,117 @@ def get_vehicle(vehicle_id):
|
||||
return jsonify(vehicle)
|
||||
|
||||
|
||||
@fleet_bp.route('/vehicles/schedules', methods=['GET'])
|
||||
@require_auth('fleet.view')
|
||||
def list_all_schedules():
|
||||
"""Return all active maintenance schedules joined with vehicle data.
|
||||
|
||||
Replaces the N+1 pattern of fetching schedules per vehicle.
|
||||
"""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
where_clauses = ["v.is_active = true", "s.is_active = true"]
|
||||
params = []
|
||||
if g.branch_id:
|
||||
where_clauses.append("v.branch_id = %s")
|
||||
params.append(g.branch_id)
|
||||
|
||||
vehicle_ids = request.args.get('vehicle_ids', '').strip()
|
||||
if vehicle_ids:
|
||||
ids = [int(x) for x in vehicle_ids.split(',') if x.strip().isdigit()]
|
||||
if ids:
|
||||
where_clauses.append("v.id = ANY(%s)")
|
||||
params.append(ids)
|
||||
|
||||
where = " AND ".join(where_clauses)
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT s.id, s.vehicle_id, s.maintenance_type, s.interval_km,
|
||||
s.interval_months, s.last_done_at, s.last_done_km,
|
||||
s.next_due_at, s.next_due_km, s.notes,
|
||||
v.plate, v.make, v.model, v.current_mileage, v.color
|
||||
FROM fleet_maintenance_schedules s
|
||||
JOIN fleet_vehicles v ON v.id = s.vehicle_id
|
||||
WHERE {where}
|
||||
ORDER BY v.plate, s.next_due_at NULLS LAST, s.next_due_km NULLS LAST
|
||||
""", params)
|
||||
|
||||
schedules = []
|
||||
for r in cur.fetchall():
|
||||
schedules.append({
|
||||
'id': r[0], 'vehicle_id': r[1], 'maintenance_type': r[2],
|
||||
'interval_km': r[3], 'interval_months': r[4],
|
||||
'last_done_at': str(r[5]) if r[5] else None, 'last_done_km': r[6],
|
||||
'next_due_at': str(r[7]) if r[7] else None, 'next_due_km': r[8],
|
||||
'notes': r[9],
|
||||
'vehicle': {
|
||||
'plate': r[10], 'make': r[11], 'model': r[12],
|
||||
'current_mileage': r[13], 'color': r[14]
|
||||
}
|
||||
})
|
||||
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'data': schedules})
|
||||
|
||||
|
||||
@fleet_bp.route('/vehicles/history', methods=['GET'])
|
||||
@require_auth('fleet.view')
|
||||
def list_all_history():
|
||||
"""Return recent maintenance logs for all vehicles joined with vehicle data.
|
||||
|
||||
Replaces the N+1 pattern of fetching logs per vehicle.
|
||||
"""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
limit = min(int(request.args.get('limit', 200)), 500)
|
||||
|
||||
where_clauses = ["v.is_active = true"]
|
||||
params = [limit]
|
||||
if g.branch_id:
|
||||
where_clauses.append("v.branch_id = %s")
|
||||
params.append(g.branch_id)
|
||||
|
||||
vehicle_ids = request.args.get('vehicle_ids', '').strip()
|
||||
if vehicle_ids:
|
||||
ids = [int(x) for x in vehicle_ids.split(',') if x.strip().isdigit()]
|
||||
if ids:
|
||||
where_clauses.append("v.id = ANY(%s)")
|
||||
params.append(ids)
|
||||
|
||||
where = " AND ".join(where_clauses)
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT l.id, l.vehicle_id, l.schedule_id, l.maintenance_type,
|
||||
l.mileage_at, l.cost, l.parts_used, l.notes, l.created_at,
|
||||
e.name as employee_name,
|
||||
v.plate, v.make, v.model, v.color
|
||||
FROM fleet_maintenance_logs l
|
||||
JOIN fleet_vehicles v ON v.id = l.vehicle_id
|
||||
LEFT JOIN employees e ON l.employee_id = e.id
|
||||
WHERE {where}
|
||||
ORDER BY l.created_at DESC
|
||||
LIMIT %s
|
||||
""", params)
|
||||
|
||||
logs = []
|
||||
for r in cur.fetchall():
|
||||
logs.append({
|
||||
'id': r[0], 'vehicle_id': r[1], 'schedule_id': r[2],
|
||||
'maintenance_type': r[3], 'mileage_at': r[4],
|
||||
'cost': float(r[5]) if r[5] else 0, 'parts_used': r[6],
|
||||
'notes': r[7], 'created_at': str(r[8]) if r[8] else None,
|
||||
'employee_name': r[9],
|
||||
'vehicle': {'plate': r[10], 'make': r[11], 'model': r[12], 'color': r[13]}
|
||||
})
|
||||
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'data': logs})
|
||||
|
||||
|
||||
@fleet_bp.route('/vehicles', methods=['POST'])
|
||||
@require_auth()
|
||||
@require_auth('fleet.create')
|
||||
def create_vehicle():
|
||||
"""Create a fleet vehicle.
|
||||
|
||||
@@ -201,7 +310,7 @@ def create_vehicle():
|
||||
|
||||
|
||||
@fleet_bp.route('/vehicles/<int:vehicle_id>', methods=['PUT'])
|
||||
@require_auth()
|
||||
@require_auth('fleet.edit')
|
||||
def update_vehicle(vehicle_id):
|
||||
"""Update vehicle fields including mileage.
|
||||
|
||||
@@ -245,7 +354,7 @@ def update_vehicle(vehicle_id):
|
||||
|
||||
|
||||
@fleet_bp.route('/vehicles/<int:vehicle_id>', methods=['DELETE'])
|
||||
@require_auth()
|
||||
@require_auth('fleet.delete')
|
||||
def deactivate_vehicle(vehicle_id):
|
||||
"""Soft-delete: set is_active = false."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
@@ -266,7 +375,7 @@ def deactivate_vehicle(vehicle_id):
|
||||
# ─── Maintenance Schedules ─────────────────────────────
|
||||
|
||||
@fleet_bp.route('/vehicles/<int:vehicle_id>/schedules', methods=['GET'])
|
||||
@require_auth()
|
||||
@require_auth('fleet.view')
|
||||
def list_schedules(vehicle_id):
|
||||
"""Maintenance schedules for a vehicle."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
@@ -299,7 +408,7 @@ def list_schedules(vehicle_id):
|
||||
|
||||
|
||||
@fleet_bp.route('/vehicles/<int:vehicle_id>/schedules', methods=['POST'])
|
||||
@require_auth()
|
||||
@require_auth('fleet.create')
|
||||
def create_schedule(vehicle_id):
|
||||
"""Create maintenance schedule for a vehicle.
|
||||
|
||||
@@ -344,7 +453,7 @@ def create_schedule(vehicle_id):
|
||||
# ─── Maintenance Logs ─────────────────────────────
|
||||
|
||||
@fleet_bp.route('/vehicles/<int:vehicle_id>/log', methods=['POST'])
|
||||
@require_auth()
|
||||
@require_auth('fleet.create')
|
||||
def record_maintenance(vehicle_id):
|
||||
"""Record maintenance done. Updates schedule next_due if schedule_id provided.
|
||||
|
||||
@@ -427,7 +536,7 @@ def record_maintenance(vehicle_id):
|
||||
# ─── Alerts ─────────────────────────────
|
||||
|
||||
@fleet_bp.route('/alerts', methods=['GET'])
|
||||
@require_auth()
|
||||
@require_auth('fleet.view')
|
||||
def fleet_alerts():
|
||||
"""Vehicles with overdue maintenance (next_due_at < NOW() or next_due_km < current_mileage)."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
@@ -472,7 +581,7 @@ def fleet_alerts():
|
||||
# ─── Stats ─────────────────────────────
|
||||
|
||||
@fleet_bp.route('/stats', methods=['GET'])
|
||||
@require_auth()
|
||||
@require_auth('fleet.view')
|
||||
def fleet_stats():
|
||||
"""Fleet summary: total vehicles, overdue count, upcoming this month, total cost this month."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
|
||||
Reference in New Issue
Block a user