# -*- 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)