#!/usr/bin/env python3 """Siembra inicial del 13-oct-2026: plan de producción, los 4 sitios, los ítems de inventario y los pendientes del tablero (brief §4). Idempotente: lo que ya existe (mismo id) se deja como está. Uso (en el servidor, como el usuario del servicio, normalmente leo): cd /srv/projects/drogheda/hbox-docs ./backend/venv/bin/python3 backend/seed_1013.py # usa los mismos valores por omisión que app.py TAREAS_DIR=/otra/ruta ./backend/venv/bin/python3 backend/seed_1013.py --usuario leo ./backend/venv/bin/python3 backend/seed_1013.py --extra tareas-notion.json # importa más pendientes ./backend/venv/bin/python3 backend/seed_1013.py --solo plan,sitios # una parte Escribe directo en disco con las mismas funciones del backend (no pasa por nginx), así que no necesita contraseña. El servicio puede seguir corriendo mientras se siembra. --extra espera un JSON con una lista de pendientes con los mismos campos que POST /api/tareas (cada uno con su "id" para que la importación también sea idempotente). """ import os, sys, json, argparse, datetime AQUI = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, AQUI) import app as backend # noqa: E402 (usa ACTAS_DIR/TAREAS_DIR/INVENTARIO_DIR/SITIOS_DIR/PLAN_FILE del entorno) HOY = datetime.date.today().isoformat() def marca(u): t = backend.ahora() return {"creado_por": u, "creado_en": t, "modificado_por": u, "modificado_en": t} # ------------------------------------------------------------------ datos SITIOS = [ {"id": "icave", "nombre": "ICAVE", "cliente": "Hutchison Ports", "ubicacion": "Veracruz, Ver.", "alias": ["ICAVE (Veracruz)", "Oficina ICAVE"], "orden": 10, "contactos": [{"nombre": "por confirmar", "rol": "supervisor de patio (autoriza acceso a máquinas)", "telefono": "", "correo": ""}], "notas": "Terminal de contenedores. El acceso a cada máquina se pide por correo (Operaciones). " "Compara el HBOX RTK con un CalAmp legado (viajes en Wialon)."}, {"id": "lzc", "nombre": "LZC", "cliente": "Hutchison Ports", "ubicacion": "Lázaro Cárdenas, Mich.", "alias": ["LCT", "Lázaro Cárdenas"], "orden": 20, "contactos": [{"nombre": "por confirmar", "rol": "", "telefono": "", "correo": ""}], "notas": "En notas antiguas aparece como LCT."}, {"id": "eit", "nombre": "EIT", "cliente": "Hutchison Ports", "ubicacion": "por confirmar", "alias": [], "orden": 30, "contactos": [{"nombre": "por confirmar", "rol": "", "telefono": "", "correo": ""}], "notas": ""}, {"id": "timsa", "nombre": "TIMSA", "cliente": "Hutchison Ports", "ubicacion": "por confirmar", "alias": [], "orden": 40, "contactos": [{"nombre": "por confirmar", "rol": "", "telefono": "", "correo": ""}], "notas": ""}, ] # stock 0: Leo y Jesús lo llenan con el conteo físico (movimiento 'ajuste'). INVENTARIO = [ ("EB-V55", "EinarBoard V5.5 (tarjeta probada o por probar)", "tarjeta", ["V5.5"], 0, "JLCPCB", "", "Lote de 15 (22-ago). Inspeccionar U6 con lupa antes de energizar."), ("EB-V56", "EinarBoard V5.6", "tarjeta", ["V5.6"], 0, "Aldair (por confirmar)", "", "Pedido 113. Pasta EP U6 ≤50 %, vías tapadas, X-ray/AOI en U6."), ("EB-V57", "EinarBoard V5.7 (fuente a bordo)", "tarjeta", ["V5.7"], 0, "Aldair (por confirmar)", "", "Pedido 327."), ("EB-V6", "EinarBoard v6 (GNSS + 4G a bordo)", "tarjeta", ["v6"], 0, "Aldair (por confirmar)", "", "Pedido 130 + reemplazos."), ("ARD-SRTK2B", "ArduSimple simpleRTK2B Pro (ZED-F9P)", "modulo", ["V5.5", "V5.6", "V5.7"], 0, "ArduSimple", "", "500 pedidos (26-sep)."), ("MODEM-4G-NTRIP", "4G NTRIP Master", "modulo", ["V5.5", "V5.6", "V5.7"], 0, "ArduSimple", "", ""), ("SD-15B-05", "Fuente Mean Well SD-15B-05 (24 V → 5 V)", "fuente", ["V5.5", "V5.6"], 113, "Mouser / Digikey", "709-SD15B05", "Solo V5.6: 113 para el pedido. V5.7 y v6 no la usan."), ("PACK-LFP-26650-1S4P", "Paquete LFP 26650 1S4P (alta temperatura)", "bateria", ["V5.6", "V5.7", "v6"], 0, "por confirmar", "", "JP2 = 0 (LiFePO4). Flotación 3.50 V (R55 5.6 k)."), ("GAB-IP67", "Gabinete IP67 (máquina)", "gabinete", ["V5.5", "V5.6", "V5.7", "v6"], 0, "por confirmar", "", ""), ("CAJA-HBOX", "Caja plástica HBOX", "caja", ["V5.5", "V5.6", "V5.7", "v6"], 0, "por confirmar", "", "Pendiente: el cable Ethernet no cabe (tablero, pista Mecánica)."), ("ANT-GNSS", "Antena GNSS (SMA, con cable)", "antena", ["V5.5", "V5.6", "V5.7", "v6"], 0, "por confirmar", "", "~4 semanas de entrega."), ("ANT-LTE", "Antena LTE (2 por unidad)", "antena", ["V5.5", "V5.6", "V5.7", "v6"], 0, "por confirmar", "", "Dos por unidad."), ("CABLE-ETH", "Cable Ethernet (exterior)", "cable", ["V5.6", "V5.7", "v6"], 0, "por confirmar", "", "Ver pendiente de la caja HBOX."), ("CONN-GX16", "Conector GX16 (aéreo + panel) y arnés", "conector", ["V5.5", "V5.6", "V5.7", "v6"], 0, "por confirmar", "", "1 = +24 V, 2 = GND, 3 = ignición, 4 = GND."), ("FUSIBLE-DIN", "Fusible DIN (gabinete)", "componente-critico", ["V5.5", "V5.6", "V5.7", "v6"], 0, "por confirmar", "", ""), ("LTC4040", "LTC4040 (U6, QFN 4x5) — cargador/respaldo", "componente-critico", ["V5.5", "V5.6", "V5.7", "v6"], 0, "LCSC / ADI", "C2057152", "Largo plazo de entrega. Regla DFM: pasta EP ≤50 %."), ("TPS54360B", "TPS54360B — buck 24 V → 5 V a bordo (U25)", "componente-critico", ["V5.7", "v6"], 0, "LCSC / TI", "", "Fuente a bordo de V5.7 y v6."), ("SRP1265A", "Inductor SRP1265A (fuente a bordo)", "componente-critico", ["V5.7", "v6"], 0, "LCSC / Bourns", "", ""), ] TAREAS = [ {"id": "t-v56-pasada-2", "pista": "ingenieria", "estado": "en-curso", "responsable": "michelle", "version_tarjeta": "V5.6", "titulo": "V5.6 pasada 2 — correcciones antes del pedido", "descripcion": "BAT_PLUS fuera de In1/In2 (bloqueante), MAIN5V, SYS5V, SW3_NODE, pistas bajo U16 a B.Cu, sueltos, rev 5.6, stackup. " "Los conectores se quedan donde están (opción A). Zona x 170–195 / y 42–72 libre para la fuente de V5.7.", "enlaces": [{"titulo": "Instrucciones pasada 2", "url": "/hardware/v56-layout-pasada-2/"}, {"titulo": "Auditoría 13-oct (inglés)", "url": "/hardware/v56-auditoria-layout-13oct/"}], "etiquetas": ["layout", "bloqueante"]}, {"id": "t-v57-fuente", "pista": "ingenieria", "estado": "en-curso", "responsable": "leo", "version_tarjeta": "V5.7", "titulo": "V5.7 — colocar y rutear la fuente a bordo (guía de layout)", "descripcion": "Bloque U25 (TPS54360B) + F2 SMD en la esquina superior derecha. Solo las zonas acordadas con Michelle.", "enlaces": [{"titulo": "Guía de layout V5.7", "url": "/hardware/v57-layout-guia/"}, {"titulo": "ECO V5.7", "url": "/hardware/v57-eco-fuente/"}], "etiquetas": ["layout"]}, {"id": "t-v57-cierre", "pista": "ingenieria", "estado": "por-hacer", "responsable": "michelle", "version_tarjeta": "V5.7", "titulo": "V5.7 — replicar correcciones de V5.6 y cerrar layout", "descripcion": "Después de que Leo deje la fuente colocada y V5.6 pasada 2 esté cerrada.", "bloqueada_por": ["t-v56-pasada-2", "t-v57-fuente"], "enlaces": [{"titulo": "Guía de layout V5.7", "url": "/hardware/v57-layout-guia/"}], "etiquetas": ["layout"]}, {"id": "t-v56-revision-gerber", "pista": "ingenieria", "estado": "por-hacer", "responsable": "claude", "version_tarjeta": "V5.6", "titulo": "V5.6 — revisión pad a pad + Gerber/BOM/CPL", "descripcion": "Paridad con netlist, DRC, pasta EP U6 ≤50 %, vías tapadas; generar el paquete de fabricación.", "bloqueada_por": ["t-v56-pasada-2"], "etiquetas": ["auditoría", "fabricación"]}, {"id": "t-v6-cierre-pedido", "pista": "ingenieria", "estado": "por-hacer", "responsable": "leo, claude", "version_tarjeta": "v6", "titulo": "v6 — cierre para pedido (según «Próximos pasos para ordenar»)", "enlaces": [{"titulo": "Próximos pasos v6", "url": "/hardware/v6-proximos-pasos/"}, {"titulo": "Plan de corrección", "url": "/hardware/v6-plan-correccion/"}], "descripcion": "Aplicar a P7/P8 lo decidido para P12 en V5.6; regla DFM U6; no habrá v6.1.", "etiquetas": ["fabricación"]}, {"id": "t-caja-ethernet", "pista": "mecanica", "estado": "bloqueado", "responsable": "", "version_tarjeta": "—", "titulo": "Caja plástica HBOX — el cable Ethernet no cabe: definir y aplicar el cambio", "descripcion": "Falta definir la causa exacta (largo del boot RJ45, tamaño de la abertura o radio de curvatura) y quién lo corrige " "(pieza impresa en 3D o proveedor). Al definirlo: escribir el procedimiento y mover a En curso.", "etiquetas": ["caja", "por asignar"]}, {"id": "t-paquetes-aldair", "pista": "compras", "estado": "por-hacer", "responsable": "leo", "version_tarjeta": "—", "titulo": "Enviar paquetes de fabricación V5.6, V5.7 y v6 a Aldair; cotización para 570 unidades (113 / 327 / 130)", "descripcion": "Gerber + BOM + CPL + planos. Aldair = fabricación/ensamble (rol por confirmar).", "bloqueada_por": ["t-v56-revision-gerber"], "etiquetas": ["pedido 570"]}, {"id": "t-compras-materiales", "pista": "compras", "estado": "por-hacer", "responsable": "leo, jesús", "version_tarjeta": "—", "titulo": "Mean Well SD-15B-05 ×113 (solo V5.6); paquetes LFP; gabinetes; cajas HBOX; antenas", "descripcion": "Chips y antenas RTK: ~4 semanas de entrega. Registrar las llegadas como entradas en el inventario.", "enlaces": [{"titulo": "Inventario", "url": "/inventario/inventario.html"}], "etiquetas": ["pedido 570"]}, {"id": "t-plan-570", "pista": "produccion", "estado": "por-hacer", "responsable": "leo", "version_tarjeta": "—", "titulo": "Plan 570: cargar objetivos por versión y fechas", "enlaces": [{"titulo": "Tablero de producción", "url": "/tableros/produccion.html"}], "descripcion": "Editar el plan desde el tablero de producción (solo admin): fecha objetivo por versión."}, {"id": "t-inventario-inicial", "pista": "produccion", "estado": "por-hacer", "responsable": "jesús", "version_tarjeta": "—", "titulo": "Inventario inicial: conteo físico de tarjetas, módulos, gabinetes, cajas, packs", "enlaces": [{"titulo": "Inventario", "url": "/inventario/inventario.html"}], "descripcion": "Un movimiento de tipo «ajuste» por ítem con el conteo físico y el motivo «conteo inicial»."}, {"id": "t-sitios-alta", "pista": "campo", "estado": "por-hacer", "responsable": "leo", "version_tarjeta": "—", "titulo": "Dar de alta los 4 sitios (ICAVE, LZC, EIT, TIMSA) con contactos y unidades instaladas", "enlaces": [{"titulo": "Sitios", "url": "/sitios/"}], "descripcion": "Los 4 sitios ya existen con «por confirmar»: completar ubicación y contactos (Editar sitio, solo admin)."}, {"id": "t-acta-instalacion", "pista": "campo", "estado": "por-hacer", "responsable": "claude", "version_tarjeta": "—", "titulo": "Acta de instalación en campo: formulario nuevo + instrucciones", "enlaces": [{"titulo": "Acta de instalación", "url": "/formularios/instalacion.html"}], "descripcion": "Formulario publicado el 13-oct; falta probarlo en una instalación real y ajustar el checklist."}, ] # ------------------------------------------------------------------ siembra def sembrar_plan(u, fuerza=False): if os.path.isfile(backend.PLAN_FILE) and not fuerza: return "plan: ya existe, se deja (usa --forzar-plan para reescribirlo)" p = json.loads(json.dumps(backend.PLAN_DEFECTO)) p["actualizado_por"] = u p["actualizado_en"] = backend.ahora() p["historial"] = [{"quien": u, "cuando": p["actualizado_en"], "cambio": "plan sembrado (seed_1013)"}] backend._escribir_json(backend.PLAN_FILE, p) return "plan: creado (570 → 113 V5.6 / 327 V5.7 / 130 v6)" def sembrar_sitios(u): n = 0 for s in SITIOS: if os.path.isfile(backend._ruta_registro(backend.SITIOS_DIR, s["id"])): continue t, err = backend._sitio_valido(s) if err: print(" sitio", s["id"], "->", err); continue t["id"] = s["id"] t["orden"] = s.get("orden", 50) t.update(marca(u)) t["historial"] = [{"quien": u, "cuando": t["creado_en"], "cambio": "sitio sembrado (seed_1013)"}] backend._guardar_sitio(t) n += 1 return f"sitios: {n} creados, {len(SITIOS) - n} ya existían" def sembrar_inventario(u): n = 0 for codigo, desc, cat, aplica, minimo, prov, cod_prov, notas in INVENTARIO: rid = backend.slug(codigo, 40) if os.path.isfile(backend._ruta_registro(backend.INV_ITEMS(), rid)): continue t, err = backend._item_valido({"codigo": codigo, "descripcion": desc, "categoria": cat, "aplica_a": aplica, "unidad": "pza", "minimo": minimo, "proveedor": prov, "codigo_proveedor": cod_prov, "notas": notas, "ubicacion": ""}) if err: print(" item", codigo, "->", err); continue t["id"] = rid t.update(marca(u)) t["historial"] = [{"quien": u, "cuando": t["creado_en"], "cambio": "ítem sembrado (seed_1013), stock 0"}] backend._guardar_item(t) n += 1 return f"inventario: {n} ítems creados, {len(INVENTARIO) - n} ya existían (stock 0: falta el conteo físico)" def sembrar_tareas(u, lista, origen="seed_1013"): n, orden = 0, {} existentes = backend._listar_json(backend.TAREAS_DIR) for t in existentes: orden[t.get("estado")] = max(orden.get(t.get("estado"), -1), t.get("orden", 0)) for d in lista: rid = backend.slug(d.get("id"), 60) if not rid: print(" pendiente sin id, se omite:", d.get("titulo")); continue if os.path.isfile(backend._ruta_registro(backend.TAREAS_DIR, rid)): continue t, err = backend._tarea_valida(d) if err: print(" pendiente", rid, "->", err); continue t["id"] = rid if "orden" not in d: orden[t["estado"]] = orden.get(t["estado"], -1) + 1 t["orden"] = orden[t["estado"]] t.update(marca(u)) t["historial"] = [{"quien": u, "cuando": t["creado_en"], "cambio": f"creado en {t['estado']} ({origen})"}] backend._guardar_tarea(t) n += 1 return f"pendientes ({origen}): {n} creados, {len(lista) - n} ya existían u omitidos" def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--usuario", default="leo", help="quién firma la siembra en los campos de auditoría") ap.add_argument("--solo", default="plan,sitios,inventario,tareas", help="partes a sembrar, separadas por coma") ap.add_argument("--extra", help="JSON con una lista de pendientes adicionales (p. ej. importación de Notion)") ap.add_argument("--forzar-plan", action="store_true", help="reescribe el plan aunque ya exista") a = ap.parse_args() partes = {p.strip() for p in a.solo.split(",")} print("Carpetas:", {"tareas": backend.TAREAS_DIR, "inventario": backend.INVENTARIO_DIR, "sitios": backend.SITIOS_DIR, "plan": backend.PLAN_FILE}) for d in (backend.TAREAS_DIR, backend.INV_ITEMS(), backend.INV_MOVS(), backend.SITIOS_DIR, os.path.dirname(backend.PLAN_FILE)): os.makedirs(d, exist_ok=True) if "plan" in partes: print(sembrar_plan(a.usuario, a.forzar_plan)) if "sitios" in partes: print(sembrar_sitios(a.usuario)) if "inventario" in partes: print(sembrar_inventario(a.usuario)) if "tareas" in partes: print(sembrar_tareas(a.usuario, TAREAS)) if a.extra: with open(a.extra, encoding="utf-8") as f: extra = json.load(f) if not isinstance(extra, list): sys.exit("--extra debe ser una lista JSON de pendientes") print(sembrar_tareas(a.usuario, extra, origen=os.path.basename(a.extra))) print("Listo.") if __name__ == "__main__": main()