Compare commits
10
Commits
b6d7767828
...
d0118d1848
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0118d1848 | ||
|
|
0a2dc12305 | ||
|
|
4ad0754ba0 | ||
|
|
e5ea57b673 | ||
|
|
1ce0da7735 | ||
|
|
ded1638cd0 | ||
|
|
14fffa8ba4 | ||
|
|
6386070f4e | ||
|
|
e475b7f48c | ||
|
|
e31524ab6b |
No files matched your search
@@ -16,11 +16,65 @@ Detalle técnico en `docs/PORTE_FUNCIONALIDAD_NUEVA.md`,
|
||||
1. **SolicitudTransferencia**: `PATCH /api/SolicitudTransferencia/{docEntry}` y
|
||||
`POST /api/SolicitudTransferencia/{docEntry}/cerrar`.
|
||||
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`.
|
||||
3. **`ConnectionServiceLayer`**: `PatchNoContentAsync` y `PostActionAsync`
|
||||
(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
|
||||
|
||||
- **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_MDCD { 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>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<AssemblyName>ITCA_API_SAP_WMS</AssemblyName>
|
||||
<Version>2.1.0</Version>
|
||||
<AssemblyVersion>2.1.0.0</AssemblyVersion>
|
||||
<FileVersion>2.1.0.0</FileVersion>
|
||||
<Version>2.1.7</Version>
|
||||
<AssemblyVersion>2.1.7.0</AssemblyVersion>
|
||||
<FileVersion>2.1.7.0</FileVersion>
|
||||
<UserSecretsId>24763424-3300-4c85-87e3-503f8fdebd14</UserSecretsId>
|
||||
<!-- 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
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace Net.Business.Services
|
||||
{
|
||||
Configuration = 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; }
|
||||
|
||||
@@ -19,6 +19,16 @@ namespace Net.Connection.ServiceLayer
|
||||
public static bool ServicioActivo = false;
|
||||
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 _company;
|
||||
private readonly string _user;
|
||||
@@ -464,6 +474,7 @@ namespace Net.Connection.ServiceLayer
|
||||
var estado = (int)response_.StatusCode;
|
||||
responseBody.CodigoHttp = estado.ToString();
|
||||
responseBody.Mensaje = "Services Unauthorized : " + _responseLoginServiceLayer.error.message.ToString();
|
||||
LogErrorEnvio(Query, obj, responseBody.Mensaje);
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
@@ -493,6 +504,7 @@ namespace Net.Connection.ServiceLayer
|
||||
var estado = (int)response_.StatusCode;
|
||||
responseBody.CodigoHttp = estado.ToString();
|
||||
responseBody.Mensaje = "Error Services : " + _responseLoginServiceLayer.error.message.ToString();
|
||||
LogErrorEnvio(Query, obj, responseBody.Mensaje);
|
||||
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
|
||||
{
|
||||
// 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;
|
||||
var estado = (int)response_.StatusCode;
|
||||
responseBody.CodigoHttp = estado.ToString();
|
||||
@@ -1068,6 +1084,7 @@ namespace Net.Connection.ServiceLayer
|
||||
var estado = (int)response_.StatusCode;
|
||||
//responseBody.CodigoHttp = estado.ToString();
|
||||
responseBody.Mensaje = "Services Unauthorized : " + _responseLoginServiceLayer.error.message.ToString();
|
||||
LogErrorEnvio(Query, obj, responseBody.Mensaje);
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
@@ -1092,6 +1109,7 @@ namespace Net.Connection.ServiceLayer
|
||||
var estado = (int)response_.StatusCode;
|
||||
//responseBody.CodigoHttp = estado.ToString();
|
||||
responseBody.Mensaje = "Error Services : " + _responseLoginServiceLayer.error.message.ToString();
|
||||
LogErrorEnvio(Query, obj, responseBody.Mensaje);
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
@@ -1159,7 +1177,9 @@ namespace Net.Connection.ServiceLayer
|
||||
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_InvoicesConsulta>> GetInvoice(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)
|
||||
{
|
||||
_metodoName = regex.Match(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name).Groups[1].Value.ToString();
|
||||
|
||||
@@ -120,11 +120,12 @@ namespace Net.Data.WMS.Picking
|
||||
//=== REALIZA PROCESO COMPLETO DE ACTUALIZACION
|
||||
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
|
||||
// estado en HANA (ya hecho arriba), NUNCA debe crear documentos en SAP.
|
||||
// Default seguro: cualquier valor que no sea "cierre" exacto cae aca.
|
||||
if (!string.Equals(entidad.Notificacion, "cierre", StringComparison.OrdinalIgnoreCase))
|
||||
// DESPACHO, PICKING y demas procesos no dependen de este campo.
|
||||
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);
|
||||
}
|
||||
@@ -162,7 +163,7 @@ namespace Net.Data.WMS.Picking
|
||||
var Response_Reopen = invoicestRepository.ReOpen(_invoices.DocEntry).Result;
|
||||
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
|
||||
// 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 =>
|
||||
{
|
||||
#region CONSULTA A HANA
|
||||
|
||||
//List<BE_ItemStock> ListaStock = HelperStock.Get(line.ItemCode, entidad.CodigoAlmacen);
|
||||
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;
|
||||
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
|
||||
});
|
||||
|
||||
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)
|
||||
{
|
||||
var result_creditNotesRepository = _creditNotesRepository.SetCreateCreditNotes(creditNotesNotes).Result;
|
||||
@@ -277,12 +318,13 @@ namespace Net.Data.WMS.Picking
|
||||
ListaUnicaArticulo.ForEach(x =>
|
||||
{
|
||||
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()
|
||||
{
|
||||
BatchNumber = r.BatchNumber,
|
||||
Quantity = r.Quantity
|
||||
BatchNumber = g.Key,
|
||||
Quantity = g.Sum(r => r.Quantity)
|
||||
});
|
||||
});
|
||||
Transfer.StockTransferLines.Add(new BE_StockTransferLine()
|
||||
@@ -481,7 +523,7 @@ namespace Net.Data.WMS.Picking
|
||||
var Response_Reopen = invoicestRepository.ReOpen(_invoices.DocEntry).Result;
|
||||
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 =>
|
||||
{
|
||||
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()
|
||||
{
|
||||
@@ -572,12 +615,13 @@ namespace Net.Data.WMS.Picking
|
||||
ListaUnicaArticulo.ForEach(x =>
|
||||
{
|
||||
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()
|
||||
{
|
||||
BatchNumber = r.BatchNumber,
|
||||
Quantity = r.Quantity
|
||||
BatchNumber = g.Key,
|
||||
Quantity = g.Sum(r => r.Quantity)
|
||||
});
|
||||
});
|
||||
Transfer.StockTransferLines.Add(new BE_StockTransferLine()
|
||||
|
||||
@@ -403,7 +403,8 @@ namespace Net.Data.WMS.Recibo
|
||||
}
|
||||
|
||||
#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);
|
||||
creditNotesNotes.DocCurrency = returnRequest.DocCurrency;
|
||||
creditNotesNotes.CardCode = returnRequest.CardCode;
|
||||
@@ -456,6 +457,10 @@ namespace Net.Data.WMS.Recibo
|
||||
//=== REALIZA DISTRIBUCION DE STOCK POR LOTE
|
||||
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
|
||||
|
||||
var ListaStock = HelperStock.Get(line.ItemCode, line.WarehouseCode);
|
||||
@@ -492,6 +497,7 @@ namespace Net.Data.WMS.Recibo
|
||||
{
|
||||
creditNotesNotes.DocumentLines.ForEach(line =>
|
||||
{
|
||||
line.BatchNumbers.Clear();
|
||||
var ReferenciaFactura = returnRequest.DocumentLines.FirstOrDefault(t => t.LineNum == line.LineNum);
|
||||
var ListaStock = _Invoice.DocumentLines.FirstOrDefault(InvoiceLine => InvoiceLine.LineNum == ReferenciaFactura.BaseLine).BatchNumbers;
|
||||
if (ListaStock.Count > 0)
|
||||
@@ -556,12 +562,13 @@ namespace Net.Data.WMS.Recibo
|
||||
ListaUnicaArticulo.ForEach(x =>
|
||||
{
|
||||
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()
|
||||
{
|
||||
BatchNumber = r.BatchNumber,
|
||||
Quantity = r.Quantity
|
||||
BatchNumber = g.Key,
|
||||
Quantity = g.Sum(r => r.Quantity)
|
||||
});
|
||||
});
|
||||
Transfer.StockTransferLines.Add(new BE_StockTransferLine()
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace Net.Data.WMS.SolicitudTransferencia
|
||||
|
||||
await connectServiceLayer.PatchNoContentAsync(
|
||||
$"InventoryTransferRequests({docEntry})",
|
||||
new { StockTransferLines = lines },
|
||||
new { U_SYP_MDTS = "TSE", StockTransferLines = lines },
|
||||
new Dictionary<string, string> { ["B1S-ReplaceCollectionsOnPatch"] = "true" });
|
||||
|
||||
return null;
|
||||
|
||||
+58
-14
@@ -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),
|
||||
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` | `"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/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:
|
||||
```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
|
||||
@@ -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);
|
||||
// <-- linea 96 actual, sin tocar
|
||||
|
||||
// === NUEVO: notificacion preliminar (diurna) - nunca toca SAP
|
||||
if (!string.Equals(entidad.Notificacion, "cierre", StringComparison.OrdinalIgnoreCase))
|
||||
// === 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);
|
||||
}
|
||||
@@ -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
|
||||
siendo exactamente la lógica de `notificacion == "cierre"` — no se toca nada
|
||||
de ahí. Solo se agrega el `if` de corte después de la línea 96.
|
||||
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
|
||||
@@ -96,15 +140,15 @@ de ahí. Solo se agrega el `if` de corte después de la línea 96.
|
||||
|
||||
### Bodies de ejemplo
|
||||
|
||||
**Notificación preliminar** (reparto en curso — `Notificacion` ausente o
|
||||
distinto de `"cierre"`):
|
||||
**Notificación preliminar** (solo `ULTIMA MILLA`, reparto en curso —
|
||||
`Notificacion` ausente o distinto de `"cierre"`):
|
||||
```json
|
||||
{
|
||||
"numOrden": "OT-2026-001",
|
||||
"docEntry": 500,
|
||||
"proceso": "DESPACHO",
|
||||
"proceso": "ULTIMA MILLA",
|
||||
"codMotivo": "M01",
|
||||
"idEstadoCubico": 6,
|
||||
"idEstadoCubico": 23,
|
||||
"codigoAlmacen": "ALM01",
|
||||
"detalle": [
|
||||
{ "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
|
||||
SAP real, misma respuesta que hoy):
|
||||
**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": "DESPACHO",
|
||||
"proceso": "ULTIMA MILLA",
|
||||
"codMotivo": "M01",
|
||||
"idEstadoCubico": 6,
|
||||
"idEstadoCubico": 23,
|
||||
"codigoAlmacen": "ALM01",
|
||||
"notificacion": "cierre",
|
||||
"detalle": [
|
||||
|
||||
Reference in new issue
Block a user