From 66b8fe3d690f26b2b4d8efc635dac34a505d36a2 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Tue, 30 Jun 2026 19:53:00 +0000 Subject: [PATCH] feat(workshop): vehicles must be assigned to a customer - Added customer_id column to fleet_vehicles with FK to customers. - Enforced customer_id on create_vehicle endpoints (workshop and fleet). - New vehicle modal now requires a selected customer and stores customer_id. - Migration v4.13 applied to all tenants. --- pos/blueprints/fleet_bp.py | 18 ++++++++++---- pos/blueprints/service_order_bp.py | 21 ++++++++++++---- .../v4.13_fleet_vehicle_customer.sql | 20 ++++++++++++++++ pos/static/js/workshop.js | 24 +++++++++++++++++-- pos/templates/workshop.html | 9 +++---- 5 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 pos/migrations/v4.13_fleet_vehicle_customer.sql diff --git a/pos/blueprints/fleet_bp.py b/pos/blueprints/fleet_bp.py index 116f412..faad711 100644 --- a/pos/blueprints/fleet_bp.py +++ b/pos/blueprints/fleet_bp.py @@ -269,11 +269,13 @@ def list_all_history(): def create_vehicle(): """Create a fleet vehicle. - Body: {plate, vin, make, model, year, current_mileage, fuel_type, color, owner_name, notes} + Body: {customer_id, plate, vin, make, model, year, current_mileage, fuel_type, color, owner_name, notes} """ data = request.get_json() or {} if not data.get('plate') and not data.get('vin'): return jsonify({'error': 'plate or vin is required'}), 400 + if not data.get('customer_id'): + return jsonify({'error': 'El vehiculo debe estar asignado a un cliente'}), 400 branch_id = data.get('branch_id', g.branch_id) @@ -281,17 +283,23 @@ def create_vehicle(): cur = conn.cursor() try: + cur.execute("SELECT name FROM customers WHERE id = %s", (data['customer_id'],)) + cust = cur.fetchone() + if not cust: + cur.close(); conn.close() + return jsonify({'error': 'Cliente no encontrado'}), 404 + cur.execute(""" INSERT INTO fleet_vehicles - (branch_id, plate, vin, make, model, year, + (branch_id, customer_id, plate, vin, make, model, year, current_mileage, fuel_type, color, owner_name, notes) - VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) RETURNING id """, ( - branch_id, data.get('plate'), data.get('vin'), + branch_id, data['customer_id'], data.get('plate'), data.get('vin'), data.get('make'), data.get('model'), data.get('year'), data.get('current_mileage', 0), data.get('fuel_type', 'gasolina'), - data.get('color'), data.get('owner_name'), data.get('notes'), + data.get('color'), data.get('owner_name') or cust[0], data.get('notes'), )) vehicle_id = cur.fetchone()[0] diff --git a/pos/blueprints/service_order_bp.py b/pos/blueprints/service_order_bp.py index 224acf3..ff468ee 100644 --- a/pos/blueprints/service_order_bp.py +++ b/pos/blueprints/service_order_bp.py @@ -413,25 +413,36 @@ def create_customer_for_workshop(): @service_order_bp.route('/vehicles', methods=['POST']) @require_auth('workshop.edit') def create_vehicle_for_workshop(): - """Create a fleet vehicle directly from the workshop flow.""" + """Create a fleet vehicle directly from the workshop flow. + + Vehicles must be assigned to a customer. + """ data = request.get_json() or {} if not data.get('plate') and not data.get('vin'): return jsonify({'error': 'Se requiere placa o VIN'}), 400 + if not data.get('customer_id'): + return jsonify({'error': 'El vehiculo debe estar asignado a un cliente'}), 400 conn = get_tenant_conn(g.tenant_id) cur = conn.cursor() try: + cur.execute("SELECT name FROM customers WHERE id = %s", (data['customer_id'],)) + cust = cur.fetchone() + if not cust: + cur.close(); conn.close() + return jsonify({'error': 'Cliente no encontrado'}), 404 + cur.execute(""" INSERT INTO fleet_vehicles - (branch_id, plate, vin, make, model, year, + (branch_id, customer_id, plate, vin, make, model, year, current_mileage, fuel_type, color, owner_name, notes) - VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) RETURNING id """, ( - data.get('branch_id', g.branch_id), data.get('plate'), data.get('vin'), + data.get('branch_id', g.branch_id), data['customer_id'], data.get('plate'), data.get('vin'), data.get('make'), data.get('model'), data.get('year'), data.get('current_mileage', 0), data.get('fuel_type', 'gasolina'), - data.get('color'), data.get('owner_name'), data.get('notes'), + data.get('color'), cust[0], data.get('notes'), )) vehicle_id = cur.fetchone()[0] conn.commit() diff --git a/pos/migrations/v4.13_fleet_vehicle_customer.sql b/pos/migrations/v4.13_fleet_vehicle_customer.sql new file mode 100644 index 0000000..8a57b76 --- /dev/null +++ b/pos/migrations/v4.13_fleet_vehicle_customer.sql @@ -0,0 +1,20 @@ +-- v4.13_fleet_vehicle_customer.sql +-- Link fleet vehicles to a customer. + +ALTER TABLE fleet_vehicles + ADD COLUMN IF NOT EXISTS customer_id INTEGER; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'fleet_vehicles_customer_id_fkey' + ) THEN + ALTER TABLE fleet_vehicles + ADD CONSTRAINT fleet_vehicles_customer_id_fkey + FOREIGN KEY (customer_id) REFERENCES customers(id); + END IF; +END $$; + +CREATE INDEX IF NOT EXISTS idx_fleet_vehicles_customer_id + ON fleet_vehicles (customer_id); diff --git a/pos/static/js/workshop.js b/pos/static/js/workshop.js index 4162cd0..856b747 100644 --- a/pos/static/js/workshop.js +++ b/pos/static/js/workshop.js @@ -708,6 +708,22 @@ var Workshop = (function() { function openNewVehicleModal(context) { newVehicleContext = context || 'edit'; + var customerId = null; + var customerName = ''; + if (newVehicleContext === 'new') { + var sel = document.getElementById('noCustomer'); + customerId = sel ? parseInt(sel.value, 10) || null : null; + customerName = sel && sel.selectedIndex >= 0 ? sel.options[sel.selectedIndex].text : ''; + } else { + customerId = currentOrder ? currentOrder.customer_id : null; + customerName = currentOrder ? (currentOrder.customer_name || '') : ''; + } + if (!customerId) { + alert('Primero selecciona un cliente para poder crear el vehículo.'); + return; + } + document.getElementById('nvCustomerId').value = customerId; + document.getElementById('nvCustomerName').value = customerName; document.getElementById('newVehicleModal').classList.add('is-open'); document.getElementById('nvPlate').value = ''; document.getElementById('nvVIN').value = ''; @@ -715,7 +731,6 @@ var Workshop = (function() { document.getElementById('nvModel').value = ''; document.getElementById('nvYear').value = ''; document.getElementById('nvColor').value = ''; - document.getElementById('nvOwner').value = ''; } function openNewVehicleModalFromNewOrder() { @@ -729,18 +744,23 @@ var Workshop = (function() { async function saveNewVehicle() { var plate = document.getElementById('nvPlate').value.trim(); var vin = document.getElementById('nvVIN').value.trim(); + var customerId = parseInt(document.getElementById('nvCustomerId').value, 10) || null; if (!plate && !vin) { alert('Se requiere al menos placa o VIN'); return; } + if (!customerId) { + alert('El vehículo debe estar asignado a un cliente'); + return; + } var payload = { plate: plate, vin: vin, + customer_id: customerId, make: document.getElementById('nvMake').value.trim() || null, model: document.getElementById('nvModel').value.trim() || null, year: document.getElementById('nvYear').value ? parseInt(document.getElementById('nvYear').value, 10) : null, color: document.getElementById('nvColor').value.trim() || null, - owner_name: document.getElementById('nvOwner').value.trim() || null, branch_id: (currentOrder ? currentOrder.branch_id : null) || (window.POS_USER ? window.POS_USER.branch_id : null) }; try { diff --git a/pos/templates/workshop.html b/pos/templates/workshop.html index 03fc1f5..4086004 100644 --- a/pos/templates/workshop.html +++ b/pos/templates/workshop.html @@ -15,7 +15,7 @@ - +