feat(permissions): granular module permissions + workshop permissions matrix
This commit is contained in:
@@ -45,6 +45,47 @@ _DEFAULT_ROLE_PERMISSIONS = {
|
||||
}
|
||||
|
||||
|
||||
# Statuses used in the workshop kanban. Must stay in sync with workshop.js COLUMNS.
|
||||
_WORKSHOP_STATUSES = [
|
||||
'por_revisar', 'en_revision', 'revisada', 'cotizada', 'por_autorizar',
|
||||
'autorizada', 'autorizacion_parcial', 'en_reparacion', 'reparada',
|
||||
'por_entregar', 'entregado', 'por_enviar', 'enviado',
|
||||
'por_facturar', 'facturada', 'por_recolectar', 'cancelada'
|
||||
]
|
||||
|
||||
# Workshop-specific actions configurable per role.
|
||||
_WORKSHOP_ACTIONS = [
|
||||
{'key': 'create_order', 'label': 'Crear órdenes'},
|
||||
{'key': 'edit_order', 'label': 'Editar órdenes'},
|
||||
{'key': 'delete_order', 'label': 'Eliminar órdenes'},
|
||||
{'key': 'assign_mechanic', 'label': 'Asignar mecánico'},
|
||||
{'key': 'add_items', 'label': 'Agregar artículos'},
|
||||
{'key': 'add_labor', 'label': 'Agregar mano de obra'},
|
||||
{'key': 'change_status', 'label': 'Cambiar estatus'},
|
||||
{'key': 'convert_to_sale', 'label': 'Convertir a venta'},
|
||||
{'key': 'convert_to_remission', 'label': 'Generar nota de remisión'},
|
||||
{'key': 'view_customer_data', 'label': 'Ver datos del cliente/vehículo'},
|
||||
{'key': 'view_prices', 'label': 'Ver precios/costos'},
|
||||
{'key': 'view_notes', 'label': 'Ver bitácora'},
|
||||
]
|
||||
|
||||
# Default workshop permissions per role. Admins see everything; restricted roles only work statuses.
|
||||
_DEFAULT_WORKSHOP_PERMISSIONS = {
|
||||
'owner': {'statuses': _WORKSHOP_STATUSES, 'actions': [a['key'] for a in _WORKSHOP_ACTIONS]},
|
||||
'admin': {'statuses': _WORKSHOP_STATUSES, 'actions': [a['key'] for a in _WORKSHOP_ACTIONS]},
|
||||
'manager': {'statuses': _WORKSHOP_STATUSES, 'actions': [a['key'] for a in _WORKSHOP_ACTIONS if a['key'] != 'delete_order']},
|
||||
'counter': {'statuses': _WORKSHOP_STATUSES, 'actions': [a['key'] for a in _WORKSHOP_ACTIONS if a['key'] != 'delete_order']},
|
||||
'cashier': {'statuses': _WORKSHOP_STATUSES, 'actions': [a['key'] for a in _WORKSHOP_ACTIONS if a['key'] not in ('delete_order', 'convert_to_sale')]},
|
||||
'workshop': {
|
||||
'statuses': [s for s in _WORKSHOP_STATUSES if s not in ('por_entregar', 'entregado', 'por_enviar', 'enviado', 'por_recolectar')],
|
||||
'actions': ['change_status', 'add_labor', 'view_notes']
|
||||
},
|
||||
'mechanic': {
|
||||
'statuses': ['por_revisar', 'en_revision', 'revisada', 'en_reparacion', 'reparada', 'autorizada', 'cancelada'],
|
||||
'actions': ['change_status', 'add_labor', 'view_notes']
|
||||
},
|
||||
}
|
||||
|
||||
_AVAILABLE_PERMISSIONS = [
|
||||
{'module': 'Dashboard', 'permissions': [
|
||||
{'key': 'dashboard.view', 'label': 'Ver Dashboard'},
|
||||
@@ -55,23 +96,33 @@ _AVAILABLE_PERMISSIONS = [
|
||||
{'key': 'pos.discount', 'label': 'Aplicar descuentos'},
|
||||
{'key': 'pos.cancel', 'label': 'Cancelar ventas'},
|
||||
{'key': 'pos.remission', 'label': 'Notas de remisión'},
|
||||
{'key': 'pos.view_cost', 'label': 'Ver costos en POS'},
|
||||
]},
|
||||
{'module': 'Inventario', 'permissions': [
|
||||
{'key': 'inventory.view', 'label': 'Ver inventario'},
|
||||
{'key': 'inventory.create', 'label': 'Crear artículos'},
|
||||
{'key': 'inventory.edit', 'label': 'Editar/eliminar artículos'},
|
||||
{'key': 'inventory.edit', 'label': 'Editar artículos'},
|
||||
{'key': 'inventory.delete', 'label': 'Eliminar artículos'},
|
||||
{'key': 'inventory.adjust', 'label': 'Ajustar stock'},
|
||||
{'key': 'inventory.transfer', 'label': 'Transferir entre sucursales'},
|
||||
{'key': 'inventory.import', 'label': 'Importar artículos masivamente'},
|
||||
{'key': 'inventory.view_cost', 'label': 'Ver costos'},
|
||||
]},
|
||||
{'module': 'Catálogo', 'permissions': [
|
||||
{'key': 'catalog.view', 'label': 'Ver catálogo'},
|
||||
{'key': 'catalog.edit', 'label': 'Editar catálogo'},
|
||||
]},
|
||||
{'module': 'Clientes', 'permissions': [
|
||||
{'key': 'customers.view', 'label': 'Ver clientes'},
|
||||
{'key': 'customers.create', 'label': 'Crear/editar clientes'},
|
||||
{'key': 'customers.create', 'label': 'Crear clientes'},
|
||||
{'key': 'customers.edit', 'label': 'Editar clientes'},
|
||||
{'key': 'customers.delete', 'label': 'Eliminar clientes'},
|
||||
{'key': 'customers.edit_credit', 'label': 'Editar límite de crédito'},
|
||||
]},
|
||||
{'module': 'Taller', 'permissions': [
|
||||
{'key': 'workshop.view', 'label': 'Ver órdenes'},
|
||||
{'key': 'workshop.edit', 'label': 'Crear/editar órdenes'},
|
||||
{'key': 'workshop.add_items', 'label': 'Agregar artículos/mano de obra'},
|
||||
{'key': 'workshop.view', 'label': 'Ver módulo Taller'},
|
||||
{'key': 'workshop.edit', 'label': 'Crear/editar órdenes (deprecado, usar matriz Taller)'},
|
||||
{'key': 'workshop.add_items', 'label': 'Agregar artículos/mano de obra (deprecado, usar matriz Taller)'},
|
||||
]},
|
||||
{'module': 'Facturación', 'permissions': [
|
||||
{'key': 'invoicing.view', 'label': 'Ver facturas'},
|
||||
@@ -79,8 +130,9 @@ _AVAILABLE_PERMISSIONS = [
|
||||
{'key': 'invoicing.cancel', 'label': 'Cancelar facturas'},
|
||||
]},
|
||||
{'module': 'Configuración', 'permissions': [
|
||||
{'key': 'config.view', 'label': 'Ver configuración'},
|
||||
{'key': 'config.edit', 'label': 'Editar configuración'},
|
||||
{'key': 'config.edit_prices', 'label': 'Modificar precios'},
|
||||
{'key': 'config.edit_prices', 'label': 'Modificar precios globales'},
|
||||
]},
|
||||
{'module': 'Contabilidad', 'permissions': [
|
||||
{'key': 'accounting.view', 'label': 'Ver contabilidad'},
|
||||
@@ -115,6 +167,30 @@ def _get_role_permissions(conn, role):
|
||||
return list(_DEFAULT_ROLE_PERMISSIONS.get(role, []))
|
||||
|
||||
|
||||
def _get_workshop_permissions(conn, role):
|
||||
"""Return configured workshop permissions for a role, falling back to defaults."""
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT value FROM tenant_config WHERE key = 'workshop_permissions'")
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
if row and row[0]:
|
||||
try:
|
||||
configured = json.loads(row[0])
|
||||
if isinstance(configured, dict) and role in configured:
|
||||
return configured[role]
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return _DEFAULT_WORKSHOP_PERMISSIONS.get(role, {'statuses': [], 'actions': []})
|
||||
|
||||
|
||||
def _get_all_workshop_permissions(conn):
|
||||
"""Return effective workshop permissions for every known role."""
|
||||
result = {}
|
||||
for role in _DEFAULT_WORKSHOP_PERMISSIONS:
|
||||
result[role] = _get_workshop_permissions(conn, role)
|
||||
return result
|
||||
|
||||
|
||||
@config_bp.route('/branches', methods=['GET'])
|
||||
@require_auth()
|
||||
def list_branches():
|
||||
@@ -444,6 +520,50 @@ def save_role_permissions_config():
|
||||
return jsonify({'ok': True, 'updated_roles': list(data['roles'].keys())})
|
||||
|
||||
|
||||
@config_bp.route('/role-permissions/workshop', methods=['GET'])
|
||||
@require_auth('config.view')
|
||||
def get_workshop_permissions_config():
|
||||
"""Return the configured workshop permissions (statuses + actions) per role."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
return jsonify({
|
||||
'roles': _get_all_workshop_permissions(conn),
|
||||
'statuses': [{'key': s, 'label': s.replace('_', ' ').title()} for s in _WORKSHOP_STATUSES],
|
||||
'actions': _WORKSHOP_ACTIONS,
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@config_bp.route('/role-permissions/workshop', methods=['PUT'])
|
||||
@require_auth('config.edit')
|
||||
def save_workshop_permissions_config():
|
||||
"""Save the workshop permission mapping per role."""
|
||||
if g.employee_role not in ('owner', 'admin'):
|
||||
return jsonify({'error': 'Solo administradores pueden editar permisos de roles'}), 403
|
||||
data = request.get_json() or {}
|
||||
if 'roles' not in data:
|
||||
return jsonify({'error': 'roles object required'}), 400
|
||||
|
||||
for role, cfg in data['roles'].items():
|
||||
if role not in _DEFAULT_WORKSHOP_PERMISSIONS:
|
||||
return jsonify({'error': f'Invalid role: {role}'}), 400
|
||||
if not isinstance(cfg, dict):
|
||||
return jsonify({'error': f'config for {role} must be an object'}), 400
|
||||
if not isinstance(cfg.get('statuses', []), list) or not isinstance(cfg.get('actions', []), list):
|
||||
return jsonify({'error': f'statuses/actions for {role} must be lists'}), 400
|
||||
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
INSERT INTO tenant_config (key, value) VALUES ('workshop_permissions', %s)
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
||||
""", (json.dumps(data['roles']),))
|
||||
conn.commit()
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'ok': True, 'updated_roles': list(data['roles'].keys())})
|
||||
|
||||
|
||||
@config_bp.route('/employees/<int:emp_id>', methods=['PUT'])
|
||||
@require_auth('config.edit')
|
||||
def update_employee(emp_id):
|
||||
|
||||
@@ -33,45 +33,43 @@ from services.service_order_engine import (
|
||||
update_status,
|
||||
)
|
||||
from tenant_db import get_tenant_conn
|
||||
from blueprints.config_bp import _get_workshop_permissions, _DEFAULT_WORKSHOP_PERMISSIONS
|
||||
|
||||
service_order_bp = Blueprint('service_orders', __name__, url_prefix='/pos/api/service-orders')
|
||||
|
||||
|
||||
# Roles allowed to view or edit service orders.
|
||||
# Counter (mostrador) creates/edits; workshop/mechanic only view restricted info.
|
||||
# Roles allowed to access the workshop module at all.
|
||||
_WORKSHOP_VIEW_ROLES = {'owner', 'admin', 'counter', 'cashier', 'workshop', 'mechanic'}
|
||||
_WORKSHOP_EDIT_ROLES = {'owner', 'admin', 'counter', 'cashier'}
|
||||
|
||||
# Statuses that mechanics are not allowed to see (single shared mechanic account).
|
||||
# 'autorizada' is intentionally NOT hidden because mechanics must work on authorized orders.
|
||||
# Delivery/logistics statuses are hidden for both workshop and mechanic.
|
||||
_MECHANIC_HIDDEN_STATUSES = {
|
||||
'cotizada', 'por_autorizar', 'autorizacion_parcial',
|
||||
'por_facturar', 'facturada',
|
||||
'por_entregar', 'entregado', 'por_enviar', 'enviado', 'por_recolectar'
|
||||
}
|
||||
|
||||
# Statuses hidden for workshop accounts (delivery/logistics only).
|
||||
_WORKSHOP_HIDDEN_STATUSES = {
|
||||
'por_entregar', 'entregado', 'por_enviar', 'enviado', 'por_recolectar'
|
||||
}
|
||||
|
||||
|
||||
def _can_view_workshop():
|
||||
return g.employee_role in _WORKSHOP_VIEW_ROLES or 'workshop.view' in g.permissions
|
||||
|
||||
|
||||
def _can_edit_workshop():
|
||||
return g.employee_role in _WORKSHOP_EDIT_ROLES
|
||||
def _get_ws_perms():
|
||||
"""Return effective workshop permissions for the current user."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
return _get_workshop_permissions(conn, g.employee_role)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _is_restricted_workshop_viewer():
|
||||
return g.employee_role in ('workshop', 'mechanic')
|
||||
def _visible_statuses():
|
||||
if g.employee_role in ('owner', 'admin'):
|
||||
return list(_DEFAULT_WORKSHOP_PERMISSIONS['admin']['statuses'])
|
||||
return _get_ws_perms().get('statuses', [])
|
||||
|
||||
|
||||
def _redact_order_for_mechanic(order):
|
||||
"""Remove customer/commercial data from the order payload for mechanics."""
|
||||
if not _is_restricted_workshop_viewer():
|
||||
def _can_do_action(action):
|
||||
if g.employee_role in ('owner', 'admin'):
|
||||
return True
|
||||
return action in _get_ws_perms().get('actions', [])
|
||||
|
||||
|
||||
def _redact_order_for_restricted(order):
|
||||
"""Remove customer/commercial data when the role cannot view sensitive data."""
|
||||
if g.employee_role in ('owner', 'admin') or _can_do_action('view_customer_data'):
|
||||
return order
|
||||
sensitive = {
|
||||
'customer_id', 'customer_name', 'customer_phone', 'customer_address',
|
||||
@@ -93,14 +91,14 @@ def _redact_order_for_mechanic(order):
|
||||
if k in sensitive:
|
||||
redacted[k] = redact_value(v)
|
||||
elif k == 'items' and isinstance(v, list):
|
||||
redacted[k] = [_redact_item_for_mechanic(it) for it in v]
|
||||
redacted[k] = [_redact_item_for_restricted(it) for it in v]
|
||||
else:
|
||||
redacted[k] = v
|
||||
return redacted
|
||||
|
||||
|
||||
def _redact_item_for_mechanic(item):
|
||||
"""Hide mechanic name from item rows for mechanics."""
|
||||
def _redact_item_for_restricted(item):
|
||||
"""Hide sensitive item rows for restricted viewers."""
|
||||
item = dict(item)
|
||||
for k in ('mechanic_id', 'unit_cost', 'unit_price'):
|
||||
if k in item:
|
||||
@@ -118,14 +116,18 @@ def require_workshop_view(f):
|
||||
return decorated
|
||||
|
||||
|
||||
def require_workshop_edit(f):
|
||||
@wraps(f)
|
||||
@require_auth()
|
||||
def decorated(*args, **kwargs):
|
||||
if not _can_edit_workshop():
|
||||
return jsonify({'error': 'Missing permissions: workshop.edit'}), 403
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
def require_workshop_action(action):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
@require_auth()
|
||||
def decorated(*args, **kwargs):
|
||||
if not _can_view_workshop():
|
||||
return jsonify({'error': 'Missing permissions: workshop.view'}), 403
|
||||
if not _can_do_action(action):
|
||||
return jsonify({'error': f'Missing permissions: workshop.{action}'}), 403
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
return decorator
|
||||
|
||||
|
||||
@service_order_bp.route('', methods=['GET'])
|
||||
@@ -149,12 +151,11 @@ def list_orders():
|
||||
employee_id=employee_id, delivery_method=delivery_method,
|
||||
is_direct=is_direct, q=q, page=page, per_page=per_page
|
||||
)
|
||||
if _is_restricted_workshop_viewer():
|
||||
result['data'] = [_redact_order_for_mechanic(o) for o in result.get('data', [])]
|
||||
hidden_statuses = _MECHANIC_HIDDEN_STATUSES if g.employee_role == 'mechanic' else _WORKSHOP_HIDDEN_STATUSES
|
||||
if g.employee_role not in ('owner', 'admin'):
|
||||
visible = _visible_statuses()
|
||||
result['data'] = [
|
||||
o for o in result.get('data', [])
|
||||
if o.get('status') not in hidden_statuses
|
||||
_redact_order_for_restricted(o) for o in result.get('data', [])
|
||||
if o.get('status') in visible
|
||||
]
|
||||
return jsonify(result)
|
||||
finally:
|
||||
@@ -204,7 +205,7 @@ def create_order_from_pos():
|
||||
|
||||
|
||||
@service_order_bp.route('', methods=['POST'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('create_order')
|
||||
def create_order():
|
||||
data = request.get_json() or {}
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
@@ -244,28 +245,22 @@ def get_order(so_id):
|
||||
order = get_service_order(conn, so_id)
|
||||
if not order:
|
||||
return jsonify({'error': 'Service order not found'}), 404
|
||||
# Workshop/mechanic accounts cannot view delivery/logistics (and mechanic also commercial) statuses.
|
||||
if g.employee_role == 'mechanic' and order.get('status') in _MECHANIC_HIDDEN_STATUSES:
|
||||
if order.get('status') not in _visible_statuses():
|
||||
return jsonify({'error': 'No tienes acceso a esta orden'}), 403
|
||||
if g.employee_role == 'workshop' and order.get('status') in _WORKSHOP_HIDDEN_STATUSES:
|
||||
return jsonify({'error': 'No tienes acceso a esta orden'}), 403
|
||||
return jsonify(_redact_order_for_mechanic(order))
|
||||
return jsonify(_redact_order_for_restricted(order))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@service_order_bp.route('/<int:so_id>', methods=['PUT'])
|
||||
@require_workshop_view
|
||||
@require_workshop_action('edit_order')
|
||||
def update_order(so_id):
|
||||
data = request.get_json() or {}
|
||||
# Full edit remains for owner/admin/counter/cashier.
|
||||
# Mechanics can only update diagnosis_notes, repair_notes and the free-text mechanic_name.
|
||||
if g.employee_role == 'mechanic':
|
||||
allowed = {'diagnosis_notes', 'repair_notes', 'mechanic_name'}
|
||||
if not data or any(k not in allowed for k in data.keys()):
|
||||
return jsonify({'error': 'Solo puedes editar notas de diagnostico, reparacion y asignar mecanico'}), 403
|
||||
elif not _can_edit_workshop():
|
||||
return jsonify({'error': 'Missing permissions: workshop.edit'}), 403
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
ok = update_service_order(conn, so_id, data)
|
||||
@@ -277,7 +272,7 @@ def update_order(so_id):
|
||||
|
||||
|
||||
@service_order_bp.route('/<int:so_id>', methods=['DELETE'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('delete_order')
|
||||
def delete_order(so_id):
|
||||
"""Soft-delete a service order. Restricted to owner/admin."""
|
||||
if g.employee_role not in ('owner', 'admin'):
|
||||
@@ -294,16 +289,13 @@ def delete_order(so_id):
|
||||
|
||||
|
||||
@service_order_bp.route('/<int:so_id>/status', methods=['PUT'])
|
||||
@require_workshop_view
|
||||
@require_workshop_action('change_status')
|
||||
def change_status(so_id):
|
||||
if not (_can_edit_workshop() or g.employee_role == 'mechanic'):
|
||||
return jsonify({'error': 'Missing permissions: workshop.edit'}), 403
|
||||
data = request.get_json() or {}
|
||||
new_status = data.get('status')
|
||||
if not new_status:
|
||||
return jsonify({'error': 'status is required'}), 400
|
||||
# Mechanics cannot move orders into commercial/closed statuses.
|
||||
if g.employee_role == 'mechanic' and new_status in _MECHANIC_HIDDEN_STATUSES:
|
||||
if new_status not in _visible_statuses():
|
||||
return jsonify({'error': 'No puedes cambiar a este estatus'}), 403
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
@@ -322,7 +314,7 @@ def change_status(so_id):
|
||||
# ─── Items (Parts) ─────────────────────────────
|
||||
|
||||
@service_order_bp.route('/<int:so_id>/items', methods=['POST'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('add_items')
|
||||
def add_order_item(so_id):
|
||||
data = request.get_json() or {}
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
@@ -334,7 +326,7 @@ def add_order_item(so_id):
|
||||
|
||||
|
||||
@service_order_bp.route('/items/<int:item_id>', methods=['PUT'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('add_items')
|
||||
def update_order_item(item_id):
|
||||
data = request.get_json() or {}
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
@@ -348,7 +340,7 @@ def update_order_item(item_id):
|
||||
|
||||
|
||||
@service_order_bp.route('/items/<int:item_id>', methods=['DELETE'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('add_items')
|
||||
def delete_order_item(item_id):
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
@@ -361,7 +353,7 @@ def delete_order_item(item_id):
|
||||
# ─── Labor ─────────────────────────────
|
||||
|
||||
@service_order_bp.route('/<int:so_id>/labor', methods=['POST'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('add_labor')
|
||||
def add_order_labor(so_id):
|
||||
data = request.get_json() or {}
|
||||
if not data.get('description'):
|
||||
@@ -375,7 +367,7 @@ def add_order_labor(so_id):
|
||||
|
||||
|
||||
@service_order_bp.route('/labor/<int:labor_id>', methods=['PUT'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('add_labor')
|
||||
def update_order_labor(labor_id):
|
||||
data = request.get_json() or {}
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
@@ -389,7 +381,7 @@ def update_order_labor(labor_id):
|
||||
|
||||
|
||||
@service_order_bp.route('/labor/<int:labor_id>', methods=['DELETE'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('add_labor')
|
||||
def delete_order_labor(labor_id):
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
@@ -416,7 +408,7 @@ def kanban_summary():
|
||||
|
||||
|
||||
@service_order_bp.route('/<int:so_id>/items/<int:item_id>/reserve', methods=['POST'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('add_items')
|
||||
def reserve_order_item(so_id, item_id):
|
||||
"""Reserve inventory for a service order item."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
@@ -430,7 +422,7 @@ def reserve_order_item(so_id, item_id):
|
||||
|
||||
|
||||
@service_order_bp.route('/<int:so_id>/items/<int:item_id>/release', methods=['POST'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('add_items')
|
||||
def release_order_item(so_id, item_id):
|
||||
"""Release a previous inventory reservation."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
@@ -448,6 +440,7 @@ def release_order_item(so_id, item_id):
|
||||
|
||||
@service_order_bp.route('/<int:so_id>/convert-to-sale', methods=['POST'])
|
||||
@require_auth('pos.sell')
|
||||
@require_workshop_action('convert_to_sale')
|
||||
def convert_order_to_sale(so_id):
|
||||
"""Convert a service order into a POS sale.
|
||||
|
||||
@@ -487,6 +480,7 @@ def convert_order_to_sale(so_id):
|
||||
|
||||
@service_order_bp.route('/<int:so_id>/convert-to-remission', methods=['POST'])
|
||||
@require_auth('pos.remission')
|
||||
@require_workshop_action('convert_to_remission')
|
||||
def convert_order_to_remission(so_id):
|
||||
"""Convert a service order into a counter remission note (pending payment).
|
||||
|
||||
@@ -517,7 +511,7 @@ def convert_order_to_remission(so_id):
|
||||
|
||||
|
||||
@service_order_bp.route('/<int:so_id>/assign-mechanic', methods=['PUT'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('assign_mechanic')
|
||||
def assign_mechanic_endpoint(so_id):
|
||||
"""Assign a mechanic/technician to a service order."""
|
||||
data = request.get_json() or {}
|
||||
@@ -536,7 +530,7 @@ def assign_mechanic_endpoint(so_id):
|
||||
|
||||
|
||||
@service_order_bp.route('/customers', methods=['POST'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('edit_order')
|
||||
def create_customer_for_workshop():
|
||||
"""Create a customer directly from the workshop flow."""
|
||||
data = request.get_json() or {}
|
||||
@@ -570,7 +564,7 @@ def create_customer_for_workshop():
|
||||
|
||||
|
||||
@service_order_bp.route('/vehicles', methods=['POST'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('edit_order')
|
||||
def create_vehicle_for_workshop():
|
||||
"""Create a fleet vehicle directly from the workshop flow.
|
||||
|
||||
@@ -685,7 +679,7 @@ def list_catalog():
|
||||
|
||||
|
||||
@service_order_bp.route('/service-catalog', methods=['POST'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('edit_order')
|
||||
def create_catalog_item():
|
||||
"""Create a reusable labor concept."""
|
||||
data = request.get_json() or {}
|
||||
@@ -701,7 +695,7 @@ def create_catalog_item():
|
||||
|
||||
|
||||
@service_order_bp.route('/service-catalog/<int:item_id>', methods=['PUT'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('edit_order')
|
||||
def update_catalog_item(item_id):
|
||||
"""Update a reusable labor concept."""
|
||||
data = request.get_json() or {}
|
||||
@@ -716,7 +710,7 @@ def update_catalog_item(item_id):
|
||||
|
||||
|
||||
@service_order_bp.route('/service-catalog/<int:item_id>', methods=['DELETE'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('edit_order')
|
||||
def delete_catalog_item(item_id):
|
||||
"""Soft-delete a reusable labor concept."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
@@ -731,7 +725,7 @@ def delete_catalog_item(item_id):
|
||||
|
||||
|
||||
@service_order_bp.route('/<int:so_id>/print', methods=['POST'])
|
||||
@require_workshop_edit
|
||||
@require_workshop_action('edit_order')
|
||||
def print_service_order_ticket(so_id):
|
||||
"""Generate a printable ticket for a service order.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user