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:
2026-07-20 07:44:23 +00:00
commit a718592291
699 changed files with 324602 additions and 0 deletions

94
migracion/README.md Normal file
View File

@@ -0,0 +1,94 @@
# Migración de datos del sistema legacy de SKEEN a Odoo 17
## Requisito previo
Obtener una **exportación autorizada** del sistema actual (`https://sistema.skeenmx.app/`).
> **Nota importante:** No se realizará scraping ni extracción masiva no autorizada desde el sistema en producción. El cliente debe proporcionar los datos en formato CSV/Excel o un dump SQL autorizado.
## Formato de CSVs
Colocar todos los archivos en `/root/migracion/datos/`.
### pacientes.csv
```csv
nombre,apellido_paterno,apellido_materno,telefono,email,fecha_nacimiento,sexo,medico_principal,etiquetas,vip,fuente
```
### servicios.csv
```csv
codigo,nombre,categoria,precio,duracion_min,descripcion,color,grupo,favorito,activo
```
### productos.csv
```csv
codigo,nombre,categoria,descripcion,precio,inventario_minimo,activo
```
### medicos.csv
```csv
nombre,apellido_paterno,apellido_materno,telefono,email,especialidad,rol
```
### citas.csv
```csv
paciente_telefono,paciente_nombre,servicio_codigo,fecha,hora,estado,doctor_nombre,sucursal,medio,notas,estado_pago,monto_pagado
```
- `estado`: pendiente, confirmada, llego, en_progreso, completada, cancelada, no_show
- `sucursal`: rosarito, tijuana
- `medio`: onsite, videollamada
- `estado_pago`: no_pagado, parcial, pagado
### ventas.csv
```csv
paciente_telefono,fecha,servicio_codigo,cantidad,precio_unitario,descuento,impuesto,estado,notas
```
### pagos.csv
```csv
paciente_telefono,cita_referencia,monto,metodo,estado,fecha_pago
```
- `metodo`: efectivo, tarjeta, transferencia, stripe, mercadopago
- `estado`: pendiente, procesando, completado, fallido, reembolsado
### adeudos.csv
```csv
telefono,concepto,monto,moneda,fecha_vencimiento
```
### monederos.csv
```csv
telefono,puntos,activo
```
## Ejecución
```bash
source /root/odoo-venv/bin/activate
python3 /root/migracion/importar_a_odoo.py
```
El script es **idempotente**: si vuelves a ejecutarlo, actualizará registros existentes (buscando por teléfono o código) en lugar de duplicarlos.
## Verificación
Después de la importación, revisa en:
- Odoo backend: `http://192.168.10.114:8069/web`
- Frontend: `http://192.168.10.114/`
## Proceso recomendado con datos reales
1. SKEEN exporta CSVs desde su sistema actual (o proporciona dump SQL).
2. Colocar CSVs en `/root/migracion/datos/`.
3. Ajustar columnas si es necesario (mapeo de nombres de campo).
4. Ejecutar script en entorno de prueba.
5. Validar datos con el cliente.
6. Ejecutar en producción durante ventana de mantenimiento.
7. Sincronizar WACRM con Odoo para que conversaciones se vinculen a pacientes.
## Archivos de ejemplo
En `/root/migracion/datos/` hay archivos CSV de demostración. Reemplazarlos con los datos reales antes de la migración oficial.

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Borra datos demo de Odoo antes de migración."""
import sys
from pathlib import Path
ODOO_PATH = Path('/root/skeen-odoo')
sys.path.insert(0, str(ODOO_PATH))
import odoo
from odoo import api, SUPERUSER_ID
odoo.tools.config.parse_config(['-c', '/root/skeen-odoo/odoo.conf'])
db = odoo.sql_db.db_connect('skeen_odoo')
with db.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
# Borrar en orden respetando dependencias
print('Borrando ventas...')
env['skeen.venta'].sudo().search([]).unlink()
print('Borrando pagos...')
env['skeen.pago'].sudo().search([]).unlink()
print('Borrando monederos...')
env['skeen.monedero'].sudo().search([]).unlink()
print('Borrando citas...')
env['skeen.cita'].sudo().search([]).unlink()
print('Borrando pacientes...')
env['res.partner'].sudo().search([('is_patient', '=', True)]).unlink()
print('Borrando servicios custom (excepto genéricos)...')
env['skeen.servicio'].sudo().search([('code', 'not in', ['GENERICO', 'ADEUDO'])]).unlink()
cr.commit()
print('Datos demo borrados.')

27
migracion/ejecutar_migracion.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Ejecuta la migración completa: extracción ya debe estar hecha.
set -e
cd /root/migracion
mkdir -p datos
echo "[1/6] Copiando CSVs extraídos..."
cp extraccion/pacientes.csv datos/
cp extraccion/servicios.csv datos/
cp extraccion/monederos.csv datos/
cp extraccion/citas.csv datos/
cp extraccion/ventas.csv datos/
echo "[2/6] Post-procesando citas (médicos)..."
/root/odoo-venv/bin/python /root/migracion/postprocesar_citas.py
echo "[3/6] Importando a Odoo..."
/root/odoo-venv/bin/python /root/migracion/importar_a_odoo.py
echo "[4/6] Sincronizando contactos a WACRM..."
/root/odoo-venv/bin/python /root/migracion/sync_odoo_to_wacrm.py
echo "[5/6] Refrescando cache WACRM en Odoo..."
curl -s -X POST http://localhost:8069/skeen/frontend/v1/wacrm/sync || true
echo "[6/6] Migración completada."

File diff suppressed because one or more lines are too long

46457
migracion/extraccion/citas.csv Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,857 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<meta name="description" content="">
<meta name="csrf-token" content="aqnBBFsD1z3cGfklS6J3LMwvO4NJFahNkuMduIyl"/>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width" name="viewport">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css">
<!-- Theme style -->
<link rel="stylesheet" href="https://sistema.skeenmx.app/vendor/css/AdminLTE-2.3.5/build/less/AdminLTE.css">
<link rel="stylesheet" href="https://sistema.skeenmx.app/vendor/css/panel.css?077"/>
<script src="https://sistema.skeenmx.app/vendor/css/AdminLTE-2.3.5/plugins/jQuery/jquery-2.2.3.min.js"></script>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>r
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body id="medicos_consulta"
class="hold-transition skin-theme sidebar-mini ">
<div class="wrapper">
<!-- Main Header -->
<header class="main-header">
<!-- Logo -->
<a href="https://sistema.skeenmx.app/dashboard" class="logo">
<!-- mini logo for sidebar mini 50x50 pixels -->
<span class="logo-mini">
<img class="img-responsive" src="https://sistema.skeenmx.app/images/icono_negro.png" alt="">
</span>
<!-- logo for regular state and mobile devices -->
<span class="logo-lg">
<img class="img-responsive" src="https://sistema.skeenmx.app/images/logo_skeen.png" alt=""> </span>
</a>
<!-- Header Navbar -->
<nav class="navbar navbar-static-top" role="navigation">
<!-- Sidebar toggle button-->
<a href="#" class="sidebar-toggle" data-toggle="offcanvas" role="button">
<span class="sr-only">Menú</span>
</a>
<div class="hidden">
<ul class="nav navbar-nav">
<!-- User Account Menu -->
<li class="dropdown">
<!-- Menu Toggle Button -->
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="fa-solid fa-headset"></i>
</a>
<ul class="dropdown-menu">
<li>
<a href="/crm/medio/1">
<i class="fa-solid fa-phone fa-2x"></i> Llamada
</a>
</li>
<li>
<a href="/crm/medio/2">
<i class="fa-brands fa-whatsapp fa-2x"></i> Whatsapp
</a>
</li>
<li>
<a href="/crm/medio/3">
<i class="fa-brands fa-facebook fa-2x"></i> Facebook
</a>
</li>
<li>
<a href="/crm/medio/4">
<i class="fa-brands fa-instagram fa-2x"></i> Instagram
</a>
</li>
<li>
<a href="/crm/medio/5">
<i class="fa-solid fa-envelope fa-2x"></i> Email
</a>
</li>
<li>
<a href="/crm/medio/6">
<i class="fa-solid fa-globe fa-2x"></i> Sitio web
</a>
</li>
</ul>
</li>
</ul>
</div>
<!-- Navbar Right Menu -->
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
<li>
<a href="#" class="">
TC: <span class="badge bg-black">16.50</span>
</a>
</li>
<li class="">
<a href="#" class="" data-toggle="modal"
data-target="#modal_valoracion_express">
<i class="fa fa-fw fa-bolt "></i> Valoración Express
</a>
</li>
<!-- User Account Menu -->
<li class="dropdown user user-menu">
<!-- Menu Toggle Button -->
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<!-- The user image in the navbar-->
<img src="https://sistema.skeenmx.app/images/profile.png" class="user-image" alt="User Image">
<!-- hidden-xs hides the username on small devices so only the image appears. -->
<span class="hidden-xs">Jos&eacute;</span>
</a>
<ul class="dropdown-menu">
<!-- The user image in the menu -->
<li class="user-header">
<img src="https://sistema.skeenmx.app/images/profile.png" class="img-circle" alt="User Image">
<p>
Jos&eacute;
<small></small>
</p>
</li>
<!-- Menu Body -->
<li class="user-body hidden">
<div class="row">
<div class="col-xs-4 text-center">
<a href="#">Followers</a>
</div>
<div class="col-xs-4 text-center">
<a href="#">Sales</a>
</div>
<div class="col-xs-4 text-center">
<a href="#">Friends</a>
</div>
</div>
<!-- /.row -->
</li>
<!-- Menu Footer-->
<li class="user-footer">
<div class="row row-no-padding">
<div class="col-xs-5">
<a href="https://sistema.skeenmx.app/perfil" class="btn btn-primary btn-flat btn-block"><i
class="fa fa-fw fa-user"></i> Perfil</a>
</div>
<div class="col-xs-1"></div>
<div class="col-xs-6 text-right">
<a href="https://sistema.skeenmx.app/logout" class="btn btn-default btn-flat">Cerrar sesión</a>
</div>
</div>
</li>
</ul>
</li>
<li class="dropdown user user-menu hidden">
<!-- Menu Toggle Button -->
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-trophy"></i>
</a>
<ul class="dropdown-menu">
<li class="text-center container-fluid">
<div>Meta Noviembre 2020</div>
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" style="width: 60%;">
<span class="sr-only">60% Complete</span>
</div>
</div>
<div>
600 USD / 1000 USD
</div>
</li>
</ul>
</li>
<li class="">
<a class="" href="#" data-toggle="modal" data-target="#modal_notas">
<i class="fa fa-sticky-note"></i>
</a>
</li>
<!-- Control Sidebar Toggle Button -->
<li class="">
<a class="total_no_leidas" href="#" data-toggle="control-sidebar">
<i class="fa fa-comments"></i>
</a>
</li>
<li><a href="https://sistema.skeenmx.app/consumibles/suministrar"><i class="fa fa-flask"></i></a></li>
<li class="">
<a class="" href="https://sistema.skeenmx.app/admin/sesiones/agregar">
<i class="fa fa-diamond"></i>
</a>
</li>
<li class="">
<a class="" href="https://sistema.skeenmx.app/recomendar">
<i class="fa fa-star"></i>
</a>
</li>
<li class="">
<a class="" href="https://sistema.skeenmx.app/admin/pedidos/agregar">
<i class="fa fa-shopping-cart"></i>
</a>
</li>
</ul>
</div>
</nav>
</header>
<!-- Left side column. contains the logo and sidebar -->
<aside class="main-sidebar">
<!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar">
<div>
<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane active " id="tab_menu_default">
<ul class="sidebar-menu">
<li class="header" style="color: #fff; font-size: 16px; text-align: center; font-weight: bold; border-bottom: solid 1px #666;">
Rosarito
</li>
<!-- Optionally, you can add icons to the links -->
<li><a href="https://sistema.skeenmx.app/dashboard"><i class="fa fa-home"></i> <span>Inicio</span></a></li>
<li><a href="https://sistema.skeenmx.app/agenda"><i class="fa-regular fa-fw fa-calendar"></i> <span>Agenda</span></a></li>
<li><a href="https://sistema.skeenmx.app/visitas"><i class="fa-solid fa-fw fa-users"></i> <span>Visitas</span></a></li>
<li><a href="https://sistema.skeenmx.app/expedientes"><i class="fa fa-folder-open"></i>
<span>Expedientes</span></a></li>
<li><a href="https://sistema.skeenmx.app/medicos"><i class="fa fa-user-md"></i>
<span>Médicos</span></a></li>
<li><a href="https://sistema.skeenmx.app/cosmetologos"><i class="fa fa-bed"></i>
<span>Cosmetolog&iacute;a</span></a></li>
<li class="hidden">
<a href="https://sistema.skeenmx.app/consultas" class="">
<i class="fa fa-fw fa-user-md"></i> <span>Consultas Médicas</span>
</a>
</li>
<li class="treeview">
<a href="#">
<i class="fa fa-cogs"></i> <span>Configuración</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/usuarios/consulta/activados"><i class="fa fa-user-md"></i> <span>Usuarios</span></a>
</li>
<li><a href="https://sistema.skeenmx.app/recetas"><i class="fa fa-list-alt"></i> <span>Recetas</span></a>
</li>
<li><a href="https://sistema.skeenmx.app/diagnosticos"><i class="fa fa-medkit"></i>
<span>Diagnósticos</span></a></li>
<li class="hidden"><a href="https://sistema.skeenmx.app/procedimientos"><i class="fa fa-stethoscope"></i> <span>Procedimientos médicos</span></a>
<li><a href="https://sistema.skeenmx.app/servicios"><i class="fa fa-star"></i> <span>Servicios</span></a>
<li><a href="https://sistema.skeenmx.app/etiquetas"><i class="fa fa-tags"></i> <span>Etiquetas</span></a>
<li><a href="https://sistema.skeenmx.app/productos"><i class="fa fa-flask"></i> <span>Productos</span></a>
</li>
<li><a href="https://sistema.skeenmx.app/inventarios"><i class="fa fa-cube"></i>
<span>Inventarios</span></a></li>
<li><a href="https://sistema.skeenmx.app/tipo_cambio"><i class="fa fa-usd"></i> <span>Tipo de cambio</span></a>
</li>
</ul>
</li>
<li class="treeview">
<a href="#">
<i class="fa fa-pie-chart"></i> <span>Reportes</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/reportes/diario"><i class="fa fa-calendar"></i>
<span>Mov. diario</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/medicos"><i class="fa fa-calendar"></i> <span>Mov. diario - Médicos</span></a>
</li>
<li><a href="https://sistema.skeenmx.app/reportes/recepcion"><i class="fa fa-calendar"></i> <span>Mov. diario - Recepción</span></a>
</li>
<li><a href="https://sistema.skeenmx.app/reportes/cortes"><i class="fa fa-book"></i> <span>Cortes de caja</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/ingresos"><i class="fa fa-line-chart"></i> <span>Ingresos</span></a></li>
<li class="hidden"><a href="#"><i class="fa fa-star-o"></i> <span>Especialidades</span></a></li>
<li class="hidden"><a href="https://sistema.skeenmx.app/reportes/comisiones"><i class="fa-solid fa-money-bill"></i>
<span>Comisiones</span></a></li>
<li class="hidden"><a href="#"><i class="fa fa-check"></i> <span>Ventas</span></a></li>
<li class="hidden"><a href="#"><i class="fa fa-line-chart"></i> <span>Estadisticas</span></a></li>
<li class=""><a href="https://sistema.skeenmx.app/correos " target="_blank"><i class="fa-solid fa-file-excel"></i> <span>Exportar Correos</span></a></li>
<li class=""><a href="https://sistema.skeenmx.app/correos/vip " target="_blank"><i class="fa-solid fa-file-excel"></i> <span>Exportar Correos ( VIP )</span></a></li>
<li class=""><a href="https://sistema.skeenmx.app/exportar/lista_precios " target="_blank"><i class="fa-solid fa-file-excel"></i> <span>Exportar lista de precios</span></a></li>
</ul>
</li>
<li class="treeview">
<a href="#">
<i class="fa fa-usd"></i> <span>Administrativo</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/reportes/adeudos"><i class="fa fa-credit-card"></i> <span>Adeudos clientes</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/pagos"><i class="fa-solid fa-money-bill"></i> <span>Pagos | Servicios</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/clientes"><i class="fa fa-folder-open"></i> <span>Pagos | Clientes</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/visitas"><i class="fa fa-ban"></i> <span>Visitas</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/devoluciones"><i class="fa fa-retweet"></i> <span>Devoluciones</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/vendedores"><i class="fa fa-shopping-basket"></i> <span>Vendedores</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/recomendaciones"><i class="fa fa-star"></i> <span>Recomendaciones</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/comisiones"><i class="fa fa-pie-chart"></i> <span>Comisiones</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/concentrado"><i class="fa fa-line-chart"></i> <span>Concentrado</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/top/clientes"><i class="fa fa-line-chart"></i> <span>Top ventas</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/agenda"><i class="fa fa-calendar"></i> <span>Horas agenda</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/paquetes"><i class="fa fa-box"></i> <span>Paquetes</span></a></li>
</ul>
</li>
<li class="treeview">
<a href="#">
<i class="fa-regular fa-id-card fa-fw"></i> <span>Monedero</span> <i class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/monederos/consulta"><i class="fa fa-id-card"></i> <span>Monederos</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/monedero"><i class="fa fa-line-chart"></i> <span>Reporte puntos</span></a></li>
<li><a href="https://sistema.skeenmx.app/configuracion/monedero"><i class="fa fa-cogs"></i> <span> Lista de precios</span></a></li>
</ul>
</li>
<li class="treeview">
<a href="#">
<i class="fa-regular fa-handshake fa-fw"></i> <span>Comunicación</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/reportes/cumple"><i class="fa fa-birthday-cake"></i> <span>Cumpleañeros</span></a></li>
</ul>
</li>
<li class=""><a href="https://sistema.skeenmx.app/comprobantes/ver"><i class="fa fa-ticket"></i> <span>Comprobantes del día</span></a></li>
<li class="hidden"><a href="https://sistema.skeenmx.app/comprobantes"><i class="fa fa-ticket"></i> <span>Comprobantes</span></a></li>
<li class="hidden"><a href="#"><i class="fa fa-envelope-o"></i> <span>Soporte</span></a></li>
<li class="treeview">
<a href="#">
<i class="fa fa-solid fa-flask"></i> <span>Consumibles</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/consumibles/inventario"><i class="fa fa-flask fa-solid"></i> <span>Inventario actual</span></a></li>
<li><a href="https://sistema.skeenmx.app/consumibles/ventas/consulta"><i class="fa fa-fw fa-ticket"></i> <span>Comprobantes</span></a></li>
<li><a href="https://sistema.skeenmx.app/consumibles/compras/consulta"><i class="fa-fw fa-solid fa-shopping-bag"></i> <span>Compras</span></a></li>
<li><a href="https://sistema.skeenmx.app/consumibles/bajas"><i class="fa-fw fa-solid fa-trash"></i> <span>Bajas</span></a></li>
<li><a href="https://sistema.skeenmx.app/consumibles/consulta"><i class="fa-fw fa-solid fa-cog"></i> <span>Consumibles</span></a></li>
</ul>
</li>
<li class="hidden treeview">
<a href="#">
<i class="fa fa-solid fa-handshake"></i> <span>Crm</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/crm"><i class="fa fa-solid fa-handshake"></i> <span>Inicio</span></a></li>
</ul>
</li>
<li><a href="https://sistema.skeenmx.app/admin/pedidos/dia"><i class="fa fa-shopping-basket"></i> <span>Ventas del día</span></a></li>
<li class="treeview hidden">
<a href="#">
<i class="fa fa-shopping-basket"></i> <span>Ventas</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li>
<a href="https://sistema.skeenmx.app/admin/pedidos/factura">Factura</a>
<a href="https://sistema.skeenmx.app/admin/pedidos/anticipo">Anticipo</a>
</li>
</ul>
</li>
<li><a href="https://sistema.skeenmx.app/admin/pedidos/clientes"><i class="fa fa-id-card"></i> <span>Clientes</span></a></li>
</ul>
<!-- /.sidebar-menu -->
</div>
<div role="tabpanel" class="hidden tab-pane " id="tab_menu_expediente">
<ul class="sidebar-menu">
<li>
<a id="btn-tab_menu_default" href="#tab_menu_default" aria-controls="tab_menu_default" role="tab" data-toggle="tab">
<i class="fa fa-bars"></i> <span>Menú principal</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</section>
<!-- /.sidebar -->
</aside>
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
<i class="fa fa-fw fa-user-md"></i> Médicos </h1>
<ol class="breadcrumb">
<li><a href="https://sistema.skeenmx.app/dashboard"><i class="fa fa-home"></i> Inicio</a></li>
<li>Médicos</li>
</ol>
</section>
<!-- Main content -->
<section class="content">
<!-- Your Page Content Here -->
<div id="container">
<div class="container">
<div class="form-group">
<a class="btn btn-success" href="https://sistema.skeenmx.app/medicos/nuevo">
<i class="fa fa-fw fa-plus"></i> Nuevo
</a>
</div>
<div class="">
<table id="tabla_consulta" class="table table-hover table-condensed" cellspacing="0" width="100%">
<thead>
<tr>
<th></th>
<th></th>
<th><i class="fa-fw fa-regular fa-calendar"></i></th>
<th>Nombre</th>
<th>Últimos 30 días</th>
<th style="width: 145px">Opciones</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="modal fade modal-theme" id="modal_valoracion_express" tabindex="-1" role="dialog"
aria-labelledby="modal_valoracion_expressLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Cerrar">
<span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel"><i class="fa fa-fw fa-bolt"></i> Valoración Express</h4>
</div>
<form method="POST" action="https://sistema.skeenmx.app/expedientes/express" accept-charset="UTF-8"><input name="_token" type="hidden" value="aqnBBFsD1z3cGfklS6J3LMwvO4NJFahNkuMduIyl">
<div class="modal-body">
<input name="status" type="hidden" value="0">
<div class="row">
<div class="col-sm-4">
<div class="form-group ">
<label for="input_apellido_paterno" class="control-label">Apellido paterno</label>
<div class="form-input has-feedback">
<input id="input_apellido_paterno" class="form-control" name="apellido_paterno" type="text">
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group ">
<label for="input_apellido_materno" class="control-label">Apellido materno</label>
<div class="form-input has-feedback">
<input id="input_apellido_materno" class="form-control" name="apellido_materno" type="text">
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group ">
<label for="input_nombre" class="control-label">Nombre</label>
<div class="form-input has-feedback">
<input id="input_nombre" class="form-control" name="nombre" type="text">
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div class="form-group ">
<label for="input_celular" class="control-label"><i class="fa fa-fw fa-mobile"></i> Celular</label>
<div class="form-input has-feedback">
<input id="input_celular" class="form-control" name="celular" type="text">
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group ">
<label for="input_whatsapp">Whatsapp</label>
<div class="form-input has-feedback">
<div class="btn-group" data-toggle="buttons">
<div class="btn btn-sm btn-primary">
<span class="selected"><i class="fa fa-fw fa-check-square"></i></span>
<span class="unselected"><i class="fa fa-fw fa-square"></i></span>
<input id="input_whatsapp" autocomplete="off" checked="checked" name="whatsapp" type="radio" value="1">
<i class="fa-fw fa-brands fa-whatsapp"></i> Whatsapp
</div>
</div>
</div>
</div>
</div>
</div>
<div id="submit_bar">
<div class="row">
<div class="col-xs-8 col-sm-4 col-sm-offset-4">
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="row">
<div class="col-sm-4 col-sm-offset-4">
<button class="btn btn-primary btn-block" type="submit">
<i class="fa fa-fw fa-check"></i> Enviar
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
<!-- Main Footer -->
<footer class="main-footer">
<!-- To the right -->
<div class="pull-right hidden-xs">
</div>
<!-- Default to the left -->
<strong>
<a target="_blank" href="https://sistema.skeenmx.app">
SKEEN
</a>
</strong>
</footer>
<!-- Control Sidebar -->
<aside class="control-sidebar control-sidebar-light">
<div id="chat_usuarios" class="container-fluid">
<div class="usuario row hidden " data-id="1" data-nombre="corporativo"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">corporativo</div>
<div class="departamento">Administrador</div>
</div>
</div>
<div class="usuario row hidden " data-id="2" data-nombre="soporte prographik"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">soporte prographik</div>
<div class="departamento">Administrador</div>
</div>
</div>
<div class="usuario row hidden " data-id="30" data-nombre="Ramos Alejandra"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Ramos Alejandra</div>
<div class="departamento">Administrador</div>
</div>
</div>
<div class="usuario row hidden " data-id="31" data-nombre="Ramirez Nev&aacute;res Patricia"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Ramirez Nev&aacute;res Patricia</div>
<div class="departamento">Administrador</div>
</div>
</div>
<div class="usuario row hidden " data-id="32" data-nombre="Ramos Villasaldo Dra. Alejandra"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Ramos Villasaldo Dra. Alejandra</div>
<div class="departamento">M&eacute;dico</div>
</div>
</div>
<div class="usuario row hidden " data-id="33" data-nombre="Mart&iacute;nez Hern&aacute;ndez Dra. Lidia Danira"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Mart&iacute;nez Hern&aacute;ndez Dra. Lidia Danira</div>
<div class="departamento">M&eacute;dico</div>
</div>
</div>
<div class="usuario row hidden " data-id="41" data-nombre="Lara Maga&ntilde;a Luisabelle"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Lara Maga&ntilde;a Luisabelle</div>
<div class="departamento">Recepci&oacute;n</div>
</div>
</div>
<div class="usuario row hidden " data-id="46" data-nombre="Dra. Fernanda Cerecer"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Dra. Fernanda Cerecer</div>
<div class="departamento">M&eacute;dico</div>
</div>
</div>
<div class="usuario row hidden " data-id="47" data-nombre="SKEEN RECEPCION"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">SKEEN RECEPCION</div>
<div class="departamento">Recepci&oacute;n</div>
</div>
</div>
<div class="usuario row hidden " data-id="49" data-nombre="RODRIGUEZ FRIDA"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">RODRIGUEZ FRIDA</div>
<div class="departamento">Cosmetolog&iacute;a</div>
</div>
</div>
<div class="usuario row hidden " data-id="50" data-nombre="xx RECEPCION1"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">xx RECEPCION1</div>
<div class="departamento">Recepci&oacute;n</div>
</div>
</div>
<div class="usuario row hidden " data-id="54" data-nombre="Vega Guzm&aacute;n Dr. Benjam&iacute;n Adri&aacute;n"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Vega Guzm&aacute;n Dr. Benjam&iacute;n Adri&aacute;n</div>
<div class="departamento">M&eacute;dico</div>
</div>
</div>
<div class="usuario row hidden " data-id="55" data-nombre=". Tablet"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">. Tablet</div>
<div class="departamento">Tablet comprobantes</div>
</div>
</div>
<div class="usuario row hidden " data-id="80" data-nombre="CHABOLLA ELIZABETH"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">CHABOLLA ELIZABETH</div>
<div class="departamento">Cosmetolog&iacute;a</div>
</div>
</div>
<div class="usuario row hidden " data-id="79" data-nombre="VERDUGO XIMENA"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">VERDUGO XIMENA</div>
<div class="departamento">Cosmetolog&iacute;a</div>
</div>
</div>
<div class="usuario row hidden " data-id="72" data-nombre="GUERREO JASMIN"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">GUERREO JASMIN</div>
<div class="departamento">Recepci&oacute;n</div>
</div>
</div>
</div>
</aside>
<!-- /.control-sidebar -->
<!-- Add the sidebar's background. This div must be placed
immediately after the control sidebar -->
<div class="control-sidebar-bg"></div>
</div>
<!-- ./wrapper -->
<div id="chat_conversaciones"></div>
<div class="modal fade modal-secondary" id="modal_notas" tabindex="-1" role="dialog"
aria-labelledby="modal_notas_label">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Cerrar"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="modal_notas_label"><i class="fa fa-fw fa-sticky-note"></i> Notas</h4>
</div>
<div class="formulario_notas_wrap container-fluid">
<form method="POST" action="https://sistema.skeenmx.app" accept-charset="UTF-8" id="formulario_notas"><input name="_token" type="hidden" value="aqnBBFsD1z3cGfklS6J3LMwvO4NJFahNkuMduIyl">
<div class="row">
<div class="col-sm-9">
<div class="form-group">
<div class="form-input has-feedback">
<textarea id="input_nota" class="form-control" rows="2" placeholder="Contenido de la nota:" name="nota" cols="50"></textarea>
</div>
</div>
</div>
<div class="col-sm-3">
<button class="btn btn-lg btn-primary btn-block" type="submit">
<i class="fa fa-fw fa-plus"></i> Agregar nota
</button>
</div>
</div>
</form>
</div>
<div class="modal-body">
<div class="">
<table id="tabla_notas" class="" cellspacing="0" width="100%">
<thead>
<tr>
<th>ID</th>
<th><i class="fa fa-fw fa-circle"></i> Ordenar por status</th>
<th>Ordenar por fecha</th>
<th></th>
<th>Ordenar por contenido</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
<script id="chat_conversation_template" type="text/template">
<!-- Construct the box with style you want. Here we are using box-danger -->
<!-- Then add the class direct-chat and choose the direct-chat-* contexual class -->
<!-- The contextual class should match the box, so we are using direct-chat-danger -->
<div class="box box-danger direct-chat direct-chat-danger">
<div class="box-header with-border">
<h3 class="box-title" data-widget="collapse">
<img src="" alt="" class="avatar img-responsive img-circle">
<span class="name"></span>
</h3>
<div class="box-tools pull-right">
<span data-toggle="tooltip" title="" class="badge bg-red"></span>
<button class="btn btn-sm btn-box-tool" data-widget="remove"><i class="fa fa-times"></i></button>
</div>
</div><!-- /.box-header -->
<div class="box-body">
<!-- Conversations are loaded here -->
<div class="direct-chat-messages">
</div><!--/.direct-chat-messages-->
</div><!-- /.box-body -->
<div class="box-footer">
<div class="input-group">
<input type="text" name="message" placeholder="Escribe tu mensaje ..."
class="form-control input-message">
<span class="input-group-btn btn-danger">
<button type="button" class="send-message btn btn-danger btn-flat"><i class="fa-fw fa-solid fa-check"></i></button>
</span>
</div>
</div><!-- /.box-footer-->
</div><!--/.direct-chat -->
</script>
<script id="chat_message_template" type="text/template">
<div class="direct-chat-msg" data-id="">
<div class="direct-chat-info clearfix">
<span class="direct-chat-name pull-left"></span>
<span class="direct-chat-timestamp pull-right"></span>
</div><!-- /.direct-chat-info -->
<div class="direct-chat-text"></div><!-- /.direct-chat-text -->
</div><!-- /.direct-chat-msg -->
</script>
<script type="text/javascript">
var myself_id =78;
</script>
<!-- REQUIRED JS SCRIPTS -->
<script type="text/javascript" src="https://sistema.skeenmx.app/vendor/js/moment/moment-with-locales.min.js?001"></script>
<script src='https://cdn.rawgit.com/admsev/jquery-play-sound/master/jquery.playSound.js'></script>
<script src="https://sistema.skeenmx.app/vendor/js/panel.js?015" type="text/javascript"></script>
<!-- AdminLTE App -->
<script src="https://sistema.skeenmx.app/vendor/css/AdminLTE-2.3.5/dist/js/app.min.js"></script>
<!-- Optionally, you can add Slimscroll and FastClick plugins.
Both of these plugins are recommended to enhance the
user experience. Slimscroll is required when using the
fixed layout. -->
<script src="https://sistema.skeenmx.app/vendor/js/DataTables/datatables.min.js" type="text/javascript"></script>
<script type="text/javascript" src="https://sistema.skeenmx.app/vendor/js/moment/moment-with-locales.min.js"></script>
<script src="https://sistema.skeenmx.app/vendor/js/DataTables/datatables.min.js" type="text/javascript"></script>
<script>
var datatable_json_data =[{"id":0,"created_at":"0000-00-00 00:00:00","status":0,"nombre":"CLINICA","total_recomendaciones":0,"total_servicios":152,"total_pagado_usd":974.29,"total_pagado_mn":45887,"total_pagado_tarjeta_mn":56212.87},{"id":32,"created_at":"2022-11-04 07:22:08","status":null,"nombre":"Ramos Villasaldo Dra. Alejandra","total_recomendaciones":0,"total_servicios":1393,"total_pagado_usd":727.88,"total_pagado_mn":34385,"total_pagado_tarjeta_mn":267939},{"id":33,"created_at":"2022-11-04 07:22:08","status":null,"nombre":"Mart\u00ednez Hern\u00e1ndez Dra. Lidia Danira","total_recomendaciones":0,"total_servicios":894,"total_pagado_usd":811.22,"total_pagado_mn":42400,"total_pagado_tarjeta_mn":131270},{"id":46,"created_at":"2023-09-06 14:53:25","status":null,"nombre":"Dra. Fernanda Cerecer","total_recomendaciones":0,"total_servicios":2,"total_pagado_usd":0,"total_pagado_mn":1600,"total_pagado_tarjeta_mn":1500},{"id":54,"created_at":"2024-04-02 13:11:42","status":null,"nombre":"Vega Guzm\u00e1n Dr. Benjam\u00edn Adri\u00e1n","total_recomendaciones":0,"total_servicios":1272,"total_pagado_usd":3165.2,"total_pagado_mn":89176,"total_pagado_tarjeta_mn":234349}];
var datatable_domain_url = "https://sistema.skeenmx.app/";
var datatable_user_depto = 1
</script>
<script src="https://sistema.skeenmx.app/vendor/js/data/datatable_medicos.js?004" type="text/javascript"></script>
<script>
var notas_datatable_json_data =[];
var notas_datatable_domain_url = "https://sistema.skeenmx.app/";
</script>
<script src="https://sistema.skeenmx.app/vendor/js/data/datatable_notas.js?000" type="text/javascript"></script>
<script type="text/javascript">
$('#btn-tab_menu_default').on('shown.bs.tab', function (e) {
$('#btn-tab_menu_expediente').parent().removeClass("active");
});
$('#btn-tab_menu_expediente').on('shown.bs.tab', function (e) {
$('#btn-tab_menu_default').parent().removeClass("active");
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,845 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<meta name="description" content="">
<meta name="csrf-token" content="aqnBBFsD1z3cGfklS6J3LMwvO4NJFahNkuMduIyl"/>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width" name="viewport">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css">
<!-- Theme style -->
<link rel="stylesheet" href="https://sistema.skeenmx.app/vendor/css/AdminLTE-2.3.5/build/less/AdminLTE.css">
<link rel="stylesheet" href="https://sistema.skeenmx.app/vendor/css/panel.css?077"/>
<script src="https://sistema.skeenmx.app/vendor/css/AdminLTE-2.3.5/plugins/jQuery/jquery-2.2.3.min.js"></script>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>r
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body id="monederos_consulta"
class="hold-transition skin-theme sidebar-mini ">
<div class="wrapper">
<!-- Main Header -->
<header class="main-header">
<!-- Logo -->
<a href="https://sistema.skeenmx.app/dashboard" class="logo">
<!-- mini logo for sidebar mini 50x50 pixels -->
<span class="logo-mini">
<img class="img-responsive" src="https://sistema.skeenmx.app/images/icono_negro.png" alt="">
</span>
<!-- logo for regular state and mobile devices -->
<span class="logo-lg">
<img class="img-responsive" src="https://sistema.skeenmx.app/images/logo_skeen.png" alt=""> </span>
</a>
<!-- Header Navbar -->
<nav class="navbar navbar-static-top" role="navigation">
<!-- Sidebar toggle button-->
<a href="#" class="sidebar-toggle" data-toggle="offcanvas" role="button">
<span class="sr-only">Menú</span>
</a>
<div class="hidden">
<ul class="nav navbar-nav">
<!-- User Account Menu -->
<li class="dropdown">
<!-- Menu Toggle Button -->
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="fa-solid fa-headset"></i>
</a>
<ul class="dropdown-menu">
<li>
<a href="/crm/medio/1">
<i class="fa-solid fa-phone fa-2x"></i> Llamada
</a>
</li>
<li>
<a href="/crm/medio/2">
<i class="fa-brands fa-whatsapp fa-2x"></i> Whatsapp
</a>
</li>
<li>
<a href="/crm/medio/3">
<i class="fa-brands fa-facebook fa-2x"></i> Facebook
</a>
</li>
<li>
<a href="/crm/medio/4">
<i class="fa-brands fa-instagram fa-2x"></i> Instagram
</a>
</li>
<li>
<a href="/crm/medio/5">
<i class="fa-solid fa-envelope fa-2x"></i> Email
</a>
</li>
<li>
<a href="/crm/medio/6">
<i class="fa-solid fa-globe fa-2x"></i> Sitio web
</a>
</li>
</ul>
</li>
</ul>
</div>
<!-- Navbar Right Menu -->
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
<li>
<a href="#" class="">
TC: <span class="badge bg-black">16.50</span>
</a>
</li>
<li class="">
<a href="#" class="" data-toggle="modal"
data-target="#modal_valoracion_express">
<i class="fa fa-fw fa-bolt "></i> Valoración Express
</a>
</li>
<!-- User Account Menu -->
<li class="dropdown user user-menu">
<!-- Menu Toggle Button -->
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<!-- The user image in the navbar-->
<img src="https://sistema.skeenmx.app/images/profile.png" class="user-image" alt="User Image">
<!-- hidden-xs hides the username on small devices so only the image appears. -->
<span class="hidden-xs">Jos&eacute;</span>
</a>
<ul class="dropdown-menu">
<!-- The user image in the menu -->
<li class="user-header">
<img src="https://sistema.skeenmx.app/images/profile.png" class="img-circle" alt="User Image">
<p>
Jos&eacute;
<small></small>
</p>
</li>
<!-- Menu Body -->
<li class="user-body hidden">
<div class="row">
<div class="col-xs-4 text-center">
<a href="#">Followers</a>
</div>
<div class="col-xs-4 text-center">
<a href="#">Sales</a>
</div>
<div class="col-xs-4 text-center">
<a href="#">Friends</a>
</div>
</div>
<!-- /.row -->
</li>
<!-- Menu Footer-->
<li class="user-footer">
<div class="row row-no-padding">
<div class="col-xs-5">
<a href="https://sistema.skeenmx.app/perfil" class="btn btn-primary btn-flat btn-block"><i
class="fa fa-fw fa-user"></i> Perfil</a>
</div>
<div class="col-xs-1"></div>
<div class="col-xs-6 text-right">
<a href="https://sistema.skeenmx.app/logout" class="btn btn-default btn-flat">Cerrar sesión</a>
</div>
</div>
</li>
</ul>
</li>
<li class="dropdown user user-menu hidden">
<!-- Menu Toggle Button -->
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-trophy"></i>
</a>
<ul class="dropdown-menu">
<li class="text-center container-fluid">
<div>Meta Noviembre 2020</div>
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" style="width: 60%;">
<span class="sr-only">60% Complete</span>
</div>
</div>
<div>
600 USD / 1000 USD
</div>
</li>
</ul>
</li>
<li class="">
<a class="" href="#" data-toggle="modal" data-target="#modal_notas">
<i class="fa fa-sticky-note"></i>
</a>
</li>
<!-- Control Sidebar Toggle Button -->
<li class="">
<a class="total_no_leidas" href="#" data-toggle="control-sidebar">
<i class="fa fa-comments"></i>
</a>
</li>
<li><a href="https://sistema.skeenmx.app/consumibles/suministrar"><i class="fa fa-flask"></i></a></li>
<li class="">
<a class="" href="https://sistema.skeenmx.app/admin/sesiones/agregar">
<i class="fa fa-diamond"></i>
</a>
</li>
<li class="">
<a class="" href="https://sistema.skeenmx.app/recomendar">
<i class="fa fa-star"></i>
</a>
</li>
<li class="">
<a class="" href="https://sistema.skeenmx.app/admin/pedidos/agregar">
<i class="fa fa-shopping-cart"></i>
</a>
</li>
</ul>
</div>
</nav>
</header>
<!-- Left side column. contains the logo and sidebar -->
<aside class="main-sidebar">
<!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar">
<div>
<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane active " id="tab_menu_default">
<ul class="sidebar-menu">
<li class="header" style="color: #fff; font-size: 16px; text-align: center; font-weight: bold; border-bottom: solid 1px #666;">
Rosarito
</li>
<!-- Optionally, you can add icons to the links -->
<li><a href="https://sistema.skeenmx.app/dashboard"><i class="fa fa-home"></i> <span>Inicio</span></a></li>
<li><a href="https://sistema.skeenmx.app/agenda"><i class="fa-regular fa-fw fa-calendar"></i> <span>Agenda</span></a></li>
<li><a href="https://sistema.skeenmx.app/visitas"><i class="fa-solid fa-fw fa-users"></i> <span>Visitas</span></a></li>
<li><a href="https://sistema.skeenmx.app/expedientes"><i class="fa fa-folder-open"></i>
<span>Expedientes</span></a></li>
<li><a href="https://sistema.skeenmx.app/medicos"><i class="fa fa-user-md"></i>
<span>Médicos</span></a></li>
<li><a href="https://sistema.skeenmx.app/cosmetologos"><i class="fa fa-bed"></i>
<span>Cosmetolog&iacute;a</span></a></li>
<li class="hidden">
<a href="https://sistema.skeenmx.app/consultas" class="">
<i class="fa fa-fw fa-user-md"></i> <span>Consultas Médicas</span>
</a>
</li>
<li class="treeview">
<a href="#">
<i class="fa fa-cogs"></i> <span>Configuración</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/usuarios/consulta/activados"><i class="fa fa-user-md"></i> <span>Usuarios</span></a>
</li>
<li><a href="https://sistema.skeenmx.app/recetas"><i class="fa fa-list-alt"></i> <span>Recetas</span></a>
</li>
<li><a href="https://sistema.skeenmx.app/diagnosticos"><i class="fa fa-medkit"></i>
<span>Diagnósticos</span></a></li>
<li class="hidden"><a href="https://sistema.skeenmx.app/procedimientos"><i class="fa fa-stethoscope"></i> <span>Procedimientos médicos</span></a>
<li><a href="https://sistema.skeenmx.app/servicios"><i class="fa fa-star"></i> <span>Servicios</span></a>
<li><a href="https://sistema.skeenmx.app/etiquetas"><i class="fa fa-tags"></i> <span>Etiquetas</span></a>
<li><a href="https://sistema.skeenmx.app/productos"><i class="fa fa-flask"></i> <span>Productos</span></a>
</li>
<li><a href="https://sistema.skeenmx.app/inventarios"><i class="fa fa-cube"></i>
<span>Inventarios</span></a></li>
<li><a href="https://sistema.skeenmx.app/tipo_cambio"><i class="fa fa-usd"></i> <span>Tipo de cambio</span></a>
</li>
</ul>
</li>
<li class="treeview">
<a href="#">
<i class="fa fa-pie-chart"></i> <span>Reportes</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/reportes/diario"><i class="fa fa-calendar"></i>
<span>Mov. diario</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/medicos"><i class="fa fa-calendar"></i> <span>Mov. diario - Médicos</span></a>
</li>
<li><a href="https://sistema.skeenmx.app/reportes/recepcion"><i class="fa fa-calendar"></i> <span>Mov. diario - Recepción</span></a>
</li>
<li><a href="https://sistema.skeenmx.app/reportes/cortes"><i class="fa fa-book"></i> <span>Cortes de caja</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/ingresos"><i class="fa fa-line-chart"></i> <span>Ingresos</span></a></li>
<li class="hidden"><a href="#"><i class="fa fa-star-o"></i> <span>Especialidades</span></a></li>
<li class="hidden"><a href="https://sistema.skeenmx.app/reportes/comisiones"><i class="fa-solid fa-money-bill"></i>
<span>Comisiones</span></a></li>
<li class="hidden"><a href="#"><i class="fa fa-check"></i> <span>Ventas</span></a></li>
<li class="hidden"><a href="#"><i class="fa fa-line-chart"></i> <span>Estadisticas</span></a></li>
<li class=""><a href="https://sistema.skeenmx.app/correos " target="_blank"><i class="fa-solid fa-file-excel"></i> <span>Exportar Correos</span></a></li>
<li class=""><a href="https://sistema.skeenmx.app/correos/vip " target="_blank"><i class="fa-solid fa-file-excel"></i> <span>Exportar Correos ( VIP )</span></a></li>
<li class=""><a href="https://sistema.skeenmx.app/exportar/lista_precios " target="_blank"><i class="fa-solid fa-file-excel"></i> <span>Exportar lista de precios</span></a></li>
</ul>
</li>
<li class="treeview">
<a href="#">
<i class="fa fa-usd"></i> <span>Administrativo</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/reportes/adeudos"><i class="fa fa-credit-card"></i> <span>Adeudos clientes</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/pagos"><i class="fa-solid fa-money-bill"></i> <span>Pagos | Servicios</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/clientes"><i class="fa fa-folder-open"></i> <span>Pagos | Clientes</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/visitas"><i class="fa fa-ban"></i> <span>Visitas</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/devoluciones"><i class="fa fa-retweet"></i> <span>Devoluciones</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/vendedores"><i class="fa fa-shopping-basket"></i> <span>Vendedores</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/recomendaciones"><i class="fa fa-star"></i> <span>Recomendaciones</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/comisiones"><i class="fa fa-pie-chart"></i> <span>Comisiones</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/concentrado"><i class="fa fa-line-chart"></i> <span>Concentrado</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/top/clientes"><i class="fa fa-line-chart"></i> <span>Top ventas</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/agenda"><i class="fa fa-calendar"></i> <span>Horas agenda</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/paquetes"><i class="fa fa-box"></i> <span>Paquetes</span></a></li>
</ul>
</li>
<li class="treeview">
<a href="#">
<i class="fa-regular fa-id-card fa-fw"></i> <span>Monedero</span> <i class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/monederos/consulta"><i class="fa fa-id-card"></i> <span>Monederos</span></a></li>
<li><a href="https://sistema.skeenmx.app/reportes/monedero"><i class="fa fa-line-chart"></i> <span>Reporte puntos</span></a></li>
<li><a href="https://sistema.skeenmx.app/configuracion/monedero"><i class="fa fa-cogs"></i> <span> Lista de precios</span></a></li>
</ul>
</li>
<li class="treeview">
<a href="#">
<i class="fa-regular fa-handshake fa-fw"></i> <span>Comunicación</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/reportes/cumple"><i class="fa fa-birthday-cake"></i> <span>Cumpleañeros</span></a></li>
</ul>
</li>
<li class=""><a href="https://sistema.skeenmx.app/comprobantes/ver"><i class="fa fa-ticket"></i> <span>Comprobantes del día</span></a></li>
<li class="hidden"><a href="https://sistema.skeenmx.app/comprobantes"><i class="fa fa-ticket"></i> <span>Comprobantes</span></a></li>
<li class="hidden"><a href="#"><i class="fa fa-envelope-o"></i> <span>Soporte</span></a></li>
<li class="treeview">
<a href="#">
<i class="fa fa-solid fa-flask"></i> <span>Consumibles</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/consumibles/inventario"><i class="fa fa-flask fa-solid"></i> <span>Inventario actual</span></a></li>
<li><a href="https://sistema.skeenmx.app/consumibles/ventas/consulta"><i class="fa fa-fw fa-ticket"></i> <span>Comprobantes</span></a></li>
<li><a href="https://sistema.skeenmx.app/consumibles/compras/consulta"><i class="fa-fw fa-solid fa-shopping-bag"></i> <span>Compras</span></a></li>
<li><a href="https://sistema.skeenmx.app/consumibles/bajas"><i class="fa-fw fa-solid fa-trash"></i> <span>Bajas</span></a></li>
<li><a href="https://sistema.skeenmx.app/consumibles/consulta"><i class="fa-fw fa-solid fa-cog"></i> <span>Consumibles</span></a></li>
</ul>
</li>
<li class="hidden treeview">
<a href="#">
<i class="fa fa-solid fa-handshake"></i> <span>Crm</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li><a href="https://sistema.skeenmx.app/crm"><i class="fa fa-solid fa-handshake"></i> <span>Inicio</span></a></li>
</ul>
</li>
<li><a href="https://sistema.skeenmx.app/admin/pedidos/dia"><i class="fa fa-shopping-basket"></i> <span>Ventas del día</span></a></li>
<li class="treeview hidden">
<a href="#">
<i class="fa fa-shopping-basket"></i> <span>Ventas</span> <i
class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
<li>
<a href="https://sistema.skeenmx.app/admin/pedidos/factura">Factura</a>
<a href="https://sistema.skeenmx.app/admin/pedidos/anticipo">Anticipo</a>
</li>
</ul>
</li>
<li><a href="https://sistema.skeenmx.app/admin/pedidos/clientes"><i class="fa fa-id-card"></i> <span>Clientes</span></a></li>
</ul>
<!-- /.sidebar-menu -->
</div>
<div role="tabpanel" class="hidden tab-pane " id="tab_menu_expediente">
<ul class="sidebar-menu">
<li>
<a id="btn-tab_menu_default" href="#tab_menu_default" aria-controls="tab_menu_default" role="tab" data-toggle="tab">
<i class="fa fa-bars"></i> <span>Menú principal</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</section>
<!-- /.sidebar -->
</aside>
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
<i class="fa fa-fw fa-id-card"></i> Monederos electrónicos </h1>
<ol class="breadcrumb">
<li><a href="https://sistema.skeenmx.app/dashboard"><i class="fa fa-home"></i> Inicio</a></li>
</ol>
</section>
<!-- Main content -->
<section class="content">
<!-- Your Page Content Here -->
<div id="container">
<div class="container">
<div id="totales"></div>
<div class="">
<table id="tabla_consulta" class="table table-hover table-condensed" cellspacing="0" width="100%">
<thead>
<tr>
<th>ID</th>
<th><i class="fa fa-fw fa-calendar-o"></i> Fecha activación</th>
<th>Nombre</th>
<th><i class="fa fa-fw fa-mobile"></i></th>
<th>Saldo</th>
<th><i class="fa fa-fw fa-toggle-on"></i> Activado</th>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="modal fade modal-theme" id="modal_valoracion_express" tabindex="-1" role="dialog"
aria-labelledby="modal_valoracion_expressLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Cerrar">
<span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel"><i class="fa fa-fw fa-bolt"></i> Valoración Express</h4>
</div>
<form method="POST" action="https://sistema.skeenmx.app/expedientes/express" accept-charset="UTF-8"><input name="_token" type="hidden" value="aqnBBFsD1z3cGfklS6J3LMwvO4NJFahNkuMduIyl">
<div class="modal-body">
<input name="status" type="hidden" value="0">
<div class="row">
<div class="col-sm-4">
<div class="form-group ">
<label for="input_apellido_paterno" class="control-label">Apellido paterno</label>
<div class="form-input has-feedback">
<input id="input_apellido_paterno" class="form-control" name="apellido_paterno" type="text">
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group ">
<label for="input_apellido_materno" class="control-label">Apellido materno</label>
<div class="form-input has-feedback">
<input id="input_apellido_materno" class="form-control" name="apellido_materno" type="text">
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group ">
<label for="input_nombre" class="control-label">Nombre</label>
<div class="form-input has-feedback">
<input id="input_nombre" class="form-control" name="nombre" type="text">
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div class="form-group ">
<label for="input_celular" class="control-label"><i class="fa fa-fw fa-mobile"></i> Celular</label>
<div class="form-input has-feedback">
<input id="input_celular" class="form-control" name="celular" type="text">
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group ">
<label for="input_whatsapp">Whatsapp</label>
<div class="form-input has-feedback">
<div class="btn-group" data-toggle="buttons">
<div class="btn btn-sm btn-primary">
<span class="selected"><i class="fa fa-fw fa-check-square"></i></span>
<span class="unselected"><i class="fa fa-fw fa-square"></i></span>
<input id="input_whatsapp" autocomplete="off" checked="checked" name="whatsapp" type="radio" value="1">
<i class="fa-fw fa-brands fa-whatsapp"></i> Whatsapp
</div>
</div>
</div>
</div>
</div>
</div>
<div id="submit_bar">
<div class="row">
<div class="col-xs-8 col-sm-4 col-sm-offset-4">
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="row">
<div class="col-sm-4 col-sm-offset-4">
<button class="btn btn-primary btn-block" type="submit">
<i class="fa fa-fw fa-check"></i> Enviar
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
<!-- Main Footer -->
<footer class="main-footer">
<!-- To the right -->
<div class="pull-right hidden-xs">
</div>
<!-- Default to the left -->
<strong>
<a target="_blank" href="https://sistema.skeenmx.app">
SKEEN
</a>
</strong>
</footer>
<!-- Control Sidebar -->
<aside class="control-sidebar control-sidebar-light">
<div id="chat_usuarios" class="container-fluid">
<div class="usuario row hidden " data-id="1" data-nombre="corporativo"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">corporativo</div>
<div class="departamento">Administrador</div>
</div>
</div>
<div class="usuario row hidden " data-id="2" data-nombre="soporte prographik"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">soporte prographik</div>
<div class="departamento">Administrador</div>
</div>
</div>
<div class="usuario row hidden " data-id="30" data-nombre="Ramos Alejandra"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Ramos Alejandra</div>
<div class="departamento">Administrador</div>
</div>
</div>
<div class="usuario row hidden " data-id="31" data-nombre="Ramirez Nev&aacute;res Patricia"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Ramirez Nev&aacute;res Patricia</div>
<div class="departamento">Administrador</div>
</div>
</div>
<div class="usuario row hidden " data-id="32" data-nombre="Ramos Villasaldo Dra. Alejandra"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Ramos Villasaldo Dra. Alejandra</div>
<div class="departamento">M&eacute;dico</div>
</div>
</div>
<div class="usuario row hidden " data-id="33" data-nombre="Mart&iacute;nez Hern&aacute;ndez Dra. Lidia Danira"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Mart&iacute;nez Hern&aacute;ndez Dra. Lidia Danira</div>
<div class="departamento">M&eacute;dico</div>
</div>
</div>
<div class="usuario row hidden " data-id="41" data-nombre="Lara Maga&ntilde;a Luisabelle"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Lara Maga&ntilde;a Luisabelle</div>
<div class="departamento">Recepci&oacute;n</div>
</div>
</div>
<div class="usuario row hidden " data-id="46" data-nombre="Dra. Fernanda Cerecer"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Dra. Fernanda Cerecer</div>
<div class="departamento">M&eacute;dico</div>
</div>
</div>
<div class="usuario row hidden " data-id="47" data-nombre="SKEEN RECEPCION"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">SKEEN RECEPCION</div>
<div class="departamento">Recepci&oacute;n</div>
</div>
</div>
<div class="usuario row hidden " data-id="49" data-nombre="RODRIGUEZ FRIDA"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">RODRIGUEZ FRIDA</div>
<div class="departamento">Cosmetolog&iacute;a</div>
</div>
</div>
<div class="usuario row hidden " data-id="50" data-nombre="xx RECEPCION1"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">xx RECEPCION1</div>
<div class="departamento">Recepci&oacute;n</div>
</div>
</div>
<div class="usuario row hidden " data-id="54" data-nombre="Vega Guzm&aacute;n Dr. Benjam&iacute;n Adri&aacute;n"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">Vega Guzm&aacute;n Dr. Benjam&iacute;n Adri&aacute;n</div>
<div class="departamento">M&eacute;dico</div>
</div>
</div>
<div class="usuario row hidden " data-id="55" data-nombre=". Tablet"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">. Tablet</div>
<div class="departamento">Tablet comprobantes</div>
</div>
</div>
<div class="usuario row hidden " data-id="80" data-nombre="CHABOLLA ELIZABETH"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">CHABOLLA ELIZABETH</div>
<div class="departamento">Cosmetolog&iacute;a</div>
</div>
</div>
<div class="usuario row hidden " data-id="79" data-nombre="VERDUGO XIMENA"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">VERDUGO XIMENA</div>
<div class="departamento">Cosmetolog&iacute;a</div>
</div>
</div>
<div class="usuario row hidden " data-id="72" data-nombre="GUERREO JASMIN"
data-avatar="https://sistema.skeenmx.app/images/profile.png"
data-no_leidas="0">
<div class="col-xs-3 avatar">
<img src="https://sistema.skeenmx.app/images/profile.png" alt="" class="img-responsive img-circle">
</div>
<div class="col-xs-9">
<div class="nombre">GUERREO JASMIN</div>
<div class="departamento">Recepci&oacute;n</div>
</div>
</div>
</div>
</aside>
<!-- /.control-sidebar -->
<!-- Add the sidebar's background. This div must be placed
immediately after the control sidebar -->
<div class="control-sidebar-bg"></div>
</div>
<!-- ./wrapper -->
<div id="chat_conversaciones"></div>
<div class="modal fade modal-secondary" id="modal_notas" tabindex="-1" role="dialog"
aria-labelledby="modal_notas_label">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Cerrar"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="modal_notas_label"><i class="fa fa-fw fa-sticky-note"></i> Notas</h4>
</div>
<div class="formulario_notas_wrap container-fluid">
<form method="POST" action="https://sistema.skeenmx.app" accept-charset="UTF-8" id="formulario_notas"><input name="_token" type="hidden" value="aqnBBFsD1z3cGfklS6J3LMwvO4NJFahNkuMduIyl">
<div class="row">
<div class="col-sm-9">
<div class="form-group">
<div class="form-input has-feedback">
<textarea id="input_nota" class="form-control" rows="2" placeholder="Contenido de la nota:" name="nota" cols="50"></textarea>
</div>
</div>
</div>
<div class="col-sm-3">
<button class="btn btn-lg btn-primary btn-block" type="submit">
<i class="fa fa-fw fa-plus"></i> Agregar nota
</button>
</div>
</div>
</form>
</div>
<div class="modal-body">
<div class="">
<table id="tabla_notas" class="" cellspacing="0" width="100%">
<thead>
<tr>
<th>ID</th>
<th><i class="fa fa-fw fa-circle"></i> Ordenar por status</th>
<th>Ordenar por fecha</th>
<th></th>
<th>Ordenar por contenido</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
<script id="chat_conversation_template" type="text/template">
<!-- Construct the box with style you want. Here we are using box-danger -->
<!-- Then add the class direct-chat and choose the direct-chat-* contexual class -->
<!-- The contextual class should match the box, so we are using direct-chat-danger -->
<div class="box box-danger direct-chat direct-chat-danger">
<div class="box-header with-border">
<h3 class="box-title" data-widget="collapse">
<img src="" alt="" class="avatar img-responsive img-circle">
<span class="name"></span>
</h3>
<div class="box-tools pull-right">
<span data-toggle="tooltip" title="" class="badge bg-red"></span>
<button class="btn btn-sm btn-box-tool" data-widget="remove"><i class="fa fa-times"></i></button>
</div>
</div><!-- /.box-header -->
<div class="box-body">
<!-- Conversations are loaded here -->
<div class="direct-chat-messages">
</div><!--/.direct-chat-messages-->
</div><!-- /.box-body -->
<div class="box-footer">
<div class="input-group">
<input type="text" name="message" placeholder="Escribe tu mensaje ..."
class="form-control input-message">
<span class="input-group-btn btn-danger">
<button type="button" class="send-message btn btn-danger btn-flat"><i class="fa-fw fa-solid fa-check"></i></button>
</span>
</div>
</div><!-- /.box-footer-->
</div><!--/.direct-chat -->
</script>
<script id="chat_message_template" type="text/template">
<div class="direct-chat-msg" data-id="">
<div class="direct-chat-info clearfix">
<span class="direct-chat-name pull-left"></span>
<span class="direct-chat-timestamp pull-right"></span>
</div><!-- /.direct-chat-info -->
<div class="direct-chat-text"></div><!-- /.direct-chat-text -->
</div><!-- /.direct-chat-msg -->
</script>
<script type="text/javascript">
var myself_id =78;
</script>
<!-- REQUIRED JS SCRIPTS -->
<script type="text/javascript" src="https://sistema.skeenmx.app/vendor/js/moment/moment-with-locales.min.js?001"></script>
<script src='https://cdn.rawgit.com/admsev/jquery-play-sound/master/jquery.playSound.js'></script>
<script src="https://sistema.skeenmx.app/vendor/js/panel.js?015" type="text/javascript"></script>
<!-- AdminLTE App -->
<script src="https://sistema.skeenmx.app/vendor/css/AdminLTE-2.3.5/dist/js/app.min.js"></script>
<!-- Optionally, you can add Slimscroll and FastClick plugins.
Both of these plugins are recommended to enhance the
user experience. Slimscroll is required when using the
fixed layout. -->
<script src="https://sistema.skeenmx.app/vendor/js/DataTables/datatables.min.js" type="text/javascript"></script>
<script src="https://sistema.skeenmx.app/vendor/js/data/datatable_monederos.js?003" type="text/javascript"></script>
<script>
var notas_datatable_json_data =[];
var notas_datatable_domain_url = "https://sistema.skeenmx.app/";
</script>
<script src="https://sistema.skeenmx.app/vendor/js/data/datatable_notas.js?000" type="text/javascript"></script>
<script type="text/javascript">
$('#btn-tab_menu_default').on('shown.bs.tab', function (e) {
$('#btn-tab_menu_expediente').parent().removeClass("active");
});
$('#btn-tab_menu_expediente').on('shown.bs.tab', function (e) {
$('#btn-tab_menu_default').parent().removeClass("active");
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,294 @@
codigo,nombre,categoria,precio,duracion_min,descripcion,color,grupo,favorito,activo
S0001,Consulta,servicios_medicos,1500.0,30,Tratamientos,c119,Tratamientos,1,1
S0002,Acné 1ra vez,servicios_medicos,900.0,30,Tratamientos,c40,Tratamientos,0,1
S0003,Nutrición consulta,servicios_medicos,800.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0004,Asesoria skincare,servicios_medicos,600.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0005,Valoración,servicios_medicos,600.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0006,Crioterapia,servicios_medicos,2200.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0007,Retiro de verrugas,servicios_medicos,2500.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0008,Revisión,servicios_medicos,0.0,30,Tratamientos,c73,Tratamientos,0,1
S0009,Retiro de quistes,servicios_medicos,10000.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0010,Biopsia por sacabocado,servicios_medicos,4200.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0011,Biopsias,servicios_medicos,5200.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0012,Procedimiento,servicios_medicos,1980.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0013,ANESTESIOLOGA SESION,servicios_medicos,4000.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0014,ANESTESIOLOGA NO SHOW,servicios_medicos,2000.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0015,otro,servicios_medicos,2000.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0016,Toxina botulínica DYSPORT,procedimientos_cosmeticos,125.0,30,Tratamientos,c90,Tratamientos,1,1
S0017,Toxina botulínica BOTOX,procedimientos_cosmeticos,120.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0018,Toxina botulínica XEOMEEN,procedimientos_cosmeticos,120.0,30,Tratamientos,c90,Tratamientos,0,1
S0019,Relleno YVOIRE CLASSIC,procedimientos_cosmeticos,8600.0,30,Tratamientos,c90,Tratamientos,0,1
S0020,Relleno YVOIRE VOLUME,procedimientos_cosmeticos,8000.0,30,Tratamientos,c90,Tratamientos,0,1
S0021,Relleno RESTYLANE KYSSE,procedimientos_cosmeticos,8600.0,30,Tratamientos,c90,Tratamientos,0,1
S0022,Relleno BELOTERO BALANCE,procedimientos_cosmeticos,8600.0,30,Tratamientos,c90,Tratamientos,0,1
S0023,Relleno BELOTERO INTENSE,procedimientos_cosmeticos,8000.0,30,Tratamientos,c90,Tratamientos,0,1
S0024,Relleno VOLUMA,procedimientos_cosmeticos,9000.0,30,Tratamientos,c90,Tratamientos,0,1
S0025,Relleno PERFECTHA SUBSKIN,procedimientos_cosmeticos,8000.0,30,Tratamientos,c90,Tratamientos,0,1
S0026,Relleno ELLANSE M,procedimientos_cosmeticos,12000.0,30,Tratamientos,c90,Tratamientos,0,1
S0027,Relleno RADIESSE,procedimientos_cosmeticos,12000.0,30,Tratamientos,c90,Tratamientos,0,1
S0028,NCTF,procedimientos_cosmeticos,3600.0,30,Tratamientos,c90,Tratamientos,0,1
S0029,RELLENO SCULPTRA,procedimientos_cosmeticos,20000.0,30,Tratamientos,c90,Tratamientos,0,1
S0030,Peeling médico,procedimientos_cosmeticos,2400.0,30,Tratamientos,c90,Tratamientos,0,1
S0031,Mesoterapia capilar,procedimientos_cosmeticos,3000.0,30,Tratamientos,c90,Tratamientos,0,1
S0032,Mesoterapia facial o corporal,procedimientos_cosmeticos,3000.0,30,Tratamientos,c90,Tratamientos,0,1
S0033,Exosomas facial,procedimientos_cosmeticos,5600.0,30,Tratamientos,c90,Tratamientos,0,1
S0034,Exosomas capilar,procedimientos_cosmeticos,5600.0,30,Tratamientos,c90,Tratamientos,0,1
S0035,Bioestimulacion capilar,procedimientos_cosmeticos,3500.0,30,Tratamientos,c90,Tratamientos,0,1
S0036,Bioestimulacion facial,procedimientos_cosmeticos,3500.0,30,Tratamientos,c90,Tratamientos,0,1
S0037,Microinyecciones Dutasteride,procedimientos_cosmeticos,4000.0,30,Tratamientos,c90,Tratamientos,0,1
S0038,Enzimas plus ,procedimientos_cosmeticos,4000.0,30,Tratamientos,c90,Tratamientos,0,1
S0039,Enzimas HA 1.5,procedimientos_cosmeticos,6900.0,30,Tratamientos,c90,Tratamientos,0,1
S0040,F CELLS,procedimientos_cosmeticos,39500.0,30,Tratamientos,c90,Tratamientos,0,1
S0041,LASER CO2 FRACCIONADO ENERGIAS SUAVES A MEDIAS,tecnologia_medica,12500.0,30,Tratamientos,c105,Tratamientos,0,1
S0047,FACIALES Facial SKEEN,cosmetologia_basica,900.0,30,Cabinas,c106,Cabinas,0,1
S0065,FACIALES Facial SKEEN,cosmetologia_basica,3440.0,30,,c106,,0,0
S0048,FACIALES Micro,cosmetologia_basica,900.0,30,Cabinas,c106,Cabinas,0,1
S0049,FACIALES Facial + micro o espalda,cosmetologia_basica,1400.0,30,Cabinas,c106,Cabinas,0,1
S0050,FotoSkeen cara,cosmetologia_avanzada,2550.0,30,Cabinas,c108,Cabinas,0,1
S0004,FotoSkeen cara,cosmetologia_avanzada,12700.0,30,,c108,,0,1
S0051,FotoSkeen espalda ,cosmetologia_avanzada,2700.0,30,Cabinas,c108,Cabinas,0,1
S0199,FotoSkeen espalda ,cosmetologia_avanzada,13500.0,30,,c108,,0,1
S0052,"FotoSkeen cara, cuello, escote",cosmetologia_avanzada,3000.0,30,Cabinas,c108,Cabinas,0,1
S0006,"FotoSkeen cara, cuello, escote",cosmetologia_avanzada,15000.0,30,,c108,,0,1
S0053,LumiSKEEN CARA O GLUTEOS,cosmetologia_avanzada,2000.0,30,Cabinas,c29,Cabinas,0,1
S0201,LumiSKEEN CARA O GLUTEOS,cosmetologia_avanzada,9990.0,30,,c29,,0,1
S0054,LumiSKEEN espalda ,cosmetologia_avanzada,2500.0,30,Cabinas,c29,Cabinas,0,1
S0202,LumiSKEEN espalda ,cosmetologia_avanzada,12400.0,30,,c29,,0,1
S0055,"LumiSKEEN cara, cuello, escote",cosmetologia_avanzada,2800.0,30,Cabinas,c29,Cabinas,0,1
S0009,"LumiSKEEN cara, cuello, escote",cosmetologia_avanzada,14000.0,30,,c29,,0,1
S0056,Lumiflash cara,cosmetologia_avanzada,1000.0,30,Cabinas,c29,Cabinas,0,1
S0057,"FotoSkeen Glow cara, cuello y escote",cosmetologia_avanzada,5000.0,30,Cabinas,c108,Cabinas,0,1
S0011,"FotoSkeen Glow cara, cuello y escote",cosmetologia_avanzada,24900.0,30,,c108,,0,1
S0058,QUANTA Hollywood skeen cara,cosmetologia_avanzada,2550.0,30,Cabinas,c116,Cabinas,0,1
S0054,QUANTA Hollywood skeen cara,cosmetologia_avanzada,10580.0,30,,c116,,0,1
S0059,OxySKEEN cara,cosmetologia_avanzada,2600.0,30,Cabinas,c94,Cabinas,0,1
S0013,OxySKEEN cara,cosmetologia_avanzada,6470.0,30,,c94,,0,1
S0060,Hydrafacial cara,cosmetologia_avanzada,2650.0,30,,c78,,0,1
S0014,Hydrafacial cara,cosmetologia_avanzada,6590.0,30,,c112,,0,1
S0061,Hydrafacial cara plus booster,cosmetologia_avanzada,3400.0,30,,c78,,0,1
S0015,Hydrafacial cara plus booster,cosmetologia_avanzada,8460.0,30,,c112,,0,1
S0062,Nanopore cara,cosmetologia_avanzada,3150.0,30,Cabinas,c119,Cabinas,0,1
S0016,Nanopore cara,cosmetologia_avanzada,7840.0,30,,c119,,0,1
S0063,Cool Liftng,cosmetologia_avanzada,1920.0,30,Cabinas,c78,Cabinas,0,0
S0017,Cool Liftng,cosmetologia_avanzada,4780.0,30,,c78,,0,0
S0069,DEPILACION LASER o IPL CARA COMPLETA,cosmetologia_avanzada,890.0,30,Cabinas,c114,Cabinas,0,1
S0022,DEPILACION LASER o IPL CARA COMPLETA,cosmetologia_avanzada,4440.0,30,,c114,,0,1
S0176,DEPILACION LASER o IPL CARA COMPLETA,cosmetologia_avanzada,3570.0,30,,c114,,0,0
S0070,DEPILACION LASER o IPL MEDIA CARA Ó HOMBRO Ó MANOS,cosmetologia_avanzada,565.0,30,Cabinas,c114,Cabinas,0,1
S0023,DEPILACION LASER o IPL MEDIA CARA Ó BARBA Ó HOMBRO Ó MANOS,cosmetologia_avanzada,2820.0,30,,c114,,0,1
S0177,DEPILACION LASER o IPL MEDIA CARA Ó HOMBRO Ó MANOS,cosmetologia_avanzada,2268.0,30,,c114,,0,0
S0071,DEPILACION LASER o IPL ZONA PEQUENA Ó BIGOTE Ó CUELLO Ó PIES,cosmetologia_avanzada,260.0,30,Cabinas,c114,Cabinas,0,1
S0024,DEPILACION LASER o IPL ZONA PEQUENA Ó BIGOTE Ó PATILLAS Ó CUELLO Ó PIES,cosmetologia_avanzada,1300.0,30,,c114,,0,1
S0178,DEPILACION LASER o IPL ZONA PEQUENA Ó BIGOTE Ó CUELLO Ó PIES,cosmetologia_avanzada,1050.0,30,,c114,,0,0
S0072,DEPILACION LASER o IPL 1/2 TRONCO ANTERIOR O POSTERIOR,cosmetologia_avanzada,890.0,30,Cabinas,c114,Cabinas,0,1
S0025,DEPILACION LASER o IPL 1/2 TRONCO ANTERIOR O POSTERIOR,cosmetologia_avanzada,4445.0,30,,c114,,0,1
S0179,DEPILACION LASER o IPL 1/2 TRONCO ANTERIOR O POSTERIOR,cosmetologia_avanzada,3570.0,30,,c114,,0,0
S0073,DEPILACION LASER o IPL BRAZO COMPLETO,cosmetologia_avanzada,1260.0,30,Cabinas,c114,Cabinas,0,1
S0026,DEPILACION LASER o IPL BRAZO COMPLETO,cosmetologia_avanzada,6275.0,30,,c114,,0,1
S0180,DEPILACION LASER o IPL BRAZO COMPLETO,cosmetologia_avanzada,5040.0,30,,c114,,0,0
S0074,DEPILACION LASER o IPL MEDIO BRAZO ,cosmetologia_avanzada,995.0,30,Cabinas,c114,Cabinas,0,1
S0027,DEPILACION LASER o IPL MEDIO BRAZO O PANTORRILLA,cosmetologia_avanzada,4970.0,30,,c114,,0,1
S0181,DEPILACION LASER o IPL MEDIO BRAZO ,cosmetologia_avanzada,3990.0,30,,c114,,0,0
S0075,DEPILACION LASER o IPL BIKINI BRASILENO,cosmetologia_avanzada,945.0,30,Cabinas,c114,Cabinas,0,1
S0028,DEPILACION LASER o IPL BIKINI BRASILENO,cosmetologia_avanzada,4700.0,30,,c114,,0,1
S0182,DEPILACION LASER o IPL BIKINI BRASILENO,cosmetologia_avanzada,3780.0,30,,c114,,0,1
S0076,DEPILACION LASER o IPL PUBIS Ó INGLE Ó LABIOS ,cosmetologia_avanzada,630.0,30,Cabinas,c114,Cabinas,0,1
S0029,DEPILACION LASER o IPL PUBIS Ó INGLE Ó LABIOS Ó AXILA,cosmetologia_avanzada,3130.0,30,,c114,,0,1
S0183,DEPILACION LASER o IPL PUBIS Ó INGLE Ó LABIOS ,cosmetologia_avanzada,2520.0,30,,c114,,0,1
S0077,DEPILACION LASER o IPL PIERNAS,cosmetologia_avanzada,1890.0,30,Cabinas,c114,Cabinas,0,1
S0030,DEPILACION LASER o IPL PIERNAS,cosmetologia_avanzada,9410.0,30,,c114,,0,1
S0184,DEPILACION LASER o IPL PIERNAS,cosmetologia_avanzada,7560.0,30,,c114,,0,1
S0078,DEPILACION LASER o IPL MEDIA PIERNA SUPERIOR O INFERIOR,cosmetologia_avanzada,1260.0,30,Cabinas,c114,Cabinas,0,1
S0031,DEPILACION LASER o IPL MEDIA PIERNA SUPERIOR O INFERIOR,cosmetologia_avanzada,6275.0,30,,c114,,0,1
S0185,DEPILACION LASER o IPL MEDIA PIERNA SUPERIOR O INFERIOR,cosmetologia_avanzada,5040.0,30,,c114,,0,0
S0079,DEPILACION LASER o IPL GLUTEOS | ZONA ALBA,cosmetologia_avanzada,890.0,30,Cabinas,c114,Cabinas,0,1
S0032,DEPILACION LASER o IPL GLUTEOS | ZONA ALBA,cosmetologia_avanzada,4445.0,30,,c114,,0,1
S0186,DEPILACION LASER o IPL GLUTEOS | ZONA ALBA,cosmetologia_avanzada,3570.0,30,,c114,,0,0
S0080,DEPILACION LASER o IPL CUERPO COMPLETO PRECIO REGULAR,cosmetologia_avanzada,7350.0,30,Cabinas,c114,Cabinas,0,1
S0033,DEPILACION LASER o IPL CUERPO COMPLETO PRECIO REGULAR,cosmetologia_avanzada,36600.0,30,,c114,,0,1
S0187,DEPILACION LASER o IPL CUERPO COMPLETO PRECIO REGULAR,cosmetologia_avanzada,29400.0,30,,c114,,0,0
S0081,RF | Venus Freeze ZONA PEQUEÑA,cosmetologia_avanzada,1300.0,30,Cabinas,c82,Cabinas,0,1
S0034,RF | Venus Freeze ZONA PEQUEÑA,cosmetologia_avanzada,6470.0,30,,c82,,0,1
S0082,RF | Venus Freeze MEDIA CARA Ó CUELLO+ESCOTE,cosmetologia_avanzada,1400.0,30,Cabinas,c82,Cabinas,0,1
S0035,RF | Venus Freeze MEDIA CARA Ó CUELLO+ESCOTE,cosmetologia_avanzada,6970.0,30,,c82,,0,1
S0083,RF | Venus Freeze Cara + Papada,cosmetologia_avanzada,2100.0,30,Cabinas,c82,Cabinas,0,1
S0036,RF | Venus Freeze Cara + Papada,cosmetologia_avanzada,10450.0,30,,c82,,0,1
S0084,RF | Venus Freeze CARA + CUELLO + ESCOTE,cosmetologia_avanzada,2950.0,30,Cabinas,c82,Cabinas,0,1
S0037,RF | Venus Freeze CARA + CUELLO + ESCOTE,cosmetologia_avanzada,14690.0,30,,c82,,0,1
S0085,RF | Venus Freeze BRAZOS Ó AXILA POSTERIOR Ó RODILLAS,cosmetologia_avanzada,1800.0,30,Cabinas,c82,Cabinas,0,1
S0038,RF | Venus Freeze BRAZOS Ó AXILA POSTERIOR Ó RODILLAS,cosmetologia_avanzada,8960.0,30,,c82,,0,1
S0086,RF | Venus Freeze ABDOMEN Ó PIERNAS,cosmetologia_avanzada,3200.0,30,Cabinas,c82,Cabinas,0,1
S0039,RF | Venus Freeze ABDOMEN Ó PIERNAS,cosmetologia_avanzada,15930.0,30,,c82,,0,1
S0087,RF | Venus Freeze GLUTEOS + MUSLOS,cosmetologia_avanzada,3000.0,30,Cabinas,c82,Cabinas,0,1
S0040,RF | Venus Freeze GLUTEOS + MUSLOS,cosmetologia_avanzada,14940.0,30,,c82,,0,1
S0088,RF | Venus Freeze GLUTEOS Ó MUSLO,cosmetologia_avanzada,1800.0,30,Cabinas,c82,Cabinas,0,1
S0041,RF | Venus Freeze GLUTEOS Ó MUSLO,cosmetologia_avanzada,8960.0,30,,c115,,0,1
S0089,QUANTA TATUAJES MENOS DE 4 CM,cosmetologia_avanzada,2100.0,30,Cabinas,c116,Cabinas,0,1
S0057,QUANTA TATUAJES MENOS DE 4 CM,cosmetologia_avanzada,8700.0,30,,c116,,0,1
S0090,QUANTA TATUAJES DE 4 CM A 8 CM,cosmetologia_avanzada,3200.0,30,Cabinas,c116,Cabinas,0,1
S0058,QUANTA TATUAJES DE 4 CM A 8 CM,cosmetologia_avanzada,13280.0,30,,c116,,0,1
S0091,QUANTA TATUAJES DE 8 CM A 15 CM,cosmetologia_avanzada,4200.0,30,Cabinas,c116,Cabinas,0,1
S0059,QUANTA TATUAJES DE 8 CM A 15 CM,cosmetologia_avanzada,17400.0,30,,c116,,0,1
S0092,QUANTA TATUAJES MAS DE 15 CM,cosmetologia_avanzada,5500.0,30,Cabinas,c116,Cabinas,0,1
S0060,QUANTA TATUAJES MAS DE 15 CM,cosmetologia_avanzada,22800.0,30,,c116,,0,1
S0093,QUANTA EFELIDES | LENTIGOS x zona,cosmetologia_avanzada,2700.0,30,Cabinas,c116,Cabinas,0,1
S0053,QUANTA EFELIDES | LENTIGOS x zona,cosmetologia_avanzada,11200.0,30,,c116,,0,1
S0068,QUANTA EFELIDES | LENTIGOS x zona,cosmetologia_avanzada,6700.0,30,,c116,,0,1
S0094,Mesoject gun,cosmetologia_avanzada,2900.0,30,Cabinas,c42,Cabinas,0,1
S0050,Mesoject gun,cosmetologia_avanzada,14940.0,30,,c42,,0,1
S0095,Consulta primera vez,servicios_medicos,1600.0,30,Tratamientos,verdelimon03,Tratamientos,1,1
S0096,LASER EXCIMER,servicios_medicos,1400.0,30,Tratamientos,verdelimon03,Tratamientos,0,1
S0097,Lumiacne,cosmetologia_avanzada,1600.0,30,Cabinas,c29,Cabinas,0,1
S0052,LUMI ACNE,cosmetologia_avanzada,7950.0,30,,c29,,0,1
S0099,NCTF área pequeña,procedimientos_cosmeticos,2600.0,30,Tratamientos,c90,Tratamientos,0,1
S0100,Toxina Botulínica PROMO,procedimientos_cosmeticos,100.0,30,Tratamientos,c90,Tratamientos,1,1
S0102,Relleno,procedimientos_cosmeticos,7000.0,30,Tratamientos,c90,Tratamientos,0,1
S0103,LUMI FLASH MARTES Y JUEVES,cosmetologia_avanzada,500.0,30,,c29,,0,1
S0104,QUANTA FRACCIONADO,cosmetologia_avanzada,6000.0,30,,c116,,0,1
S0055,QUANTA FRACCIONADO,cosmetologia_avanzada,15000.0,30,,c116,,0,1
S0105,QUANTA TONING+ FRACCIONADO,cosmetologia_avanzada,7350.0,30,,c116,,0,1
S0163,QUANTA TONING+ FRACCIONADO,cosmetologia_avanzada,18300.0,30,,c116,,0,1
S0106,QUANTA ONICOMICOSIS MENOS DE 4,cosmetologia_avanzada,2100.0,30,,c116,,0,1
S0061,QUANTA ONICOMICOSIS MENOS DE 4,cosmetologia_avanzada,5200.0,30,,c116,,0,1
S0107,QUANTA ONICOMICOSIS MAS DE 4,cosmetologia_avanzada,3000.0,30,,c116,,0,1
S0062,QUANTA ONICOMICOSIS MAS DE 4,cosmetologia_avanzada,7450.0,30,,c116,,0,1
S0108,QUANTA ESTRIAS,cosmetologia_avanzada,3200.0,30,,c116,,0,1
S0063,QUANTA ESTRIAS,cosmetologia_avanzada,13250.0,30,,c116,,0,1
S0109,LUMISKEEN GLOW CARA,cosmetologia_avanzada,3200.0,30,,c29,,0,1
S0203,LUMISKEEN GLOW CARA,cosmetologia_avanzada,15930.0,30,,c29,,0,1
S0110,FOTO SKEEN GLOW CARA,cosmetologia_avanzada,3200.0,30,,c108,,0,1
S0066,FOTO SKEEN GLOW CARA,cosmetologia_avanzada,15950.0,30,,c108,,0,1
S0111,LUMI SKEEN GLOW CARA CUELLO Y ESCOTE,cosmetologia_avanzada,4000.0,30,,c29,,0,1
S0067,LUMI SKEEN GLOW CARA CUELLO Y ESCOTE,cosmetologia_avanzada,19900.0,30,,c29,,0,1
S0112,CALECIM MESOTERAPIA FACIAL,procedimientos_cosmeticos,3000.0,30,Tratamientos,c90,Tratamientos,0,1
S0113,CALECIM MESOTERAPIA CAPILAR,procedimientos_cosmeticos,3000.0,30,Tratamientos,c90,Tratamientos,0,1
S0115,HAIR FILLER,procedimientos_cosmeticos,5500.0,30,Tratamientos,c90,Tratamientos,0,1
S0117,BOOSTR EXOSOMAS,procedimientos_cosmeticos,1500.0,30,Tratamientos,c90,Tratamientos,0,1
S0118,"LASER CO2 CUELLO, ESCOTE O MANOS",tecnologia_medica,6000.0,30,,c108,,0,1
S0119,LASER CO2 AREA ADICIONAL ,tecnologia_medica,4000.0,30,,c108,,0,1
S0120,RELLENO AQUA SHINE,procedimientos_cosmeticos,6300.0,30,Tratamientos,c90,Tratamientos,0,1
S0121,RELLENO STYLE 600,procedimientos_cosmeticos,7590.0,30,Tratamientos,c90,Tratamientos,0,1
S0122,COSMELAN,procedimientos_cosmeticos,9000.0,30,,c93,,0,1
S0123,BIOESTIMULACION CON APEE,procedimientos_cosmeticos,4400.0,30,,c93,,0,1
S0124,FOTOHYDRAFACIAL,cosmetologia_avanzada,4000.0,30,,c78,,0,1
S0127,FOTOHYDRAFACIAL,cosmetologia_avanzada,9960.0,30,,c78,,0,1
S0247,enzimas promo 2 kits ,procedimientos_cosmeticos,5000.0,30,,c93,,0,0
S0248,QUANTA CICATRIZ 4CM ,cosmetologia_avanzada,3000.0,30,,c116,,0,1
S0137,QUANTA CICATRIZ 4CM ,cosmetologia_avanzada,7500.0,30,,c116,,0,1
S0251,RELLENO RESTYLANE LYFT,procedimientos_cosmeticos,8000.0,30,,c90,,0,1
S0254,Mesoject BTX promo,cosmetologia_avanzada,1800.0,30,Cabinas,c117,Cabinas,0,1
S0129,Mesoject BTX promo,cosmetologia_avanzada,4500.0,30,,c117,,0,1
S0255,Mesoject Hair,cosmetologia_avanzada,2300.0,30,Cabinas,c117,Cabinas,0,1
S0128,Mesoject Hair,cosmetologia_avanzada,7200.0,30,,c117,,0,1
S0256,HYDRAFACIAL PLATINUM,cosmetologia_avanzada,3800.0,30,,c78,,0,1
S0257,HYDRAFACIAL CUELLO Y ESCOTE,cosmetologia_avanzada,1955.0,30,,c78,,0,1
S0258,DUO PERK OJOS + 3 SESIONES DE NCTF,procedimientos_cosmeticos,8000.0,30,,c90,,0,1
S0259,DUO PERK LABIOS + RESTYLANE KYSSE,procedimientos_cosmeticos,8000.0,30,,c90,,0,1
S0260,PERK OJOS,cosmetologia_avanzada,1300.0,30,,c78,,0,1
S0261,PERK LABIOS,cosmetologia_avanzada,1000.0,30,,c78,,0,1
S0263,RELLENO ULTRADEEP,procedimientos_cosmeticos,8000.0,30,,c90,,0,1
S0264,RELLENO GLOBAL ACTION,procedimientos_cosmeticos,7000.0,30,,c90,,0,1
S0265,RELLENO MAILI,procedimientos_cosmeticos,8600.0,30,,c90,,0,1
S0273,QUANTA CICATRIZ 5CM O MAS,cosmetologia_avanzada,5000.0,30,,c116,,0,1
S0138,QUANTA CICATRIZ 5CM O MAS,cosmetologia_avanzada,13000.0,30,,c116,,0,1
S0274,QUANTA TONNING CARA ,cosmetologia_avanzada,3000.0,30,,c116,,0,1
S0139,QUANTA TONNING CARA ,cosmetologia_avanzada,7500.0,30,,c116,,0,1
S0275,QUANTA FRACCIONADO CARA Y CUELLO,cosmetologia_avanzada,7000.0,30,,c116,,0,1
S0140,QUANTA FRACCIONADO CARA Y CUELLO,cosmetologia_avanzada,17500.0,30,,c116,,0,1
S0276,REDTOUCH CARA,cosmetologia_avanzada,3000.0,30,,c44,,0,1
S0193,REDTOUCH CARA,cosmetologia_avanzada,14940.0,30,,c44,,0,1
S0277,REDTOUCH PELO,cosmetologia_avanzada,1500.0,30,,c44,,0,1
S0194,REDTOUCH PELO,cosmetologia_avanzada,9960.0,30,,c44,,0,1
S0195,REDTOUCH PELO,cosmetologia_avanzada,4980.0,30,,c44,,0,1
S0278,REDTOUCH LABIOS,cosmetologia_avanzada,1200.0,30,,c44,,0,1
S0196,REDTOUCH LABIOS,cosmetologia_avanzada,2980.0,30,,c44,,0,1
S0279,RED TOUCH CARA PROMO FEB-ABRIL,cosmetologia_avanzada,3000.0,30,,c44,,0,0
S0150,RED TOUCH CARA PROMO FEB-ABRIL,cosmetologia_avanzada,14400.0,30,,c44,,0,0
S0280,RED TOUCH PELO PROMO FEB-ABRIL,cosmetologia_avanzada,1120.0,30,,c44,,0,0
S0142,RED TOUCH PELO PROMO FEB-ABRIL,cosmetologia_avanzada,8960.0,30,,c44,,0,0
S0143,RED TOUCH PELO PROMO FEB-ABRIL,cosmetologia_avanzada,4480.0,30,,c44,,0,0
S0281,RED TOUCH LABIOS PROMO FEB-ABRIL,cosmetologia_avanzada,800.0,30,,c44,,0,0
S0145,RED TOUCH LABIOS PROMO FEB-ABRIL,cosmetologia_avanzada,2400.0,30,,c44,,0,0
S0282,ENZIMAS 2 KITS + 4 VENUS EN PAPADA PROMO JULIO-SEPT,cosmetologia_avanzada,9600.0,30,,c113,,0,0
S0283,ENZIMAS 3 KITS + 6 VENUS ABDOMEN PROMO JULIO-SEPT,cosmetologia_avanzada,19000.0,30,,c113,,0,0
S0284,HOLLYWOOD + RED TOUCH,cosmetologia_avanzada,4500.0,30,,c44,,0,1
S0146,HOLLYWOOD + RED TOUCH,cosmetologia_avanzada,25110.0,30,,c44,,0,1
S0285,QUANTA FRACCIONADO + RED TOUCH CARA,cosmetologia_avanzada,6700.0,30,,c44,,0,1
S0148,QUANTA FRACCIONADO + RED TOUCH CARA,cosmetologia_avanzada,16700.0,30,,c44,,0,1
S0286,QUANTA FRACCIONADO + RED TOUCH CARA CUELLO,cosmetologia_avanzada,8500.0,30,,c44,,0,1
S0149,QUANTA FRACCIONADO + RED TOUCH CARA CUELLO,cosmetologia_avanzada,21150.0,30,,c44,,0,1
S0289,DEPILACION LASER o IPL BARBA,cosmetologia_avanzada,420.0,30,,c114,,0,1
S0151,DEPILACION LASER o IPL BARBA,cosmetologia_avanzada,2090.0,30,,c114,,0,1
S0188,DEPILACION LASER o IPL BARBA,cosmetologia_avanzada,1680.0,30,,c114,,0,0
S0290,DEPILACION LASER o IPL PANTORRILLA,cosmetologia_avanzada,735.0,30,,c114,,0,1
S0152,DEPILACION LASER o IPL PANTORRILLA,cosmetologia_avanzada,3660.0,30,,c114,,0,1
S0189,DEPILACION LASER o IPL PANTORRILLA,cosmetologia_avanzada,2940.0,30,,c114,,0,0
S0291,DEPILACION LASER o IPL AXILA,cosmetologia_avanzada,525.0,30,,c114,,0,1
S0153,DEPILACION LASER o IPL AXILA,cosmetologia_avanzada,2615.0,30,,c114,,0,1
S0190,DEPILACION LASER o IPL AXILA,cosmetologia_avanzada,2100.0,30,,c114,,0,0
S0292,DEPILACION LASER o IPL PATILLAS,cosmetologia_avanzada,420.0,30,,c114,,0,1
S0154,DEPILACION LASER o IPL PATILLAS,cosmetologia_avanzada,2090.0,30,,c114,,0,1
S0191,DEPILACION LASER o IPL PATILLAS,cosmetologia_avanzada,1050.0,30,,c114,,0,1
S0298,DEPILACION LASER o IPL PECHO,cosmetologia_avanzada,1260.0,30,,c114,,0,1
S0160,DEPILACION LASER o IPL PECHO,cosmetologia_avanzada,6275.0,30,,c114,,0,1
S0192,DEPILACION LASER o IPL PECHO,cosmetologia_avanzada,5040.0,30,,c114,,0,1
S0300,NATURAL PEEL,cosmetologia_avanzada,3500.0,30,,c116,,0,1
S0164,NATURAL PEEL,cosmetologia_avanzada,15000.0,30,,c116,,0,1
S0301,RED TOUCH CARA CUELLO Y ESCOTE,cosmetologia_avanzada,4500.0,30,,c44,,0,1
S0165,RED TOUCH CARA CUELLO Y ESCOTE,cosmetologia_avanzada,22410.0,30,,c101,,0,1
S0302,MORPHEUS CARA Y CUELLO,tecnologia_medica,19000.0,30,,c105,,0,1
S0168,MORPHEUS CARA Y CUELLO,tecnologia_medica,47310.0,30,,c105,,0,1
S0303,MORPHEUS CARA CUELLO Y ESCOTE,tecnologia_medica,22000.0,30,,c105,,0,1
S0167,MORPHEUS CARA CUELLO Y ESCOTE,tecnologia_medica,54780.0,30,,c105,,0,1
S0304,MORPHEUS CUERPO,tecnologia_medica,24000.0,30,,c105,,0,1
S0169,MORPHEUS CUERPO,tecnologia_medica,59760.0,30,,c105,,0,1
S0307, Morpheus 8 NO SHOW,tecnologia_medica,2000.0,30,,c105,,0,0
S0308, MORPHEUS 3 SESIONES CARA Y CUELLO UN SOLO PAGO,tecnologia_medica,40000.0,30,,c105,,0,0
S0309,MORPHEUS ABDOMEN CUERPO UN SOLO PAGO 3 SESIONES,tecnologia_medica,50000.0,30,,c105,,0,0
S0310, Lumecca estrías por zona,cosmetologia_avanzada,2200.0,30,,c82,,0,1
S0311, Lumecca,cosmetologia_avanzada,4200.0,30,,c82,,0,1
S0170, Lumecca,cosmetologia_avanzada,10450.0,30,,c82,,0,1
S0312,Morpheus8 Cara,tecnologia_medica,16000.0,30,,c105,,0,1
S0171,Morpheus8 Cara,tecnologia_medica,39840.0,30,,c105,,0,1
S0314,QUANTA CARA CUELLO Y ESCOTE,cosmetologia_avanzada,7500.0,30,,c116,,0,1
S0172,QUANTA CARA CUELLO Y ESCOTE,cosmetologia_avanzada,18650.0,30,,c116,,0,1
S0315,QUANTA + RED TOUCH CARA CUELLO Y ESCOTE,cosmetologia_avanzada,9500.0,30,,c44,,0,1
S0173,QUANTA + RED TOUCH CARA CUELLO Y ESCOTE,cosmetologia_avanzada,23650.0,30,,c44,,0,1
S0316,RED TOUCH CARA Y CUELLO,cosmetologia_avanzada,4200.0,30,,c44,,0,1
S0174,RED TOUCH CARA Y CUELLO,cosmetologia_avanzada,20900.0,30,,c44,,0,1
S0317,HOLLYWOOD + RED TOUCH CARA Y CUELLO,cosmetologia_avanzada,5500.0,30,,c44,,0,1
S0197,HOLLYWOOD + RED TOUCH CARA Y CUELLO,cosmetologia_avanzada,27390.0,30,,c44,,0,1
S0318,HOLLYWOOD CARA Y CUELLO,cosmetologia_avanzada,3000.0,30,,c44,,0,1
S0198,HOLLYWOOD CARA Y CUELLO,cosmetologia_avanzada,12000.0,30,,c44,,1,1
S0320,FOTOSKEEN GLUTEOS,cosmetologia_avanzada,2700.0,30,,c108,,0,1
S0200,FOTOSKEEN GLUTEOS,cosmetologia_avanzada,13500.0,30,,c108,,0,1
S0321,LASER CO2 CARA ENERGIA ALTA,tecnologia_medica,20000.0,30,,c105,,0,1
S0322,LASER CO2 CARA CUELLO Y ESCOTE ENERGIA MEDIA,tecnologia_medica,20000.0,30,,c105,,0,1
S0323,LASAER CO2 CUELLO,tecnologia_medica,6000.0,30,,c105,,0,1
S0324,LASER CO2 PARPADOS,tecnologia_medica,6000.0,30,,c105,,0,1
S0325,LASER CO2 BRAZOS COMPLETOS,tecnologia_medica,15000.0,30,,c105,,0,1
S0326,BOOSTER PBSERUM,cosmetologia_avanzada,2200.0,30,,c35,,0,1
S0327,BIOESTUMULACION CON PBSERUM ,procedimientos_cosmeticos,3000.0,30,,c35,,0,1
S0328,HYDRAGLOW,cosmetologia_avanzada,4000.0,30,,c78,,0,1
S0329,HYDRAFLSH,cosmetologia_avanzada,2000.0,30,,c78,,0,1
S0330,CONSULTA + LUMISKEEN,servicios_medicos,1800.0,30,,c119,,0,1
S0331,Consulta primera vez 1 sesión Dra Ramos,servicios_medicos,2000.0,30,,verdelimon03,,0,1
S0332,CONSULTA DE PELO DRA RAMOS O DR VEGA PRIMERA VEZ,servicios_medicos,2000.0,30,,verdelimon03,,0,1
S0333,CONSULTA PELO DRA RAMOS O DR VEGA SUBSECUENTES,servicios_medicos,1500.0,30,,c119,,0,1
S0334,Consulta primera vez 1 sesión PRECIO 2025,servicios_medicos,1500.0,30,,verdelimon03,,0,1
S0335,CONSULTA SUBSECUENTE PRECIO 2025,servicios_medicos,1400.0,30,,c119,,0,1
S0336,LASER CO2 ENERGIAS ALTAS O CON SEDACION,tecnologia_medica,20000.0,30,,c108,,0,1
S0337,LASER CO2 CUELLO O ESCOTE O MANOS,tecnologia_medica,6500.0,30,,c108,,0,1
S0338,LASER CO2 ADICIONAL ,tecnologia_medica,4000.0,30,,c108,,0,1
S0339,LASER CO2 PARPADOS,tecnologia_medica,5000.0,30,,c108,,0,1
S0340,FACIAL + PDRN,cosmetologia_basica,3500.0,30,,c48,,0,1
S0204,FACIAL + PDRN,cosmetologia_basica,8400.0,30,,c106,,0,1
S0341,QUANTA FRACCIONADO + PDRN,cosmetologia_avanzada,6600.0,30,,c116,,0,1
S0342,HOLLYWOOD + PDRN,cosmetologia_avanzada,4500.0,30,,c116,,0,1
S0343,KENALOG,servicios_medicos,0.01,30,,rosapalo02,,0,1
S0344,BIOESTIMULACION ADN SALMON,procedimientos_cosmeticos,3500.0,30,,c90,,0,1
S0345,Consulta primera vez Dr. Cerecer,servicios_medicos,1600.0,30,,verdelimon03,,0,1
S0346,Consulta Dr. Cerecer,servicios_medicos,1500.0,30,,c119,,0,1
S0347,ANTICIPO CREMA HORMONAL,procedimientos_cosmeticos,1000.0,30,,verdelimon02,,0,1
S0348,LIQUIDACION CREMA HORMONAL ,procedimientos_cosmeticos,0.0,30,,verdelimon02,,0,1
S0349,NANOPOR + PDRN SALMON,cosmetologia_avanzada,4000.0,30,,c42,,0,1
1 codigo nombre categoria precio duracion_min descripcion color grupo favorito activo
2 S0001 Consulta servicios_medicos 1500.0 30 Tratamientos c119 Tratamientos 1 1
3 S0002 Acné 1ra vez servicios_medicos 900.0 30 Tratamientos c40 Tratamientos 0 1
4 S0003 Nutrición consulta servicios_medicos 800.0 30 Tratamientos verdelimon03 Tratamientos 0 1
5 S0004 Asesoria skincare servicios_medicos 600.0 30 Tratamientos verdelimon03 Tratamientos 0 1
6 S0005 Valoración servicios_medicos 600.0 30 Tratamientos verdelimon03 Tratamientos 0 1
7 S0006 Crioterapia servicios_medicos 2200.0 30 Tratamientos verdelimon03 Tratamientos 0 1
8 S0007 Retiro de verrugas servicios_medicos 2500.0 30 Tratamientos verdelimon03 Tratamientos 0 1
9 S0008 Revisión servicios_medicos 0.0 30 Tratamientos c73 Tratamientos 0 1
10 S0009 Retiro de quistes servicios_medicos 10000.0 30 Tratamientos verdelimon03 Tratamientos 0 1
11 S0010 Biopsia por sacabocado servicios_medicos 4200.0 30 Tratamientos verdelimon03 Tratamientos 0 1
12 S0011 Biopsias servicios_medicos 5200.0 30 Tratamientos verdelimon03 Tratamientos 0 1
13 S0012 Procedimiento servicios_medicos 1980.0 30 Tratamientos verdelimon03 Tratamientos 0 1
14 S0013 ANESTESIOLOGA SESION servicios_medicos 4000.0 30 Tratamientos verdelimon03 Tratamientos 0 1
15 S0014 ANESTESIOLOGA NO SHOW servicios_medicos 2000.0 30 Tratamientos verdelimon03 Tratamientos 0 1
16 S0015 otro servicios_medicos 2000.0 30 Tratamientos verdelimon03 Tratamientos 0 1
17 S0016 Toxina botulínica DYSPORT procedimientos_cosmeticos 125.0 30 Tratamientos c90 Tratamientos 1 1
18 S0017 Toxina botulínica BOTOX procedimientos_cosmeticos 120.0 30 Tratamientos verdelimon03 Tratamientos 0 1
19 S0018 Toxina botulínica XEOMEEN procedimientos_cosmeticos 120.0 30 Tratamientos c90 Tratamientos 0 1
20 S0019 Relleno YVOIRE CLASSIC procedimientos_cosmeticos 8600.0 30 Tratamientos c90 Tratamientos 0 1
21 S0020 Relleno YVOIRE VOLUME procedimientos_cosmeticos 8000.0 30 Tratamientos c90 Tratamientos 0 1
22 S0021 Relleno RESTYLANE KYSSE procedimientos_cosmeticos 8600.0 30 Tratamientos c90 Tratamientos 0 1
23 S0022 Relleno BELOTERO BALANCE procedimientos_cosmeticos 8600.0 30 Tratamientos c90 Tratamientos 0 1
24 S0023 Relleno BELOTERO INTENSE procedimientos_cosmeticos 8000.0 30 Tratamientos c90 Tratamientos 0 1
25 S0024 Relleno VOLUMA procedimientos_cosmeticos 9000.0 30 Tratamientos c90 Tratamientos 0 1
26 S0025 Relleno PERFECTHA SUBSKIN procedimientos_cosmeticos 8000.0 30 Tratamientos c90 Tratamientos 0 1
27 S0026 Relleno ELLANSE M procedimientos_cosmeticos 12000.0 30 Tratamientos c90 Tratamientos 0 1
28 S0027 Relleno RADIESSE procedimientos_cosmeticos 12000.0 30 Tratamientos c90 Tratamientos 0 1
29 S0028 NCTF procedimientos_cosmeticos 3600.0 30 Tratamientos c90 Tratamientos 0 1
30 S0029 RELLENO SCULPTRA procedimientos_cosmeticos 20000.0 30 Tratamientos c90 Tratamientos 0 1
31 S0030 Peeling médico procedimientos_cosmeticos 2400.0 30 Tratamientos c90 Tratamientos 0 1
32 S0031 Mesoterapia capilar procedimientos_cosmeticos 3000.0 30 Tratamientos c90 Tratamientos 0 1
33 S0032 Mesoterapia facial o corporal procedimientos_cosmeticos 3000.0 30 Tratamientos c90 Tratamientos 0 1
34 S0033 Exosomas facial procedimientos_cosmeticos 5600.0 30 Tratamientos c90 Tratamientos 0 1
35 S0034 Exosomas capilar procedimientos_cosmeticos 5600.0 30 Tratamientos c90 Tratamientos 0 1
36 S0035 Bioestimulacion capilar procedimientos_cosmeticos 3500.0 30 Tratamientos c90 Tratamientos 0 1
37 S0036 Bioestimulacion facial procedimientos_cosmeticos 3500.0 30 Tratamientos c90 Tratamientos 0 1
38 S0037 Microinyecciones Dutasteride procedimientos_cosmeticos 4000.0 30 Tratamientos c90 Tratamientos 0 1
39 S0038 Enzimas plus procedimientos_cosmeticos 4000.0 30 Tratamientos c90 Tratamientos 0 1
40 S0039 Enzimas HA 1.5 procedimientos_cosmeticos 6900.0 30 Tratamientos c90 Tratamientos 0 1
41 S0040 F CELLS procedimientos_cosmeticos 39500.0 30 Tratamientos c90 Tratamientos 0 1
42 S0041 LASER CO2 FRACCIONADO ENERGIAS SUAVES A MEDIAS tecnologia_medica 12500.0 30 Tratamientos c105 Tratamientos 0 1
43 S0047 FACIALES Facial SKEEN cosmetologia_basica 900.0 30 Cabinas c106 Cabinas 0 1
44 S0065 FACIALES Facial SKEEN cosmetologia_basica 3440.0 30 c106 0 0
45 S0048 FACIALES Micro cosmetologia_basica 900.0 30 Cabinas c106 Cabinas 0 1
46 S0049 FACIALES Facial + micro o espalda cosmetologia_basica 1400.0 30 Cabinas c106 Cabinas 0 1
47 S0050 FotoSkeen cara cosmetologia_avanzada 2550.0 30 Cabinas c108 Cabinas 0 1
48 S0004 FotoSkeen cara cosmetologia_avanzada 12700.0 30 c108 0 1
49 S0051 FotoSkeen espalda cosmetologia_avanzada 2700.0 30 Cabinas c108 Cabinas 0 1
50 S0199 FotoSkeen espalda cosmetologia_avanzada 13500.0 30 c108 0 1
51 S0052 FotoSkeen cara, cuello, escote cosmetologia_avanzada 3000.0 30 Cabinas c108 Cabinas 0 1
52 S0006 FotoSkeen cara, cuello, escote cosmetologia_avanzada 15000.0 30 c108 0 1
53 S0053 LumiSKEEN CARA O GLUTEOS cosmetologia_avanzada 2000.0 30 Cabinas c29 Cabinas 0 1
54 S0201 LumiSKEEN CARA O GLUTEOS cosmetologia_avanzada 9990.0 30 c29 0 1
55 S0054 LumiSKEEN espalda cosmetologia_avanzada 2500.0 30 Cabinas c29 Cabinas 0 1
56 S0202 LumiSKEEN espalda cosmetologia_avanzada 12400.0 30 c29 0 1
57 S0055 LumiSKEEN cara, cuello, escote cosmetologia_avanzada 2800.0 30 Cabinas c29 Cabinas 0 1
58 S0009 LumiSKEEN cara, cuello, escote cosmetologia_avanzada 14000.0 30 c29 0 1
59 S0056 Lumiflash cara cosmetologia_avanzada 1000.0 30 Cabinas c29 Cabinas 0 1
60 S0057 FotoSkeen Glow cara, cuello y escote cosmetologia_avanzada 5000.0 30 Cabinas c108 Cabinas 0 1
61 S0011 FotoSkeen Glow cara, cuello y escote cosmetologia_avanzada 24900.0 30 c108 0 1
62 S0058 QUANTA Hollywood skeen cara cosmetologia_avanzada 2550.0 30 Cabinas c116 Cabinas 0 1
63 S0054 QUANTA Hollywood skeen cara cosmetologia_avanzada 10580.0 30 c116 0 1
64 S0059 OxySKEEN cara cosmetologia_avanzada 2600.0 30 Cabinas c94 Cabinas 0 1
65 S0013 OxySKEEN cara cosmetologia_avanzada 6470.0 30 c94 0 1
66 S0060 Hydrafacial cara cosmetologia_avanzada 2650.0 30 c78 0 1
67 S0014 Hydrafacial cara cosmetologia_avanzada 6590.0 30 c112 0 1
68 S0061 Hydrafacial cara plus booster cosmetologia_avanzada 3400.0 30 c78 0 1
69 S0015 Hydrafacial cara plus booster cosmetologia_avanzada 8460.0 30 c112 0 1
70 S0062 Nanopore cara cosmetologia_avanzada 3150.0 30 Cabinas c119 Cabinas 0 1
71 S0016 Nanopore cara cosmetologia_avanzada 7840.0 30 c119 0 1
72 S0063 Cool Liftng cosmetologia_avanzada 1920.0 30 Cabinas c78 Cabinas 0 0
73 S0017 Cool Liftng cosmetologia_avanzada 4780.0 30 c78 0 0
74 S0069 DEPILACION LASER o IPL CARA COMPLETA cosmetologia_avanzada 890.0 30 Cabinas c114 Cabinas 0 1
75 S0022 DEPILACION LASER o IPL CARA COMPLETA cosmetologia_avanzada 4440.0 30 c114 0 1
76 S0176 DEPILACION LASER o IPL CARA COMPLETA cosmetologia_avanzada 3570.0 30 c114 0 0
77 S0070 DEPILACION LASER o IPL MEDIA CARA Ó HOMBRO Ó MANOS cosmetologia_avanzada 565.0 30 Cabinas c114 Cabinas 0 1
78 S0023 DEPILACION LASER o IPL MEDIA CARA Ó BARBA Ó HOMBRO Ó MANOS cosmetologia_avanzada 2820.0 30 c114 0 1
79 S0177 DEPILACION LASER o IPL MEDIA CARA Ó HOMBRO Ó MANOS cosmetologia_avanzada 2268.0 30 c114 0 0
80 S0071 DEPILACION LASER o IPL ZONA PEQUENA Ó BIGOTE Ó CUELLO Ó PIES cosmetologia_avanzada 260.0 30 Cabinas c114 Cabinas 0 1
81 S0024 DEPILACION LASER o IPL ZONA PEQUENA Ó BIGOTE Ó PATILLAS Ó CUELLO Ó PIES cosmetologia_avanzada 1300.0 30 c114 0 1
82 S0178 DEPILACION LASER o IPL ZONA PEQUENA Ó BIGOTE Ó CUELLO Ó PIES cosmetologia_avanzada 1050.0 30 c114 0 0
83 S0072 DEPILACION LASER o IPL 1/2 TRONCO ANTERIOR O POSTERIOR cosmetologia_avanzada 890.0 30 Cabinas c114 Cabinas 0 1
84 S0025 DEPILACION LASER o IPL 1/2 TRONCO ANTERIOR O POSTERIOR cosmetologia_avanzada 4445.0 30 c114 0 1
85 S0179 DEPILACION LASER o IPL 1/2 TRONCO ANTERIOR O POSTERIOR cosmetologia_avanzada 3570.0 30 c114 0 0
86 S0073 DEPILACION LASER o IPL BRAZO COMPLETO cosmetologia_avanzada 1260.0 30 Cabinas c114 Cabinas 0 1
87 S0026 DEPILACION LASER o IPL BRAZO COMPLETO cosmetologia_avanzada 6275.0 30 c114 0 1
88 S0180 DEPILACION LASER o IPL BRAZO COMPLETO cosmetologia_avanzada 5040.0 30 c114 0 0
89 S0074 DEPILACION LASER o IPL MEDIO BRAZO cosmetologia_avanzada 995.0 30 Cabinas c114 Cabinas 0 1
90 S0027 DEPILACION LASER o IPL MEDIO BRAZO O PANTORRILLA cosmetologia_avanzada 4970.0 30 c114 0 1
91 S0181 DEPILACION LASER o IPL MEDIO BRAZO cosmetologia_avanzada 3990.0 30 c114 0 0
92 S0075 DEPILACION LASER o IPL BIKINI BRASILENO cosmetologia_avanzada 945.0 30 Cabinas c114 Cabinas 0 1
93 S0028 DEPILACION LASER o IPL BIKINI BRASILENO cosmetologia_avanzada 4700.0 30 c114 0 1
94 S0182 DEPILACION LASER o IPL BIKINI BRASILENO cosmetologia_avanzada 3780.0 30 c114 0 1
95 S0076 DEPILACION LASER o IPL PUBIS Ó INGLE Ó LABIOS cosmetologia_avanzada 630.0 30 Cabinas c114 Cabinas 0 1
96 S0029 DEPILACION LASER o IPL PUBIS Ó INGLE Ó LABIOS Ó AXILA cosmetologia_avanzada 3130.0 30 c114 0 1
97 S0183 DEPILACION LASER o IPL PUBIS Ó INGLE Ó LABIOS cosmetologia_avanzada 2520.0 30 c114 0 1
98 S0077 DEPILACION LASER o IPL PIERNAS cosmetologia_avanzada 1890.0 30 Cabinas c114 Cabinas 0 1
99 S0030 DEPILACION LASER o IPL PIERNAS cosmetologia_avanzada 9410.0 30 c114 0 1
100 S0184 DEPILACION LASER o IPL PIERNAS cosmetologia_avanzada 7560.0 30 c114 0 1
101 S0078 DEPILACION LASER o IPL MEDIA PIERNA SUPERIOR O INFERIOR cosmetologia_avanzada 1260.0 30 Cabinas c114 Cabinas 0 1
102 S0031 DEPILACION LASER o IPL MEDIA PIERNA SUPERIOR O INFERIOR cosmetologia_avanzada 6275.0 30 c114 0 1
103 S0185 DEPILACION LASER o IPL MEDIA PIERNA SUPERIOR O INFERIOR cosmetologia_avanzada 5040.0 30 c114 0 0
104 S0079 DEPILACION LASER o IPL GLUTEOS | ZONA ALBA cosmetologia_avanzada 890.0 30 Cabinas c114 Cabinas 0 1
105 S0032 DEPILACION LASER o IPL GLUTEOS | ZONA ALBA cosmetologia_avanzada 4445.0 30 c114 0 1
106 S0186 DEPILACION LASER o IPL GLUTEOS | ZONA ALBA cosmetologia_avanzada 3570.0 30 c114 0 0
107 S0080 DEPILACION LASER o IPL CUERPO COMPLETO PRECIO REGULAR cosmetologia_avanzada 7350.0 30 Cabinas c114 Cabinas 0 1
108 S0033 DEPILACION LASER o IPL CUERPO COMPLETO PRECIO REGULAR cosmetologia_avanzada 36600.0 30 c114 0 1
109 S0187 DEPILACION LASER o IPL CUERPO COMPLETO PRECIO REGULAR cosmetologia_avanzada 29400.0 30 c114 0 0
110 S0081 RF | Venus Freeze ZONA PEQUEÑA cosmetologia_avanzada 1300.0 30 Cabinas c82 Cabinas 0 1
111 S0034 RF | Venus Freeze ZONA PEQUEÑA cosmetologia_avanzada 6470.0 30 c82 0 1
112 S0082 RF | Venus Freeze MEDIA CARA Ó CUELLO+ESCOTE cosmetologia_avanzada 1400.0 30 Cabinas c82 Cabinas 0 1
113 S0035 RF | Venus Freeze MEDIA CARA Ó CUELLO+ESCOTE cosmetologia_avanzada 6970.0 30 c82 0 1
114 S0083 RF | Venus Freeze Cara + Papada cosmetologia_avanzada 2100.0 30 Cabinas c82 Cabinas 0 1
115 S0036 RF | Venus Freeze Cara + Papada cosmetologia_avanzada 10450.0 30 c82 0 1
116 S0084 RF | Venus Freeze CARA + CUELLO + ESCOTE cosmetologia_avanzada 2950.0 30 Cabinas c82 Cabinas 0 1
117 S0037 RF | Venus Freeze CARA + CUELLO + ESCOTE cosmetologia_avanzada 14690.0 30 c82 0 1
118 S0085 RF | Venus Freeze BRAZOS Ó AXILA POSTERIOR Ó RODILLAS cosmetologia_avanzada 1800.0 30 Cabinas c82 Cabinas 0 1
119 S0038 RF | Venus Freeze BRAZOS Ó AXILA POSTERIOR Ó RODILLAS cosmetologia_avanzada 8960.0 30 c82 0 1
120 S0086 RF | Venus Freeze ABDOMEN Ó PIERNAS cosmetologia_avanzada 3200.0 30 Cabinas c82 Cabinas 0 1
121 S0039 RF | Venus Freeze ABDOMEN Ó PIERNAS cosmetologia_avanzada 15930.0 30 c82 0 1
122 S0087 RF | Venus Freeze GLUTEOS + MUSLOS cosmetologia_avanzada 3000.0 30 Cabinas c82 Cabinas 0 1
123 S0040 RF | Venus Freeze GLUTEOS + MUSLOS cosmetologia_avanzada 14940.0 30 c82 0 1
124 S0088 RF | Venus Freeze GLUTEOS Ó MUSLO cosmetologia_avanzada 1800.0 30 Cabinas c82 Cabinas 0 1
125 S0041 RF | Venus Freeze GLUTEOS Ó MUSLO cosmetologia_avanzada 8960.0 30 c115 0 1
126 S0089 QUANTA TATUAJES MENOS DE 4 CM cosmetologia_avanzada 2100.0 30 Cabinas c116 Cabinas 0 1
127 S0057 QUANTA TATUAJES MENOS DE 4 CM cosmetologia_avanzada 8700.0 30 c116 0 1
128 S0090 QUANTA TATUAJES DE 4 CM A 8 CM cosmetologia_avanzada 3200.0 30 Cabinas c116 Cabinas 0 1
129 S0058 QUANTA TATUAJES DE 4 CM A 8 CM cosmetologia_avanzada 13280.0 30 c116 0 1
130 S0091 QUANTA TATUAJES DE 8 CM A 15 CM cosmetologia_avanzada 4200.0 30 Cabinas c116 Cabinas 0 1
131 S0059 QUANTA TATUAJES DE 8 CM A 15 CM cosmetologia_avanzada 17400.0 30 c116 0 1
132 S0092 QUANTA TATUAJES MAS DE 15 CM cosmetologia_avanzada 5500.0 30 Cabinas c116 Cabinas 0 1
133 S0060 QUANTA TATUAJES MAS DE 15 CM cosmetologia_avanzada 22800.0 30 c116 0 1
134 S0093 QUANTA EFELIDES | LENTIGOS x zona cosmetologia_avanzada 2700.0 30 Cabinas c116 Cabinas 0 1
135 S0053 QUANTA EFELIDES | LENTIGOS x zona cosmetologia_avanzada 11200.0 30 c116 0 1
136 S0068 QUANTA EFELIDES | LENTIGOS x zona cosmetologia_avanzada 6700.0 30 c116 0 1
137 S0094 Mesoject gun cosmetologia_avanzada 2900.0 30 Cabinas c42 Cabinas 0 1
138 S0050 Mesoject gun cosmetologia_avanzada 14940.0 30 c42 0 1
139 S0095 Consulta primera vez servicios_medicos 1600.0 30 Tratamientos verdelimon03 Tratamientos 1 1
140 S0096 LASER EXCIMER servicios_medicos 1400.0 30 Tratamientos verdelimon03 Tratamientos 0 1
141 S0097 Lumiacne cosmetologia_avanzada 1600.0 30 Cabinas c29 Cabinas 0 1
142 S0052 LUMI ACNE cosmetologia_avanzada 7950.0 30 c29 0 1
143 S0099 NCTF área pequeña procedimientos_cosmeticos 2600.0 30 Tratamientos c90 Tratamientos 0 1
144 S0100 Toxina Botulínica PROMO procedimientos_cosmeticos 100.0 30 Tratamientos c90 Tratamientos 1 1
145 S0102 Relleno procedimientos_cosmeticos 7000.0 30 Tratamientos c90 Tratamientos 0 1
146 S0103 LUMI FLASH MARTES Y JUEVES cosmetologia_avanzada 500.0 30 c29 0 1
147 S0104 QUANTA FRACCIONADO cosmetologia_avanzada 6000.0 30 c116 0 1
148 S0055 QUANTA FRACCIONADO cosmetologia_avanzada 15000.0 30 c116 0 1
149 S0105 QUANTA TONING+ FRACCIONADO cosmetologia_avanzada 7350.0 30 c116 0 1
150 S0163 QUANTA TONING+ FRACCIONADO cosmetologia_avanzada 18300.0 30 c116 0 1
151 S0106 QUANTA ONICOMICOSIS MENOS DE 4 cosmetologia_avanzada 2100.0 30 c116 0 1
152 S0061 QUANTA ONICOMICOSIS MENOS DE 4 cosmetologia_avanzada 5200.0 30 c116 0 1
153 S0107 QUANTA ONICOMICOSIS MAS DE 4 cosmetologia_avanzada 3000.0 30 c116 0 1
154 S0062 QUANTA ONICOMICOSIS MAS DE 4 cosmetologia_avanzada 7450.0 30 c116 0 1
155 S0108 QUANTA ESTRIAS cosmetologia_avanzada 3200.0 30 c116 0 1
156 S0063 QUANTA ESTRIAS cosmetologia_avanzada 13250.0 30 c116 0 1
157 S0109 LUMISKEEN GLOW CARA cosmetologia_avanzada 3200.0 30 c29 0 1
158 S0203 LUMISKEEN GLOW CARA cosmetologia_avanzada 15930.0 30 c29 0 1
159 S0110 FOTO SKEEN GLOW CARA cosmetologia_avanzada 3200.0 30 c108 0 1
160 S0066 FOTO SKEEN GLOW CARA cosmetologia_avanzada 15950.0 30 c108 0 1
161 S0111 LUMI SKEEN GLOW CARA CUELLO Y ESCOTE cosmetologia_avanzada 4000.0 30 c29 0 1
162 S0067 LUMI SKEEN GLOW CARA CUELLO Y ESCOTE cosmetologia_avanzada 19900.0 30 c29 0 1
163 S0112 CALECIM MESOTERAPIA FACIAL procedimientos_cosmeticos 3000.0 30 Tratamientos c90 Tratamientos 0 1
164 S0113 CALECIM MESOTERAPIA CAPILAR procedimientos_cosmeticos 3000.0 30 Tratamientos c90 Tratamientos 0 1
165 S0115 HAIR FILLER procedimientos_cosmeticos 5500.0 30 Tratamientos c90 Tratamientos 0 1
166 S0117 BOOSTR EXOSOMAS procedimientos_cosmeticos 1500.0 30 Tratamientos c90 Tratamientos 0 1
167 S0118 LASER CO2 CUELLO, ESCOTE O MANOS tecnologia_medica 6000.0 30 c108 0 1
168 S0119 LASER CO2 AREA ADICIONAL tecnologia_medica 4000.0 30 c108 0 1
169 S0120 RELLENO AQUA SHINE procedimientos_cosmeticos 6300.0 30 Tratamientos c90 Tratamientos 0 1
170 S0121 RELLENO STYLE 600 procedimientos_cosmeticos 7590.0 30 Tratamientos c90 Tratamientos 0 1
171 S0122 COSMELAN procedimientos_cosmeticos 9000.0 30 c93 0 1
172 S0123 BIOESTIMULACION CON APEE procedimientos_cosmeticos 4400.0 30 c93 0 1
173 S0124 FOTOHYDRAFACIAL cosmetologia_avanzada 4000.0 30 c78 0 1
174 S0127 FOTOHYDRAFACIAL cosmetologia_avanzada 9960.0 30 c78 0 1
175 S0247 enzimas promo 2 kits procedimientos_cosmeticos 5000.0 30 c93 0 0
176 S0248 QUANTA CICATRIZ 4CM cosmetologia_avanzada 3000.0 30 c116 0 1
177 S0137 QUANTA CICATRIZ 4CM cosmetologia_avanzada 7500.0 30 c116 0 1
178 S0251 RELLENO RESTYLANE LYFT procedimientos_cosmeticos 8000.0 30 c90 0 1
179 S0254 Mesoject BTX promo cosmetologia_avanzada 1800.0 30 Cabinas c117 Cabinas 0 1
180 S0129 Mesoject BTX promo cosmetologia_avanzada 4500.0 30 c117 0 1
181 S0255 Mesoject Hair cosmetologia_avanzada 2300.0 30 Cabinas c117 Cabinas 0 1
182 S0128 Mesoject Hair cosmetologia_avanzada 7200.0 30 c117 0 1
183 S0256 HYDRAFACIAL PLATINUM cosmetologia_avanzada 3800.0 30 c78 0 1
184 S0257 HYDRAFACIAL CUELLO Y ESCOTE cosmetologia_avanzada 1955.0 30 c78 0 1
185 S0258 DUO PERK OJOS + 3 SESIONES DE NCTF procedimientos_cosmeticos 8000.0 30 c90 0 1
186 S0259 DUO PERK LABIOS + RESTYLANE KYSSE procedimientos_cosmeticos 8000.0 30 c90 0 1
187 S0260 PERK OJOS cosmetologia_avanzada 1300.0 30 c78 0 1
188 S0261 PERK LABIOS cosmetologia_avanzada 1000.0 30 c78 0 1
189 S0263 RELLENO ULTRADEEP procedimientos_cosmeticos 8000.0 30 c90 0 1
190 S0264 RELLENO GLOBAL ACTION procedimientos_cosmeticos 7000.0 30 c90 0 1
191 S0265 RELLENO MAILI procedimientos_cosmeticos 8600.0 30 c90 0 1
192 S0273 QUANTA CICATRIZ 5CM O MAS cosmetologia_avanzada 5000.0 30 c116 0 1
193 S0138 QUANTA CICATRIZ 5CM O MAS cosmetologia_avanzada 13000.0 30 c116 0 1
194 S0274 QUANTA TONNING CARA cosmetologia_avanzada 3000.0 30 c116 0 1
195 S0139 QUANTA TONNING CARA cosmetologia_avanzada 7500.0 30 c116 0 1
196 S0275 QUANTA FRACCIONADO CARA Y CUELLO cosmetologia_avanzada 7000.0 30 c116 0 1
197 S0140 QUANTA FRACCIONADO CARA Y CUELLO cosmetologia_avanzada 17500.0 30 c116 0 1
198 S0276 REDTOUCH CARA cosmetologia_avanzada 3000.0 30 c44 0 1
199 S0193 REDTOUCH CARA cosmetologia_avanzada 14940.0 30 c44 0 1
200 S0277 REDTOUCH PELO cosmetologia_avanzada 1500.0 30 c44 0 1
201 S0194 REDTOUCH PELO cosmetologia_avanzada 9960.0 30 c44 0 1
202 S0195 REDTOUCH PELO cosmetologia_avanzada 4980.0 30 c44 0 1
203 S0278 REDTOUCH LABIOS cosmetologia_avanzada 1200.0 30 c44 0 1
204 S0196 REDTOUCH LABIOS cosmetologia_avanzada 2980.0 30 c44 0 1
205 S0279 RED TOUCH CARA PROMO FEB-ABRIL cosmetologia_avanzada 3000.0 30 c44 0 0
206 S0150 RED TOUCH CARA PROMO FEB-ABRIL cosmetologia_avanzada 14400.0 30 c44 0 0
207 S0280 RED TOUCH PELO PROMO FEB-ABRIL cosmetologia_avanzada 1120.0 30 c44 0 0
208 S0142 RED TOUCH PELO PROMO FEB-ABRIL cosmetologia_avanzada 8960.0 30 c44 0 0
209 S0143 RED TOUCH PELO PROMO FEB-ABRIL cosmetologia_avanzada 4480.0 30 c44 0 0
210 S0281 RED TOUCH LABIOS PROMO FEB-ABRIL cosmetologia_avanzada 800.0 30 c44 0 0
211 S0145 RED TOUCH LABIOS PROMO FEB-ABRIL cosmetologia_avanzada 2400.0 30 c44 0 0
212 S0282 ENZIMAS 2 KITS + 4 VENUS EN PAPADA PROMO JULIO-SEPT cosmetologia_avanzada 9600.0 30 c113 0 0
213 S0283 ENZIMAS 3 KITS + 6 VENUS ABDOMEN PROMO JULIO-SEPT cosmetologia_avanzada 19000.0 30 c113 0 0
214 S0284 HOLLYWOOD + RED TOUCH cosmetologia_avanzada 4500.0 30 c44 0 1
215 S0146 HOLLYWOOD + RED TOUCH cosmetologia_avanzada 25110.0 30 c44 0 1
216 S0285 QUANTA FRACCIONADO + RED TOUCH CARA cosmetologia_avanzada 6700.0 30 c44 0 1
217 S0148 QUANTA FRACCIONADO + RED TOUCH CARA cosmetologia_avanzada 16700.0 30 c44 0 1
218 S0286 QUANTA FRACCIONADO + RED TOUCH CARA CUELLO cosmetologia_avanzada 8500.0 30 c44 0 1
219 S0149 QUANTA FRACCIONADO + RED TOUCH CARA CUELLO cosmetologia_avanzada 21150.0 30 c44 0 1
220 S0289 DEPILACION LASER o IPL BARBA cosmetologia_avanzada 420.0 30 c114 0 1
221 S0151 DEPILACION LASER o IPL BARBA cosmetologia_avanzada 2090.0 30 c114 0 1
222 S0188 DEPILACION LASER o IPL BARBA cosmetologia_avanzada 1680.0 30 c114 0 0
223 S0290 DEPILACION LASER o IPL PANTORRILLA cosmetologia_avanzada 735.0 30 c114 0 1
224 S0152 DEPILACION LASER o IPL PANTORRILLA cosmetologia_avanzada 3660.0 30 c114 0 1
225 S0189 DEPILACION LASER o IPL PANTORRILLA cosmetologia_avanzada 2940.0 30 c114 0 0
226 S0291 DEPILACION LASER o IPL AXILA cosmetologia_avanzada 525.0 30 c114 0 1
227 S0153 DEPILACION LASER o IPL AXILA cosmetologia_avanzada 2615.0 30 c114 0 1
228 S0190 DEPILACION LASER o IPL AXILA cosmetologia_avanzada 2100.0 30 c114 0 0
229 S0292 DEPILACION LASER o IPL PATILLAS cosmetologia_avanzada 420.0 30 c114 0 1
230 S0154 DEPILACION LASER o IPL PATILLAS cosmetologia_avanzada 2090.0 30 c114 0 1
231 S0191 DEPILACION LASER o IPL PATILLAS cosmetologia_avanzada 1050.0 30 c114 0 1
232 S0298 DEPILACION LASER o IPL PECHO cosmetologia_avanzada 1260.0 30 c114 0 1
233 S0160 DEPILACION LASER o IPL PECHO cosmetologia_avanzada 6275.0 30 c114 0 1
234 S0192 DEPILACION LASER o IPL PECHO cosmetologia_avanzada 5040.0 30 c114 0 1
235 S0300 NATURAL PEEL cosmetologia_avanzada 3500.0 30 c116 0 1
236 S0164 NATURAL PEEL cosmetologia_avanzada 15000.0 30 c116 0 1
237 S0301 RED TOUCH CARA CUELLO Y ESCOTE cosmetologia_avanzada 4500.0 30 c44 0 1
238 S0165 RED TOUCH CARA CUELLO Y ESCOTE cosmetologia_avanzada 22410.0 30 c101 0 1
239 S0302 MORPHEUS CARA Y CUELLO tecnologia_medica 19000.0 30 c105 0 1
240 S0168 MORPHEUS CARA Y CUELLO tecnologia_medica 47310.0 30 c105 0 1
241 S0303 MORPHEUS CARA CUELLO Y ESCOTE tecnologia_medica 22000.0 30 c105 0 1
242 S0167 MORPHEUS CARA CUELLO Y ESCOTE tecnologia_medica 54780.0 30 c105 0 1
243 S0304 MORPHEUS CUERPO tecnologia_medica 24000.0 30 c105 0 1
244 S0169 MORPHEUS CUERPO tecnologia_medica 59760.0 30 c105 0 1
245 S0307 Morpheus 8 NO SHOW tecnologia_medica 2000.0 30 c105 0 0
246 S0308 MORPHEUS 3 SESIONES CARA Y CUELLO UN SOLO PAGO tecnologia_medica 40000.0 30 c105 0 0
247 S0309 MORPHEUS ABDOMEN CUERPO UN SOLO PAGO 3 SESIONES tecnologia_medica 50000.0 30 c105 0 0
248 S0310 Lumecca estrías por zona cosmetologia_avanzada 2200.0 30 c82 0 1
249 S0311 Lumecca cosmetologia_avanzada 4200.0 30 c82 0 1
250 S0170 Lumecca cosmetologia_avanzada 10450.0 30 c82 0 1
251 S0312 Morpheus8 Cara tecnologia_medica 16000.0 30 c105 0 1
252 S0171 Morpheus8 Cara tecnologia_medica 39840.0 30 c105 0 1
253 S0314 QUANTA CARA CUELLO Y ESCOTE cosmetologia_avanzada 7500.0 30 c116 0 1
254 S0172 QUANTA CARA CUELLO Y ESCOTE cosmetologia_avanzada 18650.0 30 c116 0 1
255 S0315 QUANTA + RED TOUCH CARA CUELLO Y ESCOTE cosmetologia_avanzada 9500.0 30 c44 0 1
256 S0173 QUANTA + RED TOUCH CARA CUELLO Y ESCOTE cosmetologia_avanzada 23650.0 30 c44 0 1
257 S0316 RED TOUCH CARA Y CUELLO cosmetologia_avanzada 4200.0 30 c44 0 1
258 S0174 RED TOUCH CARA Y CUELLO cosmetologia_avanzada 20900.0 30 c44 0 1
259 S0317 HOLLYWOOD + RED TOUCH CARA Y CUELLO cosmetologia_avanzada 5500.0 30 c44 0 1
260 S0197 HOLLYWOOD + RED TOUCH CARA Y CUELLO cosmetologia_avanzada 27390.0 30 c44 0 1
261 S0318 HOLLYWOOD CARA Y CUELLO cosmetologia_avanzada 3000.0 30 c44 0 1
262 S0198 HOLLYWOOD CARA Y CUELLO cosmetologia_avanzada 12000.0 30 c44 1 1
263 S0320 FOTOSKEEN GLUTEOS cosmetologia_avanzada 2700.0 30 c108 0 1
264 S0200 FOTOSKEEN GLUTEOS cosmetologia_avanzada 13500.0 30 c108 0 1
265 S0321 LASER CO2 CARA ENERGIA ALTA tecnologia_medica 20000.0 30 c105 0 1
266 S0322 LASER CO2 CARA CUELLO Y ESCOTE ENERGIA MEDIA tecnologia_medica 20000.0 30 c105 0 1
267 S0323 LASAER CO2 CUELLO tecnologia_medica 6000.0 30 c105 0 1
268 S0324 LASER CO2 PARPADOS tecnologia_medica 6000.0 30 c105 0 1
269 S0325 LASER CO2 BRAZOS COMPLETOS tecnologia_medica 15000.0 30 c105 0 1
270 S0326 BOOSTER PBSERUM cosmetologia_avanzada 2200.0 30 c35 0 1
271 S0327 BIOESTUMULACION CON PBSERUM procedimientos_cosmeticos 3000.0 30 c35 0 1
272 S0328 HYDRAGLOW cosmetologia_avanzada 4000.0 30 c78 0 1
273 S0329 HYDRAFLSH cosmetologia_avanzada 2000.0 30 c78 0 1
274 S0330 CONSULTA + LUMISKEEN servicios_medicos 1800.0 30 c119 0 1
275 S0331 Consulta primera vez 1 sesión Dra Ramos servicios_medicos 2000.0 30 verdelimon03 0 1
276 S0332 CONSULTA DE PELO DRA RAMOS O DR VEGA PRIMERA VEZ servicios_medicos 2000.0 30 verdelimon03 0 1
277 S0333 CONSULTA PELO DRA RAMOS O DR VEGA SUBSECUENTES servicios_medicos 1500.0 30 c119 0 1
278 S0334 Consulta primera vez 1 sesión PRECIO 2025 servicios_medicos 1500.0 30 verdelimon03 0 1
279 S0335 CONSULTA SUBSECUENTE PRECIO 2025 servicios_medicos 1400.0 30 c119 0 1
280 S0336 LASER CO2 ENERGIAS ALTAS O CON SEDACION tecnologia_medica 20000.0 30 c108 0 1
281 S0337 LASER CO2 CUELLO O ESCOTE O MANOS tecnologia_medica 6500.0 30 c108 0 1
282 S0338 LASER CO2 ADICIONAL tecnologia_medica 4000.0 30 c108 0 1
283 S0339 LASER CO2 PARPADOS tecnologia_medica 5000.0 30 c108 0 1
284 S0340 FACIAL + PDRN cosmetologia_basica 3500.0 30 c48 0 1
285 S0204 FACIAL + PDRN cosmetologia_basica 8400.0 30 c106 0 1
286 S0341 QUANTA FRACCIONADO + PDRN cosmetologia_avanzada 6600.0 30 c116 0 1
287 S0342 HOLLYWOOD + PDRN cosmetologia_avanzada 4500.0 30 c116 0 1
288 S0343 KENALOG servicios_medicos 0.01 30 rosapalo02 0 1
289 S0344 BIOESTIMULACION ADN SALMON procedimientos_cosmeticos 3500.0 30 c90 0 1
290 S0345 Consulta primera vez Dr. Cerecer servicios_medicos 1600.0 30 verdelimon03 0 1
291 S0346 Consulta Dr. Cerecer servicios_medicos 1500.0 30 c119 0 1
292 S0347 ANTICIPO CREMA HORMONAL procedimientos_cosmeticos 1000.0 30 verdelimon02 0 1
293 S0348 LIQUIDACION CREMA HORMONAL procedimientos_cosmeticos 0.0 30 verdelimon02 0 1
294 S0349 NANOPOR + PDRN SALMON cosmetologia_avanzada 4000.0 30 c42 0 1

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

702
migracion/extraer_skeen.py Executable file
View File

@@ -0,0 +1,702 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Extracción read-only de datos del sistema legacy SKEEN.
Uso autorizado por el responsable del proyecto con credenciales proporcionadas.
Extrae pacientes (con expediente clínico completo), citas, ventas, monederos, servicios, etc.
"""
import requests
import csv
import json
import re
import sys
import time
from pathlib import Path
from datetime import datetime, timedelta
from concurrent.futures import ThreadPoolExecutor, as_completed
from bs4 import BeautifulSoup
BASE = 'https://sistema.skeenmx.app'
USERNAME = 'JoseSkeen'
PASSWORD = 'SKr#902_88'
OUTDIR = Path('/root/migracion/extraccion')
OUTDIR.mkdir(parents=True, exist_ok=True)
CACHE = OUTDIR / 'detalle_pacientes.json'
session = requests.Session()
def log(msg):
print(f'[{datetime.now().strftime("%H:%M:%S")}] {msg}')
def login():
log('Iniciando sesión...')
r = session.get(f'{BASE}/login', timeout=30)
r.raise_for_status()
soup = BeautifulSoup(r.text, 'html.parser')
token = soup.find('input', {'name': '_token'})
token = token['value'] if token else ''
data = {
'_token': token,
'username': USERNAME,
'password': PASSWORD,
}
r = session.post(f'{BASE}/login', data=data, timeout=30, allow_redirects=True)
r.raise_for_status()
if '/login' in r.url or 'estas credenciales' in r.text.lower():
raise Exception('Login fallido. Verifica credenciales.')
log('Login exitoso')
return True
def dt_params(start=0, length=100, search=''):
return {
'draw': 1,
'columns[0][data]': 'id',
'columns[0][name]': '',
'columns[0][searchable]': 'true',
'columns[0][orderable]': 'true',
'columns[0][search][value]': '',
'columns[0][search][regex]': 'false',
'start': start,
'length': length,
'search[value]': search,
'search[regex]': 'false',
'order[0][column]': '0',
'order[0][dir]': 'asc',
'_': int(time.time() * 1000),
}
def fetch_json(url, params=None):
headers = {'X-Requested-With': 'XMLHttpRequest'}
r = session.get(f'{BASE}{url}', params=params or {}, headers=headers, timeout=120)
r.raise_for_status()
return r.json()
def paginate(url, params_builder, label, page_size=500, max_pages=None):
log(f'Extrayendo {label} desde {url}...')
all_rows = []
start = 0
page = 0
while True:
params = params_builder(start, page_size)
try:
data = fetch_json(url, params)
except Exception as e:
log(f' ERROR página {page}: {e}')
break
rows = data.get('data', [])
if not rows:
break
all_rows.extend(rows)
total = data.get('recordsTotal') or data.get('recordsFiltered') or 0
log(f' página {page + 1}: {len(rows)} filas (total acumulado: {len(all_rows)}/{total})')
if len(rows) < page_size:
break
start += page_size
page += 1
if max_pages and page >= max_pages:
break
time.sleep(0.3)
log(f'{label}: {len(all_rows)} registros extraídos')
return all_rows
def save_csv(name, rows, fieldnames):
path = OUTDIR / f'{name}.csv'
with open(path, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction='ignore')
writer.writeheader()
writer.writerows(rows)
log(f'Guardado {path}: {len(rows)} registros')
MESES_ES = {
'enero': '01', 'febrero': '02', 'marzo': '03', 'abril': '04',
'mayo': '05', 'junio': '06', 'julio': '07', 'agosto': '08',
'septiembre': '09', 'octubre': '10', 'noviembre': '11', 'diciembre': '12',
}
def parse_spanish_date(text):
text = (text or '').strip().lower()
if not text:
return ''
m = re.search(r'(\d{1,2})\s+de\s+([a-záéíóúñ]+)\s+(?:del|de)\s+(\d{4})', text)
if not m:
return ''
dia, mes, anio = m.groups()
mes_num = MESES_ES.get(mes)
if not mes_num:
return ''
return f'{anio}-{mes_num}-{int(dia):02d}'
def normalize_phone(phone):
phone = re.sub(r'\D', '', (phone or ''))
if phone and not phone.startswith('52') and len(phone) == 10:
phone = '52' + phone
return phone
def parse_patient_detail(html, fallback_name=''):
"""Extrae datos personales e historia clínica completa del HTML del expediente."""
soup = BeautifulSoup(html, 'html.parser')
text = soup.get_text(separator='\n')
lines = [l.strip() for l in text.split('\n') if l.strip()]
def find_line_after(keywords):
for i, line in enumerate(lines):
low = line.lower()
if any(k in low for k in keywords):
if i + 1 < len(lines):
return lines[i + 1]
return ''
def find_pair(keywords):
for i, line in enumerate(lines):
low = line.lower()
if any(k in low for k in keywords):
# Si la respuesta está en la misma línea separada por ':' o espacio
if ':' in line:
return line.split(':', 1)[1].strip()
if i + 1 < len(lines):
return lines[i + 1]
return ''
def yes_no(value):
v = (value or '').lower()
if v.startswith('s') or v == 'yes':
return True
return False
# Extraer email con regex del texto completo
emails = re.findall(r'[\w.\-]+@[\w.\-]+', text)
email = emails[0] if emails else ''
if email.lower() in ('no@no.com', 'no@no'):
email = ''
data = {
'celular': '',
'whatsapp': '',
'email': email,
'fecha_nacimiento': '',
'sexo': '',
'lugar_nacimiento': '',
'empleo': '',
'estado_civil': '',
'contacto_emergencia': '',
'telefono_emergencia': '',
'telefono_casa': '',
'direccion': '',
'recomendado_por': '',
'comentarios': '',
'hijos': 0,
'embarazada': False,
'lactancia': False,
'anticonceptivos': False,
'ejercicio': False,
'alimentacion': False,
'peso_normal': False,
'enfermedad_seria': False,
'alcohol': False,
'problemas_rinon': False,
'problemas_espalda': False,
'problemas_cardiacos': False,
'problemas_respiratorios': False,
'alta_presion': False,
'baja_presion': False,
'diabetes': False,
'hipertiroidismo': False,
'hipotiroidismo': False,
'colitis': False,
'estrenimiento': False,
'problemas_higado': False,
'enfermedades_cronicas': False,
'cirugias': False,
'varices': False,
'migrana': False,
'desmaya_agujas': False,
'alergias': '',
'medicamentos': '',
'tratamientos_previos': '',
}
# Recorrer líneas buscando pares pregunta/respuesta
i = 0
while i < len(lines):
line = lines[i]
low = line.lower()
nxt = lines[i + 1] if i + 1 < len(lines) else ''
# Datos personales
if 'celular' in low and len(low) < 20:
data['celular'] = re.sub(r'\D', '', nxt)
elif 'whatsapp' in low and len(low) < 20:
data['whatsapp'] = re.sub(r'\D', '', nxt)
elif 'correo electrónico' in low or 'email' in low:
data['email'] = nxt if '@' in nxt else data['email']
elif 'fecha de nacimiento' in low:
data['fecha_nacimiento'] = parse_spanish_date(nxt or line)
elif 'lugar de nacimiento' in low:
data['lugar_nacimiento'] = nxt
elif low == 'sexo':
data['sexo'] = 'female' if 'femenino' in nxt.lower() else ('male' if 'masculino' in nxt.lower() else '')
elif 'empleo' in low:
data['empleo'] = nxt
elif 'estado civil' in low:
data['estado_civil'] = nxt
elif 'en caso de emergencia llamar a' in low:
parts = nxt.split()
if parts:
data['contacto_emergencia'] = parts[0]
data['telefono_emergencia'] = normalize_phone(' '.join(parts[1:]))
elif 'teléfono casa' in low or 'telefono casa' in low:
data['telefono_casa'] = re.sub(r'\D', '', nxt)
elif low == 'dirección' or low == 'direccion':
data['direccion'] = nxt
elif 'recomendado por' in low:
data['recomendado_por'] = nxt
elif 'comentarios adicionales' in low:
data['comentarios'] = nxt
# Historia clínica
elif 'tiene hijos' in low:
val = nxt.lower()
if val.isdigit():
data['hijos'] = int(val)
elif val.startswith('s'):
data['hijos'] = 1
elif 'embarazada' in low:
data['embarazada'] = yes_no(nxt)
elif 'lactancia' in low:
data['lactancia'] = yes_no(nxt)
elif 'anticonceptivo' in low:
data['anticonceptivos'] = yes_no(nxt)
elif 'hace ejercicio' in low:
data['ejercicio'] = yes_no(nxt)
elif 'come en forma adecuada' in low:
data['alimentacion'] = yes_no(nxt)
elif 'peso corporal normal' in low:
data['peso_normal'] = yes_no(nxt)
elif 'enfermedad seria' in low or 'incapacidad física' in low:
data['enfermedad_seria'] = yes_no(nxt)
elif 'bebidas alcohólicas' in low:
data['alcohol'] = yes_no(nxt)
elif 'riñones' in low or 'rinon' in low:
data['problemas_rinon'] = yes_no(nxt)
elif 'espalda' in low:
data['problemas_espalda'] = yes_no(nxt)
elif 'cardiacos' in low or 'cardíacos' in low:
data['problemas_cardiacos'] = yes_no(nxt)
elif 'respiratorios' in low:
data['problemas_respiratorios'] = yes_no(nxt)
elif 'alta presión' in low or 'alta presion' in low:
data['alta_presion'] = yes_no(nxt)
elif 'baja presión' in low or 'baja presion' in low:
data['baja_presion'] = yes_no(nxt)
elif 'diabetes' in low:
data['diabetes'] = yes_no(nxt)
elif 'hipertiroidismo' in low:
data['hipertiroidismo'] = yes_no(nxt)
elif 'hipotiroidismo' in low:
data['hipotiroidismo'] = yes_no(nxt)
elif 'colitis' in low:
data['colitis'] = yes_no(nxt)
elif 'estreñimiento' in low:
data['estrenimiento'] = yes_no(nxt)
elif 'hígado' in low or 'higado' in low or 'vesícula' in low:
data['problemas_higado'] = yes_no(nxt)
elif 'enfermedades crónicas' in low or 'enfermedades cronicas' in low:
data['enfermedades_cronicas'] = yes_no(nxt)
elif 'realizado cirugías' in low or 'realizado cirugias' in low:
data['cirugias'] = yes_no(nxt)
elif 'varicosas' in low or 'varices' in low:
data['varices'] = yes_no(nxt)
elif 'migraña' in low or 'migrana' in low:
data['migrana'] = yes_no(nxt)
elif 'desmaya' in low and 'agujas' in low:
data['desmaya_agujas'] = yes_no(nxt)
elif low == 'alergias':
data['alergias'] = nxt if nxt.lower() != 'no' else ''
elif 'medicamentos actualmente' in low:
data['medicamentos'] = nxt if nxt.lower() != 'no' else ''
elif 'tratamientos médico' in low or 'tratamientos medico' in low or 'tratamientos cosméticos previos' in low:
data['tratamientos_previos'] = nxt if nxt.lower() != 'no' else ''
i += 1
return data
def load_detail_cache():
if CACHE.exists():
try:
with open(CACHE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
log(f'No se pudo cargar caché: {e}')
return {}
def save_detail_cache(cache):
with open(CACHE, 'w', encoding='utf-8') as f:
json.dump(cache, f, ensure_ascii=False, indent=2)
def fetch_one_detail(legacy_id, fallback_name):
try:
resp = session.get(f'{BASE}/expediente/ver/{legacy_id}', timeout=30)
resp.raise_for_status()
return str(legacy_id), parse_patient_detail(resp.text, fallback_name)
except Exception as e:
return str(legacy_id), {'_error': str(e)}
def extract_paciente_details(rows, cache, max_workers=10):
to_fetch = []
for r in rows:
legacy_id = r.get('id')
if not legacy_id:
continue
sid = str(legacy_id)
if sid in cache and cache[sid].get('celular'):
continue
to_fetch.append((legacy_id, r.get('nombre', '')))
log(f'Extrayendo detalles de {len(to_fetch)} pacientes con {max_workers} workers...')
total = len(to_fetch)
completed = 0
with ThreadPoolExecutor(max_workers=max_workers) as ex:
future_to_id = {ex.submit(fetch_one_detail, lid, name): lid for lid, name in to_fetch}
for future in as_completed(future_to_id):
lid, detail = future.result()
if '_error' not in detail:
cache[lid] = detail
completed += 1
if completed % 500 == 0:
log(f' {completed}/{total} detalles procesados')
save_detail_cache(cache)
time.sleep(0.01)
save_detail_cache(cache)
log('Detalles de pacientes completados')
def extract_expedientes():
rows = paginate('/expedientes/json', lambda s, l: dt_params(s, l), 'pacientes')
cache = load_detail_cache()
extract_paciente_details(rows, cache)
out = []
for r in rows:
nombre = (r.get('nombre') or '').strip()
if not nombre or nombre.upper() == '. . NO CITAR':
continue
detail = cache.get(str(r.get('id', '')), {})
phone = detail.get('celular') or detail.get('whatsapp') or (r.get('telefono') or '').strip()
phone = normalize_phone(phone)
email = detail.get('email', '')
if email.lower() in ('no@no.com', 'no@no'):
email = ''
out.append({
'legacy_id': r.get('id', ''),
'folio': r.get('folio', ''),
'nombre': nombre,
'apellido_paterno': '',
'apellido_materno': '',
'telefono': phone,
'email': email,
'fecha_nacimiento': detail.get('fecha_nacimiento', ''),
'sexo': detail.get('sexo', ''),
'lugar_nacimiento': detail.get('lugar_nacimiento', ''),
'empleo': detail.get('empleo', ''),
'estado_civil': detail.get('estado_civil', ''),
'contacto_emergencia': detail.get('contacto_emergencia', ''),
'telefono_emergencia': detail.get('telefono_emergencia', ''),
'telefono_casa': detail.get('telefono_casa', ''),
'direccion': detail.get('direccion', ''),
'recomendado_por': detail.get('recomendado_por', ''),
'comentarios': detail.get('comentarios', ''),
'medico_principal': r.get('medico_principal') or '',
'etiquetas': ','.join(str(c) for c in (r.get('condiciones') or [])) if r.get('condiciones') else '',
'vip': '1' if r.get('vip') else '0',
'fuente': 'legacy',
# Historia clínica
'hijos': detail.get('hijos', 0),
'embarazada': '1' if detail.get('embarazada') else '0',
'lactancia': '1' if detail.get('lactancia') else '0',
'anticonceptivos': '1' if detail.get('anticonceptivos') else '0',
'ejercicio': '1' if detail.get('ejercicio') else '0',
'alimentacion': '1' if detail.get('alimentacion') else '0',
'peso_normal': '1' if detail.get('peso_normal') else '0',
'enfermedad_seria': '1' if detail.get('enfermedad_seria') else '0',
'alcohol': '1' if detail.get('alcohol') else '0',
'problemas_rinon': '1' if detail.get('problemas_rinon') else '0',
'problemas_espalda': '1' if detail.get('problemas_espalda') else '0',
'problemas_cardiacos': '1' if detail.get('problemas_cardiacos') else '0',
'problemas_respiratorios': '1' if detail.get('problemas_respiratorios') else '0',
'alta_presion': '1' if detail.get('alta_presion') else '0',
'baja_presion': '1' if detail.get('baja_presion') else '0',
'diabetes': '1' if detail.get('diabetes') else '0',
'hipertiroidismo': '1' if detail.get('hipertiroidismo') else '0',
'hipotiroidismo': '1' if detail.get('hipotiroidismo') else '0',
'colitis': '1' if detail.get('colitis') else '0',
'estrenimiento': '1' if detail.get('estrenimiento') else '0',
'problemas_higado': '1' if detail.get('problemas_higado') else '0',
'enfermedades_cronicas': '1' if detail.get('enfermedades_cronicas') else '0',
'cirugias': '1' if detail.get('cirugias') else '0',
'varices': '1' if detail.get('varices') else '0',
'migrana': '1' if detail.get('migrana') else '0',
'desmaya_agujas': '1' if detail.get('desmaya_agujas') else '0',
'alergias': detail.get('alergias', ''),
'medicamentos': detail.get('medicamentos', ''),
'tratamientos_previos': detail.get('tratamientos_previos', ''),
})
save_csv('pacientes', out, [
'legacy_id', 'folio', 'nombre', 'apellido_paterno', 'apellido_materno',
'telefono', 'email', 'fecha_nacimiento', 'sexo', 'lugar_nacimiento',
'empleo', 'estado_civil', 'contacto_emergencia', 'telefono_emergencia',
'telefono_casa', 'direccion', 'recomendado_por', 'comentarios',
'medico_principal', 'etiquetas', 'vip', 'fuente',
'hijos', 'embarazada', 'lactancia', 'anticonceptivos', 'ejercicio',
'alimentacion', 'peso_normal', 'enfermedad_seria', 'alcohol',
'problemas_rinon', 'problemas_espalda', 'problemas_cardiacos',
'problemas_respiratorios', 'alta_presion', 'baja_presion', 'diabetes',
'hipertiroidismo', 'hipotiroidismo', 'colitis', 'estrenimiento',
'problemas_higado', 'enfermedades_cronicas', 'cirugias', 'varices',
'migrana', 'desmaya_agujas', 'alergias', 'medicamentos', 'tratamientos_previos'
])
return out
def extract_monederos():
rows = paginate('/monederos/json', lambda s, l: dt_params(s, l), 'monederos')
out = []
for r in rows:
saldo = r.get('saldo') or {}
phone = normalize_phone(r.get('celular') or '')
out.append({
'telefono': phone,
'puntos': float(saldo.get('mn') or 0),
'activo': '1' if r.get('activado') else '0',
})
save_csv('monederos', out, ['telefono', 'puntos', 'activo'])
return out
def extract_crm():
rows = paginate('/crm/prospectos/json', lambda s, l: dt_params(s, l), 'crm prospectos')
out = []
for r in rows:
out.append({
'nombre': r.get('nombre') or '',
'folio': r.get('folio') or '',
'status': r.get('status') or 0,
'total_citas': r.get('total_citas') or 0,
'siguiente_cita': r.get('siguiente_cita') or '',
})
save_csv('crm_prospectos', out, ['nombre', 'folio', 'status', 'total_citas', 'siguiente_cita'])
return out
def extract_servicios():
rows = paginate('/servicios/json', lambda s, l: dt_params(s, l), 'servicios')
out = []
for r in rows:
precio_str = (r.get('precio_minimo') or '0').replace('$', '').replace('m.n.', '').replace(',', '').strip()
try:
precio = float(precio_str.split()[0])
except Exception:
precio = 0
out.append({
'codigo': f"S{r.get('id', 0):04d}",
'nombre': r.get('servicio') or '',
'categoria': (r.get('categoria') or '').lower().replace(' ', '_'),
'precio': precio,
'duracion_min': 30,
'descripcion': r.get('grupo_texto') or '',
'color': r.get('color') or '#1abc9c',
'grupo': r.get('grupo_texto') or '',
'favorito': '1' if r.get('favorito') else '0',
'activo': '1' if r.get('activado') else '0',
})
save_csv('servicios', out, [
'codigo', 'nombre', 'categoria', 'precio', 'duracion_min',
'descripcion', 'color', 'grupo', 'favorito', 'activo'
])
return out
def extract_ventas(start_date='2022-01-01', end_date='2026-07-06'):
log(f'Extrayendo ventas de {start_date} a {end_date}...')
phone_by_legacy = {}
pac_path = OUTDIR / 'pacientes.csv'
if pac_path.exists():
with open(pac_path, newline='', encoding='utf-8') as f:
for row in csv.DictReader(f):
phone_by_legacy[row.get('legacy_id', '')] = row.get('telefono', '')
all_rows = []
current = datetime.strptime(start_date, '%Y-%m-%d').date()
end = datetime.strptime(end_date, '%Y-%m-%d').date()
while current <= end:
chunk_end = min(current + timedelta(days=30), end)
url = f"/ventas/json/{current.strftime('%Y-%m-%d')}/{chunk_end.strftime('%Y-%m-%d')}"
try:
data = fetch_json(url, dt_params(0, 10000))
rows = data.get('data', [])
all_rows.extend(rows)
log(f' {current} - {chunk_end}: {len(rows)} ventas (total: {len(all_rows)})')
except Exception as e:
log(f' ERROR {current}-{chunk_end}: {e}')
current = chunk_end + timedelta(days=1)
time.sleep(0.5)
out = []
for r in all_rows:
expediente_id = r.get('expedientes_id') or ''
phone = normalize_phone(phone_by_legacy.get(str(expediente_id), ''))
conceptos = r.get('conceptos') or []
if not conceptos:
conceptos = [{'titulo': r.get('motivo') or 'Servicio', 'subtotal': r.get('subtotal') or 0}]
for c in conceptos:
precio = 0
try:
precio = float(c.get('subtotal') or c.get('pagado') or 0)
except Exception:
pass
out.append({
'legacy_id': r.get('id', ''),
'expedientes_id': expediente_id,
'paciente_telefono': phone,
'paciente_nombre': r.get('nombre') or '',
'fecha': (r.get('created_at') or '')[:10],
'servicio_codigo': '',
'servicio_nombre': c.get('titulo') or '',
'cantidad': c.get('cantidad', 1) or 1,
'precio_unitario': precio,
'descuento': 0,
'impuesto': 0,
'estado': 'confirmed' if not r.get('cancelada') else 'cancelled',
'notas': r.get('motivo') or '',
})
save_csv('ventas', out, [
'legacy_id', 'expedientes_id', 'paciente_telefono', 'paciente_nombre', 'fecha',
'servicio_codigo', 'servicio_nombre', 'cantidad', 'precio_unitario',
'descuento', 'impuesto', 'estado', 'notas'
])
return out
def extract_citas(start_date='2022-11-01', end_date='2026-07-06'):
log(f'Extrayendo citas de {start_date} a {end_date}...')
phone_by_legacy = {}
pac_path = OUTDIR / 'pacientes.csv'
if pac_path.exists():
with open(pac_path, newline='', encoding='utf-8') as f:
for row in csv.DictReader(f):
phone_by_legacy[row.get('legacy_id', '')] = row.get('telefono', '')
medicos_map = {
'32': 'Dra. Alejandra Ramos',
'33': 'Dra. Lidia Martinez',
'46': 'Dra. Fernanda Cerecer',
'49': 'RODRIGUEZ FRIDA',
'54': 'Dr. Benjamín Adrián',
'79': 'XIMENA',
'80': 'ELY',
}
all_citas = []
current = datetime.strptime(start_date, '%Y-%m-%d').date()
end = datetime.strptime(end_date, '%Y-%m-%d').date()
days_with = 0
while current <= end:
url = f"/agenda/citas/{current.strftime('%Y-%m-%d')}"
try:
data = fetch_json(url)
citas = data.get('citas', [])
if citas:
days_with += 1
all_citas.extend(citas)
if current.day == 1 or len(citas) > 0:
log(f' {current}: {len(citas)} citas (total acumulado: {len(all_citas)})')
except Exception as e:
log(f' ERROR {current}: {e}')
current += timedelta(days=1)
time.sleep(0.05)
out = []
for c in all_citas:
nombre = (c.get('nombre') or '').strip()
if not nombre or nombre.upper().startswith('. . NO CITAR'):
continue
expediente_id = c.get('expedientes_id') or ''
phone = normalize_phone(c.get('telefono') or phone_by_legacy.get(str(expediente_id), ''))
if not phone and c.get('whatsapp'):
phone = normalize_phone(c.get('whatsapp'))
email = (c.get('correo') or '').strip()
if email.lower() in ('no@no.com', 'no@no'):
email = ''
inicio = c.get('inicio') or ''
fin = c.get('fin') or ''
hora_str = ''
if len(inicio) == 4:
hora_str = f"{inicio[:2]}:{inicio[2:]}"
elif len(inicio) == 3:
hora_str = f"0{inicio[0]}:{inicio[1:]}"
medico_id = str(c.get('medicos_id') or '')
doctor_nombre = medicos_map.get(medico_id, medico_id if medico_id and medico_id != '0' else '')
out.append({
'legacy_id': c.get('id', ''),
'expedientes_id': expediente_id,
'paciente_telefono': phone,
'paciente_nombre': nombre,
'servicio_codigo': '',
'servicio_nombre': c.get('titulo') or '',
'fecha': c.get('fecha', ''),
'hora': hora_str,
'hora_fin': f"{fin[:2]}:{fin[2:]}" if len(fin) == 4 else '',
'estado': 'confirmed',
'doctor_nombre': doctor_nombre,
'sucursal': 'rosarito',
'medio': 'onsite',
'notas': c.get('observaciones') or '',
'estado_pago': 'not_paid',
'monto_pagado': 0,
})
save_csv('citas', out, [
'legacy_id', 'expedientes_id', 'paciente_telefono', 'paciente_nombre',
'servicio_codigo', 'servicio_nombre', 'fecha', 'hora', 'hora_fin',
'estado', 'doctor_nombre', 'sucursal', 'medio', 'notas',
'estado_pago', 'monto_pagado'
])
log(f'Citas: {len(out)} registros útiles en {days_with} días con citas')
return out
def main():
try:
login()
except Exception as e:
log(f'ERROR login: {e}')
sys.exit(1)
extract_expedientes()
extract_monederos()
extract_crm()
extract_servicios()
extract_ventas()
extract_citas()
log('Extracción completada. Archivos en /root/migracion/extraccion/')
if __name__ == '__main__':
main()

517
migracion/extraer_skeen.py.bak Executable file
View File

@@ -0,0 +1,517 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Extracción read-only de datos del sistema legacy SKEEN.
Uso autorizado por el responsable del proyecto con credenciales proporcionadas.
Solo lee datos; no modifica nada en https://sistema.skeenmx.app/
"""
import requests
import csv
import json
import re
import sys
import time
from pathlib import Path
from datetime import datetime, timedelta
from concurrent.futures import ThreadPoolExecutor, as_completed
from bs4 import BeautifulSoup
BASE = 'https://sistema.skeenmx.app'
USERNAME = 'JoseSkeen'
PASSWORD = 'SKr#902_88'
OUTDIR = Path('/root/migracion/extraccion')
OUTDIR.mkdir(parents=True, exist_ok=True)
CACHE = OUTDIR / 'detalle_pacientes.json'
session = requests.Session()
def log(msg):
print(f'[{datetime.now().strftime("%H:%M:%S")}] {msg}')
def login():
log('Iniciando sesión...')
r = session.get(f'{BASE}/login', timeout=30)
r.raise_for_status()
soup = BeautifulSoup(r.text, 'html.parser')
token = soup.find('input', {'name': '_token'})
token = token['value'] if token else ''
data = {
'_token': token,
'username': USERNAME,
'password': PASSWORD,
}
r = session.post(f'{BASE}/login', data=data, timeout=30, allow_redirects=True)
r.raise_for_status()
if '/login' in r.url or 'estas credenciales' in r.text.lower():
raise Exception('Login fallido. Verifica credenciales.')
log('Login exitoso')
return True
def dt_params(start=0, length=100, search=''):
"""Genera parámetros DataTables server-side."""
return {
'draw': 1,
'columns[0][data]': 'id',
'columns[0][name]': '',
'columns[0][searchable]': 'true',
'columns[0][orderable]': 'true',
'columns[0][search][value]': '',
'columns[0][search][regex]': 'false',
'start': start,
'length': length,
'search[value]': search,
'search[regex]': 'false',
'order[0][column]': '0',
'order[0][dir]': 'asc',
'_': int(time.time() * 1000),
}
def fetch_json(url, params=None):
headers = {'X-Requested-With': 'XMLHttpRequest'}
r = session.get(f'{BASE}{url}', params=params or {}, headers=headers, timeout=120)
r.raise_for_status()
return r.json()
def paginate(url, params_builder, label, page_size=500, max_pages=None):
"""Recorre un endpoint DataTables paginado y devuelve todas las filas."""
log(f'Extrayendo {label} desde {url}...')
all_rows = []
start = 0
page = 0
while True:
params = params_builder(start, page_size)
try:
data = fetch_json(url, params)
except Exception as e:
log(f' ERROR página {page}: {e}')
break
rows = data.get('data', [])
if not rows:
break
all_rows.extend(rows)
total = data.get('recordsTotal') or data.get('recordsFiltered') or 0
log(f' página {page + 1}: {len(rows)} filas (total acumulado: {len(all_rows)}/{total})')
if len(rows) < page_size:
break
start += page_size
page += 1
if max_pages and page >= max_pages:
break
time.sleep(0.3)
log(f'{label}: {len(all_rows)} registros extraídos')
return all_rows
def save_csv(name, rows, fieldnames):
path = OUTDIR / f'{name}.csv'
with open(path, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction='ignore')
writer.writeheader()
writer.writerows(rows)
log(f'Guardado {path}: {len(rows)} registros')
MESES_ES = {
'enero': '01', 'febrero': '02', 'marzo': '03', 'abril': '04',
'mayo': '05', 'junio': '06', 'julio': '07', 'agosto': '08',
'septiembre': '09', 'octubre': '10', 'noviembre': '11', 'diciembre': '12',
}
def parse_spanish_date(text):
"""Convierte '05 de Septiembre del 1998''1998-09-05'."""
text = (text or '').strip().lower()
if not text:
return ''
m = re.search(r'(\d{1,2})\s+de\s+([a-záéíóúñ]+)\s+(?:del|de)\s+(\d{4})', text)
if not m:
return ''
dia, mes, anio = m.groups()
mes_num = MESES_ES.get(mes)
if not mes_num:
return ''
return f'{anio}-{mes_num}-{int(dia):02d}'
def parse_patient_detail(html, fallback_name=''):
"""Extrae teléfono, email, fecha de nacimiento y sexo del detalle."""
soup = BeautifulSoup(html, 'html.parser')
data = {
'celular': '',
'whatsapp': '',
'email': '',
'fecha_nacimiento': '',
'sexo': '',
}
# Campos de tablas
for tr in soup.find_all('tr'):
th = tr.find('th')
td = tr.find('td')
if not th or not td:
continue
key = ' '.join(th.get_text(strip=True).split()).lower().rstrip(':')
val = ' '.join(td.get_text(strip=True).split())
if key == 'celular':
data['celular'] = re.sub(r'\D', '', val)
elif key == 'whatsapp':
data['whatsapp'] = re.sub(r'\D', '', val)
elif key == 'sexo':
data['sexo'] = 'female' if 'femenino' in val.lower() else ('male' if 'masculino' in val.lower() else val)
elif key == 'fecha de nacimiento':
data['fecha_nacimiento'] = parse_spanish_date(val)
# Email suele aparecer como texto suelto o en un mailto
emails = re.findall(r'[\w.\-]+@[\w.\-]+', soup.get_text())
if emails:
data['email'] = emails[0]
return data
def load_detail_cache():
if CACHE.exists():
try:
with open(CACHE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
log(f'No se pudo cargar caché: {e}')
return {}
def save_detail_cache(cache):
with open(CACHE, 'w', encoding='utf-8') as f:
json.dump(cache, f, ensure_ascii=False, indent=2)
def fetch_one_detail(legacy_id, fallback_name):
try:
resp = session.get(f'{BASE}/expediente/ver/{legacy_id}', timeout=30)
resp.raise_for_status()
return str(legacy_id), parse_patient_detail(resp.text, fallback_name)
except Exception as e:
return str(legacy_id), {'_error': str(e)}
def extract_paciente_details(rows, cache, max_workers=10):
"""Recorre /expediente/ver/{id} para enriquecer teléfono/email/etc."""
to_fetch = []
for r in rows:
legacy_id = r.get('id')
if not legacy_id:
continue
sid = str(legacy_id)
# Solo re-extraer si no tenemos teléfono o no está cacheado
if sid in cache and (cache[sid].get('celular') or cache[sid].get('whatsapp')):
continue
to_fetch.append((legacy_id, r.get('nombre', '')))
log(f'Extrayendo detalles de {len(to_fetch)} pacientes con {max_workers} workers...')
total = len(to_fetch)
completed = 0
with ThreadPoolExecutor(max_workers=max_workers) as ex:
future_to_id = {ex.submit(fetch_one_detail, lid, name): lid for lid, name in to_fetch}
for future in as_completed(future_to_id):
lid, detail = future.result()
if '_error' not in detail:
cache[lid] = detail
completed += 1
if completed % 500 == 0:
log(f' {completed}/{total} detalles procesados')
save_detail_cache(cache)
time.sleep(0.01) # pequeña pausa entre completados
save_detail_cache(cache)
log('Detalles de pacientes completados')
def extract_expedientes():
rows = paginate('/expedientes/json', lambda s, l: dt_params(s, l), 'pacientes')
cache = load_detail_cache()
extract_paciente_details(rows, cache)
out = []
for r in rows:
nombre = (r.get('nombre') or '').strip()
if not nombre or nombre.upper() == '. . NO CITAR':
continue
detail = cache.get(str(r.get('id', '')), {})
# Prioridad: celular > whatsapp > teléfono del listado
phone = detail.get('celular') or detail.get('whatsapp') or (r.get('telefono') or '').strip()
# Limpieza: quitar todo excepto dígitos; si empieza con 52 y tiene 12+ dígitos dejarlo, sino agregar prefijo MX si aplica
phone = re.sub(r'\D', '', phone)
if phone and not phone.startswith('52') and len(phone) == 10:
phone = '52' + phone
email = detail.get('email', '')
# Algunos correos dummy los ignoramos
if email.lower() in ('no@no.com', 'no@no'):
email = ''
out.append({
'legacy_id': r.get('id', ''),
'folio': r.get('folio', ''),
'nombre': nombre,
'apellido_paterno': '',
'apellido_materno': '',
'telefono': phone,
'email': email,
'fecha_nacimiento': detail.get('fecha_nacimiento', ''),
'sexo': detail.get('sexo', ''),
'medico_principal': r.get('medico_principal') or '',
'etiquetas': ','.join(str(c) for c in (r.get('condiciones') or [])) if r.get('condiciones') else '',
'vip': '1' if r.get('vip') else '0',
'fuente': 'legacy',
})
save_csv('pacientes', out, [
'legacy_id', 'folio', 'nombre', 'apellido_paterno', 'apellido_materno',
'telefono', 'email', 'fecha_nacimiento', 'sexo', 'medico_principal',
'etiquetas', 'vip', 'fuente'
])
return out
def extract_monederos():
rows = paginate('/monederos/json', lambda s, l: dt_params(s, l), 'monederos')
out = []
for r in rows:
saldo = r.get('saldo') or {}
phone = re.sub(r'\D', '', (r.get('celular') or '').strip())
if phone and not phone.startswith('52') and len(phone) == 10:
phone = '52' + phone
out.append({
'telefono': phone,
'puntos': float(saldo.get('mn') or 0),
'activo': '1' if r.get('activado') else '0',
})
save_csv('monederos', out, ['telefono', 'puntos', 'activo'])
return out
def extract_crm():
rows = paginate('/crm/prospectos/json', lambda s, l: dt_params(s, l), 'crm prospectos')
out = []
for r in rows:
out.append({
'nombre': r.get('nombre') or '',
'folio': r.get('folio') or '',
'status': r.get('status') or 0,
'total_citas': r.get('total_citas') or 0,
'siguiente_cita': r.get('siguiente_cita') or '',
})
save_csv('crm_prospectos', out, ['nombre', 'folio', 'status', 'total_citas', 'siguiente_cita'])
return out
def extract_servicios():
rows = paginate('/servicios/json', lambda s, l: dt_params(s, l), 'servicios')
out = []
for r in rows:
precio_str = (r.get('precio_minimo') or '0').replace('$', '').replace('m.n.', '').replace(',', '').strip()
try:
precio = float(precio_str.split()[0])
except Exception:
precio = 0
out.append({
'codigo': f"S{r.get('id', 0):04d}",
'nombre': r.get('servicio') or '',
'categoria': (r.get('categoria') or '').lower().replace(' ', '_'),
'precio': precio,
'duracion_min': 30,
'descripcion': r.get('grupo_texto') or '',
'color': r.get('color') or '#1abc9c',
'grupo': r.get('grupo_texto') or '',
'favorito': '1' if r.get('favorito') else '0',
'activo': '1' if r.get('activado') else '0',
})
save_csv('servicios', out, [
'codigo', 'nombre', 'categoria', 'precio', 'duracion_min',
'descripcion', 'color', 'grupo', 'favorito', 'activo'
])
return out
def normalize_phone(phone):
phone = re.sub(r'\D', '', (phone or ''))
if phone and not phone.startswith('52') and len(phone) == 10:
phone = '52' + phone
return phone
def extract_ventas(start_date='2022-01-01', end_date='2026-07-06'):
"""Extrae ventas por mes y enriquece con teléfono del paciente."""
log(f'Extrayendo ventas de {start_date} a {end_date}...')
# Cargar mapeo legacy_id → teléfono desde pacientes.csv si ya existe
phone_by_legacy = {}
pac_path = OUTDIR / 'pacientes.csv'
if pac_path.exists():
with open(pac_path, newline='', encoding='utf-8') as f:
for row in csv.DictReader(f):
phone_by_legacy[row.get('legacy_id', '')] = row.get('telefono', '')
all_rows = []
current = datetime.strptime(start_date, '%Y-%m-%d').date()
end = datetime.strptime(end_date, '%Y-%m-%d').date()
while current <= end:
chunk_end = min(current + timedelta(days=30), end)
url = f"/ventas/json/{current.strftime('%Y-%m-%d')}/{chunk_end.strftime('%Y-%m-%d')}"
try:
data = fetch_json(url, dt_params(0, 10000))
rows = data.get('data', [])
all_rows.extend(rows)
log(f' {current} - {chunk_end}: {len(rows)} ventas (total: {len(all_rows)})')
except Exception as e:
log(f' ERROR {current}-{chunk_end}: {e}')
current = chunk_end + timedelta(days=1)
time.sleep(0.5)
out = []
for r in all_rows:
expediente_id = r.get('expedientes_id') or ''
phone = normalize_phone(phone_by_legacy.get(str(expediente_id), ''))
# Servicios vendidos: tomar del primer concepto; si hay varios, crear una línea por concepto
conceptos = r.get('conceptos') or []
if not conceptos:
conceptos = [{'titulo': r.get('motivo') or 'Servicio', 'subtotal': r.get('subtotal') or 0}]
for c in conceptos:
precio = 0
try:
precio = float(c.get('subtotal') or c.get('pagado') or 0)
except Exception:
pass
out.append({
'legacy_id': r.get('id', ''),
'expedientes_id': expediente_id,
'paciente_telefono': phone,
'paciente_nombre': r.get('nombre') or '',
'fecha': (r.get('created_at') or '')[:10],
'servicio_codigo': '',
'servicio_nombre': c.get('titulo') or '',
'cantidad': c.get('cantidad', 1) or 1,
'precio_unitario': precio,
'descuento': 0,
'impuesto': 0,
'estado': 'confirmed' if not r.get('cancelada') else 'cancelled',
'notas': r.get('motivo') or '',
})
save_csv('ventas', out, [
'legacy_id', 'expedientes_id', 'paciente_telefono', 'paciente_nombre', 'fecha',
'servicio_codigo', 'servicio_nombre', 'cantidad', 'precio_unitario',
'descuento', 'impuesto', 'estado', 'notas'
])
return out
def extract_citas(start_date='2022-11-01', end_date='2026-07-06'):
"""Itera día a día sobre /agenda/citas/YYYY-MM-DD para obtener todas las citas."""
log(f'Extrayendo citas de {start_date} a {end_date}...')
phone_by_legacy = {}
pac_path = OUTDIR / 'pacientes.csv'
if pac_path.exists():
with open(pac_path, newline='', encoding='utf-8') as f:
for row in csv.DictReader(f):
phone_by_legacy[row.get('legacy_id', '')] = row.get('telefono', '')
# Mapeo de IDs de médicos del sistema legacy a nombres (de /agenda)
medicos_map = {
'32': 'Dra. Alejandra Ramos',
'33': 'Dra. Lidia Martinez',
'46': 'Dra. Fernanda Cerecer',
'49': 'RODRIGUEZ FRIDA',
'54': 'Dr. Benjamín Adrián',
'79': 'XIMENA',
'80': 'ELY',
}
all_citas = []
current = datetime.strptime(start_date, '%Y-%m-%d').date()
end = datetime.strptime(end_date, '%Y-%m-%d').date()
days_with = 0
while current <= end:
url = f"/agenda/citas/{current.strftime('%Y-%m-%d')}"
try:
data = fetch_json(url)
citas = data.get('citas', [])
if citas:
days_with += 1
all_citas.extend(citas)
if current.day == 1 or len(citas) > 0:
log(f' {current}: {len(citas)} citas (total acumulado: {len(all_citas)})')
except Exception as e:
log(f' ERROR {current}: {e}')
current += timedelta(days=1)
time.sleep(0.05)
out = []
for c in all_citas:
nombre = (c.get('nombre') or '').strip()
if not nombre or nombre.upper().startswith('. . NO CITAR'):
continue
expediente_id = c.get('expedientes_id') or ''
# El teléfono de la cita suele estar en el registro; si no, buscamos en pacientes
phone = normalize_phone(c.get('telefono') or phone_by_legacy.get(str(expediente_id), ''))
if not phone and c.get('whatsapp'):
phone = normalize_phone(c.get('whatsapp'))
email = (c.get('correo') or '').strip()
if email.lower() in ('no@no.com', 'no@no'):
email = ''
inicio = c.get('inicio') or ''
fin = c.get('fin') or ''
# inicio/fin vienen como '1430' (HHMM)
hora_str = ''
if len(inicio) == 4:
hora_str = f"{inicio[:2]}:{inicio[2:]}"
elif len(inicio) == 3:
hora_str = f"0{inicio[0]}:{inicio[1:]}"
# Nombre del médico
medico_id = str(c.get('medicos_id') or '')
doctor_nombre = medicos_map.get(medico_id, medico_id if medico_id and medico_id != '0' else '')
out.append({
'legacy_id': c.get('id', ''),
'expedientes_id': expediente_id,
'paciente_telefono': phone,
'paciente_nombre': nombre,
'servicio_codigo': '',
'servicio_nombre': c.get('titulo') or '',
'fecha': c.get('fecha', ''),
'hora': hora_str,
'hora_fin': f"{fin[:2]}:{fin[2:]}" if len(fin) == 4 else '',
'estado': 'confirmed',
'doctor_nombre': doctor_nombre,
'sucursal': 'rosarito',
'medio': 'onsite',
'notas': c.get('observaciones') or '',
'estado_pago': 'not_paid',
'monto_pagado': 0,
})
save_csv('citas', out, [
'legacy_id', 'expedientes_id', 'paciente_telefono', 'paciente_nombre',
'servicio_codigo', 'servicio_nombre', 'fecha', 'hora', 'hora_fin',
'estado', 'doctor_nombre', 'sucursal', 'medio', 'notas',
'estado_pago', 'monto_pagado'
])
log(f'Citas: {len(out)} registros útiles en {days_with} días con citas')
return out
def main():
try:
login()
except Exception as e:
log(f'ERROR login: {e}')
sys.exit(1)
extract_expedientes()
extract_monederos()
extract_crm()
extract_servicios()
extract_ventas()
extract_citas()
log('Extracción completada. Archivos en /root/migracion/extraccion/')
if __name__ == '__main__':
main()

666
migracion/importar_a_odoo.py Executable file
View 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', ''):
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', '')
vals['is_breastfeeding'] = row.get('lactancia', '').lower() in ('1', 'true', 'si', '')
vals['uses_contraceptives'] = row.get('anticonceptivos', '').lower() in ('1', 'true', 'si', '')
vals['kidney_problems'] = row.get('problemas_rinon', '').lower() in ('1', 'true', 'si', '')
vals['back_pain'] = row.get('problemas_espalda', '').lower() in ('1', 'true', 'si', '')
vals['heart_disease'] = row.get('problemas_cardiacos', '').lower() in ('1', 'true', 'si', '')
vals['respiratory_problems'] = row.get('problemas_respiratorios', '').lower() in ('1', 'true', 'si', '')
vals['blood_pressure'] = (
row.get('alta_presion', '').lower() in ('1', 'true', 'si', '') or
row.get('baja_presion', '').lower() in ('1', 'true', 'si', '')
)
vals['diabetes'] = row.get('diabetes', '').lower() in ('1', 'true', 'si', '')
vals['thyroid'] = (
row.get('hipertiroidismo', '').lower() in ('1', 'true', 'si', '') or
row.get('hipotiroidismo', '').lower() in ('1', 'true', 'si', '')
)
vals['colitis'] = row.get('colitis', '').lower() in ('1', 'true', 'si', '')
vals['constipation'] = row.get('estrenimiento', '').lower() in ('1', 'true', 'si', '')
vals['liver_problems'] = row.get('problemas_higado', '').lower() in ('1', 'true', 'si', '')
vals['surgeries'] = row.get('cirugias', '').lower() in ('1', 'true', 'si', '')
vals['varicose_veins'] = row.get('varices', '').lower() in ('1', 'true', 'si', '')
vals['migraine'] = row.get('migrana', '').lower() in ('1', 'true', 'si', '')
vals['faints_with_needles'] = row.get('desmaya_agujas', '').lower() in ('1', 'true', 'si', '')
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()

609
migracion/importar_a_odoo.py.bak Executable file
View File

@@ -0,0 +1,609 @@
#!/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."""
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', ''):
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')
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()

View File

@@ -0,0 +1,181 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Importador rápido de pacientes SKEEN a Odoo usando legacy_id como clave única."""
import csv
import sys
from pathlib import Path
ODOO_PATH = Path('/root/skeen-odoo')
sys.path.insert(0, str(ODOO_PATH))
import odoo
from odoo import api, SUPERUSER_ID
DATA_DIR = Path('/root/migracion/datos')
def normalize_bool(v):
return str(v).lower() in ('1', 'true', 'si', '')
def main():
odoo.tools.config.parse_config(['-c', '/root/skeen-odoo/odoo.conf'])
db = odoo.sql_db.db_connect('skeen_odoo')
cr = db.cursor()
env = api.Environment(cr, SUPERUSER_ID, {})
Partner = env['res.partner'].sudo()
Tag = env['skeen.patient.tag'].sudo()
Employee = env['hr.employee'].sudo()
# Cachés en memoria
existing_legacy = {p.legacy_id: p.id for p in Partner.search([('legacy_id', '!=', False)]) if p.legacy_id}
doctor_cache = {}
tag_cache = {}
def get_or_create_doctor(name):
name = ' '.join(str(name).split())
if not name:
return False
if name in doctor_cache:
return doctor_cache[name]
clean = name.lower().replace('dra.', '').replace('dr.', '').replace('dr ', '').replace('dra ', '').strip()
doctor = Employee.search(['|', ('name', 'ilike', name), ('name', 'ilike', clean)], limit=1)
if not doctor:
doctor = Employee.create({'name': name})
doctor_cache[name] = doctor
return doctor
def get_tag(name):
if name in tag_cache:
return tag_cache[name]
tag = Tag.search([('name', '=', name)], limit=1)
if not tag:
tag = Tag.create({'name': name})
tag_cache[name] = tag.id
return tag.id
rows = []
with open(DATA_DIR / 'pacientes.csv', newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
rows.append(row)
print(f'Importando {len(rows)} pacientes...')
count = 0
skipped = 0
batch = []
BATCH_SIZE = 500
for row in rows:
legacy_id = (row.get('legacy_id') or '').strip()
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:
skipped += 1
continue
if legacy_id and legacy_id in existing_legacy:
skipped += 1
continue
vals = {
'name': name,
'phone': phone,
'email': row.get('email', ''),
'is_patient': True,
'source': 'other',
'is_vip': normalize_bool(row.get('vip')),
}
if legacy_id:
vals['legacy_id'] = legacy_id
if row.get('fecha_nacimiento'):
vals['birth_date'] = row.get('fecha_nacimiento')
if row.get('sexo'):
vals['gender'] = row.get('sexo')
# Datos personales
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:
vals['children_count'] = int(row.get('hijos', 0) or 0)
except Exception:
vals['children_count'] = 0
vals['is_pregnant'] = normalize_bool(row.get('embarazada'))
vals['is_breastfeeding'] = normalize_bool(row.get('lactancia'))
vals['uses_contraceptives'] = normalize_bool(row.get('anticonceptivos'))
vals['kidney_problems'] = normalize_bool(row.get('problemas_rinon'))
vals['back_pain'] = normalize_bool(row.get('problemas_espalda'))
vals['heart_disease'] = normalize_bool(row.get('problemas_cardiacos'))
vals['respiratory_problems'] = normalize_bool(row.get('problemas_respiratorios'))
vals['blood_pressure'] = normalize_bool(row.get('alta_presion')) or normalize_bool(row.get('baja_presion'))
vals['diabetes'] = normalize_bool(row.get('diabetes'))
vals['thyroid'] = normalize_bool(row.get('hipertiroidismo')) or normalize_bool(row.get('hipotiroidismo'))
vals['colitis'] = normalize_bool(row.get('colitis'))
vals['constipation'] = normalize_bool(row.get('estrenimiento'))
vals['liver_problems'] = normalize_bool(row.get('problemas_higado'))
vals['surgeries'] = normalize_bool(row.get('cirugias'))
vals['varicose_veins'] = normalize_bool(row.get('varices'))
vals['migraine'] = normalize_bool(row.get('migrana'))
vals['faints_with_needles'] = normalize_bool(row.get('desmaya_agujas'))
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 = get_or_create_doctor(row.get('medico_principal', ''))
if medico:
vals['primary_doctor_id'] = medico.id
etiquetas = row.get('etiquetas', '').strip()
if etiquetas:
tag_ids = [get_tag(t) for t in etiquetas.split(',') if t.strip()]
vals['tag_ids'] = [(6, 0, tag_ids)]
batch.append(vals)
if legacy_id:
existing_legacy[legacy_id] = True
if len(batch) >= BATCH_SIZE:
Partner.create(batch)
count += len(batch)
batch = []
cr.commit()
print(f' {count} pacientes creados')
if batch:
Partner.create(batch)
count += len(batch)
cr.commit()
cr.commit()
cr.close()
print(f'Importación completada: {count} pacientes creados, {skipped} omitidos')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,415 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Importador rápido de servicios, citas, ventas y monederos SKEEN a Odoo."""
import csv
import sys
import re
import html
import hashlib
from pathlib import Path
ODOO_PATH = Path('/root/skeen-odoo')
sys.path.insert(0, str(ODOO_PATH))
import odoo
from odoo import api, SUPERUSER_ID
DATA_DIR = Path('/root/migracion/datos')
def normalize_bool(v):
return str(v).lower() in ('1', 'true', 'si', '')
def parse_time(value):
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 extract_service(text, patient_name):
"""Extrae el nombre del servicio del campo servicio_nombre."""
text = html.unescape(text or '')
if patient_name:
text = re.sub(r'^' + re.escape(patient_name) + r'\s*[-–—]\s*', '', text, flags=re.IGNORECASE)
text = re.sub(r'\s*\(\d+\s*(sesión|sesiones|unidad|unidades)\s*\)\s*$', '', text, flags=re.IGNORECASE)
text = ' '.join(text.split())
return text.strip()
def normalize_name(name):
return ' '.join(str(name).split()).lower()
def main():
odoo.tools.config.parse_config(['-c', '/root/skeen-odoo/odoo.conf'])
db = odoo.sql_db.db_connect('skeen_odoo')
cr = db.cursor()
env = api.Environment(cr, SUPERUSER_ID, {})
Partner = env['res.partner'].sudo()
Servicio = env['skeen.servicio'].sudo()
Employee = env['hr.employee'].sudo()
CitaModel = env.registry['skeen.cita']
Cita = env['skeen.cita'].sudo()
Venta = env['skeen.venta'].sudo()
Monedero = env['skeen.monedero'].sudo()
MonederoTx = env['skeen.monedero.transaction'].sudo()
# Cachés en memoria (SQL directo para velocidad)
print('Cargando cachés...')
partner_by_phone = {}
partner_by_name = {}
partner_by_legacy = {}
cr.execute("SELECT id, phone, name, legacy_id FROM res_partner WHERE is_patient = true")
for pid, phone, name, legacy_id in cr.fetchall():
if phone:
partner_by_phone.setdefault(phone, pid)
if name:
partner_by_name.setdefault(normalize_name(name), pid)
if legacy_id:
partner_by_legacy[legacy_id] = pid
service_by_code = {}
service_by_name = {}
cr.execute("SELECT id, code, name FROM skeen_servicio")
for sid, code, name in cr.fetchall():
if code:
service_by_code[code] = sid
if name:
service_by_name[normalize_name(name)] = sid
doctor_by_name = {}
cr.execute("SELECT id, name FROM hr_employee")
for eid, name in cr.fetchall():
if name:
doctor_by_name[normalize_name(name)] = eid
print(f' Pacientes: {len(partner_by_phone)} por teléfono, {len(partner_by_name)} por nombre')
print(f' Servicios: {len(service_by_name)}')
print(f' Doctores: {len(doctor_by_name)}')
skipped_reasons = {'no_patient': 0, 'no_service': 0, 'no_date': 0}
def resolve_partner(row, phone_field='paciente_telefono', name_field='paciente_nombre', legacy_field='expedientes_id'):
legacy_id = (row.get(legacy_field) or '').strip()
if legacy_id and legacy_id in partner_by_legacy:
return partner_by_legacy[legacy_id]
phone = (row.get(phone_field) or '').strip()
if phone and phone in partner_by_phone:
return partner_by_phone[phone]
name = normalize_name(row.get(name_field, ''))
if name and name in partner_by_name:
return partner_by_name[name]
return False
# Desactivar validación de solapamiento de citas
original_check = CitaModel._check_availability
CitaModel._check_availability = lambda self: None
resultados = {}
# 1. Servicios
print('Importando 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_codes = set()
batch = []
count = 0
with open(DATA_DIR / 'servicios.csv', newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
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_codes:
continue
seen_codes.add(code)
if code in service_by_code:
continue
cat = category_map.get((row.get('categoria') or '').strip().lower(), 'tratamiento')
batch.append({
'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', ''),
'color': row.get('color', '#1abc9c'),
'service_group': row.get('grupo', ''),
'is_favorite': normalize_bool(row.get('favorito')),
'active': normalize_bool(row.get('activo', '1')),
})
if len(batch) >= 100:
created = Servicio.create(batch)
count += len(created)
cr.commit()
batch = []
if batch:
created = Servicio.create(batch)
count += len(created)
cr.commit()
# Recargar caché de servicios (SQL directo)
service_by_code = {}
service_by_name = {}
cr.execute("SELECT id, code, name FROM skeen_servicio")
for sid, code, name in cr.fetchall():
if code:
service_by_code[code] = sid
if name:
service_by_name[normalize_name(name)] = sid
resultados['servicios'] = count
print(f' {count} servicios creados')
# Helper: obtener o crear servicio por nombre
service_name_cache = dict(service_by_name)
def get_or_create_service_by_name(name):
name = name.strip()
if not name:
name = 'Servicio genérico'
key = normalize_name(name)
if key in service_name_cache:
return service_name_cache[key]
# Crear nuevo servicio
code_hash = hashlib.md5(key.encode('utf-8')).hexdigest()[:12]
code = f'HIST_{code_hash}'
new_s = Servicio.create({
'name': name,
'code': code,
'category': 'tratamiento',
'price': 0,
'duration_min': 30,
})
service_name_cache[key] = new_s.id
return new_s.id
# 2. Citas
print('Importando citas...')
state_map = {
'pendiente': 'pending',
'confirmada': 'confirmed',
'llego': 'arrived',
'en_progreso': 'in_progress',
'completada': 'done',
'cancelada': 'cancelled',
'no_show': 'no_show',
}
payment_state_map = {
'no_pagado': 'not_paid',
'parcial': 'partial',
'pagado': 'paid',
}
batch = []
count = 0
skipped = 0
BATCH_SIZE = 500
with open(DATA_DIR / 'citas.csv', newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
partner_id = resolve_partner(row)
date_str = (row.get('fecha') or '').strip()
if not partner_id:
skipped += 1
skipped_reasons['no_patient'] += 1
continue
if not date_str:
skipped += 1
skipped_reasons['no_date'] += 1
continue
service_name = extract_service(row.get('servicio_nombre', ''), row.get('paciente_nombre', ''))
if not service_name:
service_name = row.get('servicio_nombre', '').strip() or 'Servicio genérico'
servicio_id = get_or_create_service_by_name(service_name)
doctor_name = (row.get('doctor_nombre') or '').strip()
doctor_id = doctor_by_name.get(normalize_name(doctor_name)) if doctor_name else False
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'
state = state_map.get((row.get('estado') or '').strip().lower(), 'confirmed')
payment_state = payment_state_map.get((row.get('estado_pago') or '').strip().lower(), 'not_paid')
batch.append({
'partner_id': partner_id,
'servicio_id': servicio_id,
'date': date_str,
'time': parse_time(row.get('hora')),
'state': state,
'payment_state': payment_state,
'amount_paid': float(row.get('monto_pagado', 0) or 0),
'doctor_id': doctor_id,
'branch': branch,
'medium': medium,
'notes': row.get('notas', ''),
})
if len(batch) >= BATCH_SIZE:
Cita.create(batch)
count += len(batch)
batch = []
cr.commit()
print(f' {count} citas creadas')
if batch:
Cita.create(batch)
count += len(batch)
cr.commit()
resultados['citas'] = count
print(f' {count} citas creadas, {skipped} omitidas ({skipped_reasons})')
# 3. Ventas
print('Importando ventas...')
state_map_venta = {
'borrador': 'draft',
'confirmada': 'confirmed',
'pagada': 'paid',
'parcial': 'partial',
'cancelada': 'cancelled',
}
count = 0
skipped = 0
batch = []
BATCH_SIZE = 500
with open(DATA_DIR / 'ventas.csv', newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
partner_id = resolve_partner(row)
if not partner_id:
skipped += 1
skipped_reasons['no_patient'] += 1
continue
service_name = extract_service(row.get('servicio_nombre', ''), row.get('paciente_nombre', ''))
if not service_name:
service_name = row.get('servicio_nombre', '').strip() or 'Servicio genérico'
servicio_id = get_or_create_service_by_name(service_name)
qty = float(row.get('cantidad', 1) or 1)
price = float(row.get('precio_unitario', 0) or 0)
discount = float(row.get('descuento', 0) or 0)
tax = float(row.get('impuesto', 0) or 0)
state = state_map_venta.get((row.get('estado') or '').strip().lower(), 'confirmed')
date = row.get('fecha') or odoo.fields.Date.today()
batch.append({
'partner_id': partner_id,
'date': date,
'discount': discount,
'tax': tax,
'state': state,
'notes': row.get('notas', ''),
'line_ids': [(0, 0, {
'service_id': servicio_id,
'description': service_name or 'Servicio',
'quantity': qty,
'price_unit': price,
})],
})
if len(batch) >= BATCH_SIZE:
Venta.create(batch)
count += len(batch)
batch = []
cr.commit()
print(f' {count} ventas creadas')
if batch:
Venta.create(batch)
count += len(batch)
cr.commit()
resultados['ventas'] = count
print(f' {count} ventas creadas, {skipped} omitidas')
# 4. Monederos
print('Importando monederos...')
count = 0
skipped = 0
batch_monedero = []
batch_tx = []
BATCH_SIZE = 500
with open(DATA_DIR / 'monederos.csv', newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
phone = (row.get('telefono') or '').strip()
points = int(float(row.get('puntos', 0) or 0))
if not phone or phone not in partner_by_phone:
skipped += 1
continue
partner_id = partner_by_phone[phone]
existing = Monedero.search([('partner_id', '=', partner_id)], limit=1)
if existing:
existing.write({'points': points})
count += 1
continue
batch_monedero.append({
'partner_id': partner_id,
'points': points,
})
if len(batch_monedero) >= BATCH_SIZE:
created = Monedero.create(batch_monedero)
for m in created:
batch_tx.append({
'monedero_id': m.id,
'type': 'adjustment',
'points': m.points,
'description': 'Saldo inicial migrado de SKEEN',
'reference': 'MIGRACION',
})
if batch_tx:
MonederoTx.create(batch_tx)
batch_tx = []
count += len(batch_monedero)
batch_monedero = []
cr.commit()
print(f' {count} monederos creados')
if batch_monedero:
created = Monedero.create(batch_monedero)
for m in created:
batch_tx.append({
'monedero_id': m.id,
'type': 'adjustment',
'points': m.points,
'description': 'Saldo inicial migrado de SKEEN',
'reference': 'MIGRACION',
})
if batch_tx:
MonederoTx.create(batch_tx)
count += len(batch_monedero)
cr.commit()
resultados['monederos'] = count
print(f' {count} monederos creados, {skipped} omitidos')
# Restaurar validación
CitaModel._check_availability = original_check
cr.commit()
cr.close()
print('Importación completada:')
for k, v in resultados.items():
print(f' {k}: {v} registros')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,365 @@
#!/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()

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
import requests
from bs4 import BeautifulSoup
BASE = 'https://sistema.skeenmx.app'
USERNAME = 'JoseSkeen'
PASSWORD = 'SKr#902_88'
session = requests.Session()
r = session.get(f'{BASE}/login', timeout=30)
soup = BeautifulSoup(r.text, 'html.parser')
token = soup.find('input', {'name': '_token'})
token = token['value'] if token else ''
r = session.post(f'{BASE}/login', data={
'_token': token,
'username': USERNAME,
'password': PASSWORD,
}, timeout=30, allow_redirects=True)
r = session.get(f'{BASE}/expediente/ver/1', timeout=30)
soup = BeautifulSoup(r.text, 'html.parser')
# Extraer tablas con th/td
for table in soup.find_all('table'):
for tr in table.find_all('tr'):
ths = tr.find_all('th')
tds = tr.find_all('td')
if ths and tds and len(ths) == len(tds):
for th, td in zip(ths, tds):
key = ' '.join(th.get_text(strip=True).split())
val = ' '.join(td.get_text(strip=True).split())
if key and val:
print(f'{key}: {val}')
print('--- FIN TABLA ---')
# Buscar inputs/checkboxes
for inp in soup.find_all(['input', 'textarea', 'select']):
name = inp.get('name') or inp.get('id') or ''
val = inp.get('value') or ''
checked = inp.get('checked')
if name:
print(f'INPUT {name}: value={val} checked={checked}')

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python3
import requests
from bs4 import BeautifulSoup
import re
BASE = 'https://sistema.skeenmx.app'
USERNAME = 'JoseSkeen'
PASSWORD = 'SKr#902_88'
session = requests.Session()
r = session.get(f'{BASE}/login', timeout=30)
soup = BeautifulSoup(r.text, 'html.parser')
token = soup.find('input', {'name': '_token'})
token = token['value'] if token else ''
session.post(f'{BASE}/login', data={
'_token': token,
'username': USERNAME,
'password': PASSWORD,
}, timeout=30, allow_redirects=True)
r = session.get(f'{BASE}/expediente/ver/1', timeout=30)
text = r.text
# Encontrar todas las etiquetas de formulario
for m in re.finditer(r'<label[^>]*>(.*?)</label>', text, re.S):
label = re.sub(r'<[^>]+>', '', m.group(1)).strip()
if label:
print(f'LABEL: {label}')

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
import requests
from bs4 import BeautifulSoup
import re
BASE = 'https://sistema.skeenmx.app'
USERNAME = 'JoseSkeen'
PASSWORD = 'SKr#902_88'
session = requests.Session()
r = session.get(f'{BASE}/login', timeout=30)
soup = BeautifulSoup(r.text, 'html.parser')
token = soup.find('input', {'name': '_token'})
token = token['value'] if token else ''
session.post(f'{BASE}/login', data={
'_token': token,
'username': USERNAME,
'password': PASSWORD,
}, timeout=30, allow_redirects=True)
# Probar varios IDs directamente
for pid in [1, 2, 3, 5, 10, 20, 50, 100, 200, 500, 1000]:
try:
r = session.get(f'{BASE}/expediente/ver/{pid}', timeout=30)
soup = BeautifulSoup(r.text, 'html.parser')
text = soup.get_text(separator='\n')
if any(k in text.lower() for k in ['riñón', 'rinon', 'embarazada', 'alergias', 'medicamentos', 'cirugías']):
print(f'\n=== PACIENTE {pid} tiene historia clínica ===')
lines = [l.strip() for l in text.split('\n') if l.strip()]
for i, line in enumerate(lines):
if any(k in line.lower() for k in ['riñón', 'rinon', 'embarazada', 'alergias', 'medicamentos', 'cirugías', 'antecedentes', 'patológicos', 'historia']):
print(f' {i}: {line}')
for j in range(max(0, i-3), min(len(lines), i+6)):
print(f' {j}: {lines[j]}')
print('---')
except Exception as e:
print(f'Error {pid}: {e}')

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
import requests
from bs4 import BeautifulSoup
import re
BASE = 'https://sistema.skeenmx.app'
USERNAME = 'JoseSkeen'
PASSWORD = 'SKr#902_88'
session = requests.Session()
r = session.get(f'{BASE}/login', timeout=30)
soup = BeautifulSoup(r.text, 'html.parser')
token = soup.find('input', {'name': '_token'})
token = token['value'] if token else ''
session.post(f'{BASE}/login', data={
'_token': token,
'username': USERNAME,
'password': PASSWORD,
}, timeout=30, allow_redirects=True)
# Paciente 5 tiene historia clínica más completa
r = session.get(f'{BASE}/expediente/ver/5', timeout=30)
soup = BeautifulSoup(r.text, 'html.parser')
# Buscar divs/cards con clase que contengan historia clínica
# Imprimir estructura de inputs/checkboxes con sus labels cercanos
for div in soup.find_all(['div', 'section']):
text = div.get_text(separator=' ', strip=True)
if any(k in text.lower() for k in ['embarazada', 'lactancia', 'anticonceptivo', 'hijos', 'cirugías', 'varicosas', 'migraña', 'agujas', 'alergias', 'medicamentos', 'enfermedades crónicas']):
print('---DIV---')
print(text[:1000])
print('---')

View File

@@ -0,0 +1,6 @@
name,kind,sku,category,unit,qty,qty_optimal,qty_min,cost,expiry_date,notes
Suero Vitamina C,producto,PRD-001,Skincare,pieza,24,30,5,320,2027-06-15,Suero antioxidante
Protector Solar SPF 50,producto,PRD-002,Skincare,pieza,18,25,5,280,2027-08-20,Protector solar facial
Acido Hialuronico,consumible,CON-001,Inyecciones,ampolla,45,60,15,120,2026-12-01,Para rellenos
Toxina Botulinica,consumible,CON-002,Inyecciones,unidad,12,20,5,850,2026-11-30,Unidades de toxina
Guantes Estériles,consumible,CON-003,Instrumental,caja,8,15,3,95,,Caja de 100 unidades
1 name kind sku category unit qty qty_optimal qty_min cost expiry_date notes
2 Suero Vitamina C producto PRD-001 Skincare pieza 24 30 5 320 2027-06-15 Suero antioxidante
3 Protector Solar SPF 50 producto PRD-002 Skincare pieza 18 25 5 280 2027-08-20 Protector solar facial
4 Acido Hialuronico consumible CON-001 Inyecciones ampolla 45 60 15 120 2026-12-01 Para rellenos
5 Toxina Botulinica consumible CON-002 Inyecciones unidad 12 20 5 850 2026-11-30 Unidades de toxina
6 Guantes Estériles consumible CON-003 Instrumental caja 8 15 3 95 Caja de 100 unidades

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Limpieza de nombres de pacientes (reversible).
Pasos:
1) Respaldo CSV id -> nombre original.
2) Normaliza: control chars, frases marcador (no usar exp...), barras, espacios, signos.
3) Title Case con particulas en minuscula (de, del, de la, los, las, y, etc.).
4) Dry-run por defecto; con --apply actualiza solo las filas que cambian.
"""
import csv
import os
import re
import sys
import unicodedata
from datetime import datetime
import psycopg2
DSN = "host=localhost dbname=skeen_odoo user=skeen password=skeen_dev_2026"
PARTICULAS = {
'de', 'del', 'la', 'las', 'los', 'y', 'e', 'da', 'do', 'das', 'dos',
'di', 'van', 'von', 'der', 'den', 'san', 'santa', 'mc',
}
# Frases marcador inyectadas por el personal dentro del nombre
MARKER_RE = re.compile(
r'\bno\s*usar(\s+(este\s+)?exp(?:ediente)?)?\b[!.,;:/\\-]*',
re.IGNORECASE,
)
ERROR_RE = re.compile(r'\berror\b[!.,;:/\\-]*', re.IGNORECASE)
SLASHES_RE = re.compile(r'[/\\]+')
MULTISPACE_RE = re.compile(r'\s{2,}')
LEAD_TRAIL_JUNK_RE = re.compile(r'^[\s.\-_,;:]+|[\s.\-_,;:]+$')
CONTROL_RE = re.compile(r'[\x00-\x1f\x7f]')
def cap_token(tok, first):
low = tok.lower()
if not first and low in PARTICULAS:
return low
# Mantener siglas cortas (iniciales) en mayuscula: J, GPE -> J, Gpe
if len(tok) <= 1:
return tok.upper()
return low.capitalize()
def clean_name(raw):
if raw is None:
return raw
s = CONTROL_RE.sub('', raw)
s = SLASHES_RE.sub(' ', s) # barras -> espacio
s = MARKER_RE.sub(' ', s) # quita "no usar exp..."
s = ERROR_RE.sub(' ', s) # quita "ERROR" suelto
s = s.replace('.', ' ') # puntos (iniciales, GPE.) -> espacio
s = MULTISPACE_RE.sub(' ', s)
s = LEAD_TRAIL_JUNK_RE.sub('', s).strip()
s = MULTISPACE_RE.sub(' ', s)
if not s:
return s
toks = s.split(' ')
toks = [cap_token(t, i == 0) for i, t in enumerate(toks) if t]
return ' '.join(toks)
def main():
apply = '--apply' in sys.argv
conn = psycopg2.connect(DSN)
conn.autocommit = False
cur = conn.cursor()
cur.execute("SELECT id, name FROM res_partner WHERE is_patient ORDER BY id")
rows = cur.fetchall()
changes = []
for rid, name in rows:
new = clean_name(name)
if new != name:
changes.append((rid, name, new))
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_path = f'/root/migracion/datos/name_backup_{ts}.csv'
with open(backup_path, 'w', newline='', encoding='utf-8') as f:
w = csv.writer(f)
w.writerow(['id', 'name_original'])
for rid, name, _ in changes:
w.writerow([rid, name])
print(f'Total pacientes: {len(rows)}')
print(f'A modificar: {len(changes)}')
print(f'Sin cambios: {len(rows) - len(changes)}')
print(f'Respaldo: {backup_path}')
print()
print('--- Ejemplos antes -> despues (30) ---')
shown = 0
# prioriza los que tenian marcador o signos
def score(c):
n = c[1] or ''
return (('no usar' in n.lower()) * 10) + (('/' in n) * 5) + ((' ' in n) * 2)
for rid, old, new in sorted(changes, key=score, reverse=True)[:30]:
print(f' [{rid}] "{old}" -> "{new}"')
shown += 1
print(f' ({shown} ejemplos)')
if not apply:
print('\nDRY-RUN: no se aplicaron cambios. Ejecuta con --apply para aplicar.')
conn.rollback()
cur.close(); conn.close()
return
updated = 0
for rid, old, new in changes:
cur.execute("UPDATE res_partner SET name = %s WHERE id = %s", (new, rid))
updated += 1
conn.commit()
print(f'\nAPLICADO: {updated} nombres actualizados.')
cur.close(); conn.close()
if __name__ == '__main__':
main()

43
migracion/postprocesar_citas.py Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Post-procesa citas.csv para mapear IDs numéricos de médicos a nombres reales.
"""
import csv
from pathlib import Path
DATA_DIR = Path('/root/migracion/datos')
medicos_map = {
'32': 'Dra. Alejandra Ramos',
'33': 'Dra. Lidia Martinez',
'46': 'Dra. Fernanda Cerecer',
'49': 'RODRIGUEZ FRIDA',
'54': 'Dr. Benjamín Adrián',
'79': 'XIMENA',
'80': 'ELY',
}
def main():
path = DATA_DIR / 'citas.csv'
if not path.exists():
print(f'No existe {path}')
return
rows = []
with open(path, newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
doctor = (row.get('doctor_nombre') or '').strip()
if doctor in medicos_map:
row['doctor_nombre'] = medicos_map[doctor]
rows.append(row)
with open(path, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
print(f'Post-procesadas {len(rows)} citas')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Recalcula campos compute de skeen.cita tras importación masiva."""
import sys
sys.path.insert(0, '/root/skeen-odoo')
import odoo
from odoo.tools import config
config.parse_config([
'-c', '/root/skeen-odoo/odoo.conf',
'--no-http',
'--log-level', 'error',
])
odoo.service.server.load_server_wide_modules()
registry = odoo.registry(config['db_name'])
with registry.cursor() as cr:
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
Citas = env['skeen.cita']
total = Citas.search_count([])
print(f'Total citas: {total}')
# Recalcular campos compute para todas las citas
todas = Citas.search([])
todas._compute_datetime()
todas._compute_time_str()
cr.commit()
nulls = Citas.search_count([('datetime_start', '=', False)])
print(f'Citas con datetime_start NULL tras recálculo: {nulls}')
print('Recálculo completado.')

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""Reset completo de datos migrados (pacientes, citas, ventas, pagos, monederos y servicios genéricos)."""
import sys
from pathlib import Path
ODOO_PATH = Path('/root/skeen-odoo')
sys.path.insert(0, str(ODOO_PATH))
import odoo
from odoo import api, SUPERUSER_ID
odoo.tools.config.parse_config(['-c', '/root/skeen-odoo/odoo.conf'])
db = odoo.sql_db.db_connect('skeen_odoo')
with db.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
print('Borrando citas...')
env['skeen.cita'].sudo().search([]).unlink()
print('Borrando ventas...')
env['skeen.venta'].sudo().search([]).unlink()
print('Borrando pagos...')
env['skeen.pago'].sudo().search([]).unlink()
print('Borrando transacciones de monedero...')
env['skeen.monedero.transaction'].sudo().search([]).unlink()
print('Borrando monederos...')
env['skeen.monedero'].sudo().search([]).unlink()
print('Borrando pacientes...')
env['res.partner'].sudo().search([('is_patient', '=', True)]).unlink()
print('Borrando servicios genéricos (HIST_)...')
env['skeen.servicio'].sudo().search([('code', 'like', 'HIST_%')]).unlink()
cr.commit()
print('Datos reseteados completamente')

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env python3
import sys
from pathlib import Path
ODOO_PATH = Path('/root/skeen-odoo')
sys.path.insert(0, str(ODOO_PATH))
import odoo
from odoo import api, SUPERUSER_ID
odoo.tools.config.parse_config(['-c', '/root/skeen-odoo/odoo.conf'])
db = odoo.sql_db.db_connect('skeen_odoo')
with db.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
env['skeen.cita'].sudo().search([]).unlink()
env['skeen.venta'].sudo().search([]).unlink()
env['skeen.pago'].sudo().search([]).unlink()
env['skeen.monedero'].sudo().search([]).unlink()
env['res.partner'].sudo().search([('is_patient', '=', True)]).unlink()
cr.commit()
print('Datos reseteados')

View File

@@ -0,0 +1,8 @@
name,code,category,price,duration_min,package_price,package_sessions,package_notes,description,service_group,color,is_favorite
Consulta Dermatologica,CONS-DERM,consulta,800,30,0,0,,Evaluacion completa de la piel con diagnostico y plan de tratamiento personalizado.,Consultas,#1abc9c,true
Consulta de Valoracion,CONS-VAL,consulta,0,15,0,0,,Evaluacion breve para recomendar el tratamiento mas adecuado. Sin compromiso.,Consultas,#1abc9c,false
Tratamiento Facial para Acne,ACNE-FAC,tratamiento,1500,60,0,0,,Limpieza profunda extraccion de comedones y mascarilla calmante.,Faciales,#3498db,false
Peel Quimico,PEEL-QUI,tratamiento,1500,30,0,0,,Exfoliacion quimica para renovar la piel tratar manchas y mejorar textura.,Faciales,#9b59b6,false
Depilacion Laser Axila,LASER-AXI,tratamiento,500,15,0,0,,Depilacion laser por zona pequena.,Corporal,#e74c3c,false
Depilacion Laser Full Body,LASER-FUL,tratamiento,2500,90,7000,3,Ahorro de $500,Depilacion laser full body. Precio por sesion o paquete de 3 sesiones.,Corporal,#e74c3c,true
Tratamiento Anti-Edad,ANTI-EDAD,tratamiento,2500,75,6500,3,Ahorro de $1000,Combinacion de microdermoabrasion vitaminas y mascarilla reafirmante.,Faciales,#f39c12,false
1 name code category price duration_min package_price package_sessions package_notes description service_group color is_favorite
2 Consulta Dermatologica CONS-DERM consulta 800 30 0 0 Evaluacion completa de la piel con diagnostico y plan de tratamiento personalizado. Consultas #1abc9c true
3 Consulta de Valoracion CONS-VAL consulta 0 15 0 0 Evaluacion breve para recomendar el tratamiento mas adecuado. Sin compromiso. Consultas #1abc9c false
4 Tratamiento Facial para Acne ACNE-FAC tratamiento 1500 60 0 0 Limpieza profunda extraccion de comedones y mascarilla calmante. Faciales #3498db false
5 Peel Quimico PEEL-QUI tratamiento 1500 30 0 0 Exfoliacion quimica para renovar la piel tratar manchas y mejorar textura. Faciales #9b59b6 false
6 Depilacion Laser Axila LASER-AXI tratamiento 500 15 0 0 Depilacion laser por zona pequena. Corporal #e74c3c false
7 Depilacion Laser Full Body LASER-FUL tratamiento 2500 90 7000 3 Ahorro de $500 Depilacion laser full body. Precio por sesion o paquete de 3 sesiones. Corporal #e74c3c true
8 Tratamiento Anti-Edad ANTI-EDAD tratamiento 2500 75 6500 3 Ahorro de $1000 Combinacion de microdermoabrasion vitaminas y mascarilla reafirmante. Faciales #f39c12 false

125
migracion/sync_odoo_to_wacrm.py Executable file
View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Sincroniza pacientes de Odoo → WACRM (Supabase self-hosted).
Inserta/actualiza contactos en la tabla public.contacts.
Uso:
source /root/odoo-venv/bin/activate && python3 /root/migracion/sync_odoo_to_wacrm.py
"""
import sys
from pathlib import Path
ODOO_PATH = Path('/root/skeen-odoo')
sys.path.insert(0, str(ODOO_PATH))
import odoo
from odoo import api, SUPERUSER_ID
import psycopg2
from psycopg2.extras import execute_values
# Configuración de Supabase / WACRM
SUPABASE_HOST = 'localhost'
SUPABASE_PORT = 6543
SUPABASE_USER = 'postgres.your-tenant-id'
SUPABASE_PASSWORD = '8bbf201d9d60488a1e86e5396b31e22d'
SUPABASE_DB = 'postgres'
WACRM_ACCOUNT_ID = 'a2dc0db3-dea0-4bd8-8733-1d62975c5f89'
WACRM_OWNER_USER_ID = '00000000-0000-0000-0000-000000000001'
def get_odoo_env(database='skeen_odoo'):
odoo.tools.config.parse_config(['-c', '/root/skeen-odoo/odoo.conf'])
db = odoo.sql_db.db_connect(database)
cr = db.cursor()
return api.Environment(cr, SUPERUSER_ID, {}), cr
def get_wacrm_connection():
return psycopg2.connect(
host=SUPABASE_HOST,
port=SUPABASE_PORT,
user=SUPABASE_USER,
password=SUPABASE_PASSWORD,
dbname=SUPABASE_DB,
)
def normalize_phone(phone):
digits = ''.join(c for c in (phone or '') if c.isdigit())
if digits and not digits.startswith('52') and len(digits) == 10:
digits = '52' + digits
return digits
def sync_contacts(limit=None):
env, cr = get_odoo_env()
try:
domain = [('is_patient', '=', True), ('phone', '!=', False), ('phone', '!=', '')]
partners = env['res.partner'].sudo().search(domain)
if limit:
partners = partners[:limit]
rows = []
for p in partners:
phone = normalize_phone(p.phone)
if not phone:
continue
rows.append((
WACRM_OWNER_USER_ID,
phone,
p.name or 'Paciente',
p.email or '',
'', # company
'', # avatar_url
WACRM_ACCOUNT_ID,
))
if not rows:
print('No hay pacientes para sincronizar')
return
conn = get_wacrm_connection()
try:
with conn.cursor() as cur:
# UPSERT por (account_id, phone_normalized)
execute_values(cur, """
INSERT INTO public.contacts
(user_id, phone, name, email, company, avatar_url, account_id)
VALUES %s
ON CONFLICT (account_id, phone_normalized) WHERE phone_normalized <> ''
DO UPDATE SET
name = EXCLUDED.name,
email = EXCLUDED.email,
company = EXCLUDED.company,
avatar_url = EXCLUDED.avatar_url,
user_id = EXCLUDED.user_id,
updated_at = NOW()
RETURNING id, phone
""", rows, page_size=1000)
result = cur.fetchall()
phone_to_id = {phone: str(cid) for cid, phone in result}
# Actualizar referencia en Odoo
for p in partners:
phone = normalize_phone(p.phone)
if phone in phone_to_id:
p.write({
'wacrm_synced': True,
'wacrm_contact_id': phone_to_id[phone],
})
conn.commit()
print(f'Sincronizados {len(rows)} contactos a WACRM')
finally:
conn.close()
finally:
cr.commit()
cr.close()
if __name__ == '__main__':
sync_contacts()