280 lines
12 KiB
Python
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
|