Mejoras integrales: visitas, POS, WACRM, reportes, temas y módulos legacy

- Módulo Visitas completo (auto desde agenda, insumos con descargo de
  inventario, fotos antes/después y documentos, receta imprimible)
- Punto de Venta (catálogo + ticket sticky, cobro con cambio, pago con
  puntos monedero, ticket imprimible)
- WACRM: leads automáticos desde WhatsApp, asignación de conversaciones
  y leads a agentes, conversión lead→paciente, ficha del paciente en chat
- Pacientes: completitud de expediente, alertas clínicas, historial
  unificado con detalle, foto, WhatsApp, estado de cuenta, filtros
  rápidos (VIP/recientes/médico), documentos (expediente escaneado + galería)
- Agenda: vistas por médico y por hora, filtros rápidos (libres, primera
  vez, check-in, no-show), modal de acciones, bloqueos por médico,
  drag&drop para mover citas
- Reportes: 18 pestañas (diario, cortes, ingresos, inventario, adeudos,
  comisiones, pagos, devoluciones, top clientes, horas, paquetes,
  vendedores, concentrado, recomendaciones, KPIs) con exportación Excel
- Temas: nuevo tema Clásico (look legacy AdminLTE) con submenús tipo
  treeview, selector de tema; accesos rápidos personalizables con 3
  presentaciones; búsqueda global; notificaciones reales
- Configuración: secciones (clínica, usuarios con permisos por sección,
  recetas, catálogos de diagnósticos y procedimientos)
- Inventario: alertas de caducidad y sugerencia de compra, cron diario
  que descuenta artículos caducados, compras/bajas
- Consultas Médicas, página Expedientes, importadores delta
  (citas/visitas legacy idempotentes), depuración de duplicados
- Infra: tema Tailwind conectado (@config), gzip en nginx, secuencias
  Odoo corregidas (noupdate, company_id), rollback en validaciones
This commit is contained in:
2026-08-13 23:30:08 +00:00
parent a718592291
commit 01f6007e30
109 changed files with 19067 additions and 931 deletions

View File

@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
class SkeenVisita(models.Model):
_name = 'skeen.visita'
_description = 'Visita Clínica'
_order = 'date_start desc'
# Identificación
name = fields.Char(string='Referencia', readonly=True, copy=False)
legacy_id = fields.Char(string='ID Legacy', index=True, copy=False,
help='ID de la visita en el sistema legacy SKEEN')
# Relaciones
cita_id = fields.Many2one('skeen.cita', string='Cita Origen', ondelete='set null')
partner_id = fields.Many2one('res.partner', string='Paciente', required=True,
domain=[('is_patient', '=', True)])
doctor_id = fields.Many2one('hr.employee', string='Médico')
cosmetologa_id = fields.Many2one('hr.employee', string='Cosmetóloga')
servicio_id = fields.Many2one('skeen.servicio', string='Servicio')
# Fechas
date_start = fields.Datetime(string='Inicio', default=fields.Datetime.now, required=True)
date_end = fields.Datetime(string='Fin')
# Datos clínicos
motivo = fields.Char(string='Motivo de Consulta')
diagnostico = fields.Text(string='Diagnóstico')
tratamiento = fields.Text(string='Tratamiento')
notas = fields.Text(string='Notas')
receta_text = fields.Text(string='Receta')
# Estado
state = fields.Selection([
('en_curso', 'En Curso'),
('completada', 'Completada'),
('cancelada', 'Cancelada'),
], string='Estado', default='en_curso', required=True)
# Origen: creada desde una cita o como consulta médica directa
origen = fields.Selection([
('cita', 'Cita'),
('consulta', 'Consulta Directa'),
], string='Origen', default='cita', required=True)
# Insumos (se descargan del inventario al completar la visita)
insumo_ids = fields.One2many('skeen.visita.insumo', 'visita_id', string='Insumos')
insumos_descargados = fields.Boolean(string='Insumos Descargados', default=False, readonly=True, copy=False)
# Adjuntos (fotos antes/después y documentos)
adjunto_ids = fields.One2many('skeen.visita.adjunto', 'visita_id', string='Adjuntos')
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('name'):
vals['name'] = self.env['ir.sequence'].next_by_code('skeen.visita') or 'VIS-00001'
return super(SkeenVisita, self).create(vals_list)
def action_complete(self):
self.write({'state': 'completada', 'date_end': fields.Datetime.now()})
for visita in self:
visita._descargar_insumos()
def _descargar_insumos(self):
"""Crea el movimiento de baja en inventario por cada insumo (una sola vez)"""
self.ensure_one()
if self.insumos_descargados:
return
Move = self.env['skeen.inventario.move']
for linea in self.insumo_ids:
Move.create({
'item_id': linea.item_id.id,
'type': 'baja',
'qty': linea.qty,
'reference': self.name,
'notes': 'Consumo en visita',
})
self.insumos_descargados = True
def action_cancel(self):
self.write({'state': 'cancelada'})