Files
itca_api_sap_wms/docs/SOLICITUD_TRANSFERENCIA.md
T
2026-10-05 17:53:41 -05:00

12 KiB

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 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

// 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})

{
  "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:

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.

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):

[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)

{
  "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)

{
  "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).