302 lines
12 KiB
Markdown
302 lines
12 KiB
Markdown
# Endpoint nuevo — Solicitud de Transferencia (a construir en .NET)
|
|
|
|
Módulo **100% nuevo** — no existe ningún `SolicitudTransferenciaController`
|
|
ni repositorio para `InventoryTransferRequests` en este proyecto hoy
|
|
(confirmado: no hay ninguna referencia a `InventoryTransferRequests` en
|
|
`Net.Data/`). Esta es la especificación para construirlo, traducida 1:1 de
|
|
la implementación ya validada en Python
|
|
(`itca_api_sap_wms_python/app/services/solicitud_transferencia_service.py`,
|
|
`app/schemas/solicitud_transferencia.py`, `app/api/routes/solicitud_transferencia.py`).
|
|
|
|
Ver también [`PORTE_FUNCIONALIDAD_NUEVA.md`](./PORTE_FUNCIONALIDAD_NUEVA.md)
|
|
para los gaps de infraestructura compartida (headers extra en PATCH, GET de
|
|
un solo recurso, acción `/Close`) y los bugs encontrados en el código que
|
|
este módulo va a reusar.
|
|
|
|
## Resumen del contrato
|
|
|
|
| Método | Ruta | Respuesta éxito | Qué hace |
|
|
|---|---|---|---|
|
|
| `PATCH` | `/api/solicitudtransferencia/{docEntry}` | `200` + `BE_ResultadoTransaccion` | Actualiza cantidades de línea; `cantidad: 0` elimina la línea. Hay que reenviar todas las líneas a conservar. |
|
|
| `POST` | `/api/solicitudtransferencia/{docEntry}/cerrar` | `200` + `BE_ResultadoTransaccion` | Cierra el documento completo. Sin body de request. |
|
|
|
|
- **Ruta base**: `api/SolicitudTransferencia` (bajo el virtual path `/integracion`
|
|
de IIS, igual que el resto de los controllers — no hace falta nada especial
|
|
en el código para eso).
|
|
- **Autenticación**: `[Authorize(AuthenticationSchemes = "Bearer")]`, igual
|
|
que el resto de los controllers (ver `TransferenciaController`/`PickingController`
|
|
como referencia).
|
|
- **Objeto SAP asociado**: `InventoryTransferRequests` (Service Layer).
|
|
|
|
## Cómo funciona el `PATCH` (reglas de negocio, confirmadas contra SAP real en QA)
|
|
|
|
El `PATCH` de una Solicitud de Traslado **solo actualiza cantidades de línea
|
|
existentes**, y **elimina la línea** cuando la cantidad llega en `0`. No toca
|
|
fechas, almacenes, comentarios ni ningún otro campo del documento — por eso
|
|
el DTO de este endpoint es mucho más chico que el de `Transferencia` (solo
|
|
`NumOrden` + `Detalle[{Linea, Cantidad}]`).
|
|
|
|
Reglas confirmadas directo contra el Service Layer real de QA:
|
|
|
|
1. Alcanza con mandar `LineNum` + `Quantity` por línea — nada de `ItemCode`,
|
|
`WarehouseCode`, fechas, etc.
|
|
2. SAP **rechaza** una línea si se manda explícita con `Quantity: 0` —
|
|
por eso una línea con `Cantidad: 0` en el request **se omite del array**
|
|
en vez de mandarse en cero.
|
|
3. Para que SAP realmente **elimine** la línea omitida (en vez de dejarla
|
|
intacta), hace falta el header `B1S-ReplaceCollectionsOnPatch: true` — le
|
|
indica a Service Layer que trate `StockTransferLines` como reemplazo
|
|
**completo** del array, no un merge parcial.
|
|
4. **Hallazgo posterior, crítico** (no estaba cubierto en la primera tanda de
|
|
pruebas, que usó un documento con `Rate=0` en todas las líneas): al ser un
|
|
reemplazo completo, SAP **también exige el `Rate` de cada línea** — si el
|
|
documento tiene líneas valorizadas (`Rate != 0`) y no se reenvía, el PATCH
|
|
falla. Por eso hay que hacer un `GET InventoryTransferRequests({docEntry})`
|
|
**antes** del PATCH, leer el `Rate` real de cada línea, y reenviarlo tal
|
|
cual junto con la `Quantity` nueva — **nunca mandar `Rate: 0` fijo**, eso
|
|
pisaría la valorización real del documento (en C# esto no tiene el
|
|
problema de serialización que tuvo la versión Python con `decimal.Decimal`
|
|
de `hdbcli` — `Newtonsoft.Json` serializa `decimal`/`double` sin problema
|
|
— pero la regla de negocio de todas formas aplica igual: hay que leer el
|
|
`Rate` real, no inventarlo).
|
|
|
|
> ⚠️ **Consecuencia del header para el WMS**: como el array es reemplazo
|
|
> completo, cualquier línea que el request **no incluya** en `Detalle`
|
|
> también se elimina de SAP — igual que si viniera con `Cantidad: 0`. No es
|
|
> un PATCH parcial tradicional. El cliente (WMS) tiene que mandar **siempre**
|
|
> todas las líneas que el documento debe conservar.
|
|
|
|
## DTO/Entidad a crear
|
|
|
|
```csharp
|
|
// Net.Business.Entities/WMS/SolicitudTransferencia/BE_SolicitudTransferenciaPatchDetalle.cs
|
|
public class BE_SolicitudTransferenciaPatchDetalle
|
|
{
|
|
public int Linea { get; set; }
|
|
public double Cantidad { get; set; }
|
|
}
|
|
|
|
// Net.Business.Entities/WMS/SolicitudTransferencia/BE_SolicitudTransferenciaPatchRequest.cs
|
|
public class BE_SolicitudTransferenciaPatchRequest
|
|
{
|
|
public string NumOrden { get; set; } // opcional, solo para el log de auditoria - no se usa en el PATCH a SAP
|
|
public List<BE_SolicitudTransferenciaPatchDetalle> Detalle { get; set; }
|
|
}
|
|
```
|
|
|
|
El `Dto_SolicitudTransferenciaPatchRequest` (en `Net.Business.DTO/WMS/SolicitudTransferencia/`)
|
|
es el mismo shape, con su `RetornaModelo()`, siguiendo el patrón de
|
|
`Dto_TransferenciaRequest`/`Dto_PickingRequest`. El binding de ASP.NET Core ya
|
|
es case-insensitive por defecto (igual que en Python), así que no hace falta
|
|
nada especial para aceptar `numOrden`/`NumOrden`/`detalle`/`Detalle`.
|
|
|
|
### Body de ejemplo (`PATCH /api/SolicitudTransferencia/{docEntry}`)
|
|
|
|
```json
|
|
{
|
|
"numOrden": "SOL-000123",
|
|
"detalle": [
|
|
{ "linea": 0, "cantidad": 8 },
|
|
{ "linea": 1, "cantidad": 0 }
|
|
]
|
|
}
|
|
```
|
|
|
|
## Lógica del repositorio (`SolicitudTransferenciaRepository.Update`)
|
|
|
|
Traducción directa de `solicitud_transferencia_service.py::update`:
|
|
|
|
```csharp
|
|
public async Task<BE_ResultadoTransaccion<object>> Update(int docEntry, BE_SolicitudTransferenciaPatchRequest entidad)
|
|
{
|
|
_metodoName = "Update";
|
|
ResultadoTransaccion<object> resultadoTransaccion = new ResultadoTransaccion<object>(_metodoName, _aplicacionName);
|
|
|
|
try
|
|
{
|
|
// 1. GET del documento actual para leer el Rate real de cada linea.
|
|
// Reusar ConnectionServiceLayer.GetAsync<T>(..., dinamico: true) -
|
|
// ver nota de "gap de infraestructura" en PORTE_FUNCIONALIDAD_NUEVA.md.
|
|
var doc = (await _connectServiceLayer.GetAsync<BE_InventoryTransferRequest>(
|
|
$"InventoryTransferRequests({docEntry})", 1, dinamico: true)).FirstOrDefault();
|
|
|
|
var ratesByLine = doc.StockTransferLines.ToDictionary(l => l.LineNum, l => l.Rate);
|
|
|
|
// 2. Armar las lineas: omitir cantidad=0, preservar Rate real.
|
|
var lines = new List<object>();
|
|
foreach (var detalle in entidad.Detalle)
|
|
{
|
|
if (detalle.Cantidad == 0) continue;
|
|
|
|
if (!ratesByLine.TryGetValue(detalle.Linea, out var rate))
|
|
{
|
|
return resultadoTransaccion.GetInformacionResultado(
|
|
$"La linea {detalle.Linea} no existe en la Solicitud de Traslado {docEntry}", entidad);
|
|
}
|
|
|
|
lines.Add(new { LineNum = detalle.Linea, Quantity = detalle.Cantidad, Rate = rate });
|
|
}
|
|
|
|
// 3. PATCH con el header B1S-ReplaceCollectionsOnPatch.
|
|
// Requiere el overload de PatchAsyncSBA con headers extra - ver gap #1.
|
|
await _connectServiceLayer.PatchAsyncSBA<object>(
|
|
$"InventoryTransferRequests({docEntry})",
|
|
new { StockTransferLines = lines },
|
|
extraHeaders: new Dictionary<string, string> { ["B1S-ReplaceCollectionsOnPatch"] = "true" });
|
|
|
|
return null; // exito real de SAP = 204/200 real (ver controller abajo)
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return resultadoTransaccion.GetInformacionResultado(ex.Message.ToString(), entidad);
|
|
}
|
|
}
|
|
```
|
|
|
|
(Pseudocódigo de referencia — ajustar nombres exactos de clases/métodos una
|
|
vez que exista el overload de `PatchAsyncSBA` con headers y la entidad
|
|
`BE_InventoryTransferRequest`/`BE_InventoryTransferRequestLine` para
|
|
deserializar el `GET`.)
|
|
|
|
## `POST /api/SolicitudTransferencia/{docEntry}/cerrar`
|
|
|
|
Cierra el documento completo vía `POST InventoryTransferRequests({docEntry})/Close`
|
|
— acción fija de Service Layer (no se traduce). Sin body de request, body
|
|
vacío (`{}`) hacia SAP. Confirmado contra SAP real que cierra el documento
|
|
completo aunque alguna línea ya se haya convertido en una Transferencia.
|
|
|
|
```csharp
|
|
public async Task<BE_ResultadoTransaccion<object>> Cerrar(int docEntry)
|
|
{
|
|
_metodoName = "Cerrar";
|
|
ResultadoTransaccion<object> resultadoTransaccion = new ResultadoTransaccion<object>(_metodoName, _aplicacionName);
|
|
|
|
try
|
|
{
|
|
await _connectServiceLayer.PostAsync<object>($"InventoryTransferRequests({docEntry})/Close", new { });
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return resultadoTransaccion.GetInformacionResultado(ex.Message.ToString(), null);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Controller
|
|
|
|
Igual patrón que `TransferenciaController`, con una diferencia importante de
|
|
status code (ver nota abajo):
|
|
|
|
```csharp
|
|
[HttpPatch("{docEntry}")]
|
|
public async Task<IActionResult> Patch(int docEntry, [FromBody] Dto_SolicitudTransferenciaPatchRequest value)
|
|
{
|
|
var error = await _repository.SolicitudTransferencia.Update(docEntry, value.RetornaModelo());
|
|
|
|
var result = error ?? new BE_ResultadoTransaccion<object>
|
|
{
|
|
DocEntry = docEntry,
|
|
StatusCode = 200,
|
|
Message = "Registrado Correctamente",
|
|
AppName = "SolicitudTransferenciaRepository",
|
|
MethodName = "Update",
|
|
Data = value,
|
|
};
|
|
|
|
B_Log.Add(/* ... mismo patron que el resto de los controllers ... */);
|
|
|
|
return error == null ? Ok(result) : BadRequest(result); // 200 con body si OK, 400 si error
|
|
}
|
|
|
|
[HttpPost("{docEntry}/cerrar")]
|
|
public async Task<IActionResult> Cerrar(int docEntry)
|
|
{
|
|
var error = await _repository.SolicitudTransferencia.Cerrar(docEntry);
|
|
|
|
var result = error ?? new BE_ResultadoTransaccion<object>
|
|
{
|
|
DocEntry = docEntry,
|
|
StatusCode = 200,
|
|
Message = "Cerrado Correctamente",
|
|
AppName = "SolicitudTransferenciaRepository",
|
|
MethodName = "Cerrar",
|
|
};
|
|
|
|
B_Log.Add(/* ... */);
|
|
|
|
return error == null ? Ok(result) : BadRequest(result);
|
|
}
|
|
```
|
|
|
|
> **Por qué `200` con body y no `204`**: decisión deliberada, igual que en
|
|
> Python. Un `204 No Content` no puede llevar body por spec HTTP (RFC 7231
|
|
> §6.3.5), y muchos clientes HTTP — **incluido `HttpClient` de .NET**, que es
|
|
> justo el que va a consumir esto el WMS si está en .NET también — ni
|
|
> siquiera lo leen si el status es `204`. `201` se descartó porque ni el
|
|
> `PATCH` ni el `cerrar` crean un recurso nuevo. El resto de los endpoints
|
|
> `PATCH` del proyecto (`Transferencia`) sigue en `204` sin body — esta es
|
|
> una excepción deliberada, solo para estos dos endpoints nuevos.
|
|
|
|
## Respuestas de ejemplo
|
|
|
|
### `PATCH` éxito (`200`)
|
|
|
|
```json
|
|
{
|
|
"docEntry": 4521,
|
|
"docNum": null,
|
|
"statusCode": 200,
|
|
"message": "Registrado Correctamente",
|
|
"error": null,
|
|
"appName": "SolicitudTransferenciaRepository",
|
|
"methodName": "Update",
|
|
"listData": null,
|
|
"data": { "numOrden": "SOL-000123", "detalle": [ { "linea": 0, "cantidad": 8 } ] },
|
|
"nombreEstacion": "SERVER-WMS"
|
|
}
|
|
```
|
|
|
|
### `PATCH` error (`400`)
|
|
|
|
```json
|
|
{
|
|
"docEntry": null,
|
|
"docNum": null,
|
|
"statusCode": 500,
|
|
"message": null,
|
|
"error": "DocEntry no existe",
|
|
"appName": "SolicitudTransferenciaRepository",
|
|
"methodName": "Update",
|
|
"listData": null,
|
|
"data": { "numOrden": "SOL-000123", "detalle": [ "..." ] },
|
|
"nombreEstacion": "SERVER-WMS"
|
|
}
|
|
```
|
|
|
|
(El `statusCode: 500` interno + el `400` HTTP real del controller es el mismo
|
|
patrón que usa `ResultadoTransaccion<T>.GetInformacionResultado(string, T)` ya
|
|
existente — ver `Net.CrossCotting/ResultadoTransaccion.cs`.)
|
|
|
|
## Checklist de integración para el WMS (igual que la versión Python)
|
|
|
|
1. **Mandar solo la línea que cambia y no las demás** → borra las no
|
|
incluidas (header de reemplazo completo). Reconstruir siempre el
|
|
`detalle` completo.
|
|
2. **Mandar `cantidad: 0` esperando que quede en cero** → no, `0` significa
|
|
"eliminar esta línea".
|
|
3. **Usar `linea` con un número que no existe en el documento** → SAP
|
|
devuelve error, no crea una línea nueva.
|
|
4. **Esperar un `204 No Content`** → este endpoint responde `200` con body.
|
|
|
|
## Pendiente de validar en QA antes de dar por cerrado el porte
|
|
|
|
- Correr los mismos casos de `tools/solicitud-transferencia-patch.http` y
|
|
`tools/solicitud-transferencia-post-close.http` (del proyecto Python)
|
|
contra el nuevo endpoint `.NET` levantado, no solo contra Service Layer
|
|
directo.
|
|
- Confirmar el nombre exacto del método de binding de parámetros del driver
|
|
`Sap.Data.Hana` instalado (si se toca `HelperProcesoPiciking` como parte de
|
|
este trabajo — ver bug #6 en `PORTE_FUNCIONALIDAD_NUEVA.md`, no aplica
|
|
directo a este módulo pero comparte el mismo cliente HANA).
|