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/
This commit is contained in:
666
migracion/importar_a_odoo.py
Executable file
666
migracion/importar_a_odoo.py
Executable file
@@ -0,0 +1,666 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Scripts de migración de datos del sistema Laravel de SKEEN a Odoo 17.
|
||||
|
||||
Uso:
|
||||
1. Exportar CSVs del sistema Laravel (pacientes, servicios, productos, médicos, etc.)
|
||||
o generar CSVs con el formato esperado.
|
||||
2. Colocar los archivos en /root/migracion/datos/
|
||||
3. Ejecutar: source /root/odoo-venv/bin/activate && python3 /root/migracion/importar_a_odoo.py
|
||||
|
||||
Estructura esperada de CSVs:
|
||||
- pacientes.csv: nombre, apellido_paterno, apellido_materno, telefono, email, fecha_nacimiento, sexo, medico_principal, etiquetas, vip, fuente
|
||||
- servicios.csv: codigo, nombre, categoria, precio, duracion_min, descripcion, color, grupo, favorito, activo
|
||||
- productos.csv: codigo, nombre, categoria, descripcion, precio, inventario_minimo, activo
|
||||
- medicos.csv: nombre, apellido_paterno, apellido_materno, telefono, email, especialidad, rol
|
||||
- usuarios.csv: nombre, login, email, password, rol, activo
|
||||
- citas.csv: paciente_telefono, paciente_nombre, servicio_codigo, fecha, hora, estado, doctor_nombre, sucursal, medio, notas, estado_pago, monto_pagado
|
||||
- ventas.csv: paciente_telefono, fecha, servicio_codigo, cantidad, precio_unitario, descuento, impuesto, estado, notas
|
||||
- pagos.csv: paciente_telefono, cita_referencia, monto, metodo, estado, fecha_pago
|
||||
- adeudos.csv: paciente_telefono, concepto, monto, moneda, fecha_vencimiento
|
||||
- monederos.csv: telefono, puntos, activo
|
||||
|
||||
El script es idempotente: busca por teléfono / código y actualiza si ya existe.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# Odoo bootstrap
|
||||
ODOO_PATH = Path('/root/skeen-odoo')
|
||||
sys.path.insert(0, str(ODOO_PATH))
|
||||
|
||||
import odoo
|
||||
from odoo import api, fields, SUPERUSER_ID
|
||||
|
||||
DATA_DIR = Path('/root/migracion/datos')
|
||||
|
||||
|
||||
def _normalize_name(name):
|
||||
return ' '.join(str(name).split())
|
||||
|
||||
|
||||
def get_or_create_doctor(env, doctor_name):
|
||||
"""Busca o crea un empleado (médico/especialista)."""
|
||||
name = _normalize_name(doctor_name or '')
|
||||
if not name:
|
||||
return False
|
||||
# Normalizar títulos para búsqueda
|
||||
clean = name.lower().replace('dra.', '').replace('dr.', '').replace('dr ', '').replace('dra ', '').strip()
|
||||
doctor = env['hr.employee'].sudo().search([
|
||||
'|', ('name', 'ilike', name), ('name', 'ilike', clean)
|
||||
], limit=1)
|
||||
if not doctor:
|
||||
doctor = env['hr.employee'].sudo().create({'name': name})
|
||||
return doctor
|
||||
|
||||
|
||||
def get_or_create_service(env, service_code='', service_name=''):
|
||||
"""Busca servicio por código o nombre; si no existe crea uno genérico."""
|
||||
service_code = (service_code or '').strip()
|
||||
service_name = _normalize_name(service_name or '')
|
||||
servicio = False
|
||||
if service_code:
|
||||
servicio = env['skeen.servicio'].sudo().with_context(active_test=False).search([('code', '=', service_code)], limit=1)
|
||||
if not servicio and service_name:
|
||||
servicio = env['skeen.servicio'].sudo().with_context(active_test=False).search([('name', 'ilike', service_name)], limit=1)
|
||||
if not servicio and service_name:
|
||||
# Crear servicio genérico basado en el nombre histórico
|
||||
import hashlib
|
||||
name_hash = hashlib.md5(service_name.encode('utf-8')).hexdigest()[:12]
|
||||
code = f'HIST_{name_hash}'
|
||||
servicio = env['skeen.servicio'].sudo().with_context(active_test=False).search([('code', '=', code)], limit=1)
|
||||
if not servicio:
|
||||
servicio = env['skeen.servicio'].sudo().create({
|
||||
'name': service_name,
|
||||
'code': code,
|
||||
'category': 'tratamiento',
|
||||
'price': 0,
|
||||
'duration_min': 30,
|
||||
})
|
||||
if not servicio:
|
||||
servicio = env['skeen.servicio'].sudo().with_context(active_test=False).search([('code', '=', 'GENERICO')], limit=1)
|
||||
if not servicio:
|
||||
servicio = env['skeen.servicio'].sudo().create({
|
||||
'name': 'Servicio genérico',
|
||||
'code': 'GENERICO',
|
||||
'category': 'tratamiento',
|
||||
'price': 0,
|
||||
'duration_min': 30,
|
||||
})
|
||||
return servicio
|
||||
|
||||
|
||||
def get_or_create_patient(env, phone, name, **vals):
|
||||
"""Busca o crea un paciente por teléfono."""
|
||||
partner = env['res.partner'].sudo().search([('phone', '=', phone)], limit=1)
|
||||
if partner:
|
||||
return partner
|
||||
create_vals = {
|
||||
'name': name,
|
||||
'phone': phone,
|
||||
'is_patient': True,
|
||||
}
|
||||
create_vals.update(vals)
|
||||
return env['res.partner'].sudo().create(create_vals)
|
||||
|
||||
|
||||
def importar_pacientes(env, filepath):
|
||||
"""Importa pacientes desde CSV con expediente clínico completo."""
|
||||
with open(filepath, newline='', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
count = 0
|
||||
for row in reader:
|
||||
name = ' '.join(filter(None, [
|
||||
row.get('nombre', ''),
|
||||
row.get('apellido_paterno', ''),
|
||||
row.get('apellido_materno', '')
|
||||
])).strip()
|
||||
phone = (row.get('telefono') or '').strip()
|
||||
if not name or not phone:
|
||||
continue
|
||||
|
||||
vals = {
|
||||
'email': row.get('email', ''),
|
||||
}
|
||||
source = (row.get('fuente') or 'other').strip().lower()
|
||||
source_map = {
|
||||
'whatsapp': 'whatsapp',
|
||||
'facebook': 'facebook',
|
||||
'instagram': 'instagram',
|
||||
'google': 'google',
|
||||
'referral': 'referral',
|
||||
'walkin': 'walkin',
|
||||
'other': 'other',
|
||||
}
|
||||
vals['source'] = source_map.get(source, 'other')
|
||||
if row.get('vip', '').lower() in ('1', 'true', 'si', 'sí'):
|
||||
vals['is_vip'] = True
|
||||
if row.get('fecha_nacimiento'):
|
||||
vals['birth_date'] = row.get('fecha_nacimiento')
|
||||
if row.get('sexo'):
|
||||
vals['gender'] = row.get('sexo')
|
||||
|
||||
# Datos personales extendidos
|
||||
if row.get('lugar_nacimiento'):
|
||||
vals['birthplace'] = row.get('lugar_nacimiento')
|
||||
if row.get('empleo'):
|
||||
vals['occupation'] = row.get('empleo')
|
||||
if row.get('estado_civil'):
|
||||
vals['marital_status'] = row.get('estado_civil')
|
||||
if row.get('contacto_emergencia'):
|
||||
vals['emergency_contact'] = row.get('contacto_emergencia')
|
||||
if row.get('telefono_emergencia'):
|
||||
vals['emergency_phone'] = row.get('telefono_emergencia')
|
||||
if row.get('telefono_casa'):
|
||||
vals['home_phone'] = row.get('telefono_casa')
|
||||
if row.get('direccion'):
|
||||
vals['address_notes'] = row.get('direccion')
|
||||
if row.get('recomendado_por'):
|
||||
vals['referred_by'] = row.get('recomendado_por')
|
||||
if row.get('comentarios'):
|
||||
vals['patient_comments'] = row.get('comentarios')
|
||||
|
||||
# Historia clínica
|
||||
try:
|
||||
hijos = int(row.get('hijos', 0) or 0)
|
||||
vals['children_count'] = hijos
|
||||
except Exception:
|
||||
vals['children_count'] = 0
|
||||
vals['is_pregnant'] = row.get('embarazada', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['is_breastfeeding'] = row.get('lactancia', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['uses_contraceptives'] = row.get('anticonceptivos', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['kidney_problems'] = row.get('problemas_rinon', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['back_pain'] = row.get('problemas_espalda', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['heart_disease'] = row.get('problemas_cardiacos', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['respiratory_problems'] = row.get('problemas_respiratorios', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['blood_pressure'] = (
|
||||
row.get('alta_presion', '').lower() in ('1', 'true', 'si', 'sí') or
|
||||
row.get('baja_presion', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
)
|
||||
vals['diabetes'] = row.get('diabetes', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['thyroid'] = (
|
||||
row.get('hipertiroidismo', '').lower() in ('1', 'true', 'si', 'sí') or
|
||||
row.get('hipotiroidismo', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
)
|
||||
vals['colitis'] = row.get('colitis', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['constipation'] = row.get('estrenimiento', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['liver_problems'] = row.get('problemas_higado', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['surgeries'] = row.get('cirugias', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['varicose_veins'] = row.get('varices', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['migraine'] = row.get('migrana', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
vals['faints_with_needles'] = row.get('desmaya_agujas', '').lower() in ('1', 'true', 'si', 'sí')
|
||||
|
||||
if row.get('alergias'):
|
||||
vals['allergies'] = row.get('alergias')
|
||||
if row.get('medicamentos'):
|
||||
vals['current_medication'] = row.get('medicamentos')
|
||||
if row.get('tratamientos_previos'):
|
||||
vals['medical_history'] = row.get('tratamientos_previos')
|
||||
|
||||
medico_nombre = row.get('medico_principal', '').strip()
|
||||
if medico_nombre:
|
||||
medico = get_or_create_doctor(env, medico_nombre)
|
||||
if medico:
|
||||
vals['primary_doctor_id'] = medico.id
|
||||
|
||||
etiquetas = row.get('etiquetas', '').strip()
|
||||
if etiquetas:
|
||||
tag_names = [t.strip() for t in etiquetas.split(',') if t.strip()]
|
||||
tags = env['skeen.patient.tag'].sudo()
|
||||
tag_ids = []
|
||||
for tag_name in tag_names:
|
||||
tag = tags.search([('name', '=', tag_name)], limit=1)
|
||||
if not tag:
|
||||
tag = tags.create({'name': tag_name})
|
||||
tag_ids.append(tag.id)
|
||||
vals['tag_ids'] = [(6, 0, tag_ids)]
|
||||
|
||||
get_or_create_patient(env, phone, name, **vals)
|
||||
count += 1
|
||||
if count % 500 == 0:
|
||||
env.cr.commit()
|
||||
print(f' pacientes: {count} procesados')
|
||||
env.cr.commit()
|
||||
return count
|
||||
|
||||
|
||||
def importar_servicios(env, filepath):
|
||||
"""Importa catálogo de servicios."""
|
||||
category_map = {
|
||||
'consulta': 'consulta',
|
||||
'tratamiento': 'tratamiento',
|
||||
'procedimiento': 'procedimiento',
|
||||
'paquete': 'paquete',
|
||||
'servicios_medicos': 'consulta',
|
||||
'procedimientos_cosmeticos': 'procedimiento',
|
||||
'tecnologia_medica': 'tratamiento',
|
||||
'cosmetologia_basica': 'tratamiento',
|
||||
'cosmetologia_avanzada': 'tratamiento',
|
||||
}
|
||||
seen = set()
|
||||
with open(filepath, newline='', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
count = 0
|
||||
for row in reader:
|
||||
code = (row.get('codigo') or '').strip()
|
||||
name = (row.get('nombre') or '').strip()
|
||||
if not code or not name or code in seen:
|
||||
continue
|
||||
seen.add(code)
|
||||
existing = env['skeen.servicio'].sudo().with_context(active_test=False).search([('code', '=', code)], limit=1)
|
||||
if existing:
|
||||
continue
|
||||
cat = category_map.get((row.get('categoria') or '').strip().lower(), 'tratamiento')
|
||||
env['skeen.servicio'].sudo().create({
|
||||
'name': name,
|
||||
'code': code,
|
||||
'category': cat,
|
||||
'price': float(row.get('precio', 0) or 0),
|
||||
'duration_min': int(row.get('duracion_min', 30) or 30),
|
||||
'description': row.get('descripcion', ''),
|
||||
'active': row.get('activo', '1').lower() in ('1', 'true', 'si'),
|
||||
})
|
||||
count += 1
|
||||
if count % 100 == 0:
|
||||
env.cr.commit()
|
||||
print(f' servicios: {count} procesados')
|
||||
env.cr.commit()
|
||||
return count
|
||||
|
||||
|
||||
def importar_productos(env, filepath):
|
||||
"""Importa productos / inventario."""
|
||||
with open(filepath, newline='', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
count = 0
|
||||
for row in reader:
|
||||
name = (row.get('nombre') or '').strip()
|
||||
if not name:
|
||||
continue
|
||||
# Crear como product.template básico de Odoo + campos custom si aplica
|
||||
existing = env['product.template'].sudo().search([('name', '=', name)], limit=1)
|
||||
if existing:
|
||||
continue
|
||||
env['product.template'].sudo().create({
|
||||
'name': name,
|
||||
'list_price': float(row.get('precio', 0) or 0),
|
||||
'type': 'product',
|
||||
'default_code': row.get('codigo', ''),
|
||||
'description_sale': row.get('descripcion', ''),
|
||||
})
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def importar_medicos(env, filepath):
|
||||
"""Importa médicos/cosmetólogos como empleados."""
|
||||
with open(filepath, newline='', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
count = 0
|
||||
for row in reader:
|
||||
name = ' '.join(filter(None, [
|
||||
row.get('nombre', ''),
|
||||
row.get('apellido_paterno', ''),
|
||||
row.get('apellido_materno', '')
|
||||
])).strip()
|
||||
if not name:
|
||||
continue
|
||||
existing = env['hr.employee'].sudo().search([('name', '=', name)], limit=1)
|
||||
if existing:
|
||||
continue
|
||||
env['hr.employee'].sudo().create({
|
||||
'name': name,
|
||||
'work_phone': row.get('telefono', ''),
|
||||
'work_email': row.get('email', ''),
|
||||
'job_title': row.get('especialidad', ''),
|
||||
})
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def importar_monederos(env, filepath):
|
||||
"""Importa saldos de monedero por teléfono de paciente."""
|
||||
with open(filepath, newline='', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
count = 0
|
||||
for row in reader:
|
||||
phone = (row.get('telefono') or '').strip()
|
||||
points = int(float(row.get('puntos', 0) or 0))
|
||||
if not phone:
|
||||
continue
|
||||
partner = env['res.partner'].sudo().search([('phone', '=', phone)], limit=1)
|
||||
if not partner:
|
||||
partner = get_or_create_patient(env, phone, 'Paciente ' + phone)
|
||||
monedero = env['skeen.monedero'].sudo().search([
|
||||
('partner_id', '=', partner.id)
|
||||
], limit=1)
|
||||
if monedero:
|
||||
monedero.write({'points': points})
|
||||
else:
|
||||
env['skeen.monedero'].sudo().create({
|
||||
'partner_id': partner.id,
|
||||
'points': points,
|
||||
})
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def _parse_time(value):
|
||||
"""Convierte '09:30' o '9.5' a float de horas."""
|
||||
if not value:
|
||||
return 9.0
|
||||
value = str(value).strip()
|
||||
if ':' in value:
|
||||
h, m = value.split(':')[:2]
|
||||
return float(h) + float(m) / 60.0
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return 9.0
|
||||
|
||||
|
||||
def importar_citas(env, filepath):
|
||||
"""Importa citas históricas y futuras."""
|
||||
# Desactivar temporalmente la validación de solapamiento para la migración masiva
|
||||
CitaModel = env.registry['skeen.cita']
|
||||
original_constraint = CitaModel._check_availability
|
||||
CitaModel._check_availability = lambda self: None
|
||||
|
||||
try:
|
||||
with open(filepath, newline='', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
count = 0
|
||||
for row in reader:
|
||||
phone = (row.get('paciente_telefono') or '').strip()
|
||||
name = (row.get('paciente_nombre') or '').strip()
|
||||
date_str = (row.get('fecha') or '').strip()
|
||||
time_str = (row.get('hora') or '').strip()
|
||||
if not phone or not date_str:
|
||||
continue
|
||||
|
||||
partner = env['res.partner'].sudo().search([('phone', '=', phone)], limit=1)
|
||||
if not partner:
|
||||
partner = get_or_create_patient(env, phone, name or 'Paciente ' + phone)
|
||||
|
||||
servicio = get_or_create_service(
|
||||
env,
|
||||
row.get('servicio_codigo', ''),
|
||||
row.get('servicio_nombre', '')
|
||||
)
|
||||
|
||||
state_map = {
|
||||
'pendiente': 'pending',
|
||||
'confirmada': 'confirmed',
|
||||
'llego': 'arrived',
|
||||
'en_progreso': 'in_progress',
|
||||
'completada': 'done',
|
||||
'cancelada': 'cancelled',
|
||||
'no_show': 'no_show',
|
||||
}
|
||||
state = state_map.get((row.get('estado') or '').strip().lower(), 'confirmed')
|
||||
|
||||
payment_state_map = {
|
||||
'no_pagado': 'not_paid',
|
||||
'parcial': 'partial',
|
||||
'pagado': 'paid',
|
||||
}
|
||||
payment_state = payment_state_map.get((row.get('estado_pago') or '').strip().lower(), 'not_paid')
|
||||
|
||||
doctor = get_or_create_doctor(env, row.get('doctor_nombre', ''))
|
||||
|
||||
branch = 'rosarito'
|
||||
if (row.get('sucursal') or '').strip().lower() in ('tijuana', 'tj'):
|
||||
branch = 'tijuana'
|
||||
|
||||
medium = 'onsite'
|
||||
if (row.get('medio') or '').strip().lower() in ('videollamada', 'video', 'remota'):
|
||||
medium = 'videocall'
|
||||
|
||||
vals = {
|
||||
'partner_id': partner.id,
|
||||
'servicio_id': servicio.id,
|
||||
'date': date_str,
|
||||
'time': _parse_time(time_str),
|
||||
'state': state,
|
||||
'payment_state': payment_state,
|
||||
'amount_paid': float(row.get('monto_pagado', 0) or 0),
|
||||
'doctor_id': doctor.id if doctor else False,
|
||||
'branch': branch,
|
||||
'medium': medium,
|
||||
'notes': row.get('notas', ''),
|
||||
}
|
||||
env['skeen.cita'].sudo().create(vals)
|
||||
count += 1
|
||||
if count % 500 == 0:
|
||||
env.cr.commit()
|
||||
print(f' citas: {count} procesadas')
|
||||
env.cr.commit()
|
||||
return count
|
||||
finally:
|
||||
CitaModel._check_availability = original_constraint
|
||||
|
||||
|
||||
def importar_ventas(env, filepath):
|
||||
"""Importa ventas históricas."""
|
||||
with open(filepath, newline='', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
count = 0
|
||||
for row in reader:
|
||||
phone = (row.get('paciente_telefono') or '').strip()
|
||||
if not phone:
|
||||
continue
|
||||
|
||||
partner = env['res.partner'].sudo().search([('phone', '=', phone)], limit=1)
|
||||
if not partner:
|
||||
partner = get_or_create_patient(env, phone, 'Paciente ' + phone)
|
||||
|
||||
servicio = get_or_create_service(
|
||||
env,
|
||||
row.get('servicio_codigo', ''),
|
||||
row.get('servicio_nombre', '')
|
||||
)
|
||||
|
||||
qty = float(row.get('cantidad', 1) or 1)
|
||||
price = float(row.get('precio_unitario', servicio.price) or servicio.price)
|
||||
discount = float(row.get('descuento', 0) or 0)
|
||||
tax = float(row.get('impuesto', 0) or 0)
|
||||
|
||||
state_map = {
|
||||
'borrador': 'draft',
|
||||
'confirmada': 'confirmed',
|
||||
'pagada': 'paid',
|
||||
'parcial': 'partial',
|
||||
'cancelada': 'cancelled',
|
||||
}
|
||||
state = state_map.get((row.get('estado') or '').strip().lower(), 'confirmed')
|
||||
|
||||
venta = env['skeen.venta'].sudo().create({
|
||||
'partner_id': partner.id,
|
||||
'date': row.get('fecha') or fields.Date.today(),
|
||||
'discount': discount,
|
||||
'tax': tax,
|
||||
'state': state,
|
||||
'notes': row.get('notas', ''),
|
||||
'line_ids': [(0, 0, {
|
||||
'service_id': servicio.id,
|
||||
'description': servicio.name,
|
||||
'quantity': qty,
|
||||
'price_unit': price,
|
||||
})],
|
||||
})
|
||||
|
||||
# Si está pagada, marcar como pagada para que acumule puntos
|
||||
if state == 'paid':
|
||||
venta.action_mark_paid()
|
||||
|
||||
count += 1
|
||||
if count % 500 == 0:
|
||||
env.cr.commit()
|
||||
print(f' ventas: {count} procesadas')
|
||||
env.cr.commit()
|
||||
return count
|
||||
|
||||
|
||||
def importar_pagos(env, filepath):
|
||||
"""Importa pagos históricos vinculados a pacientes y citas."""
|
||||
with open(filepath, newline='', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
count = 0
|
||||
for row in reader:
|
||||
phone = (row.get('paciente_telefono') or '').strip()
|
||||
amount = float(row.get('monto', 0) or 0)
|
||||
if not phone or amount <= 0:
|
||||
continue
|
||||
|
||||
partner = env['res.partner'].sudo().search([('phone', '=', phone)], limit=1)
|
||||
if not partner:
|
||||
partner = get_or_create_patient(env, phone, 'Paciente ' + phone)
|
||||
|
||||
cita_ref = (row.get('cita_referencia') or '').strip()
|
||||
cita_id = False
|
||||
if cita_ref:
|
||||
cita = env['skeen.cita'].sudo().search([('name', '=', cita_ref)], limit=1)
|
||||
cita_id = cita.id if cita else False
|
||||
|
||||
method_map = {
|
||||
'efectivo': 'cash',
|
||||
'tarjeta': 'card',
|
||||
'transferencia': 'transfer',
|
||||
'stripe': 'stripe',
|
||||
'mercadopago': 'mercadopago',
|
||||
}
|
||||
method = method_map.get((row.get('metodo') or '').strip().lower(), 'cash')
|
||||
|
||||
state_map = {
|
||||
'pendiente': 'pending',
|
||||
'procesando': 'processing',
|
||||
'completado': 'completed',
|
||||
'fallido': 'failed',
|
||||
'reembolsado': 'refunded',
|
||||
}
|
||||
state = state_map.get((row.get('estado') or '').strip().lower(), 'completed')
|
||||
|
||||
vals = {
|
||||
'partner_id': partner.id,
|
||||
'cita_id': cita_id,
|
||||
'amount': amount,
|
||||
'payment_method': method,
|
||||
'state': state,
|
||||
}
|
||||
if row.get('fecha_pago'):
|
||||
vals['payment_date'] = row.get('fecha_pago')
|
||||
|
||||
pago = env['skeen.pago'].sudo().create(vals)
|
||||
if state == 'completed':
|
||||
pago.action_confirm_payment()
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def importar_adeudos(env, filepath):
|
||||
"""Importa adeudos como ventas en estado draft/con adeudo."""
|
||||
with open(filepath, newline='', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
count = 0
|
||||
for row in reader:
|
||||
phone = (row.get('telefono') or '').strip()
|
||||
concepto = (row.get('concepto') or 'Adeudo').strip()
|
||||
monto = float(row.get('monto', 0) or 0)
|
||||
if not phone or monto <= 0:
|
||||
continue
|
||||
|
||||
partner = env['res.partner'].sudo().search([('phone', '=', phone)], limit=1)
|
||||
if not partner:
|
||||
partner = get_or_create_patient(env, phone, 'Paciente ' + phone)
|
||||
|
||||
# Crear un servicio genérico de adeudo si no existe
|
||||
servicio = env['skeen.servicio'].sudo().with_context(active_test=False).search([('code', '=', 'ADEUDO')], limit=1)
|
||||
if not servicio:
|
||||
servicio = env['skeen.servicio'].sudo().create({
|
||||
'name': 'Adeudo histórico',
|
||||
'code': 'ADEUDO',
|
||||
'category': 'consulta',
|
||||
'price': 0,
|
||||
'duration_min': 0,
|
||||
})
|
||||
|
||||
env['skeen.venta'].sudo().create({
|
||||
'partner_id': partner.id,
|
||||
'date': row.get('fecha_vencimiento') or fields.Date.today(),
|
||||
'state': 'confirmed',
|
||||
'notes': concepto,
|
||||
'line_ids': [(0, 0, {
|
||||
'service_id': servicio.id,
|
||||
'description': concepto,
|
||||
'quantity': 1,
|
||||
'price_unit': monto,
|
||||
})],
|
||||
})
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Importar datos de SKEEN a Odoo')
|
||||
parser.add_argument('--odoo-config', default='/root/skeen-odoo/odoo.conf')
|
||||
parser.add_argument('--database', default='skeen_odoo')
|
||||
args = parser.parse_args()
|
||||
|
||||
odoo.tools.config.parse_config(['-c', args.odoo_config])
|
||||
db = odoo.sql_db.db_connect(args.database)
|
||||
with db.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
resultados = {}
|
||||
|
||||
pacientes_path = DATA_DIR / 'pacientes.csv'
|
||||
if pacientes_path.exists():
|
||||
resultados['pacientes'] = importar_pacientes(env, pacientes_path)
|
||||
|
||||
servicios_path = DATA_DIR / 'servicios.csv'
|
||||
if servicios_path.exists():
|
||||
resultados['servicios'] = importar_servicios(env, servicios_path)
|
||||
|
||||
productos_path = DATA_DIR / 'productos.csv'
|
||||
if productos_path.exists():
|
||||
resultados['productos'] = importar_productos(env, productos_path)
|
||||
|
||||
medicos_path = DATA_DIR / 'medicos.csv'
|
||||
if medicos_path.exists():
|
||||
resultados['medicos'] = importar_medicos(env, medicos_path)
|
||||
|
||||
citas_path = DATA_DIR / 'citas.csv'
|
||||
if citas_path.exists():
|
||||
resultados['citas'] = importar_citas(env, citas_path)
|
||||
|
||||
ventas_path = DATA_DIR / 'ventas.csv'
|
||||
if ventas_path.exists():
|
||||
resultados['ventas'] = importar_ventas(env, ventas_path)
|
||||
|
||||
pagos_path = DATA_DIR / 'pagos.csv'
|
||||
if pagos_path.exists():
|
||||
resultados['pagos'] = importar_pagos(env, pagos_path)
|
||||
|
||||
adeudos_path = DATA_DIR / 'adeudos.csv'
|
||||
if adeudos_path.exists():
|
||||
resultados['adeudos'] = importar_adeudos(env, adeudos_path)
|
||||
|
||||
monederos_path = DATA_DIR / 'monederos.csv'
|
||||
if monederos_path.exists():
|
||||
resultados['monederos'] = importar_monederos(env, monederos_path)
|
||||
|
||||
cr.commit()
|
||||
|
||||
print('Importación completada:')
|
||||
for k, v in resultados.items():
|
||||
print(f' {k}: {v} registros')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user