Compare commits

7 Commits
Author SHA1 Message Date
GeorgeandClaude Sonnet 5.5 3111bb3842 Documentar la transferencia desde ALM99 (docs/TRANSFERENCIA_IMPORTACION.md) y comentar el codigo
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 07:38:01 -05:00
GeorgeandClaude Sonnet 5.5 d53d5d07c5 csproj: Version 2.1.9
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 00:22:16 -05:00
GeorgeandClaude Sonnet 5.5 baaf85554a Transferencia desde ALM99: elegir la entrada de la factura de reserva por articulos/cantidades y que no tenga transferencia. Version 2.1.9.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 00:22:11 -05:00
GeorgeandClaude Sonnet 5.5 31b0929342 ConnectionServiceLayer: PostAsync, PostReOpen y PostClose comparten un nucleo (EnviarPostAsync)
Eran tres copias (94% a 97% iguales). Se verifico con un servidor simulado
que el resultado es identico al anterior en reintento por 401, 204 sin cuerpo,
rechazo y log de errores. PatchAsyncSBA no se toca.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:58:12 -05:00
GeorgeandClaude Sonnet 5.5 3ea0aca535 Unificar la transferencia automatica de la NC en TransferenciaAutomaticaNC.Crear
El mismo bloque (~100 lineas) estaba copiado en Picking DESPACHO, Picking PICKING
y Recibo. Ahora los tres llaman al mismo metodo. Sin cambios de comportamiento
(la referencia de la NC en la transferencia se asigna una vez antes de las lineas
en vez de dentro del ciclo de articulos).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:52:53 -05:00
GeorgeandClaude Sonnet 5.5 3bce3ed117 Quitar dos copias exactas sin llamadores: HelperHojaRuta.GetConsolidado y HRRepository.UpdateCreateCreditNotes
Quedan HelperStock.GetConsolidado y CreditNotesRepository.UpdateCreateCreditNotes,
que son los que usan Picking, Inventario y Recibo. Sin cambios de comportamiento.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:51:11 -05:00
GeorgeandClaude Sonnet 5.5 ac725aed9b ConnectionServiceLayer: unificar LoginPost en Login (eran identicos)
Las 4 llamadas a LoginPost() ahora llaman a Login(). Sin cambios de comportamiento.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:49:20 -05:00
14 changed files with 479 additions and 744 deletions

No files matched your search

+35 -7
View File
@@ -10,7 +10,7 @@ comprimido (versión antigua)**.
## Versión y control de versiones
- **Versión actual: 2.1.8.0** (rama `limpieza-2.1.8.0`; `main` = 2.1.7.0) (`Net.Business.Services.csproj`: `Version`,
- **Versión actual: 2.1.9.0** (rama `limpieza-2.1.8.0`; `main` = 2.1.7.0) (`Net.Business.Services.csproj`: `Version`,
`AssemblyVersion`, `FileVersion`). Cada versión tiene su commit; al publicar
se sube la versión (ver `publicar.ps1`).
- **Git**: rama `main`, remoto `origin` =
@@ -93,8 +93,37 @@ transferencias siempre, sin depender de `Notificacion`. Detalle y tabla en
`TotalDetalle_Parcial`/`_Invoice`/`SerieOrigen` y un `if` vacío en
`CreateHojaRuta`) y se agregaron comentarios en Picking, Recibo,
`ConnectionServiceLayer`, `CreditNotesRepository`, `InvoiceRepository` y
`RepositoryWrapper`. Sigue pendiente: código comentado, `Login`/`LoginPost`
duplicados y `PatchAsyncSBA`.
`RepositoryWrapper`. Además se unificó `LoginPost()` en `Login()` (eran idénticos;
las 4 llamadas a `LoginPost` ahora llaman a `Login`) y se quitaron dos copias
exactas sin llamadores: `HelperHojaRuta.GetConsolidado` (queda `HelperStock.GetConsolidado`)
y `HRRepository.UpdateCreateCreditNotes` (queda `CreditNotesRepository`). Sigue pendiente: código
comentado, `PatchAsyncSBA`, repositorios de compras sin uso
(`SetCreatePurchaseInvoices` duplicado en `PurchaseOrdersRepository`) y el resto de la redundancia (serie `BC10`/`FC10` y cabecera de la NC).
`PostAsync`, `PostReOpen` y `PostClose` de `ConnectionServiceLayer` comparten ahora un
núcleo privado (`EnviarPostAsync`: con/sin cuerpo, acepta o no el 204); se verificó con un
servidor simulado que el resultado es idéntico al anterior (reintento por 401, 204 sin
cuerpo, rechazo y log). `PatchAsyncSBA` sigue aparte: su manejo de errores es distinto y
mantiene su bug del 204 (sin marcar éxito; el único llamador ignora el resultado).
La transferencia automática de stock de la NC (3 copias en Picking x2 y Recibo)
pasó a un solo método: `Net.Data/ServicesLayer/StockTransfer/TransferenciaAutomaticaNC.Crear`.
- **v2.1.9.0 — Transferencia desde ALM99 (importaciones)**: WMS recibe por partes; cada
entrada de mercadería (base: factura de reserva, `BaseType 18`) va a `ALM99` y, tras el
costeo, WMS notifica **una transferencia por entrada** enviando en `NumOrden` el `DocNum`
de la factura de reserva y en el detalle los artículos/cantidades de esa entrada. Si el
JSON no trae `DocEntryEM`, `TransferenciaRepository` busca por HANA
(`HelperInvoice.GetEntradasPorFactura`: `OPCH.isIns='Y'` → `PDN1` → `OPDN`, sin anuladas;
una entrada ya tiene transferencia si la línea tiene `TargetType 67` y su `OWTR` no está
anulada), compara **solo `ItemCode` y cantidad** (sumada por artículo) contra cada entrada y
transfiere la más antigua que coincida y no esté transferida (`EntradaImportacion.Elegir`).
Las líneas de la transferencia salen de la entrada (lotes reales, validando que cubran la
cantidad) con base en ella. Errores claros: ninguna coincide / todas ya transferidas. Si la
factura no tiene entradas se mantiene el comportamiento anterior (lotes del stock de HANA,
sin amarre). Con `DocEntryEM` informado se respeta (antes reventaba con referencia nula si
la entrada o la línea no existían). Sin probar contra SAP: validar la consulta, que
`isIns='Y'` sea factura de reserva y que SAP acepte `BaseType "PurchaseDeliveryNotes"` en
la línea de `StockTransfers`. Detalle, errores y diagnóstico en
`docs/TRANSFERENCIA_IMPORTACION.md`. Probado con datos reales (SQL Server) solo la consulta: entradas
de 1 y 2 líneas, y una transferencia cancelada libera la entrada.
- 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.
@@ -148,7 +177,7 @@ Picking, columna `HORA` en `B_Log`. Lo que **no** se trajo: `Chatbots` y
- `BE_ResultadoTransaccion<T>.NombreEstacion`: setter con recursión infinita
(bomba de tiempo; hoy nada lo asigna).
- `ConnectionServiceLayer.PatchAsyncSBA<T>`: con un `204` real devuelve `null`
sin marcar éxito. `Login()`/`LoginPost()` duplicados.
sin marcar éxito.
- El cliente HTTP `bypass-ssl-validation` acepta cualquier certificado TLS hacia
el Service Layer (se quitó el método `ValidarCertificado`, que no se usaba).
- `appsettings.json` (fuera de git, en `.gitignore`) contiene credenciales
@@ -159,9 +188,8 @@ Picking, columna `HORA` en `B_Log`. Lo que **no** se trajo: `Chatbots` y
`ITC_WMS_SAP` (`ITCA_LOG_INTEGRACION`), no en `LocalData.Schema`; si la tabla
está en otro esquema el `INSERT` falla. `B_Log.Add` no tiene `try/catch`, así
que un fallo del log puede devolver 500 tras haber creado el documento en SAP.
- En Picking y Recibo, si la transferencia automática de stock falla, el `if`
de error está vacío: el fallo se ignora (la NC ya está creada) y no se informa
al consumidor.
- Si una transferencia automática de stock falla (`TransferenciaAutomaticaNC.Crear`),
el fallo se ignora (la NC ya está creada) y no se informa al consumidor.
## Despliegue
@@ -0,0 +1,14 @@
namespace Net.Business.Entities.WMS.Transferencia
{
// Una linea de una Entrada de Mercaderia enlazada a una Factura de Reserva de proveedores (importacion).
// TransferenciaDocEntry > 0 cuando la linea ya tiene una transferencia de stock vigente (TargetType 67, no anulada).
public class BE_EntradaImportacionLinea
{
public int DocEntry { get; set; }
public int DocNum { get; set; }
public int LineNum { get; set; }
public string ItemCode { get; set; }
public decimal Quantity { get; set; }
public int TransferenciaDocEntry { get; set; }
}
}
@@ -3,9 +3,9 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>ITCA_API_SAP_WMS</AssemblyName>
<Version>2.1.8</Version>
<AssemblyVersion>2.1.8.0</AssemblyVersion>
<FileVersion>2.1.8.0</FileVersion>
<Version>2.1.9</Version>
<AssemblyVersion>2.1.9.0</AssemblyVersion>
<FileVersion>2.1.9.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
@@ -136,95 +136,6 @@ namespace Net.Connection.ServiceLayer
return _responseLoginServiceLayer;
}
}
// Igual que Login(); duplicado historico (candidato a unificar)
public async Task<ResponseLoginServiceLayer> LoginPost()
{
try
{
// CONSTRUIMOS LA URL DE LA ACCIÓN
var urlBuilder_ = new StringBuilder();
urlBuilder_.Append(_url != null ? _url.TrimEnd('/') : "")
.Append("/Login");
var url_ = urlBuilder_.ToString();
// RECUPERAMOS EL HttpClient
var client_ = _httpClient.CreateClient("bypass-ssl-validation");
using (var request_ = new HttpRequestMessage())
{
///////////////////////////////////////
// CONSTRUIMOS LA PETICIÓN (REQUEST) //
///////////////////////////////////////
// DEFINIMOS EL Content CON EL OBJETO A ENVIAR SERIALIZADO.
request_.Content = new StringContent("{\"CompanyDB\":\"" + _company + "\",\"Password\":\"" + _password + "\",\"UserName\":\"" + _user + "\"}");
//request_.Content = new StringContent("{\"CompanyDB\":\"" + _company + "\",\"Password\":\"" + _password + "\",\"UserName\":\"" + _user + "\",\"Language\":\"" + _language + "\"}");
// DEFINIMOS EL ContentType, EN ESTE CASO ES "application/json"
request_.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
request_.Headers.Add("cache-control", "no-cache");
// DEFINIMOS EL MÉTODO HTTP
request_.Method = new HttpMethod("POST");
// DEFINIMOS LA URI
request_.RequestUri = new Uri(url_, System.UriKind.RelativeOrAbsolute);
// DEFINIMOS EL Accept, EN ESTE CASO ES "application/json"
request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
/////////////////////////////////////////
// CONSTRUIMOS LA RESPUESTA (RESPONSE) //
/////////////////////////////////////////
// Utilizamos ConfigureAwait(false) para evitar el DeadLock.
var response_ = await client_.SendAsync(request_).ConfigureAwait(false);
// OBTENEMOS EL Content DEL RESPONSE como un String
// Utilizamos ConfigureAwait(false) para evitar el DeadLock.
var responseText_ = await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
// SI ES LA RESPUESTA ESPERADA !! ...
if (response_.StatusCode == System.Net.HttpStatusCode.OK) // 200
{
// DESERIALIZAMOS Content DEL RESPONSE
_responseLoginServiceLayer = JsonConvert.DeserializeObject<ResponseLoginServiceLayer>(responseText_);
_responseLoginServiceLayer.ServicioActivo = true;
_responseLoginServiceLayer.MensajeLogin = "Autenticación Correcta";
return _responseLoginServiceLayer;
}
else
// SI NO SE ESTÁ AUTORIZADO ...
if (response_.StatusCode == System.Net.HttpStatusCode.Unauthorized) // 401
{
_responseLoginServiceLayer = JsonConvert.DeserializeObject<ResponseLoginServiceLayer>(responseText_);
_responseLoginServiceLayer.ServicioActivo = false;
_responseLoginServiceLayer.MensajeLogin = "401 Unauthorized. Las credenciales de acceso del usuario son incorrectas.";
return _responseLoginServiceLayer;
}
else
// CUALQUIER OTRA RESPUESTA ...
if (response_.StatusCode != System.Net.HttpStatusCode.OK && // 200
response_.StatusCode != System.Net.HttpStatusCode.NoContent) // 204
{
_responseLoginServiceLayer = JsonConvert.DeserializeObject<ResponseLoginServiceLayer>(responseText_);
_responseLoginServiceLayer.ServicioActivo = false;
_responseLoginServiceLayer.MensajeLogin = "401 Unauthorized. Las credenciales de acceso del usuario son incorrectas.";
return _responseLoginServiceLayer;
}
// RETORNAMOS EL OBJETO POR DEFECTO ESPERADO
return _responseLoginServiceLayer;
}
}
catch (Exception ex)
{
_responseLoginServiceLayer.ServicioActivo = false;
_responseLoginServiceLayer.MensajeLogin = ex.Message.ToString();
return _responseLoginServiceLayer;
}
}
// GET al Service Layer. dinamico = true: la respuesta es una sola entidad (ej. Invoices(123)); false: coleccion { value: [...] }
public async Task<List<T>> GetAsync<T>(string Query, int maxpagesize,bool dinamico = false)
{
@@ -307,140 +218,29 @@ namespace Net.Connection.ServiceLayer
// PASAMOS LA EXCEPCIÓN A LA APP.
}
}
// POST con cuerpo JSON. Reintenta una vez si la sesion expiro (401). Si SAP rechaza, devuelve Exito = false y guarda
// el JSON enviado con ErrorLogger (tabla ITCA_LOG_INTEGRACION)
public async Task<T> PostAsync<T>(string Query, object obj)
// POST con cuerpo JSON. Crea un documento en SAP. Si SAP rechaza, devuelve Exito = false y guarda el JSON enviado
// con ErrorLogger (tabla ITCA_LOG_INTEGRACION)
public Task<T> PostAsync<T>(string Query, object obj)
{
try
{
// CONSTRUIMOS LA URL DE LA ACCIÓN
var urlBuilder_ = new StringBuilder();
urlBuilder_.Append(_url != null ? _url.TrimEnd('/') : "")
.Append("/" + Query);
var url_ = urlBuilder_.ToString();
// RECUPERAMOS EL HttpClient
var client_ = _httpClient.CreateClient("bypass-ssl-validation");
int NumError = 0;
band:
using (var request_ = new HttpRequestMessage())
{
///////////////////////////////////////
// CONSTRUIMOS LA PETICIÓN (REQUEST) //
///////////////////////////////////////
// DEFINIMOS EL MÉTODO HTTP GET
request_.Method = new HttpMethod("POST");
// DEFINIMOS LA URI
request_.RequestUri = new Uri(url_, System.UriKind.RelativeOrAbsolute);
// DEFINIMOS EL Accept, EN ESTE CASO ES "application/json"
request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
//request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("odata=minimalmetadata"));
//request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("charset=utf-8"));
var a = JsonConvert.SerializeObject(obj);
request_.Content = new StringContent(JsonConvert.SerializeObject(obj), System.Text.Encoding.UTF8, "application/json");
// ASIGNAMOS A LA CABECERA DE LA PETICIÓN EL TOKEN JWT.
if (_responseLoginServiceLayer.ServicioActivo)
request_.Headers.Add("Cookie", "B1SESSION=" + _responseLoginServiceLayer.SessionId);
/////////////////////////////////////////
// CONSTRUIMOS LA RESPUESTA (RESPONSE) //
/////////////////////////////////////////
// Utilizamos ConfigureAwait(false) para evitar el DeadLock.
var response_ = await client_.SendAsync(request_, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
// OBTENEMOS EL Content DEL RESPONSE como un String
// Utilizamos ConfigureAwait(false) para evitar el DeadLock.
var responseText_ = await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
// SI ES LA RESPUESTA ESPERADA !! ...
if (response_.StatusCode == System.Net.HttpStatusCode.OK || response_.StatusCode == System.Net.HttpStatusCode.Created) // 200 or Created
{
// DESERIALIZAMOS Content DEL RESPONSE
dynamic responseBody = JsonConvert.DeserializeObject<T>(responseText_);
responseBody.Exito = true;
var estado = (int)response_.StatusCode;
responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "SE ENVIO CORRECTAMENTE A SAP BO ";
return responseBody;
}
else
// SI NO SE ESTÁ AUTORIZADO ...
if (response_.StatusCode == System.Net.HttpStatusCode.Unauthorized) // 401
{
NumError += 1;
_responseLoginServiceLayer = JsonConvert.DeserializeObject<ResponseLoginServiceLayer>(responseText_);
_responseLoginServiceLayer.ServicioActivo = false;
if (NumError == 2)
{
dynamic responseBody = JsonConvert.DeserializeObject<T>(responseText_);
responseBody.Exito = false;
var estado = (int)response_.StatusCode;
responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "Services Unauthorized : " + _responseLoginServiceLayer.error.message.ToString();
LogErrorEnvio(Query, obj, responseBody.Mensaje);
return responseBody;
}
if (_responseLoginServiceLayer.error.code.ToString() == "301")
{
await LoginPost();
}
goto band;
}
else
// CUALQUIER OTRA RESPUESTA ...
if (response_.StatusCode != System.Net.HttpStatusCode.OK && // 200
response_.StatusCode != System.Net.HttpStatusCode.NoContent) // 204
{
NumError += 1;
_responseLoginServiceLayer = JsonConvert.DeserializeObject<ResponseLoginServiceLayer>(responseText_);
_responseLoginServiceLayer.ServicioActivo = false;
if (NumError == 2)
{
dynamic responseBody = JsonConvert.DeserializeObject<T>(responseText_);
responseBody.Exito = false;
var estado = (int)response_.StatusCode;
responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "Error Services : " + _responseLoginServiceLayer.error.message.ToString();
LogErrorEnvio(Query, obj, responseBody.Mensaje);
return responseBody;
}
if (_responseLoginServiceLayer.error.code.ToString() == "301")
{
await Login();
}
goto band;
}
// RETORNAMOS EL OBJETO POR DEFECTO ESPERADO
return default(T);
}
}
finally
{
// NO UTILIZAMOS CATCH,
// PASAMOS LA EXCEPCIÓN A LA APP.
}
return EnviarPostAsync<T>(Query, obj, true, false);
}
// POST sin cuerpo para reabrir un documento (ej. Invoices(n)/Reopen). SAP responde 204 sin contenido y se toma como exito
public async Task<T> PostReOpen<T>(string Query)
public Task<T> PostReOpen<T>(string Query)
{
return EnviarPostAsync<T>(Query, null, false, true);
}
// POST sin cuerpo para cerrar un documento (.../Close). SAP responde 204 sin contenido y se toma como exito
public Task<T> PostClose<T>(string Query)
{
return EnviarPostAsync<T>(Query, null, false, true);
}
// Nucleo comun de los envios POST al Service Layer (PostAsync, PostReOpen, PostClose). Reintenta una vez si la sesion
// expiro (401). conCuerpo: envia obj como JSON y, si SAP rechaza, guarda el JSON enviado con ErrorLogger.
// aceptaSinContenido: toma el 204 (sin cuerpo) como exito.
private async Task<T> EnviarPostAsync<T>(string Query, object obj, bool conCuerpo, bool aceptaSinContenido)
{
try
@@ -473,8 +273,8 @@ namespace Net.Connection.ServiceLayer
request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
//request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("odata=minimalmetadata"));
//request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("charset=utf-8"));
//var a = JsonConvert.SerializeObject(obj);
//request_.Content = new StringContent(JsonConvert.SerializeObject(obj), System.Text.Encoding.UTF8, "application/json");
if (conCuerpo)
request_.Content = new StringContent(JsonConvert.SerializeObject(obj), System.Text.Encoding.UTF8, "application/json");
// ASIGNAMOS A LA CABECERA DE LA PETICIÓN EL TOKEN JWT.
if (_responseLoginServiceLayer.ServicioActivo)
@@ -491,12 +291,11 @@ namespace Net.Connection.ServiceLayer
var responseText_ = await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
// SI ES LA RESPUESTA ESPERADA !! ...
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 || (aceptaSinContenido && response_.StatusCode == System.Net.HttpStatusCode.NoContent)) // 200, Created o 204
{
// DESERIALIZAMOS Content DEL RESPONSE
// 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_)
// Un 204 (Reopen/Close) viene sin cuerpo: Deserialize devolveria null, asi que se crea una instancia vacia
dynamic responseBody = (aceptaSinContenido && string.IsNullOrWhiteSpace(responseText_))
? Activator.CreateInstance<T>()
: JsonConvert.DeserializeObject<T>(responseText_);
responseBody.Exito = true;
@@ -522,38 +321,11 @@ namespace Net.Connection.ServiceLayer
var estado = (int)response_.StatusCode;
responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "Services Unauthorized : " + _responseLoginServiceLayer.error.message.ToString();
if (conCuerpo) LogErrorEnvio(Query, obj, responseBody.Mensaje);
return responseBody;
}
if (_responseLoginServiceLayer.error.code.ToString() == "301")
{
await LoginPost();
}
goto band;
}
else
// CUALQUIER OTRA RESPUESTA ...
if (response_.StatusCode != System.Net.HttpStatusCode.OK && // 200
response_.StatusCode != System.Net.HttpStatusCode.NoContent) // 204
{
NumError += 1;
_responseLoginServiceLayer = JsonConvert.DeserializeObject<ResponseLoginServiceLayer>(responseText_);
_responseLoginServiceLayer.ServicioActivo = false;
if (NumError == 2)
{
dynamic responseBody = JsonConvert.DeserializeObject<T>(responseText_);
responseBody.Exito = false;
var estado = (int)response_.StatusCode;
responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "Error Services : " + _responseLoginServiceLayer.error.message.ToString();
return responseBody;
}
if (_responseLoginServiceLayer.error.code.ToString() == "301")
{
await Login();
@@ -561,107 +333,6 @@ namespace Net.Connection.ServiceLayer
goto band;
}
// RETORNAMOS EL OBJETO POR DEFECTO ESPERADO
return default(T);
}
}
finally
{
// NO UTILIZAMOS CATCH,
// PASAMOS LA EXCEPCIÓN A LA APP.
}
}
// POST sin cuerpo para cerrar un documento (.../Close)
public async Task<T> PostClose<T>(string Query)
{
try
{
// CONSTRUIMOS LA URL DE LA ACCIÓN
var urlBuilder_ = new StringBuilder();
urlBuilder_.Append(_url != null ? _url.TrimEnd('/') : "")
.Append("/" + Query);
var url_ = urlBuilder_.ToString();
// RECUPERAMOS EL HttpClient
var client_ = _httpClient.CreateClient("bypass-ssl-validation");
int NumError = 0;
band:
using (var request_ = new HttpRequestMessage())
{
///////////////////////////////////////
// CONSTRUIMOS LA PETICIÓN (REQUEST) //
///////////////////////////////////////
// DEFINIMOS EL MÉTODO HTTP GET
request_.Method = new HttpMethod("POST");
// DEFINIMOS LA URI
request_.RequestUri = new Uri(url_, System.UriKind.RelativeOrAbsolute);
// DEFINIMOS EL Accept, EN ESTE CASO ES "application/json"
request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
//request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("odata=minimalmetadata"));
//request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("charset=utf-8"));
//var a = JsonConvert.SerializeObject(obj);
//request_.Content = new StringContent(JsonConvert.SerializeObject(obj), System.Text.Encoding.UTF8, "application/json");
// ASIGNAMOS A LA CABECERA DE LA PETICIÓN EL TOKEN JWT.
if (_responseLoginServiceLayer.ServicioActivo)
request_.Headers.Add("Cookie", "B1SESSION=" + _responseLoginServiceLayer.SessionId);
/////////////////////////////////////////
// CONSTRUIMOS LA RESPUESTA (RESPONSE) //
/////////////////////////////////////////
// Utilizamos ConfigureAwait(false) para evitar el DeadLock.
var response_ = await client_.SendAsync(request_, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
// OBTENEMOS EL Content DEL RESPONSE como un String
// Utilizamos ConfigureAwait(false) para evitar el DeadLock.
var responseText_ = await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
// SI ES LA RESPUESTA ESPERADA !! ...
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 = new BE_BaseResponseSap(); //JsonConvert.DeserializeObject<T>(responseText_);
responseBody.Exito = true;
var estado = (int)response_.StatusCode;
responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "SE ENVIO CORRECTAMENTE A SAP BO ";
return responseBody;
}
else
// SI NO SE ESTÁ AUTORIZADO ...
if (response_.StatusCode == System.Net.HttpStatusCode.Unauthorized) // 401
{
NumError += 1;
_responseLoginServiceLayer = JsonConvert.DeserializeObject<ResponseLoginServiceLayer>(responseText_);
_responseLoginServiceLayer.ServicioActivo = false;
if (NumError == 2)
{
dynamic responseBody = JsonConvert.DeserializeObject<T>(responseText_);
responseBody.Exito = false;
var estado = (int)response_.StatusCode;
responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "Services Unauthorized : " + _responseLoginServiceLayer.error.message.ToString();
return responseBody;
}
if (_responseLoginServiceLayer.error.code.ToString() == "301")
{
await LoginPost();
}
goto band;
}
else
// CUALQUIER OTRA RESPUESTA ...
if (response_.StatusCode != System.Net.HttpStatusCode.OK && // 200
@@ -680,6 +351,7 @@ namespace Net.Connection.ServiceLayer
var estado = (int)response_.StatusCode;
responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "Error Services : " + _responseLoginServiceLayer.error.message.ToString();
if (conCuerpo) LogErrorEnvio(Query, obj, responseBody.Mensaje);
return responseBody;
}
@@ -783,7 +455,7 @@ namespace Net.Connection.ServiceLayer
if (_responseLoginServiceLayer.error.code.ToString() == "301")
{
await LoginPost();
await Login();
goto band;
}
}
@@ -56,42 +56,5 @@ namespace Net.Data.HelperData.HojaRuta
return ListaStock;
}
public static List<BE_ItemStock> GetConsolidado(string ItemCode, string FromWarehouseCode)
{
List<BE_ItemStock> ListaStock = new List<BE_ItemStock>();
DataTable dt = new DataTable();
using (HanaConnection con = new HanaConnection(LocalData.ConnectionString))
{
con.Open();
HanaCommand cmd = new HanaCommand($"CALL\"{LocalData.Schema}\".\"ITCA_WMS_SAP_GetStockConsolidado\"(?, ?)", con);
cmd.Parameters.Add("ItemCode", HanaDbType.VarChar).Value = ItemCode ?? "";
cmd.Parameters.Add("FromWarehouseCode", HanaDbType.VarChar).Value = FromWarehouseCode ?? "";
HanaDataAdapter da = new HanaDataAdapter(cmd);
if (da != null)
{
da.Fill(dt);
}
con.Close();
}
for (int x = 0; x < dt.Rows.Count; x++)
{
ListaStock.Add(new BE_ItemStock()
{
BinCode = (dt.Rows[x]["BinCode"] != DBNull.Value) ? Convert.ToString(dt.Rows[x]["BinCode"]) : string.Empty,
DistNumber = (dt.Rows[x]["DistNumber"] != DBNull.Value) ? Convert.ToString(dt.Rows[x]["DistNumber"]) : string.Empty,
ExpDate = (dt.Rows[x]["ExpDate"] != DBNull.Value) ? Convert.ToDateTime(dt.Rows[x]["ExpDate"]) : DateTime.Now,
ItemCode = (dt.Rows[x]["ItemCode"] != DBNull.Value) ? Convert.ToString(dt.Rows[x]["ItemCode"]) : string.Empty,
ItemName = (dt.Rows[x]["ItemName"] != DBNull.Value) ? Convert.ToString(dt.Rows[x]["ItemName"]) : string.Empty,
OnHandQty = (dt.Rows[x]["OnHandQty"] != DBNull.Value) ? Convert.ToDouble(dt.Rows[x]["OnHandQty"]) : 0,
WhsCode = (dt.Rows[x]["WhsCode"] != DBNull.Value) ? Convert.ToString(dt.Rows[x]["WhsCode"]) : string.Empty,
WhsName = (dt.Rows[x]["WhsName"] != DBNull.Value) ? Convert.ToString(dt.Rows[x]["WhsName"]) : string.Empty,
AvgPrice = (dt.Rows[x]["AvgPrice"] != DBNull.Value) ? Convert.ToDouble(dt.Rows[x]["AvgPrice"]) : 0,
});
}
return ListaStock;
}
}
}
+47 -1
View File
@@ -1,5 +1,7 @@
using Sap.Data.Hana;
using Net.Business.Entities.WMS.Transferencia;
using Sap.Data.Hana;
using System;
using System.Collections.Generic;
using System.Data;
namespace Net.Data.HelperData.Invoices
@@ -30,5 +32,49 @@ namespace Net.Data.HelperData.Invoices
return DocEntry;
}
// Lineas de todas las Entradas de Mercaderia (no anuladas) enlazadas (BaseType 18) a la Factura de Reserva
// de proveedores con ese DocNum (isIns = 'Y'). Si una linea ya tiene transferencia de stock (TargetType 67)
// y esa transferencia no esta anulada, TransferenciaDocEntry trae su DocEntry; si no, 0 (una transferencia cancelada
// libera la entrada). Lo usa TransferenciaRepository (origen ALM99); ver docs/TRANSFERENCIA_IMPORTACION.md.
// OJO: Get() (procedimiento ITCA_WMS_SAP_GetEntradaPorImportacion) ya no se usa; devolvia un solo DocEntry.
public static List<BE_EntradaImportacionLinea> GetEntradasPorFactura(string DocNum)
{
var lista = new List<BE_EntradaImportacionLinea>();
DataTable dt = new DataTable();
string esquema = LocalData.Schema;
using (HanaConnection con = new HanaConnection(LocalData.ConnectionString))
{
con.Open();
HanaCommand cmd = new HanaCommand(
$"SELECT E.\"DocEntry\", E.\"DocNum\", L.\"LineNum\", L.\"ItemCode\", L.\"Quantity\", " +
$"CASE WHEN L.\"TargetType\" = 67 AND W.\"DocEntry\" IS NOT NULL AND W.\"CANCELED\" = 'N' THEN W.\"DocEntry\" ELSE 0 END AS \"TransferenciaDocEntry\" " +
$"FROM \"{esquema}\".\"OPCH\" F " +
$"INNER JOIN \"{esquema}\".\"PDN1\" L ON L.\"BaseType\" = 18 AND L.\"BaseEntry\" = F.\"DocEntry\" " +
$"INNER JOIN \"{esquema}\".\"OPDN\" E ON E.\"DocEntry\" = L.\"DocEntry\" " +
$"LEFT JOIN \"{esquema}\".\"OWTR\" W ON L.\"TargetType\" = 67 AND W.\"DocEntry\" = L.\"TrgetEntry\" " +
$"WHERE F.\"DocNum\" = ? AND F.\"isIns\" = 'Y' AND E.\"CANCELED\" = 'N' " +
$"ORDER BY E.\"DocEntry\", L.\"LineNum\"", con);
cmd.Parameters.Add("DocNum", HanaDbType.VarChar).Value = DocNum ?? "";
HanaDataAdapter da = new HanaDataAdapter(cmd);
da.Fill(dt);
con.Close();
}
for (int x = 0; x < dt.Rows.Count; x++)
{
lista.Add(new BE_EntradaImportacionLinea()
{
DocEntry = Convert.ToInt32(dt.Rows[x]["DocEntry"]),
DocNum = Convert.ToInt32(dt.Rows[x]["DocNum"]),
LineNum = Convert.ToInt32(dt.Rows[x]["LineNum"]),
ItemCode = Convert.ToString(dt.Rows[x]["ItemCode"]),
Quantity = Convert.ToDecimal(dt.Rows[x]["Quantity"]),
TransferenciaDocEntry = Convert.ToInt32(dt.Rows[x]["TransferenciaDocEntry"]),
});
}
return lista;
}
}
}
-24
View File
@@ -116,30 +116,6 @@ namespace Net.Data.ServicesLayer.HR
}
public async Task<BE_ResultadoTransaccion<BE_BaseResponseSap>> UpdateCreateCreditNotes(dynamic entidad, int DocEntry)
{
_metodoName = regex.Match(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name).Groups[1].Value.ToString();
ResultadoTransaccion<BE_BaseResponseSap> resultadoTransaccion = new ResultadoTransaccion<BE_BaseResponseSap>(_metodoName, _aplicacionName);
try
{
//var cadena = "Drafts";
var cadena = $"CreditNotes({DocEntry})";
var data = await _connectServiceLayer.PatchAsyncSBA<BE_BaseResponseSap>(cadena, entidad);
if (!data.Exito)
{
return resultadoTransaccion.GetInformacionResultado(-1, -1, data.Mensaje);
}
return resultadoTransaccion.GetInformacionResultado(0, "", "Se Listo correctamento...!!!", data);
}
catch (Exception ex)
{
return resultadoTransaccion.GetInformacionResultado(-1, -1, ex.Message.ToString());
}
}
}
@@ -0,0 +1,114 @@
using Microsoft.Extensions.Configuration;
using Net.Business.Entities.ServicesLayer.CreditNotes;
using Net.Business.Entities.ServicesLayer.StockTransfer;
using Net.Data.ServicesLayer.CreditNotes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
namespace Net.Data.ServicesLayer.StockTransfer
{
// Transferencias de stock automaticas de una nota de credito (Picking DESPACHO/PICKING y Recibo Devolucion).
// Antes este bloque estaba copiado 3 veces.
public static class TransferenciaAutomaticaNC
{
// Por cada almacen de origen (WarehouseCode) y almacen destino (U_ITC_ALMDES) distintos de las lineas de la NC
// crea una transferencia de stock en SAP, con las cantidades y lotes de esas lineas y la NC como referencia.
// Luego agrega a la NC una referencia por cada transferencia creada.
// OJO: si SAP rechaza una transferencia se ignora (la NC ya esta creada) y no se informa al consumidor.
public static void Crear(BE_CreditNotesNotes creditNotesNotes, int docEntryNC, IHttpClientFactory clientFactory, IConfiguration configuration, CreditNotesRepository creditNotesRepository)
{
List<string> ListaUnicaAlmacen = creditNotesNotes.DocumentLines.Select(x => x.WarehouseCode).Distinct().ToList();
List<int> ListDocEntry = new List<int>();
ListaUnicaAlmacen.ForEach(Almacen =>
{
List<string> ListaUnicaAlmDestino = creditNotesNotes.DocumentLines.Where(Linea => Linea.WarehouseCode == Almacen).ToList().Select(x => x.U_ITC_ALMDES).Distinct().ToList().Where(x => !string.IsNullOrEmpty(x)).ToList();
ListaUnicaAlmDestino.ForEach(Destino =>
{
if (Almacen != Destino)
{
BE_StockTransferAutomatico Transfer = new BE_StockTransferAutomatico();
Transfer.FromWarehouse = Almacen;
Transfer.ToWarehouse = Destino;
Transfer.DocDate = creditNotesNotes.DocDate;
Transfer.TaxDate = creditNotesNotes.DocDate;
Transfer.U_SYP_MDTS = "TSI";
Transfer.U_SYP_MDMT = "24";
Transfer.DueDate = creditNotesNotes.DocDueDate;
Transfer.PriceList = -2;
var ListaDetalle_PorAlmacen = creditNotesNotes.DocumentLines.Where(x => x.WarehouseCode == Almacen && x.U_ITC_ALMDES == Destino).ToList();
var ListaUnicaArticulo = ListaDetalle_PorAlmacen.Select(x => x.ItemCode).Distinct().ToList();
Transfer.StockTransferLines = new List<BE_StockTransferLine>();
//=== AGREGA DOCUMENTO REFERENCIA
Transfer.DocumentReferences = new List<BE_StockTransferDocumentReferences>
{
new BE_StockTransferDocumentReferences()
{
RefDocEntr = docEntryNC,
RefObjType = "rot_SalesCreditNote",
IssueDate = DateTime.Now,
Remark = "Nota de Credito",
}
};
ListaUnicaArticulo.ForEach(x =>
{
var ListLote = new List<BE_StockTransferLineBatchNumber>();
// 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 = g.Key,
Quantity = g.Sum(r => r.Quantity)
});
});
Transfer.StockTransferLines.Add(new BE_StockTransferLine()
{
ItemCode = x,
ItemDescription = ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).FirstOrDefault().ItemDescription,
Quantity = Convert.ToDouble(ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).Sum(t => t.Quantity)),
WarehouseCode = Destino,
FromWarehouseCode = Almacen,
BatchNumbers = ListLote
});
});
//=== CREA INSTANCIA PARA CONECTAR A SAP
StockTransferRepository stockTransferRepository = new StockTransferRepository(clientFactory, configuration);
var ResultTransfer = stockTransferRepository.StockTransfer(Transfer).Result;
if (ResultTransfer.StatusCode != -1)
{
ListDocEntry.Add((int)ResultTransfer.Data.DocEntry);
}
}
});
});
// Referencia las transferencias creadas en la NC
if (ListDocEntry.Count > 0)
{
var listaReferencia = new List<BE_CreditNotesDocumentReferences>();
ListDocEntry.ForEach(x =>
{
listaReferencia.Add(new BE_CreditNotesDocumentReferences()
{
RefDocEntr = x,
IssueDate = DateTime.Now,
RefObjType = "rot_InventoryTransfer",
Remark = "Transferencias Automatica"
});
});
var credit = new
{
DocumentReferences = listaReferencia
};
var Actualizacion = creditNotesRepository.UpdateCreateCreditNotes(credit, docEntryNC).Result;
}
}
}
}
+4 -198
View File
@@ -299,108 +299,8 @@ namespace Net.Data.WMS.Picking
}
else
{
// Por cada almacen de origen y almacen destino (U_ITC_ALMDES) distintos se crea una transferencia de stock automatica
List<string> ListaUnicaAlmacen = creditNotesNotes.DocumentLines.Select(x => x.WarehouseCode).Distinct().ToList();
List<int> ListDocEntry = new List<int>();
ListaUnicaAlmacen.ForEach(Almacen =>
{
List<string> ListaUnicaAlmDestino = creditNotesNotes.DocumentLines.Where(Linea => Linea.WarehouseCode == Almacen).ToList().Select(x => x.U_ITC_ALMDES).Distinct().ToList().Where(x => !string.IsNullOrEmpty(x)).ToList();
ListaUnicaAlmDestino.ForEach(Destino =>
{
if (Almacen != Destino)
{
BE_StockTransferAutomatico Transfer = new BE_StockTransferAutomatico();
Transfer.FromWarehouse = Almacen;
Transfer.ToWarehouse = Destino;
Transfer.DocDate = creditNotesNotes.DocDate;
Transfer.TaxDate = creditNotesNotes.DocDate;
Transfer.U_SYP_MDTS = "TSI";
Transfer.U_SYP_MDMT = "24";
Transfer.DueDate = creditNotesNotes.DocDueDate;
Transfer.PriceList = -2;
var ListaDetalle_PorAlmacen = creditNotesNotes.DocumentLines.Where(x => x.WarehouseCode == Almacen && x.U_ITC_ALMDES == Destino).ToList();
var ListaUnicaArticulo = ListaDetalle_PorAlmacen.Select(x => x.ItemCode).Distinct().ToList();
Transfer.StockTransferLines = new List<BE_StockTransferLine>();
Transfer.DocumentReferences = new List<BE_StockTransferDocumentReferences>();
ListaUnicaArticulo.ForEach(x =>
{
var ListLote = new List<BE_StockTransferLineBatchNumber>();
// 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 = g.Key,
Quantity = g.Sum(r => r.Quantity)
});
});
Transfer.StockTransferLines.Add(new BE_StockTransferLine()
{
ItemCode = x,
ItemDescription = ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).FirstOrDefault().ItemDescription,
Quantity = Convert.ToDouble(ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).Sum(t => t.Quantity)),
WarehouseCode = Destino,
FromWarehouseCode = Almacen,
BatchNumbers = ListLote
});
//=== AGREGA DOCUMENTO REFERENCIA
Transfer.DocumentReferences = new List<BE_StockTransferDocumentReferences>
{
new BE_StockTransferDocumentReferences()
{
RefDocEntr = (int)result_creditNotesRepository.Data.DocEntry,
RefObjType = "rot_SalesCreditNote",
IssueDate = DateTime.Now,
Remark = "Nota de Credito",
}
};
});
//=== CREA INSTANCIA PARA CONECTAR A SAP
StockTransferRepository stockTransferRepository = new StockTransferRepository(_clientFactory, _configuration);
var ResultTransfer = stockTransferRepository.StockTransfer(Transfer).Result;
// OJO: si la transferencia falla se ignora (la NC ya esta creada) y no se informa al consumidor
if (ResultTransfer.StatusCode == -1)
{
}
else
{
ListDocEntry.Add((int)ResultTransfer.Data.DocEntry);
}
}
});
});
// Referencia las transferencias creadas en la NC
if (ListDocEntry.Count > 0)
{
var listaReferencia = new List<BE_CreditNotesDocumentReferences>();
ListDocEntry.ForEach(x =>
{
listaReferencia.Add(new BE_CreditNotesDocumentReferences()
{
RefDocEntr = x,
IssueDate = DateTime.Now,
RefObjType = "rot_InventoryTransfer",
Remark = "Transferencias Automatica"
});
});
var credit = new
{
DocumentReferences = listaReferencia
};
var Actualizacion = _creditNotesRepository.UpdateCreateCreditNotes(credit, (int)result_creditNotesRepository.Data.DocEntry).Result;
}
// Transferencias de stock automaticas por almacen destino (U_ITC_ALMDES) y su referencia en la NC
TransferenciaAutomaticaNC.Crear(creditNotesNotes, (int)result_creditNotesRepository.Data.DocEntry, _clientFactory, _configuration, _creditNotesRepository);
return resultadoTransaccion.GetInformacionResultado((int)result_creditNotesRepository.Data.DocEntry, result_creditNotesRepository.Data.DocNum.ToString(), "Se registro correctamente...!!!", entidad);
}
}
@@ -598,102 +498,8 @@ namespace Net.Data.WMS.Picking
else
{
// Transferencias automaticas por almacen origen/destino (igual que en DESPACHO)
List<string> ListaUnicaAlmacen = creditNotesNotes.DocumentLines.Select(x => x.WarehouseCode).Distinct().ToList();
List<int> ListDocEntry = new List<int>();
ListaUnicaAlmacen.ForEach(Almacen =>
{
List<string> ListaUnicaAlmDestino = creditNotesNotes.DocumentLines.Where(Linea => Linea.WarehouseCode == Almacen).ToList().Select(x => x.U_ITC_ALMDES).Distinct().ToList().Where(x => !string.IsNullOrEmpty(x)).ToList();
ListaUnicaAlmDestino.ForEach(Destino =>
{
if (Almacen != Destino)
{
BE_StockTransferAutomatico Transfer = new BE_StockTransferAutomatico();
Transfer.FromWarehouse = Almacen;
Transfer.ToWarehouse = Destino;
Transfer.DocDate = creditNotesNotes.DocDate;
Transfer.TaxDate = creditNotesNotes.DocDate;
Transfer.DueDate = creditNotesNotes.DocDueDate;
Transfer.U_SYP_MDTS = "TSI";
Transfer.U_SYP_MDMT = "24";
Transfer.PriceList = -2;
var ListaDetalle_PorAlmacen = creditNotesNotes.DocumentLines.Where(x => x.WarehouseCode == Almacen && x.U_ITC_ALMDES == Destino).ToList();
var ListaUnicaArticulo = ListaDetalle_PorAlmacen.Select(x => x.ItemCode).Distinct().ToList();
Transfer.StockTransferLines = new List<BE_StockTransferLine>();
Transfer.DocumentReferences = new List<BE_StockTransferDocumentReferences>();
ListaUnicaArticulo.ForEach(x =>
{
var ListLote = new List<BE_StockTransferLineBatchNumber>();
// 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 = g.Key,
Quantity = g.Sum(r => r.Quantity)
});
});
Transfer.StockTransferLines.Add(new BE_StockTransferLine()
{
ItemCode = x,
ItemDescription = ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).FirstOrDefault().ItemDescription,
Quantity = Convert.ToDouble(ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).Sum(t => t.Quantity)),
WarehouseCode = Destino,
FromWarehouseCode = Almacen,
BatchNumbers = ListLote
});
//=== AGREGA DOCUMENTO REFERENCIA
Transfer.DocumentReferences = new List<BE_StockTransferDocumentReferences>
{
new BE_StockTransferDocumentReferences()
{
RefDocEntr = (int)result_creditNotesRepository.Data.DocEntry,
RefObjType = "rot_SalesCreditNote",
IssueDate = DateTime.Now,
Remark = "Nota de Credito",
}
};
});
//=== CREA INSTANCIA PARA CONECTAR A SAP
StockTransferRepository stockTransferRepository = new StockTransferRepository(_clientFactory, _configuration);
var ResultTransfer = stockTransferRepository.StockTransfer(Transfer).Result;
// OJO: si la transferencia falla se ignora (la NC ya esta creada)
if (ResultTransfer.StatusCode == -1)
{
}
else
{
ListDocEntry.Add((int)ResultTransfer.Data.DocEntry);
}
}
});
});
if (ListDocEntry.Count > 0)
{
var listaReferencia = new List<BE_CreditNotesDocumentReferences>();
ListDocEntry.ForEach(x =>
{
listaReferencia.Add(new BE_CreditNotesDocumentReferences()
{
RefDocEntr = x,
IssueDate = DateTime.Now,
RefObjType = "rot_InventoryTransfer",
Remark = "Transferencias Automatica"
});
});
var credit = new
{
DocumentReferences = listaReferencia
};
var Actualizacion = _creditNotesRepository.UpdateCreateCreditNotes(credit, (int)result_creditNotesRepository.Data.DocEntry).Result;
}
// Transferencias de stock automaticas por almacen destino (U_ITC_ALMDES) y su referencia en la NC
TransferenciaAutomaticaNC.Crear(creditNotesNotes, (int)result_creditNotesRepository.Data.DocEntry, _clientFactory, _configuration, _creditNotesRepository);
return resultadoTransaccion.GetInformacionResultado((int)result_creditNotesRepository.Data.DocEntry, result_creditNotesRepository.Data.DocNum.ToString(), "Se registro correctamente...!!!", entidad);
}
}
+3 -96
View File
@@ -537,102 +537,9 @@ namespace Net.Data.WMS.Recibo
}
else
{
// Transferencias por almacen destino (U_ITC_ALMDES). OJO: en Recibo las lineas de la NC no llenan U_ITC_ALMDES, por lo que hoy no se genera ninguna
List<string> ListaUnicaAlmacen = creditNotesNotes.DocumentLines.Select(x => x.WarehouseCode).Distinct().ToList();
List<int> ListDocEntry = new List<int>();
ListaUnicaAlmacen.ForEach(Almacen =>
{
List<string> ListaUnicaAlmDestino = creditNotesNotes.DocumentLines.Where(Linea => Linea.WarehouseCode == Almacen).ToList().Select(x => x.U_ITC_ALMDES).Distinct().ToList().Where(x => !string.IsNullOrEmpty(x)).ToList();
ListaUnicaAlmDestino.ForEach(Destino =>
{
if (Almacen != Destino)
{
BE_StockTransferAutomatico Transfer = new BE_StockTransferAutomatico();
Transfer.FromWarehouse = Almacen;
Transfer.ToWarehouse = Destino;
Transfer.DocDate = creditNotesNotes.DocDate;
Transfer.TaxDate = creditNotesNotes.DocDate;
Transfer.DueDate = creditNotesNotes.DocDueDate;
Transfer.U_SYP_MDTS = "TSI";
Transfer.U_SYP_MDMT = "24";
Transfer.PriceList = -2;
var ListaDetalle_PorAlmacen = creditNotesNotes.DocumentLines.Where(x => x.WarehouseCode == Almacen && x.U_ITC_ALMDES == Destino).ToList();
var ListaUnicaArticulo = ListaDetalle_PorAlmacen.Select(x => x.ItemCode).Distinct().ToList();
Transfer.StockTransferLines = new List<BE_StockTransferLine>();
Transfer.DocumentReferences = new List<BE_StockTransferDocumentReferences>();
ListaUnicaArticulo.ForEach(x =>
{
var ListLote = new List<BE_StockTransferLineBatchNumber>();
// 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 = g.Key,
Quantity = g.Sum(r => r.Quantity)
});
});
Transfer.StockTransferLines.Add(new BE_StockTransferLine()
{
ItemCode = x,
ItemDescription = ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).FirstOrDefault().ItemDescription,
Quantity = Convert.ToDouble(ListaDetalle_PorAlmacen.Where(t => t.ItemCode == x).Sum(t => t.Quantity)),
WarehouseCode = Destino,
FromWarehouseCode = Almacen,
BatchNumbers = ListLote
});
//=== AGREGA DOCUMENTO REFERENCIA
Transfer.DocumentReferences = new List<BE_StockTransferDocumentReferences>
{
new BE_StockTransferDocumentReferences()
{
RefDocEntr = (int)result_creditNotesRepository.Data.DocEntry,
RefObjType = "rot_SalesCreditNote",
IssueDate = DateTime.Now,
Remark = "Nota de Credito",
}
};
});
//=== CREA INSTANCIA PARA CONECTAR A SAP
StockTransferRepository stockTransferRepository = new StockTransferRepository(_clientFactory, _configuration);
var ResultTransfer = stockTransferRepository.StockTransfer(Transfer).Result;
// OJO: si la transferencia falla se ignora (la NC ya esta creada)
if (ResultTransfer.StatusCode == -1)
{
}
else
{
ListDocEntry.Add((int)ResultTransfer.Data.DocEntry);
}
}
});
});
if (ListDocEntry.Count > 0)
{
var listaReferencia = new List<BE_CreditNotesDocumentReferences>();
ListDocEntry.ForEach(x =>
{
listaReferencia.Add(new BE_CreditNotesDocumentReferences()
{
RefDocEntr = x,
IssueDate = DateTime.Now,
RefObjType = "rot_InventoryTransfer",
Remark = "Transferencias Automatica"
});
});
var credit = new
{
DocumentReferences = listaReferencia
};
var Actualizacion = _creditNotesRepository.UpdateCreateCreditNotes(credit, (int)result_creditNotesRepository.Data.DocEntry).Result;
}
// Transferencias de stock automaticas por almacen destino (U_ITC_ALMDES). OJO: en Recibo las lineas de la NC no llenan
// U_ITC_ALMDES, por lo que hoy no se genera ninguna
TransferenciaAutomaticaNC.Crear(creditNotesNotes, (int)result_creditNotesRepository.Data.DocEntry, _clientFactory, _configuration, _creditNotesRepository);
return resultadoTransaccion.GetInformacionResultado((int)result_creditNotesRepository.Data.DocEntry, result_creditNotesRepository.Data.DocNum.ToString(), "Se registro correctamente...!!!", entidad);
}
@@ -0,0 +1,56 @@
using Net.Business.Entities.WMS.Transferencia;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Net.Data.WMS.Transferencia
{
// Elige la Entrada de Mercaderia de una Factura de Reserva que corresponde a una transferencia desde ALM99.
// WMS recibe por partes: cada entrada tiene su propio juego de articulos/cantidades, y WMS notifica una
// transferencia por entrada con ese mismo juego (se compara solo ItemCode y cantidad, no el numero de item).
// Reglas: coincidencia exacta de articulos y cantidades (sumadas por articulo); entre las que coinciden se toma la mas antigua
// (menor DocEntry) sin transferencia; si todas las que coinciden ya estan transferidas se rechaza para no duplicar.
// Ver docs/TRANSFERENCIA_IMPORTACION.md.
public static class EntradaImportacion
{
private const decimal Tolerancia = 0.0001m;
// Devuelve el DocEntry de la entrada elegida, o 0 con el motivo en "error".
public static int Elegir(string docNumFactura, List<BE_EntradaImportacionLinea> filas, List<BE_TransferenciaDetalle> detalle, out string error)
{
error = null;
var pedido = (detalle ?? new List<BE_TransferenciaDetalle>())
.GroupBy(x => x.CodigoArticulo)
.ToDictionary(g => g.Key, g => g.Sum(x => x.CantidadPedida));
var entradas = filas.GroupBy(x => x.DocEntry).OrderBy(g => g.Key).ToList();
var coincidentes = entradas.Where(g => Coincide(g, pedido)).ToList();
// La mas antigua que aun no tenga transferencia
var libre = coincidentes.FirstOrDefault(g => !g.Any(l => l.TransferenciaDocEntry > 0));
if (libre != null)
{
return libre.Key;
}
if (coincidentes.Count > 0)
{
var detalleYa = coincidentes.Select(g => $"entrada {g.First().DocNum} (DocEntry {g.Key}) ya transferida en la transferencia {g.First(l => l.TransferenciaDocEntry > 0).TransferenciaDocEntry}");
error = $"SAP: Todas las entradas de la factura de reserva {docNumFactura} con esos articulos y cantidades ya estan transferidas: {string.Join("; ", detalleYa)}";
return 0;
}
var pendientes = entradas.Where(g => !g.Any(l => l.TransferenciaDocEntry > 0)).Select(g => $"{g.First().DocNum}");
error = $"SAP: Ninguna entrada de la factura de reserva {docNumFactura} coincide con los articulos y cantidades enviados. " +
$"Entradas pendientes de transferir (DocNum): {(pendientes.Any() ? string.Join(", ", pendientes) : "ninguna")}";
return 0;
}
private static bool Coincide(IGrouping<int, BE_EntradaImportacionLinea> entrada, Dictionary<string, decimal> pedido)
{
var enEntrada = entrada.GroupBy(x => x.ItemCode).ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity));
if (enEntrada.Count != pedido.Count) return false;
return enEntrada.All(kv => pedido.TryGetValue(kv.Key, out decimal cant) && Math.Abs(cant - kv.Value) <= Tolerancia);
}
}
}
@@ -4,6 +4,7 @@ using Net.Business.Entities;
using Net.Business.Entities.ServicesLayer;
using Net.Business.Entities.ServicesLayer.Invoices;
using Net.Business.Entities.ServicesLayer.Items;
using Net.Business.Entities.ServicesLayer.PurchaseDeliveryNotes;
using Net.Business.Entities.ServicesLayer.StockTransfer;
using Net.Business.Entities.WMS.Transferencia;
using Net.Connection;
@@ -110,36 +111,86 @@ namespace Net.Data.WMS.Transferencia
#endregion
});
//=== SI EL ALMACEN ES ALM99, FUERZA EL AMARRE CON LA ENTRADA DE MERCANCIA
//=== SI EL ALMACEN ES ALM99 (almacen de costeo de importaciones), AMARRA CON LA ENTRADA DE MERCANCIA
// Proceso: factura de reserva -> WMS recibe por partes y crea una entrada por recepcion (todas a ALM99) -> costeo en SAP ->
// WMS notifica UNA transferencia por entrada, con numorden = DocNum de la factura de reserva y en el detalle los
// articulos/cantidades de esa entrada. Detalle completo y diagnostico en docs/TRANSFERENCIA_IMPORTACION.md.
// Casos: (1) DocEntryEM informado -> se usa esa entrada; (2) sin DocEntryEM -> se busca la entrada de la factura que
// coincida con el detalle y no tenga transferencia (EntradaImportacion.Elegir); (3) la factura no tiene entradas ->
// como antes, lotes del stock de HANA sin amarre.
if (entidad.CodigoSubAlmacenOrigen == "ALM99")
{
StockTransfer.U_SYP_MDMT = "21";
int DocEntry_Entada = entidad.DocEntryEM; //HelperInvoice.Get(entidad.NumOrden);
if(DocEntry_Entada > 0)
PurchaseDeliveryNotesRepository _purchaseDeliveryNotesRepository = new PurchaseDeliveryNotesRepository(_clientFactory, _configuration);
if (entidad.DocEntryEM > 0)
{
PurchaseDeliveryNotesRepository _purchaseDeliveryNotesRepository = new PurchaseDeliveryNotesRepository(_clientFactory, _configuration);
var _purchaseDeliveryNotesRepository_Response = _purchaseDeliveryNotesRepository.GetPurchaseDeliveryNotes(DocEntry_Entada).Result.ListData.FirstOrDefault();
//=== RESETEA STOCK POR LOTES
StockTransfer.StockTransferLines.ForEach(line =>
// WMS indica la entrada: se toman sus lotes y se amarra linea por linea (BaseLine del JSON == BaseLine de la entrada)
var pdn = _purchaseDeliveryNotesRepository.GetPurchaseDeliveryNotes(entidad.DocEntryEM).Result.ListData?.FirstOrDefault();
if (pdn == null)
{
var Lines = _purchaseDeliveryNotesRepository_Response.DocumentLines.FirstOrDefault(line_purchaseDeliveryNotesRepository_Response => line_purchaseDeliveryNotesRepository_Response.BaseLine == line.LineNum);
line.BatchNumbers = new List<BE_StockTransferLineBatchNumber>();
return resultadoTransaccion.GetInformacionResultado($"SAP: La entrada de mercaderia {entidad.DocEntryEM} no se encuentra", entidad);
}
//=== AGREGA LOTES
Lines.BatchNumbers.ForEach(lotes =>
foreach (var line in StockTransfer.StockTransferLines)
{
var Lines = pdn.DocumentLines.FirstOrDefault(l => l.BaseLine == line.LineNum);
if (Lines == null)
{
line.BatchNumbers.Add(new BE_StockTransferLineBatchNumber()
{
BatchNumber = lotes.BatchNumber,
Quantity = lotes.Quantity,
});
});
return resultadoTransaccion.GetInformacionResultado($"SAP: La linea {line.LineNum} no existe en la entrada de mercaderia {entidad.DocEntryEM}", entidad);
}
line.BatchNumbers = (Lines.BatchNumbers ?? new List<BE_PurchaseDeliveryNotesBatchNumbers>())
.Select(lotes => new BE_StockTransferLineBatchNumber() { BatchNumber = lotes.BatchNumber, Quantity = lotes.Quantity }).ToList();
line.BaseLine = Lines.LineNum;
line.BaseType = "PurchaseDeliveryNotes";
line.BaseEntry = DocEntry_Entada;
});
line.BaseEntry = entidad.DocEntryEM;
}
}
else
{
// WMS envia en NumOrden el DocNum de la Factura de Reserva y en el detalle los articulos/cantidades de UNA de sus
// entradas: se busca la entrada con ese mismo juego que aun no tenga transferencia y se transfiere esa.
var filas = HelperInvoice.GetEntradasPorFactura(entidad.NumOrden);
if (filas.Count > 0)
{
int docEntryEntrada = EntradaImportacion.Elegir(entidad.NumOrden, filas, entidad.TransferenciaDetalles, out string errorEntrada);
if (docEntryEntrada == 0)
{
return resultadoTransaccion.GetInformacionResultado(errorEntrada, entidad);
}
var pdn = _purchaseDeliveryNotesRepository.GetPurchaseDeliveryNotes(docEntryEntrada).Result.ListData?.FirstOrDefault();
if (pdn == null)
{
return resultadoTransaccion.GetInformacionResultado($"SAP: La entrada de mercaderia {docEntryEntrada} no se puede leer", entidad);
}
// Las lineas de la transferencia salen de la entrada (articulo, cantidad y lotes reales de cada linea)
StockTransfer.StockTransferLines = new List<BE_StockTransferLine>();
foreach (var l in pdn.DocumentLines)
{
var lotes = (l.BatchNumbers ?? new List<BE_PurchaseDeliveryNotesBatchNumbers>())
.Select(b => new BE_StockTransferLineBatchNumber() { BatchNumber = b.BatchNumber, Quantity = b.Quantity }).ToList();
if (lotes.Count > 0 && Math.Abs(lotes.Sum(b => b.Quantity) - l.Quantity) > 0.0001)
{
return resultadoTransaccion.GetInformacionResultado($"SAP: Los lotes de la linea {l.LineNum} ({l.ItemCode}) de la entrada {docEntryEntrada} no cubren la cantidad {l.Quantity}", entidad);
}
StockTransfer.StockTransferLines.Add(new BE_StockTransferLine
{
FromWarehouseCode = entidad.CodigoSubAlmacenOrigen,
WarehouseCode = entidad.CodigoSubAlmacenDestino,
ItemCode = l.ItemCode,
Quantity = l.Quantity,
LineNum = l.LineNum,
BaseType = "PurchaseDeliveryNotes",
BaseEntry = docEntryEntrada,
BaseLine = l.LineNum,
BatchNumbers = lotes
});
}
}
// Si la factura no tiene entradas (o NumOrden no es una factura de reserva) se mantiene lo de antes:
// transferencia con los lotes del stock de HANA, sin amarre.
}
}
+1 -1
View File
@@ -76,7 +76,7 @@ dotnet publish Net.Business.Services/Net.Business.Services.csproj -c Release -r
## Versión y script de publish
- La versión sale de `<Version>`/`<AssemblyVersion>`/`<FileVersion>` en
`Net.Business.Services/Net.Business.Services.csproj` (actual: **2.1.8.0** en la rama de limpieza; `main` = 2.1.7.0);
`Net.Business.Services/Net.Business.Services.csproj` (actual: **2.1.9.0** en la rama de limpieza; `main` = 2.1.7.0);
súbela en cada publish.
- `publicar.ps1` (solo Windows) genera `dist\ITCA_API_SAP_WMS_v<versión>.zip` sin
`appsettings.json`. Desde macOS se hace el mismo `dotnet publish` a mano con
+102
View File
@@ -0,0 +1,102 @@
# Transferencia de stock desde ALM99 (importaciones) — v2.1.9.0
Documento de soporte. Código: `Net.Data/WMS/Transferencia/TransferenciaRepository.cs` (bloque `ALM99`),
`Net.Data/WMS/Transferencia/EntradaImportacion.cs` (elección de la entrada) y
`Net.Data/HelperData/Invoices/HelperInvoice.GetEntradasPorFactura` (consulta HANA).
Endpoint: `POST /api/Transferencia`.
## Proceso de negocio
1. En SAP se crea una **Factura de Reserva de proveedores** y se envía al WMS para su recepción.
2. El WMS recibe **por partes**: por cada recepción crea una **Entrada de Mercadería**
(`PurchaseDeliveryNotes`) enlazada a la factura (`BaseType 18`, `BaseEntry`, `BaseLine`).
Hay una o varias entradas por factura y todas ingresan al almacén **ALM99** (costeo).
El WMS no guarda el `DocEntry` de las entradas, solo sabe qué artículos recibió en cada una.
3. En SAP se hace el costeo (precio de entrega) de las entradas.
4. Terminado el costeo, el WMS notifica **una transferencia por entrada** hacia el almacén final.
## JSON que envía el WMS
- `numorden` = **DocNum de la factura de reserva** (único).
- `codigoSubAlmacenOrigen` = `ALM99`; `codigoSubAlmacenDestino` = almacén final (p. ej. `ALM21`).
- `docEntry` = 0 (no hay solicitud de traslado); `docEntryEM` no se envía.
- `detalle` = los artículos y cantidades **de una sola entrada**. Solo se usan `codigoarticulo`
y `cantidadpedida`; `item` y `loteproducto` se ignoran en este caso.
## Qué hace la API (origen = ALM99 y sin `docEntryEM`)
1. Consulta HANA (`HelperInvoice.GetEntradasPorFactura`): factura `OPCH` con `DocNum = numorden`
y `isIns = 'Y'` (= factura de reserva) → líneas `PDN1` con `BaseType 18` y `BaseEntry` = factura
→ entrada `OPDN` no anulada (`CANCELED = 'N'`).
2. Una entrada **ya tiene transferencia** si la línea tiene `TargetType = 67` y su `TrgetEntry`
apunta a una transferencia `OWTR` con `CANCELED = 'N'`. Una transferencia anulada libera la entrada.
3. `EntradaImportacion.Elegir` compara, por entrada, el conjunto `ItemCode` → cantidad (sumada por
artículo) contra el detalle del JSON (también sumado). Debe coincidir **exacto** (mismos artículos,
mismas cantidades, tolerancia 0.0001). Se toma la **entrada más antigua** (menor `DocEntry`) que
coincida y no tenga transferencia.
4. Se lee la entrada por Service Layer (`PurchaseDeliveryNotes(docEntry)`) y las líneas de la
transferencia salen **de la entrada**: artículo, cantidad, lotes reales y base
(`BaseType "PurchaseDeliveryNotes"`, `BaseEntry`, `BaseLine` = línea de la entrada). Si los lotes
de una línea no suman su cantidad, responde error antes de enviar a SAP.
5. Se envía `StockTransfers` con `U_SYP_MDMT = "21"`.
## Respuestas de error (400, mensaje en la respuesta y en `ITCA_LOG_INTEGRACION`)
| Mensaje | Causa |
|---|---|
| `SAP: Ninguna entrada de la factura de reserva X coincide con los articulos y cantidades enviados. Entradas pendientes de transferir (DocNum): ...` | El JSON no es igual a ninguna entrada (falta/sobra un artículo o cambia una cantidad). Lista las entradas aún sin transferir para comparar. |
| `SAP: Todas las entradas de la factura de reserva X con esos articulos y cantidades ya estan transferidas: entrada N ya transferida en la transferencia M` | Reintento del mismo JSON o entrada ya transferida (evita duplicados). |
| `SAP: Los lotes de la linea L (item) de la entrada N no cubren la cantidad Q` | La entrada tiene lotes que no suman la cantidad de la línea. |
| `SAP: La entrada de mercaderia N no se puede leer` | Falló el GET al Service Layer. |
| Error de SAP al crear `StockTransfers` | Ver la fila `PROCESO='ServiceLayer Error'` (JSON enviado + mensaje de SAP). |
## Otros casos del mismo endpoint (no cambiaron)
- **Transferencia simple** (origen distinto de ALM99, `docEntry = 0`): lotes por stock HANA (FEFO), sin base.
- **Con solicitud de traslado** (`docEntry ≠ 0`): la solicitud es la base y se cierra al terminar.
- **ALM99 con `docEntryEM` informado**: se respeta; lotes y base de esa entrada, amarrando
`BaseLine` del JSON con `BaseLine` de la entrada. Si la entrada o la línea no existen responde error claro.
- **ALM99 cuando la factura no tiene entradas** (o `numorden` no es una factura de reserva): se mantiene el
comportamiento anterior (lotes del stock de HANA, sin amarre).
## Verificado y pendiente
- Verificado con datos reales (SQL Server): la consulta encuentra las entradas por `BaseType 18` y
`isIns='Y'`; muestra el estado de transferencia por línea (entradas con 1 y con 2 líneas) y una
transferencia **cancelada** vuelve a dejar la entrada libre (`TransferenciaDocEntry = 0`).
- Verificado con simulación: la lógica de `Elegir` (coincidencia, entrada más antigua, ya transferida, no coincide).
- **Pendiente de confirmar contra SAP/HANA reales**: que la consulta corra en HANA (la API usa HANA);
que SAP acepte `BaseType "PurchaseDeliveryNotes"` en las líneas de `StockTransfers`; y el caso en que
una entrada de varias líneas tenga transferida solo una línea (hoy cuenta como ya transferida).
## Cómo diagnosticar ("no se genera la transferencia")
1. Ver la fila en `"ITC_WMS_SAP"."ITCA_LOG_INTEGRACION"` con `PROCESO = 'Transferencia'` y
`NRO_ORDEN = <numorden>`: trae el JSON del WMS y la respuesta (el mensaje de la tabla de arriba).
2. Ejecutar la consulta de `HelperInvoice.GetEntradasPorFactura` (versión SQL Server/HANA al final) con el
`DocNum`: si no devuelve filas, la factura no existe, no es de reserva (`isIns`) o sus entradas no tienen
`BaseType 18`; si `TransferenciaDocEntry ≠ 0`, esa entrada ya fue transferida.
3. Comparar artículos y cantidades del JSON contra las filas de esa entrada.
```sql
-- HANA (reemplazar <ESQUEMA> y el DocNum de la factura de reserva)
SELECT E."DocEntry", E."DocNum", L."LineNum", L."ItemCode", L."Quantity",
CASE WHEN L."TargetType" = 67 AND W."DocEntry" IS NOT NULL AND W."CANCELED" = 'N'
THEN W."DocEntry" ELSE 0 END AS "TransferenciaDocEntry"
FROM "<ESQUEMA>"."OPCH" F
INNER JOIN "<ESQUEMA>"."PDN1" L ON L."BaseType" = 18 AND L."BaseEntry" = F."DocEntry"
INNER JOIN "<ESQUEMA>"."OPDN" E ON E."DocEntry" = L."DocEntry"
LEFT JOIN "<ESQUEMA>"."OWTR" W ON L."TargetType" = 67 AND W."DocEntry" = L."TrgetEntry"
WHERE F."DocNum" = 262000082 AND F."isIns" = 'Y' AND E."CANCELED" = 'N'
ORDER BY E."DocEntry", L."LineNum";
```
En SQL Server es la misma consulta sin comillas dobles y con `[<BASE>].dbo.OPCH`, etc.
## Historia
Antes (también en el código del desarrollador anterior y en el commit inicial) el bloque dependía de
`DocEntryEM`, que el WMS no envía: nunca se amarraba a la entrada y los lotes salían solo del stock de HANA
(sin validar que cubrieran la cantidad). La versión original buscaba la entrada por `NumOrden` con
`HelperInvoice.Get` (procedimiento `ITCA_WMS_SAP_GetEntradaPorImportacion`, un solo `DocEntry`), que quedó
comentado; no servía para varias entradas por factura. Esa función sigue en el código sin llamadores.