DESPACHO y PICKING vuelven a generar nota de crédito sin depender de Notificacion. Documentada la regla Proceso + Notificacion. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
11 KiB
Cambio de negocio a portar — Picking diurno/nocturno
Requerimiento de negocio sin equivalente en este .NET: las
notificaciones de Picking durante el día ya no deben crear documentos
transaccionales en SAP inmediatamente — eso causaba que el WMS reportara
éxito sin que SAP reflejara nada. Especificación traducida 1:1 de la
implementación ya validada en Python
(itca_api_sap_wms_python/app/services/picking_service.py,
app/schemas/picking.py, app/api/routes/picking.py).
Ver también PORTE_FUNCIONALIDAD_NUEVA.md
para el detalle de un bug de SQL injection real (HelperProcesoPiciking.Set)
que hay que corregir como parte de este mismo trabajo, porque es
exactamente el código que este cambio va a aislar y reusar.
Flujo de negocio
La hoja de ruta contiene varias facturas. El repartidor va notificando el
estado de cada factura a medida que llega a cada punto de entrega
(preliminar, sin crear nada en SAP). Al volver a la oficina, el tercero
recorre esas mismas facturas con un FOR de su lado llamando otra vez al
mismo endpoint pero en modo cierre (ahí sí se crean los documentos SAP),
y al final cierra la hoja de ruta completa una sola vez.
Regla Proceso + Notificacion (versión 2.1.2.0)
Notificacion solo se valida cuando Proceso = "ULTIMA MILLA". En los
demás procesos el campo se ignora.
Proceso |
Notificacion |
Resultado |
|---|---|---|
ULTIMA MILLA |
"cierre" |
Lógica completa: crea documentos en SAP (solicitud de devolución, estado 23). |
ULTIMA MILLA |
ausente u otro valor | Solo actualiza estado en HANA; no crea nada en SAP. |
DESPACHO |
cualquiera / ausente | Lógica completa siempre: NC y transferencias automáticas (estados 5 y 6). |
PICKING |
cualquiera / ausente | Lógica completa siempre: NC y transferencias automáticas (estado 13). |
Historial: en 2.1.1.0 el filtro se aplicaba a todos los procesos, por lo que
DESPACHO y PICKING no generaban la NC si no llegaba "cierre". Corregido en
2.1.2.0.
Resumen del contrato
| Método | Ruta | Cambia hoy | Qué hace |
|---|---|---|---|
POST |
/api/picking |
Sí — agrega campo Notificacion |
Solo con Proceso = "ULTIMA MILLA": "cierre" → lógica completa (crea documento SAP); cualquier otro valor/ausente → solo actualiza estado en HANA, nunca toca SAP. Con DESPACHO, PICKING y demás procesos el campo se ignora y la lógica corre siempre (NC y transferencias). |
POST |
/api/picking/ruta |
No | Sin cambios — CreateHojaRuta ya existe y sigue igual. |
POST |
/api/picking/hojaruta/{docEntry}/cerrar |
Nuevo | Cierra el UDO ARD_PHR completo en SAP. Sin body. |
1. Campo Notificacion en POST /api/Picking
DTO/Entidad — agregar una propiedad
Archivo: Net.Business.Entities/WMS/Picking/BE_PickingRequest.cs — agregar:
public string Notificacion { get; set; } // solo se evalua con Proceso "ULTIMA MILLA": "cierre" | otro/ausente = preliminar
Archivo: Net.Business.DTO/WMS/Picking/Dto_PickingRequest.cs — agregar la
misma propiedad y copiarla en RetornaModelo():
public string Notificacion { get; set; }
public BE_PickingRequest RetornaModelo()
{
return new BE_PickingRequest()
{
// ... resto de los campos existentes, sin tocar ...
Notificacion = Notificacion,
};
}
Dónde cortar la lógica — PickingRepository.Create
Archivo: Net.Data/WMS/Picking/PickingRepository.cs, método Create.
La buena noticia: el corte es un único early-return, justo después de la
línea que ya actualiza el estado en HANA — no hace falta duplicar ninguna de
las validaciones de arriba (a diferencia de la versión Python, que sí
duplicó el bloque de validación en una función separada porque Python
separa actualizar_estado()/create() en dos funciones independientes; acá
alcanza con un if adentro del mismo método).
// ... validaciones existentes sin cambios (lineas 51-93: DocEntry, Proceso,
// IdEstadoCubico, HelperProcesoPermitido.Get, desambiguacion Existe > 1) ...
//=== REALIZA PROCESO COMPLETO DE ACTUALIZACION
HelperProcesoPiciking.Set(entidad.Proceso, entidad.IdEstadoCubico, entidad.DocEntry, entidad.CodMotivo, Proceso.Code);
// <-- linea 96 actual, sin tocar
// === Solo ULTIMA MILLA: notificacion preliminar (diurna) - nunca toca SAP.
// DESPACHO / PICKING no dependen de Notificacion.
if (string.Equals(entidad.Proceso, "ULTIMA MILLA", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(entidad.Notificacion, "cierre", StringComparison.OrdinalIgnoreCase))
{
return resultadoTransaccion.GetInformacionResultado(entidad.DocEntry, entidad.NumOrden, "Estado actualizado correctamente", entidad);
}
#region PROCESO DE NOTA DE CREDITO
// ... todo el resto del metodo (lineas 98-333 actuales) sin ninguna modificacion ...
#endregion
Es decir: todo lo que hoy hace Create() (líneas 45-341 completas) sigue
siendo exactamente la lógica de cierre — no se toca nada de ahí. Solo se
agrega el if de corte después de la línea 96, y únicamente para
ULTIMA MILLA. Para DESPACHO y PICKING la NC se genera sin importar
Notificacion.
Nota de contrato:
detalleya es obligatorio hoy en este endpoint (la validación de arriba lo necesita para desambiguarExiste > 1), así que no hace falta agregar ninguna validación nueva ahí — la versión Python solo lo documentó explícito porque separó la función, acá ya viene gratis al reusar el mismo método.
Bodies de ejemplo
Notificación preliminar (solo ULTIMA MILLA, reparto en curso —
Notificacion ausente o distinto de "cierre"):
{
"numOrden": "OT-2026-001",
"docEntry": 500,
"proceso": "ULTIMA MILLA",
"codMotivo": "M01",
"idEstadoCubico": 23,
"codigoAlmacen": "ALM01",
"detalle": [
{ "item": 0, "codigoArticulo": "ART-001", "cantidadPedida": 3, "estado": "C" }
]
}
Cierre (ULTIMA MILLA, mismo shape, agrega notificacion: "cierre" —
crea el documento SAP real, misma respuesta que hoy):
{
"numOrden": "OT-2026-001",
"docEntry": 500,
"proceso": "ULTIMA MILLA",
"codMotivo": "M01",
"idEstadoCubico": 23,
"codigoAlmacen": "ALM01",
"notificacion": "cierre",
"detalle": [
{ "item": 0, "codigoArticulo": "ART-001", "cantidadPedida": 3, "estado": "C" }
]
}
Respuesta — notificación preliminar (éxito)
{
"docEntry": 500,
"docNum": "OT-2026-001",
"statusCode": 200,
"message": "Estado actualizado correctamente",
"error": null,
"appName": "PickingRepository",
"methodName": "Create",
"listData": null,
"data": { "...": "eco del PickingRequest enviado" },
"nombreEstacion": "SERVER-WMS"
}
(La respuesta de cierre no cambia respecto a la que ya devuelve Create()
hoy — ver el controller existente.)
2. POST /api/Picking/HojaRuta/{docEntry}/Cerrar (nuevo)
Cierra la Hoja de Ruta completa en SAP — se llama una sola vez, después
de que el tercero terminó el FOR de cierre por cada factura.
- Hoja de Ruta es un UDO en SAP:
ARD_PHR. - SAP Service Layer:
POST ARD_PHR({docEntry})/Close— mismo patrón queSolicitudTransferencia/{docEntry}/cerrar(verSOLICITUD_TRANSFERENCIA.mdpara el gap de infraestructura compartido: hace falta un método paraPOST .../Closecon body vacío, no existe ninguno hoy enConnectionServiceLayer). - Body del request: ninguno (solo
docEntryen la ruta). - Body enviado a SAP:
{}. - Interfaz: agregar a
IPickingRepository.cs:Task<BE_ResultadoTransaccion<object>> CerrarHojaRuta(int docEntry);
public async Task<BE_ResultadoTransaccion<object>> CerrarHojaRuta(int docEntry)
{
_metodoName = "CerrarHojaRuta";
ResultadoTransaccion<object> resultadoTransaccion = new ResultadoTransaccion<object>(_metodoName, _aplicacionName);
try
{
var connectServiceLayer = new ConnectionServiceLayer(_configuration, _clientFactory);
await connectServiceLayer.PostAsync<object>($"ARD_PHR({docEntry})/Close", new { });
return null;
}
catch (Exception ex)
{
return resultadoTransaccion.GetInformacionResultado(ex.Message.ToString(), null);
}
}
Controller (PickingController.cs, agregar junto a Ruta):
[HttpPost]
[Route("HojaRuta/{docEntry}/Cerrar")]
public async Task<IActionResult> CerrarHojaRuta(int docEntry)
{
var error = await _repository.Picking.CerrarHojaRuta(docEntry);
var result = error ?? new BE_ResultadoTransaccion<object>
{
DocEntry = docEntry,
StatusCode = 200,
Message = "Cerrado Correctamente",
AppName = "PickingRepository",
MethodName = "CerrarHojaRuta",
};
B_Log.Add(/* mismo patron que el resto - proceso "Hoja Ruta" */);
return error == null ? Ok(result) : BadRequest(result);
}
Por qué
200con body y no204: mismo motivo queSolicitudTransferencia— un204no puede llevar body (RFC 7231 §6.3.5) y muchos clientes HTTP, incluidoHttpClientde .NET, no lo leen.201se descarta porque no se crea nada nuevo.
Respuesta éxito (200)
{
"docEntry": 77,
"docNum": null,
"statusCode": 200,
"message": "Cerrado Correctamente",
"error": null,
"appName": "PickingRepository",
"methodName": "CerrarHojaRuta",
"listData": null,
"data": null,
"nombreEstacion": "SERVER-WMS"
}
Respuesta error (400)
{
"docEntry": null,
"docNum": null,
"statusCode": 500,
"message": null,
"error": "La hoja de ruta ya esta cerrada",
"appName": "PickingRepository",
"methodName": "CerrarHojaRuta",
"listData": null,
"data": null,
"nombreEstacion": "SERVER-WMS"
}
Checklist de integración para el WMS
- Nunca mandar un array a
POST /api/Picking. Un documento (una factura) por llamada — elFORpor factura es responsabilidad del WMS. notificacionausente o distinto de"cierre"= preliminar (default seguro, nunca crea nada en SAP).- El orden importa: primero el
FORde cierre por cada factura (notificacion: "cierre"), y recién al final —una sola vez— elPOST /api/Picking/HojaRuta/{docEntry}/Cerrar. docEntryenPOST /api/Pickinges el de la Factura, no el de la Hoja de Ruta.
Pendiente de validar en QA
- Confirmar en QA si el nuevo
Closedel UDOARD_PHRefectivamente bloquea modificaciones futuras sobre elDocEntryuna vez cerrado (no confirmado todavía ni en la versión Python). - Correr los mismos casos contra el endpoint
.NETlevantado antes de dar por válido este port — en Python tampoco se probó esto todavía end-to-end contra Service Layer real (verCLAUDE.mddel proyecto Python, sección "Pruebas contra HANA/SAP reales").