"""Service Order Blueprint: workshop Kanban management. Prefix: /pos/api/service-orders """ from flask import Blueprint, g, jsonify, request from middleware import require_auth from services.service_order_engine import ( add_item, add_labor, assign_mechanic, convert_to_sale, create_service_catalog_item, create_service_order, delete_service_catalog_item, get_kanban_summary, get_service_order, list_service_catalog, list_service_orders, release_item, remove_item, remove_labor, reserve_item, update_item, update_labor, update_service_catalog_item, update_service_order, update_status, ) from tenant_db import get_tenant_conn 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('/', 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('/', 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('//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('//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/', 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/', 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('//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/', 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/', 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() # ─── Inventory reservation ──────────────────────── @service_order_bp.route('//items//reserve', methods=['POST']) @require_auth() def reserve_order_item(so_id, item_id): """Reserve inventory for a service order item.""" conn = get_tenant_conn(g.tenant_id) try: result = reserve_item(conn, item_id, branch_id=g.branch_id, employee_id=g.employee_id) return jsonify(result) except ValueError as e: return jsonify({'error': str(e)}), 400 finally: conn.close() @service_order_bp.route('//items//release', methods=['POST']) @require_auth() def release_order_item(so_id, item_id): """Release a previous inventory reservation.""" conn = get_tenant_conn(g.tenant_id) try: result = release_item(conn, item_id, employee_id=g.employee_id) return jsonify(result) except ValueError as e: return jsonify({'error': str(e)}), 400 finally: conn.close() # ─── Convert to sale ────────────────────────────── @service_order_bp.route('//convert-to-sale', methods=['POST']) @require_auth('pos.sell') def convert_order_to_sale(so_id): """Convert a service order into a POS sale. Body: { payment_method: 'efectivo' | 'transferencia' | 'tarjeta' | 'mixto', sale_type: 'cash' | 'credit' | 'mixed', register_id: int (optional), amount_paid: float (optional), payment_details: [...] (optional), notes: str (optional) } """ data = request.get_json() or {} sale_payload = { 'payment_method': data.get('payment_method', 'efectivo'), 'sale_type': data.get('sale_type', 'cash'), 'register_id': data.get('register_id'), 'amount_paid': data.get('amount_paid'), 'payment_details': data.get('payment_details', []), 'notes': data.get('notes'), } conn = get_tenant_conn(g.tenant_id) try: result = convert_to_sale( conn, so_id, sale_payload, employee_id=g.employee_id ) return jsonify(result), 201 except ValueError as e: return jsonify({'error': str(e)}), 400 finally: conn.close() # ─── Mechanic assignment ────────────────────────── @service_order_bp.route('//assign-mechanic', methods=['PUT']) @require_auth() def assign_mechanic_endpoint(so_id): """Assign a mechanic/technician to a service order.""" data = request.get_json() or {} employee_id = data.get('employee_id') if not employee_id: return jsonify({'error': 'employee_id is required'}), 400 conn = get_tenant_conn(g.tenant_id) try: result = assign_mechanic(conn, so_id, employee_id) return jsonify(result) except ValueError as e: return jsonify({'error': str(e)}), 400 finally: conn.close() # ─── Service catalog (reusable labor) ───────────── @service_order_bp.route('/service-catalog', methods=['GET']) @require_auth() def list_catalog(): """List reusable labor/service concepts.""" active_only = request.args.get('active_only', 'true').lower() != 'false' conn = get_tenant_conn(g.tenant_id) try: items = list_service_catalog(conn, active_only=active_only) return jsonify({'data': items}) finally: conn.close() @service_order_bp.route('/service-catalog', methods=['POST']) @require_auth() def create_catalog_item(): """Create a reusable labor concept.""" data = request.get_json() or {} if not data.get('name'): return jsonify({'error': 'name is required'}), 400 conn = get_tenant_conn(g.tenant_id) try: result = create_service_catalog_item(conn, g.tenant_id, data) return jsonify(result), 201 finally: conn.close() @service_order_bp.route('/service-catalog/', methods=['PUT']) @require_auth() def update_catalog_item(item_id): """Update a reusable labor concept.""" data = request.get_json() or {} conn = get_tenant_conn(g.tenant_id) try: ok = update_service_catalog_item(conn, item_id, data) if not ok: return jsonify({'error': 'No fields to update'}), 400 return jsonify({'message': 'Catalog item updated'}) finally: conn.close() @service_order_bp.route('/service-catalog/', methods=['DELETE']) @require_auth() def delete_catalog_item(item_id): """Soft-delete a reusable labor concept.""" conn = get_tenant_conn(g.tenant_id) try: delete_service_catalog_item(conn, item_id) return jsonify({'message': 'Catalog item deactivated'}) finally: conn.close() # ─── Thermal printing ───────────────────────────── @service_order_bp.route('//print', methods=['POST']) @require_auth() def print_service_order_ticket(so_id): """Generate a printable ticket for a service order. Body (optional): {printer_type: 'escpos_raw' | 'browser', width: 58 | 80} - escpos_raw: returns raw ESC/POS bytes (application/octet-stream) - browser: returns the data dict as JSON for browser-side rendering """ from flask import Response from services.thermal_printer import generate_service_order_ticket body = request.get_json(silent=True) or {} printer_type = body.get('printer_type', 'escpos_raw') width = int(body.get('width', 80)) conn = get_tenant_conn(g.tenant_id) cur = conn.cursor() order = get_service_order(conn, so_id) if not order: cur.close() conn.close() return jsonify({'error': 'Service order not found'}), 404 # Fetch business info from config business_info = {'name': 'NEXUS AUTOPARTS', 'rfc': '', 'address': ''} try: cur.execute( "SELECT key, value FROM config WHERE key IN ('business_name','rfc','address')" ) for rw in cur.fetchall(): if rw[0] == 'business_name': business_info['name'] = rw[1] else: business_info[rw[0]] = rw[1] except Exception: pass cur.close() conn.close() if printer_type == 'browser': return jsonify(order) raw = generate_service_order_ticket(order, business_info, width=width) return Response( raw, mimetype='application/octet-stream', headers={ 'Content-Disposition': f'attachment; filename=orden_{order.get("order_number", so_id)}.bin' }, )