Compare commits

...
10 Commits
Author SHA1 Message Date
GeorgeandClaude Sonnet 5.5 d0118d1848 Recibo (NC desde Solicitud de Devolucion): lotes solo de la distribucion y serie segun tipo de documento. Versión 2.1.7.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 18:03:58 -05:00
GeorgeandClaude Sonnet 5.5 0a2dc12305 Log de errores del Service Layer: DOCENTRY = BaseEntry del documento base; columnas VARCHAR(100)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 17:41:08 -05:00
GeorgeandClaude Sonnet 5.5 4ad0754ba0 Cantidades: PICKING filtra lineas sin diferencia; transferencias juntan lotes de todas las lineas del articulo. Versión 2.1.6.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 17:27:29 -05:00
GeorgeandClaude Sonnet 5.5 e5ea57b673 Log del JSON enviado al Service Layer cuando SAP rechaza, para todos los documentos. Versión 2.1.5.0
Se mueve de CreditNotesRepository a ConnectionServiceLayer (PostAsync,
PatchAsyncSBA, PatchNoContentAsync) con un hook asignado en Startup.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 17:06:07 -05:00
GeorgeandClaude Sonnet 5.5 1ce0da7735 NC: guardar en ITCA_LOG_INTEGRACION el JSON enviado al Service Layer cuando SAP rechaza. Versión 2.1.5.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 17:00:52 -05:00
GeorgeandClaude Sonnet 5.5 ded1638cd0 Documentar correcciones de v2.1.3.0 (reopen) y v2.1.4.0 (lotes de la NC)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 10:33:20 -05:00
GeorgeandClaude Sonnet 5.5 14fffa8ba4 NC de DESPACHO: lotes de la Entrega origen o de la factura, linea por linea. Versión 2.1.4.0
Si la linea de la factura viene de una Entrega (BaseType 15) los lotes se
toman de la linea de la Entrega (BaseLine); si no, de la linea de la factura
(LineNum). Se valida que los lotes cubran la cantidad y se devuelve un error
claro por linea en vez del error generico de SAP.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 10:29:02 -05:00
GeorgeandClaude Sonnet 5.5 6386070f4e Reopen de factura: tratar 204 sin cuerpo como exito y mostrar el motivo de SAP. Versión 2.1.3.0
PostReOpen deserializaba el cuerpo vacio del 204 a null y fallaba al marcar
Exito, devolviendo "La Factura no se puede reabrir" aunque SAP la reabria.
Picking ahora agrega el mensaje real de SAP a ese error.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 10:15:40 -05:00
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
GeorgeandClaude Sonnet 5.5 e31524ab6b SolicitudTransferencia PATCH: agregar campo de usuario U_SYP_MDTS = TSE en cabecera. Versión 2.1.1.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 09:26:19 -05:00
12 changed files with 310 additions and 39 deletions

No files matched your search

+55 -1
View File
@@ -16,11 +16,65 @@ Detalle técnico en `docs/PORTE_FUNCIONALIDAD_NUEVA.md`,
1. **SolicitudTransferencia**: `PATCH /api/SolicitudTransferencia/{docEntry}` y 1. **SolicitudTransferencia**: `PATCH /api/SolicitudTransferencia/{docEntry}` y
`POST /api/SolicitudTransferencia/{docEntry}/cerrar`. `POST /api/SolicitudTransferencia/{docEntry}/cerrar`.
2. **Picking diurno/nocturno**: campo `Notificacion` (early-return en 2. **Picking diurno/nocturno**: campo `Notificacion` (early-return en
`PickingRepository.Create` tras `HelperProcesoPiciking.Set`) y `PickingRepository.Create` tras `HelperProcesoPiciking.Set`, **solo para
`Proceso = "ULTIMA MILLA"`**; DESPACHO/PICKING generan NC sin depender de
él) y
`POST /api/Picking/HojaRuta/{docEntry}/Cerrar`. `POST /api/Picking/HojaRuta/{docEntry}/Cerrar`.
3. **`ConnectionServiceLayer`**: `PatchNoContentAsync` y `PostActionAsync` 3. **`ConnectionServiceLayer`**: `PatchNoContentAsync` y `PostActionAsync`
(métodos nuevos; los existentes no se tocaron). (métodos nuevos; los existentes no se tocaron).
## Regla Proceso + Notificacion (v2.1.2.0)
`Notificacion = "cierre"` se exige **solo si `Proceso = "ULTIMA MILLA"`**
(si no, solo actualiza estado en HANA). `DESPACHO` y `PICKING` generan NC y
transferencias siempre, sin depender de `Notificacion`. Detalle y tabla en
`docs/PICKING.md`.
## Correcciones de la NC en Picking/DESPACHO (v2.1.3.0 y v2.1.4.0)
- **v2.1.3.0 — reapertura de factura**: `ConnectionServiceLayer.PostReOpen`
trata el `204` sin cuerpo como éxito (antes fallaba al marcar `Exito` sobre
`null` y respondía *"La Factura no se puede reabrir"* aunque SAP la
reabría). El error de Picking ahora incluye el motivo real de SAP.
- **v2.1.4.0 — lotes de la NC en DESPACHO, línea por línea**: si la línea de la
factura tiene Entrega de origen (`BaseType = 15`) los lotes salen de la línea
de la Entrega (`DeliveryNotes(BaseEntry)`, línea `BaseLine`); si no, de la
línea de la factura (`LineNum`). Si los lotes no cubren la cantidad, o no se
lee la Entrega, responde un error con línea y artículo. PICKING (estado 13)
no cambió (lote nuevo por fecha y hora). Detalle en `docs/PICKING.md`.
- **v2.1.5.0 — log del JSON enviado al Service Layer cuando SAP lo rechaza**
(cualquier documento, no solo la NC): `ConnectionServiceLayer` llama a
`ErrorLogger` (asignado en `Startup` a `Net.Data/HelperData/Log/HelperLogServiceLayer.AddError`)
en los fallos de `PostAsync`, `PatchAsyncSBA` y `PatchNoContentAsync`. Guarda en
`ITC_WMS_SAP.ITCA_LOG_INTEGRACION`: `ORIGEN = "ServiceLayer"`,
`PROCESO = "ServiceLayer Error"`, `NRO_ORDEN` = ruta del recurso (p. ej.
`CreditNotes`), `DOCENTRY` = `BaseEntry` de la primera línea del JSON (factura o
solicitud base; vacío si no hay), `DOCENTRYSAP`/`DOCNUMSAP` = `0` (SAP rechazó),
`TRAMA_ORIGEN` = JSON enviado,
`TRAMA_RESPUESTA` = mensaje de SAP. Solo en error. La respuesta de la API al
consumidor no cambia y si el log falla no rompe el flujo. No cubre
`PostActionAsync` (cuerpo `{}`) ni `PostReOpen`/`PostClose`. Las columnas `PROCESO`, `NRO_ORDEN`
y `DOCENTRY` son `VARCHAR(100)`.
- **v2.1.6.0 — cantidades**: (1) la NC de PICKING (estado 13) solo incluye líneas
con `cantidad facturada − CantidadPedida > 0`, igual que DESPACHO (antes una
línea completa generaba una línea de NC con cantidad 0); (2) la transferencia
automática de stock (Picking y Recibo) junta los lotes de **todas** las líneas
del mismo artículo para el par origen/destino (antes solo tomaba los de la
primera línea y la suma no cuadraba con la cantidad). El almacén de la línea de
la NC sigue saliendo de la factura, no del `codigoSubAlmacen` del request.
- **v2.1.7.0 — NC de Recibo (Devolución, base Solicitud de Devolución)**: (1) los
lotes de cada línea eran los del request (`LoteProducto`) **más** los de la
distribución (stock HANA o de la factura, o lote de respaldo por fecha/hora),
con lo que sumaban el doble y podían incluir un lote vacío; ahora se limpia el
lote inicial y quedan solo los de la distribución (el `LoteProducto` del
request ya no se usa en la NC); (2) la serie se elige por el **tipo** del
documento origen (`U_SYP_MDTO == "03"` → `BC10`, otro → `FC10`); antes se
comparaba la serie origen (p. ej. `B005`) con `"03"` y siempre salía `FC10`.
Pendiente: validar con contabilidad la serie de boletas, y que Recibo no
genera transferencias por almacén destino (nunca llena `U_ITC_ALMDES`).
- Sin probar contra SAP: confirmar con la factura 292410 que SAP devuelve
`BaseType`/`BaseEntry` en las líneas y `BatchNumbers` en las de la Entrega.
## Cambios de configuración y correcciones hechas en esta carpeta ## Cambios de configuración y correcciones hechas en esta carpeta
- **Credenciales y esquema HANA en `appsettings.json`**, sección `Hana` - **Credenciales y esquema HANA en `appsettings.json`**, sección `Hana`
@@ -33,6 +33,20 @@ namespace Net.Business.Entities.ServicesLayer.Invoices
public string U_SYP_MDSD { get; set; } public string U_SYP_MDSD { get; set; }
public string U_SYP_MDCD { get; set; } public string U_SYP_MDCD { get; set; }
public string U_SYP_MDTD { get; set; } public string U_SYP_MDTD { get; set; }
public List<BE_InvoicesLine> DocumentLines { get; set; } public List<BE_InvoicesConsultaLine> DocumentLines { get; set; }
}
// Linea de factura leida de SAP: agrega el documento origen (BaseType 15 = Entrega)
public class BE_InvoicesConsultaLine : BE_InvoicesLine
{
public int? BaseType { get; set; }
public int? BaseEntry { get; set; }
}
// Entrega (DeliveryNotes) leida de SAP, solo para obtener los lotes por linea
public class BE_DeliveryNotesConsulta
{
public int DocEntry { get; set; }
public List<BE_InvoicesConsultaLine> DocumentLines { get; set; }
} }
} }
@@ -3,9 +3,9 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>ITCA_API_SAP_WMS</AssemblyName> <AssemblyName>ITCA_API_SAP_WMS</AssemblyName>
<Version>2.1.0</Version> <Version>2.1.7</Version>
<AssemblyVersion>2.1.0.0</AssemblyVersion> <AssemblyVersion>2.1.7.0</AssemblyVersion>
<FileVersion>2.1.0.0</FileVersion> <FileVersion>2.1.7.0</FileVersion>
<UserSecretsId>24763424-3300-4c85-87e3-503f8fdebd14</UserSecretsId> <UserSecretsId>24763424-3300-4c85-87e3-503f8fdebd14</UserSecretsId>
<!-- Antes solo estaba condicionado a Debug|AnyCPU - un "dotnet publish -c Release" <!-- Antes solo estaba condicionado a Debug|AnyCPU - un "dotnet publish -c Release"
(lo normal para IIS) se quedaba sin este archivo, y Startup.cs revienta con (lo normal para IIS) se quedaba sin este archivo, y Startup.cs revienta con
+2
View File
@@ -19,6 +19,8 @@ namespace Net.Business.Services
{ {
Configuration = configuration; Configuration = configuration;
Net.Data.LocalData.Configure(configuration); Net.Data.LocalData.Configure(configuration);
// Guarda en ITCA_LOG_INTEGRACION el JSON enviado al Service Layer cuando SAP lo rechaza
Net.Connection.ServiceLayer.ConnectionServiceLayer.ErrorLogger = Net.Data.HelperData.Log.HelperLogServiceLayer.AddError;
} }
public IConfiguration Configuration { get; } public IConfiguration Configuration { get; }
@@ -19,6 +19,16 @@ namespace Net.Connection.ServiceLayer
public static bool ServicioActivo = false; public static bool ServicioActivo = false;
public static string SLSession; public static string SLSession;
// Se invoca solo cuando SAP rechaza un envio: (ruta del recurso, JSON enviado, respuesta de SAP).
// Lo asigna Startup (esta capa no conoce HANA).
public static Action<string, object, string> ErrorLogger;
private static void LogErrorEnvio(string query, object jsonEnviado, string respuesta)
{
try { ErrorLogger?.Invoke(query, jsonEnviado, respuesta); }
catch { /* el log nunca debe romper el flujo principal */ }
}
private readonly string _url; private readonly string _url;
private readonly string _company; private readonly string _company;
private readonly string _user; private readonly string _user;
@@ -464,6 +474,7 @@ namespace Net.Connection.ServiceLayer
var estado = (int)response_.StatusCode; var estado = (int)response_.StatusCode;
responseBody.CodigoHttp = estado.ToString(); responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "Services Unauthorized : " + _responseLoginServiceLayer.error.message.ToString(); responseBody.Mensaje = "Services Unauthorized : " + _responseLoginServiceLayer.error.message.ToString();
LogErrorEnvio(Query, obj, responseBody.Mensaje);
return responseBody; return responseBody;
} }
@@ -493,6 +504,7 @@ namespace Net.Connection.ServiceLayer
var estado = (int)response_.StatusCode; var estado = (int)response_.StatusCode;
responseBody.CodigoHttp = estado.ToString(); responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "Error Services : " + _responseLoginServiceLayer.error.message.ToString(); responseBody.Mensaje = "Error Services : " + _responseLoginServiceLayer.error.message.ToString();
LogErrorEnvio(Query, obj, responseBody.Mensaje);
return responseBody; return responseBody;
} }
@@ -568,7 +580,11 @@ namespace Net.Connection.ServiceLayer
if (response_.StatusCode == System.Net.HttpStatusCode.OK || response_.StatusCode == System.Net.HttpStatusCode.Created || response_.StatusCode == HttpStatusCode.NoContent) // 200 or Created if (response_.StatusCode == System.Net.HttpStatusCode.OK || response_.StatusCode == System.Net.HttpStatusCode.Created || response_.StatusCode == HttpStatusCode.NoContent) // 200 or Created
{ {
// DESERIALIZAMOS Content DEL RESPONSE // DESERIALIZAMOS Content DEL RESPONSE
dynamic responseBody = JsonConvert.DeserializeObject<T>(responseText_); // Reopen responde 204 sin cuerpo: DeserializeObject devuelve null, asi que
// se crea una instancia vacia para poder marcar el exito.
dynamic responseBody = string.IsNullOrWhiteSpace(responseText_)
? Activator.CreateInstance<T>()
: JsonConvert.DeserializeObject<T>(responseText_);
responseBody.Exito = true; responseBody.Exito = true;
var estado = (int)response_.StatusCode; var estado = (int)response_.StatusCode;
responseBody.CodigoHttp = estado.ToString(); responseBody.CodigoHttp = estado.ToString();
@@ -1068,6 +1084,7 @@ namespace Net.Connection.ServiceLayer
var estado = (int)response_.StatusCode; var estado = (int)response_.StatusCode;
//responseBody.CodigoHttp = estado.ToString(); //responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "Services Unauthorized : " + _responseLoginServiceLayer.error.message.ToString(); responseBody.Mensaje = "Services Unauthorized : " + _responseLoginServiceLayer.error.message.ToString();
LogErrorEnvio(Query, obj, responseBody.Mensaje);
return responseBody; return responseBody;
} }
@@ -1092,6 +1109,7 @@ namespace Net.Connection.ServiceLayer
var estado = (int)response_.StatusCode; var estado = (int)response_.StatusCode;
//responseBody.CodigoHttp = estado.ToString(); //responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "Error Services : " + _responseLoginServiceLayer.error.message.ToString(); responseBody.Mensaje = "Error Services : " + _responseLoginServiceLayer.error.message.ToString();
LogErrorEnvio(Query, obj, responseBody.Mensaje);
return responseBody; return responseBody;
} }
@@ -1159,7 +1177,9 @@ namespace Net.Connection.ServiceLayer
return; // exito real - sin body que parsear return; // exito real - sin body que parsear
} }
throw new Exception(ExtraerMensajeErrorSap(responseText_)); var mensajeError = ExtraerMensajeErrorSap(responseText_);
LogErrorEnvio(Query, obj, mensajeError);
throw new Exception(mensajeError);
} }
} }
@@ -0,0 +1,66 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Sap.Data.Hana;
using System;
using System.Text;
namespace Net.Data.HelperData.Log
{
// Guarda en ITCA_LOG_INTEGRACION el JSON enviado al Service Layer cuando SAP rechaza un envio (cualquier documento).
// Misma tabla que Net.Business.Services.Log.B_Log (esa clase no se puede usar desde Net.Data).
public class HelperLogServiceLayer
{
// query: ruta del recurso del Service Layer (ej. "CreditNotes", "StockTransfers")
public static void AddError(string query, object jsonEnviado, string respuesta)
{
try
{
var recurso = query ?? "";
if (recurso.Length > 100) recurso = recurso.Substring(0, 100);
var json = JsonConvert.SerializeObject(jsonEnviado);
var docEntryBase = ObtenerDocEntryBase(json);
var trama = Encoding.UTF8.GetBytes(json);
var tramaRespuesta = Encoding.UTF8.GetBytes(respuesta ?? "");
using (var hana = new HanaConnection(LocalData.ConnectionString))
{
hana.Open();
HanaCommand hanaCommand = hana.CreateCommand();
hanaCommand.CommandText = "INSERT INTO \"ITC_WMS_SAP\".\"ITCA_LOG_INTEGRACION\" (\"CODIGO\", \"ORIGEN\", \"TRAMA_ORIGEN\", \"TRAMA_RESPUESTA\", \"PROCESO\", \"FECHA\", \"NRO_ORDEN\", \"DOCENTRY\", \"DOCNUMSAP\", \"DOCENTRYSAP\", \"HORA\") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
hanaCommand.Parameters.Add("CODIGO", HanaDbType.VarChar).Value = Guid.NewGuid().ToString();
hanaCommand.Parameters.Add("ORIGEN", HanaDbType.VarChar).Value = "ServiceLayer";
hanaCommand.Parameters.Add("TRAMA_ORIGEN", HanaDbType.Blob).Value = trama;
hanaCommand.Parameters.Add("TRAMA_RESPUESTA", HanaDbType.Blob).Value = tramaRespuesta;
hanaCommand.Parameters.Add("PROCESO", HanaDbType.VarChar).Value = "ServiceLayer Error";
hanaCommand.Parameters.Add("FECHA", HanaDbType.Date).Value = DateTime.Now.ToString("yyyy-MM-dd");
hanaCommand.Parameters.Add("NRO_ORDEN", HanaDbType.VarChar).Value = recurso;
hanaCommand.Parameters.Add("DOCENTRY", HanaDbType.VarChar).Value = docEntryBase;
hanaCommand.Parameters.Add("DOCNUMSAP", HanaDbType.VarChar).Value = "0";
hanaCommand.Parameters.Add("DOCENTRYSAP", HanaDbType.VarChar).Value = "0";
hanaCommand.Parameters.Add("HORA", HanaDbType.VarChar).Value = DateTime.Now.ToString("HH:mm:ss");
hanaCommand.CommandType = System.Data.CommandType.Text;
hanaCommand.ExecuteNonQuery();
}
}
catch
{
// el log nunca debe romper el flujo principal
}
}
// DocEntry del documento base (factura o solicitud) de la primera linea del JSON; vacio si no hay
private static string ObtenerDocEntryBase(string json)
{
try
{
var baseEntry = JObject.Parse(json)["DocumentLines"]?[0]?["BaseEntry"];
return baseEntry == null || baseEntry.Type == JTokenType.Null ? "" : baseEntry.ToString();
}
catch
{
return "";
}
}
}
}
@@ -13,5 +13,6 @@ namespace Net.Data.ServicesLayer
Task<BE_ResultadoTransaccion<BE_BaseResponseSap>> SetCreateInvoices(BE_Invoices entidad); Task<BE_ResultadoTransaccion<BE_BaseResponseSap>> SetCreateInvoices(BE_Invoices entidad);
Task<BE_ResultadoTransaccion<BE_InvoicesConsulta>> GetInvoice(int docEntry); Task<BE_ResultadoTransaccion<BE_InvoicesConsulta>> GetInvoice(int docEntry);
Task<BE_ResultadoTransaccion<BE_BaseResponseSap>> ReOpen(int docEntry); Task<BE_ResultadoTransaccion<BE_BaseResponseSap>> ReOpen(int docEntry);
Task<BE_ResultadoTransaccion<BE_DeliveryNotesConsulta>> GetDeliveryNote(int docEntry);
} }
} }
@@ -92,6 +92,25 @@ namespace Net.Data.ServicesLayer
} }
} }
// GET DELIVERY NOTE (para obtener los lotes de la entrega origen de una factura)
public async Task<BE_ResultadoTransaccion<BE_DeliveryNotesConsulta>> GetDeliveryNote(int docEntry)
{
_metodoName = regex.Match(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name).Groups[1].Value.ToString();
ResultadoTransaccion<BE_DeliveryNotesConsulta> resultadoTransaccion = new ResultadoTransaccion<BE_DeliveryNotesConsulta>(_metodoName, _aplicacionName);
try
{
string cadena = $"DeliveryNotes({docEntry})";
List<BE_DeliveryNotesConsulta> objectList = await _connectServiceLayer.GetAsync<BE_DeliveryNotesConsulta>(cadena, 0, true);
return resultadoTransaccion.GetInformacionResultado(0, 0, "Se Listo correctamento...!!!", objectList);
}
catch (Exception ex)
{
return resultadoTransaccion.GetInformacionResultado(-1, -1, ex.Message.ToString());
}
}
public async Task<BE_ResultadoTransaccion<BE_BaseResponseSap>> ReOpen(int docEntry) public async Task<BE_ResultadoTransaccion<BE_BaseResponseSap>> ReOpen(int docEntry)
{ {
_metodoName = regex.Match(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name).Groups[1].Value.ToString(); _metodoName = regex.Match(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name).Groups[1].Value.ToString();
+57 -13
View File
@@ -120,11 +120,12 @@ namespace Net.Data.WMS.Picking
//=== REALIZA PROCESO COMPLETO DE ACTUALIZACION //=== REALIZA PROCESO COMPLETO DE ACTUALIZACION
HelperProcesoPiciking.Set(entidad.Proceso, entidad.IdEstadoCubico, entidad.DocEntry, entidad.CodMotivo, Proceso.Code); HelperProcesoPiciking.Set(entidad.Proceso, entidad.IdEstadoCubico, entidad.DocEntry, entidad.CodMotivo, Proceso.Code);
// === NUEVO (Picking diurno/nocturno): "notificacion" distinta de "cierre" // === Solo para el proceso "ULTIMA MILLA": "notificacion" distinta de "cierre"
// (o ausente) es la notificacion preliminar en tiempo real - solo actualiza // (o ausente) es la notificacion preliminar en tiempo real - solo actualiza
// estado en HANA (ya hecho arriba), NUNCA debe crear documentos en SAP. // estado en HANA (ya hecho arriba), NUNCA debe crear documentos en SAP.
// Default seguro: cualquier valor que no sea "cierre" exacto cae aca. // DESPACHO, PICKING y demas procesos no dependen de este campo.
if (!string.Equals(entidad.Notificacion, "cierre", StringComparison.OrdinalIgnoreCase)) 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); return resultadoTransaccion.GetInformacionResultado(entidad.DocEntry, entidad.NumOrden, "Estado actualizado correctamente", entidad);
} }
@@ -162,7 +163,7 @@ namespace Net.Data.WMS.Picking
var Response_Reopen = invoicestRepository.ReOpen(_invoices.DocEntry).Result; var Response_Reopen = invoicestRepository.ReOpen(_invoices.DocEntry).Result;
if (Response_Reopen.DocEntry == -1) if (Response_Reopen.DocEntry == -1)
{ {
return resultadoTransaccion.GetInformacionResultado("SAP: La Factura no se puede reabrir", entidad); return resultadoTransaccion.GetInformacionResultado("SAP: La Factura no se puede reabrir. " + Response_Reopen.Message, entidad);
} }
} }
@@ -212,13 +213,42 @@ namespace Net.Data.WMS.Picking
}); });
// == REALIZA DISTRIBUCION DE STOCK POR LOTE / ALMACEN // == REALIZA DISTRIBUCION DE STOCK POR LOTE / ALMACEN
// Linea por linea: si la factura tiene una Entrega (BaseType 15) como documento
// origen, los lotes salen de la linea de esa Entrega (BaseLine); si no, de la
// propia linea de la factura (LineNum).
var Entregas = new Dictionary<int, BE_DeliveryNotesConsulta>();
var ErroresLote = new List<string>();
creditNotesNotes.DocumentLines.ForEach(line => creditNotesNotes.DocumentLines.ForEach(line =>
{ {
#region CONSULTA A HANA #region CONSULTA A HANA
//List<BE_ItemStock> ListaStock = HelperStock.Get(line.ItemCode, entidad.CodigoAlmacen); //List<BE_ItemStock> ListaStock = HelperStock.Get(line.ItemCode, entidad.CodigoAlmacen);
var ReferenciaFactura = _invoices.DocumentLines.FirstOrDefault(t => t.LineNum == line.LineNum); var ReferenciaFactura = _invoices.DocumentLines.FirstOrDefault(t => t.LineNum == line.LineNum);
var ListaStock = _invoices.DocumentLines.FirstOrDefault(InvoiceLine => InvoiceLine.LineNum == ReferenciaFactura.BaseLine).BatchNumbers; List<BE_InvoicesBatchNumbers> ListaStock;
if (ReferenciaFactura.BaseType == 15 && ReferenciaFactura.BaseEntry.HasValue && ReferenciaFactura.BaseLine.HasValue)
{
int DocEntryEntrega = ReferenciaFactura.BaseEntry.Value;
if (!Entregas.TryGetValue(DocEntryEntrega, out BE_DeliveryNotesConsulta Entrega))
{
var response_entrega = invoicestRepository.GetDeliveryNote(DocEntryEntrega).Result;
Entrega = (response_entrega.DocEntry == -1) ? null : response_entrega.ListData?.FirstOrDefault();
Entregas[DocEntryEntrega] = Entrega;
}
if (Entrega == null)
{
ErroresLote.Add($"Linea {line.LineNum} ({line.ItemCode}): no se pudo leer la Entrega {DocEntryEntrega} origen de la factura");
return;
}
var LineaEntrega = Entrega.DocumentLines?.FirstOrDefault(t => t.LineNum == ReferenciaFactura.BaseLine.Value);
ListaStock = LineaEntrega?.BatchNumbers ?? new List<BE_InvoicesBatchNumbers>();
}
else
{
ListaStock = ReferenciaFactura.BatchNumbers ?? new List<BE_InvoicesBatchNumbers>();
}
double CantidadPendiente = (double)line.Quantity; double CantidadPendiente = (double)line.Quantity;
var ListOrdenadaFecha = ListaStock; var ListOrdenadaFecha = ListaStock;
@@ -239,9 +269,20 @@ namespace Net.Data.WMS.Picking
} }
}); });
// Hay lotes de origen pero no alcanzan para la cantidad a devolver
if (ListaStock.Count > 0 && CantidadPendiente > 0.000001)
{
ErroresLote.Add($"Linea {line.LineNum} ({line.ItemCode}): los lotes de origen no cubren la cantidad {line.Quantity} (faltan {CantidadPendiente})");
}
#endregion #endregion
}); });
if (ErroresLote.Count > 0)
{
return resultadoTransaccion.GetInformacionResultado("SAP: No se pudo asignar lotes a la nota de credito. " + string.Join(" | ", ErroresLote), entidad);
}
if (creditNotesNotes.DocumentLines.Count > 0) if (creditNotesNotes.DocumentLines.Count > 0)
{ {
var result_creditNotesRepository = _creditNotesRepository.SetCreateCreditNotes(creditNotesNotes).Result; var result_creditNotesRepository = _creditNotesRepository.SetCreateCreditNotes(creditNotesNotes).Result;
@@ -277,12 +318,13 @@ namespace Net.Data.WMS.Picking
ListaUnicaArticulo.ForEach(x => ListaUnicaArticulo.ForEach(x =>
{ {
var ListLote = new List<BE_StockTransferLineBatchNumber>(); var ListLote = new List<BE_StockTransferLineBatchNumber>();
ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).FirstOrDefault().BatchNumbers.ForEach(r => // Lotes de todas las lineas del articulo (si el mismo lote esta en varias lineas se suma)
ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).SelectMany(t => t.BatchNumbers).GroupBy(r => r.BatchNumber).ToList().ForEach(g =>
{ {
ListLote.Add(new BE_StockTransferLineBatchNumber() ListLote.Add(new BE_StockTransferLineBatchNumber()
{ {
BatchNumber = r.BatchNumber, BatchNumber = g.Key,
Quantity = r.Quantity Quantity = g.Sum(r => r.Quantity)
}); });
}); });
Transfer.StockTransferLines.Add(new BE_StockTransferLine() Transfer.StockTransferLines.Add(new BE_StockTransferLine()
@@ -481,7 +523,7 @@ namespace Net.Data.WMS.Picking
var Response_Reopen = invoicestRepository.ReOpen(_invoices.DocEntry).Result; var Response_Reopen = invoicestRepository.ReOpen(_invoices.DocEntry).Result;
if (Response_Reopen.DocEntry == -1) if (Response_Reopen.DocEntry == -1)
{ {
return resultadoTransaccion.GetInformacionResultado("SAP: La Factura no se puede reabrir", entidad); return resultadoTransaccion.GetInformacionResultado("SAP: La Factura no se puede reabrir. " + Response_Reopen.Message, entidad);
} }
} }
@@ -508,7 +550,8 @@ namespace Net.Data.WMS.Picking
entidad.Detalle.ForEach(x => entidad.Detalle.ForEach(x =>
{ {
var Item = _invoices.DocumentLines.ToList().FirstOrDefault(t => t.LineNum == x.Item); var Item = _invoices.DocumentLines.ToList().FirstOrDefault(t => t.LineNum == x.Item);
if (Item != null) // Solo lineas con diferencia a devolver (igual que DESPACHO): una linea completa daria cantidad 0
if (Item != null && Item.Quantity - x.CantidadPedida > 0)
{ {
creditNotesNotes.DocumentLines.Add(new BE_CreditNotesLine() creditNotesNotes.DocumentLines.Add(new BE_CreditNotesLine()
{ {
@@ -572,12 +615,13 @@ namespace Net.Data.WMS.Picking
ListaUnicaArticulo.ForEach(x => ListaUnicaArticulo.ForEach(x =>
{ {
var ListLote = new List<BE_StockTransferLineBatchNumber>(); var ListLote = new List<BE_StockTransferLineBatchNumber>();
ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).FirstOrDefault().BatchNumbers.ForEach(r => // Lotes de todas las lineas del articulo (si el mismo lote esta en varias lineas se suma)
ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).SelectMany(t => t.BatchNumbers).GroupBy(r => r.BatchNumber).ToList().ForEach(g =>
{ {
ListLote.Add(new BE_StockTransferLineBatchNumber() ListLote.Add(new BE_StockTransferLineBatchNumber()
{ {
BatchNumber = r.BatchNumber, BatchNumber = g.Key,
Quantity = r.Quantity Quantity = g.Sum(r => r.Quantity)
}); });
}); });
Transfer.StockTransferLines.Add(new BE_StockTransferLine() Transfer.StockTransferLines.Add(new BE_StockTransferLine()
+11 -4
View File
@@ -403,7 +403,8 @@ namespace Net.Data.WMS.Recibo
} }
#endregion #endregion
string Serie = SerieOrigen == "03" ? "BC10" : "FC10"; // Serie segun el TIPO de documento origen (03 = boleta), no segun su serie
string Serie = U_SYP_MDTO == "03" ? "BC10" : "FC10";
string Correlativo = HelperCorrelativo.Get(Serie); string Correlativo = HelperCorrelativo.Get(Serie);
creditNotesNotes.DocCurrency = returnRequest.DocCurrency; creditNotesNotes.DocCurrency = returnRequest.DocCurrency;
creditNotesNotes.CardCode = returnRequest.CardCode; creditNotesNotes.CardCode = returnRequest.CardCode;
@@ -456,6 +457,10 @@ namespace Net.Data.WMS.Recibo
//=== REALIZA DISTRIBUCION DE STOCK POR LOTE //=== REALIZA DISTRIBUCION DE STOCK POR LOTE
creditNotesNotes.DocumentLines.ForEach(line => creditNotesNotes.DocumentLines.ForEach(line =>
{ {
// La linea nace con un lote del request (LoteProducto); se descarta para que
// los lotes finales sean solo los de la distribucion y sumen la cantidad de la linea
line.BatchNumbers.Clear();
#region CONSULTA A HANA #region CONSULTA A HANA
var ListaStock = HelperStock.Get(line.ItemCode, line.WarehouseCode); var ListaStock = HelperStock.Get(line.ItemCode, line.WarehouseCode);
@@ -492,6 +497,7 @@ namespace Net.Data.WMS.Recibo
{ {
creditNotesNotes.DocumentLines.ForEach(line => creditNotesNotes.DocumentLines.ForEach(line =>
{ {
line.BatchNumbers.Clear();
var ReferenciaFactura = returnRequest.DocumentLines.FirstOrDefault(t => t.LineNum == line.LineNum); var ReferenciaFactura = returnRequest.DocumentLines.FirstOrDefault(t => t.LineNum == line.LineNum);
var ListaStock = _Invoice.DocumentLines.FirstOrDefault(InvoiceLine => InvoiceLine.LineNum == ReferenciaFactura.BaseLine).BatchNumbers; var ListaStock = _Invoice.DocumentLines.FirstOrDefault(InvoiceLine => InvoiceLine.LineNum == ReferenciaFactura.BaseLine).BatchNumbers;
if (ListaStock.Count > 0) if (ListaStock.Count > 0)
@@ -556,12 +562,13 @@ namespace Net.Data.WMS.Recibo
ListaUnicaArticulo.ForEach(x => ListaUnicaArticulo.ForEach(x =>
{ {
var ListLote = new List<BE_StockTransferLineBatchNumber>(); var ListLote = new List<BE_StockTransferLineBatchNumber>();
ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).FirstOrDefault().BatchNumbers.ForEach(r => // Lotes de todas las lineas del articulo (si el mismo lote esta en varias lineas se suma)
ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).SelectMany(t => t.BatchNumbers).GroupBy(r => r.BatchNumber).ToList().ForEach(g =>
{ {
ListLote.Add(new BE_StockTransferLineBatchNumber() ListLote.Add(new BE_StockTransferLineBatchNumber()
{ {
BatchNumber = r.BatchNumber, BatchNumber = g.Key,
Quantity = r.Quantity Quantity = g.Sum(r => r.Quantity)
}); });
}); });
Transfer.StockTransferLines.Add(new BE_StockTransferLine() Transfer.StockTransferLines.Add(new BE_StockTransferLine()
@@ -66,7 +66,7 @@ namespace Net.Data.WMS.SolicitudTransferencia
await connectServiceLayer.PatchNoContentAsync( await connectServiceLayer.PatchNoContentAsync(
$"InventoryTransferRequests({docEntry})", $"InventoryTransferRequests({docEntry})",
new { StockTransferLines = lines }, new { U_SYP_MDTS = "TSE", StockTransferLines = lines },
new Dictionary<string, string> { ["B1S-ReplaceCollectionsOnPatch"] = "true" }); new Dictionary<string, string> { ["B1S-ReplaceCollectionsOnPatch"] = "true" });
return null; return null;
+58 -14
View File
@@ -22,11 +22,51 @@ 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), 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**. 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 ## Resumen del contrato
| Método | Ruta | Cambia hoy | Qué hace | | 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` | **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/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. | | `POST` | `/api/picking/hojaruta/{docEntry}/cerrar` | **Nuevo** | Cierra el UDO `ARD_PHR` completo en SAP. Sin body. |
@@ -36,7 +76,7 @@ y al final cierra la hoja de ruta completa **una sola vez**.
**Archivo**: `Net.Business.Entities/WMS/Picking/BE_PickingRequest.cs` — agregar: **Archivo**: `Net.Business.Entities/WMS/Picking/BE_PickingRequest.cs` — agregar:
```csharp ```csharp
public string Notificacion { get; set; } // "preliminar" | "cierre"; cualquier otro valor o ausente = "preliminar" 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 **Archivo**: `Net.Business.DTO/WMS/Picking/Dto_PickingRequest.cs` — agregar la
@@ -73,8 +113,10 @@ alcanza con un `if` adentro del mismo método).
HelperProcesoPiciking.Set(entidad.Proceso, entidad.IdEstadoCubico, entidad.DocEntry, entidad.CodMotivo, Proceso.Code); HelperProcesoPiciking.Set(entidad.Proceso, entidad.IdEstadoCubico, entidad.DocEntry, entidad.CodMotivo, Proceso.Code);
// <-- linea 96 actual, sin tocar // <-- linea 96 actual, sin tocar
// === NUEVO: notificacion preliminar (diurna) - nunca toca SAP // === Solo ULTIMA MILLA: notificacion preliminar (diurna) - nunca toca SAP.
if (!string.Equals(entidad.Notificacion, "cierre", StringComparison.OrdinalIgnoreCase)) // 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); return resultadoTransaccion.GetInformacionResultado(entidad.DocEntry, entidad.NumOrden, "Estado actualizado correctamente", entidad);
} }
@@ -85,8 +127,10 @@ if (!string.Equals(entidad.Notificacion, "cierre", StringComparison.OrdinalIgnor
``` ```
Es decir: **todo lo que hoy hace `Create()` (líneas 45-341 completas)** sigue 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 siendo exactamente la lógica de cierre — no se toca nada de ahí. Solo se
de ahí. Solo se agrega el `if` de corte después de la línea 96. 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 > **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 > validación de arriba lo necesita para desambiguar `Existe > 1`), así que no
@@ -96,15 +140,15 @@ de ahí. Solo se agrega el `if` de corte después de la línea 96.
### Bodies de ejemplo ### Bodies de ejemplo
**Notificación preliminar** (reparto en curso — `Notificacion` ausente o **Notificación preliminar** (solo `ULTIMA MILLA`, reparto en curso —
distinto de `"cierre"`): `Notificacion` ausente o distinto de `"cierre"`):
```json ```json
{ {
"numOrden": "OT-2026-001", "numOrden": "OT-2026-001",
"docEntry": 500, "docEntry": 500,
"proceso": "DESPACHO", "proceso": "ULTIMA MILLA",
"codMotivo": "M01", "codMotivo": "M01",
"idEstadoCubico": 6, "idEstadoCubico": 23,
"codigoAlmacen": "ALM01", "codigoAlmacen": "ALM01",
"detalle": [ "detalle": [
{ "item": 0, "codigoArticulo": "ART-001", "cantidadPedida": 3, "estado": "C" } { "item": 0, "codigoArticulo": "ART-001", "cantidadPedida": 3, "estado": "C" }
@@ -112,15 +156,15 @@ distinto de `"cierre"`):
} }
``` ```
**Cierre** (mismo shape, agrega `notificacion: "cierre"` — crea el documento **Cierre** (`ULTIMA MILLA`, mismo shape, agrega `notificacion: "cierre"` —
SAP real, misma respuesta que hoy): crea el documento SAP real, misma respuesta que hoy):
```json ```json
{ {
"numOrden": "OT-2026-001", "numOrden": "OT-2026-001",
"docEntry": 500, "docEntry": 500,
"proceso": "DESPACHO", "proceso": "ULTIMA MILLA",
"codMotivo": "M01", "codMotivo": "M01",
"idEstadoCubico": 6, "idEstadoCubico": 23,
"codigoAlmacen": "ALM01", "codigoAlmacen": "ALM01",
"notificacion": "cierre", "notificacion": "cierre",
"detalle": [ "detalle": [