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>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-16 00:22:11 -05:00
1 parent 31b0929342
commit baaf85554a
7 files changed
+198 -26

No files matched your search

+17 -1
View File
@@ -10,7 +10,7 @@ comprimido (versión antigua)**.
## Versión y control de versiones
- **Versión actual: 2.1.8.0** (rama `limpieza-2.1.8.0`; `main` = 2.1.7.0) (`Net.Business.Services.csproj`: `Version`,
- **Versión actual: 2.1.9.0** (rama `limpieza-2.1.8.0`; `main` = 2.1.7.0) (`Net.Business.Services.csproj`: `Version`,
`AssemblyVersion`, `FileVersion`). Cada versión tiene su commit; al publicar
se sube la versión (ver `publicar.ps1`).
- **Git**: rama `main`, remoto `origin` =
@@ -106,6 +106,22 @@ transferencias siempre, sin depender de `Notificacion`. Detalle y tabla en
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`.
- 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.
@@ -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; }
}
}
@@ -4,8 +4,8 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>ITCA_API_SAP_WMS</AssemblyName>
<Version>2.1.8</Version>
<AssemblyVersion>2.1.8.0</AssemblyVersion>
<FileVersion>2.1.8.0</FileVersion>
<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
+45 -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,47 @@ 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.
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;
}
}
}
@@ -0,0 +1,53 @@
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).
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,80 @@ 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
if (entidad.CodigoSubAlmacenOrigen == "ALM99")
{
StockTransfer.U_SYP_MDMT = "21";
int DocEntry_Entada = entidad.DocEntryEM; //HelperInvoice.Get(entidad.NumOrden);
if(DocEntry_Entada > 0)
PurchaseDeliveryNotesRepository _purchaseDeliveryNotesRepository = new PurchaseDeliveryNotesRepository(_clientFactory, _configuration);
if (entidad.DocEntryEM > 0)
{
PurchaseDeliveryNotesRepository _purchaseDeliveryNotesRepository = new PurchaseDeliveryNotesRepository(_clientFactory, _configuration);
var _purchaseDeliveryNotesRepository_Response = _purchaseDeliveryNotesRepository.GetPurchaseDeliveryNotes(DocEntry_Entada).Result.ListData.FirstOrDefault();
//=== RESETEA STOCK POR LOTES
StockTransfer.StockTransferLines.ForEach(line =>
// WMS indica la entrada: se toman sus lotes y se amarra linea por linea (BaseLine del JSON == BaseLine de la entrada)
var pdn = _purchaseDeliveryNotesRepository.GetPurchaseDeliveryNotes(entidad.DocEntryEM).Result.ListData?.FirstOrDefault();
if (pdn == null)
{
var Lines = _purchaseDeliveryNotesRepository_Response.DocumentLines.FirstOrDefault(line_purchaseDeliveryNotesRepository_Response => line_purchaseDeliveryNotesRepository_Response.BaseLine == line.LineNum);
line.BatchNumbers = new List<BE_StockTransferLineBatchNumber>();
return resultadoTransaccion.GetInformacionResultado($"SAP: La entrada de mercaderia {entidad.DocEntryEM} no se encuentra", entidad);
}
//=== AGREGA LOTES
Lines.BatchNumbers.ForEach(lotes =>
foreach (var line in StockTransfer.StockTransferLines)
{
var Lines = pdn.DocumentLines.FirstOrDefault(l => l.BaseLine == line.LineNum);
if (Lines == null)
{
line.BatchNumbers.Add(new BE_StockTransferLineBatchNumber()
{
BatchNumber = lotes.BatchNumber,
Quantity = lotes.Quantity,
});
});
return resultadoTransaccion.GetInformacionResultado($"SAP: La linea {line.LineNum} no existe en la entrada de mercaderia {entidad.DocEntryEM}", entidad);
}
line.BatchNumbers = (Lines.BatchNumbers ?? new List<BE_PurchaseDeliveryNotesBatchNumbers>())
.Select(lotes => new BE_StockTransferLineBatchNumber() { BatchNumber = lotes.BatchNumber, Quantity = lotes.Quantity }).ToList();
line.BaseLine = Lines.LineNum;
line.BaseType = "PurchaseDeliveryNotes";
line.BaseEntry = DocEntry_Entada;
});
line.BaseEntry = entidad.DocEntryEM;
}
}
else
{
// WMS envia en NumOrden el DocNum de la Factura de Reserva y en el detalle los articulos/cantidades de UNA de sus
// entradas: se busca la entrada con ese mismo juego que aun no tenga transferencia y se transfiere esa.
var filas = HelperInvoice.GetEntradasPorFactura(entidad.NumOrden);
if (filas.Count > 0)
{
int docEntryEntrada = EntradaImportacion.Elegir(entidad.NumOrden, filas, entidad.TransferenciaDetalles, out string errorEntrada);
if (docEntryEntrada == 0)
{
return resultadoTransaccion.GetInformacionResultado(errorEntrada, entidad);
}
var pdn = _purchaseDeliveryNotesRepository.GetPurchaseDeliveryNotes(docEntryEntrada).Result.ListData?.FirstOrDefault();
if (pdn == null)
{
return resultadoTransaccion.GetInformacionResultado($"SAP: La entrada de mercaderia {docEntryEntrada} no se puede leer", entidad);
}
// Las lineas de la transferencia salen de la entrada (articulo, cantidad y lotes reales de cada linea)
StockTransfer.StockTransferLines = new List<BE_StockTransferLine>();
foreach (var l in pdn.DocumentLines)
{
var lotes = (l.BatchNumbers ?? new List<BE_PurchaseDeliveryNotesBatchNumbers>())
.Select(b => new BE_StockTransferLineBatchNumber() { BatchNumber = b.BatchNumber, Quantity = b.Quantity }).ToList();
if (lotes.Count > 0 && Math.Abs(lotes.Sum(b => b.Quantity) - l.Quantity) > 0.0001)
{
return resultadoTransaccion.GetInformacionResultado($"SAP: Los lotes de la linea {l.LineNum} ({l.ItemCode}) de la entrada {docEntryEntrada} no cubren la cantidad {l.Quantity}", entidad);
}
StockTransfer.StockTransferLines.Add(new BE_StockTransferLine
{
FromWarehouseCode = entidad.CodigoSubAlmacenOrigen,
WarehouseCode = entidad.CodigoSubAlmacenDestino,
ItemCode = l.ItemCode,
Quantity = l.Quantity,
LineNum = l.LineNum,
BaseType = "PurchaseDeliveryNotes",
BaseEntry = docEntryEntrada,
BaseLine = l.LineNum,
BatchNumbers = lotes
});
}
}
// Si la factura no tiene entradas (o NumOrden no es una factura de reserva) se mantiene lo de antes:
// transferencia con los lotes del stock de HANA, sin amarre.
}
}
+1 -1
View File
@@ -76,7 +76,7 @@ dotnet publish Net.Business.Services/Net.Business.Services.csproj -c Release -r
## Versión y script de publish
- La versión sale de `<Version>`/`<AssemblyVersion>`/`<FileVersion>` en
`Net.Business.Services/Net.Business.Services.csproj` (actual: **2.1.8.0** en la rama de limpieza; `main` = 2.1.7.0);
`Net.Business.Services/Net.Business.Services.csproj` (actual: **2.1.9.0** en la rama de limpieza; `main` = 2.1.7.0);
súbela en cada publish.
- `publicar.ps1` (solo Windows) genera `dist\ITCA_API_SAP_WMS_v<versión>.zip` sin
`appsettings.json`. Desde macOS se hace el mismo `dotnet publish` a mano con