From 973c8030d2835529214ae9b03dbbf21d1c286aa7 Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 16 Jun 2026 21:45:44 +0200 Subject: [PATCH] =?UTF-8?q?Refactor=20EK=C3=81ER:=20support=20multi-doc=20?= =?UTF-8?q?declarations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored EKÁER declaration logic to allow one EkaerHistory to reference multiple source documents via a new EkaerHistoryMapping junction table. Removed ForeignKey from EkaerHistory and updated all usages to use the Mappings collection. Updated service, controller, SignalR, and client interfaces to operate by EkaerHistoryId. Adjusted grid UI to display all mapped source IDs. Added EkaerHistoryMapping entity and updated documentation, constants, and ToonDescription attributes accordingly. --- .claude/settings.local.json | 3 ++- .../Services/Ekaer/FruitBankEkaerService.cs | 16 ++++++----- .../Services/Ekaer/IFruitBankEkaerService.cs | 8 +++--- FruitBank.Common/Entities/EkaerHistory.cs | 10 ++++--- .../Entities/EkaerHistoryMapping.cs | 26 ++++++++++++++++++ FruitBank.Common/FruitBankConstClient.cs | 1 + .../IFruitBankDataControllerCommon.cs | 2 +- .../FruitBankEkaerTests.cs | 18 +++++++++---- .../Grids/Ekaers/GridEkaerHistory.razor | 27 +++++++++++++++---- .../Grids/Ekaers/GridEkaerHistoryBase.cs | 6 +++-- .../SignalRs/FruitBankSignalRClient.cs | 2 +- 11 files changed, 90 insertions(+), 29 deletions(-) create mode 100644 FruitBank.Common/Entities/EkaerHistoryMapping.cs diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d1ba595c..696d3504 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -74,7 +74,8 @@ "WebFetch(domain:net.jogtar.hu)", "WebFetch(domain:www.itrack.hu)", "WebFetch(domain:docplayer.hu)", - "WebFetch(domain:supportcenter.devexpress.com)" + "WebFetch(domain:supportcenter.devexpress.com)", + "PowerShell($c = Get-Command claude -ErrorAction SilentlyContinue; if \\($c\\) { $c.Source; & claude --help 2>&1 | Select-Object -First 90 } else { \"claude not on PATH\" })" ] } } diff --git a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs index f20e8d4e..f0175bbf 100644 --- a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs +++ b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs @@ -37,19 +37,23 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService return _submitService.SubmitAsync(operations, cancellationToken); } - public EkaerHistory GenerateEkaerXmlDocument(ShippingDocument document, EkaerHistory? ekaerHistory = null) + public EkaerHistory GenerateEkaerXmlDocument(IReadOnlyCollection documents, EkaerHistory? ekaerHistory = null) { - ArgumentNullException.ThrowIfNull(document); - ekaerHistory ??= new EkaerHistory { ForeignKey = document.Id, IsOutgoing = false }; + ArgumentNullException.ThrowIfNull(documents); + if (documents.Count == 0) throw new ArgumentException("documents is empty", nameof(documents)); + ekaerHistory ??= new EkaerHistory { IsOutgoing = false }; - var currency = document.Partner?.Currency; - return TryConfigError(ekaerHistory, currency) ?? Finalize(ekaerHistory, _mapper.MapDocument(document, _settings.Company), currency); + // A csoport azonos Partneré (a kapu így csoportosít) → az első dokumentum pénzneme a mérvadó. + var currency = documents.First().Partner?.Currency; + // A csoport ÖSSZES dokumentuma EGY tradeCard-dá (összevont tömeg/érték): ToConsignment → BuildTradeCard. + var tradeCard = _mapper.BuildTradeCard(_mapper.ToConsignment(documents, _settings.Company)); + return TryConfigError(ekaerHistory, currency) ?? Finalize(ekaerHistory, tradeCard, currency); } public EkaerHistory GenerateEkaerXmlDocument(OrderDto order, EkaerHistory? ekaerHistory = null) { ArgumentNullException.ThrowIfNull(order); - ekaerHistory ??= new EkaerHistory { ForeignKey = order.Id, IsOutgoing = true }; + ekaerHistory ??= new EkaerHistory { IsOutgoing = true }; // Kimenő pénznem: jelenleg minden HUF (a deviza az OrderDto-ba kerül, amint bekötik) → ConversionRate = 1. const string currency = "HUF"; diff --git a/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs index 604ce31e..929d2107 100644 --- a/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs +++ b/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs @@ -20,14 +20,14 @@ public interface IFruitBankEkaerService Task SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default); /// - /// Egy szállítólevélből legenerálja az EKÁER tradeCard XML-t és validálja — a (meglévő vagy új) + /// Egy szállítólevél-CSOPORTBÓL (egy EKÁER-egység dokumentumai) legenerálja az EKÁER tradeCard XML-t és validálja — a (meglévő vagy új) /// rekordot tölti: XmlDoc + Status /// ( / ) + ErrorText. /// NEM perzisztál és NEM hív NAV-ot — a mentés (upsert) a hívó SignalR endpoint dolga. /// - /// A szállítólevél a betöltött gráffal (Partner, Items+ProductDto, Shipping→járművek). - /// A dokumentum meglévő rekordja (újrageneráláskor); null → új rekord készül. - EkaerHistory GenerateEkaerXmlDocument(ShippingDocument document, EkaerHistory? ekaerHistory = null); + /// A deklarációhoz tartozó szállítólevél-CSOPORT (egy (Shipping, Partner, PartnerDepot) egység) a betöltött gráffal (Partner, Items+ProductDto, Shipping→járművek) — összevont tömeggel/értékkel EGY tradeCard. + /// A csoport meglévő rekordja (újrageneráláskor); null → új rekord készül. + EkaerHistory GenerateEkaerXmlDocument(IReadOnlyCollection documents, EkaerHistory? ekaerHistory = null); /// /// Egy kimenő rendelésből () legenerálja az EKÁER tradeCard XML-t és validálja — diff --git a/FruitBank.Common/Entities/EkaerHistory.cs b/FruitBank.Common/Entities/EkaerHistory.cs index 34b9f56a..47a3060f 100644 --- a/FruitBank.Common/Entities/EkaerHistory.cs +++ b/FruitBank.Common/Entities/EkaerHistory.cs @@ -10,15 +10,12 @@ using Mango.Nop.Core.Entities; namespace FruitBank.Common.Entities; [AcBinarySerializable(false, true, false, true, false, false)] -[ToonDescription("NAV EKÁER declaration lifecycle record", Purpose = "Work-queue and audit row for one EKÁER road-freight declaration — one row per tradeCard: an incoming ShippingDocument (delivery note) or a completed outgoing Order. Tracks the declaration through generation, validation and submission to the Hungarian tax authority (NAV).")] +[ToonDescription("NAV EKÁER declaration lifecycle record", Purpose = "Work-queue and audit row for one EKÁER road-freight declaration. One declaration covers one or more source records via EkaerHistoryMapping (the link lives in that junction table, NOT here): inbound, the ShippingDocuments of one (Shipping, Partner, PartnerDepot) group aggregated into a single tradeCard; outbound, a single completed Order. Tracks the declaration through generation, validation and submission to the Hungarian tax authority (NAV).")] [Table(Name = FruitBankConstClient.EkaerHistoryDbTableName)] [System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.EkaerHistoryDbTableName)] public sealed class EkaerHistory: MgEntityBase, ITimeStampInfo { - [ToonDescription(Purpose = "Id of the source entity the declaration belongs to: ShippingDocument.Id when IsOutgoing is false (one declaration per delivery note, matching NAV's one-TCN-per-tradeCard granularity), Order id when IsOutgoing is true.")] - public int ForeignKey { get; set; } - [ToonDescription(Purpose = "Direction of the goods movement: false = incoming shipment (Shipping), true = outgoing delivery (Order).")] public bool IsOutgoing { get; set; } @@ -51,6 +48,11 @@ public sealed class EkaerHistory: MgEntityBase, ITimeStampInfo [ToonDescription(Purpose = "Validation or NAV submission error details for the ValidationError / SendError states. Null when the last operation succeeded.")] public string? ErrorText { get; set; } + /// A deklarációhoz tartozó forrás-rekordok (EkaerHistoryMapping): bejövő → N szállítólevél (ShippingDocument.Id), + /// kimenő → 1 rendelés (Order.Id). NEM auto-loadol — a DbTable GetAll(loadRelations: true) tölti (LoadWith). + [Association(ThisKey = nameof(Id), OtherKey = nameof(EkaerHistoryMapping.EkaerHistoryId), CanBeNull = true)] + public List? Mappings { get; set; } + public DateTime Created { get; set; } public DateTime Modified { get; set; } } diff --git a/FruitBank.Common/Entities/EkaerHistoryMapping.cs b/FruitBank.Common/Entities/EkaerHistoryMapping.cs new file mode 100644 index 00000000..27fc7b00 --- /dev/null +++ b/FruitBank.Common/Entities/EkaerHistoryMapping.cs @@ -0,0 +1,26 @@ +using AyCode.Core.Serializers.Attributes; +using AyCode.Core.Serializers.Toons; +using AyCode.Interfaces.TimeStampInfo; +using LinqToDB.Mapping; +using Mango.Nop.Core.Entities; + +namespace FruitBank.Common.Entities; + +[AcBinarySerializable(false, true, false, true, false, false)] +[ToonDescription("EkaerHistory ↔ source document/order junction (1:N)", Purpose = "Junction for the EKÁER declaration ↔ source relationship: one EkaerHistory can cover several inbound ShippingDocuments (grouped by Shipping + PartnerDepot) or a single outbound Order. ForeignKey is the source entity Id, interpreted via the parent EkaerHistory.IsOutgoing. Keeps the EKÁER link OUT of the core ShippingDocument/Order tables, so declarations can be deleted and regenerated without touching system data.")] +[Table(Name = FruitBankConstClient.EkaerHistoryMappingDbTableName)] +[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.EkaerHistoryMappingDbTableName)] +public sealed class EkaerHistoryMapping : MgEntityBase, ITimeStampInfo +{ + [ToonDescription(Purpose = "The owning EKÁER declaration (EkaerHistory.Id).")] + public int EkaerHistoryId { get; set; } + + [ToonDescription(Purpose = "Id of the covered source entity: ShippingDocument.Id when the parent EkaerHistory.IsOutgoing is false, Order id when true.")] + public int ForeignKey { get; set; } + + [Association(ThisKey = nameof(EkaerHistoryId), OtherKey = nameof(EkaerHistory.Id), CanBeNull = true)] + public EkaerHistory? EkaerHistory { get; set; } + + public DateTime Created { get; set; } + public DateTime Modified { get; set; } +} diff --git a/FruitBank.Common/FruitBankConstClient.cs b/FruitBank.Common/FruitBankConstClient.cs index e608f89c..81e49407 100644 --- a/FruitBank.Common/FruitBankConstClient.cs +++ b/FruitBank.Common/FruitBankConstClient.cs @@ -33,6 +33,7 @@ public static class FruitBankConstClient public const string PartnerDepotDbTableName = "fbPartnerDepot"; public const string EkaerHistoryDbTableName = "fbEkaerHistory"; + public const string EkaerHistoryMappingDbTableName = "fbEkaerHistoryMapping"; public const string OrderItemPalletDbTableName = "fbOrderItemPallet"; diff --git a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs index 7815ec09..23fe64e0 100644 --- a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs +++ b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs @@ -34,7 +34,7 @@ public interface IFruitBankDataControllerCommon public Task?> GetEkaerHistoriesByForeignKey(int foreignKey); public Task AddEkaerHistory(EkaerHistory ekaerHistory); public Task UpdateEkaerHistory(EkaerHistory ekaerHistory); - public Task GenerateEkaerXmlDocument(int foreignKey, bool isOutgoing); + public Task GenerateEkaerXmlDocument(int ekaerHistoryId); public Task CreateEkaerHistory(int foreignKey, bool isOutgoing); public Task CreateMissingEkaerHistories(DateTime fromDate); public Task GetEkaerHistoryCount(EkaerHistoryFilter filter); diff --git a/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs b/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs index c8e8c716..5aecaa92 100644 --- a/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs +++ b/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs @@ -41,8 +41,9 @@ namespace FruitBankHybrid.Shared.Tests Assert.IsNotNull(ekaerHistory, $"shippingDocument.Id: {shippingDocument.Id}"); Assert.IsGreaterThan(0, ekaerHistory.Id, $"shippingDocument.Id: {shippingDocument.Id}"); - Assert.AreEqual(shippingDocument.Id, ekaerHistory.ForeignKey); Assert.IsFalse(ekaerHistory.IsOutgoing); + Assert.IsNotNull(ekaerHistory.Mappings, $"Mappings null (loadRelations?); shippingDocument.Id: {shippingDocument.Id}"); + Assert.AreEqual(shippingDocument.Id, ekaerHistory.Mappings.Single().ForeignKey, $"A mapping nem a forrás-dokumentumra mutat; shippingDocument.Id: {shippingDocument.Id}"); } // Idempotencia: a második hívás a meglévőt adja vissza, nem duplikál. @@ -73,15 +74,18 @@ namespace FruitBankHybrid.Shared.Tests foreach (var shippingDocument in shippingDocuments) { - var ekaerHistory = await _signalRClient.GenerateEkaerXmlDocument(shippingDocument.Id, false); + // A generálás mostantól EkaerHistory-Id alapú → előbb a (mapping-elt) rekordot hozzuk létre. + var created = await _signalRClient.CreateEkaerHistory(shippingDocument.Id, false); + Assert.IsNotNull(created, $"CreateEkaerHistory null; shippingDocument.Id: {shippingDocument.Id}"); + var ekaerHistory = await _signalRClient.GenerateEkaerXmlDocument(created.Id); // A szerver által visszaadott állapot/hibalista logolása — az assertek ELŐTT, hogy hibánál is látsszon. Console.WriteLine($"doc#{shippingDocument.Id}: Status: {(ekaerHistory == null ? "NULL VÁLASZ!" : ekaerHistory.Status.ToString())}"); if (!string.IsNullOrWhiteSpace(ekaerHistory?.ErrorText)) Console.WriteLine($" ErrorText: {ekaerHistory.ErrorText}"); Assert.IsNotNull(ekaerHistory, $"shippingDocument.Id: {shippingDocument.Id}"); - Assert.AreEqual(shippingDocument.Id, ekaerHistory.ForeignKey); Assert.IsFalse(ekaerHistory.IsOutgoing); + Assert.AreEqual(shippingDocument.Id, ekaerHistory.Mappings!.Single().ForeignKey, $"A mapping nem a forrás-dokumentumra mutat; shippingDocument.Id: {shippingDocument.Id}"); Assert.IsFalse(string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc), $"XmlDoc üres; shippingDocument.Id: {shippingDocument.Id}"); Assert.IsTrue(ekaerHistory.Status is EkaerStatus.Generated or EkaerStatus.ValidationError, $"Status: {ekaerHistory.Status}; shippingDocument.Id: {shippingDocument.Id}; ErrorText: {ekaerHistory.ErrorText}"); @@ -105,8 +109,12 @@ namespace FruitBankHybrid.Shared.Tests var shippingDocumentId = shippingDocuments[0].Id; - var first = await _signalRClient.GenerateEkaerXmlDocument(shippingDocumentId, false); - var second = await _signalRClient.GenerateEkaerXmlDocument(shippingDocumentId, false); + // Generálás EkaerHistory-Id alapján → előbb a rekord (mapping-gel), majd kétszeri generálás ugyanarra az Id-ra. + var created = await _signalRClient.CreateEkaerHistory(shippingDocumentId, false); + Assert.IsNotNull(created); + + var first = await _signalRClient.GenerateEkaerXmlDocument(created.Id); + var second = await _signalRClient.GenerateEkaerXmlDocument(created.Id); Assert.IsNotNull(first); Assert.IsNotNull(second); diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor index 70a2391b..a5d62d52 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor @@ -31,8 +31,25 @@ OnGridFocusedRowChanged="Grid_FocusedRowChanged"> - - + + @* A lefedett forrás-rekordok (ShippingDocument/Order Id-k) a betöltött Mappings-ből összefűzve — + egyből látszik, ha több dokumentum van egy EKÁER-deklaráció alá vonva. *@ + + @{ + var row = (EkaerHistory)context.DataItem; + } + @if (row.Mappings is { Count: > 0 } maps) + { + var noun = row.IsOutgoing ? "rendelés" : "szállítólevél"; + @string.Join(", ", maps.Select(m => $"#{m.ForeignKey}")) + } + else + { + + } + + + @* A kézi NAV-beadás fázisában a Status / EKÁER szám / SentDate kézzel szerkeszthető. *@ @@ -209,11 +226,11 @@ try { - var updated = await FruitBankSignalRClient.GenerateEkaerXmlDocument(ekaerHistory.ForeignKey, ekaerHistory.IsOutgoing); + var updated = await FruitBankSignalRClient.GenerateEkaerXmlDocument(ekaerHistory.Id); if (updated == null) { - _logger.Error($"GenerateEkaerXmlDocument null választ adott; ForeignKey: {ekaerHistory.ForeignKey}"); + _logger.Error($"GenerateEkaerXmlDocument null választ adott; EkaerHistory #{ekaerHistory.Id}"); return; } @@ -228,7 +245,7 @@ } catch (Exception ex) { - _logger.Error($"GenerateEkaerXmlDocument hiba; ForeignKey: {ekaerHistory.ForeignKey}", ex); + _logger.Error($"GenerateEkaerXmlDocument hiba; EkaerHistory #{ekaerHistory.Id}", ex); } finally { diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistoryBase.cs b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistoryBase.cs index c3b27be1..c4cbe0f0 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistoryBase.cs +++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistoryBase.cs @@ -37,9 +37,11 @@ public class GridEkaerHistoryBase : FruitBankGridBase, IGrid { if (ContextIds == null || ContextIds.Length == 0) ContextIds = [ParentDataItem!.Id]; - // A ForeignKey általános (Shipping/Order stb.) — bármely szülőnél a ForeignKey-re szűrünk. + // Detail-mód: a forrás-rekord (Shipping/Order/ShippingDocument) EKÁER-deklarációi a mappingen át. + // Megj.: az EkaerHistory már NEM hordoz per-szülő mezőt (a kapcsolat az EkaerHistoryMapping junctionben él), + // ezért a kliens-oldali KeyFieldNameToParentId itt nem alkalmazható — a szerver-oldali szűrés + // (GetEkaerHistoriesByForeignKey → mapping) adja a szülőhöz tartozó sorokat. (Jelenleg nincs detail-használat.) GetAllMessageTag = SignalRTags.GetEkaerHistoriesByForeignKey; - if (KeyFieldNameToParentId.IsNullOrWhiteSpace()) KeyFieldNameToParentId = nameof(EkaerHistory.ForeignKey); } await base.OnInitializedAsync(); diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs index f4a15b07..c18aafc8 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs @@ -79,7 +79,7 @@ namespace FruitBankHybrid.Shared.Services.SignalRs public Task?> GetEkaerHistoriesByForeignKey(int foreignKey) => GetAllAsync>(SignalRTags.GetEkaerHistoriesByForeignKey, [foreignKey]); public Task AddEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.AddEkaerHistory, ekaerHistory); public Task UpdateEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.UpdateEkaerHistory, ekaerHistory); - public Task GenerateEkaerXmlDocument(int foreignKey, bool isOutgoing) => GetByIdAsync(SignalRTags.GenerateEkaerXmlDocument, [foreignKey, isOutgoing]); + public Task GenerateEkaerXmlDocument(int ekaerHistoryId) => GetByIdAsync(SignalRTags.GenerateEkaerXmlDocument, ekaerHistoryId); public Task CreateEkaerHistory(int foreignKey, bool isOutgoing) => GetByIdAsync(SignalRTags.CreateEkaerHistory, [foreignKey, isOutgoing]); public Task CreateMissingEkaerHistories(DateTime fromDate) => GetByIdAsync(SignalRTags.CreateMissingEkaerHistories, fromDate); public Task GetEkaerHistoryCount(EkaerHistoryFilter filter) => GetByIdAsync(SignalRTags.GetEkaerHistoryCount, filter);