diff --git a/pos/blueprints/service_order_bp.py b/pos/blueprints/service_order_bp.py index 85dc91d..14ff9bd 100644 --- a/pos/blueprints/service_order_bp.py +++ b/pos/blueprints/service_order_bp.py @@ -13,6 +13,7 @@ from services.service_order_engine import ( create_service_catalog_item, create_service_order, delete_service_catalog_item, + delete_service_order, get_kanban_summary, get_service_order, list_service_catalog, @@ -150,6 +151,23 @@ def update_order(so_id): conn.close() +@service_order_bp.route('/', methods=['DELETE']) +@require_auth('workshop.edit') +def delete_order(so_id): + """Soft-delete a service order. Restricted to owner/admin.""" + if g.employee_role not in ('owner', 'admin'): + return jsonify({'error': 'Solo administradores pueden eliminar órdenes'}), 403 + + conn = get_tenant_conn(g.tenant_id) + try: + delete_service_order(conn, so_id) + return jsonify({'message': 'Orden eliminada'}) + except ValueError as e: + return jsonify({'error': str(e)}), 404 + finally: + conn.close() + + @service_order_bp.route('//status', methods=['PUT']) @require_auth('workshop.edit') def change_status(so_id): diff --git a/pos/migrations/v4.12_service_order_soft_delete.sql b/pos/migrations/v4.12_service_order_soft_delete.sql new file mode 100644 index 0000000..54b3a4a --- /dev/null +++ b/pos/migrations/v4.12_service_order_soft_delete.sql @@ -0,0 +1,11 @@ +-- v4.12_service_order_soft_delete.sql +-- Soft-delete support for service orders. + +ALTER TABLE service_orders + ADD COLUMN IF NOT EXISTS is_deleted BOOLEAN NOT NULL DEFAULT FALSE; + +CREATE INDEX IF NOT EXISTS idx_service_orders_is_deleted + ON service_orders (is_deleted); + +-- Exclude deleted orders from common queries by default via partial index helpers +-- (application filters explicitly). diff --git a/pos/services/service_order_engine.py b/pos/services/service_order_engine.py index ffb55e8..0a36e34 100644 --- a/pos/services/service_order_engine.py +++ b/pos/services/service_order_engine.py @@ -115,7 +115,7 @@ def get_service_order(conn, so_id): LEFT JOIN employees creator ON so.created_by = creator.id LEFT JOIN branches b ON so.branch_id = b.id LEFT JOIN couriers co ON so.courier_id = co.id - WHERE so.id = %s + WHERE so.id = %s AND so.is_deleted = false """, (so_id,)) row = cur.fetchone() if not row: @@ -242,7 +242,8 @@ def list_service_orders(conn, status=None, branch_id=None, customer_id=None, where_clauses.append("(so.order_number ILIKE %s OR c.name ILIKE %s OR fv.plate ILIKE %s)") params.extend([f'%{q}%', f'%{q}%', f'%{q}%']) - where = " AND ".join(where_clauses) if where_clauses else "true" + where_clauses.append("so.is_deleted = false") + where = " AND ".join(where_clauses) cur.execute(f""" SELECT count(*) FROM service_orders so @@ -309,7 +310,7 @@ def list_service_orders(conn, status=None, branch_id=None, customer_id=None, def update_status(conn, so_id, new_status, changed_by=None, notes=None): """Update service order status with validation.""" cur = conn.cursor() - cur.execute("SELECT status FROM service_orders WHERE id = %s", (so_id,)) + cur.execute("SELECT status FROM service_orders WHERE id = %s AND is_deleted = false", (so_id,)) row = cur.fetchone() if not row: cur.close() @@ -486,7 +487,7 @@ def get_kanban_summary(conn, branch_id=None): cur.execute(f""" SELECT status, COUNT(*) as cnt FROM service_orders - WHERE status != 'cancelled' {branch_filter} + WHERE status != 'cancelled' AND is_deleted = false {branch_filter} GROUP BY status """, params) @@ -499,6 +500,7 @@ def get_kanban_summary(conn, branch_id=None): SELECT count(*) FROM service_orders WHERE estimated_completion < NOW() AND status NOT IN ('ready', 'delivered', 'cancelled') + AND is_deleted = false {branch_filter} """, params) overdue = cur.fetchone()[0] @@ -813,7 +815,7 @@ def convert_to_sale(conn, so_id, sale_data, employee_id=None): def assign_mechanic(conn, so_id, employee_id): """Assign a mechanic/technician to a service order.""" cur = conn.cursor() - cur.execute("SELECT id FROM service_orders WHERE id = %s", (so_id,)) + cur.execute("SELECT id FROM service_orders WHERE id = %s AND is_deleted = false", (so_id,)) if not cur.fetchone(): cur.close() raise ValueError("Service order not found") @@ -827,6 +829,21 @@ def assign_mechanic(conn, so_id, employee_id): return {"employee_id": employee_id} +def delete_service_order(conn, so_id): + """Soft-delete a service order.""" + cur = conn.cursor() + cur.execute( + "UPDATE service_orders SET is_deleted = true WHERE id = %s AND is_deleted = false", + (so_id,), + ) + deleted = cur.rowcount + conn.commit() + cur.close() + if deleted == 0: + raise ValueError("Service order not found") + return {"deleted": True} + + # ─── Service catalog (reusable labor concepts) ─────────────────────────────── diff --git a/pos/static/js/workshop.js b/pos/static/js/workshop.js index 4da383a..15eeb1c 100644 --- a/pos/static/js/workshop.js +++ b/pos/static/js/workshop.js @@ -27,6 +27,7 @@ var Workshop = (function() { var hidePrices = role === 'workshop' || role === 'mechanic'; var perms = user.permissions || []; var canEdit = role === 'owner' || role === 'admin' || perms.indexOf('workshop.edit') !== -1; + var canDelete = role === 'owner' || role === 'admin'; var canSell = role === 'owner' || role === 'admin' || perms.indexOf('pos.sell') !== -1; var COLUMNS = [ @@ -413,6 +414,7 @@ var Workshop = (function() { '' : ''; footer.innerHTML = statusHtml + '' + + (canDelete ? '' : '') + '' + @@ -699,6 +701,18 @@ var Workshop = (function() { }).catch(function(e) { alert('Error: ' + e.message); }); } + function deleteOrder() { + if (!currentOrderId) return; + if (!confirm('¿Eliminar esta orden de servicio? Se ocultará del taller pero las reservas de inventario y la venta asociada (si existe) no se verán afectadas.')) return; + api('DELETE', '/' + currentOrderId, {}) + .then(function() { + closeDetailModal(); + loadSummary(); + loadOrders(); + }) + .catch(function(e) { alert('Error: ' + e.message); }); + } + function printOrder() { if (!currentOrderId) return; if (!window.NexusPrinter || !window.NexusPrinter.isConnected()) { @@ -907,6 +921,7 @@ var Workshop = (function() { addSelectedItem: addSelectedItem, addLabor: addLabor, convertToSale: convertToSale, + deleteOrder: deleteOrder, printOrder: printOrder, openNewOrderModal: openNewOrderModal, closeNewOrderModal: closeNewOrderModal, diff --git a/pos/templates/workshop.html b/pos/templates/workshop.html index 3234038..e56ac24 100644 --- a/pos/templates/workshop.html +++ b/pos/templates/workshop.html @@ -15,7 +15,7 @@ - +