feat(pos/workshop): add lightweight workshop/taller module
- Add DB migration v4.4_workshop.sql (sale_id, service_catalog, reserved_quantity, SO_RESERVE/SO_RELEASE operation types). - Extend service_order_engine with inventory reservation, release, convert-to-sale, mechanic assignment, and service catalog CRUD. - Extend service_order_bp with /reserve, /convert-to-sale, /assign-mechanic, and /service-catalog endpoints. - Create workshop Kanban UI: workshop.html, workshop.js, workshop.css. - Add /pos/workshop route and sidebar navigation (sidebar.js + inline templates). - Add 11 unit tests with mocked cursors. - Update FASES_IMPLEMENTADAS.md with FASE 9 documentation. Tests: 92 passing (61 console + 20 Facturapi + 11 workshop).
This commit is contained in:
@@ -3,15 +3,31 @@
|
||||
Prefix: /pos/api/service-orders
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify, g
|
||||
from flask import Blueprint, g, jsonify, request
|
||||
from middleware import require_auth
|
||||
from tenant_db import get_tenant_conn
|
||||
from services.service_order_engine import (
|
||||
create_service_order, get_service_order, list_service_orders,
|
||||
update_status, add_item, update_item, remove_item,
|
||||
add_labor, update_labor, remove_labor,
|
||||
update_service_order, get_kanban_summary,
|
||||
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')
|
||||
|
||||
@@ -202,3 +218,154 @@ def kanban_summary():
|
||||
return jsonify(summary)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── Inventory reservation ────────────────────────
|
||||
|
||||
|
||||
@service_order_bp.route('/<int:so_id>/items/<int:item_id>/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('/<int:so_id>/items/<int:item_id>/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('/<int:so_id>/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('/<int:so_id>/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/<int:item_id>', 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/<int:item_id>', 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()
|
||||
|
||||
Reference in New Issue
Block a user