fase 1 productizacion: capa de configuracion (glosario multi-giro, sucursales y categorias configurables, ajustes de clinica por instancia, colores de marca, horarios, URLs de pago dinamicas) + script de provisioning de instancia nueva con datos demo + frontend consumiendo la config
This commit is contained in:
445
infra/provision/postinstall_clinica.py
Executable file
445
infra/provision/postinstall_clinica.py
Executable file
@@ -0,0 +1,445 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Post-instalación de una instancia white-label de SKEEN.
|
||||
|
||||
Se ejecuta con el Python del venv de Odoo después de inicializar los módulos:
|
||||
|
||||
/root/odoo-venv/bin/python postinstall_clinica.py /ruta/config.json
|
||||
|
||||
El config.json trae: conf, db, nombre, dominio, pack, admin_login,
|
||||
admin_password, con_wacrm, demo, report.
|
||||
|
||||
Hace, en orden:
|
||||
1. Brand neutro (ir.config_parameter 'skeen.clinic.brand').
|
||||
2. Oculta WACRM (skeen.menu_roles) salvo que con_wacrm sea True.
|
||||
3. Renombra res.company principal.
|
||||
4. Usuario admin del frontend con password propia + cambio obligatorio.
|
||||
5. Siembra, SOLO si existen en runtime, los parámetros/modelos del nuevo
|
||||
sistema de configuración: glosario (pack), skeen.clinic.settings,
|
||||
skeen.sucursal, skeen.service.category. Lo que no exista queda como TODO
|
||||
en el reporte; nunca aborta por eso.
|
||||
6. Datos demo opcionales (--demo), marcados con '[DEMO]'/'DEMO-' para poder
|
||||
limpiarlos luego con demo_limpiar.py.
|
||||
|
||||
No toca ninguna otra base de datos: todo corre sobre `db` del config.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
# ---------------------------------------------------------------- packs giro
|
||||
# (nombre, precio, duración_min, categoría) por giro. La categoría usa la
|
||||
# selección de skeen.servicio: consulta | tratamiento | procedimiento | paquete.
|
||||
PACKS = {
|
||||
'generico': [
|
||||
('Consulta general', 600, 30, 'consulta'),
|
||||
('Consulta de seguimiento', 450, 30, 'consulta'),
|
||||
('Tratamiento básico', 800, 45, 'tratamiento'),
|
||||
('Tratamiento intensivo', 1200, 60, 'tratamiento'),
|
||||
('Procedimiento menor', 950, 30, 'procedimiento'),
|
||||
('Procedimiento mayor', 1800, 90, 'procedimiento'),
|
||||
('Valoración inicial', 350, 20, 'consulta'),
|
||||
('Terapia de mantenimiento', 700, 45, 'tratamiento'),
|
||||
('Sesión express', 400, 20, 'tratamiento'),
|
||||
('Paquete 5 sesiones', 3000, 45, 'paquete'),
|
||||
],
|
||||
'spa': [
|
||||
('Masaje relajante 60 min', 900, 60, 'tratamiento'),
|
||||
('Masaje descontracturante', 1100, 60, 'tratamiento'),
|
||||
('Facial hidratante', 850, 50, 'tratamiento'),
|
||||
('Facial antiedad', 1400, 60, 'tratamiento'),
|
||||
('Exfoliación corporal', 750, 45, 'tratamiento'),
|
||||
('Envoltura de algas', 1200, 60, 'tratamiento'),
|
||||
('Drenaje linfático', 1000, 60, 'procedimiento'),
|
||||
('Depilación con cera (pierna)', 500, 40, 'procedimiento'),
|
||||
('Manicure spa', 450, 40, 'procedimiento'),
|
||||
('Paquete 5 masajes', 4000, 60, 'paquete'),
|
||||
],
|
||||
'clinica': [
|
||||
('Consulta médica general', 700, 30, 'consulta'),
|
||||
('Consulta de especialidad', 950, 40, 'consulta'),
|
||||
('Consulta de seguimiento', 500, 20, 'consulta'),
|
||||
('Aplicación de medicamento', 350, 15, 'procedimiento'),
|
||||
('Curación de herida', 450, 25, 'procedimiento'),
|
||||
('Toma de muestra', 300, 15, 'procedimiento'),
|
||||
('Terapia física', 800, 50, 'tratamiento'),
|
||||
('Rehabilitación', 900, 60, 'tratamiento'),
|
||||
('Valoración preoperatoria', 1100, 45, 'consulta'),
|
||||
('Paquete 10 terapias', 6500, 50, 'paquete'),
|
||||
],
|
||||
'derma': [
|
||||
('Consulta dermatológica', 900, 30, 'consulta'),
|
||||
('Consulta de seguimiento', 600, 20, 'consulta'),
|
||||
('Limpieza facial profunda', 950, 50, 'tratamiento'),
|
||||
('Peeling químico', 1600, 45, 'tratamiento'),
|
||||
('Microdermoabrasión', 1300, 45, 'tratamiento'),
|
||||
('Depilación láser (zona pequeña)', 1200, 30, 'procedimiento'),
|
||||
('Depilación láser (zona grande)', 2200, 45, 'procedimiento'),
|
||||
('Mesoterapia facial', 1800, 40, 'procedimiento'),
|
||||
('Crioterapia', 700, 20, 'procedimiento'),
|
||||
('Paquete 5 limpiezas', 4000, 50, 'paquete'),
|
||||
],
|
||||
}
|
||||
|
||||
DEMO_PATIENTS = [
|
||||
('Ana López Márquez', '6681230001', 'F'), ('Carlos Ruiz Peña', '6681230002', 'M'),
|
||||
('María Fernanda Soto', '6681230003', 'F'), ('Jorge Alberto Nava', '6681230004', 'M'),
|
||||
('Lucía Ramírez Ortega', '6681230005', 'F'), ('Pedro Sánchez Luna', '6681230006', 'M'),
|
||||
('Sofía Herrera Cano', '6681230007', 'F'), ('Diego Morales Vega', '6681230008', 'M'),
|
||||
('Valeria Castro Ibarra', '6681230009', 'F'), ('Andrés Gil Mendoza', '6681230010', 'M'),
|
||||
('Renata Flores Cuevas', '6681230011', 'F'), ('Miguel Ángel Ponce', '6681230012', 'M'),
|
||||
('Camila Ríos Delgado', '6681230013', 'F'), ('Fernando Ochoa Palma', '6681230014', 'M'),
|
||||
('Isabela Vargas Rocha', '6681230015', 'F'),
|
||||
]
|
||||
|
||||
DEMO_EMPLOYEES = [
|
||||
('Dra. Elena Márquez', 'doctor', 10.0),
|
||||
('Dr. Hugo Salinas', 'doctor', 8.0),
|
||||
('Cosmet. Paula Rincón', 'cosmetologa', 12.0),
|
||||
]
|
||||
|
||||
|
||||
def load_env(conf_path, db_name):
|
||||
sys.path.insert(0, '/root/skeen-odoo')
|
||||
import odoo
|
||||
from odoo.tools import config as odoo_config
|
||||
odoo_config.parse_config(['-c', conf_path, '-d', db_name,
|
||||
'--no-http', '--log-level', 'error'])
|
||||
odoo.service.server.load_server_wide_modules()
|
||||
return odoo, odoo.registry(db_name)
|
||||
|
||||
|
||||
def has_model(env, model):
|
||||
try:
|
||||
return model in env
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def write_first(record, candidates, value):
|
||||
"""Escribe `value` en el primer campo de `candidates` que exista."""
|
||||
for field in candidates:
|
||||
if field in record._fields:
|
||||
try:
|
||||
record.write({field: value})
|
||||
return field
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def create_first(model, candidates, value, extra=None):
|
||||
vals = {}
|
||||
for field in candidates:
|
||||
if field in model._fields:
|
||||
vals[field] = value
|
||||
break
|
||||
vals.update(extra or {})
|
||||
if not vals:
|
||||
return None
|
||||
return model.create(vals)
|
||||
|
||||
|
||||
def seed_brand(ICP, cfg, report):
|
||||
brand = {
|
||||
'nombre': cfg['nombre'],
|
||||
'logo_claro': '/brand/logo-blanco.svg',
|
||||
'logo_oscuro': '/brand/logo-negro.svg',
|
||||
'pie': cfg.get('pie') or 'Sistema de gestión',
|
||||
}
|
||||
ICP.set_param('skeen.clinic.brand', json.dumps(brand, ensure_ascii=False))
|
||||
report['seeded'].append('skeen.clinic.brand')
|
||||
|
||||
|
||||
def seed_menu_roles(ICP, cfg, report):
|
||||
if cfg.get('con_wacrm'):
|
||||
report['skipped'].append('skeen.menu_roles (se pidió --con-wacrm)')
|
||||
return
|
||||
roles = json.loads(ICP.get_param('skeen.menu_roles') or '{}')
|
||||
roles['/wacrm/messages'] = []
|
||||
roles['/wacrm/leads'] = []
|
||||
ICP.set_param('skeen.menu_roles', json.dumps(roles, ensure_ascii=False))
|
||||
report['seeded'].append('skeen.menu_roles (WACRM oculto)')
|
||||
|
||||
|
||||
def seed_company(env, cfg, report):
|
||||
company = env['res.company'].sudo().search([], limit=1, order='id')
|
||||
if company:
|
||||
company.write({'name': cfg['nombre']})
|
||||
report['seeded'].append('res.company.name = %s' % cfg['nombre'])
|
||||
|
||||
|
||||
def seed_admin(env, cfg, report):
|
||||
User = env['skeen.frontend.user'].sudo()
|
||||
admin = User.search([('login', '=', cfg['admin_login'])], limit=1)
|
||||
if not admin:
|
||||
vals = {'login': cfg['admin_login'], 'name': '%s Admin' % cfg['nombre'],
|
||||
'role': 'admin', 'password_hash': 'pendiente'}
|
||||
admin = User.create(vals)
|
||||
report['seeded'].append('skeen.frontend.user %s (creado)' % cfg['admin_login'])
|
||||
else:
|
||||
report['seeded'].append('skeen.frontend.user %s (password propia)' % cfg['admin_login'])
|
||||
admin.set_password(cfg['admin_password'])
|
||||
if 'must_change_password' in admin._fields:
|
||||
admin.write({'must_change_password': True})
|
||||
|
||||
|
||||
# --------------------------------------------------------- config system v2
|
||||
def seed_glossary(env, ICP, cfg, report):
|
||||
"""Glosario con el pack elegido. Tolera los tres estados posibles:
|
||||
- llave ir.config_parameter 'skeen.glosario' ya existe -> se actualiza;
|
||||
- modelo 'skeen.glosario' existe -> se crea/actualiza un registro;
|
||||
- ninguno de los dos existe -> TODO (no aborta)."""
|
||||
pack = cfg.get('pack') or 'generico'
|
||||
raw = ICP.get_param('skeen.glosario')
|
||||
if raw is not None:
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
except Exception:
|
||||
data = {}
|
||||
data['pack'] = pack
|
||||
ICP.set_param('skeen.glosario', json.dumps(data, ensure_ascii=False))
|
||||
report['seeded'].append("skeen.glosario.pack = %s" % pack)
|
||||
return
|
||||
if has_model(env, 'skeen.glosario'):
|
||||
Model = env['skeen.glosario'].sudo()
|
||||
rec = Model.search([], limit=1)
|
||||
if not rec:
|
||||
rec = create_first(Model, ('pack', 'name', 'nombre'), pack)
|
||||
elif 'pack' in Model._fields:
|
||||
rec.write({'pack': pack})
|
||||
if rec:
|
||||
report['seeded'].append("skeen.glosario -> pack %s" % pack)
|
||||
return
|
||||
report['todo'].append(
|
||||
"glosario: no existe la llave ir.config_parameter 'skeen.glosario' ni "
|
||||
"el modelo skeen.glosario; sembrar el pack '%s' cuando el otro cambio "
|
||||
"esté desplegado" % pack)
|
||||
|
||||
|
||||
def seed_clinic_settings(env, cfg, report):
|
||||
if not has_model(env, 'skeen.clinic.settings'):
|
||||
report['todo'].append(
|
||||
"skeen.clinic.settings: modelo ausente; cuando exista, sembrar "
|
||||
"nombre=%r, url_publica=https://%s, horario 08:00-19:00, moneda MXN"
|
||||
% (cfg['nombre'], cfg['dominio']))
|
||||
return
|
||||
Model = env['skeen.clinic.settings'].sudo()
|
||||
rec = Model.search([], limit=1)
|
||||
if not rec:
|
||||
rec = Model.create({})
|
||||
url = 'https://%s' % cfg['dominio']
|
||||
write_first(rec, ('name', 'nombre', 'nombre_comercial'), cfg['nombre'])
|
||||
write_first(rec, ('url_publica', 'url', 'website'), url)
|
||||
write_first(rec, ('moneda', 'currency', 'currency_name'), 'MXN')
|
||||
# 08:00 - 19:00 en los campos típicos de horario
|
||||
write_first(rec, ('hora_apertura', 'horario_apertura', 'open_time',
|
||||
'hour_from'), 8.0)
|
||||
write_first(rec, ('hora_cierre', 'horario_cierre', 'close_time',
|
||||
'hour_to'), 19.0)
|
||||
write_first(rec, ('apertura', 'hora_inicio', 'inicio'), '08:00')
|
||||
write_first(rec, ('cierre', 'hora_fin', 'fin'), '19:00')
|
||||
report['seeded'].append('skeen.clinic.settings (nombre, url, horario, MXN)')
|
||||
|
||||
|
||||
def seed_sucursal(env, cfg, report):
|
||||
if not has_model(env, 'skeen.sucursal'):
|
||||
report['todo'].append(
|
||||
"skeen.sucursal: modelo ausente; cuando exista, crear 1 sucursal "
|
||||
"'default' con nombre=%r" % cfg['nombre'])
|
||||
return
|
||||
Model = env['skeen.sucursal'].sudo()
|
||||
if Model.search([], limit=1):
|
||||
report['skipped'].append('skeen.sucursal (ya hay sucursales)')
|
||||
return
|
||||
rec = create_first(Model, ('name', 'nombre'), cfg['nombre'])
|
||||
if rec:
|
||||
report['seeded'].append('skeen.sucursal (1 sucursal default)')
|
||||
|
||||
|
||||
def seed_service_category(env, cfg, report):
|
||||
if not has_model(env, 'skeen.service.category'):
|
||||
report['todo'].append(
|
||||
"skeen.service.category: modelo ausente; cuando exista, sembrar la "
|
||||
"semilla de categorías del pack '%s'" % (cfg.get('pack') or 'generico'))
|
||||
return
|
||||
Model = env['skeen.service.category'].sudo()
|
||||
cats = []
|
||||
for _n, _p, _d, cat in PACKS.get(cfg.get('pack') or 'generico', PACKS['generico']):
|
||||
if cat not in cats:
|
||||
cats.append(cat)
|
||||
created = 0
|
||||
for cat in cats:
|
||||
label = cat.capitalize()
|
||||
if Model.search([('name', '=', label)], limit=1):
|
||||
continue
|
||||
if create_first(Model, ('name', 'nombre'), label):
|
||||
created += 1
|
||||
report['seeded'].append('skeen.service.category (%d nuevas)' % created)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- demo
|
||||
def _mark(env, model, field, value, ids):
|
||||
try:
|
||||
recs = env[model].sudo().browse(ids)
|
||||
if field in env[model]._fields:
|
||||
recs.write({field: value})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def seed_demo(env, cfg, report):
|
||||
pack = PACKS.get(cfg.get('pack') or 'generico', PACKS['generico'])
|
||||
today = datetime.date.today()
|
||||
week_start = today - datetime.timedelta(days=today.weekday())
|
||||
ids = {'servicio': [], 'paciente': [], 'empleado': [], 'cita': [],
|
||||
'paquete': [], 'monedero': []}
|
||||
|
||||
Servicio = env['skeen.servicio'].sudo()
|
||||
Serv = []
|
||||
for i, (name, price, dur, cat) in enumerate(pack, start=1):
|
||||
rec = Servicio.create({
|
||||
'name': name, 'code': 'DEMO-%02d' % i, 'price': price,
|
||||
'duration_min': dur, 'category': cat, 'is_favorite': i <= 3,
|
||||
})
|
||||
Serv.append(rec)
|
||||
ids['servicio'].append(rec.id)
|
||||
|
||||
Partner = env['res.partner'].sudo()
|
||||
Pacientes = []
|
||||
for i, (name, phone, gender) in enumerate(DEMO_PATIENTS):
|
||||
vals = {'name': name, 'is_patient': True, 'phone': phone,
|
||||
'mobile': phone, 'referred_by': 'DEMO',
|
||||
'email': 'demo%02d@demo.local' % (i + 1)}
|
||||
if 'gender' in Partner._fields:
|
||||
vals['gender'] = 'female' if gender == 'F' else 'male'
|
||||
try:
|
||||
rec = Partner.create(vals)
|
||||
except Exception:
|
||||
rec = Partner.create({'name': name, 'is_patient': True,
|
||||
'referred_by': 'DEMO'})
|
||||
Pacientes.append(rec)
|
||||
ids['paciente'].append(rec.id)
|
||||
|
||||
Employee = env['hr.employee'].sudo()
|
||||
Empleados = []
|
||||
for name, stype, comm in DEMO_EMPLOYEES:
|
||||
vals = {'name': name, 'commission_pct': comm}
|
||||
if 'staff_type' in Employee._fields:
|
||||
vals['staff_type'] = stype
|
||||
if 'notes' in Employee._fields:
|
||||
vals['notes'] = '[DEMO]'
|
||||
Empleados.append(Employee.create(vals))
|
||||
ids['empleado'] = [e.id for e in Empleados]
|
||||
|
||||
Cita = env['skeen.cita'].sudo()
|
||||
hours = [9.0, 10.0, 11.0, 12.0, 13.0, 16.0, 17.0, 18.0]
|
||||
for i in range(20):
|
||||
day = week_start + datetime.timedelta(days=i % 6)
|
||||
serv = Serv[i % len(Serv)]
|
||||
vals = {
|
||||
'partner_id': Pacientes[i % len(Pacientes)].id,
|
||||
'servicio_id': serv.id,
|
||||
'date': day.isoformat(),
|
||||
'time': hours[i % len(hours)],
|
||||
'doctor_id': Empleados[i % len(Empleados)].id,
|
||||
'state': 'confirmed' if i % 3 else 'pending',
|
||||
'medium': 'onsite',
|
||||
}
|
||||
if 'notes' in Cita._fields:
|
||||
vals['notes'] = '[DEMO]'
|
||||
try:
|
||||
c = Cita.create(vals)
|
||||
ids['cita'].append(c.id)
|
||||
except Exception:
|
||||
report['warnings'].append('demo: no se pudo crear cita %d' % i)
|
||||
|
||||
# Paquetes + alguna compra
|
||||
Paquete = env['skeen.paquete'].sudo()
|
||||
for i, serv in enumerate(Serv[:3]):
|
||||
try:
|
||||
p = Paquete.create({
|
||||
'name': 'Paquete 5 %s' % serv.name, 'code': 'DEMO-P%02d' % (i + 1),
|
||||
'service_id': serv.id, 'sessions': 5,
|
||||
'price': serv.price * 4, 'validity_days': 180,
|
||||
})
|
||||
ids['paquete'].append(p.id)
|
||||
if 'skeen.paquete.compra' in env:
|
||||
env['skeen.paquete.compra'].sudo().create({
|
||||
'partner_id': Pacientes[i].id, 'paquete_id': p.id,
|
||||
'date': today.isoformat(), 'sessions_total': 5,
|
||||
'price': p.price, 'user_login': 'demo',
|
||||
})
|
||||
except Exception as e:
|
||||
report['warnings'].append('demo: paquete %d: %s' % (i, e))
|
||||
|
||||
# Monedero con saldo para algunos pacientes
|
||||
Monedero = env['skeen.monedero'].sudo()
|
||||
for i in range(5):
|
||||
try:
|
||||
m = Monedero.create({'partner_id': Pacientes[i].id,
|
||||
'points': (i + 1) * 50})
|
||||
ids['monedero'].append(m.id)
|
||||
if 'skeen.monedero.transaction' in env:
|
||||
env['skeen.monedero.transaction'].sudo().create({
|
||||
'monedero_id': m.id, 'type': 'accrual',
|
||||
'points': (i + 1) * 50,
|
||||
'description': 'Saldo demo', 'operator': 'demo',
|
||||
})
|
||||
except Exception as e:
|
||||
report['warnings'].append('demo: monedero %d: %s' % (i, e))
|
||||
|
||||
report['demo'] = ids
|
||||
report['seeded'].append('datos demo (%s)' % ', '.join(
|
||||
'%s:%d' % (k, len(v)) for k, v in ids.items()))
|
||||
|
||||
|
||||
def main():
|
||||
cfg = json.load(open(sys.argv[1], encoding='utf-8'))
|
||||
report = {'seeded': [], 'skipped': [], 'warnings': [], 'todo': [], 'demo': {}}
|
||||
|
||||
odoo, registry = load_env(cfg['conf'], cfg['db'])
|
||||
with registry.cursor() as cr:
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
ICP = env['ir.config_parameter'].sudo()
|
||||
steps = [
|
||||
lambda: seed_brand(ICP, cfg, report),
|
||||
lambda: seed_menu_roles(ICP, cfg, report),
|
||||
lambda: seed_company(env, cfg, report),
|
||||
lambda: seed_admin(env, cfg, report),
|
||||
lambda: seed_glossary(env, ICP, cfg, report),
|
||||
lambda: seed_clinic_settings(env, cfg, report),
|
||||
lambda: seed_sucursal(env, cfg, report),
|
||||
lambda: seed_service_category(env, cfg, report),
|
||||
]
|
||||
for step in steps:
|
||||
try:
|
||||
step()
|
||||
except Exception as e:
|
||||
report['warnings'].append('%s: %s' % (step, e))
|
||||
if cfg.get('demo'):
|
||||
try:
|
||||
seed_demo(env, cfg, report)
|
||||
if any(report['demo'].values()):
|
||||
ICP.set_param('skeen.demo.ids',
|
||||
json.dumps(report['demo'], ensure_ascii=False))
|
||||
except Exception as e:
|
||||
report['warnings'].append('demo: %s' % e)
|
||||
cr.commit()
|
||||
|
||||
print('POST-INSTALL OK')
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
if cfg.get('report'):
|
||||
with open(cfg['report'], 'w', encoding='utf-8') as fh:
|
||||
json.dump(report, fh, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user