240 lines
16 KiB
Python
240 lines
16 KiB
Python
#!/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()
|