Files
Autoparts-DB/scripts/import_rached_workshop.py
consultoria-as f42910f4f6
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled
feat: cashier/counter reports, service-order & remission flows, Rached migration utils
- Add "Mis cortes de caja" report for cashiers/counters with sales detail.
- Cash register history scoped to own cuts for non-admin roles; new /register/<id>/sales endpoint.
- Remove dashboard from cashier menu; add Reports to cashier/counter.
- Service orders: assign mechanic, budget field, invoice flag, counter/cashier can add items/remissions, convert to remission.
- Remission notes module (UI, CSS, courier, counter remissions).
- Customer hard-delete and vehicle/customer linkage in workshop.
- POS: always show search results, compact payment grid, credit validation, tier pricing (5%/10%), ticket with customer/folio.
- Inventory: CSV template with sku_secondary, alias import.
- Rached migration scripts and DB migrations.
- Version-bump cached JS/CSS query strings.

Excludes local Rached session tokens/captures (rached_*.json / rached_*.txt).
2026-07-02 12:51:56 +00:00

443 lines
14 KiB
Python

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