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:
148
odoo-addons/skeen_comisiones/models/liquidacion.py
Normal file
148
odoo-addons/skeen_comisiones/models/liquidacion.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Liquidación de comisiones por periodo.
|
||||
|
||||
Es el único lugar donde la comisión se PERSISTE: al generar el borrador se corre
|
||||
el motor y el desglose completo (por venta/línea/regla y metas alcanzadas) queda
|
||||
congelado en `detalle` (JSON). Así el recibo de un periodo ya cerrado no cambia
|
||||
aunque después se editen reglas, precios o devoluciones.
|
||||
|
||||
Estados: borrador -> pagada, o borrador -> cancelada. Una liquidación pagada o
|
||||
cancelada es inmutable (se protege el histórico de lo ya entregado).
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
ESTADOS = ('borrador', 'pagada', 'cancelada')
|
||||
ESTADO_LABEL = {'borrador': 'Borrador', 'pagada': 'Pagada', 'cancelada': 'Cancelada'}
|
||||
|
||||
|
||||
class SkeenComisionLiquidacion(models.Model):
|
||||
_name = 'skeen.comision.liquidacion'
|
||||
_description = 'Liquidación de comisiones SKEEN'
|
||||
_order = 'periodo_inicio desc, id desc'
|
||||
|
||||
name = fields.Char(string='Folio', readonly=True, copy=False, index=True)
|
||||
empleado_id = fields.Many2one('hr.employee', string='Empleado', required=True,
|
||||
ondelete='restrict', index=True)
|
||||
periodo_inicio = fields.Date(string='Periodo desde', required=True)
|
||||
periodo_fin = fields.Date(string='Periodo hasta', required=True)
|
||||
periodo = fields.Char(string='Periodo (AAAA-MM)', compute='_compute_periodo',
|
||||
store=True, index=True)
|
||||
|
||||
total_calculado = fields.Float(string='Comisión calculada', default=0.0)
|
||||
total_ajustes = fields.Float(string='Ajustes', compute='_compute_total',
|
||||
store=True)
|
||||
total = fields.Float(string='Total a pagar', compute='_compute_total', store=True)
|
||||
|
||||
estado = fields.Selection([(e, ESTADO_LABEL[e]) for e in ESTADOS],
|
||||
string='Estado', default='borrador', required=True,
|
||||
index=True)
|
||||
fecha_pago = fields.Date(string='Fecha de pago')
|
||||
metodo_pago = fields.Char(string='Método de pago')
|
||||
referencia = fields.Char(string='Referencia')
|
||||
notas = fields.Text(string='Notas')
|
||||
detalle = fields.Text(string='Desglose (JSON)')
|
||||
creado_por_login = fields.Char(string='Creada por')
|
||||
pagada_por_login = fields.Char(string='Pagada por')
|
||||
|
||||
ajuste_ids = fields.One2many('skeen.comision.ajuste', 'liquidacion_id',
|
||||
string='Ajustes')
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_empleado_periodo',
|
||||
'unique(empleado_id, periodo_inicio, periodo_fin)',
|
||||
'Ya existe una liquidación de este empleado para el mismo periodo.'),
|
||||
]
|
||||
|
||||
@api.depends('periodo_inicio')
|
||||
def _compute_periodo(self):
|
||||
for l in self:
|
||||
l.periodo = l.periodo_inicio.strftime('%Y-%m') if l.periodo_inicio else ''
|
||||
|
||||
@api.depends('ajuste_ids.monto', 'total_calculado')
|
||||
def _compute_total(self):
|
||||
for l in self:
|
||||
l.total_ajustes = sum(a.monto for a in l.ajuste_ids)
|
||||
l.total = (l.total_calculado or 0.0) + l.total_ajustes
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
seq = self.env['ir.sequence'].sudo().search(
|
||||
[('code', '=', 'skeen.comision.liquidacion')], limit=1)
|
||||
for vals in vals_list:
|
||||
if not vals.get('name'):
|
||||
vals['name'] = (seq.next_by_id() if seq else False) or 'LIQ-0001'
|
||||
return super().create(vals_list)
|
||||
|
||||
def ajustar(self, monto, motivo='', usuario=''):
|
||||
"""Agrega un ajuste manual con signo (+/-)."""
|
||||
self.ensure_one()
|
||||
if self.estado != 'borrador':
|
||||
raise UserError('Solo una liquidación en borrador admite ajustes.')
|
||||
try:
|
||||
monto = float(monto)
|
||||
except (TypeError, ValueError):
|
||||
raise UserError('El monto del ajuste debe ser un número.')
|
||||
if abs(monto) < 1e-9:
|
||||
raise UserError('El monto del ajuste no puede ser cero.')
|
||||
return self.env['skeen.comision.ajuste'].create({
|
||||
'liquidacion_id': self.id,
|
||||
'empleado_id': self.empleado_id.id,
|
||||
'periodo': self.periodo,
|
||||
'monto': monto,
|
||||
'motivo': (motivo or '').strip(),
|
||||
'usuario': usuario or '',
|
||||
'fecha': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
def pagar(self, fecha=None, metodo='', referencia='', usuario=''):
|
||||
self.ensure_one()
|
||||
if self.estado != 'borrador':
|
||||
raise UserError('Solo una liquidación en borrador se puede pagar.')
|
||||
self.write({
|
||||
'estado': 'pagada',
|
||||
'fecha_pago': fecha or fields.Date.today(),
|
||||
'metodo_pago': (metodo or '').strip(),
|
||||
'referencia': (referencia or '').strip(),
|
||||
'pagada_por_login': usuario or '',
|
||||
})
|
||||
return True
|
||||
|
||||
def cancelar(self, motivo=''):
|
||||
self.ensure_one()
|
||||
if self.estado == 'cancelada':
|
||||
raise UserError('La liquidación ya está cancelada.')
|
||||
if self.estado == 'pagada':
|
||||
raise UserError('Una liquidación pagada no se cancela; registra un ajuste.')
|
||||
texto = (self.notas or '').strip()
|
||||
self.write({'estado': 'cancelada',
|
||||
'notas': (texto + ('\n' if texto else '') +
|
||||
'Cancelada: ' + (motivo or 'sin motivo')).strip()})
|
||||
return True
|
||||
|
||||
def detalle_dict(self):
|
||||
self.ensure_one()
|
||||
try:
|
||||
return json.loads(self.detalle or '{}')
|
||||
except (ValueError, TypeError):
|
||||
return {}
|
||||
|
||||
|
||||
class SkeenComisionAjuste(models.Model):
|
||||
_name = 'skeen.comision.ajuste'
|
||||
_description = 'Ajuste manual de comisión'
|
||||
_order = 'fecha desc, id desc'
|
||||
|
||||
liquidacion_id = fields.Many2one('skeen.comision.liquidacion', string='Liquidación',
|
||||
ondelete='cascade', index=True)
|
||||
empleado_id = fields.Many2one('hr.employee', string='Empleado', required=True,
|
||||
ondelete='cascade', index=True)
|
||||
periodo = fields.Char(string='Periodo (AAAA-MM)')
|
||||
monto = fields.Float(string='Monto', default=0.0,
|
||||
help='Positivo suma a la comisión; negativo la reduce.')
|
||||
motivo = fields.Char(string='Motivo')
|
||||
usuario = fields.Char(string='Usuario')
|
||||
fecha = fields.Datetime(string='Fecha', default=fields.Datetime.now)
|
||||
Reference in New Issue
Block a user