# -*- coding: utf-8 -*- """API del frontend React para Comisiones por reglas y liquidación. Vive en su propio controlador (no en `skeen_whatsapp/controllers/frontend.py`) para no chocar con otros trabajos. Las rutas cuelgan de `/skeen/frontend/v1/comisiones` y `/skeen/frontend/v1/reports/comisiones_reglas`, y exigen token (`@require_role`) más el permiso por acción (`@require_perm`). Dos familias de rutas: * CONFIGURACIÓN (reglas y metas): disponibles aunque `comisiones.activo=false`, porque la pestaña Configuración → Comisiones es la excepción que se muestra siempre (para poder dejar todo listo antes de encender). Exigen `comisiones.editar` (admin/gerencia). * OPERACIÓN (cálculo, reporte de reglas y liquidaciones): exigen `comisiones.activo=true`; con el módulo apagado responden 403 y no tocan nada. Así apagar el motor devuelve el sistema al estado anterior. OJO: las rutas operativas NO viven bajo `/reports/` para que el rol médico pueda ver sus propias liquidaciones (el veto de 'medico' bloquea `/reports/`). """ import functools import json from datetime import date, timedelta from odoo import fields, http from odoo.exceptions import UserError, ValidationError from odoo.http import request, Response from odoo.addons.skeen_whatsapp.controllers.auth import require_perm, require_role from odoo.addons.skeen_citas.clinic_settings import get_comisiones BASE = '/skeen/frontend/v1' COM = BASE + '/comisiones' METODOS_PAGO = ('efectivo', 'transferencia', 'tarjeta', 'otro') def _json(data, status=200): return Response( json.dumps(data, default=str), status=status, mimetype='application/json', headers={ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, ) def _body(): try: raw = request.httprequest.get_data(as_text=True) return json.loads(raw) if raw else {} except Exception: return {} def _cfg(): return get_comisiones(request.env) def _activo(): return bool(_cfg().get('activo')) def comisiones_activas(func): """Niega la operación si la instancia no tiene el módulo encendido.""" @functools.wraps(func) def wrapper(self, *args, **kw): if request.httprequest.method == 'OPTIONS': return _json({}, 200) if not _activo(): return _json({'status': 'error', 'message': 'Las comisiones por reglas no están activadas ' 'en esta clínica.'}, 403) return func(self, *args, **kw) return wrapper def _login(): return getattr(request.frontend_user, 'login', '') if hasattr(request, 'frontend_user') else '' def _num(v, d=None): try: return float(v) except (TypeError, ValueError): return d def _linked_employee(user): """hr.employee vinculado a un usuario frontend (por login).""" if user is None: return request.env['hr.employee'].sudo().browse() Emp = request.env['hr.employee'].sudo() ru = request.env['res.users'].sudo().search([('login', '=', user.login)], limit=1) emp = ru.employee_ids[:1] if ru else Emp.browse() if not emp: emp = Emp.search([('user_id', '=', ru.id)], limit=1) if ru else Emp.browse() return emp class SkeenComisionesController(http.Controller): # ================================================================== # Serializadores # ================================================================== def _regla_to_dict(self, r): return { 'id': r.id, 'name': r.name or '', 'activo': r.activo, 'prioridad': r.prioridad, 'empleado_id': r.empleado_id.id if r.empleado_id else None, 'empleado': r.empleado_id.name if r.empleado_id else '', 'staff_type': r.staff_type or '', 'alcance': r.alcance, 'alcance_label': dict(r._fields['alcance'].selection).get(r.alcance, r.alcance), 'categoria_code': r.categoria_code or '', 'servicio_id': r.servicio_id.id if r.servicio_id else None, 'servicio': r.servicio_id.name if r.servicio_id else '', 'item_id': r.item_id.id if r.item_id else None, 'item': r.item_id.name if r.item_id else '', 'plan_id': r.plan_id.id if r.plan_id else None, 'plan': r.plan_id.name if r.plan_id else '', 'tipo': r.tipo, 'tipo_label': dict(r._fields['tipo'].selection).get(r.tipo, r.tipo), 'valor': r.valor, 'escalones': r.escalones_ordenados(), 'base_override': r.base_override or '', 'fecha_desde': r.fecha_desde.strftime('%Y-%m-%d') if r.fecha_desde else None, 'fecha_hasta': r.fecha_hasta.strftime('%Y-%m-%d') if r.fecha_hasta else None, 'notas': r.notas or '', 'resumen': r._etiqueta_corta(), } def _meta_to_dict(self, m): return { 'id': m.id, 'empleado_id': m.empleado_id.id, 'empleado': m.empleado_id.name or '', 'periodo': m.periodo or '', 'monto_objetivo': m.monto_objetivo, 'bono': m.bono, 'tipo_bono': m.tipo_bono, 'activo': m.activo, 'notas': m.notas or '', } def _ajuste_to_dict(self, a): return { 'id': a.id, 'liquidacion_id': a.liquidacion_id.id if a.liquidacion_id else None, 'empleado_id': a.empleado_id.id, 'empleado': a.empleado_id.name or '', 'periodo': a.periodo or '', 'monto': a.monto, 'motivo': a.motivo or '', 'usuario': a.usuario or '', 'fecha': a.fecha.strftime('%Y-%m-%d %H:%M') if a.fecha else None, } def _liq_to_dict(self, l, with_detalle=False): d = { 'id': l.id, 'folio': l.name or '', 'empleado_id': l.empleado_id.id, 'empleado': l.empleado_id.name or '', 'staff_type': l.empleado_id.staff_type or 'doctor', 'periodo_inicio': l.periodo_inicio.strftime('%Y-%m-%d') if l.periodo_inicio else None, 'periodo_fin': l.periodo_fin.strftime('%Y-%m-%d') if l.periodo_fin else None, 'periodo': l.periodo or '', 'total_calculado': l.total_calculado, 'total_ajustes': l.total_ajustes, 'total': l.total, 'estado': l.estado, 'estado_label': dict(l._fields['estado'].selection).get(l.estado, l.estado), 'fecha_pago': l.fecha_pago.strftime('%Y-%m-%d') if l.fecha_pago else None, 'metodo_pago': l.metodo_pago or '', 'referencia': l.referencia or '', 'notas': l.notas or '', 'creado_por_login': l.creado_por_login or '', 'pagada_por_login': l.pagada_por_login or '', 'ajustes': [self._ajuste_to_dict(a) for a in l.ajuste_ids], } if with_detalle: d['detalle'] = l.detalle_dict() return d def _rango(self, kw, defecto_dias=30): start = fields.Date.to_date(kw.get('start')) or (fields.Date.today() - timedelta(days=defecto_dias)) end = fields.Date.to_date(kw.get('end')) or fields.Date.today() if end < start: start, end = end, start return start, end def _solo_propio(self, user): """True si el usuario debe ver solo sus propias comisiones (rol médico).""" return bool(user and user.role == 'medico') # ================================================================== # CONFIGURACIÓN: reglas (disponibles aunque el motor esté apagado) # ================================================================== @http.route(COM + '/reglas', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.editar') def list_reglas(self, **kw): try: domain = [] if kw.get('empleado_id'): try: domain = ['|', ('empleado_id', '=', int(kw['empleado_id'])), ('empleado_id', '=', False)] except (TypeError, ValueError): return _json({'status': 'error', 'message': 'empleado_id inválido'}, 400) if kw.get('alcance'): domain.append(('alcance', '=', kw['alcance'])) if kw.get('activo') in ('1', 'true', 'True'): domain.append(('activo', '=', True)) if kw.get('q'): domain.append('|') domain.append(('name', 'ilike', kw['q'])) domain.append(('categoria_code', 'ilike', kw['q'])) reglas = request.env['skeen.comision.regla'].sudo().search(domain) return _json({'status': 'success', 'reglas': [self._regla_to_dict(r) for r in reglas], 'total': len(reglas)}) except Exception as e: return _json({'status': 'error', 'message': str(e)}, 500) @http.route(COM + '/reglas', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.editar') def create_regla(self, **kw): try: data = _body() if not (data.get('name') or '').strip(): return _json({'status': 'error', 'message': 'El nombre es obligatorio'}, 400) vals = self._regla_vals(data) regla = request.env['skeen.comision.regla'].sudo().create(vals) return _json({'status': 'success', 'regla': self._regla_to_dict(regla)}, 201) except (ValidationError, UserError) as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 400) except Exception as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 500) @http.route(COM + '/reglas/', type='http', auth='none', methods=['PUT', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.editar') def update_regla(self, rid, **kw): try: regla = request.env['skeen.comision.regla'].sudo().browse(rid) if not regla.exists(): return _json({'status': 'error', 'message': 'Regla no encontrada'}, 404) regla.write(self._regla_vals(_body(), parcial=True)) return _json({'status': 'success', 'regla': self._regla_to_dict(regla)}) except (ValidationError, UserError) as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 400) except Exception as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 500) @http.route(COM + '/reglas/', type='http', auth='none', methods=['DELETE', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.editar') def delete_regla(self, rid, **kw): try: regla = request.env['skeen.comision.regla'].sudo().browse(rid) if not regla.exists(): return _json({'status': 'error', 'message': 'Regla no encontrada'}, 404) regla.write({'activo': False}) return _json({'status': 'success', 'message': 'Regla desactivada', 'regla': self._regla_to_dict(regla)}) except Exception as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 500) def _regla_vals(self, data, parcial=False): campos = ('name', 'activo', 'prioridad', 'empleado_id', 'staff_type', 'alcance', 'categoria_code', 'servicio_id', 'item_id', 'plan_id', 'tipo', 'valor', 'base_override', 'fecha_desde', 'fecha_hasta', 'notas') vals = {} for k in campos: if k not in data: continue v = data[k] if k in ('empleado_id', 'servicio_id', 'item_id', 'plan_id'): vals[k] = int(v) if v else False elif k in ('fecha_desde', 'fecha_hasta'): vals[k] = fields.Date.to_date(v) if v else False elif v is None: vals[k] = False if k in ('base_override', 'staff_type') else v else: vals[k] = v if 'escalones' in data: esc = data['escalones'] vals['escalones'] = json.dumps(esc, ensure_ascii=False) if esc else '' if not parcial and 'alcance' not in vals: vals['alcance'] = 'global' return vals # ================================================================== # CONFIGURACIÓN: metas # ================================================================== @http.route(COM + '/metas', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.editar') def list_metas(self, **kw): try: domain = [] if kw.get('empleado_id'): domain.append(('empleado_id', '=', int(kw['empleado_id']))) if kw.get('activo') in ('1', 'true', 'True'): domain.append(('activo', '=', True)) metas = request.env['skeen.comision.meta'].sudo().search(domain) return _json({'status': 'success', 'metas': [self._meta_to_dict(m) for m in metas], 'total': len(metas)}) except Exception as e: return _json({'status': 'error', 'message': str(e)}, 500) @http.route(COM + '/metas', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.editar') def create_meta(self, **kw): try: data = _body() vals = self._meta_vals(data) if not vals.get('empleado_id') or not vals.get('periodo'): return _json({'status': 'error', 'message': 'empleado_id y periodo son obligatorios'}, 400) meta = request.env['skeen.comision.meta'].sudo().create(vals) return _json({'status': 'success', 'meta': self._meta_to_dict(meta)}, 201) except (ValidationError, UserError) as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 400) except Exception as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 500) @http.route(COM + '/metas/', type='http', auth='none', methods=['PUT', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.editar') def update_meta(self, mid, **kw): try: meta = request.env['skeen.comision.meta'].sudo().browse(mid) if not meta.exists(): return _json({'status': 'error', 'message': 'Meta no encontrada'}, 404) meta.write(self._meta_vals(_body())) return _json({'status': 'success', 'meta': self._meta_to_dict(meta)}) except (ValidationError, UserError) as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 400) except Exception as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 500) @http.route(COM + '/metas/', type='http', auth='none', methods=['DELETE', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.editar') def delete_meta(self, mid, **kw): try: meta = request.env['skeen.comision.meta'].sudo().browse(mid) if not meta.exists(): return _json({'status': 'error', 'message': 'Meta no encontrada'}, 404) meta.write({'activo': False}) return _json({'status': 'success', 'message': 'Meta desactivada'}) except Exception as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 500) def _meta_vals(self, data): campos = ('empleado_id', 'periodo', 'monto_objetivo', 'bono', 'tipo_bono', 'activo', 'notas') vals = {} for k in campos: if k not in data: continue v = data[k] if k == 'empleado_id': vals[k] = int(v) if v else False elif k in ('monto_objetivo', 'bono'): n = _num(v) vals[k] = n if n is not None else 0.0 elif k == 'periodo': vals[k] = str(v or '').strip() else: vals[k] = v return vals @http.route(COM + '/empleados', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.ver') def list_empleados(self, **kw): """Empleados con su commission_pct de respaldo, reglas y metas activas.""" try: Emp = request.env['hr.employee'].sudo() emps = Emp.search([('active', '=', True)], order='name') Regla = request.env['skeen.comision.regla'].sudo() Meta = request.env['skeen.comision.meta'].sudo() data = [] for e in emps: reglas = Regla.search_count([ ('activo', '=', True), '|', ('empleado_id', '=', e.id), '&', ('empleado_id', '=', False), '|', ('staff_type', '=', False), ('staff_type', '=', e.staff_type or 'doctor'), ]) metas = Meta.search_count([('activo', '=', True), ('empleado_id', '=', e.id)]) data.append({ 'id': e.id, 'name': e.name, 'staff_type': e.staff_type or 'doctor', 'job_title': e.job_title or '', 'commission_pct': e.commission_pct or 0.0, 'reglas_count': reglas, 'metas_count': metas, }) return _json({'status': 'success', 'empleados': data}) except Exception as e: return _json({'status': 'error', 'message': str(e)}, 500) # ================================================================== # OPERACIÓN: cálculo en vivo # ================================================================== @http.route(COM + '/calculo', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.ver') @comisiones_activas def calculo(self, **kw): try: start, end = self._rango(kw) user = request.frontend_user emp_ids = None if self._solo_propio(user): emp = _linked_employee(user) if not emp: return _json({'status': 'success', 'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d'), 'empleados': [], 'totales': {'base': 0, 'comision': 0, 'bonos': 0, 'ajustes': 0, 'total': 0}, 'medico_sin_employee': True}) emp_ids = [emp.id] elif kw.get('empleado_id'): emp_ids = [int(kw['empleado_id'])] motor = request.env['skeen.comision.motor'].sudo() data = motor.calcular(start, end, empleado_ids=emp_ids) data['status'] = 'success' return _json(data) except Exception as e: return _json({'status': 'error', 'message': str(e)}, 500) @http.route(BASE + '/reports/comisiones_reglas', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.ver') @comisiones_activas def reporte_reglas(self, **kw): """Reporte por empleado, por regla, por servicio/categoría + metas.""" try: start, end = self._rango(kw) motor = request.env['skeen.comision.motor'].sudo() data = motor.calcular(start, end) por_servicio = {} por_categoria = {} for emp in data['empleados']: for d in emp['desglose']: svc = d['servicio'] or 'Sin servicio' por_servicio.setdefault(svc, {'servicio': svc, 'base': 0.0, 'comision': 0.0, 'lineas': 0}) por_servicio[svc]['base'] += d['base'] por_servicio[svc]['comision'] += d['comision'] por_servicio[svc]['lineas'] += 1 cat = d['categoria'] or 'Sin categoría' por_categoria.setdefault(cat, {'categoria': cat, 'base': 0.0, 'comision': 0.0, 'lineas': 0}) por_categoria[cat]['base'] += d['base'] por_categoria[cat]['comision'] += d['comision'] por_categoria[cat]['lineas'] += 1 return _json({ 'status': 'success', 'start': data['start'], 'end': data['end'], 'config': data['config'], 'empleados': data['empleados'], 'por_servicio': sorted(por_servicio.values(), key=lambda x: -x['comision']), 'por_categoria': sorted(por_categoria.values(), key=lambda x: -x['comision']), 'totales': data['totales'], }) except Exception as e: return _json({'status': 'error', 'message': str(e)}, 500) # ================================================================== # OPERACIÓN: liquidaciones # ================================================================== @http.route(COM + '/liquidaciones', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.ver') @comisiones_activas def list_liquidaciones(self, **kw): try: user = request.frontend_user domain = [] if self._solo_propio(user): emp = _linked_employee(user) domain.append(('empleado_id', '=', emp.id if emp else 0)) # El médico ve solo sus liquidaciones PAGADAS (recibo). El # borrador es información interna de administración. domain.append(('estado', '=', 'pagada')) else: if kw.get('empleado_id'): domain.append(('empleado_id', '=', int(kw['empleado_id']))) if kw.get('estado'): domain.append(('estado', '=', kw['estado'])) if kw.get('periodo'): domain.append(('periodo', '=', kw['periodo'])) liqs = request.env['skeen.comision.liquidacion'].sudo().search(domain) return _json({'status': 'success', 'liquidaciones': [self._liq_to_dict(l) for l in liqs], 'total': len(liqs)}) except Exception as e: return _json({'status': 'error', 'message': str(e)}, 500) @http.route(COM + '/liquidaciones/', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.ver') @comisiones_activas def get_liquidacion(self, lid, **kw): try: liq = request.env['skeen.comision.liquidacion'].sudo().browse(lid) if not liq.exists(): return _json({'status': 'error', 'message': 'Liquidación no encontrada'}, 404) user = request.frontend_user if self._solo_propio(user): emp = _linked_employee(user) if not emp or liq.empleado_id.id != emp.id or liq.estado != 'pagada': return _json({'status': 'error', 'message': 'Sin permiso'}, 403) return _json({'status': 'success', 'liquidacion': self._liq_to_dict(liq, True)}) except Exception as e: return _json({'status': 'error', 'message': str(e)}, 500) @http.route(COM + '/mis-liquidaciones', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.ver') @comisiones_activas def mis_liquidaciones(self, **kw): """Recibo propio del empleado vinculado (rol médico). Solo pagadas.""" try: emp = _linked_employee(request.frontend_user) if not emp: return _json({'status': 'success', 'liquidaciones': [], 'medico_sin_employee': True}) liqs = request.env['skeen.comision.liquidacion'].sudo().search([ ('empleado_id', '=', emp.id), ('estado', '=', 'pagada')]) return _json({'status': 'success', 'empleado_id': emp.id, 'empleado': emp.name, 'liquidaciones': [self._liq_to_dict(l, True) for l in liqs]}) except Exception as e: return _json({'status': 'error', 'message': str(e)}, 500) @http.route(COM + '/liquidaciones', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.liquidar') @comisiones_activas def generar_liquidaciones(self, **kw): """Genera borradores del periodo para uno, varios o todos los empleados.""" try: data = _body() start, end = self._rango(data) Liq = request.env['skeen.comision.liquidacion'].sudo() motor = request.env['skeen.comision.motor'].sudo() emp_ids = None if data.get('empleado_ids'): emp_ids = [int(x) for x in data['empleado_ids']] elif data.get('empleado_id'): emp_ids = [int(data['empleado_id'])] calc = motor.calcular(start, end, empleado_ids=emp_ids) creadas, omitidas = [], [] for emp in calc['empleados']: if emp['lineas_count'] == 0 and abs(emp['total']) < 1e-9: continue existente = Liq.search([ ('empleado_id', '=', emp['empleado_id']), ('periodo_inicio', '=', start), ('periodo_fin', '=', end)], limit=1) if existente: omitidas.append({'empleado_id': emp['empleado_id'], 'empleado': emp['empleado'], 'folio': existente.name, 'motivo': 'ya existe liquidación del periodo'}) continue liq = Liq.create({ 'empleado_id': emp['empleado_id'], 'periodo_inicio': start, 'periodo_fin': end, 'total_calculado': emp['total'], 'detalle': json.dumps(emp, ensure_ascii=False), 'creado_por_login': _login(), 'notas': (data.get('notas') or '').strip(), }) creadas.append(self._liq_to_dict(liq, True)) return _json({'status': 'success', 'liquidaciones': creadas, 'omitidas': omitidas, 'total_creadas': len(creadas)}, 201) except (ValidationError, UserError) as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 400) except Exception as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 500) @http.route(COM + '/liquidaciones//ajustar', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.liquidar') @comisiones_activas def ajustar_liquidacion(self, lid, **kw): try: cfg = _cfg() if not cfg.get('permitir_ajustes_manuales', True): return _json({'status': 'error', 'message': 'Los ajustes manuales están desactivados'}, 400) liq = request.env['skeen.comision.liquidacion'].sudo().browse(lid) if not liq.exists(): return _json({'status': 'error', 'message': 'Liquidación no encontrada'}, 404) data = _body() monto = _num(data.get('monto')) if monto is None: return _json({'status': 'error', 'message': 'Monto inválido'}, 400) liq.ajustar(monto, motivo=data.get('motivo') or '', usuario=_login()) return _json({'status': 'success', 'liquidacion': self._liq_to_dict(liq, True)}) except (ValidationError, UserError) as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 400) except Exception as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 500) @http.route(COM + '/liquidaciones//pagar', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.liquidar') @comisiones_activas def pagar_liquidacion(self, lid, **kw): try: liq = request.env['skeen.comision.liquidacion'].sudo().browse(lid) if not liq.exists(): return _json({'status': 'error', 'message': 'Liquidación no encontrada'}, 404) data = _body() fecha = fields.Date.to_date(data.get('fecha')) or fields.Date.today() metodo = data.get('metodo_pago') or data.get('metodo') or '' if metodo and metodo not in METODOS_PAGO: return _json({'status': 'error', 'message': 'Método de pago inválido'}, 400) liq.pagar(fecha=fecha, metodo=metodo, referencia=data.get('referencia') or '', usuario=_login()) return _json({'status': 'success', 'liquidacion': self._liq_to_dict(liq, True)}) except (ValidationError, UserError) as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 400) except Exception as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 500) @http.route(COM + '/liquidaciones//cancelar', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False) @require_role('lectura') @require_perm('comisiones.liquidar') @comisiones_activas def cancelar_liquidacion(self, lid, **kw): try: liq = request.env['skeen.comision.liquidacion'].sudo().browse(lid) if not liq.exists(): return _json({'status': 'error', 'message': 'Liquidación no encontrada'}, 404) data = _body() liq.cancelar(motivo=data.get('motivo') or '') return _json({'status': 'success', 'liquidacion': self._liq_to_dict(liq, True)}) except (ValidationError, UserError) as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 400) except Exception as e: request.env.cr.rollback() return _json({'status': 'error', 'message': str(e)}, 500)