nexusflex-mcp 3.71.0 → 3.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +18 -0
- package/INSTALAR.md +18 -0
- package/docs.mjs +53 -1
- package/package.json +4 -2
- package/server.mjs +206 -1
- package/skills/control-por-foto/SKILL.md +71 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nexusflex",
|
|
3
|
+
"description": "Nexus Flex en Claude: el MCP (altas, consultas, control por foto; nunca mueve dinero) y las skills (control por foto) en un solo instalable.",
|
|
4
|
+
"version": "3.72.0",
|
|
5
|
+
"author": { "name": "Nexus Flex", "url": "https://nexusflex.com.ar" },
|
|
6
|
+
"homepage": "https://nexusflex.com.ar/mcp",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"mcpServers": {
|
|
9
|
+
"nexusflex": {
|
|
10
|
+
"command": "npx",
|
|
11
|
+
"args": ["-y", "nexusflex-mcp@latest"],
|
|
12
|
+
"env": {
|
|
13
|
+
"NEXUSFLEX_MCP_ALLOW_WRITE": "true",
|
|
14
|
+
"NEXUSFLEX_MCP_ALLOW_PRECIOS": "false"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/INSTALAR.md
CHANGED
|
@@ -11,6 +11,24 @@ Nexus Flex **con tu cuenta**, en tu computadora, hablando con la API igual que l
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## La forma fácil: el PLUGIN (MCP + skills en un solo paso)
|
|
15
|
+
|
|
16
|
+
El plugin **nexusflex** trae el MCP **y** las skills (control por foto) juntos: no hay que
|
|
17
|
+
editar ningún archivo ni instalar la skill aparte. Necesitás Node.js 20+ (paso 1).
|
|
18
|
+
|
|
19
|
+
- **Claude Desktop / claude.ai:** *Personalizar* (Customize) → **Plugins** → **Agregar** →
|
|
20
|
+
**Agregar marketplace** → pegá `https://nexusflex.com.ar/claude/marketplace.json` → en
|
|
21
|
+
la lista aparece **nexusflex** → **Agregar**. Las skills quedan en *Personalizar → Skills*
|
|
22
|
+
y el conector en la solapa *Conectores* del plugin.
|
|
23
|
+
- **Claude Code:**
|
|
24
|
+
```bash
|
|
25
|
+
claude plugin marketplace add https://nexusflex.com.ar/claude/marketplace.json
|
|
26
|
+
claude plugin install nexusflex@nexusflex
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
La primera vez que lo uses te muestra un link + código para **autorizar** con tu cuenta
|
|
30
|
+
de Nexus Flex (paso 2). Si preferís hacerlo a mano, seguí los pasos de abajo.
|
|
31
|
+
|
|
14
32
|
## 1. Requisitos
|
|
15
33
|
- **Node.js 20+** — https://nodejs.org (LTS). Verificá con `node --version`.
|
|
16
34
|
- **Claude Desktop** (https://claude.ai/download) o Claude Code.
|
package/docs.mjs
CHANGED
|
@@ -149,6 +149,7 @@ const GROUPS = [
|
|
|
149
149
|
{ titulo: "Vendedor \u2014 mi operaci\xF3n", tools: ["mis_envios", "envio_consultar", "mis_sucursales", "sucursal_guardar", "mi_colecta", "colecta_solicitar", "colecta_auto"] },
|
|
150
150
|
{ titulo: "Vendedor \u2014 mi stock y n\xFAmeros", tools: ["mi_stock", "mi_disponible", "mi_rentabilidad", "mis_productos", "mis_top_productos", "mis_kpis"] },
|
|
151
151
|
{ titulo: "Mensajero", tools: ["mi_ruta", "mis_colectas"] },
|
|
152
|
+
{ titulo: "Control por foto (staff)", tools: ["control_foto_preparar", "control_foto_estructura", "control_foto_carpeta", "control_foto_foto"] },
|
|
152
153
|
{ titulo: "Env\xEDos (staff)", tools: ["envio_cargar", "envio_desde_etiqueta_ml", "envio_procesar", "envio_entregar", "envio_estado", "envios_trabados", "planilla_reporte", "envio_asignar_grupo", "nodo_provisorio_crear", "provisorio_conciliar", "envio_reasignar_cliente", "envio_mensajero_externo", "envio_editar_zona", "envio_editar_fecha", "cobro_corregir", "sin_vendedor", "asignar_sin_vendedor", "envio_completar_ciclo", "envio_pago_mensajero", "envio_corregir_estado", "envio_asignar_mensajero"] },
|
|
153
154
|
{ titulo: "Colecta y zonas (staff)", tools: ["colecta_ver", "colecta_pendientes", "colecta_historial", "colecta_configurar", "colecta_fija", "colecta_asignar", "colecta_desasignar", "zonas_reparto", "zona_barrios", "zonas_simetria", "asignar_mensajero_zona", "asignar_nodo_zona", "envios_por_zona", "procesar_zona", "rechazar_zona", "grupos_tarifas", "nodo_alias_poner", "zona_dejar", "nodo_link_confirmacion", "zonas_grupo", "zona_mover_metazona", "zona_componer_grupo", "grupo_tarifa_set", "retiros_cargar"] },
|
|
154
155
|
{ titulo: "Rendiciones y reclamos", tools: ["rendiciones", "rendicion_revertir", "reclamos_listar"] },
|
|
@@ -213,6 +214,10 @@ const TOOL_DOCS = {
|
|
|
213
214
|
mi_ruta: { que: "Tus entregas asignadas: las paradas de tu ruta del d\xEDa.", uso: "Sin par\xE1metros.", ej: "mi_ruta \u2192 orden de reparto de hoy.", no: "No marca entregas (eso se hace escaneando en la app)." },
|
|
214
215
|
mis_colectas: { que: "Las colectas/retiros que ten\xE9s asignados.", uso: "Sin par\xE1metros.", ej: "mis_colectas \u2192 a qu\xE9 vendedores ten\xE9s que ir a buscar.", no: "No las completa/confirma desde ac\xE1." },
|
|
215
216
|
envio_cargar: { que: "Registra un env\xEDo nuevo (queda 'A retirar') y devuelve tracking + link de etiqueta + link para subir foto.", uso: "Obligatorios: destinatario, telefono, direccion, localidad. Staff pasa `cliente` (nombre del vendedor de su nodo); el vendedor no. montoCobro = cobro contra entrega (opcional). `fecha` (solo staff, dd/mm/aaaa) si el env\xEDo NO es de hoy: es la que define en qu\xE9 semana se liquida; no se admite futura.", ej: "Staff: envio_cargar cliente:'Distri Sur' destinatario:'Ana' telefono:'11...' direccion:'Belgrano 100' localidad:'Lan\xFAs' montoCobro:15000.", no: "No mueve dinero (montoCobro es el cobro a destino, no un asiento); no imprime, devuelve el link. Un vendedor/cadete NO puede pasar `fecha` (mover\xEDa sus paquetes de per\xEDodo de liquidaci\xF3n)." },
|
|
217
|
+
control_foto_preparar: { que: "Crea en la PC las carpetas del control por foto de uno o varios d\xEDas, con los nombres EXACTOS de tus grupos y cadetes (DADOS/<grupo>, DADOS/<cadete>, RECIBIDOS/<cadete>, RECIBIDOS/<grupo>) + un LEEME.txt.", uso: "Cuando el usuario dice 'hagamos el control por foto del d\xEDa tal': ruta (d\xF3nde armarlas, ej. el Escritorio) + fechas ('14/09/2026', '14/09 a 19/09', '14/09, 16/09'). Decile d\xF3nde quedaron y que suelte las fotos; despu\xE9s `control_foto_carpeta`.", ej: "control_foto_preparar ruta:'C:/Users/\u2026/Desktop/Control por fotos' fechas:'14/09 a 19/09' \u2192 6 d\xEDas con DADOS/Bonorino, DADOS/Portela, DADOS/Gerardo Soveron\u2026", no: "Solo en el MCP local (npx). No pisa carpetas que ya existen; no procesa fotos." },
|
|
218
|
+
control_foto_estructura: { que: "C\xF3mo se arman las carpetas del control por foto para TU nodo: tus grupos log\xEDsticos y qui\xE9nes reparten (los nombres que valen en DADOS/\u2026 y RECIBIDOS/\u2026).", uso: "Sin argumentos (el admin global pasa `nodo`). Llamalo antes del primer control para explicarle la estructura al usuario, o cuando una carpeta no se reconoce.", ej: "control_foto_estructura \u2192 grupos: Bonorino, Portela, Z\xE1rate Campana \xB7 cadetes: Gerardo Soveron, Miguel Punturirero\u2026", no: "No procesa fotos ni cambia nada." },
|
|
219
|
+
control_foto_carpeta: { que: "Procesa la CARPETA de un d\xEDa (o de varios) del control por foto, desde tu PC: cada foto bajo DADOS/\u2026 o RECIBIDOS/\u2026 con su carpeta y el d\xEDa que sale del nombre de la carpeta.", uso: "ruta: la carpeta del d\xEDa (o la que contiene varios d\xEDas). Primero SIN `aplicar` (simula) \u2192 mostr\xE1 el resumen y resolv\xE9 las preguntas (cuentas de ML desconocidas, zonas con varios responsables, duplicados) \u2192 despu\xE9s `aplicar:true`. Reanudable: lo ya procesado no se repite. Deja _control-foto-<d\xEDa>.json y copia lo dudoso a _sin_identificar.", ej: "control_foto_carpeta ruta:'\u2026/Control por fotos/Lunes 14-9' \u2192 312 fotos: 280 ok, 25 preguntas (3 cuentas de ML desconocidas\u2026). Despu\xE9s, aplicar:true.", no: "Solo en el MCP local (npx): el conector remoto no ve tu disco. No mueve dinero; no descomprime .zip (avisa)." },
|
|
220
|
+
control_foto_foto: { que: "Procesa UNA foto de etiqueta del control por foto: QR o IA \u2192 env\xEDo sin duplicar \u2192 asignaci\xF3n seg\xFAn la carpeta \u2192 Entregado en el d\xEDa real \u2192 foto en el historial.", uso: "ruta (MCP local) o imagen en base64 (remoto) + carpeta ('DADOS/Bonorino', 'RECIBIDOS/Matanza/Gerardo'\u2026) + fecha. Sirve para CONTESTAR una pregunta: repet\xED la foto con `nodoEntrega` (qui\xE9n la llev\xF3) o `cliente` (de qui\xE9n es la cuenta de ML). `simular:true` = solo mirar.", ej: "control_foto_foto ruta:'\u2026/RECIBIDOS/Matanza/Gerardo/IMG_1.jpg' carpeta:'RECIBIDOS/Matanza/Gerardo' fecha:'14/09/2026' cliente:306", no: "No mueve dinero; no toca env\xEDos de afuera de tu red (tu nodo + los de tus grupos); no reasigna lo que ya entr\xF3 en un cierre semanal congelado." },
|
|
216
221
|
envio_entregar: { que: "Marca como ENTREGADO uno o varios env\xEDos que YA est\xE1n cargados, por tracking \u2014 y con `fecha`, el d\xEDa REAL en que se entreg\xF3.", uso: "trackings: ['TN-123', 'NFABC123'] + fecha (opcional, dd/mm/aaaa). Usalo cuando no est\xE1s cargando desde una etiqueta (para eso est\xE1 `envio_desde_etiqueta_ml` con `avanzarA`). Sin `fecha` el historial dice que se entreg\xF3 ahora, que es mentira cuando la tanda se controla por foto d\xEDas despu\xE9s; sobre un env\xEDo que YA figura entregado, `fecha` CORRIGE el cierre ya registrado (vuelve en `fechaCorregida`). Entrega solo con los datos (sin foto ni firma). Scopeado a tu nodo.", ej: "envio_entregar trackings:['TN-2057600163'] fecha:'02/09/2026' \u2192 lo cierra con el d\xEDa en que sali\xF3 y sella la log\xEDstica de entrega para el clearing.", no: "No mueve dinero; `fecha` toca el evento 'Entregado' del historial, NO la semana en que se liquida (eso es envio_editar_fecha); no pide foto ni firma (para eso est\xE1 la entrega del cadete en la PWA); no revierte una entrega." },
|
|
217
222
|
envio_procesar: { que: "Marca un env\xEDo como PROCESADO / recibido en el centro de distribuci\xF3n (queda 'En centro de distribuci\xF3n') por su tracking, sin escanear.", uso: "tracking del env\xEDo. Scopeado a tu nodo/grupo (solo proces\xE1s lo que te corresponde). Es la misma acci\xF3n 'procesar' del esc\xE1ner.", ej: "envio_procesar tracking:'NFABC123' \u2192 lo marca recibido en el centro.", no: "No mueve dinero (registra el tramo de clearing del grupo, como procesar_zona); no lo entrega ni lo asigna a un cadete." },
|
|
218
223
|
envio_desde_etiqueta_ml: { que: "Registra un env\xEDo a partir de lo que VOS (Claude) le\xEDste de la foto de una etiqueta de Mercado Libre.", uso: "Le\xE9 la etiqueta: mlShipmentId + (si pod\xE9s) mlQr crudo, mlSenderId del vendedor y el destino. El vendedor se mapea por mlSenderId (cuenta vinculada) o `cliente` por nombre. Una llamada por etiqueta. Si la planilla es de d\xEDas anteriores, pas\xE1 `fecha` (solo staff, dd/mm/aaaa): fecha el env\xEDo Y toda la cadena de `avanzarA` (cada estado con su hora de ESE d\xEDa: Colectado 9, En centro 12, En camino 15, Entregado 18). Sin eso todo queda con la hora del control y se liquida en la semana equivocada. Si el env\xEDo YA estaba cargado tambi\xE9n le mueve la fecha, salvo que ese per\xEDodo ya est\xE9 liquidado (ah\xED rechaza nombrando la liquidaci\xF3n).", ej: "envio_desde_etiqueta_ml mlSenderId:'123456' mlShipmentId:'44000...' destinatario:'Juan' direccion:'...' localidad:'Avellaneda'.", no: "No mueve dinero; si no reconoce el vendedor por mlSenderId ten\xE9s que pasar `cliente`." },
|
|
@@ -351,7 +356,54 @@ function indice(version, disponibles) {
|
|
|
351
356
|
return out.join("\n");
|
|
352
357
|
}
|
|
353
358
|
const CONTROL_POR_FOTO = [
|
|
354
|
-
"FLUJO \u2014 CONTROL POR FOTO
|
|
359
|
+
"FLUJO \u2014 CONTROL POR FOTO (carpetas por d\xEDa) \xB7 sirve para cualquier nodo y cualquiera de sus grupos",
|
|
360
|
+
"",
|
|
361
|
+
"0) EMPEZAR: si el usuario dice 'hagamos el control por foto del d\xEDa tal', creale las carpetas",
|
|
362
|
+
" con `control_foto_preparar ruta:<d\xF3nde> fechas:<d\xEDa o d\xEDas>` (MCP local): quedan con los",
|
|
363
|
+
" nombres exactos de SUS grupos y cadetes + un LEEME.txt. Que suelte ah\xED las fotos.",
|
|
364
|
+
"",
|
|
365
|
+
"1) LAS CARPETAS (una por d\xEDa; el nombre trae la fecha: 'Lunes 14-9-2026', '2026-09-14'):",
|
|
366
|
+
" <d\xEDa>/DADOS/<grupo>/\u2026 lo que pasaste a ese grupo (ej. DADOS/Bonorino) \u2192 se asigna",
|
|
367
|
+
" al nodo responsable de la ZONA del destino en ese grupo",
|
|
368
|
+
" <d\xEDa>/DADOS/<cadete>/\u2026 lo reparti\xF3 un cadete tuyo",
|
|
369
|
+
" <d\xEDa>/RECIBIDOS/\u2026/<cadete>/\u2026 te lo dio otro nodo y lo reparti\xF3 ese cadete (una carpeta",
|
|
370
|
+
" intermedia tipo 'Matanza' o '1ra vuelta' se ignora)",
|
|
371
|
+
" <d\xEDa>/RECIBIDOS/<grupo>/<cadete> igual, pero lleg\xF3 por ESE grupo (si comparten varios)",
|
|
372
|
+
" <d\xEDa>/RECIBIDOS/<grupo>/\u2026 te lo dieron y lo DERIVASTE por ese grupo (ej. Z\xE1rate",
|
|
373
|
+
" Campana): vendedor \u2192 vos \u2192 el responsable de ese grupo",
|
|
374
|
+
" Los nombres son los de TUS grupos y TUS cadetes: `control_foto_estructura` te los lista.",
|
|
375
|
+
" Un s\xE1bado que sale por otro grupo (ej. 'S\xE1bado Zinny') va en DADOS/<ese grupo>.",
|
|
376
|
+
" Fotos en .zip: descomprimilas primero. Carpetas que empiezan con '_' se ignoran.",
|
|
377
|
+
"",
|
|
378
|
+
"2) SIMULAR: `control_foto_carpeta ruta:<carpeta del d\xEDa o de varios d\xEDas>` (MCP local, npx).",
|
|
379
|
+
" No escribe nada. Mostrale al usuario el resumen por d\xEDa: fotos, ok, altas, preguntas.",
|
|
380
|
+
"",
|
|
381
|
+
"3) RESOLVER LAS PREGUNTAS (no adivines nada):",
|
|
382
|
+
" \u2022 cuentasMLDesconocidas \u2192 pregunt\xE1 de qu\xE9 nodo/cliente es cada cuenta (te paso vendedor y",
|
|
383
|
+
" marca a mano). Con la respuesta: `control_foto_foto ruta:<foto> carpeta fecha cliente:<id>`,",
|
|
384
|
+
" o que el nodo due\xF1o vincule/aprenda esa cuenta y volv\xE9s a correr la carpeta.",
|
|
385
|
+
" \u2022 responsable (zona con varios responsables o sin ninguno) \u2192 pregunt\xE1 qui\xE9n lo llev\xF3 y",
|
|
386
|
+
" repet\xED esa foto con `nodoEntrega`.",
|
|
387
|
+
" \u2022 duplicado / no_ml / sin_codigo / fuera_de_red \u2192 se revisan a mano (la foto qued\xF3 copiada",
|
|
388
|
+
" en <d\xEDa>/_sin_identificar).",
|
|
389
|
+
" \u2022 erroresDeCarpeta \u2192 una carpeta que no coincide con un grupo o cadete tuyo: renombrala.",
|
|
390
|
+
"",
|
|
391
|
+
" \u279C M\xC1S C\xD3MODO: al aplicar, lo dudoso se SUBE solo a la app (Log\xEDstica \u2192 \u{1F4F7} Control por foto):",
|
|
392
|
+
" ah\xED se ve la foto grande y se elige el cliente (de cualquier nodo de la red), el nodo que",
|
|
393
|
+
" te lo dio, qui\xE9n lo llev\xF3 o la carpeta. \xABRecordar\xBB asocia la cuenta de ML para la pr\xF3xima",
|
|
394
|
+
" (no marcarlo si el vendedor despacha con varias log\xEDsticas). Ah\xED tambi\xE9n est\xE1n los env\xEDos",
|
|
395
|
+
" SIN VENDEDOR del nodo (paquetes de otro nodo que escane\xF3 un cadete) para asignarles nodo.",
|
|
396
|
+
"",
|
|
397
|
+
"4) APLICAR: `control_foto_carpeta ruta:<\u2026> aplicar:true`. Es reanudable: lo ya procesado",
|
|
398
|
+
" no se repite. Cada paquete queda Entregado con sus estados fechados en el d\xEDa real, su",
|
|
399
|
+
" grupo / nodo que entrega / cadete, y la FOTO en el historial (la que ven los nodos en su",
|
|
400
|
+
" cuenta semanal). Nunca crea duplicados ni toca env\xEDos de afuera de tu red. Lo que ya",
|
|
401
|
+
" entr\xF3 en un cierre semanal congelado no se reasigna; lo nuevo de una semana cerrada entra",
|
|
402
|
+
" en el cierre siguiente, marcado como tarde.",
|
|
403
|
+
"",
|
|
404
|
+
"5) AL TERMINAR: resumen por d\xEDa y la lista de lo que qued\xF3 en _sin_identificar.",
|
|
405
|
+
"",
|
|
406
|
+
"\u2014\u2014 Carga suelta desde UNA etiqueta (sin carpetas) \u2014\u2014",
|
|
355
407
|
"Qu\xE9 es: cargar paquetes que YA vienen con etiqueta de Mercado Libre pero NO est\xE1n vinculados",
|
|
356
408
|
"a una cuenta ML (un cliente/nodo te los baj\xF3 etiquetados y te dej\xF3 las fotos en una CARPETA).",
|
|
357
409
|
"Sirve para registrar r\xE1pido el movimiento y el CLEARING entre nodos, sin escanear.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexusflex-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.72.0",
|
|
4
4
|
"description": "MCP de Nexus Flex: operá tu nodo/cuenta desde un asistente de IA (altas de clientes, stock, KPIs). Login por autorización web (device-flow). NO toca dinero.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"device-auth.mjs",
|
|
34
34
|
"docs.mjs",
|
|
35
35
|
"README.md",
|
|
36
|
-
"INSTALAR.md"
|
|
36
|
+
"INSTALAR.md",
|
|
37
|
+
"skills/",
|
|
38
|
+
".claude-plugin/"
|
|
37
39
|
],
|
|
38
40
|
"scripts": {
|
|
39
41
|
"start": "node server.mjs",
|
package/server.mjs
CHANGED
|
@@ -56,8 +56,9 @@ const ALLOW_PRECIOS = truthy(process.env.NEXUSFLEX_MCP_ALLOW_PRECIOS);
|
|
|
56
56
|
|
|
57
57
|
// ATADO CON: backend/src/services/mcp-remote.ts (MCP_VERSION + NOVEDADES + mismos tools, salvo SOLO_REMOTO)
|
|
58
58
|
// y mcp/package.json. Lo verifica backend/src/coherencia.test.ts. Ver CLAUDE.md → "Cosas que van juntas".
|
|
59
|
-
const MCP_VERSION = "3.
|
|
59
|
+
const MCP_VERSION = "3.72.0"; // bumpear junto con package.json + backend/services/mcp-remote.ts
|
|
60
60
|
const NOVEDADES = [
|
|
61
|
+
"3.72.0 — CONTROL POR FOTO POR CARPETAS, para cualquier nodo y cualquiera de sus grupos. `control_foto_preparar` (MCP local) te crea las carpetas del día (o de varios) con los nombres exactos de tus grupos y cadetes. Armás una carpeta por día con DADOS/<grupo> (lo que pasaste a un grupo), DADOS/<cadete> (lo repartió un cadete tuyo), RECIBIDOS/…/<cadete> (te lo dio otro nodo y lo repartió tu cadete) y RECIBIDOS/<grupo> (te lo dieron y lo derivaste por ese grupo). `control_foto_carpeta` (MCP local) recorre la carpeta, lee el QR de cada foto (o la IA de etiquetas), busca o da de alta el envío SIN duplicar, lo asigna al responsable de zona / cadete / grupo, lo lleva a Entregado en el día real y le cuelga la foto (la que ven los nodos en su cuenta semanal). Primero simula y te devuelve las preguntas (cuentas de ML desconocidas, zonas con varios responsables, duplicados); se contestan con `control_foto_foto` o, más cómodo, en la app: Logística → 📷 Control por foto (ahí también quedan los envíos SIN VENDEDOR para asignarles el cliente o el nodo que te los dio; «recordar» asocia la cuenta de ML para la próxima, salvo que sea compartida entre logísticas). `control_foto_estructura` te dice qué nombres de carpeta valen. Nada sale de tu red (tu nodo + los de tus grupos). No mueve dinero.",
|
|
61
62
|
"3.71.0 — Habilitadas por el dueño: `liquidacion_excluir_envio` / `liquidacion_reincluir_envio` (sacar o devolver UN envío del cobro de una liquidación emitida, con motivo obligatorio) y `facturacion_estado` / `facturacion_preparar_cliente` (qué falta para facturar y cargar los datos fiscales de un cliente) ya funcionan por MCP: el filtro de dinero las deja pasar con su método exacto. Siguen sin pagar, cobrar ni emitir comprobantes, y piden permiso finanzas. Además `mis_envios` ahora sí filtra por `estado`.",
|
|
62
63
|
"3.70.0 — Descripciones al día: textos de las herramientas corregidos para que digan lo que hacen hoy (`envio_entregar`, `envio_asignar_mensajero`, `generar_enlace_vinculacion` vence en 48 hs, `grupo_miembros` y `provisorio_conciliar` apuntan a `grupos_tarifas`), `flujo` lista todos los flujos disponibles, `sugerencias_listar` solo para el superadmin, `envio_cargar` exige `telefono`, parámetros con formato (fechas YYYY-MM-DD, montos en ARS, nombre o id) y las instrucciones del conector más cortas (la guía sigue en `guia`). `liquidacion_excluir_envio`/`reincluir` avisan que hoy el filtro de dinero los bloquea por MCP (se hace desde la PWA).",
|
|
63
64
|
"3.69.1 — AYUDA completa: `ayuda` ahora tiene la ficha (qué hace, cómo usarlo, ejemplo, qué NO hace) de los 20 tools que no la tenían (cierre semanal, zonas de grupo, tarifas de grupo, sin vendedor, corregir estado, pago al cadete, retiros…) y los 26 que no aparecían en el índice. La ayuda del paquete npm se genera desde la del conector, así que ya no se desincronizan. No mueve dinero.",
|
|
@@ -174,6 +175,209 @@ const q = (params) => {
|
|
|
174
175
|
const registrados = new Set(); // para que `ayuda` liste solo lo que este usuario tiene
|
|
175
176
|
const tool = (name, def, handler) => { registrados.add(name); server.registerTool(name, def, handler); };
|
|
176
177
|
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// CONTROL POR FOTO (staff). El servidor hace el trabajo foto por foto
|
|
180
|
+
// (/control-foto/foto); acá, en la PC, se recorre la CARPETA del día — cosa que el
|
|
181
|
+
// conector remoto no puede (no ve tu disco). Ver `flujo control_por_foto`.
|
|
182
|
+
// ============================================================================
|
|
183
|
+
const EXT_FOTO = /\.(jpe?g|png|webp)$/i;
|
|
184
|
+
/** Día de la carpeta: "Lunes 14-9", "14-09-2026", "2026-09-14"… → "aaaa-mm-dd" (o null). */
|
|
185
|
+
function diaDeCarpeta(nombre, anioDefault = new Date().getFullYear()) {
|
|
186
|
+
const iso = nombre.match(/(20\d{2})-(\d{1,2})-(\d{1,2})/);
|
|
187
|
+
if (iso) return `${iso[1]}-${iso[2].padStart(2, "0")}-${iso[3].padStart(2, "0")}`;
|
|
188
|
+
const m = nombre.match(/(\d{1,2})[-./](\d{1,2})(?:[-./](\d{2,4}))?/);
|
|
189
|
+
if (!m) return null;
|
|
190
|
+
const anio = m[3] ? (m[3].length === 2 ? `20${m[3]}` : m[3]) : String(anioDefault);
|
|
191
|
+
return `${anio}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`;
|
|
192
|
+
}
|
|
193
|
+
/** Fotos de un día: [{ archivo, carpeta (relativa al día, sin el archivo) }]. Saltea carpetas "_…". */
|
|
194
|
+
async function fotosDelDia(diaDir) {
|
|
195
|
+
const fs = await import("node:fs");
|
|
196
|
+
const path = await import("node:path");
|
|
197
|
+
const out = [];
|
|
198
|
+
const zips = [];
|
|
199
|
+
const recorrer = (dir) => {
|
|
200
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
201
|
+
if (e.name.startsWith("_")) continue;
|
|
202
|
+
const p = path.join(dir, e.name);
|
|
203
|
+
if (e.isDirectory()) recorrer(p);
|
|
204
|
+
else if (EXT_FOTO.test(e.name)) out.push({ archivo: p, carpeta: path.relative(diaDir, dir).split(path.sep).join("/") });
|
|
205
|
+
else if (/\.zip$/i.test(e.name)) zips.push(path.relative(diaDir, p));
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
recorrer(diaDir);
|
|
209
|
+
return { fotos: out.filter((f) => /^(dados|recibidos)\b/i.test(f.carpeta)), sueltas: out.filter((f) => !/^(dados|recibidos)\b/i.test(f.carpeta)).length, zips };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const DIAS_SEMANA = ["Domingo", "Lunes", "Martes", "Miercoles", "Jueves", "Viernes", "Sabado"];
|
|
213
|
+
/** "2026-09-14" → "Lunes 14-9-2026" (la convención de las carpetas del control, con el año). */
|
|
214
|
+
function nombreCarpetaDia(iso) {
|
|
215
|
+
const [a, m, d] = iso.split("-").map(Number);
|
|
216
|
+
return `${DIAS_SEMANA[new Date(Date.UTC(a, m - 1, d)).getUTCDay()]} ${d}-${m}-${a}`;
|
|
217
|
+
}
|
|
218
|
+
/** Un nombre de grupo/cadete como nombre de carpeta válido en Windows (sin "(operador)", sin / \ : * ? " < > |). */
|
|
219
|
+
const nombreCarpeta = (s) => String(s).replace(/\s*\([^)]*\)\s*$/, "").replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, " ").trim();
|
|
220
|
+
/** Fechas pedidas: "14/09/2026", "2026-09-14", rangos "14/09/2026 a 19/09/2026" o listas separadas por coma. */
|
|
221
|
+
function fechasPedidas(texto, anioDefault) {
|
|
222
|
+
const una = (t) => {
|
|
223
|
+
const s = String(t).trim();
|
|
224
|
+
const iso = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
|
225
|
+
if (iso) return `${iso[1]}-${iso[2].padStart(2, "0")}-${iso[3].padStart(2, "0")}`;
|
|
226
|
+
const m = s.match(/^(\d{1,2})[-/.](\d{1,2})(?:[-/.](\d{2,4}))?$/);
|
|
227
|
+
if (!m) return null;
|
|
228
|
+
const a = m[3] ? (m[3].length === 2 ? `20${m[3]}` : m[3]) : String(anioDefault);
|
|
229
|
+
return `${a}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`;
|
|
230
|
+
};
|
|
231
|
+
const out = [];
|
|
232
|
+
for (const parte of String(texto).split(",")) {
|
|
233
|
+
const [desde, hasta] = parte.split(/\s+(?:a|al|hasta)\s+/i).map(una);
|
|
234
|
+
if (!desde) return null;
|
|
235
|
+
if (!hasta) { out.push(desde); continue; }
|
|
236
|
+
for (let t = Date.parse(`${desde}T00:00:00Z`); t <= Date.parse(`${hasta}T00:00:00Z`) && out.length < 62; t += 864e5) out.push(new Date(t).toISOString().slice(0, 10));
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function registrarControlFoto() {
|
|
242
|
+
tool("control_foto_preparar", {
|
|
243
|
+
title: "Control por foto: crear las carpetas del día (o de varios días)",
|
|
244
|
+
description: "Arma en la PC las carpetas para el control por foto de uno o varios días, con los nombres EXACTOS de tus grupos y cadetes: <día>/DADOS/<cada grupo>, <día>/DADOS/<cada cadete>, <día>/RECIBIDOS/<cada cadete>, <día>/RECIBIDOS/<cada grupo> (lo que derivaste por ese grupo) + un LEEME.txt que explica qué foto va en cada una. Usalo cuando el usuario dice 'hagamos el control por foto del día tal': creás las carpetas, le decís dónde quedaron y que suelte ahí las fotos; después, `control_foto_carpeta`. No pisa nada que ya exista. No mueve dinero.",
|
|
245
|
+
inputSchema: {
|
|
246
|
+
ruta: zStr().describe("Carpeta donde crear los días (ej. 'C:\\\\Users\\\\...\\\\Desktop\\\\Control por fotos'); se crea si no existe"),
|
|
247
|
+
fechas: zStr().describe("Día o días: '14/09/2026', '14/09 a 19/09', '14/09, 16/09' (sin año = el actual)"),
|
|
248
|
+
nodo: zNodo(),
|
|
249
|
+
},
|
|
250
|
+
}, async (a) => run(async () => {
|
|
251
|
+
const fs = await import("node:fs");
|
|
252
|
+
const path = await import("node:path");
|
|
253
|
+
const raiz = String(a.ruta ?? "").trim();
|
|
254
|
+
if (!raiz) return { ok: false, status: 400, data: { message: "Decime en qué carpeta de tu PC armo los días (ej. el Escritorio)." } };
|
|
255
|
+
const fechas = fechasPedidas(a.fechas ?? "", new Date().getFullYear());
|
|
256
|
+
if (!fechas?.length) return { ok: false, status: 400, data: { message: `No entiendo las fechas "${a.fechas ?? ""}". Usá '14/09/2026' o '14/09 a 19/09'.` } };
|
|
257
|
+
const est = await api("GET", `/control-foto/estructura${q({ nodo: a.nodo })}`);
|
|
258
|
+
if (!est.ok) return est;
|
|
259
|
+
const grupos = (est.data.grupos ?? []).map((g) => nombreCarpeta(g.nombre));
|
|
260
|
+
const cadetes = (est.data.cadetes ?? []).map((c) => nombreCarpeta(c.nombre));
|
|
261
|
+
const leeme = [
|
|
262
|
+
"CONTROL POR FOTO — qué foto va en cada carpeta",
|
|
263
|
+
"",
|
|
264
|
+
"DADOS/<grupo> → los paquetes que le pasaste a ese grupo (juntada, colecta del grupo…).",
|
|
265
|
+
"DADOS/<cadete> → los que salieron a repartir con ese cadete tuyo.",
|
|
266
|
+
"RECIBIDOS/<cadete> → los que te dieron otros nodos y repartió ese cadete.",
|
|
267
|
+
"RECIBIDOS/<grupo> → los que te dieron y mandaste por ese grupo a otro nodo (ej. Zárate Campana).",
|
|
268
|
+
"",
|
|
269
|
+
"Una foto por paquete, que se lea la etiqueta. Si un paquete llegó por un grupo en particular,",
|
|
270
|
+
"podés poner la carpeta del cadete adentro: RECIBIDOS/<grupo>/<cadete>.",
|
|
271
|
+
"Las carpetas que no uses quedan vacías: no pasa nada. No renombres las carpetas.",
|
|
272
|
+
"Cuando termines de soltar las fotos, pedile a Claude: \"hacé el control por foto de esta carpeta\".",
|
|
273
|
+
].join("\r\n");
|
|
274
|
+
const creadas = [];
|
|
275
|
+
for (const f of fechas) {
|
|
276
|
+
const dia = path.join(raiz, nombreCarpetaDia(f));
|
|
277
|
+
const subs = [...grupos.map((g) => ["DADOS", g]), ...cadetes.map((c) => ["DADOS", c]), ...cadetes.map((c) => ["RECIBIDOS", c]), ...grupos.map((g) => ["RECIBIDOS", g])];
|
|
278
|
+
for (const s of subs) fs.mkdirSync(path.join(dia, ...s), { recursive: true });
|
|
279
|
+
const lm = path.join(dia, "LEEME.txt");
|
|
280
|
+
if (!fs.existsSync(lm)) fs.writeFileSync(lm, leeme);
|
|
281
|
+
creadas.push(dia);
|
|
282
|
+
}
|
|
283
|
+
return { ok: true, status: 200, data: { creadas, grupos, cadetes, siguiente: "Soltá las fotos en cada carpeta y después corré control_foto_carpeta sobre la carpeta del día (o sobre la de todos los días)." } };
|
|
284
|
+
}));
|
|
285
|
+
|
|
286
|
+
tool("control_foto_estructura", {
|
|
287
|
+
title: "Control por foto: cómo armar las carpetas (tus grupos y cadetes)",
|
|
288
|
+
description: "Devuelve cómo tienen que llamarse las carpetas del control por foto para TU nodo: tus grupos logísticos y quiénes reparten. Llamalo antes de `control_foto_carpeta` para explicarle al usuario la estructura o para entender por qué una carpeta no se reconoce. Solo lectura.",
|
|
289
|
+
inputSchema: { nodo: zNodo() },
|
|
290
|
+
}, async ({ nodo }) => run(() => api("GET", `/control-foto/estructura${q({ nodo })}`)));
|
|
291
|
+
|
|
292
|
+
tool("control_foto_foto", {
|
|
293
|
+
title: "Control por foto: procesar UNA foto de etiqueta",
|
|
294
|
+
description: "Procesa una sola foto (por su RUTA en la PC): lee el QR (o la IA de etiquetas), busca o da de alta el envío (sin duplicar), lo asigna según la CARPETA (DADOS/<grupo>, DADOS/<cadete>, RECIBIDOS/…/<cadete>, RECIBIDOS/<grupo>), lo lleva a Entregado en el `fecha` real y le cuelga la foto. Usalo para CONTESTAR una pregunta de `control_foto_carpeta`: repetí esa foto con `nodoEntrega` (quién la llevó) o `cliente` (de quién es la cuenta de ML). `simular:true` = solo mirar. No mueve dinero.",
|
|
295
|
+
inputSchema: {
|
|
296
|
+
ruta: zStr().describe("Ruta de la foto en la PC"),
|
|
297
|
+
carpeta: zStr().describe("Carpeta relativa al día, ej. 'DADOS/Bonorino' o 'RECIBIDOS/Matanza/Gerardo'"),
|
|
298
|
+
fecha: zStr().describe("Día real (dd/mm/aaaa o aaaa-mm-dd)"),
|
|
299
|
+
simular: z.boolean().optional(),
|
|
300
|
+
nodoEntrega: zStr().describe("Respuesta: nodo (nombre o id) que lo llevó, cuando la zona tiene varios responsables"),
|
|
301
|
+
cliente: zStr().describe("Respuesta: id del cliente dueño de una cuenta de ML desconocida"),
|
|
302
|
+
nodo: zNodo(),
|
|
303
|
+
},
|
|
304
|
+
}, async (a) => run(async () => {
|
|
305
|
+
const fs = await import("node:fs");
|
|
306
|
+
const path = await import("node:path");
|
|
307
|
+
if (!a.ruta || !fs.existsSync(a.ruta)) return { ok: false, status: 400, data: { message: `No encuentro la foto ${a.ruta ?? ""}.` } };
|
|
308
|
+
return api("POST", "/control-foto/foto", { imagen: fs.readFileSync(a.ruta).toString("base64"), carpeta: a.carpeta, fecha: a.fecha, simular: a.simular === true, nodoEntrega: a.nodoEntrega, cliente: a.cliente, nombreArchivo: path.basename(a.ruta), nodo: a.nodo });
|
|
309
|
+
}));
|
|
310
|
+
|
|
311
|
+
tool("control_foto_carpeta", {
|
|
312
|
+
title: "Control por foto: procesar la carpeta de un día (o de varios)",
|
|
313
|
+
description: "Recorre la carpeta de UN DÍA (o una carpeta con varios días adentro) en la PC: cada foto bajo DADOS/… o RECIBIDOS/… se manda al servidor con su carpeta y el día (sale del nombre de la carpeta: 'Lunes 14-9', '2026-09-14'…). Por defecto SIMULA (no escribe): devuelve qué haría, cuántos paquetes, y las PREGUNTAS (cuentas de ML que no conoce, zonas con varios responsables, posibles duplicados, carpetas que no reconoce). Mostrale el resumen al usuario, resolvé las preguntas y recién ahí corré con `aplicar:true`. Al aplicar, lo que siga dudoso se SUBE a la app (Logística → 📷 Control por foto) para resolverlo mirando la foto. Es reanudable: una foto ya procesada no se repite. Las fotos dudosas se copian a `_sin_identificar` dentro de cada día y deja el detalle en `_control-foto-<día>.json`. No mueve dinero.",
|
|
314
|
+
inputSchema: {
|
|
315
|
+
ruta: zStr().describe("Carpeta del día (ej. 'C:\\\\Users\\\\...\\\\Control por fotos\\\\Lunes 14-9') o la carpeta que contiene varios días"),
|
|
316
|
+
aplicar: z.boolean().optional().describe("true = escribe. Sin esto, solo simula."),
|
|
317
|
+
anio: zNum().describe("Año, si el nombre de la carpeta no lo trae (default: el actual)"),
|
|
318
|
+
nodo: zNodo(),
|
|
319
|
+
},
|
|
320
|
+
}, async (a) => run(async () => {
|
|
321
|
+
const fs = await import("node:fs");
|
|
322
|
+
const path = await import("node:path");
|
|
323
|
+
const raiz = String(a.ruta ?? "");
|
|
324
|
+
if (!raiz || !fs.existsSync(raiz)) return { ok: false, status: 400, data: { message: `No encuentro la carpeta ${raiz}.` } };
|
|
325
|
+
if (a.aplicar === true && !ALLOW_WRITE) return { ok: false, status: 403, data: { message: "Escritura deshabilitada en este MCP (NEXUSFLEX_MCP_ALLOW_WRITE)." } };
|
|
326
|
+
const esDia = (d) => fs.readdirSync(d, { withFileTypes: true }).some((e) => e.isDirectory() && /^(dados|recibidos)$/i.test(e.name));
|
|
327
|
+
const dias = esDia(raiz) ? [raiz] : fs.readdirSync(raiz, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("_") && esDia(path.join(raiz, e.name))).map((e) => path.join(raiz, e.name));
|
|
328
|
+
if (!dias.length) return { ok: false, status: 400, data: { message: "No encontré carpetas DADOS/RECIBIDOS (¿están comprimidas en .zip? descomprimilas primero). Ver `control_foto_estructura`." } };
|
|
329
|
+
const resumen = { modo: a.aplicar === true ? "APLICADO" : "SIMULACIÓN (no se escribió nada)", dias: [] };
|
|
330
|
+
for (const dir of dias) {
|
|
331
|
+
const fecha = diaDeCarpeta(path.basename(dir), Number(a.anio) || undefined);
|
|
332
|
+
if (!fecha) { resumen.dias.push({ dia: path.basename(dir), error: "El nombre de la carpeta no tiene la fecha (ej. 'Lunes 14-9')." }); continue; }
|
|
333
|
+
const { fotos, sueltas, zips } = await fotosDelDia(dir);
|
|
334
|
+
const res = [];
|
|
335
|
+
let i = 0;
|
|
336
|
+
const trabajador = async () => {
|
|
337
|
+
while (i < fotos.length) {
|
|
338
|
+
const f = fotos[i++];
|
|
339
|
+
const r = await api("POST", "/control-foto/foto", { imagen: fs.readFileSync(f.archivo).toString("base64"), carpeta: f.carpeta, fecha, simular: a.aplicar !== true, nombreArchivo: path.basename(f.archivo), nodo: a.nodo }).catch((e) => ({ ok: false, data: { message: String(e) } }));
|
|
340
|
+
res.push({ archivo: path.relative(dir, f.archivo), carpeta: f.carpeta, ...(r.ok ? r.data : { estado: "error", error: r.data?.message ?? `HTTP ${r.status}` }) });
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
await Promise.all([trabajador(), trabajador(), trabajador()]);
|
|
344
|
+
fs.writeFileSync(path.join(dir, `_control-foto-${fecha}.json`), JSON.stringify(res, null, 1));
|
|
345
|
+
// Lo dudoso a mano: copia en _sin_identificar (el original queda donde estaba).
|
|
346
|
+
const dudosas = res.filter((r) => r.estado === "pregunta" || r.estado === "error");
|
|
347
|
+
if (dudosas.length) {
|
|
348
|
+
const destino = path.join(dir, "_sin_identificar");
|
|
349
|
+
fs.mkdirSync(destino, { recursive: true });
|
|
350
|
+
for (const d of dudosas) {
|
|
351
|
+
const motivo = (d.pregunta?.tipo ?? "error") + (d.pregunta?.datos?.cuentaML ? ` ML ${d.pregunta.datos.cuentaML}` : "");
|
|
352
|
+
try { fs.copyFileSync(path.join(dir, d.archivo), path.join(destino, `${motivo} - ${d.carpeta.replace(/\//g, " ")} - ${path.basename(d.archivo)}`)); } catch { /* no rompe el lote */ }
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const cuenta = (k) => res.filter((r) => r.estado === k).length;
|
|
356
|
+
const porCuenta = {};
|
|
357
|
+
for (const r of res) if (r.pregunta?.tipo === "cuenta_ml") {
|
|
358
|
+
const c = r.pregunta.datos?.cuentaML;
|
|
359
|
+
(porCuenta[c] ??= { cuentaML: c, vendedor: r.pregunta.datos?.vendedor ?? null, marcas: new Set(), fotos: 0, ejemplo: r.archivo, carpeta: r.carpeta });
|
|
360
|
+
porCuenta[c].fotos++;
|
|
361
|
+
if (r.pregunta.datos?.marcaManual) porCuenta[c].marcas.add(r.pregunta.datos.marcaManual);
|
|
362
|
+
}
|
|
363
|
+
const otras = res.filter((r) => r.estado === "pregunta" && r.pregunta?.tipo !== "cuenta_ml").map((r) => ({ archivo: r.archivo, carpeta: r.carpeta, tipo: r.pregunta.tipo, texto: r.pregunta.texto, opciones: r.pregunta.opciones?.map((o) => o.nombre) }));
|
|
364
|
+
const errores = [...new Set(res.filter((r) => r.estado === "error").map((r) => `${r.carpeta}: ${r.error}`))];
|
|
365
|
+
resumen.dias.push({
|
|
366
|
+
dia: path.basename(dir), fecha, fotos: fotos.length,
|
|
367
|
+
ok: cuenta("ok"), sinCambios: cuenta("sin_cambios"), yaProcesadas: cuenta("ya_procesada"), altas: res.filter((r) => r.alta).length,
|
|
368
|
+
preguntas: cuenta("pregunta"), errores: cuenta("error"),
|
|
369
|
+
...(sueltas ? { fuera_de_DADOS_RECIBIDOS: sueltas } : {}), ...(zips.length ? { zipsSinDescomprimir: zips } : {}),
|
|
370
|
+
cuentasMLDesconocidas: Object.values(porCuenta).map((c) => ({ ...c, marcas: [...c.marcas] })),
|
|
371
|
+
otrasPreguntas: otras.slice(0, 40), ...(otras.length > 40 ? { masPreguntas: otras.length - 40 } : {}),
|
|
372
|
+
erroresDeCarpeta: errores,
|
|
373
|
+
...(a.aplicar === true && cuenta("pregunta") + cuenta("error") ? { resolverEnLaApp: `Las ${cuenta("pregunta") + cuenta("error")} foto(s) dudosas quedaron subidas en la app: menú Logística → 📷 Control por foto. Ahí se ven grandes y se elige el cliente, el nodo o el cadete.` } : {}),
|
|
374
|
+
detalle: path.join(dir, `_control-foto-${fecha}.json`),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return { ok: true, status: 200, data: resumen };
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
|
|
177
381
|
// ============================================================================
|
|
178
382
|
// CAPA 2 — Identidad: leemos /auth/me ANTES de registrar tools. Cada usuario ve
|
|
179
383
|
// SOLO los tools de su rol. Si falla el login, se registra solo `mis_datos`.
|
|
@@ -880,6 +1084,7 @@ if (ALLOW_WRITE) {
|
|
|
880
1084
|
fecha: zStr().describe("Solo staff — día REAL en que el paquete se movió (dd/mm/aaaa o aaaa-mm-dd) si NO es HOY: para planillas que se controlan días después. Define en qué SEMANA se liquida el envío (no se admite futura) y fecha TODA la cadena de `avanzarA`: cada estado queda con su hora de ESE día (Colectado 9, En centro 12, En camino 15, Entregado 18), no con la hora del control. Si el envío YA estaba cargado, también le mueve la fecha — y si ese período ya está liquidado, rechaza la llamada nombrando la liquidación en vez de dejarlo a medio corregir."),
|
|
881
1085
|
},
|
|
882
1086
|
}, async (args) => run(() => api("POST", "/envios/desde-etiqueta", args)));
|
|
1087
|
+
if (isStaff) registrarControlFoto();
|
|
883
1088
|
tool("envio_procesar", {
|
|
884
1089
|
title: "Procesar / recibir un envío en el centro",
|
|
885
1090
|
description: "Marca un envío como PROCESADO / recibido en el centro de distribución (estado 'En centro de distribución') por su TRACKING, sin escanear — igual que la acción 'procesar' del escáner. Scopeado a tu nodo/grupo. No mueve dinero (registra el tramo de clearing del grupo, como `procesar_zona`).",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: control-por-foto
|
|
3
|
+
description: Control por foto de Nexus Flex — preparar las carpetas del día y cargar y cerrar lo que un nodo dio, recibió y repartió en un día, a partir de carpetas de fotos de etiquetas (DADOS/RECIBIDOS por día). Usar cuando el usuario quiere "hacer el control por foto", "controlar las fotos del día", "marcar lo que salió con los grupos/cadetes" o suelta carpetas de fotos de etiquetas de Mercado Libre. Requiere el MCP de Nexus Flex local (npx nexusflex-mcp) con escritura habilitada.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Control por foto (Nexus Flex)
|
|
7
|
+
|
|
8
|
+
Sirve para cualquier nodo y cualquiera de sus grupos logísticos. El servidor hace el trabajo foto por foto (lee el QR o la etiqueta con IA, busca o da de alta el envío sin duplicar, lo asigna y lo cierra en el día real, y le cuelga la foto). Vos guiás al usuario, mostrás lo que va a pasar y **no adivinás nada**: lo dudoso se pregunta.
|
|
9
|
+
|
|
10
|
+
## 0. Empezar: crear las carpetas
|
|
11
|
+
|
|
12
|
+
Cuando el usuario dice "hagamos el control por foto del día tal", preguntale dónde quiere las carpetas (por ejemplo el Escritorio) y corré:
|
|
13
|
+
|
|
14
|
+
`control_foto_preparar ruta:"<dónde>" fechas:"<día o días>"`
|
|
15
|
+
|
|
16
|
+
Las fechas pueden ser un día (`14/09/2026`), un rango (`14/09 a 19/09`) o una lista (`14/09, 16/09`). Se crea una carpeta por día, con las subcarpetas de todos sus grupos y cadetes con el nombre exacto y un `LEEME.txt`. Decile dónde quedaron y que suelte cada foto en la carpeta que corresponde. Las que no use quedan vacías y no pasa nada.
|
|
17
|
+
|
|
18
|
+
## 1. Las carpetas
|
|
19
|
+
|
|
20
|
+
Una carpeta por día. El nombre tiene que traer la fecha (`Lunes 14-9-2026`, `14-09-2026`, `2026-09-14`). Adentro:
|
|
21
|
+
|
|
22
|
+
| Carpeta | Qué significa | Qué se hace |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `DADOS/<grupo>/` | Lo que el nodo pasó a ese grupo | Se asigna al nodo responsable de la zona del destino en ese grupo |
|
|
25
|
+
| `DADOS/<cadete>/` | Lo repartió un cadete del nodo | Cadete asignado, Entregado |
|
|
26
|
+
| `RECIBIDOS/…/<cadete>/` | Otro nodo se lo dio y lo repartió ese cadete | Origen = nodo del vendedor → entrega = este nodo, por el grupo que comparten |
|
|
27
|
+
| `RECIBIDOS/<grupo>/<cadete>/` | Igual, pero llegó por ese grupo | Cuando el vendedor comparte varios grupos con el nodo |
|
|
28
|
+
| `RECIBIDOS/<grupo>/` | Se lo dieron y lo derivó por ese grupo | Cadena: vendedor → este nodo → responsable de ese grupo |
|
|
29
|
+
|
|
30
|
+
- Los nombres de `<grupo>` y `<cadete>` son los del nodo. Llamá a `control_foto_estructura` para tenerlos y mostrárselos al usuario la primera vez.
|
|
31
|
+
- Carpetas intermedias que no son grupo ni cadete (ej. `Matanza`, `1ra vuelta`) se ignoran.
|
|
32
|
+
- Un grupo que solo sale ciertos días (ej. un "Sábado …") va como `DADOS/<ese grupo>`.
|
|
33
|
+
- Si las fotos vienen en `.zip`, descomprimilas antes. Las carpetas que empiezan con `_` se ignoran.
|
|
34
|
+
|
|
35
|
+
## 2. Simular primero (nunca escribir sin mostrar)
|
|
36
|
+
|
|
37
|
+
`control_foto_carpeta ruta:"<carpeta del día o de varios días>"`, sin `aplicar`.
|
|
38
|
+
|
|
39
|
+
Mostrá un resumen corto por día: fotos, ok, altas nuevas, preguntas y errores de carpeta. Si hay `zipsSinDescomprimir` o `fuera_de_DADOS_RECIBIDOS`, avisá.
|
|
40
|
+
|
|
41
|
+
## 3. Resolver las preguntas
|
|
42
|
+
|
|
43
|
+
- **cuentasMLDesconocidas**: preguntá de qué nodo y cliente es cada cuenta. Mostrá el vendedor, la marca a mano y la foto de ejemplo. Con el id del cliente: `control_foto_foto ruta:<foto> carpeta:<…> fecha:<…> cliente:<id>`. Si es un vendedor de otro nodo, puede vincular o aprender la cuenta ese nodo, y después se vuelve a correr la carpeta.
|
|
44
|
+
- **responsable** (la zona tiene varios responsables o ninguno): preguntá quién lo llevó y repetí la foto con `nodoEntrega`.
|
|
45
|
+
- **duplicado / no_ml / sin_codigo / fuera_de_red**: van a revisión manual. La foto quedó copiada en `<día>/_sin_identificar`.
|
|
46
|
+
- **erroresDeCarpeta**: una carpeta no coincide con un grupo o cadete del nodo. Que la renombren (con `control_foto_estructura` a la vista).
|
|
47
|
+
|
|
48
|
+
**Más cómodo:** al aplicar, lo dudoso se sube solo a la app, en **Logística → 📷 Control por foto**. Ahí cada foto se ve grande y se elige:
|
|
49
|
+
- el cliente (de cualquier nodo de la red);
|
|
50
|
+
- o el nodo que se lo dio;
|
|
51
|
+
- quién lo llevó, o la carpeta.
|
|
52
|
+
|
|
53
|
+
La opción "Recordar" asocia la cuenta de ML para la próxima vez. No la marques si el vendedor despacha con varias logísticas. En esa misma pantalla están los envíos **sin vendedor** del nodo (paquetes de otro nodo que escaneó un cadete), para asignarles el nodo o el cliente.
|
|
54
|
+
|
|
55
|
+
## 4. Aplicar
|
|
56
|
+
|
|
57
|
+
Con el OK del usuario: `control_foto_carpeta ruta:"<…>" aplicar:true`.
|
|
58
|
+
|
|
59
|
+
- Es reanudable: una foto ya procesada no se repite. Se puede correr de nuevo después de contestar preguntas.
|
|
60
|
+
- Cada paquete queda Entregado, con los estados fechados en el día real, su grupo, el nodo que entrega, el cadete y la foto en el historial (la que ven los nodos en su cuenta semanal).
|
|
61
|
+
- No toca envíos de fuera de la red del nodo, no crea duplicados y no reasigna lo que ya entró en un cierre semanal congelado. Lo nuevo de una semana ya cerrada entra en el cierre siguiente, marcado como tarde.
|
|
62
|
+
|
|
63
|
+
## 5. Cierre
|
|
64
|
+
|
|
65
|
+
Resumí por día lo que se cargó y lo que quedó en `_sin_identificar` con su motivo. El detalle completo queda en `<día>/_control-foto-<fecha>.json`.
|
|
66
|
+
|
|
67
|
+
## Reglas
|
|
68
|
+
|
|
69
|
+
- No mueve dinero: registra el movimiento y el traspaso entre nodos; los cierres y las liquidaciones los hace el sistema.
|
|
70
|
+
- No adivines un nodo, un cliente ni un responsable. Si no está claro, preguntá.
|
|
71
|
+
- Los Flex de cuentas vinculadas los cierra Mercado Libre: el control solo los asigna.
|