11 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
GeorgeandClaude Sonnet 5.5 a5a4160471 Limpieza: comentarios en el codigo, if vacio de CreateHojaRuta y documentacion. Versión 2.1.8.0
Sin cambios de comportamiento. Comentarios en Picking, Recibo,
ConnectionServiceLayer, CreditNotesRepository, InvoiceRepository y
RepositoryWrapper.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:31:06 -05:00
GeorgeandClaude Sonnet 5.5 57b4578f1f Limpieza: quitar codigo sin uso (metodos sin llamadores, throw inalcanzable, variables y propiedades del RepositoryWrapper sin uso)
- ConnectionServiceLayer: ValidarCertificado, Logout, CancelAsyncSBA, DeleteAsyncSBA y throw inalcanzable en PatchAsyncSBA
- HelperDB (SET ejecutaba SQL arbitrario, sin llamadores)
- RepositoryWrapper/IRepositoryWrapper: 11 propiedades que ningun controller usa
- PickingRepository: TotalDetalle_Parcial y _Invoice sin uso; ReciboRepository: SerieOrigen sin uso

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:29:41 -05:00
GeorgeandClaude Sonnet 5.5 3d82f7380c README: como volver a la version 2.1.7.0 (etiqueta v2.1.7.0)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:24:02 -05:00
GeorgeandClaude Sonnet 5.5 23f1a3df44 Actualizar CLAUDE.md y README.md: git y remoto, version 2.1.7.0, appsettings fuera de git, logs, pendientes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:17:14 -05:00
19 changed files with 613 additions and 1255 deletions

No files matched your search

+77 -12
View File
@@ -3,10 +3,24 @@
API de integración WMS ↔ SAP Business One, **ASP.NET Core 3.1**. Para
build/publish/despliegue en IIS ver [`README.md`](./README.md).
Esta carpeta (`backend_octubre`) es **el proyecto vigente**. Antes existía
una copia paralela, `ITCA_API_SAP_WMS`, con la funcionalidad nueva portada
Esta carpeta (`ITCA_API_SAP_WMS`, antes `backend_octubre`) es **el proyecto
vigente**. Antes existía una copia paralela con la funcionalidad nueva portada
desde la reescritura Python; se portó aquí y esa copia quedó **archivada en un
comprimido (versión antigua)**. No hay `git` en esta carpeta.
comprimido (versión antigua)**.
## Versión y control de versiones
- **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` =
`http://100.68.242.27:30000/george/itca_api_sap_wms.git` (HTTP sin cifrar,
red interna; confirmar que el repositorio sea **privado**). Etiqueta
`v2.1.7.0` = último estado antes de la limpieza de código.
- `appsettings.json` y `appsettings.*.json` están en `.gitignore` (solo se
versiona `appsettings.example.json`); las credenciales no están en el
historial (verificado con `git log -S`).
- Los publish (`dist/`, zip del escritorio) no se versionan.
## Qué se portó desde `ITCA_API_SAP_WMS` (2026-10-05)
@@ -72,6 +86,44 @@ transferencias siempre, sin depender de `Notificacion`. Detalle y tabla en
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`).
- **v2.1.8.0 — limpieza y comentarios** (rama `limpieza-2.1.8.0`, sin cambios de
comportamiento): se quitó código sin uso (`ValidarCertificado`, `Logout`,
`CancelAsyncSBA`, `DeleteAsyncSBA`, el `throw` inalcanzable de `PatchAsyncSBA`,
`HelperDB`, 11 propiedades de `RepositoryWrapper` sin uso, variables
`TotalDetalle_Parcial`/`_Invoice`/`SerieOrigen` y un `if` vacío en
`CreateHojaRuta`) y se agregaron comentarios en Picking, Recibo,
`ConnectionServiceLayer`, `CreditNotesRepository`, `InvoiceRepository` y
`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.
@@ -125,15 +177,19 @@ 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; `throw` inalcanzable
tras `return` en varios `catch`.
- `ValidarCertificado` acepta cualquier certificado TLS (siempre `true`).
- `HelperDB.SET(string query)` ejecuta SQL arbitrario; hoy sin llamadores
(candidato a borrar).
- `appsettings.json` versionado en texto plano con credenciales reales
(SAP Service Layer, **HANA `B1ADMIN`**, semilla JWT). Rotar la contraseña de
`B1ADMIN` (estuvo en el código fuente) y evaluar un usuario HANA de mínimos
permisos.
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
reales en texto plano (SAP Service Layer, **HANA `B1ADMIN`**, semilla JWT).
Rotar la contraseña de `B1ADMIN` (estuvo en el código fuente) y evaluar un
usuario HANA de mínimos permisos.
- Los logs (`B_Log` y `HelperLogServiceLayer`) escriben en el esquema fijo
`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.
- 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
@@ -152,3 +208,12 @@ usa `processPath=".\ITCA_API_SAP_WMS.exe"`; el del código fuente apunta a
- Verificar que existan en el esquema del sitio los procedimientos `ITCA_*`
y las tablas `@ITCA_PROWMS`, `@SYP_MOTIVOS`, `@SYP_NUMDOC`.
- Decidir si se corrigen los bugs de la lista de arriba.
- Validar con contabilidad la serie de NC de boletas (`BC10`) en Recibo y si ya
hay NC de boletas emitidas con `FC10` antes de la 2.1.7.0.
- Recibo (Devolución) no genera transferencias por almacén destino: nunca llena
`U_ITC_ALMDES` en las líneas de la NC.
- Confirmar en QA el log de errores del Service Layer (provocar un rechazo y
ver la fila con `PROCESO = 'ServiceLayer Error'`) y el esquema de la tabla de
log en cada servidor.
- Confirmar el repositorio remoto como privado; hacer la limpieza de código en
una rama aparte (2.1.8.0) partiendo de la etiqueta `v2.1.7.0`.
@@ -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.7</Version>
<AssemblyVersion>2.1.7.0</AssemblyVersion>
<FileVersion>2.1.7.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
@@ -48,11 +48,7 @@ namespace Net.Connection.ServiceLayer
_responseLoginServiceLayer = new ResponseLoginServiceLayer();
}
private bool ValidarCertificado(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors ssl)
{
return true;
}
// Inicia sesion en el Service Layer (cookie B1SESSION). Usa SapServiceLayer:company/user/password de appsettings.json
public async Task<ResponseLoginServiceLayer> Login()
{
try
@@ -140,181 +136,7 @@ namespace Net.Connection.ServiceLayer
return _responseLoginServiceLayer;
}
}
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;
}
}
public async Task<ResponseLoginServiceLayer> Logout()
{
try
{
// CONSTRUIMOS LA URL DE LA ACCIÓN
var urlBuilder_ = new StringBuilder();
urlBuilder_.Append(_url != null ? _url.TrimEnd('/') : "")
.Append("/Logout");
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 + "\"}");
//// 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.NoContent) // 204
{
// DESERIALIZAMOS Content DEL RESPONSE
_responseLoginServiceLayer = JsonConvert.DeserializeObject<ResponseLoginServiceLayer>(responseText_);
_responseLoginServiceLayer.ServicioActivo = true;
_responseLoginServiceLayer.MensajeLogin = "Cierre de Sesió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)
{
try
@@ -396,137 +218,29 @@ namespace Net.Connection.ServiceLayer
// PASAMOS LA EXCEPCIÓN A LA APP.
}
}
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);
}
public async Task<T> PostReOpen<T>(string Query)
// POST sin cuerpo para reabrir un documento (ej. Invoices(n)/Reopen). SAP responde 204 sin contenido y se toma como exito
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
@@ -559,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)
@@ -577,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;
@@ -608,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();
@@ -647,106 +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.
}
}
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
@@ -765,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;
}
@@ -786,230 +373,7 @@ namespace Net.Connection.ServiceLayer
// PASAMOS LA EXCEPCIÓN A LA APP.
}
}
public async Task<T> CancelAsyncSBA<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"));
//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_);
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_);
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
response_.StatusCode != System.Net.HttpStatusCode.NoContent) // 204
{
//NumError += 1;
_responseLoginServiceLayer = JsonConvert.DeserializeObject<ResponseLoginServiceLayer>(responseText_);
_responseLoginServiceLayer.ServicioActivo = false;
dynamic responseBody = JsonConvert.DeserializeObject<T>(responseText_);
var estado = (int)response_.StatusCode;
//responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "Error Services : " + _responseLoginServiceLayer.error.message.ToString();
return responseBody;
}
// RETORNAMOS EL OBJETO POR DEFECTO ESPERADO
return default(T);
}
}
catch (Exception ex)
{
return default(T);
throw new Exception(ex.Message.ToString());
}
}
public async Task<T> DeleteAsyncSBA<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("DELETE");
// 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"));
//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_);
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_);
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
response_.StatusCode != System.Net.HttpStatusCode.NoContent) // 204
{
//NumError += 1;
_responseLoginServiceLayer = JsonConvert.DeserializeObject<ResponseLoginServiceLayer>(responseText_);
_responseLoginServiceLayer.ServicioActivo = false;
dynamic responseBody = JsonConvert.DeserializeObject<T>(responseText_);
var estado = (int)response_.StatusCode;
//responseBody.CodigoHttp = estado.ToString();
responseBody.Mensaje = "Error Services : " + _responseLoginServiceLayer.error.message.ToString();
return responseBody;
}
// RETORNAMOS EL OBJETO POR DEFECTO ESPERADO
return default(T);
}
}
catch (Exception ex)
{
return default(T);
throw new Exception(ex.Message.ToString());
}
}
// PATCH con cuerpo JSON. OJO: con un 204 real devuelve null sin marcar exito (bug conocido); para PATCH sin respuesta usar PatchNoContentAsync
public async Task<T> PatchAsyncSBA<T>(string Query, object obj)
{
try
@@ -1091,7 +455,7 @@ namespace Net.Connection.ServiceLayer
if (_responseLoginServiceLayer.error.code.ToString() == "301")
{
await LoginPost();
await Login();
goto band;
}
}
@@ -1127,7 +491,6 @@ namespace Net.Connection.ServiceLayer
catch (Exception ex)
{
return default(T);
throw new Exception(ex.Message.ToString());
}
}
@@ -28,20 +28,9 @@ namespace Net.Data
#endregion
#region SAP (SERVICES LAYER)
IBusinessPartnersRepository BusinessPartners { get; }
ICreditNotesRepository CreditNotes { get; }
IPurchaseInvoicesRepository PurchaseInvoices { get; }
IInvoiceRepository InvoiceRepository { get; }
IPurchaseDeliveryNotesRepository PurchaseDeliveryNotes { get; }
IPurchaseOrdersRepository PurchaseOrders { get; }
IAuthenticateRepository Authenticate { get; }
IItemsRepository Items { get; }
IReturnRequestRepository ReturnRequest { get; }
IStockTransferRepository StockTransfer { get; }
IInventoryGenEntriesRepository InventoryGenEntries { get; }
IInventoryGenExitsRepository InventoryGenExits { get; }
IHRRepository HR { get; }
IGlobalDataRepository GlobalData { get; }
#endregion
@@ -21,6 +21,8 @@ using System.Net.Http;
namespace Net.Data
{
// Expone solo los repositorios que usan los controllers. Los de Service Layer (CreditNotes, Invoice, StockTransfer, ReturnRequest...)
// se instancian directamente con new donde se necesitan.
public class RepositoryWrapper : IRepositoryWrapper
{
#region Variables de conexion
@@ -42,7 +44,6 @@ namespace Net.Data
private IReciboRepository _Recibo;
private ITransferenciaRepository _Transferencia;
private IStockTransferRepository _StockTransfer;
private IPickingRepository _Picking;
private IInventarioRepository _Inventario;
private ISolicitudTransferenciaRepository _SolicitudTransferencia;
@@ -51,19 +52,9 @@ namespace Net.Data
#region SAP (SERVICES LAYER)
private IBusinessPartnersRepository _BusinessPartners;
private IPurchaseInvoicesRepository _PurchaseInvoices;
private IInvoiceRepository _Invoice;
private ICreditNotesRepository _CreditNotes;
private IAuthenticateRepository _AuthenticateRepository;
private IPurchaseDeliveryNotesRepository _PurchaseDeliveryNotes;
private IPurchaseOrdersRepository _PurchaseOrders;
private IItemsRepository _Items;
private IReturnRequestRepository _ReturnRequest;
private IInventoryGenEntriesRepository _InventoryGenEntries;
private IInventoryGenExitsRepository _InventoryGenExits;
private IHRRepository _HR;
private IGlobalDataRepository _GlobalData;
#endregion
@@ -162,79 +153,6 @@ namespace Net.Data
#endregion
#region SAP (SERVICES LAYER)
public IBusinessPartnersRepository BusinessPartners
{
get
{
if (_BusinessPartners == null)
{
_repoContext._cnx = _conSQL;
_BusinessPartners = new BusinessPartnersRepository(_clientFactory, _configuration, _repoContext);
}
return _BusinessPartners;
}
}
public ICreditNotesRepository CreditNotes
{
get
{
if (_CreditNotes == null)
{
_repoContext._cnx = _conSQL;
_CreditNotes = new CreditNotesRepository(_clientFactory, _configuration, _repoContext);
}
return _CreditNotes;
}
}
public IInvoiceRepository InvoiceRepository
{
get
{
if (_Invoice == null)
{
_repoContext._cnx = _conSQL;
_Invoice = new InvoiceRepository(_clientFactory, _configuration, _repoContext);
}
return _Invoice;
}
}
public IPurchaseInvoicesRepository PurchaseInvoices
{
get
{
if (_PurchaseInvoices == null)
{
_repoContext._cnx = _conSQL;
_PurchaseInvoices = new PurchaseInvoicesRepository(_clientFactory, _configuration);
}
return _PurchaseInvoices;
}
}
public IPurchaseDeliveryNotesRepository PurchaseDeliveryNotes
{
get
{
if (_PurchaseDeliveryNotes == null)
{
_repoContext._cnx = _conSQL;
_PurchaseDeliveryNotes = new PurchaseDeliveryNotesRepository(_clientFactory, _configuration);
}
return _PurchaseDeliveryNotes;
}
}
public IPurchaseOrdersRepository PurchaseOrders
{
get
{
if (_PurchaseOrders == null)
{
_repoContext._cnx = _conSQL;
_PurchaseOrders = new PurchaseOrdersRepository(_clientFactory, _configuration);
}
return _PurchaseOrders;
}
}
public IAuthenticateRepository Authenticate
{
get
@@ -247,54 +165,6 @@ namespace Net.Data
return _AuthenticateRepository;
}
}
public IReturnRequestRepository ReturnRequest
{
get
{
if(_ReturnRequest == null)
{
_repoContext._cnx = _conSQL;
_ReturnRequest = new ReturnRequestRepository(_clientFactory, _configuration);
}
return _ReturnRequest;
}
}
public IStockTransferRepository StockTransfer
{
get
{
if (_StockTransfer == null)
{
_repoContext._cnx = _conSQL;
_StockTransfer = new StockTransferRepository(_clientFactory, _configuration);
}
return _StockTransfer;
}
}
public IInventoryGenEntriesRepository InventoryGenEntries
{
get
{
if (_InventoryGenEntries == null)
{
_repoContext._cnx = _conSQL;
_InventoryGenEntries = new InventoryGenEntriesRepository(_clientFactory, _configuration, _repoContext);
}
return _InventoryGenEntries;
}
}
public IInventoryGenExitsRepository InventoryGenExits
{
get
{
if (_InventoryGenExits == null)
{
_repoContext._cnx = _conSQL;
_InventoryGenExits = new InventoryGenExitsRepository(_clientFactory, _configuration, _repoContext);
}
return _InventoryGenExits;
}
}
public IHRRepository HR
{
@@ -309,18 +179,6 @@ namespace Net.Data
}
}
public IGlobalDataRepository GlobalData
{
get
{
if (_GlobalData == null)
{
_repoContext._cnx = _conSQL;
_GlobalData = new GlobalDataRepository(_clientFactory, _configuration, _repoContext);
}
return _GlobalData;
}
}
#endregion
}
}
-27
View File
@@ -1,27 +0,0 @@
using Sap.Data.Hana;
using System.Data;
namespace Net.Data.HelperData
{
public class HelperDB
{
public static void SET(string query)
{
using (HanaConnection con = new HanaConnection(LocalData.ConnectionString))
{
con.Open();
HanaCommand cmd = new HanaCommand
{
CommandText = query,
Connection = con,
CommandType = CommandType.Text
};
cmd.ExecuteNonQuery();
con.Close();
}
}
}
}
@@ -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;
}
}
}
@@ -36,6 +36,8 @@ namespace Net.Data.ServicesLayer.CreditNotes
_context = context;
}
// Crea la nota de credito en SAP (POST CreditNotes). Si SAP rechaza, devuelve StatusCode -1 con el mensaje de SAP;
// el JSON enviado queda en el log (ver ConnectionServiceLayer.PostAsync)
public async Task<BE_ResultadoTransaccion<BE_BaseResponseSap>> SetCreateCreditNotes(BE_CreditNotesNotes entidad)
{
_metodoName = regex.Match(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name).Groups[1].Value.ToString();
@@ -62,6 +64,7 @@ namespace Net.Data.ServicesLayer.CreditNotes
}
// Actualiza una NC ya creada (PATCH); se usa para agregar las referencias de las transferencias automaticas
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();
-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());
}
}
}
@@ -72,6 +72,7 @@ namespace Net.Data.ServicesLayer
}
// GET INVOICES
// Lee una factura de venta (GET Invoices(docEntry)): cabecera, campos U_SYP_*, lineas con BaseType/BaseEntry/BaseLine y lotes
public async Task<BE_ResultadoTransaccion<BE_InvoicesConsulta>> GetInvoice(int docEntry)
{
_metodoName = regex.Match(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name).Groups[1].Value.ToString();
@@ -111,6 +112,7 @@ namespace Net.Data.ServicesLayer
}
}
// Reabre una factura cerrada (POST Invoices(n)/Reopen); la NC solo se puede crear sobre una factura abierta
public async Task<BE_ResultadoTransaccion<BE_BaseResponseSap>> ReOpen(int docEntry)
{
_metodoName = regex.Match(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name).Groups[1].Value.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;
}
}
}
}
+19 -202
View File
@@ -52,6 +52,10 @@ namespace Net.Data.WMS.Picking
_aplicacionName = GetType().Name;
}
// POST /api/Picking: actualiza el estado de la factura en HANA y, segun Proceso + IdEstadoCubico, genera documentos en SAP:
// DESPACHO (estado 5/6) -> NC por lo no entregado + transferencias a los almacenes destino (U_ITC_ALMDES)
// PICKING (estado 13) -> NC con un lote nuevo por linea + transferencias
// ULTIMA MILLA (estado 23, solo con Notificacion = "cierre") -> Solicitud de Devolucion
public async Task<BE_ResultadoTransaccion<object>> Create(BE_PickingRequest entidad)
{
_metodoName = regex.Match(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name).Groups[1].Value.ToString();
@@ -67,6 +71,7 @@ namespace Net.Data.WMS.Picking
{
try
{
// Datos del motivo (@SYP_MOTIVOS): almacen destino, si es reproceso y tipo/descripcion de la NC
var Almacen = HelperProcesoPermitido.GetMotivoAlmacen(entidad.CodMotivo, out bool Reproceso, out string MotivoNotaCredito, out string DescripcionMotivo);
if (string.IsNullOrEmpty(entidad.Proceso))
@@ -88,13 +93,11 @@ namespace Net.Data.WMS.Picking
var TotalDetalle = 0;
var TotalDetalle_Anulado = 0;
var TotalDetalle_Parcial = 0;
var TotalDetalle_Completo = 0;
if (Existe > 1)
{
TotalDetalle = entidad.Detalle.Count();
TotalDetalle_Anulado = entidad.Detalle.Where(x=> x.Estado == "A").Count();
TotalDetalle_Parcial = entidad.Detalle.Where(x => x.Estado == "P").Count();
TotalDetalle_Completo = entidad.Detalle.Where(x => x.Estado == "C").Count();
if(TotalDetalle == TotalDetalle_Anulado)
{
@@ -113,7 +116,6 @@ namespace Net.Data.WMS.Picking
{
TotalDetalle = entidad.Detalle.Count();
TotalDetalle_Anulado = entidad.Detalle.Where(x => x.Estado == "A").Count();
TotalDetalle_Parcial = entidad.Detalle.Where(x => x.Estado == "P").Count();
TotalDetalle_Completo = entidad.Detalle.Where(x => x.Estado == "C").Count();
}
@@ -129,6 +131,7 @@ namespace Net.Data.WMS.Picking
{
return resultadoTransaccion.GetInformacionResultado(entidad.DocEntry, entidad.NumOrden, "Estado actualizado correctamente", entidad);
}
// Todas las lineas completas: no hay nada que devolver (el tipo de documento 9 sigue el flujo)
if(TotalDetalle == TotalDetalle_Completo && entidad.IdTipoDoc != 9)
{
return resultadoTransaccion.GetInformacionResultado(entidad.DocEntry, entidad.NumOrden, "Se registro correctamente...!!!", entidad);
@@ -136,6 +139,7 @@ namespace Net.Data.WMS.Picking
#region PROCESO DE NOTA DE CREDITO
// DESPACHO (estado 5 o 6): NC desde la factura; no aplica para IdTipoDoc 9
if (entidad.Proceso.ToUpper() == "DESPACHO" && (entidad.IdEstadoCubico == 6 || entidad.IdEstadoCubico == 5))
{
if (entidad.IdTipoDoc != 9)
@@ -167,6 +171,8 @@ namespace Net.Data.WMS.Picking
}
}
// Serie de la NC segun el TIPO de documento de la factura (U_SYP_MDTD): 03 = boleta -> BC10, otro -> FC10.
// El correlativo sale de @SYP_NUMDOC (HelperCorrelativo)
string Serie = _invoices.U_SYP_MDTD == "03" ? "BC10" : "FC10";
string Correlativo = HelperCorrelativo.Get(Serie);
creditNotesNotes.DocCurrency = _invoices.DocCurrency;
@@ -293,105 +299,8 @@ namespace Net.Data.WMS.Picking
}
else
{
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;
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);
}
}
@@ -500,6 +409,7 @@ namespace Net.Data.WMS.Picking
}
}
// PICKING (estado 13): NC con un lote nuevo (fecha y hora) por linea, sin usar los lotes de la factura; solo lineas con diferencia
if (entidad.Proceso.ToUpper() == "PICKING" && entidad.IdEstadoCubico == 13)
{
CreditNotesRepository _creditNotesRepository = new CreditNotesRepository(_clientFactory, _configuration, context);
@@ -527,6 +437,7 @@ namespace Net.Data.WMS.Picking
}
}
// Serie segun el tipo de documento de la factura (03 = boleta -> BC10, otro -> FC10)
string Serie = _invoices.U_SYP_MDTD == "03" ? "BC10" : "FC10";
string Correlativo = HelperCorrelativo.Get(Serie);
@@ -571,7 +482,6 @@ namespace Net.Data.WMS.Picking
}
});
var _Invoice = response_invoicesRepository.ListData.FirstOrDefault();
creditNotesNotes.DocumentLines.ForEach(line =>
{
@@ -588,100 +498,8 @@ namespace Net.Data.WMS.Picking
else
{
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;
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);
}
}
@@ -690,6 +508,8 @@ namespace Net.Data.WMS.Picking
return resultadoTransaccion.GetInformacionResultado((int)entidad.DocEntry, entidad.NumOrden.ToString(), "Se registro correctamente...!!!", entidad);
}
}
// ULTIMA MILLA (estado 23): con Notificacion = "cierre" (el filtro esta arriba) y el proceso configurado como SOLICITUD DEV,
// crea la Solicitud de Devolucion con lo no entregado; no genera NC ni transferencias
if(entidad.Proceso.ToUpper() == "ULTIMA MILLA" && entidad.IdEstadoCubico == 23)
{
if(Proceso.U_ITCA_GNRPROC == "SOLICITUD DEV" && !Reproceso)
@@ -769,6 +589,7 @@ namespace Net.Data.WMS.Picking
});
}
// POST /api/Picking/ruta: registra el estado de la hoja de ruta (inicio/fin de ruta) en HANA; no crea documentos en SAP
public async Task<BE_ResultadoTransaccion<object>> CreateHojaRuta(BE_HojaRutaEntities entidad)
{
_metodoName = regex.Match(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name).Groups[1].Value.ToString();
@@ -801,11 +622,6 @@ namespace Net.Data.WMS.Picking
}
HelperProcesoPiciking.Set(entidad.Proceso, entidad.IdEstadoCubico, entidad.DocEntry, HelperProcesoPermitido.ConvertTimeToInteger(entidad.HoraInicio), HelperProcesoPermitido.ConvertTimeToInteger(entidad.FinRuta), entidad.CodMotivo, ListaProceso.FirstOrDefault().Code);
if (entidad.Proceso == "Picking")
{
//=== IDENTIFICA DESCRIPCION DE ESTADO
}
return resultadoTransaccion.GetInformacionResultado(entidad.DocEntry, entidad.DocEntry.ToString(), "Registrado Correctamente", entidad);
}
catch (Exception ex)
@@ -815,6 +631,7 @@ namespace Net.Data.WMS.Picking
});
}
// POST /api/Picking/HojaRuta/{docEntry}/Cerrar: cierra el UDO ARD_PHR en SAP (se llama una sola vez al terminar la ruta)
public async Task<BE_ResultadoTransaccion<object>> CerrarHojaRuta(int docEntry)
{
_metodoName = "CerrarHojaRuta";
+8 -97
View File
@@ -345,6 +345,8 @@ namespace Net.Data.WMS.Recibo
#endregion
// DEVOLUCION: NC (oCreditNotes) con base en la Solicitud de Devolucion (BaseType 234000031).
// Si las lineas de la solicitud vienen de una factura se usan los datos y lotes de la factura; si no, los de la solicitud y el stock de HANA
#region PROCESO DE NOTA DE CREDITO
if (entidad.IdTipo == EnumRecibo.DEVOLUCION)
@@ -379,7 +381,6 @@ namespace Net.Data.WMS.Recibo
_Invoice = _Invoice_Result.ListData.FirstOrDefault();
}
});
string SerieOrigen = "";
string U_SYP_MDTO = "";
string U_SYP_MDCO = "";
string U_SYP_MDSO = "";
@@ -387,7 +388,6 @@ namespace Net.Data.WMS.Recibo
if (_Invoice == null)
{
SerieOrigen = returnRequest.U_SYP_MDSO;
U_SYP_MDTO = returnRequest.U_SYP_MDTO;
U_SYP_MDCO = returnRequest.U_SYP_MDCO;
U_SYP_MDSO = returnRequest.U_SYP_MDSO;
@@ -395,7 +395,6 @@ namespace Net.Data.WMS.Recibo
}
else
{
SerieOrigen = _Invoice.U_SYP_MDSD;
U_SYP_MDTO = _Invoice.U_SYP_MDTD;
U_SYP_MDCO = _Invoice.U_SYP_MDCD;
U_SYP_MDSO = _Invoice.U_SYP_MDSD;
@@ -404,6 +403,7 @@ namespace Net.Data.WMS.Recibo
#endregion
// Serie segun el TIPO de documento origen (03 = boleta), no segun su serie
// El correlativo sale de @SYP_NUMDOC (HelperCorrelativo)
string Serie = U_SYP_MDTO == "03" ? "BC10" : "FC10";
string Correlativo = HelperCorrelativo.Get(Serie);
creditNotesNotes.DocCurrency = returnRequest.DocCurrency;
@@ -452,6 +452,8 @@ namespace Net.Data.WMS.Recibo
}
});
// Lotes de cada linea: sin factura relacionada, FIFO por vencimiento del stock de HANA; con factura, los lotes de la factura.
// En ambos casos se parte de la lista vacia para que los lotes sumen exactamente la cantidad de la linea
if (_Invoice == null)
{
//=== REALIZA DISTRIBUCION DE STOCK POR LOTE
@@ -535,100 +537,9 @@ namespace Net.Data.WMS.Recibo
}
else
{
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;
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.
}
}
+62 -7
View File
@@ -12,7 +12,10 @@ API de integración WMS ↔ SAP Business One (Service Layer + HANA directo),
(ruta hardcodeada vía `HintPath` en los `.csproj` de `Net.Data` y
`Net.Business.Services` — sin esto no compila ningún proyecto que dependa
de HANA).
- **Compilar desde macOS/Linux**: el `HintPath` es relativo (6 niveles hacia
- **Compilar desde macOS/Linux** (forma simple): pasar la ruta de la DLL con
`-p:HanaDll=<ruta>/Sap.Data.Hana.Core.v2.1.dll` a `dotnet build`/`publish`
(los `.csproj` usan esa propiedad si está definida). Forma alternativa: el
`HintPath` es relativo (6 niveles hacia
arriba + `Program Files\sap\...`). Se puede compilar copiando el repo a
`<raiz>/a/b/c/d/repo/` y poniendo la DLL en
`<raiz>/Program Files/sap/hdbclient/dotnetcore/v2.1/` — así el `HintPath`
@@ -70,6 +73,58 @@ dotnet publish Net.Business.Services/Net.Business.Services.csproj -c Release -r
`web.config` del código fuente (ese apunta a `bin\Debug\...`, es el de
desarrollo) a una carpeta de publish.
## 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.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
`-p:HanaDll=...`, se borran `appsettings.json`/`appsettings.Development.json` de
la salida y se comprime.
- Control de versiones: rama `main` y etiquetas `vX.Y.Z.W` por versión
publicada (ver `CLAUDE.md`).
## Volver a la versión 2.1.7.0 (punto de retorno antes de la limpieza)
La etiqueta `v2.1.7.0` (commit `d0118d1`) es el último estado antes de la
limpieza de código (rama `limpieza-2.1.8.0`, versión 2.1.8.0). Está en el
remoto `origin` y en el repositorio local.
```
git fetch --tags # si la etiqueta no está en tu copia local
git tag -l "v2.1.*" # ver las etiquetas disponibles
git diff v2.1.7.0 --stat # qué cambió respecto a 2.1.7.0
git checkout v2.1.7.0 # ver/compilar exactamente esa versión (solo lectura)
git switch -c recuperar-2.1.7.0 v2.1.7.0 # trabajar desde ahí en una rama nueva
```
- Para **volver a publicar 2.1.7.0**: `git checkout v2.1.7.0` y generar el
publish como en "Versión y script de publish" (`AssemblyVersion` ya vale
2.1.7.0 en ese commit). Luego `git switch main` para regresar.
- Si la limpieza se une a `main` y hay que **deshacerla**, lo más seguro es
`git revert` del merge (no reescribe el historial). Evitar `git reset --hard`
y `git push --force` sobre `main`.
- La rama `main` no se toca durante la limpieza; el trabajo va en
`limpieza-2.1.8.0`.
## Logs de integración
- **`B_Log`** (controllers): en cada llamada guarda en
`ITC_WMS_SAP.ITCA_LOG_INTEGRACION` el request del WMS (`TRAMA_ORIGEN`) y la
respuesta de la API (`TRAMA_RESPUESTA`), con `PROCESO` (Picking, Recibo...),
`NRO_ORDEN`, `DOCENTRY`, `DOCNUMSAP` y `DOCENTRYSAP`.
- **Log de errores del Service Layer** (desde 2.1.5.0): cuando SAP rechaza un
envío con cuerpo JSON (`PostAsync`, `PatchAsyncSBA`, `PatchNoContentAsync`)
se guarda en esa misma tabla el JSON enviado y el mensaje de SAP, con
`PROCESO = 'ServiceLayer Error'`, `NRO_ORDEN` = ruta del recurso y `DOCENTRY`
= `BaseEntry` del documento base. Solo en error; no cambia la respuesta de la
API.
- Ambos usan el esquema fijo `ITC_WMS_SAP` (columnas `PROCESO`, `NRO_ORDEN` y
`DOCENTRY` son `VARCHAR(100)`).
- Consulta de ejemplo:
`SELECT * FROM "ITC_WMS_SAP"."ITCA_LOG_INTEGRACION" WHERE "PROCESO" = 'ServiceLayer Error' ORDER BY "FECHA" DESC, "HORA" DESC`
## Desplegar/actualizar un sitio en IIS
### Si el sitio YA corre desde una carpeta de publish (actualización de versión)
@@ -137,13 +192,13 @@ para armar el body del `Login` contra el Service Layer.
## Riesgos de seguridad heredados (documentados, no corregidos sin coordinar antes)
- `appsettings.json` tiene credenciales reales en texto plano (SAP Service
Layer, **HANA `B1ADMIN`**, semilla JWT) — no hay `git` en esta carpeta; si alguna vez se versiona o se sube
a un remoto compartido, hay que decidir qué hacer con ese archivo primero.
Layer, **HANA `B1ADMIN`**, semilla JWT). Está en `.gitignore` y no se
versiona (solo `appsettings.example.json`); no subirlo a ningún repositorio.
- `B1ADMIN` es un usuario administrador de SAP B1; la API solo necesita
ejecutar los `ITCA_*` y leer algunas tablas. Conviene un usuario HANA de
permisos mínimos y rotar la contraseña (estuvo en texto plano en el código
y en el historial git del repo antiguo archivado).
- `ValidarCertificado` en `ConnectionServiceLayer.cs` acepta cualquier
certificado TLS (`return true;` siempre) — bypass de validación de
certificado hacia el Service Layer, igual criterio que
`SERVICE_LAYER_VERIFY_SSL=false` en la reescritura Python.
- El cliente HTTP `bypass-ssl-validation` (usado por `ConnectionServiceLayer`)
acepta cualquier certificado TLS — bypass de validación de certificado hacia
el Service Layer, igual criterio que `SERVICE_LAYER_VERIFY_SSL=false` en la
reescritura Python. (Se quitó el método `ValidarCertificado`, que no se usaba.)
+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.