446 lines
18 KiB
Python
Executable File
446 lines
18 KiB
Python
Executable File
#!/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)
|