Infra (todo convive en la misma VPS, mismo addons_path read-only):
- infra/clinica2/: odoo.conf (puerto 8070, db clinica2_odoo, data_dir
propio), odoo-clinica2.service (sin env vars de WACRM/Supabase/Hermes),
nginx-vhost (80 con server_name clinica2.consultoria-as.com + 8880 LAN,
/api/odoo -> 8070, /wacrm/ cortado con 403), postinstall.py (brand neutro,
menu WACRM vacio, admin con password propio y must_change_password)
- docs/segunda-instancia.md: mapa completo de la instalacion, pasos para
reproducir (instancia 3+), pendiente del hostname en dashboard del tunel
(remotamente administrado), aislamiento de datos escenario B
- frontend/public/brand/: logos SVG neutros + favicon para la instancia 2
Fix de menus (aplica a todas las instalaciones limpias, una sola pasada):
- menu_skeen_root se mueve a skeen_pacientes (modulo raiz de la jerarquia);
en bases existentes se transfirio el ownership del xmlid (ir_model_data)
asi actualizar no duplica el menu
- menu_skeen_citas (padre) se declara en skeen_citas/views/menus.xml que
carga antes que bloqueo/lista_espera; menu_views.xml conserva las entradas
con action (cargan despues de cita_views, donde se define la accion)
- referencias actualizadas: pacientes(catalogos/recetas), monedero, ventas,
inventario, visitas -> skeen_pacientes.menu_skeen_root
Verificacion: produccion (skeen_odoo) actualizada sin duplicar menus y API
intacta; instalacion limpia clinica2_odoo 9 modulos OK; 15/15 tests
end-to-end (brand neutro, login admin, WACRM oculto+403, datos vacios)
- backend: GET/PUT /skeen/frontend/v1/brand leyendo ir.config_parameter
skeen.clinic.brand (JSON: nombre, logo_claro, logo_oscuro, pie) con
defaults SKEEN; GET publico (el login lo necesita sin sesion), PUT admin
con auditoria. frontend_users.xml: 'Administrador SKEEN' -> 'Administrador'
- frontend: ClinicBrand + BRAND_DEFAULT en odoo.ts; brand y setBrand en el
contexto de auth (fetch al boot + cache localStorage + document.title)
- Login/layouts/mensajes/exports leen el brand en vez de strings SKEEN:
Login.tsx, SkeenLayout, HomeNestLayout, Cumpleanos (wa + export),
Pacientes/Ventas/Cortes/Reportes/Kpis/Alertas/AgendaPrint (titulos),
useClinicaPrint (fallback nombre), Login hace GET publico al montar
- Configuracion > Clinica: tarjeta 'Identidad de la clinica' (solo admin)
para editar nombre/pie/logos con preview
- no renombrado (invisible al usuario): rutas API /skeen/*, claves
localStorage, nombres de modulos, parametros skeen.*
Verificacion API: 11/11 tests OK (defaults, PUT merge, campo invalido
ignorado, restore, admin neutral). Deploy: build + rsync + Odoo reiniciado
- skeen_citas: campo sala en skeen.lista.espera (consultoria/cabina, indexado)
- frontend.py: sala en _lista_espera_to_dict, create y update (toggle desde
la etiqueta; valor invalido vuelve a sin sala; cambio queda en historial)
- ListaEspera.tsx: toggle Todas/Consultoria/Cabina con conteo, etiqueta
clicable en cada tarjeta que alterna de sala, selector en el modal de alta
- Agenda.tsx: cambiarSala() conectado al panel
- WacrmMessages.tsx: el auto-refresco ya no empuja el scroll abajo si el
usuario esta leyendo mensajes anteriores (solo baja al cambiar de
conversacion, por deep-link o al enviar respuesta propia)
Verificacion API: 7/7 tests OK (alta con sala, toggle, valor invalido, GET)
Auditoria de 260 visitas (26 rutas x 10 roles) sobre el build actual: 0 hallazgos.
Dos defectos reales que salieron del barrido anterior se corrigen aqui.
1) Reportes: la matriz guarda claves '/reportes?tab=x' y puede conceder una
pestana suelta (Horas agenda al medico, Exportar a cosmetologia) sin dar el
modulo. Nadie la respetaba:
- App.tsx: nuevo ReportesRoute que evalua la pestana del URL; si el rol no
tiene '/reportes' pero si alguna pestana, aterriza en la primera permitida.
- Reportes.tsx: familias, favoritos y el boton "Exportar datos" se filtran
por canSee('/reportes?tab=x'). 'exportar' no pertenece a ninguna familia,
por eso se lista en TABS_FUERA_DE_FAMILIA (si no, el guard lo reescribia a
'general' y admin perdia la vista). El query se sincroniza con lo renderizado.
- SkeenLayout/HomeNestLayout: "Horas agenda" y "Exportar datos" aparecen en
Analitica para que el permiso sea alcanzable desde el menu, no solo a
mano. Clasico ya los traia.
- auth.py: _medico_reporte_permitido consulta la matriz antes de vetar
/reports/ al medico. Sin ese puente el menu ofrecia la opcion y la API
respondia 403. Las demas rutas vetadas (sales, payments, wallets, pos)
siguen cerradas: verificado comisiones=403, horas-agenda=200.
2) skeen_chat/_touch: 211 "could not serialize access due to concurrent update"
en un barrido. Bandeja, badge y heartbeat salen en paralelo y cada uno
emitia el UPDATE de last_seen aunque no aplicara cambios, abortando la
transaccion de otra peticion. Se corta antes de tocar la fila cuando el
registro es reciente; el savepoint queda para el caso de empate exacto.
Ráfaga de 128 peticiones concurrentes: todas 200, presencia sigue
actualizando (online=true, last_seen avanza) y el ruido bajo de 211 a 5.
El modelo tenia el campo price y el POS lo usa para cobrar, pero ni el
formulario de alta lo capturaba ni existia modal de edicion: los 170
articulos tenian precio en 0 y recepcion tecleaba el precio a mano en
cada venta.
- Backend: inventory_create acepta price; inventory_update lo permite
entre los campos editables (con conversion float).
- Frontend: columna 'Precio venta' en la tabla, campo en el alta y
modal nuevo 'Editar articulo' (nombre, precio, costo, SKU, categoria,
unidad, niveles y notas). La existencia sigue cambiando solo via
Ajustar para dejar registro del movimiento.
Recorrido dinamico de las 26 rutas con los 10 roles (260 visitas con
navegador real) mas revision estatica de endpoints FE vs BE. Hallazgos
y correcciones:
- frontend.py: expedientes_recientes no tenia @http.route (el endpoint
/expedientes-recientes respondia 404). Se expone con GET + lectura.
- auth.py: MEDICO_BLOCKED_PREFIXES hacia match por startswith, asi que
/sales_goal quedaba bloqueado para medicos (403 en consola en todas
las pantallas por el widget de meta del mes). Ahora el match exige
frontera de path (/sales bloquea /sales/x pero no /sales_goal).
- Reportes: getCommissions (endpoint admin-only) iba dentro de un
Promise.all; el 403 tumbaba todo el tab General para recepcion,
gerencia, enfermeria, psicologa, farmacia y lectura. Ahora va en
peticion aislada y solo se llama para admin; la seccion de comisiones
del tab General solo se renderiza para admin.
- Matriz de menus guardada: medico habia quedado fuera de /pacientes
y /visitas (los defaults del codigo lo incluyen y los medicos lo
necesitan para ver clientes e historial). Restaurado via PUT
/roles/permissions (queda en skeen_config_audit).
Verificado con navegador: medico entra a /pacientes y /visitas sin
403; recepcion y gerencia cargan /reportes sin error; admin sigue
viendo la seccion de comisiones. Usuarios temporales de auditoria
eliminados.
- Nuevo modelo skeen.ticket.config (AbstractModel) con layout, datos de clinica
y logo en ir.config_parameter; _mezclar() hace que layouts viejos hereden las
secciones nuevas; _validar() rechaza valores imposibles y textos largos.
- Endpoints GET/PUT /ticket_config, PUT /ticket_config/clinica,
POST /ticket_config/reset (reset solo admin) con auditoria en skeen_config_audit.
- Los datos de la clinica pasan de localStorage (cada caja imprimia distinto)
a configuracion compartida en el servidor.
- TicketDoc.tsx renderiza segun el diseno: ancho 58/80mm, fuente, tamano de letra,
separador, mayusculas, orden de bloques y 29 secciones activables.
- TicketDesigner.tsx: editor completo con vista previa en vivo, reorden de bloques,
textos personalizables y carga de logo.
- TicketPrint.tsx ahora es un wrapper de TicketDoc; ReciboPrint, EstadoCuentaPrint,
RecetaPrint, CortePrint y ConteoPrint usan useClinicaPrint().
- _sale_to_dict expone telefono del paciente, medico que atendio y datos de paquete;
pos_checkout devuelve recibido/cambio en MXN y USD para imprimirlos.
- Columna 'Estado' con switch Activo/Baja (confirmación previa) y checkbox
'Mostrar dados de baja' para poder reactivar.
- GET /doctors solo devuelve activos; include_inactive=1 para la pantalla de
Médicos. Cada médico expone su campo 'active'.
- Reglas en modelo: un empleado dado de baja no admite citas nuevas
(skeen.cita), ni asignación de pacientes (res.partner.primary_doctor_id),
ni líneas de venta (skeen.venta.line.prescribed_by_id). El historial
existente no se toca: los constraints solo corren cuando cambia la
asignación.
- ValidationError se responde 400 con mensaje legible en pacientes y ventas.
- fix: badges de estado de cita en agenda y dashboard usan la etiqueta de la
clínica en lugar del valor técnico.
- BloqueosModal: alta multi-médico, rango repetible por día de la semana,
motivos predefinidos, edición/borrado y cierre rápido desde la cabecera
de cada columna (todo el día / resto del día / rango).
- skeen.lista.espera + panel y modal en la agenda: prioridad, franja, rango
de fechas, agendar precargado, avisar por WhatsApp y aviso cuando se
libera un hueco compatible.
- duration en skeen.cita (0 = la del servicio): la cita ocupa sus bloques de
15 min reales en las 4 vistas, con celdas de continuación.
- Checklist de estados (Agendada → Confirmada → En cabina → En consulta →
Completada) con avance/retroceso, cancelación y no-show con confirmación,
y bitácora de quién/cuándo en skeen.cita.state.log.
- Etiquetas del sistema legacy alineadas (Canceló / Lista de espera / En
cabina) en badges de agenda y dashboard.
- Auto refresco silencioso cada 30 s (pausable, se pospone con modales,
escritura o drag) en citas + bloqueos + lista de espera en un solo request.
- Píldora verde 'N ventas' + gris 'N recomendaciones' y píldoras
índigo apiladas (usd / m.n. / tarjeta m.n.) en una sola columna
- recomendaciones_30d: pacientes captados con 'recomendado por'
- card_amount en ventas (pagos con terminal física)
- Stats por médico vía líneas recetadas, métrica consistente con comisiones
- 4 columnas nuevas en la página de Médicos
- Campo genera_monedero (default true) en servicios e items de inventario
- Puntos proporcionales a las líneas elegibles al liquidar la venta
- Toggle con icono Wallet en Servicios e Inventario
- Equivalente USD en totales del ticket (TC configurable)
- Selector MXN/USD al cobrar: recibido en dólares convertido a MXN,
cambio en ambas monedas, referencia 'USD X @ TC Y' en el pago
- Validación de TC antes de crear la venta (sin ventas huérfanas)