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
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
[[...]]. ElresponsePath: '[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,
detallesse reemplaza con los detalles aplanados de todos los remitos. Si no hay remitos, se mantiene eldetallesoriginal 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 arraydetalles. 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
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:
- Evalúa la
condition(si existe) con los datos completos del row - Si pasa, itera sobre cada item del array indicado
- Para cada item, mergea los datos del padre con los del item (item tiene prioridad)
- 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
- Verificar que aparece en GET del WMS:
- Verificar logs del poller:
- Verificar hash en Redis:
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
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: