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:
@@ -15,6 +15,7 @@
|
||||
'security/ir.model.access.csv',
|
||||
'data/sequences.xml',
|
||||
'views/cita_views.xml',
|
||||
'views/bloqueo_views.xml',
|
||||
'views/menu_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Secuencia para citas -->
|
||||
<record id="seq_skeen_cita" model="ir.sequence">
|
||||
<field name="name">Cita SKEEN</field>
|
||||
@@ -19,4 +20,5 @@
|
||||
<field name="number_next">1</field>
|
||||
<field name="number_increment">1</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import cita
|
||||
from . import bloqueo
|
||||
|
||||
23
odoo-addons/skeen_citas/models/bloqueo.py
Normal file
23
odoo-addons/skeen_citas/models/bloqueo.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class SkeenCitaBloqueo(models.Model):
|
||||
_name = 'skeen.cita.bloqueo'
|
||||
_description = 'Bloqueo de Agenda por Médico'
|
||||
_order = 'date desc'
|
||||
|
||||
doctor_id = fields.Many2one('hr.employee', string='Médico', required=True)
|
||||
date = fields.Date(string='Fecha', required=True, index=True)
|
||||
all_day = fields.Boolean(string='Todo el día', default=False)
|
||||
time_from = fields.Float(string='Desde', default=9.0)
|
||||
time_to = fields.Float(string='Hasta', default=10.0)
|
||||
motivo = fields.Char(string='Motivo')
|
||||
|
||||
@api.constrains('time_from', 'time_to', 'all_day')
|
||||
def _check_horas(self):
|
||||
for b in self:
|
||||
if not b.all_day and b.time_to <= b.time_from:
|
||||
raise ValidationError(_('La hora final debe ser mayor a la inicial'))
|
||||
@@ -232,6 +232,23 @@ class SkeenCita(models.Model):
|
||||
overlapping = self.search(domain)
|
||||
if overlapping:
|
||||
raise ValidationError(_('Ya existe una cita en ese horario!'))
|
||||
|
||||
@api.constrains('date', 'time', 'doctor_id', 'servicio_id')
|
||||
def _check_bloqueos(self):
|
||||
"""Rechaza citas que se solapan con un bloqueo de agenda del médico"""
|
||||
for cita in self:
|
||||
if cita.state in ('cancelled', 'no_show') or not cita.doctor_id or not cita.date:
|
||||
continue
|
||||
dur = ((cita.servicio_id.duration_min if cita.servicio_id else 30) or 30) / 60.0
|
||||
ini = cita.time
|
||||
fin = ini + dur
|
||||
bloqueos = self.env['skeen.cita.bloqueo'].search([
|
||||
('doctor_id', '=', cita.doctor_id.id),
|
||||
('date', '=', cita.date),
|
||||
])
|
||||
for b in bloqueos:
|
||||
if b.all_day or (ini < b.time_to and fin > b.time_from):
|
||||
raise ValidationError(_('El médico tiene un bloqueo (%s)') % (b.motivo or 'agenda bloqueada'))
|
||||
|
||||
def get_available_slots(self, date, servicio_id):
|
||||
"""Retorna slots disponibles para una fecha y servicio"""
|
||||
|
||||
@@ -2,3 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_skeen_servicio_user,Acceso a Servicios SKEEN,model_skeen_servicio,base.group_user,1,1,1,1
|
||||
access_skeen_service_tag_user,Acceso a Etiquetas de Servicio SKEEN,model_skeen_service_tag,base.group_user,1,1,1,1
|
||||
access_skeen_cita_user,Acceso a Citas SKEEN,model_skeen_cita,base.group_user,1,1,1,1
|
||||
access_skeen_cita_bloqueo_user,Acceso a Bloqueos de Agenda SKEEN,model_skeen_cita_bloqueo,base.group_user,1,1,1,1
|
||||
|
||||
|
53
odoo-addons/skeen_citas/views/bloqueo_views.xml
Normal file
53
odoo-addons/skeen_citas/views/bloqueo_views.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Vista de lista de bloqueos -->
|
||||
<record id="view_skeen_cita_bloqueo_tree" model="ir.ui.view">
|
||||
<field name="name">skeen.cita.bloqueo.tree</field>
|
||||
<field name="model">skeen.cita.bloqueo</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="date"/>
|
||||
<field name="doctor_id"/>
|
||||
<field name="all_day"/>
|
||||
<field name="time_from" widget="float_time"/>
|
||||
<field name="time_to" widget="float_time"/>
|
||||
<field name="motivo"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Vista de formulario de bloqueos -->
|
||||
<record id="view_skeen_cita_bloqueo_form" model="ir.ui.view">
|
||||
<field name="name">skeen.cita.bloqueo.form</field>
|
||||
<field name="model">skeen.cita.bloqueo</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="doctor_id"/>
|
||||
<field name="date"/>
|
||||
<field name="motivo"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="all_day"/>
|
||||
<field name="time_from" widget="float_time" invisible="all_day"/>
|
||||
<field name="time_to" widget="float_time" invisible="all_day"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Acción para bloqueos -->
|
||||
<record id="action_skeen_cita_bloqueo_tree" model="ir.actions.act_window">
|
||||
<field name="name">Bloqueos de Agenda</field>
|
||||
<field name="res_model">skeen.cita.bloqueo</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Menú Bloqueos (bajo el menú Citas) -->
|
||||
<menuitem id="menu_skeen_citas_bloqueos" name="Bloqueos de Agenda" parent="menu_skeen_citas"
|
||||
action="action_skeen_cita_bloqueo_tree" sequence="20"/>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user