fase 2.1: roles editables (modelo skeen.role) y permisos por accion (43 claves, 9 grupos) con enforcement en backend y gating en frontend; editor de roles/permisos/menus en Configuracion
This commit is contained in:
@@ -9,9 +9,12 @@ import re
|
||||
import uuid
|
||||
from datetime import date, datetime, timedelta
|
||||
from .auth import sign_token, current_user, require_role, forbid_roles, verify_token
|
||||
from .auth import require_agenda, agenda_perm_error
|
||||
from .auth import require_agenda, agenda_perm_error, require_perm
|
||||
from ..roles import ROLE_SELECTION, ROLE_KEYS, MENU_ROLES_KEY, get_menu_roles
|
||||
from ..roles import AGENDA_PERM_KEYS
|
||||
from ..roles import AGENDA_PERM_KEYS, role_keys, role_selection
|
||||
from ..permissions import (PERMISSION_GROUPS, PERM_KEYS, PERM_LABELS,
|
||||
ROLE_PERMS_KEY, has_perm, all_roles_perms,
|
||||
default_role_perms, resolve_role_perms, set_role_perms)
|
||||
from odoo.addons.skeen_citas.clinic_settings import (
|
||||
agenda_bounds as clinic_agenda_bounds,
|
||||
get_settings as get_clinic_settings,
|
||||
@@ -278,6 +281,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/auth/users', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('config.usuarios')
|
||||
def auth_users_list(self, **kw):
|
||||
try:
|
||||
if not self._puede_gestionar_usuarios():
|
||||
@@ -289,6 +293,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/auth/users', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('config.usuarios')
|
||||
def auth_users_create(self, **kw):
|
||||
try:
|
||||
if not self._puede_gestionar_usuarios():
|
||||
@@ -302,7 +307,7 @@ class SkeenFrontendController(http.Controller):
|
||||
return json_response({'status': 'error', 'message': 'login y name son obligatorios'}, 400)
|
||||
if len(password) < 8:
|
||||
return json_response({'status': 'error', 'message': 'La contraseña debe tener al menos 8 caracteres'}, 400)
|
||||
if role not in ROLE_KEYS:
|
||||
if role not in role_keys(request.env):
|
||||
return json_response({'status': 'error', 'message': 'Rol inválido'}, 400)
|
||||
if role == 'admin' and not self._puede_asignar_admin():
|
||||
return json_response({'status': 'error', 'message': 'Solo un administrador puede crear administradores'}, 403)
|
||||
@@ -319,6 +324,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/auth/users/<int:user_id>', type='http', auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('config.usuarios')
|
||||
def auth_users_update(self, user_id, **kw):
|
||||
try:
|
||||
if not self._puede_gestionar_usuarios():
|
||||
@@ -348,7 +354,7 @@ class SkeenFrontendController(http.Controller):
|
||||
return json_response({'status': 'error', 'message': 'Ese usuario ya existe'}, 409)
|
||||
vals['login'] = login
|
||||
if 'role' in data:
|
||||
if data['role'] not in ROLE_KEYS:
|
||||
if data['role'] not in role_keys(request.env):
|
||||
return json_response({'status': 'error', 'message': 'Rol inválido'}, 400)
|
||||
if not self._puede_gestionar_roles():
|
||||
return json_response({'status': 'error', 'message': 'Sin permiso para cambiar el rol'}, 403)
|
||||
@@ -393,6 +399,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/auth/users/<int:user_id>', type='http', auth='none', methods=['DELETE'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('config.usuarios')
|
||||
def auth_users_delete(self, user_id, **kw):
|
||||
try:
|
||||
if not self._puede_gestionar_usuarios():
|
||||
@@ -420,13 +427,18 @@ class SkeenFrontendController(http.Controller):
|
||||
return json_response({
|
||||
'status': 'success',
|
||||
'matrix': get_menu_roles(request.env),
|
||||
'roles': [{'key': k, 'label': l} for k, l in ROLE_SELECTION],
|
||||
'roles': [{'key': r.code, 'label': r.name, 'active': r.active, 'rank': r.rank}
|
||||
for r in request.env['skeen.role'].sudo().with_context(
|
||||
active_test=False).search([], order='sequence, name')] or
|
||||
[{'key': k, 'label': l, 'active': True, 'rank': 0}
|
||||
for k, l in ROLE_SELECTION],
|
||||
})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/roles/permissions', type='http', auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('config.roles')
|
||||
def roles_permissions_put(self, **kw):
|
||||
try:
|
||||
if not self._puede_gestionar_roles():
|
||||
@@ -435,11 +447,12 @@ class SkeenFrontendController(http.Controller):
|
||||
matrix = data.get('matrix')
|
||||
if not isinstance(matrix, dict):
|
||||
return json_response({'status': 'error', 'message': 'matrix debe ser un objeto {href: [roles]}'}, 400)
|
||||
valid_roles = role_keys(request.env)
|
||||
clean = {}
|
||||
for href, roles in matrix.items():
|
||||
if not isinstance(href, str):
|
||||
return json_response({'status': 'error', 'message': 'Las llaves de matrix deben ser strings (href)'}, 400)
|
||||
if not isinstance(roles, list) or any(not isinstance(r, str) or r not in ROLE_KEYS for r in roles):
|
||||
if not isinstance(roles, list) or any(not isinstance(r, str) or r not in valid_roles for r in roles):
|
||||
return json_response({'status': 'error', 'message': f'Roles inválidos para {href}'}, 400)
|
||||
clean[href] = list(dict.fromkeys(roles))
|
||||
if 'admin' not in clean.get('/configuracion', []):
|
||||
@@ -455,6 +468,264 @@ class SkeenFrontendController(http.Controller):
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
# ============================================================
|
||||
# Roles configurables (skeen.role)
|
||||
# ============================================================
|
||||
# 'admin' y 'lectura' no se pueden desactivar: sin admin nadie administra y
|
||||
# sin lectura no queda el rol base de solo consulta.
|
||||
ROLES_PROTEGIDOS = ('admin', 'lectura')
|
||||
|
||||
def _role_to_dict(self, r):
|
||||
return {
|
||||
'id': r.id,
|
||||
'code': r.code,
|
||||
'name': r.name,
|
||||
'rank': r.rank,
|
||||
'sequence': r.sequence,
|
||||
'active': r.active,
|
||||
'is_system': r.is_system,
|
||||
'description': r.description or '',
|
||||
}
|
||||
|
||||
@http.route('/skeen/frontend/v1/roles', type='http', auth='none',
|
||||
methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('lectura')
|
||||
def list_roles(self, **kw):
|
||||
"""Roles configurados. ?include_inactive=1 trae también los inactivos."""
|
||||
try:
|
||||
incluir = kw.get('include_inactive') in ('1', 'true', 'True')
|
||||
Role = request.env['skeen.role'].sudo()
|
||||
if incluir:
|
||||
Role = Role.with_context(active_test=False)
|
||||
recs = Role.search([], order='sequence, name')
|
||||
return json_response({'status': 'success',
|
||||
'roles': [self._role_to_dict(r) for r in recs],
|
||||
'total': len(recs)})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/roles', type='http', auth='none',
|
||||
methods=['POST'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('config.roles')
|
||||
def create_role(self, **kw):
|
||||
"""Crea un rol. El `code` se genera como slug único del nombre."""
|
||||
try:
|
||||
if not self._puede_gestionar_roles():
|
||||
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
|
||||
data = _parse_json_body()
|
||||
name = (data.get('name') or '').strip()
|
||||
if not name:
|
||||
return json_response({'status': 'error',
|
||||
'message': 'El nombre es obligatorio'}, 400)
|
||||
Role = request.env['skeen.role'].sudo()
|
||||
codigo = (data.get('code') or '').strip()
|
||||
if codigo:
|
||||
codigo = catalog_slugify(codigo) or codigo
|
||||
if Role.with_context(active_test=False).search_count([('code', '=', codigo)]):
|
||||
return json_response({
|
||||
'status': 'error',
|
||||
'message': 'Ya existe un rol con el código %s' % codigo}, 409)
|
||||
else:
|
||||
codigo = catalog_unique_slug(Role, name)
|
||||
try:
|
||||
rank = int(data.get('rank', 50))
|
||||
except (TypeError, ValueError):
|
||||
return json_response({'status': 'error',
|
||||
'message': 'El rango debe ser numérico'}, 400)
|
||||
try:
|
||||
sequence = int(data.get('sequence') or 10)
|
||||
except (TypeError, ValueError):
|
||||
sequence = 10
|
||||
rol = Role.create({
|
||||
'name': name,
|
||||
'code': codigo,
|
||||
'rank': rank,
|
||||
'sequence': sequence,
|
||||
'active': bool(data.get('active', True)),
|
||||
'description': data.get('description') or '',
|
||||
'is_system': False,
|
||||
})
|
||||
self._auditar('roles', 'role:%s' % rol.id, 'crear',
|
||||
new_value=json.dumps(self._role_to_dict(rol), ensure_ascii=False)[:500],
|
||||
entity_label='Rol %s' % rol.name,
|
||||
notes=(data.get('motivo') or 'creado').strip())
|
||||
return json_response({'status': 'success',
|
||||
'role': self._role_to_dict(rol)}, 201)
|
||||
except Exception as e:
|
||||
request.env.cr.rollback()
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/roles/<int:role_id>', type='http', auth='none',
|
||||
methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('config.roles')
|
||||
def update_role(self, role_id, **kw):
|
||||
"""Renombra / ajusta rango, secuencia, estado y descripción de un rol.
|
||||
|
||||
El `code` NUNCA se cambia: es el valor guardado en los usuarios.
|
||||
"""
|
||||
try:
|
||||
if not self._puede_gestionar_roles():
|
||||
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
|
||||
rol = request.env['skeen.role'].sudo().with_context(
|
||||
active_test=False).browse(role_id)
|
||||
if not rol.exists():
|
||||
return json_response({'status': 'error', 'message': 'Rol no encontrado'}, 404)
|
||||
data = _parse_json_body()
|
||||
vals = {}
|
||||
if 'name' in data and str(data.get('name') or '').strip():
|
||||
vals['name'] = str(data['name']).strip()
|
||||
if 'description' in data:
|
||||
vals['description'] = data.get('description') or ''
|
||||
if 'rank' in data:
|
||||
try:
|
||||
vals['rank'] = int(data['rank'])
|
||||
except (TypeError, ValueError):
|
||||
return json_response({'status': 'error',
|
||||
'message': 'El rango debe ser numérico'}, 400)
|
||||
if 'sequence' in data:
|
||||
try:
|
||||
vals['sequence'] = int(data['sequence'])
|
||||
except (TypeError, ValueError):
|
||||
return json_response({'status': 'error',
|
||||
'message': 'La secuencia debe ser numérica'}, 400)
|
||||
if 'active' in data:
|
||||
activo = bool(data['active'])
|
||||
if not activo and rol.code in self.ROLES_PROTEGIDOS:
|
||||
return json_response({
|
||||
'status': 'error',
|
||||
'message': 'El rol "%s" no se puede desactivar.' % rol.name}, 400)
|
||||
vals['active'] = activo
|
||||
if not vals:
|
||||
return json_response({'status': 'error',
|
||||
'message': 'Nada que actualizar'}, 400)
|
||||
rol.write(vals)
|
||||
self._auditar('roles', 'role:%s' % rol.id, 'editar',
|
||||
new_value=json.dumps(self._role_to_dict(rol), ensure_ascii=False)[:500],
|
||||
entity_label='Rol %s' % rol.name,
|
||||
notes=(data.get('motivo') or 'actualizado').strip())
|
||||
return json_response({'status': 'success',
|
||||
'role': self._role_to_dict(rol)})
|
||||
except Exception as e:
|
||||
request.env.cr.rollback()
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/roles/<int:role_id>', type='http', auth='none',
|
||||
methods=['DELETE'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('config.roles')
|
||||
def delete_role(self, role_id, **kw):
|
||||
"""Desactiva un rol (nunca se borra: los usuarios lo referencian).
|
||||
|
||||
Los roles de sistema no se pueden eliminar (400).
|
||||
"""
|
||||
try:
|
||||
if not self._puede_gestionar_roles():
|
||||
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
|
||||
rol = request.env['skeen.role'].sudo().with_context(
|
||||
active_test=False).browse(role_id)
|
||||
if not rol.exists():
|
||||
return json_response({'status': 'error', 'message': 'Rol no encontrado'}, 404)
|
||||
if rol.is_system:
|
||||
return json_response({
|
||||
'status': 'error',
|
||||
'message': ('"%s" es un rol del sistema: no se puede eliminar, '
|
||||
'solo desactivar (si aplica) o renombrar.' % rol.name)}, 400)
|
||||
if rol.code in self.ROLES_PROTEGIDOS:
|
||||
return json_response({
|
||||
'status': 'error',
|
||||
'message': 'El rol "%s" no se puede desactivar.' % rol.name}, 400)
|
||||
en_uso = request.env['skeen.frontend.user'].sudo().search_count(
|
||||
[('role', '=', rol.code)])
|
||||
rol.write({'active': False})
|
||||
self._auditar('roles', 'role:%s' % rol.id, 'desactivar',
|
||||
old_value=json.dumps(self._role_to_dict(rol), ensure_ascii=False)[:500],
|
||||
entity_label='Rol %s' % rol.name,
|
||||
notes='desactivado (%d usuarios)' % en_uso)
|
||||
mensaje = 'Rol desactivado.'
|
||||
if en_uso:
|
||||
mensaje = ('El rol tiene %d usuario(s); quedó desactivado y sus '
|
||||
'usuarios conservan el rol.' % en_uso)
|
||||
return json_response({'status': 'success',
|
||||
'role': self._role_to_dict(rol),
|
||||
'en_uso': en_uso, 'message': mensaje})
|
||||
except Exception as e:
|
||||
request.env.cr.rollback()
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
# ============================================================
|
||||
# Permisos por acción (catálogo + por rol)
|
||||
# ============================================================
|
||||
def _perm_catalogo(self):
|
||||
return [{'grupo': g, 'label': label,
|
||||
'permisos': [{'key': k, 'label': l} for k, l in perms]}
|
||||
for g, label, perms in PERMISSION_GROUPS]
|
||||
|
||||
@http.route('/skeen/frontend/v1/permisos', type='http', auth='none',
|
||||
methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('config.roles')
|
||||
def permisos_get(self, **kw):
|
||||
"""Catálogo de permisos + permisos vigentes por rol + defaults."""
|
||||
try:
|
||||
if not self._puede_gestionar_roles():
|
||||
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
|
||||
Role = request.env['skeen.role'].sudo().with_context(active_test=False)
|
||||
return json_response({
|
||||
'status': 'success',
|
||||
'catalogo': self._perm_catalogo(),
|
||||
'por_rol': all_roles_perms(request.env),
|
||||
'defaults': default_role_perms(),
|
||||
'roles': [{'code': r.code, 'name': r.name, 'rank': r.rank,
|
||||
'active': r.active, 'is_system': r.is_system}
|
||||
for r in Role.search([], order='sequence, name')],
|
||||
})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/permisos', type='http', auth='none',
|
||||
methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('config.roles')
|
||||
def permisos_put(self, **kw):
|
||||
"""Guarda permisos de uno o varios roles.
|
||||
|
||||
Body: {role_code, permisos:{clave:bool}} o {por_rol:{role_code:{...}}}.
|
||||
"""
|
||||
try:
|
||||
if not self._puede_gestionar_roles():
|
||||
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
|
||||
data = _parse_json_body()
|
||||
validos = role_keys(request.env)
|
||||
guardados = 0
|
||||
if isinstance(data.get('por_rol'), dict):
|
||||
por_rol = data['por_rol']
|
||||
elif data.get('role_code'):
|
||||
por_rol = {data['role_code']: data.get('permisos') or {}}
|
||||
else:
|
||||
return json_response({'status': 'error',
|
||||
'message': 'Se espera role_code+permisos o por_rol'}, 400)
|
||||
for code, perms in por_rol.items():
|
||||
if code not in validos:
|
||||
return json_response({'status': 'error',
|
||||
'message': 'Rol inválido: %s' % code}, 400)
|
||||
if not isinstance(perms, dict):
|
||||
return json_response({'status': 'error',
|
||||
'message': 'permisos de %s debe ser un objeto' % code}, 400)
|
||||
if set_role_perms(request.env, code, perms):
|
||||
guardados += 1
|
||||
self._auditar('roles', 'role_perms', 'permisos',
|
||||
new_value=json.dumps(por_rol, ensure_ascii=False)[:2000],
|
||||
entity_label='Permisos por rol',
|
||||
notes='%d rol(es)' % guardados)
|
||||
return json_response({'status': 'success',
|
||||
'por_rol': all_roles_perms(request.env),
|
||||
'guardados': guardados})
|
||||
except Exception as e:
|
||||
request.env.cr.rollback()
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
# ============================================================
|
||||
# Dashboard
|
||||
# ============================================================
|
||||
@@ -1932,6 +2203,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/patients/export', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('lectura')
|
||||
@require_perm('pacientes.exportar')
|
||||
def export_patients(self, **kw):
|
||||
"""Todos los pacientes activos que cumplen los filtros, SIN paginar.
|
||||
|
||||
@@ -1973,6 +2245,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/patients', type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('pacientes.crear')
|
||||
def create_patient(self, **kw):
|
||||
try:
|
||||
data = _parse_json_body()
|
||||
@@ -2048,6 +2321,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/patients/<int:patient_id>', type='http', auth='none', methods=['PUT'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('pacientes.editar')
|
||||
def update_patient(self, patient_id, **kw):
|
||||
try:
|
||||
patient = request.env['res.partner'].sudo().browse(patient_id)
|
||||
@@ -2642,6 +2916,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/patients/<int:patient_id>/deposit', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('ventas.cobrar')
|
||||
@forbid_roles('marketing')
|
||||
def patient_deposit(self, patient_id, **kw):
|
||||
"""Recarga de saldo a favor (anticipo): dinero a cuenta del paciente.
|
||||
@@ -2740,6 +3015,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/patients/<int:patient_id>/pay_adeudo', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('ventas.cobrar')
|
||||
@forbid_roles('marketing')
|
||||
def pay_patient_adeudo(self, patient_id, **kw):
|
||||
"""Registra un pago del adeudo (parcial o total) y devuelve el recibo.
|
||||
@@ -3450,7 +3726,8 @@ class SkeenFrontendController(http.Controller):
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/services', type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@require_role('admin')
|
||||
@require_role('gerencia')
|
||||
@require_perm('servicios.editar')
|
||||
def create_service(self, **kw):
|
||||
try:
|
||||
data = _parse_json_body()
|
||||
@@ -3492,7 +3769,8 @@ class SkeenFrontendController(http.Controller):
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/services/reorder', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('admin')
|
||||
@require_role('gerencia')
|
||||
@require_perm('servicios.editar')
|
||||
def reorder_services(self, **kw):
|
||||
"""Reordena el catálogo y renumera las secuencias (10, 20, 30...).
|
||||
|
||||
@@ -3533,7 +3811,8 @@ class SkeenFrontendController(http.Controller):
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/services/<int:service_id>', type='http', auth='none', methods=['PUT'], csrf=False)
|
||||
@require_role('admin')
|
||||
@require_role('gerencia')
|
||||
@require_perm('servicios.editar')
|
||||
def update_service(self, service_id, **kw):
|
||||
try:
|
||||
service = request.env['skeen.servicio'].sudo().browse(service_id)
|
||||
@@ -4345,6 +4624,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/payments', type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('ventas.cobrar')
|
||||
@forbid_roles('marketing')
|
||||
def create_payment(self, **kw):
|
||||
try:
|
||||
@@ -4362,6 +4642,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/payments/<int:payment_id>/confirm', type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('ventas.cobrar')
|
||||
@forbid_roles('marketing')
|
||||
def confirm_payment(self, payment_id, **kw):
|
||||
try:
|
||||
@@ -4530,6 +4811,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/sales/<int:sale_id>/pay', type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('ventas.cobrar')
|
||||
@forbid_roles('marketing')
|
||||
def pay_sale(self, sale_id, **kw):
|
||||
try:
|
||||
@@ -4568,6 +4850,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/sales/<int:sale_id>/refund', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('ventas.devolver')
|
||||
@forbid_roles('marketing')
|
||||
def refund_sale(self, sale_id, **kw):
|
||||
try:
|
||||
@@ -4584,6 +4867,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/sales/<int:sale_id>/cancel', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('ventas.cancelar')
|
||||
@forbid_roles('marketing')
|
||||
def cancel_sale(self, sale_id, **kw):
|
||||
"""Cancelación de venta con nota de crédito (motivo obligatorio).
|
||||
@@ -4747,6 +5031,7 @@ class SkeenFrontendController(http.Controller):
|
||||
# ============================================================
|
||||
@http.route('/skeen/frontend/v1/pos/checkout', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('ventas.vender')
|
||||
@forbid_roles('marketing')
|
||||
def pos_checkout(self, **kw):
|
||||
"""Crea la venta confirmada, aplica canje de puntos y/o registra el pago.
|
||||
@@ -4767,6 +5052,11 @@ class SkeenFrontendController(http.Controller):
|
||||
return json_response({'status': 'error', 'message': 'Paciente no encontrado'}, 404)
|
||||
|
||||
discount = float(data.get('discount', 0) or 0)
|
||||
if discount > 0 and not has_perm(request.frontend_user, 'ventas.descuentos'):
|
||||
return json_response({
|
||||
'status': 'error',
|
||||
'message': 'Sin permiso: %s' % PERM_LABELS.get('ventas.descuentos', 'Aplicar descuentos'),
|
||||
}, 403)
|
||||
line_vals = []
|
||||
item_lines = [] # líneas de artículo: para el move 'venta' tras confirmar
|
||||
paquete_lines = [] # (paquete_id, qty, price): materializan el saldo del paciente
|
||||
@@ -5016,6 +5306,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/cash_closings/<int:closing_id>/close', type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('caja.cerrar')
|
||||
@forbid_roles('marketing')
|
||||
def close_cash_closing(self, closing_id, **kw):
|
||||
try:
|
||||
@@ -6181,6 +6472,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/exchange_rate', type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@require_role('admin')
|
||||
@require_perm('config.editar')
|
||||
def set_exchange_rate(self, **kw):
|
||||
try:
|
||||
data = _parse_json_body()
|
||||
@@ -6238,6 +6530,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/sales_goal', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('admin')
|
||||
@require_perm('config.editar')
|
||||
def set_sales_goal(self, **kw):
|
||||
try:
|
||||
data = _parse_json_body()
|
||||
@@ -6262,6 +6555,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/config/commission_default', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('admin')
|
||||
@require_perm('config.editar')
|
||||
def set_commission_default(self, **kw):
|
||||
try:
|
||||
data = _parse_json_body()
|
||||
@@ -6285,6 +6579,7 @@ class SkeenFrontendController(http.Controller):
|
||||
# ============================================================
|
||||
@http.route('/skeen/frontend/v1/config/impact_simulation', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('admin')
|
||||
@require_perm('config.editar')
|
||||
def config_impact_simulation(self, **kw):
|
||||
"""Qué habría pasado con los valores configurados (o unos propuestos).
|
||||
|
||||
@@ -6381,6 +6676,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/config/audit', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('admin')
|
||||
@require_perm('config.auditoria')
|
||||
def config_audit_list(self, **kw):
|
||||
"""Bitácora de cambios de configuración (filtrable y paginada)."""
|
||||
try:
|
||||
@@ -6492,6 +6788,7 @@ class SkeenFrontendController(http.Controller):
|
||||
@http.route('/skeen/frontend/v1/brand', type='http',
|
||||
auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('admin')
|
||||
@require_perm('config.editar')
|
||||
def put_brand(self, **kw):
|
||||
"""Guarda el brand. Solo admin del sistema; queda en la bitácora."""
|
||||
try:
|
||||
@@ -6571,6 +6868,7 @@ class SkeenFrontendController(http.Controller):
|
||||
@http.route('/skeen/frontend/v1/glosario', type='http',
|
||||
auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('config.editar')
|
||||
def put_glosario(self, **kw):
|
||||
"""Guarda pack y overrides. Los overrides pisan al pack.
|
||||
|
||||
@@ -6632,6 +6930,7 @@ class SkeenFrontendController(http.Controller):
|
||||
@http.route('/skeen/frontend/v1/clinic-settings', type='http',
|
||||
auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('config.editar')
|
||||
def put_clinic_settings_api(self, **kw):
|
||||
"""Guarda los ajustes mergeando sobre lo actual.
|
||||
|
||||
@@ -6699,6 +6998,7 @@ class SkeenFrontendController(http.Controller):
|
||||
@http.route('/skeen/frontend/v1/sucursales', type='http',
|
||||
auth='none', methods=['POST'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('config.editar')
|
||||
def create_sucursal(self, **kw):
|
||||
"""Crea una sucursal. Sin `code` se genera un slug del nombre."""
|
||||
try:
|
||||
@@ -6750,6 +7050,7 @@ class SkeenFrontendController(http.Controller):
|
||||
@http.route('/skeen/frontend/v1/sucursales/<int:sucursal_id>', type='http',
|
||||
auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('config.editar')
|
||||
def update_sucursal(self, sucursal_id, **kw):
|
||||
try:
|
||||
if not self._puede_configurar_catalogo():
|
||||
@@ -6806,6 +7107,7 @@ class SkeenFrontendController(http.Controller):
|
||||
@http.route('/skeen/frontend/v1/sucursales/<int:sucursal_id>', type='http',
|
||||
auth='none', methods=['DELETE'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('config.editar')
|
||||
def delete_sucursal(self, sucursal_id, **kw):
|
||||
"""Desactiva la sucursal (nunca se borra: las citas la referencian)."""
|
||||
try:
|
||||
@@ -6868,6 +7170,7 @@ class SkeenFrontendController(http.Controller):
|
||||
@http.route('/skeen/frontend/v1/service-categories', type='http',
|
||||
auth='none', methods=['POST'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('config.editar')
|
||||
def create_service_category(self, **kw):
|
||||
"""Crea una categoría (las creadas por API nunca son de sistema)."""
|
||||
try:
|
||||
@@ -6914,6 +7217,7 @@ class SkeenFrontendController(http.Controller):
|
||||
@http.route('/skeen/frontend/v1/service-categories/<int:category_id>', type='http',
|
||||
auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('config.editar')
|
||||
def update_service_category(self, category_id, **kw):
|
||||
"""Edita una categoría. `is_system` no se puede cambiar."""
|
||||
try:
|
||||
@@ -6964,6 +7268,7 @@ class SkeenFrontendController(http.Controller):
|
||||
@http.route('/skeen/frontend/v1/service-categories/<int:category_id>', type='http',
|
||||
auth='none', methods=['DELETE'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('config.editar')
|
||||
def delete_service_category(self, category_id, **kw):
|
||||
"""Desactiva la categoría. Las de sistema no se pueden eliminar."""
|
||||
try:
|
||||
@@ -7018,6 +7323,7 @@ class SkeenFrontendController(http.Controller):
|
||||
@http.route('/skeen/frontend/v1/ticket_config', type='http',
|
||||
auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('admin')
|
||||
@require_perm('config.editar')
|
||||
def put_ticket_config(self, **kw):
|
||||
"""Guarda el diseño del ticket. Requiere motivo para la bitácora."""
|
||||
try:
|
||||
@@ -7048,6 +7354,7 @@ class SkeenFrontendController(http.Controller):
|
||||
@http.route('/skeen/frontend/v1/ticket_config/clinica', type='http',
|
||||
auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('admin')
|
||||
@require_perm('config.editar')
|
||||
def put_ticket_clinica(self, **kw):
|
||||
"""Perfil de la clínica que sale en el encabezado del ticket (y logo)."""
|
||||
try:
|
||||
@@ -7081,6 +7388,7 @@ class SkeenFrontendController(http.Controller):
|
||||
@http.route('/skeen/frontend/v1/ticket_config/reset', type='http',
|
||||
auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('admin')
|
||||
@require_perm('config.editar')
|
||||
def reset_ticket_config(self, **kw):
|
||||
"""Devuelve el diseño al formato original (el del ticket de siempre)."""
|
||||
try:
|
||||
@@ -7244,6 +7552,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/inventory/items', type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('inventario.mover')
|
||||
@forbid_roles('marketing')
|
||||
def inventory_create(self, **kw):
|
||||
try:
|
||||
@@ -7271,6 +7580,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/inventory/items/<int:item_id>', type='http', auth='none', methods=['PUT'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('inventario.ajustar')
|
||||
@forbid_roles('marketing')
|
||||
def inventory_update(self, item_id, **kw):
|
||||
try:
|
||||
@@ -7303,6 +7613,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/inventory/items/<int:item_id>/moves', type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('inventario.mover')
|
||||
@forbid_roles('marketing')
|
||||
def inventory_move_create(self, item_id, **kw):
|
||||
try:
|
||||
@@ -7366,6 +7677,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/inventario/compras', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('inventario.mover')
|
||||
@forbid_roles('marketing')
|
||||
def inventario_compra(self, **kw):
|
||||
"""Registrar compra de consumible (sube stock; actualiza costo si viene)"""
|
||||
@@ -7455,6 +7767,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/inventario/bajas', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('inventario.mover')
|
||||
@forbid_roles('marketing')
|
||||
def inventario_baja(self, **kw):
|
||||
"""Registrar baja/merma de consumible (baja stock)"""
|
||||
@@ -7532,6 +7845,7 @@ class SkeenFrontendController(http.Controller):
|
||||
|
||||
@http.route('/skeen/frontend/v1/inventario/conteos', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('inventario.ajustar')
|
||||
@forbid_roles('marketing')
|
||||
def create_conteo(self, **kw):
|
||||
"""Registra un conteo físico y aplica los ajustes por diferencia.
|
||||
|
||||
Reference in New Issue
Block a user