Files
itca_api_sap_wms/docs/PICKING.md
T
GeorgeandClaude Sonnet 5.5 e475b7f48c Picking: Notificacion=cierre solo se exige con Proceso ULTIMA MILLA. Versión 2.1.2.0
DESPACHO y PICKING vuelven a generar nota de crédito sin depender de
Notificacion. Documentada la regla Proceso + Notificacion.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 08:47:44 -05:00

11 KiB

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

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:

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

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

// ... 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"):

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

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

{
  "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 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:
    Task<BE_ResultadoTransaccion<object>> CerrarHojaRuta(int docEntry);
    
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):

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

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

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