2026-10-05 17:53:41 -05:00
# 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` ](./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** .
2026-10-14 08:47:44 -05:00
## 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.
2026-10-14 10:29:02 -05:00
## 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.
2026-10-05 17:53:41 -05:00
## Resumen del contrato
| Método | Ruta | Cambia hoy | Qué hace |
|---|---|---|---|
2026-10-14 08:47:44 -05:00
| `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). |
2026-10-05 17:53:41 -05:00
| `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:
```csharp
2026-10-14 08:47:44 -05:00
public string Notificacion { get ; set ; } // solo se evalua con Proceso "ULTIMA MILLA": "cierre" | otro/ausente = preliminar
2026-10-05 17:53:41 -05:00
```
**Archivo** : `Net.Business.DTO/WMS/Picking/Dto_PickingRequest.cs` — agregar la
misma propiedad **y** copiarla en `RetornaModelo()` :
```csharp
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).
```csharp
// ... 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
2026-10-14 08:47:44 -05:00
// === 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 ))
2026-10-05 17:53:41 -05:00
{
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
2026-10-14 08:47:44 -05:00
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` .
2026-10-05 17:53:41 -05:00
> **Nota de contrato**: `detalle` ya es obligatorio hoy en este endpoint (la
> validación de arriba lo necesita para desambiguar `Existe > 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
2026-10-14 08:47:44 -05:00
**Notificación preliminar** (solo `ULTIMA MILLA` , reparto en curso —
`Notificacion` ausente o distinto de `"cierre"` ):
2026-10-05 17:53:41 -05:00
```json
{
"numOrden" : "OT-2026-001" ,
"docEntry" : 500 ,
2026-10-14 08:47:44 -05:00
"proceso" : "ULTIMA MILLA" ,
2026-10-05 17:53:41 -05:00
"codMotivo" : "M01" ,
2026-10-14 08:47:44 -05:00
"idEstadoCubico" : 23 ,
2026-10-05 17:53:41 -05:00
"codigoAlmacen" : "ALM01" ,
"detalle" : [
{ "item" : 0 , "codigoArticulo" : "ART-001" , "cantidadPedida" : 3 , "estado" : "C" }
]
}
```
2026-10-14 08:47:44 -05:00
**Cierre** (`ULTIMA MILLA` , mismo shape, agrega `notificacion: "cierre"` —
crea el documento SAP real, misma respuesta que hoy):
2026-10-05 17:53:41 -05:00
```json
{
"numOrden" : "OT-2026-001" ,
"docEntry" : 500 ,
2026-10-14 08:47:44 -05:00
"proceso" : "ULTIMA MILLA" ,
2026-10-05 17:53:41 -05:00
"codMotivo" : "M01" ,
2026-10-14 08:47:44 -05:00
"idEstadoCubico" : 23 ,
2026-10-05 17:53:41 -05:00
"codigoAlmacen" : "ALM01" ,
"notificacion" : "cierre" ,
"detalle" : [
{ "item" : 0 , "codigoArticulo" : "ART-001" , "cantidadPedida" : 3 , "estado" : "C" }
]
}
```
### Respuesta — notificación preliminar (éxito)
```json
{
"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 que
`SolicitudTransferencia/{docEntry}/cerrar` (ver
[`SOLICITUD_TRANSFERENCIA.md` ](./SOLICITUD_TRANSFERENCIA.md ) para el gap de
infraestructura compartido: hace falta un método para `POST .../Close` con
body vacío, no existe ninguno hoy en `ConnectionServiceLayer` ).
- **Body del request**: ninguno (solo `docEntry` en la ruta).
- **Body enviado a SAP**: `{}` .
- **Interfaz**: agregar a `IPickingRepository.cs` :
```csharp
Task<BE_ResultadoTransaccion<object>> CerrarHojaRuta(int docEntry);
` ``
` ``csharp
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`):
` ``csharp
[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é ` 200` con body y no ` 204`**: mismo motivo que
> ` SolicitudTransferencia` — un ` 204` no puede llevar body (RFC 7231 §6.3.5)
> y muchos clientes HTTP, incluido ` HttpClient` de .NET, no lo leen. ` 201` se
> descarta porque no se crea nada nuevo.
### Respuesta éxito (` 200`)
` ``json
{
"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`)
` ``json
{
"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
1. **Nunca mandar un array a ` POST /api/Picking`.** Un documento (una
factura) por llamada — el ` FOR` por factura es responsabilidad del WMS.
2. **` notificacion` ausente o distinto de ` "cierre"` = preliminar** (default
seguro, nunca crea nada en SAP).
3. **El orden importa**: primero el ` FOR` de cierre por cada factura
(` notificacion: "cierre"`), y **recién al final** —una sola vez— el
` POST /api/Picking/HojaRuta/{docEntry}/Cerrar`.
4. **` docEntry` en ` POST /api/Picking` es el de la Factura**, no el de la
Hoja de Ruta.
## Pendiente de validar en QA
- Confirmar en QA si el nuevo ` Close` del UDO ` ARD_PHR` efectivamente bloquea
modificaciones futuras sobre el ` DocEntry` una vez cerrado (no confirmado
todavía ni en la versión Python).
- Correr los mismos casos contra el endpoint ` .NET` levantado antes de dar
por válido este port — en Python tampoco se probó esto todavía end-to-end
contra Service Layer real (ver ` CLAUDE.md` del proyecto Python, sección
"Pruebas contra HANA/SAP reales").