Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3111bb3842 | ||
|
|
d53d5d07c5 | ||
|
|
baaf85554a |
No files matched your search
@@ -10,7 +10,7 @@ comprimido (versión antigua)**.
|
|||||||
|
|
||||||
## Versión y control de versiones
|
## 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
|
`AssemblyVersion`, `FileVersion`). Cada versión tiene su commit; al publicar
|
||||||
se sube la versión (ver `publicar.ps1`).
|
se sube la versión (ver `publicar.ps1`).
|
||||||
- **Git**: rama `main`, remoto `origin` =
|
- **Git**: rama `main`, remoto `origin` =
|
||||||
@@ -106,6 +106,24 @@ transferencias siempre, sin depender de `Notificacion`. Detalle y tabla en
|
|||||||
mantiene su bug del 204 (sin marcar éxito; el único llamador ignora el resultado).
|
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)
|
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`.
|
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
|
- 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.
|
`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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<AssemblyName>ITCA_API_SAP_WMS</AssemblyName>
|
<AssemblyName>ITCA_API_SAP_WMS</AssemblyName>
|
||||||
<Version>2.1.8</Version>
|
<Version>2.1.9</Version>
|
||||||
<AssemblyVersion>2.1.8.0</AssemblyVersion>
|
<AssemblyVersion>2.1.9.0</AssemblyVersion>
|
||||||
<FileVersion>2.1.8.0</FileVersion>
|
<FileVersion>2.1.9.0</FileVersion>
|
||||||
<UserSecretsId>24763424-3300-4c85-87e3-503f8fdebd14</UserSecretsId>
|
<UserSecretsId>24763424-3300-4c85-87e3-503f8fdebd14</UserSecretsId>
|
||||||
<!-- Antes solo estaba condicionado a Debug|AnyCPU - un "dotnet publish -c Release"
|
<!-- 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
|
(lo normal para IIS) se quedaba sin este archivo, y Startup.cs revienta con
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Sap.Data.Hana;
|
using Net.Business.Entities.WMS.Transferencia;
|
||||||
|
using Sap.Data.Hana;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
|
||||||
namespace Net.Data.HelperData.Invoices
|
namespace Net.Data.HelperData.Invoices
|
||||||
@@ -30,5 +32,49 @@ namespace Net.Data.HelperData.Invoices
|
|||||||
|
|
||||||
return DocEntry;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
using Net.Business.Entities.ServicesLayer.Invoices;
|
using Net.Business.Entities.ServicesLayer.Invoices;
|
||||||
using Net.Business.Entities.ServicesLayer.Items;
|
using Net.Business.Entities.ServicesLayer.Items;
|
||||||
|
using Net.Business.Entities.ServicesLayer.PurchaseDeliveryNotes;
|
||||||
using Net.Business.Entities.ServicesLayer.StockTransfer;
|
using Net.Business.Entities.ServicesLayer.StockTransfer;
|
||||||
using Net.Business.Entities.WMS.Transferencia;
|
using Net.Business.Entities.WMS.Transferencia;
|
||||||
using Net.Connection;
|
using Net.Connection;
|
||||||
@@ -110,38 +111,88 @@ namespace Net.Data.WMS.Transferencia
|
|||||||
#endregion
|
#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")
|
if (entidad.CodigoSubAlmacenOrigen == "ALM99")
|
||||||
{
|
{
|
||||||
StockTransfer.U_SYP_MDMT = "21";
|
StockTransfer.U_SYP_MDMT = "21";
|
||||||
int DocEntry_Entada = entidad.DocEntryEM; //HelperInvoice.Get(entidad.NumOrden);
|
|
||||||
if(DocEntry_Entada > 0)
|
|
||||||
{
|
|
||||||
PurchaseDeliveryNotesRepository _purchaseDeliveryNotesRepository = new PurchaseDeliveryNotesRepository(_clientFactory, _configuration);
|
PurchaseDeliveryNotesRepository _purchaseDeliveryNotesRepository = new PurchaseDeliveryNotesRepository(_clientFactory, _configuration);
|
||||||
var _purchaseDeliveryNotesRepository_Response = _purchaseDeliveryNotesRepository.GetPurchaseDeliveryNotes(DocEntry_Entada).Result.ListData.FirstOrDefault();
|
|
||||||
|
|
||||||
//=== RESETEA STOCK POR LOTES
|
if (entidad.DocEntryEM > 0)
|
||||||
StockTransfer.StockTransferLines.ForEach(line =>
|
|
||||||
{
|
{
|
||||||
var Lines = _purchaseDeliveryNotesRepository_Response.DocumentLines.FirstOrDefault(line_purchaseDeliveryNotesRepository_Response => line_purchaseDeliveryNotesRepository_Response.BaseLine == line.LineNum);
|
// WMS indica la entrada: se toman sus lotes y se amarra linea por linea (BaseLine del JSON == BaseLine de la entrada)
|
||||||
line.BatchNumbers = new List<BE_StockTransferLineBatchNumber>();
|
var pdn = _purchaseDeliveryNotesRepository.GetPurchaseDeliveryNotes(entidad.DocEntryEM).Result.ListData?.FirstOrDefault();
|
||||||
|
if (pdn == null)
|
||||||
|
{
|
||||||
|
return resultadoTransaccion.GetInformacionResultado($"SAP: La entrada de mercaderia {entidad.DocEntryEM} no se encuentra", entidad);
|
||||||
|
}
|
||||||
|
|
||||||
//=== AGREGA LOTES
|
foreach (var line in StockTransfer.StockTransferLines)
|
||||||
Lines.BatchNumbers.ForEach(lotes =>
|
|
||||||
{
|
{
|
||||||
line.BatchNumbers.Add(new BE_StockTransferLineBatchNumber()
|
var Lines = pdn.DocumentLines.FirstOrDefault(l => l.BaseLine == line.LineNum);
|
||||||
|
if (Lines == null)
|
||||||
{
|
{
|
||||||
BatchNumber = lotes.BatchNumber,
|
return resultadoTransaccion.GetInformacionResultado($"SAP: La linea {line.LineNum} no existe en la entrada de mercaderia {entidad.DocEntryEM}", entidad);
|
||||||
Quantity = lotes.Quantity,
|
}
|
||||||
});
|
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.BaseLine = Lines.LineNum;
|
||||||
line.BaseType = "PurchaseDeliveryNotes";
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var ResultTransaction = stockTransferRepository.StockTransfer(StockTransfer).Result;
|
var ResultTransaction = stockTransferRepository.StockTransfer(StockTransfer).Result;
|
||||||
if (ResultTransaction.StatusCode == -1)
|
if (ResultTransaction.StatusCode == -1)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ dotnet publish Net.Business.Services/Net.Business.Services.csproj -c Release -r
|
|||||||
## Versión y script de publish
|
## Versión y script de publish
|
||||||
|
|
||||||
- La versión sale de `<Version>`/`<AssemblyVersion>`/`<FileVersion>` en
|
- 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.
|
súbela en cada publish.
|
||||||
- `publicar.ps1` (solo Windows) genera `dist\ITCA_API_SAP_WMS_v<versión>.zip` sin
|
- `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
|
`appsettings.json`. Desde macOS se hace el mismo `dotnet publish` a mano con
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in new issue
Block a user