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

122 lines
3.8 KiB
Python

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