diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 696d3504..a019b102 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -75,7 +75,13 @@
"WebFetch(domain:www.itrack.hu)",
"WebFetch(domain:docplayer.hu)",
"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\" })"
+ "PowerShell($c = Get-Command claude -ErrorAction SilentlyContinue; if \\($c\\) { $c.Source; & claude --help 2>&1 | Select-Object -First 90 } else { \"claude not on PATH\" })",
+ "PowerShell(dotnet test \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Services.Tests\\\\AyCode.Services.Tests.csproj\" --filter \"FullyQualifiedName~EkaerTradeCardValidatorTests\" --nologo -v minimal)",
+ "PowerShell(dotnet build \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBankHybridApp\\\\FruitBank.Common\\\\FruitBank.Common.csproj\" -v q --nologo 2>&1)",
+ "WebFetch(domain:fruitveb.hu)",
+ "PowerShell(Get-Item \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Services\\\\Nav\\\\docs\\\\eKAERManagementService_2.2.pdf\" | Select-Object Length, LastWriteTime, CreationTime; try { $r = Invoke-WebRequest -Uri \"https://ekaer.nav.gov.hu/faq/content/uploads/2025/05/eKAERManagementService_2.2.pdf\" -Method Head -UseBasicParsing; \"Online Content-Length: $\\($r.Headers['Content-Length']\\); Last-Modified: $\\($r.Headers['Last-Modified']\\)\" } catch { \"HEAD request failed: $\\($_.Exception.Message\\)\" })",
+ "PowerShell($out = dotnet build \"H:\\\\Applications\\\\Mango\\\\Source\\\\NopCommerce.Common\\\\4.70\\\\Plugins\\\\Nop.Plugin.Misc.AIPlugin\\\\Nop.Plugin.Misc.FruitBankPlugin.csproj\" -v q --nologo 2>&1 | Out-String; $errLines = \\($out -split \"`n\"\\) | Where-Object { $_ -match \": error \" }; \"Osszes hiba-sor: $\\($errLines.Count\\)\"; $errLines | ForEach-Object { if \\($_ -match \"\\([A-Za-z]+\\\\.cs\\)\"\\) { $matches[1] } } | Group-Object | Sort-Object Count -Descending | Format-Table Name, Count -AutoSize)",
+ "PowerShell($out = dotnet build \"H:\\\\Applications\\\\Mango\\\\Source\\\\NopCommerce.Common\\\\4.70\\\\Plugins\\\\Nop.Plugin.Misc.AIPlugin\\\\Nop.Plugin.Misc.FruitBankPlugin.csproj\" -v q --nologo 2>&1 | Out-String; $errLines = \\($out -split \"`n\"\\) | Where-Object { $_ -match \": error \" }; \"Osszes hiba-sor: $\\($errLines.Count\\)\"; $errLines | Where-Object { $_ -match \"FruitBankDataController\" } | Select-Object -First 5)"
]
}
}
diff --git a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs
index 23f266a0..65c470fe 100644
--- a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs
+++ b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs
@@ -45,9 +45,11 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService
// 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);
+ // Guardok a leképezés ELŐTT (nincs félrevezető XML), majd a csoport ÖSSZES dokumentuma EGY tradeCard-dá
+ // (összevont tömeg/érték): ToConsignment → BuildTradeCard.
+ return TryDepotError(ekaerHistory, documents)
+ ?? TryConfigError(ekaerHistory, currency)
+ ?? Finalize(ekaerHistory, _mapper.BuildTradeCard(_mapper.ToConsignment(documents, _settings.Company)), currency);
}
public EkaerHistory GenerateEkaerXmlDocument(OrderDto order, EkaerHistory? ekaerHistory = null)
@@ -72,6 +74,18 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService
history.Partner = partner;
}
+ /// Depot-kapu (T-D8R4): a PartnerDepot az EKÁER-hez kötelező — a felrakodási hely a depot címe. Hiányzó
+ /// vagy be nem töltött telephelynél a leképezés ELŐTT ValidationError (nincs félrevezető, partner-központi-címes XML).
+ /// null = rendben, mehet a generálás.
+ private static EkaerHistory? TryDepotError(EkaerHistory ekaerHistory, IReadOnlyCollection documents)
+ {
+ if (documents.All(d => d.PartnerDepot != null)) return null;
+
+ ekaerHistory.Status = EkaerStatus.ValidationError;
+ ekaerHistory.ErrorText = "A szállítólevélhez nincs telephely (PartnerDepot) rendelve — EKÁER-hez kötelező (a felrakodási hely a telephely címe); pótlás után generáld újra.";
+ return ekaerHistory;
+ }
+
/// Config-kapu: külföldi (nem HUF) feladónál az árfolyam kötelező — különben a leképezés ELŐTT
/// ValidationError (nincs félrevezető XML). null = rendben, mehet a generálás.
private EkaerHistory? TryConfigError(EkaerHistory ekaerHistory, string? currency)
diff --git a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs
index 23fe64e0..7e1277b4 100644
--- a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs
+++ b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs
@@ -117,4 +117,18 @@ public interface IFruitBankDataControllerCommon
public Task UpdateGenericAttributeDto(GenericAttributeDto genericAttributeDto);
+ ///
+ /// Returns the stored grid-layout JSON for the composite layout key, or null if none saved.
+ ///
+ public Task GetGridLayout(string key, int userId);
+
+ ///
+ /// Saves (upserts) the grid-layout JSON under the composite layout key for the given user.
+ ///
+ public Task SaveGridLayout(string key, string layoutJson, int userId);
+
+ ///
+ /// Deletes the stored grid layout for the composite layout key. Returns false if the user does not exist.
+ ///
+ public Task DeleteGridLayout(string key, int userId);
}
\ No newline at end of file
diff --git a/FruitBank.Common/Services/Ekaer/EkaerReportability.cs b/FruitBank.Common/Services/Ekaer/EkaerReportability.cs
index 16aa7751..437130fb 100644
--- a/FruitBank.Common/Services/Ekaer/EkaerReportability.cs
+++ b/FruitBank.Common/Services/Ekaer/EkaerReportability.cs
@@ -31,10 +31,11 @@ public sealed class EkaerObligationResult
/// is bírság), ezért: a bizonytalan adatot (érvénytelen országkód) NEM döntjük el magától → .
///
///
-/// Sorrend: (1) az AGGREGÁLT tömeg/érték küszöb FELETT → Required, az országkódtól FÜGGETLENÜL — belföld+küszöb
-/// felett és külföld egyaránt kötelező; az országkód-hibát ilyenkor a generate-validálás jelzi, a sor létrejön;
-/// (2) küszöb ALATT → itt számít az ország: hiányzó/érvénytelen országkód → DataError (a foreign-vs-belföld nem
-/// dönthető el); (3) küszöb alatt, érvényes országkódok: külföld (eltér) → Required, belföld (egyezik) → NotRequired.
+/// Sorrend: (0) TÉTELENKÉNTI előfeltétel — bruttó súly > 0, különben DataError (a 0 súly csendben alul-számolná a
+/// küszöb-summát → alul-jelentés); (1) az AGGREGÁLT tömeg/érték küszöb FELETT → Required, az országkódtól FÜGGETLENÜL
+/// — belföld+küszöb felett és külföld egyaránt kötelező; az országkód-hibát ilyenkor a generate-validálás jelzi, a sor
+/// létrejön; (2) küszöb ALATT → itt számít az ország: hiányzó/érvénytelen országkód → DataError (a foreign-vs-belföld
+/// nem dönthető el); (3) küszöb alatt, érvényes országkódok: külföld (eltér) → Required, belföld (egyezik) → NotRequired.
/// A 13/2020. (XII. 23.) PM rendelet alapján a küszöb feladó→címzett→jármű relációra aggregált — a hívó már
/// így állítja össze a -t (bejövőnél a (Shipping, Partner) csoport tételei).
///
@@ -45,6 +46,18 @@ public static class EkaerReportability
ArgumentNullException.ThrowIfNull(consignment);
ArgumentNullException.ThrowIfNull(settings);
+ var subject = Subject(consignment);
+
+ // (0) Tételenkénti előfeltétel: bruttó súly > 0. A 0 súlyú tétel (kitöltetlen dokumentum-súly / méretlen
+ // rendelés) csendben alul-számolná a küszöb-aggregátumot → alul-jelentési kockázat; a NAV amúgy is tételenként
+ // pozitív bruttó súlyt vár. Hiányzó súly → DataError (a sor nem jön létre; pótlás után a következő futás felveszi).
+ var weightErrors = consignment.Lines
+ .Where(l => l.WeightKg <= 0)
+ .Select(l => $"{subject} / {(string.IsNullOrWhiteSpace(l.Name) ? $"tétel #{l.ExternalId}" : l.Name)}: a tétel bruttó súlya hiányzik (0 kg) — a kötelezettség nem dönthető el.")
+ .ToList();
+ if (weightErrors.Count > 0)
+ return EkaerObligationResult.DataError(weightErrors);
+
// (1) Küszöb FELETT → kötelező, FÜGGETLENÜL az országkódtól: belföld+küszöb felett ÉS külföld egyaránt kötelező.
// Az országkód-hibát ilyenkor NEM itt blokkoljuk — a sor létrejön, a hibát a generate-validálás jelzi.
var totalWeight = consignment.Lines.Sum(l => l.WeightKg);
@@ -54,7 +67,6 @@ public static class EkaerReportability
// (2) Küszöb ALATT: itt MÁR az ország dönt — csak a külföldi (eltérő országkód) reláció kötelező. Ehhez érvényes
// ISO-2 országkódok kellenek; hiányzó/érvénytelen → a foreign-vs-belföld nem dönthető el → DataError.
- var subject = Subject(consignment);
var errors = new List();
if (!IsValidCountry(consignment.Seller.CountryCode))
errors.Add($"{subject}: a feladó országkódja hiányzik vagy érvénytelen ('{consignment.Seller.CountryCode}').");
diff --git a/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs
index 2dee6d65..828cc30f 100644
--- a/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs
+++ b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs
@@ -74,7 +74,7 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
// ── Adapterek: forrás → normalizált EkaerConsignment (az EGYETLEN irányfüggő rész) ────────────
- /// Bejövő: egy vagy több ShippingDocument (a hívó (Shipping, Partner)-re csoportosít) → egy
+ /// Bejövő: egy vagy több ShippingDocument (a hívó (Shipping, Partner, PartnerDepot)-ra csoportosít) → egy
/// szállítmány a csoport ÖSSZES tételével. Egy dokumentummal a generáláshoz, a csoporttal a kötelezettség-summához.
public EkaerConsignment ToConsignment(IReadOnlyCollection documents, EkaerCompanyInfo company)
{
@@ -83,19 +83,24 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
if (documents.Count == 0) throw new ArgumentException("Legalább egy ShippingDocument szükséges.", nameof(documents));
var first = documents.First();
- var partner = first.Partner; // a csoport azonos partneré (a hívó (Shipping, Partner)-re csoportosít)
+ var partner = first.Partner; // a csoport azonos partneré (a hívó (Shipping, Partner, PartnerDepot)-ra csoportosít)
var shipping = first.Shipping; // azonos Shipping; a kapunál lehet null (a jármű ott nem kell)
var rateToHuf = SafeRateToHuf(partner?.Currency);
+ // A depot TaxId-ja Partner-passthrough (a nested Partner-t nem töltjük külön query-vel) — ugyanaz a minta,
+ // mint a MapShipping `document.Shipping ??=` visszakötése.
+ if (first.PartnerDepot is { } depot) depot.Partner ??= partner;
var lines = documents
.SelectMany(d => d.ShippingItems ?? [])
.Select(item => new EkaerLine
{
ExternalId = item.Id.ToString(),
- TradeReason = TradeReasonType.A, // bejövő áru = beszerzés
+ TradeReason = TradeReasonType.S, // S = értékesítés ÉS beszerzés (egy kód); az A kivezetett, D-nél csak S adható
Vtsz = NormalizeVtsz(item.ProductDto?.Gtin),
Name = item.ProductName,
- WeightKg = item.MeasuredGrossWeight,
+ // A szállítólevélen szereplő bruttó — a bejelentés a kísérő dokumentum adatairól megy (mint a value),
+ // nem a (későbbi) mérlegelésről; méretlen tétel így nem 0 kg-mal jelenik meg.
+ WeightKg = item.GrossWeightOnDocument,
ValueHuf = EkaerValueCalculator.ItemValueHuf(item, rateToHuf),
})
.ToList();
@@ -107,7 +112,9 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
Currency = partner?.Currency,
Seller = PartnerEndpoint(partner),
Buyer = CompanyEndpoint(company),
- LoadLocation = BuildLocation(partner), // a beszállító telephelye (a PartnerDepot-bekötés külön feladat)
+ // A felrakodási hely a DEPOT címe (T-D8R4) — a generate-út betölti (LoadWith) és guardolja (TryDepotError).
+ // A kapu-útvonalon a depot nincs betöltve → partner-központi cím, ami ott rendben van: a kötelezettség-döntés címet nem használ.
+ LoadLocation = BuildLocation((ICompanyInfoBase?)first.PartnerDepot ?? partner),
UnloadLocation = company.Site, // a saját telephely (kész LocationType — mezővesztés nélkül átengedve)
Lines = lines,
Vehicle = BuildVehicle(shipping?.CargoTruck),
diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs
index c18aafc8..a441c500 100644
--- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs
+++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs
@@ -238,6 +238,17 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
=> PostDataAsync(SignalRTags.AuthenticateUser, loginModelRequest);
#endregion Authenticate
+ #region GridLayout
+ public Task GetGridLayout(string key, int userId)
+ => GetByIdAsync(SignalRTags.GetGridLayoutTag, [key, userId]);
+
+ public Task SaveGridLayout(string key, string layoutJson, int userId)
+ => PostAsync(SignalRTags.SaveGridLayoutTag, [key, layoutJson, userId]);
+
+ public Task DeleteGridLayout(string key, int userId)
+ => PostAsync(SignalRTags.DeleteGridLayoutTag, [key, userId]);
+ #endregion GridLayout
+
#region Orders
public Task?> GetAllOrderDtoTableItems()
=> GetAllAsync>(SignalRTags.GetAllOrderDtos);