Si la linea de la factura viene de una Entrega (BaseType 15) los lotes se toman de la linea de la Entrega (BaseLine); si no, de la linea de la factura (LineNum). Se valida que los lotes cubran la cantidad y se devuelve un error claro por linea en vez del error generico de SAP. 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.
Lotes de la nota de crédito en DESPACHO (versión 2.1.4.0)
Se resuelve línea por línea según el documento origen de la línea de la factura:
| La línea de la factura tiene... | Los lotes salen de... |
|---|---|
Entrega como origen (BaseType = 15) |
La línea de la Entrega (DeliveryNotes(BaseEntry), línea BaseLine). |
| Otro origen o ninguno | La propia línea de la factura (LineNum). |
Si hay lotes de origen y no cubren la cantidad a devolver, o no se puede leer la Entrega, la API responde un error con la línea y el artículo en vez de enviar lotes incompletos a SAP. Si no hay lotes de origen (artículo sin control por lote) la línea se envía sin lotes. PICKING (estado 13) no cambia: genera un lote nuevo por fecha y hora.
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").