Files
SKEEN-Proyecto/migracion/importar_servicios_inventario.py
Consultoría Alcaraz Salazar a718592291 Initial commit: SKEEN Derma Experts - Sistema Integral de Gestión Clínica
- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind
- Frontend Homenest (versión alternativa)
- Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp)
- WACRM fork (Next.js 16 + Supabase)
- Hermes + Bridge + Skills (Qwen3.6 via Nan Builders)
- Scripts de migración y operación
- Documentación extensiva en docs/
2026-07-20 07:44:23 +00:00

366 lines
14 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Importador de servicios e inventario para SKEEN Derma Experts.
Uso:
python3 importar_servicios_inventario.py \
--servicios servicios.csv \
--inventario inventario.csv \
[--purge-citas] \
[--generar-hermes]
Formato CSV servicios:
name,code,category,price,duration_min,package_price,package_sessions,package_notes,description,service_group,color,is_favorite
category: consulta | tratamiento | procedimiento | paquete
price: precio por sesion (float)
package_price: precio total del paquete (float, 0 si no aplica)
package_sessions: numero de sesiones del paquete (int, 0 si no aplica)
Formato CSV inventario:
name,kind,sku,category,unit,qty,qty_optimal,qty_min,cost,expiry_date,notes
kind: producto | consumible
expiry_date: opcional, formato YYYY-MM-DD
"""
import argparse
import csv
import json
import os
import sys
import xmlrpc.client
from datetime import datetime
ODOO_URL = os.environ.get('ODOO_URL', 'http://localhost:8069')
ODOO_DB = os.environ.get('ODOO_DB', 'skeen_odoo')
ODOO_USER = os.environ.get('ODOO_USER', 'admin')
ODOO_PASSWORD = os.environ.get('ODOO_PASSWORD', 'skeen_admin_2026')
def conectar():
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASSWORD, {})
if not uid:
raise Exception("Autenticacion fallida. Verifica ODOO_USER / ODOO_PASSWORD.")
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
return uid, models
def leer_csv(path):
if not path or not os.path.exists(path):
return []
with open(path, 'r', encoding='utf-8-sig', newline='') as f:
reader = csv.DictReader(f)
rows = [row for row in reader]
return rows
def _run_sql(command):
"""Ejecuta un comando SQL via psql como usuario postgres."""
import subprocess
full = f"sudo -u postgres psql -d {ODOO_DB} -c \"{command}\""
result = subprocess.run(full, shell=True, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"SQL error: {result.stderr}\n{result.stdout}")
return result.stdout
def limpiar_inventario(models, uid, use_sql=False):
if use_sql:
print("[INVENTARIO] Eliminando movimientos e items via SQL...")
_run_sql("TRUNCATE TABLE skeen_inventario_move RESTART IDENTITY CASCADE")
_run_sql("TRUNCATE TABLE skeen_inventario_item RESTART IDENTITY CASCADE")
print(" -> Inventario limpio")
return
print("[INVENTARIO] Eliminando movimientos...")
move_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.inventario.move', 'search', [[]])
if move_ids:
models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.inventario.move', 'unlink', [move_ids])
print(f" -> Eliminados {len(move_ids)} movimientos")
print("[INVENTARIO] Eliminando items...")
item_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.inventario.item', 'search', [[]])
if item_ids:
models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.inventario.item', 'unlink', [item_ids])
print(f" -> Eliminados {len(item_ids)} items")
def limpiar_servicios(models, uid, purge_citas=False, use_sql=False):
if use_sql and purge_citas:
print("[SERVICIOS] Eliminando citas y servicios via SQL...")
_run_sql("TRUNCATE TABLE skeen_cita RESTART IDENTITY CASCADE")
_run_sql("TRUNCATE TABLE skeen_service_tag_skeen_servicio_rel RESTART IDENTITY CASCADE")
_run_sql("TRUNCATE TABLE skeen_service_tag RESTART IDENTITY CASCADE")
_run_sql("TRUNCATE TABLE skeen_servicio RESTART IDENTITY CASCADE")
print(" -> Servicios y citas limpios")
return
if purge_citas:
print("[SERVICIOS] Eliminando citas existentes...")
cita_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.cita', 'search', [[]])
if cita_ids:
models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.cita', 'unlink', [cita_ids])
print(f" -> Eliminadas {len(cita_ids)} citas")
print("[SERVICIOS] Eliminando servicios...")
servicio_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.servicio', 'search', [[]])
if servicio_ids:
if purge_citas:
models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.servicio', 'unlink', [servicio_ids])
print(f" -> Eliminados {len(servicio_ids)} servicios")
else:
# Intentar eliminar uno a uno; los que tengan citas fallaran
eliminados = 0
fallidos = []
for sid in servicio_ids:
try:
models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.servicio', 'unlink', [[sid]])
eliminados += 1
except Exception as e:
serv = models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.servicio', 'read',
[[sid], ['name', 'code']])
fallidos.append((sid, serv[0] if serv else {}, str(e)))
print(f" -> Eliminados {eliminados} servicios")
if fallidos:
print(f" -> No se pudieron eliminar {len(fallidos)} servicios (tienen citas/historial):")
for sid, info, err in fallidos[:10]:
print(f" - {info.get('name', sid)} ({info.get('code', '')}): {err[:120]}")
print("[SERVICIOS] Eliminando tags de servicios...")
tag_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.service.tag', 'search', [[]])
if tag_ids:
models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.service.tag', 'unlink', [tag_ids])
print(f" -> Eliminados {len(tag_ids)} tags")
def parse_float(val, default=0.0):
if val is None:
return default
val = str(val).strip().replace(',', '').replace('$', '')
if val == '':
return default
try:
return float(val)
except ValueError:
return default
def parse_int(val, default=0):
if val is None:
return default
val = str(val).strip()
if val == '':
return default
try:
return int(float(val))
except ValueError:
return default
def importar_servicios(models, uid, rows):
print(f"[SERVICIOS] Importando {len(rows)} servicios...")
creados = 0
for row in rows:
name = (row.get('name') or '').strip()
code = (row.get('code') or '').strip()
if not name or not code:
print(f" -> Saltando fila sin nombre o codigo: {row}")
continue
category = (row.get('category') or 'tratamiento').strip().lower()
if category not in ('consulta', 'tratamiento', 'procedimiento', 'paquete'):
category = 'tratamiento'
vals = {
'name': name,
'code': code,
'category': category,
'price': parse_float(row.get('price')),
'duration_min': parse_int(row.get('duration_min'), 30),
'package_price': parse_float(row.get('package_price')),
'package_sessions': parse_int(row.get('package_sessions')),
'package_notes': (row.get('package_notes') or '').strip(),
'description': (row.get('description') or '').strip(),
'service_group': (row.get('service_group') or '').strip(),
'color': (row.get('color') or '#1abc9c').strip(),
'is_favorite': str(row.get('is_favorite', '')).strip().lower() in ('1', 'true', 'si', '', 'yes'),
}
# Si es categoria paquete pero no tiene sesiones, forzar consulta
if category == 'paquete' and vals['package_sessions'] <= 0:
vals['category'] = 'tratamiento'
existing = models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.servicio', 'search',
[[['code', '=', code]]])
if existing:
models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.servicio', 'write', [existing, vals])
print(f" -> Actualizado: {name} ({code})")
else:
models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.servicio', 'create', [vals])
creados += 1
print(f" -> Creado: {name} ({code})")
print(f"[SERVICIOS] {creados} nuevos creados.")
def importar_inventario(models, uid, rows):
print(f"[INVENTARIO] Importando {len(rows)} items...")
creados = 0
for row in rows:
name = (row.get('name') or '').strip()
if not name:
continue
kind = (row.get('kind') or 'producto').strip().lower()
if kind not in ('producto', 'consumible'):
kind = 'producto'
sku = (row.get('sku') or '').strip()
expiry = (row.get('expiry_date') or '').strip()
vals = {
'name': name,
'kind': kind,
'sku': sku,
'category': (row.get('category') or '').strip(),
'unit': (row.get('unit') or 'pieza').strip(),
'qty': parse_float(row.get('qty')),
'qty_optimal': parse_float(row.get('qty_optimal')),
'qty_min': parse_float(row.get('qty_min')),
'cost': parse_float(row.get('cost')),
'notes': (row.get('notes') or '').strip(),
}
if expiry:
try:
datetime.strptime(expiry, '%Y-%m-%d')
vals['expiry_date'] = expiry
except ValueError:
pass
existing = models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.inventario.item', 'search',
[[['sku', '=', sku]]] if sku else [[['name', '=', name]]])
if existing:
models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.inventario.item', 'write', [existing, vals])
print(f" -> Actualizado: {name}")
else:
models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.inventario.item', 'create', [vals])
creados += 1
print(f" -> Creado: {name}")
print(f"[INVENTARIO] {creados} nuevos items creados.")
def generar_hermes_services(models, uid):
print("[HERMES] Generando catalogo para skills...")
servicios = models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
'skeen.servicio', 'search_read',
[[['active', '=', True]]],
{'fields': ['name', 'code', 'category', 'price',
'duration_min', 'package_price',
'package_sessions', 'package_notes',
'description']})
payload = {
"clinica": {
"nombre": "SKEEN Derma Experts",
"direccion": "Blvd. Popotla 123, Playas de Rosarito, BC, Mexico",
"telefono": "+52 (664) 123-4567",
"horario": {
"lunes_viernes": "9:00 - 18:00",
"sabado": "9:00 - 14:00",
"domingo": "Cerrado"
}
},
"servicios": []
}
for s in servicios:
payload['servicios'].append({
"id": s['code'].lower().replace(' ', '-'),
"nombre": s['name'],
"categoria": s['category'],
"precio": s['price'],
"duracion_min": s['duration_min'],
"precio_paquete": s['package_price'] or 0,
"sesiones_paquete": s['package_sessions'] or 0,
"notas_paquete": s['package_notes'] or '',
"descripcion": s['description'] or '',
"contraindicaciones": []
})
paths = [
'/root/.hermes/skills/skeen-services.json',
'/root/skeen-roadmap-hermes/hermes/skills/skeen-services.json',
]
for p in paths:
try:
with open(p, 'w', encoding='utf-8') as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
print(f" -> Escrito: {p}")
except Exception as e:
print(f" -> Error escribiendo {p}: {e}")
def main():
parser = argparse.ArgumentParser(description='Importar servicios e inventario a Odoo')
parser.add_argument('--servicios', help='CSV de servicios')
parser.add_argument('--inventario', help='CSV de inventario')
parser.add_argument('--purge-citas', action='store_true',
help='Eliminar citas existentes antes de borrar servicios')
parser.add_argument('--purge-sql', action='store_true',
help='Usar TRUNCATE SQL directo para limpieza rapida (miles de registros)')
parser.add_argument('--generar-hermes', action='store_true',
help='Generar skeen-services.json para Hermes')
parser.add_argument('--solo-hermes', action='store_true',
help='Solo generar skeen-services.json desde Odoo actual')
args = parser.parse_args()
uid, models = conectar()
print(f"[OK] Conectado a Odoo {ODOO_URL} como {ODOO_USER} (uid={uid})")
if args.solo_hermes:
generar_hermes_services(models, uid)
return
if not args.servicios and not args.inventario:
print("[INFO] No se especificaron CSVs. Usa --servicios y/o --inventario.")
return
if args.inventario:
limpiar_inventario(models, uid, use_sql=args.purge_sql)
rows_inv = leer_csv(args.inventario)
importar_inventario(models, uid, rows_inv)
if args.servicios:
limpiar_servicios(models, uid, purge_citas=args.purge_citas, use_sql=args.purge_sql)
rows_srv = leer_csv(args.servicios)
importar_servicios(models, uid, rows_srv)
if args.generar_hermes:
generar_hermes_services(models, uid)
print("[OK] Proceso completado.")
if __name__ == '__main__':
main()