Files

317 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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**.
## 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.
## Cantidades de la NC y de las transferencias (versión 2.1.6.0)
- **Cantidad de la línea de NC** (DESPACHO y PICKING estado 13): `cantidad
facturada − CantidadPedida`; solo entran las líneas con resultado mayor a 0.
- **Transferencia automática** (cuando `U_ITC_ALMDES` difiere del almacén de la
línea): una línea por artículo, con la cantidad sumada de las líneas de la NC
de ese par origen/destino y los lotes de todas esas líneas (el mismo lote en
varias líneas se suma).
## 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:
```csharp
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()`:
```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
// === 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**: `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** (solo `ULTIMA MILLA`, reparto en curso —
`Notificacion` ausente o distinto de `"cierre"`):
```json
{
"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):
```json
{
"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)
```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").