Saltar a contenido

Flujo: Pedidos Preparados

Detalle completo del flujo de sincronización de pedidos preparados desde WMS hacia Tango.

Resumen

Propiedad Valor
Nombre pedidosPreparados
Dirección WMS → Tango
Source wms-source (HTTP)
Destination tango (MSSQL)
Intervalo 5 minutos
Prioridad high
Transform procesarPedidoPreparado

Diagrama de Secuencia

sequenceDiagram
    actor User as Usuario WMS
    participant WMS as WMS API
    participant Poller
    participant HashStore as Redis
    participant Queue as BullMQ
    participant Worker
    participant Tango as Tango DB

    Note over User,WMS: Usuario prepara pedido en WMS
    User->>WMS: Marcar pedido como preparado
    WMS->>WMS: estado = "PREPARACION COMPLETA"

    Note over Poller: Cada 5 minutos
    Poller->>WMS: GET /pedidos/preparados?sin_sincronizar=1
    WMS-->>Poller: [{id, idPedidoERP, detalles[], remitos[{detalles[]}], ...}]

    Poller->>Poller: Transform: fechaSQL, hora, aplanar detalles de remitos
    Poller->>HashStore: getHash("pedidosPreparados", pk)
    HashStore-->>Poller: null (nuevo)
    Poller->>Queue: addJob(upsert, data)
    Poller->>HashStore: setHash("pedidosPreparados", pk, hash)

    Worker->>Queue: getJob()
    Queue-->>Worker: job

    Worker->>Tango: BEGIN TRANSACTION
    Worker->>Tango: Q1: UPDATE gva21 (aprobar pedido)
    Worker->>Tango: Q2: INSERT gva77 (historial)
    Worker->>Tango: Q3: forEach detalle → UPDATE gva03 (cantidades por artículo)
    Worker->>Tango: COMMIT

    Worker->>WMS: POST /pedidos/preparados/sincronizar {pedidos: [id]}
    WMS-->>Worker: 200 OK
    WMS->>WMS: Marcar como sincronizado

    Note over WMS: Próximo GET no incluye este pedido

Datos del WMS

Request

GET /pedidos/preparados?sin_sincronizar=1
X-API-KEY: mi-api-key

Response

[[
  {
    "id": 78,
    "idERP": "0000000057982",
    "numero": "0000000057982",
    "codigoCliente": 0,
    "cuitCliente": "33-70858131-9",
    "nombreCliente": "N.FONTANA S.R.L.",
    "codigoTipoPedido": "2",
    "estado": "DESPACHO FINALIZADO",
    "fecha": "04/02/2026 00:00:00",
    "fechaActualizacion": "17/03/2026 07:23:22",
    "fechaNovedad": "2026-03-17 07:23:22",
    "codigoDepositoOrigen": "2",
    "direccionEntrega": "RUTA 8 KM. 174",
    "cantidadBultos": 0,
    "sincronizado": 1,
    "detalles": [
      {
        "idUnicoDetalleERP": "0000000057982-1",
        "numeroRenglon": 1,
        "codigoArticulo": "RONA4903INA",
        "cantidad": 2,
        "descripcionArticulo": "ROD INA NA 4903"
      }
    ],
    "remitos": [
      {
        "id": 82,
        "fecha": "04/02/2026 12:07:31",
        "estado": "DESPACHO FINALIZADO",
        "sincronizado": 0,
        "detalles": [
          {
            "idUnicoDetalleERP": "0000000057982-1",
            "numeroRenglon": 1,
            "codigoArticulo": "RONA4903INA",
            "cantidadDespachada": 2,
            "cantidadYaRemitida": 0,
            "descripcionArticulo": "ROD INA NA 4903"
          }
        ]
      }
    ]
  }
]]

Response Path: La respuesta viene envuelta en doble array [[...]]. El responsePath: '[0]' extrae el array interno.

Estructura de datos clave

Los detalles del pedido (detalles[]) contienen la información original del pedido (cantidades pedidas). Los detalles de los remitos (remitos[].detalles[]) contienen las cantidades reales despachadas/remitidas. El transform aplana los detalles de todos los remitos y los usa como detalles del row para las queries de orden 3.

Campo (remito detalle) Descripción
cantidadDespachada Cantidad que se despachó físicamente
cantidadYaRemitida Cantidad ya registrada en remito previo
codigoArticulo Código del artículo
numeroRenglon Número de renglón en el pedido

Transform

Código

export function procesarPedidoPreparado(rows: RowData[]): RowData[] {
  return rows.map((row) => {
    const [fechaStr, hora] =
      typeof row['fechaActualizacion'] === 'string' ? row['fechaActualizacion'].split(' ') : ['', ''];

    // Aplanar detalles de todos los remitos
    const remitos = row.remitos as Array<Record<string, unknown>> | undefined;
    const detallesRemitos = Array.isArray(remitos)
      ? remitos.flatMap((remito) => {
          const dets = remito.detalles as Array<Record<string, unknown>> | undefined;
          return Array.isArray(dets) ? dets : [];
        })
      : row.detalles;

    return {
      ...row,
      _pk: String(row.idPedidoERP || row.idERP),
      idPedidoERP: ' ' + (row.idPedidoERP || row.idERP), // espacio porque así lo necesita SQL Server
      idERP: row.idPedidoERP || row.idERP,
      fechaSQL: fechaStr.split('/').reverse().join('-'),
      hora: hora.replace(/:/g, '').slice(0, 4), // HHMM
      detalles: detallesRemitos,
    };
  });
}

Entrada → Salida

Entrada (del WMS):

{
  "id": 78,
  "idERP": "0000000057982",
  "fechaActualizacion": "17/03/2026 07:23:22",
  "detalles": [{ "codigoArticulo": "RONA4903INA", "cantidad": 2 }],
  "remitos": [{
    "detalles": [{
      "codigoArticulo": "RONA4903INA",
      "cantidadDespachada": 2,
      "cantidadYaRemitida": 0,
      "numeroRenglon": 1
    }]
  }]
}

Salida (post-transform):

{
  "id": 78,
  "_pk": "0000000057982",
  "idPedidoERP": " 0000000057982",
  "idERP": "0000000057982",
  "fechaSQL": "2026-03-17",
  "hora": "0723",
  "detalles": [{
    "codigoArticulo": "RONA4903INA",
    "cantidadDespachada": 2,
    "cantidadYaRemitida": 0,
    "numeroRenglon": 1
  }]
}

Nota: Si hay remitos, detalles se reemplaza con los detalles aplanados de todos los remitos. Si no hay remitos, se mantiene el detalles original como fallback.

Queries en Tango

Todas las queries se ejecutan dentro de una única transacción. Si alguna falla, se hace rollback completo.

Query 1: Aprobar Pedido (gva21)

UPDATE gva21 SET
  aprueba = 'WMS',
  estado = 2,
  fecha_apru = ('2026-03-17'),
  hora_aprue = 0723,
  reviso_stk = 'A'
WHERE nro_pedido = ' 0000000057982'
Campo Valor Descripción
aprueba 'WMS' Quién aprobó
estado 2 Estado aprobado
fecha_apru fechaSQL Fecha de aprobación (YYYY-MM-DD)
hora_aprue hora Hora de aprobación (HHMM)
reviso_stk 'A' Stock revisado

Query 2: Insertar Historial (gva77)

IF NOT EXISTS (
  SELECT 1 FROM gva77
  WHERE nro_pedido = ' 0000000057982'
  AND estado_ped = 2
  AND fecha = ('2026-03-17')
  AND hora = 0723
  AND proceso = 'APROBACION'
)
BEGIN
  INSERT INTO gva77 (
    filler, estado_ped, fecha, hora,
    nro_pedido, proceso, reviso_fac, reviso_pre,
    reviso_stk, usuario, talon_ped, id_gva21
  )
  SELECT
    filler, 2, ('2026-03-17'), 0723,
    nro_pedido, 'APROBACION', reviso_fac, reviso_pre,
    'A', 'WMS', talon_ped, id_gva21
  FROM GVA77
  WHERE NRO_PEDIDO = ' 0000000057982' AND ESTADO_PED = 1
END

Query 3: Actualizar Cantidades por Artículo (gva03) — forEach: 'detalles'

Esta query se ejecuta una vez por cada detalle del pedido (aplanado de los remitos). Usa el mecanismo forEach del adaptador MSSQL.

Condición: Solo se ejecuta si detalles es un array no vacío y al menos un detalle tiene el campo cantidadYaRemitida.

{
  forEach: 'detalles',
  condition: (data) => {
    const detalles = data.detalles as Array<Record<string, unknown>> | undefined;
    return Array.isArray(detalles) && detalles.length > 0 && detalles.some((d) => d.cantidadYaRemitida !== undefined);
  },
  sql: `...`,
}

SQL por cada detalle:

UPDATE gva03 SET
  cant_a_des = 0,
  cant_pen_d = 0,
  pen_fac_re = pen_fac_re + 0
WHERE NRO_PEDIDO = ' 0000000057982'
  AND cod_articu = 'RONA4903INA'
  AND n_renglon = 1
Campo Fuente Descripción
cant_a_des {{cantidadYaRemitida}} Cantidad ya remitida
cant_pen_d {{cantidadYaRemitida}} Cantidad pendiente despacho
pen_fac_re pen_fac_re + {{cantidadYaRemitida}} Acumulado pendiente facturación
NRO_PEDIDO {{idPedidoERP}} Número de pedido (del padre, con espacio)
cod_articu {{codigoArticulo}} Código del artículo (del detalle)
n_renglon {{numeroRenglon}} Renglón del detalle

Mecanismo forEach: El adaptador mergea los datos del padre (idPedidoERP, fechaSQL, etc.) con cada item del array detalles. En caso de conflicto de claves, el item del detalle tiene prioridad.

Webhook de Confirmación

Request

POST /pedidos/preparados/sincronizar
Content-Type: application/json
X-API-KEY: mi-api-key

{
  "pedidos": [78]
}

Response

HTTP/1.1 200 OK

Configuración Completa

Source

// src/config/sources/wms-source.ts
{
  name: 'wms-source',
  type: 'http-source',
  config: {
    baseUrl: process.env.WMS_API_URL,
    timeout: 30000,
    headers: {
      'X-API-KEY': process.env.WMS_API_KEY,
    },
  },
  entities: {
    pedidosPreparados: {
      endpoint: 'pedidos/preparados',
      dateField: 'fechaNovedad',
      responsePath: '[0]',
      primaryKey: ['idERP'],
      queryParams: {
        sin_sincronizar: 1,
      },
    },
  },
}

Destination

// src/config/destinations/tango.ts
{
  name: 'tango',
  type: 'mssql',
  config: {
    server: process.env.MSSQL_SERVER_SAVE,
    port: parseInt(process.env.MSSQL_PORT_SAVE),
    database: process.env.MSSQL_DATABASE_SAVE,
    user: process.env.MSSQL_USER_SAVE,
    password: process.env.MSSQL_PASSWORD_SAVE,
  },
  entities: {
    pedidosPreparados: {
      queries: [
        // ORDEN 1 - Aprobar pedido
        {
          sql: `UPDATE gva21 SET aprueba='WMS', estado=2, fecha_apru=({{fechaSQL}}), hora_aprue={{hora}}, reviso_stk='A'
                WHERE nro_pedido={{idPedidoERP}}`,
        },
        // ORDEN 2 - Historial
        {
          sql: `IF NOT EXISTS (
                  SELECT 1 FROM gva77
                  WHERE nro_pedido = {{idPedidoERP}} AND estado_ped = 2
                  AND fecha = ({{fechaSQL}}) AND hora = {{hora}} AND proceso = 'APROBACION'
                )
                BEGIN
                  INSERT INTO gva77 (filler, estado_ped, fecha, hora, nro_pedido, proceso, reviso_fac, reviso_pre, reviso_stk, usuario, talon_ped, id_gva21)
                  SELECT filler, 2, {{fechaSQL}}, {{hora}}, nro_pedido, 'APROBACION', reviso_fac, reviso_pre, 'A', 'WMS', talon_ped, id_gva21
                  FROM GVA77 WHERE (NRO_PEDIDO = {{idPedidoERP}} AND ESTADO_PED=1)
                END`,
        },
        // ORDEN 3 - Cantidades por artículo (forEach detalles)
        {
          forEach: 'detalles',
          condition: (data) => {
            const detalles = data.detalles as Array<Record<string, unknown>> | undefined;
            return Array.isArray(detalles) && detalles.length > 0 && detalles.some((d) => d.cantidadYaRemitida !== undefined);
          },
          sql: `UPDATE gva03 SET
                  cant_a_des = {{cantidadYaRemitida}},
                  cant_pen_d = {{cantidadYaRemitida}},
                  pen_fac_re = pen_fac_re + {{cantidadYaRemitida}}
                WHERE NRO_PEDIDO = {{idPedidoERP}}
                  AND cod_articu = {{codigoArticulo}}
                  AND n_renglon = {{numeroRenglon}}`,
        },
        // ORDEN 4 - Recalcular totales
        {
          sql: `UPDATE gva21
                SET gva21.TOTAL_PEDI_EXTRANJERA = x.Total,
                    gva21.TOTAL_PEDI = ROUND(x.Total * gva21.COTIZ, 2),
                    gva21.TOTAL_PEDI_EXTRANJERA_CON_IMPUESTOS = ROUND(x.Total * 1.21, 2),
                    gva21.TOTAL_PEDI_CON_IMPUESTOS = ROUND((x.Total * gva21.COTIZ) * 1.21, 2)
                FROM GVA21
                INNER JOIN (SELECT ID_GVA21, SUM(IMPORTE_SIN_IMPUESTOS) as Total
                            FROM GVA03 GROUP BY ID_GVA21) x
                ON gva21.ID_GVA21 = x.ID_GVA21
                WHERE GVA21.NRO_PEDIDO = {{idPedidoERP}}`,
        },
      ],
      notifications: {
        url: `${process.env.WMS_API_URL}/pedidos/preparados/sincronizar`,
        headers: { 'X-API-KEY': process.env.WMS_API_KEY || '' },
        bodyBuilder: (data) => ({ pedidos: [data.id] }),
      },
    },
  },
}

Flow

// src/config/sync-flows.ts
{
  name: 'pedidosPreparados',
  source: { adapter: 'wms-source' },
  destinations: ['tango'],
  pollInterval: 60 * 1000,
  priority: 'high',
  useLastSync: true,
  transform: 'procesarPedidoPreparado',
}

Mecanismo forEach en el Adaptador MSSQL

El adaptador soporta queries que iteran sobre arrays anidados en los datos. Cuando una query tiene forEach: 'detalles', el adaptador:

  1. Evalúa la condition (si existe) con los datos completos del row
  2. Si pasa, itera sobre cada item del array indicado
  3. Para cada item, mergea los datos del padre con los del item (item tiene prioridad)
  4. Ejecuta la query interpolada con los datos mergeados
// En MssqlDestAdapter.send()
if (queryConfig.forEach) {
  const items = processedData[queryConfig.forEach];
  if (Array.isArray(items)) {
    for (const item of items) {
      const merged = { ...processedData, ...item };
      const query = this.interpolateQuery(queryConfig.sql, merged);
      await transaction.request().query(query);
    }
  }
} else {
  const query = this.interpolateQuery(queryConfig.sql, processedData);
  await transaction.request().query(query);
}

Troubleshooting

Pedido no se sincroniza

  1. Verificar que aparece en GET del WMS:
    curl "http://wms/pedidos/preparados?sin_sincronizar=1"
    
  2. Verificar logs del poller:
    docker logs rodamar_sync | grep pedidosPreparados
    
  3. Verificar hash en Redis:
    redis-cli GET "sync:hash:pedidosPreparados:0000000057982"
    

Error en query de Tango

Revisar logs detallados:

{
  "level": "error",
  "entity": "pedidosPreparados",
  "pk": "0000000057982",
  "error": "Cannot insert duplicate key",
  "msg": "MSSQL transaction failed"
}

Query 3 no se ejecuta (condition false)

Si la query de gva03 no se ejecuta, verificar que los detalles de los remitos tengan cantidadYaRemitida:

# Verificar en logs del transform
docker logs rodamar_sync | grep "Transform applied" | grep pedidosPreparados

Causas comunes: - El pedido no tiene remitos → detalles queda con los detalles originales que no tienen cantidadYaRemitida - Los remitos existen pero sus detalles están vacíos

Webhook falla

El webhook no bloquea el éxito, pero loggea warning:

{
  "level": "warn",
  "url": "http://wms/pedidos/preparados/sincronizar",
  "status": 500,
  "msg": "Webhook notify failed"
}

Forzar resync de un pedido

# Borrar hash y forzar sync
curl -X POST http://localhost:8080/sync/pedidosPreparados/0000000057982

Verificación

En Tango

-- Ver pedido actualizado (Q1)
SELECT nro_pedido, estado, aprueba, fecha_apru, hora_aprue
FROM gva21
WHERE nro_pedido = ' 0000000057982';

-- Ver historial (Q2)
SELECT *
FROM gva77
WHERE nro_pedido = ' 0000000057982'
ORDER BY fecha DESC, hora DESC;

-- Ver cantidades por artículo (Q3)
SELECT nro_pedido, cod_articu, n_renglon, cant_a_des, cant_pen_d, pen_fac_re
FROM gva03
WHERE nro_pedido = ' 0000000057982';

En WMS

El pedido no debería aparecer en próximas consultas:

curl "http://wms/pedidos/preparados?sin_sincronizar=1" | jq '.[] | select(.idERP == "0000000057982")'
# Debería devolver vacío