#!/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)