Compare commits

4 Commits
Author SHA1 Message Date
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
11 changed files with 156 additions and 533 deletions

No files matched your search

+49 -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.8.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,15 @@ 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`. Sigue pendiente: código comentado, `Login`/`LoginPost`
duplicados y `PatchAsyncSBA`.
- 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 +148,20 @@ 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. `Login()`/`LoginPost()` duplicados.
- 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.
- En Picking y Recibo, si la transferencia automática de stock falla, el `if`
de error está vacío: el fallo se ignora (la NC ya está creada) y no se informa
al consumidor.
## Despliegue
@@ -152,3 +180,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`.
@@ -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.8</Version>
<AssemblyVersion>2.1.8.0</AssemblyVersion>
<FileVersion>2.1.8.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,6 +136,7 @@ namespace Net.Connection.ServiceLayer
return _responseLoginServiceLayer;
}
}
// Igual que Login(); duplicado historico (candidato a unificar)
public async Task<ResponseLoginServiceLayer> LoginPost()
{
try
@@ -228,93 +225,7 @@ namespace Net.Connection.ServiceLayer
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,6 +307,8 @@ namespace Net.Connection.ServiceLayer
// PASAMOS LA EXCEPCIÓN A LA APP.
}
}
// POST con cuerpo JSON. Reintenta una vez si la sesion expiro (401). Si SAP rechaza, devuelve Exito = false y guarda
// el JSON enviado con ErrorLogger (tabla ITCA_LOG_INTEGRACION)
public async Task<T> PostAsync<T>(string Query, object obj)
{
@@ -526,6 +439,7 @@ namespace Net.Connection.ServiceLayer
// PASAMOS LA EXCEPCIÓN A LA APP.
}
}
// POST sin cuerpo para reabrir un documento (ej. Invoices(n)/Reopen). SAP responde 204 sin contenido y se toma como exito
public async Task<T> PostReOpen<T>(string Query)
{
@@ -658,6 +572,7 @@ namespace Net.Connection.ServiceLayer
// PASAMOS LA EXCEPCIÓN A LA APP.
}
}
// POST sin cuerpo para cerrar un documento (.../Close)
public async Task<T> PostClose<T>(string Query)
{
@@ -786,230 +701,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
@@ -1127,7 +819,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();
}
}
}
}
@@ -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();
@@ -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();
+20 -9
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,6 +299,7 @@ namespace Net.Data.WMS.Picking
}
else
{
// Por cada almacen de origen y almacen destino (U_ITC_ALMDES) distintos se crea una transferencia de stock automatica
List<string> ListaUnicaAlmacen = creditNotesNotes.DocumentLines.Select(x => x.WarehouseCode).Distinct().ToList();
List<int> ListDocEntry = new List<int>();
ListaUnicaAlmacen.ForEach(Almacen =>
@@ -358,6 +365,7 @@ namespace Net.Data.WMS.Picking
//=== CREA INSTANCIA PARA CONECTAR A SAP
StockTransferRepository stockTransferRepository = new StockTransferRepository(_clientFactory, _configuration);
var ResultTransfer = stockTransferRepository.StockTransfer(Transfer).Result;
// OJO: si la transferencia falla se ignora (la NC ya esta creada) y no se informa al consumidor
if (ResultTransfer.StatusCode == -1)
{
@@ -371,6 +379,7 @@ namespace Net.Data.WMS.Picking
});
// Referencia las transferencias creadas en la NC
if (ListDocEntry.Count > 0)
{
var listaReferencia = new List<BE_CreditNotesDocumentReferences>();
@@ -500,6 +509,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 +537,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 +582,6 @@ namespace Net.Data.WMS.Picking
}
});
var _Invoice = response_invoicesRepository.ListData.FirstOrDefault();
creditNotesNotes.DocumentLines.ForEach(line =>
{
@@ -588,6 +598,7 @@ namespace Net.Data.WMS.Picking
else
{
// Transferencias automaticas por almacen origen/destino (igual que en DESPACHO)
List<string> ListaUnicaAlmacen = creditNotesNotes.DocumentLines.Select(x => x.WarehouseCode).Distinct().ToList();
List<int> ListDocEntry = new List<int>();
ListaUnicaAlmacen.ForEach(Almacen =>
@@ -650,6 +661,7 @@ namespace Net.Data.WMS.Picking
//=== CREA INSTANCIA PARA CONECTAR A SAP
StockTransferRepository stockTransferRepository = new StockTransferRepository(_clientFactory, _configuration);
var ResultTransfer = stockTransferRepository.StockTransfer(Transfer).Result;
// OJO: si la transferencia falla se ignora (la NC ya esta creada)
if (ResultTransfer.StatusCode == -1)
{
@@ -690,6 +702,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 +783,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 +816,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 +825,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";
+7 -3
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,6 +537,7 @@ namespace Net.Data.WMS.Recibo
}
else
{
// Transferencias por almacen destino (U_ITC_ALMDES). OJO: en Recibo las lineas de la NC no llenan U_ITC_ALMDES, por lo que hoy no se genera ninguna
List<string> ListaUnicaAlmacen = creditNotesNotes.DocumentLines.Select(x => x.WarehouseCode).Distinct().ToList();
List<int> ListDocEntry = new List<int>();
ListaUnicaAlmacen.ForEach(Almacen =>
@@ -597,6 +600,7 @@ namespace Net.Data.WMS.Recibo
//=== CREA INSTANCIA PARA CONECTAR A SAP
StockTransferRepository stockTransferRepository = new StockTransferRepository(_clientFactory, _configuration);
var ResultTransfer = stockTransferRepository.StockTransfer(Transfer).Result;
// OJO: si la transferencia falla se ignora (la NC ya esta creada)
if (ResultTransfer.StatusCode == -1)
{
+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.8.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.)