#!/usr/bin/env python3 """Import Rached legacy workshop data into the Nexus tenant_refaccionaria_rached DB.""" import json import os import re import sys from datetime import datetime from pathlib import Path import psycopg2 BASE_DIR = Path(__file__).resolve().parent.parent DATA_DIR = BASE_DIR / 'data' / 'rached_import' TENANT_DB = 'tenant_refaccionaria_rached' # Load DB URL from environment or use localhost defaults DB_URL = os.environ.get( 'TENANT_DB_URL', f'postgresql://postgres@localhost/{TENANT_DB}' ) def normalize_name(s): if not s: return '' return re.sub(r'[^a-z0-9]', '', s.lower().replace('sucursal', '').strip()) def load_json(name): with open(DATA_DIR / f'{name}.json', encoding='utf-8') as f: return json.load(f) def load_detail_json(cid): path = DATA_DIR / 'client_details' / f'client_{cid}.json' if path.exists(): with open(path, encoding='utf-8') as f: return json.load(f) return None def connect(): return psycopg2.connect(DB_URL) ORDER_STATUS_MAP = { 1: 'por_recolectar', 2: 'por_revisar', 3: 'en_revision', 4: 'revisada', 5: 'cotizada', 6: 'por_autorizar', 7: 'autorizada', 8: 'autorizacion_parcial', 9: 'cancelada', 10: 'en_reparacion', 11: 'reparada', 12: 'por_entregar', 13: 'enviado', 14: 'entregado', 15: 'por_facturar', 16: 'facturada', 17: 'por_enviar', } ITEM_STATUS_MAP = { 1: 'por_revisar', 2: 'revisando', 3: 'revisado', 4: 'cotizado', 5: 'por_autorizar', 6: 'autorizado', 7: 'cancelado', 8: 'en_reparacion', 9: 'reparado', 10: 'por_entregar', 11: 'enviado', 12: 'entregado', 13: 'por_enviar', } def get_default_branch(cur): cur.execute("SELECT id FROM branches WHERE is_main = true LIMIT 1") row = cur.fetchone() if row: return row[0] cur.execute("SELECT id FROM branches ORDER BY id LIMIT 1") return cur.fetchone()[0] def ensure_branches(cur): cur.execute("SELECT id, name FROM branches") existing = {normalize_name(name): id for id, name in cur.fetchall()} catalog = load_json('catalog_sucursales')['datos'] mapping = {} for s in catalog: name = (s.get('nombre') or '').strip() # Skip the erroneous Rached branch if name.upper() == 'CREMALLERAS': continue key = normalize_name(name) if key in existing: mapping[s['id']] = existing[key] else: cur.execute( "INSERT INTO branches (name, is_active) VALUES (%s, true) RETURNING id", (s['nombre'],) ) new_id = cur.fetchone()[0] existing[key] = new_id mapping[s['id']] = new_id print(f"Created branch {s['nombre']} -> {new_id}") return mapping def ensure_customers(cur, branch_map): # collect unique client ids referenced in orders client_ids = set() for f in DATA_DIR.glob('details/order_*.json'): d = json.loads(f.read_text(encoding='utf-8')).get('datos', {}) cid = d.get('idCliente') if cid: client_ids.add(cid) # load existing customers by name cur.execute("SELECT id, name FROM customers") existing_by_name = {name.strip().lower(): id for id, name in cur.fetchall()} client_map = {} default_branch = get_default_branch(cur) for cid in sorted(client_ids): detail = load_detail_json(cid) datos = detail.get('datos', {}) if detail else {} # Name: prefer 'taller' if meaningful, else full name taller = (datos.get('taller') or '').strip() nombre = (datos.get('nombre') or '').strip() ap1 = (datos.get('primerApellido') or '').strip() ap2 = (datos.get('segundoApellido') or '').strip() if taller: name = taller else: name = ' '.join([nombre, ap1, ap2]).strip() if not name: name = f"Cliente Rached {cid}" if name.lower() in existing_by_name: client_map[cid] = existing_by_name[name.lower()] continue # phone / address phone = None for t in datos.get('telefonos', []): num = t.get('numero') if num: phone = str(num) break address = None for drec in datos.get('direcciones', []): address = drec.get('completa') if address: break cur.execute( """ INSERT INTO customers (branch_id, name, phone, address, price_tier, is_active) VALUES (%s, %s, %s, %s, 2, true) RETURNING id """, (default_branch, name, phone, address) ) new_id = cur.fetchone()[0] existing_by_name[name.lower()] = new_id client_map[cid] = new_id print(f"Created customer {cid}: {name}") return client_map def ensure_employees(cur, branch_map, role, catalog, existing_names=None): default_branch = get_default_branch(cur) emp_map = {} if existing_names is None: cur.execute("SELECT id, name FROM employees") existing_names = {name.strip().lower(): id for id, name in cur.fetchall()} for e in catalog: name = (e.get('nombre') or '').strip() if not name: continue key = name.lower() if key in existing_names: emp_map[e['id']] = existing_names[key] continue is_active = bool(e.get('activo', True)) cur.execute( """ INSERT INTO employees (name, role, branch_id, is_active) VALUES (%s, %s, %s, %s) RETURNING id """, (name, role, default_branch, is_active) ) new_id = cur.fetchone()[0] existing_names[key] = new_id emp_map[e['id']] = new_id print(f"Created employee {role} {e['id']}: {name}") return emp_map def ensure_couriers(cur, catalog, tenant_id=31): cur.execute("SELECT id, name FROM couriers WHERE tenant_id = %s", (tenant_id,)) existing = {name.strip().lower(): id for id, name in cur.fetchall()} courier_map = {} for c in catalog: name = (c.get('nombre') or '').strip() if not name: continue key = name.lower() if key in existing: courier_map[c['id']] = existing[key] continue cur.execute( "INSERT INTO couriers (tenant_id, name, code, is_active) VALUES (%s, %s, %s, true) RETURNING id", (tenant_id, name, f"MOT-{c['id']}") ) new_id = cur.fetchone()[0] existing[key] = new_id courier_map[c['id']] = new_id print(f"Created courier {c['id']}: {name}") return courier_map def ensure_articles(cur, branch_map): """Rached articles are kept as free-text lines, not inventory products.""" catalog = load_json('catalog_articulos') article_map = {} for a in catalog: name = (a.get('nombre') or '').strip() if name: article_map[a['id']] = name return article_map def ensure_users(cur, branch_map, order_details): # Map Rached user ids to employees default_branch = get_default_branch(cur) cur.execute("SELECT id, name FROM employees") existing = {name.strip().lower(): id for id, name in cur.fetchall()} user_map = {} for d in order_details: datos = d.get('datos', {}) uid = datos.get('idUsuario') name = (datos.get('usuarioNombre') or '').strip() if not uid or not name: continue key = name.lower() if key in existing: user_map[uid] = existing[key] continue cur.execute( "INSERT INTO employees (name, role, branch_id, is_active) VALUES (%s, %s, %s, true) RETURNING id", (name, 'workshop', default_branch) ) new_id = cur.fetchone()[0] existing[key] = new_id user_map[uid] = new_id print(f"Created user {uid}: {name}") return user_map def import_orders(cur, branch_map, client_map, mech_map, courier_map, article_map, user_map): order_files = sorted(DATA_DIR.glob('details/order_*.json')) imported = 0 skipped = 0 for f in order_files: d = json.loads(f.read_text(encoding='utf-8')) datos = d.get('datos', {}) order_number = datos.get('numero') if not order_number: continue cur.execute("SELECT id FROM service_orders WHERE order_number = %s", (order_number,)) if cur.fetchone(): skipped += 1 continue branch_id = branch_map.get(datos.get('idSucursal')) customer_id = client_map.get(datos.get('idCliente')) status = ORDER_STATUS_MAP.get(datos.get('idEstatus'), 'por_revisar') # delivery method via = datos.get('idViaEntrega') mot_rec = datos.get('idMotociclistaRecoleccion') mot_ent = datos.get('idMotociclistaEntrega') courier_id = None if via == 1: delivery_method = 'pickup' elif via == 2: if mot_ent: delivery_method = 'courier' courier_id = courier_map.get(mot_ent) elif mot_rec: delivery_method = 'courier' courier_id = courier_map.get(mot_rec) else: delivery_method = 'delivery' else: delivery_method = None # customer address/phone from order overrides if present? order detail only has ids. # Prefill from customer record (already stored in customers) workshop_name = None customer_phone = None customer_address = None if customer_id: cur.execute("SELECT name, phone, address FROM customers WHERE id = %s", (customer_id,)) row = cur.fetchone() if row: workshop_name, customer_phone, customer_address = row created_by = user_map.get(datos.get('idUsuario')) fecha = datos.get('fecha') created_at = datetime.strptime(fecha, '%Y-%m-%d %H:%M:%S') if fecha else datetime.now() cur.execute( """ INSERT INTO service_orders (tenant_id, branch_id, customer_id, order_number, status, workshop_name, customer_address, customer_phone, vehicle_description, reception_notes, estimated_cost, final_cost, delivery_method, courier_id, is_direct, requires_invoice, created_by, created_at, updated_at) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """, ( 31, # tenant_id for Rached branch_id, customer_id, order_number, status, workshop_name, customer_address, customer_phone, datos.get('vehiculo'), datos.get('observaciones'), datos.get('presupuesto') or 0, datos.get('total') or 0, delivery_method, courier_id, bool(datos.get('ordenDirecta')), bool(datos.get('requiereFactura')), created_by, created_at, created_at, ) ) so_id = cur.fetchone()[0] # status history cur.execute( """ INSERT INTO service_order_status_history (service_order_id, new_status, changed_by, notes, created_at) VALUES (%s, %s, %s, %s, %s) """, (so_id, status, created_by, 'Importado desde app Rached', created_at) ) # items (free-text lines; not linked to inventory) for it in datos.get('detalles', []): articulo = it.get('articulo', {}) art_id = articulo.get('id') art_name = article_map.get(art_id) or articulo.get('nombre') or 'Concepto' mech_id = mech_map.get(it.get('idMecanico')) item_status = ITEM_STATUS_MAP.get(it.get('idEstatusDetalle'), 'por_revisar') qty = it.get('cantidad', 1) price = it.get('precio') or 0 cur.execute( """ INSERT INTO service_order_items (service_order_id, inventory_id, part_number, name, quantity, unit_cost, unit_price, status, mechanic_id, observations) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( so_id, None, None, art_name, qty, price, price, item_status, mech_id, it.get('observaciones'), ) ) imported += 1 if imported % 100 == 0: print(f"Imported {imported} orders...") return imported, skipped def main(): print(f"Importing Rached workshop data into {TENANT_DB}") conn = connect() cur = conn.cursor() try: branch_map = ensure_branches(cur) print(f"Branch map: {branch_map}") client_map = ensure_customers(cur, branch_map) print(f"Customers to import: {len(client_map)}") mech_catalog = load_json('catalog_mecanicos') mech_map = ensure_employees(cur, branch_map, 'mechanic', mech_catalog) print(f"Mechanics map: {len(mech_map)}") courier_catalog = load_json('catalog_motociclistas') courier_map = ensure_couriers(cur, courier_catalog, tenant_id=31) print(f"Couriers map: {len(courier_map)}") article_map = ensure_articles(cur, branch_map) print(f"Articles map: {len(article_map)}") order_details = [json.loads(f.read_text(encoding='utf-8')) for f in DATA_DIR.glob('details/order_*.json')] user_map = ensure_users(cur, branch_map, order_details) print(f"Users map: {len(user_map)}") imported, skipped = import_orders(cur, branch_map, client_map, mech_map, courier_map, article_map, user_map) print(f"Imported: {imported}, Skipped (already exist): {skipped}") conn.commit() except Exception as e: conn.rollback() print(f"ERROR: {e}") raise finally: cur.close() conn.close() if __name__ == '__main__': main()