fase 3.3: comisiones por reglas y liquidacion (modulo skeen_comisiones: motor con reglas por servicio/categoria/articulo/membresia/paquete, monto fijo y escalonado por metas, base vendido/cobrado con y sin impuestos, neto de devoluciones, metas con bonos, ajustes y liquidacion por periodo); reporte legacy intacto con el motor apagado
This commit is contained in:
703
odoo-addons/skeen_comisiones/controllers/comisiones.py
Normal file
703
odoo-addons/skeen_comisiones/controllers/comisiones.py
Normal file
@@ -0,0 +1,703 @@
|
||||
# -*- 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/<int:rid>', 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/<int:rid>', 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/<int:mid>', 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/<int:mid>', 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/<int:lid>', 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/<int:lid>/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/<int:lid>/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/<int:lid>/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)
|
||||
Reference in New Issue
Block a user