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);