272 lines
9.5 KiB
Markdown
272 lines
9.5 KiB
Markdown
# 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**.
|
||
|
|
|
||
|
|
## Resumen del contrato
|
||
|
|
|
||
|
|
| Método | Ruta | Cambia hoy | Qué hace |
|
||
|
|
|---|---|---|---|
|
||
|
|
| `POST` | `/api/picking` | **Sí** — agrega campo `Notificacion` | `"cierre"` → lógica actual completa (crea documento SAP); cualquier otro valor/ausente → solo actualiza estado en HANA, nunca toca SAP. |
|
||
|
|
| `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
|
||
|
|
public string Notificacion { get; set; } // "preliminar" | "cierre"; cualquier otro valor o ausente = "preliminar"
|
||
|
|
```
|
||
|
|
|
||
|
|
**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
|
||
|
|
|
||
|
|
// === NUEVO: notificacion preliminar (diurna) - nunca toca SAP
|
||
|
|
if (!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 `notificacion == "cierre"` — no se toca nada
|
||
|
|
de ahí. Solo se agrega el `if` de corte después de la línea 96.
|
||
|
|
|
||
|
|
> **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
|
||
|
|
|
||
|
|
**Notificación preliminar** (reparto en curso — `Notificacion` ausente o
|
||
|
|
distinto de `"cierre"`):
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"numOrden": "OT-2026-001",
|
||
|
|
"docEntry": 500,
|
||
|
|
"proceso": "DESPACHO",
|
||
|
|
"codMotivo": "M01",
|
||
|
|
"idEstadoCubico": 6,
|
||
|
|
"codigoAlmacen": "ALM01",
|
||
|
|
"detalle": [
|
||
|
|
{ "item": 0, "codigoArticulo": "ART-001", "cantidadPedida": 3, "estado": "C" }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Cierre** (mismo shape, agrega `notificacion: "cierre"` — crea el documento
|
||
|
|
SAP real, misma respuesta que hoy):
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"numOrden": "OT-2026-001",
|
||
|
|
"docEntry": 500,
|
||
|
|
"proceso": "DESPACHO",
|
||
|
|
"codMotivo": "M01",
|
||
|
|
"idEstadoCubico": 6,
|
||
|
|
"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").
|