- 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/
122 lines
3.8 KiB
Python
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()
|