Files

280 lines
12 KiB
Python

# -*- coding: utf-8 -*-
"""Paquetes de sesiones.
El catálogo de paquetes vive en `skeen.paquete` (un paquete = N sesiones de un
servicio a un precio cerrado). Cada vez que el POS vende uno se materializa una
`skeen.paquete.compra` a nombre del paciente, que es la que lleva el saldo:
sessions_total sesiones contratadas
sessions_used sesiones ya consumidas (citas atendidas)
-> restantes = sessions_total - sessions_used
El consumo lo hace `SkeenCitaPaquete` (cita_package.py) al pasar la cita por
'arrived'/'done', y lo revierte si la cita se cancela.
"""
from datetime import timedelta
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
# Estados de cita que ya gastaron una sesión del paquete
CONSUME_STATES = ('arrived', 'in_progress', 'done')
class SkeenPaquete(models.Model):
_name = 'skeen.paquete'
_description = 'Paquete de Sesiones'
_order = 'sequence, name'
name = fields.Char(string='Nombre', required=True)
code = fields.Char(string='Código')
service_id = fields.Many2one('skeen.servicio', string='Servicio', required=True,
help='Servicio cuyas sesiones incluye el paquete.')
duration_min = fields.Integer(string='Duración sesión (min)',
help='Solo informativo / para agendar. Si se deja '
'en 0 se usa la del servicio.')
sessions = fields.Integer(string='Sesiones', required=True, default=1)
price = fields.Float(string='Precio paquete', required=True, default=0.0)
validity_days = fields.Integer(string='Vigencia (días)', default=0,
help='Días desde la compra para usar las sesiones. '
'0 = sin caducidad.')
notes = fields.Char(string='Notas')
color = fields.Char(string='Color', default='#1abc9c')
genera_monedero = fields.Boolean(string='Genera monedero', default=True)
active = fields.Boolean(string='Activo', default=True)
sequence = fields.Integer(string='Secuencia', default=10)
compra_ids = fields.One2many('skeen.paquete.compra', 'paquete_id', string='Compras')
compras_count = fields.Integer(string='Compras', compute='_compute_compras_count')
@api.depends('compra_ids')
def _compute_compras_count(self):
for p in self:
p.compras_count = len(p.compra_ids)
@api.constrains('sessions', 'price')
def _check_sessions_price(self):
for p in self:
if p.sessions < 1:
raise ValidationError(_('Un paquete debe tener al menos 1 sesión.'))
if p.price < 0:
raise ValidationError(_('El precio del paquete no puede ser negativo.'))
@api.onchange('service_id')
def _onchange_service(self):
if self.service_id and not self.duration_min:
self.duration_min = self.service_id.duration_min
def name_get(self):
return [(p.id, '%s (%d sesiones)' % (p.name, p.sessions)) for p in self]
class SkeenPaqueteCompra(models.Model):
_name = 'skeen.paquete.compra'
_description = 'Paquete Comprado'
_order = 'date desc, id desc'
partner_id = fields.Many2one('res.partner', string='Paciente', required=True,
domain=[('is_patient', '=', True)])
paquete_id = fields.Many2one('skeen.paquete', string='Paquete', required=True,
ondelete='restrict')
service_id = fields.Many2one(related='paquete_id.service_id', string='Servicio',
store=True, readonly=True)
venta_id = fields.Many2one('skeen.venta', string='Venta', ondelete='set null')
sale_line_id = fields.Many2one('skeen.venta.line', string='Línea de venta',
ondelete='set null')
date = fields.Date(string='Fecha compra', required=True, default=fields.Date.today)
sessions_total = fields.Integer(string='Sesiones', required=True, default=1)
sessions_used = fields.Integer(string='Sesiones usadas', default=0)
price = fields.Float(string='Precio pagado', default=0.0)
user_login = fields.Char(string='Vendió', index=True)
sessions_remaining = fields.Integer(string='Restantes', compute='_compute_remaining',
store=True)
expiry_date = fields.Date(string='Vence', compute='_compute_expiry_date', store=True)
state = fields.Selection([
('active', 'Vigente'),
('finished', 'Completado'),
('expired', 'Vencido'),
('cancelled', 'Anulado'),
], string='Estado', default='active', compute='_compute_state', store=True)
line_ids = fields.One2many('skeen.paquete.consumo', 'compra_id', string='Consumos')
consumo_count = fields.Integer(string='Consumos', compute='_compute_consumo_count')
@api.depends('sessions_total', 'sessions_used')
def _compute_remaining(self):
for c in self:
c.sessions_remaining = max(0, c.sessions_total - c.sessions_used)
@api.depends('paquete_id.validity_days', 'date')
def _compute_expiry_date(self):
for c in self:
days = c.paquete_id.validity_days or 0
c.expiry_date = (c.date + timedelta(days=days)) \
if (c.date and days > 0) else False
@api.depends('sessions_remaining', 'expiry_date', 'date')
def _compute_state(self):
today = fields.Date.today()
for c in self:
if c.sessions_total <= 0:
c.state = 'cancelled'
elif c.sessions_remaining <= 0:
c.state = 'finished'
elif c.expiry_date and c.expiry_date < today:
c.state = 'expired'
else:
c.state = 'active'
@api.depends('line_ids')
def _compute_consumo_count(self):
for c in self:
c.consumo_count = len(c.line_ids)
@api.model_create_multi
def create(self, vals_list):
"""sessions_total por defecto = las del paquete (el POS no siempre lo manda)."""
for vals in vals_list:
if not vals.get('sessions_total') and vals.get('paquete_id'):
paquete = self.env['skeen.paquete'].browse(vals['paquete_id'])
vals['sessions_total'] = paquete.sessions or 1
return super().create(vals_list)
def action_cancel(self):
"""Anula el paquete: libera los consumos registrados."""
for c in self:
c.line_ids.write({'state': 'cancelled'})
c.write({'sessions_used': 0})
def action_adjust_sessions(self, sessions_total=None, sessions_used=None):
vals = {}
if sessions_total is not None:
vals['sessions_total'] = int(sessions_total)
if sessions_used is not None:
vals['sessions_used'] = int(sessions_used)
if vals:
self.write(vals)
return True
class SkeenPaqueteConsumo(models.Model):
_name = 'skeen.paquete.consumo'
_description = 'Consumo de Sesión de Paquete'
_order = 'date desc, id desc'
compra_id = fields.Many2one('skeen.paquete.compra', string='Paquete comprado',
required=True, ondelete='cascade')
cita_id = fields.Many2one('skeen.cita', string='Cita', ondelete='set null', index=True)
partner_id = fields.Many2one(related='compra_id.partner_id', string='Paciente',
store=True, readonly=True)
date = fields.Datetime(string='Fecha', default=fields.Datetime.now)
state = fields.Selection([
('consumed', 'Consumida'),
('cancelled', 'Liberada'),
], string='Estado', default='consumed')
_sql_constraints = [
('unique_cita_compra', 'unique(cita_id, compra_id)',
'Una misma cita solo puede consumir una sesión de un paquete.')
]
class SkeenVentaLinePaquete(models.Model):
_inherit = 'skeen.venta.line'
paquete_id = fields.Many2one('skeen.paquete', string='Paquete',
help='Si la línea vende un paquete de sesiones, '
'aquí se enlaza. Al confirmar la venta se crea '
'el saldo del paciente.')
is_package = fields.Boolean(string='Es paquete', compute='_compute_is_package',
store=True)
@api.depends('paquete_id')
def _compute_is_package(self):
for line in self:
line.is_package = bool(line.paquete_id)
@api.constrains('paquete_id', 'service_id')
def _check_paquete_service(self):
for line in self:
if line.paquete_id and line.service_id and \
line.service_id != line.paquete_id.service_id:
raise ValidationError(_(
'La línea mezcla un paquete con un servicio distinto al del paquete.'
))
class SkeenCitaPaquete(models.Model):
_inherit = 'skeen.cita'
paquete_compra_id = fields.Many2one('skeen.paquete.compra', string='Paquete origen',
ondelete='set null', index=True,
help='Compra de paquete que pagó esta cita.')
def _paquete_asignable(self):
"""Compra vigente del paciente para el servicio de esta cita."""
self.ensure_one()
if not self.partner_id or not self.servicio_id:
return None
Compra = self.env['skeen.paquete.compra'].sudo()
return Compra.search([
('partner_id', '=', self.partner_id.id),
('service_id', '=', self.servicio_id.id),
('sessions_remaining', '>', 0),
('state', '=', 'active'),
], order='expiry_date asc, date asc', limit=1)
def _registrar_consumo(self, compra):
for cita in self:
existe = self.env['skeen.paquete.consumo'].sudo().search([
('compra_id', '=', compra.id), ('cita_id', '=', cita.id)], limit=1)
if existe:
if existe.state != 'consumed':
existe.write({'state': 'consumed'})
compra.write({'sessions_used': compra.sessions_used + 1})
continue
self.env['skeen.paquete.consumo'].sudo().create({
'compra_id': compra.id,
'cita_id': cita.id,
})
compra.write({'sessions_used': compra.sessions_used + 1})
if not cita.paquete_compra_id:
cita.write({'paquete_compra_id': compra.id})
def _liberar_consumo(self):
Consumos = self.env['skeen.paquete.consumo'].sudo()
for cita in self:
registro = Consumos.search([('cita_id', '=', cita.id),
('state', '=', 'consumed')], limit=1)
if not registro:
continue
compra = registro.compra_id
registro.write({'state': 'cancelled'})
compra.write({'sessions_used': max(0, compra.sessions_used - 1)})
def write(self, vals):
cambio_estado = 'state' in vals
antes = {c.id: c.state for c in self} if cambio_estado else {}
res = super().write(vals)
if not cambio_estado:
return res
Consumir, Liberar = self.browse(), self.browse()
for c in self:
previo = antes.get(c.id)
if c.state in CONSUME_STATES and previo not in CONSUME_STATES:
if not c.paquete_compra_id:
compra = c._paquete_asignable()
if compra:
Consumir |= c
elif c.state in ('cancelled', 'no_show', 'waitlist') and \
previo in CONSUME_STATES:
Liberar |= c
for c in Consumir:
compra = c._paquete_asignable()
if compra:
c._registrar_consumo(compra)
Liberar._liberar_consumo()
return res