Compare commits
No commits in common. "1d56eba8ecb868125dcd7386121215c8ec39c506" and "6b105bb5ed13f8a874722171ad248d5ce9277fdd" have entirely different histories.
1d56eba8ec
...
6b105bb5ed
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,6 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Loggers;
|
||||
using AyCode.Services.Nav.Ekaer;
|
||||
using AyCode.Services.SignalRs;
|
||||
using DocumentFormat.OpenXml.Office2010.Excel;
|
||||
using FruitBank.Common.Dtos;
|
||||
|
|
@ -14,8 +13,6 @@ using FruitBank.Common.Loggers;
|
|||
using FruitBank.Common.Models;
|
||||
using FruitBank.Common.Server;
|
||||
using FruitBank.Common.Server.Interfaces;
|
||||
using FruitBank.Common.Server.Services.Ekaer;
|
||||
using FruitBank.Common.Services.Ekaer;
|
||||
using FruitBank.Common.SignalRs;
|
||||
using LinqToDB;
|
||||
using Mango.Nop.Core.Dtos;
|
||||
|
|
@ -23,7 +20,6 @@ using Mango.Nop.Core.Loggers;
|
|||
using Mango.Nop.Core.Models;
|
||||
using Nop.Core;
|
||||
using Nop.Core.Domain.Customers;
|
||||
using Nop.Core.Domain.Orders;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Factories;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||
|
|
@ -44,8 +40,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
|
|||
ICustomerRegistrationService customerRegistrationService,
|
||||
ILocalizationService localizationService,
|
||||
PreOrderConversionService preorderConversionService,
|
||||
IFruitBankEkaerService fruitBankEkaerService,
|
||||
IEkaerSettings ekaerSettings,
|
||||
IEnumerable<IAcLogWriterBase> logWriters)
|
||||
: BasePluginController, IFruitBankDataControllerServer
|
||||
{
|
||||
|
|
@ -107,9 +101,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
|
|||
{
|
||||
_logger.Detail($"GetPartners invoked");
|
||||
|
||||
// A telephelyeket is behúzzuk (CSAK a depókat, NEM a teljes GetAll(true) láncot) — a partnerek
|
||||
// úgyis cache-eltek, így a ShippingDocument-grid telephely-legördülője kliens-oldalon kaszkádolhat.
|
||||
return await ctx.Partners.GetAll().LoadWith(p => p.PartnerDepots).ToListAsync();
|
||||
return await ctx.Partners.GetAll().ToListAsync();
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.GetPartnerById)]
|
||||
|
|
@ -120,7 +112,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
|
|||
//var customers = await ctx.GetCustormersBySystemRoleName("Measuring").ToListAsync();
|
||||
//_logger.Error($"COUNT: {customers.Count}");
|
||||
|
||||
return await ctx.Partners.GetByIdAsync(id, false);
|
||||
return await ctx.Partners.GetByIdAsync(id);
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.AddPartner)]
|
||||
|
|
@ -227,247 +219,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
|
|||
return await ctx.CargoTrucks.GetByIdAsync(cargoTruck.Id, true);
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.GetPartnerDepots)]
|
||||
public async Task<List<PartnerDepot>> GetPartnerDepots()
|
||||
{
|
||||
_logger.Detail($"GetPartnerDepots invoked");
|
||||
|
||||
return await ctx.PartnerDepots.GetAll(true).ToListAsync();
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.GetPartnerDepotById)]
|
||||
public async Task<PartnerDepot> GetPartnerDepotById(int id)
|
||||
{
|
||||
_logger.Detail($"GetPartnerDepotById invoked; id: {id}");
|
||||
return await ctx.PartnerDepots.GetByIdAsync(id, true);
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.GetPartnerDepotsByPartnerId)]
|
||||
public async Task<List<PartnerDepot>> GetPartnerDepotsByPartnerId(int partnerId)
|
||||
{
|
||||
_logger.Detail($"GetPartnerDepotsByPartnerId invoked; partnerId: {partnerId}");
|
||||
return await ctx.PartnerDepots.GetByPartnerId(partnerId, true).ToListAsync();
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.AddPartnerDepot)]
|
||||
public async Task<PartnerDepot> AddPartnerDepot(PartnerDepot partnerDepot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(partnerDepot);
|
||||
|
||||
_logger.Detail($"AddPartnerDepot invoked; id: {partnerDepot.Id}");
|
||||
|
||||
await ctx.PartnerDepots.InsertAsync(partnerDepot);
|
||||
return await ctx.PartnerDepots.GetByIdAsync(partnerDepot.Id, true);
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.UpdatePartnerDepot)]
|
||||
public async Task<PartnerDepot> UpdatePartnerDepot(PartnerDepot partnerDepot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(partnerDepot);
|
||||
|
||||
_logger.Detail($"UpdatePartnerDepot invoked; id: {partnerDepot.Id}");
|
||||
|
||||
await ctx.PartnerDepots.UpdateAsync(partnerDepot);
|
||||
return await ctx.PartnerDepots.GetByIdAsync(partnerDepot.Id, true);
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.GetEkaerHistories)]
|
||||
public async Task<List<EkaerHistory>> GetEkaerHistories(EkaerHistoryFilter ekaerHistoryFilter)
|
||||
{
|
||||
_logger.Detail($"GetEkaerHistories invoked; ekaerHistoryFilter: {ekaerHistoryFilter}");
|
||||
return await ctx.EkaerHistories.GetByFilter(ekaerHistoryFilter).ToListAsync();
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.GetEkaerHistoryCount)]
|
||||
public async Task<int> GetEkaerHistoryCount(EkaerHistoryFilter ekaerHistoryFilter)
|
||||
{
|
||||
_logger.Detail($"GetEkaerHistoryCount invoked; ekaerHistoryFilter: {ekaerHistoryFilter}");
|
||||
return await ctx.EkaerHistories.CountByFilterAsync(ekaerHistoryFilter);
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.GetEkaerHistoryById)]
|
||||
public async Task<EkaerHistory> GetEkaerHistoryById(int id)
|
||||
{
|
||||
_logger.Detail($"GetEkaerHistoryById invoked; id: {id}");
|
||||
return await ctx.EkaerHistories.GetByIdAsync(id);
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.GetEkaerHistoriesByForeignKey)]
|
||||
public async Task<List<EkaerHistory>> GetEkaerHistoriesByForeignKey(int foreignKey)
|
||||
{
|
||||
_logger.Detail($"GetEkaerHistoriesByForeignKey invoked; foreignKey: {foreignKey}");
|
||||
return await ctx.EkaerHistories.GetByForeignKey(foreignKey).ToListAsync();
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.AddEkaerHistory)]
|
||||
public async Task<EkaerHistory> AddEkaerHistory(EkaerHistory ekaerHistory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ekaerHistory);
|
||||
|
||||
_logger.Detail($"AddEkaerHistory invoked; id: {ekaerHistory.Id}");
|
||||
|
||||
await ctx.EkaerHistories.InsertAsync(ekaerHistory);
|
||||
return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id);
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.UpdateEkaerHistory)]
|
||||
public async Task<EkaerHistory> UpdateEkaerHistory(EkaerHistory ekaerHistory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ekaerHistory);
|
||||
|
||||
_logger.Detail($"UpdateEkaerHistory invoked; id: {ekaerHistory.Id}");
|
||||
|
||||
await ctx.EkaerHistories.UpdateAsync(ekaerHistory);
|
||||
return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id);
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.GenerateEkaerXmlDocument)]
|
||||
public async Task<EkaerHistory> GenerateEkaerXmlDocument(int foreignKey, bool isOutgoing)
|
||||
{
|
||||
_logger.Detail($"GenerateEkaerXmlDocument invoked; foreignKey: {foreignKey}; isOutgoing: {isOutgoing}");
|
||||
|
||||
// Upsert: (ForeignKey + IsOutgoing) párra EGY rekord — az újragenerálás nem duplikál.
|
||||
var ekaerHistory = await ctx.EkaerHistories.GetByForeignKey(foreignKey).FirstOrDefaultAsync(eh => eh.IsOutgoing == isOutgoing);
|
||||
|
||||
if (!isOutgoing)
|
||||
{
|
||||
// Bejövő: a GetAll(true) a mapperhez kellő teljes gráfot tölti (Partner, Items+ProductDto, Shipping→járművek).
|
||||
var shippingDocument = await ctx.ShippingDocuments.GetByIdAsync(foreignKey, true)
|
||||
?? throw new ArgumentException($"ShippingDocument not found; id: {foreignKey}", nameof(foreignKey));
|
||||
ekaerHistory = fruitBankEkaerService.GenerateEkaerXmlDocument(shippingDocument, ekaerHistory);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Kimenő: a GetByIdAsync(true) betölti a Customer-t, tételeket, palettákat (GrossWeight) és a ProductDto-t.
|
||||
var order = await ctx.OrderDtos.GetByIdAsync(foreignKey, true)
|
||||
?? throw new ArgumentException($"Order not found; id: {foreignKey}", nameof(foreignKey));
|
||||
ekaerHistory = fruitBankEkaerService.GenerateEkaerXmlDocument(order, ekaerHistory);
|
||||
}
|
||||
|
||||
if (ekaerHistory.Id > 0) await ctx.EkaerHistories.UpdateAsync(ekaerHistory);
|
||||
else await ctx.EkaerHistories.InsertAsync(ekaerHistory);
|
||||
|
||||
return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id) ?? ekaerHistory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Idempotens rekord-létrehozás: ha a (foreignKey, isOutgoing) párra már van EkaerHistory, azt adja vissza
|
||||
/// érintetlenül; különben új Pending rekordot hoz létre. A generálás (XmlDoc) külön lépés: GenerateEkaerXmlDocument.
|
||||
/// </summary>
|
||||
[SignalR(SignalRTags.CreateEkaerHistory)]
|
||||
public async Task<EkaerHistory> CreateEkaerHistory(int foreignKey, bool isOutgoing)
|
||||
{
|
||||
_logger.Detail($"CreateEkaerHistory invoked; foreignKey: {foreignKey}; isOutgoing: {isOutgoing}");
|
||||
|
||||
var existing = await ctx.EkaerHistories.GetByForeignKey(foreignKey).FirstOrDefaultAsync(eh => eh.IsOutgoing == isOutgoing);
|
||||
if (existing != null) return existing;
|
||||
|
||||
// A forrás-entitás léte irányfüggő: bejövő → ShippingDocument, kimenő → Order.
|
||||
if (!isOutgoing)
|
||||
{
|
||||
_ = await ctx.ShippingDocuments.GetByIdAsync(foreignKey, false) ?? throw new ArgumentException($"ShippingDocument not found; id: {foreignKey}", nameof(foreignKey));
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = await ctx.OrderDtos.GetByIdAsync(foreignKey, false) ?? throw new ArgumentException($"Order not found; id: {foreignKey}", nameof(foreignKey));
|
||||
}
|
||||
|
||||
var ekaerHistory = new EkaerHistory { ForeignKey = foreignKey, IsOutgoing = isOutgoing, Status = EkaerStatus.Pending };
|
||||
await ctx.EkaerHistories.InsertAsync(ekaerHistory);
|
||||
|
||||
return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id) ?? ekaerHistory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A megadott dátumtól kezdődő, EKÁER-rekord nélküli szállítólevelekre Pending rekordot hoz létre.
|
||||
/// User-vezérelt (toolbar gomb) — szándékosan NINCS automata rekord-érzékelés: a létrehozás explicit,
|
||||
/// idempotens, és bármikor újrafuttatható (maga a gomb a "reconciliation").
|
||||
/// </summary>
|
||||
[SignalR(SignalRTags.CreateMissingEkaerHistories)]
|
||||
public async Task<EkaerCreateResult> CreateMissingEkaerHistories(DateTime fromDate)
|
||||
{
|
||||
_logger.Detail($"CreateMissingEkaerHistories invoked; fromDate: {fromDate:yyyy-MM-dd}");
|
||||
|
||||
var toCreate = new List<EkaerHistory>();
|
||||
var messages = new HashSet<string>(); // dedup: ugyanaz a partner (pl. hiányzó országkód) több csoportban is előjöhet
|
||||
|
||||
// ── Bejövő ── Rekord nélküli szállítólevelek a dátumtól, Partnerrel + tételekkel betöltve a kötelezettség-kapuhoz.
|
||||
// A bejelentés-egység (Shipping, Partner) = feladó→címzett→jármű: a küszöb a csoport ÖSSZES dokumentumára aggregál.
|
||||
var candidates = await ctx.ShippingDocuments.GetAll()
|
||||
.Where(sd => sd.ShippingDate >= fromDate)
|
||||
.Where(sd => !ctx.EkaerHistories.Table.Any(eh => eh.ForeignKey == sd.Id && !eh.IsOutgoing))
|
||||
.LoadWith(sd => sd.Partner)
|
||||
.LoadWith(sd => sd.ShippingItems)
|
||||
.ToListAsync();
|
||||
|
||||
// Partner nélküli (még nincs rendesen felvett) szállítólevél CSENDBEN kihagyva — nincs feladó, nem kész;
|
||||
// a mentes partner (IsEkaer=false) is. A maradék (Shipping, Partner) csoportban (a dokumentum mindig kap Shippinget).
|
||||
foreach (var group in candidates.Where(sd => sd.Partner != null && sd.Partner.IsEkaer != false).GroupBy(sd => (sd.ShippingId, sd.PartnerId)))
|
||||
{
|
||||
var docs = group.ToList();
|
||||
try
|
||||
{
|
||||
var result = fruitBankEkaerService.EvaluateObligation(docs);
|
||||
if (result.Obligation == EkaerObligation.DataError) { messages.UnionWith(result.Errors); continue; }
|
||||
if (result.Obligation == EkaerObligation.NotRequired) continue;
|
||||
|
||||
// Kötelező → a csoport minden dokumentumára egy-egy Pending sor (a rekord-szerkezet per-dokumentum marad).
|
||||
foreach (var doc in docs)
|
||||
toCreate.Add(new EkaerHistory { ForeignKey = doc.Id, IsOutgoing = false, StatusId = (int)EkaerStatus.Pending });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"CreateMissingEkaerHistories; inbound evaluate failed; ShippingId: {group.Key.ShippingId}; PartnerId: {group.Key.PartnerId}", ex);
|
||||
messages.Add($"Szállítólevél-csoport (Shipping #{group.Key.ShippingId}, Partner #{group.Key.PartnerId}): a kötelezettség nem dönthető el — {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Kimenő ── Rekord nélküli, lezárt (Complete) rendelések a dátumtól; rendelésenként vizsgáljuk (nincs Shipping).
|
||||
// Előbb csak az azonosítók (könnyű szűrés), majd EGY batch-betöltés a teljes gráffal (GetAllByIds) — nincs N+1.
|
||||
var missingOrderIds = await ctx.OrderDtos.GetAllByOrderStatus(OrderStatus.Complete, false)
|
||||
.Where(o => o.GenericAttributes.Any(ga => ga.Key == nameof(OrderDto.DateOfReceipt) && DateTime.Parse(ga.Value) >= fromDate.Date))
|
||||
.Where(o => !ctx.EkaerHistories.Table.Any(eh => eh.ForeignKey == o.Id && eh.IsOutgoing))
|
||||
.Select(o => o.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var missingOrders = missingOrderIds.Count == 0 ? [] : await ctx.OrderDtos.GetAllByIds(missingOrderIds, true).ToListAsync();
|
||||
|
||||
foreach (var order in missingOrders)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = fruitBankEkaerService.EvaluateObligation(order);
|
||||
if (result.Obligation == EkaerObligation.DataError) { messages.UnionWith(result.Errors); continue; }
|
||||
if (result.Obligation == EkaerObligation.NotRequired) continue;
|
||||
|
||||
toCreate.Add(new EkaerHistory { ForeignKey = order.Id, IsOutgoing = true, StatusId = (int)EkaerStatus.Pending });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"CreateMissingEkaerHistories; outbound evaluate failed; OrderId: {order.Id}", ex);
|
||||
messages.Add($"Rendelés #{order.Id}: a kötelezettség nem dönthető el — {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Beszúrás ──
|
||||
var createdCount = 0;
|
||||
foreach (var ekaerHistory in toCreate)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ctx.EkaerHistories.InsertAsync(ekaerHistory);
|
||||
createdCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"CreateMissingEkaerHistories; insert failed; ForeignKey: {ekaerHistory.ForeignKey}; IsOutgoing: {ekaerHistory.IsOutgoing}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Info($"CreateMissingEkaerHistories; created: {createdCount}; messages: {messages.Count}; fromDate: {fromDate:yyyy-MM-dd}");
|
||||
return new EkaerCreateResult { CreatedCount = createdCount, Messages = [.. messages.OrderBy(m => m)] };
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.GetShippings)]
|
||||
public async Task<List<Shipping>> GetShippings()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
using FruitBank.Common.Entities;
|
||||
using LinqToDB;
|
||||
using Mango.Nop.Data.Repositories;
|
||||
using Nop.Core.Caching;
|
||||
using Nop.Core.Configuration;
|
||||
using Nop.Core.Events;
|
||||
using Nop.Data;
|
||||
|
||||
namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||
|
||||
public class EkaerHistoryDbTable : MgDbTableBase<EkaerHistory>
|
||||
{
|
||||
public EkaerHistoryDbTable(IEventPublisher eventPublisher, INopDataProvider dataProvider, IShortTermCacheManager shortTermCacheManager, IStaticCacheManager staticCacheManager, AppSettings appSettings)
|
||||
: base(eventPublisher, dataProvider, shortTermCacheManager, staticCacheManager, appSettings)
|
||||
{
|
||||
}
|
||||
|
||||
// History tábla: legújabb elöl. Nincs asszociáció → nincs LoadWith / loadRelations.
|
||||
public override IOrderedQueryable<EkaerHistory> GetAll() => base.GetAll().OrderByDescending(p => p.Id);
|
||||
|
||||
public Task<EkaerHistory> GetByIdAsync(int id) => GetAll().FirstOrDefaultAsync(p => p.Id == id);
|
||||
public IQueryable<EkaerHistory> GetByForeignKey(int foreignKey) => GetAll().Where(p => p.ForeignKey == foreignKey);
|
||||
|
||||
// A NAV-nál lévő partíciók fix StatusId-k; minden más (a jövőbeni új státuszok is) „beküldésre váró".
|
||||
private static readonly int[] ToSubmitStatusIds = [.. Enum.GetValues<EkaerStatus>().Select(s => (int)s).Except([(int)EkaerStatus.Sent, (int)EkaerStatus.SentWithMissingData])];
|
||||
|
||||
/// <summary>Flag-alapú szűrés a tabokhoz/count-hoz. <see cref="EkaerHistoryFilter.All"/> (= 0) → minden;
|
||||
/// egyébként a beállított flag-ek diszjunkt StatusId-partícióinak UNIÓJA (IN). A státusz-helperek (IsSent stb.)
|
||||
/// nem fordulnak SQL-re, ezért közvetlen StatusId-halmazzal szűrünk.</summary>
|
||||
public IQueryable<EkaerHistory> GetByFilter(EkaerHistoryFilter filter)
|
||||
{
|
||||
// HasFlag(All) mindig true (0 bit), ezért előbb az All-ágat zárjuk rövidre.
|
||||
if (filter == EkaerHistoryFilter.All) return GetAll();
|
||||
|
||||
var statusIds = new List<int>();
|
||||
if (filter.HasFlag(EkaerHistoryFilter.ToSubmit)) statusIds.AddRange(ToSubmitStatusIds);
|
||||
if (filter.HasFlag(EkaerHistoryFilter.Sent)) statusIds.Add((int)EkaerStatus.Sent);
|
||||
if (filter.HasFlag(EkaerHistoryFilter.NeedsCompletion)) statusIds.Add((int)EkaerStatus.SentWithMissingData);
|
||||
|
||||
return GetAll().Where(eh => statusIds.Contains(eh.StatusId));
|
||||
}
|
||||
|
||||
// A CountAsync kétértelmű (a System.Linq.Async ÉS a LinqToDB is hozza ugyanazt a nevet, no-arg hívásnál mindkettő illik)
|
||||
// → explicit a LinqToDB-é. (A ToListAsync nem ütközik, azt a hívó közvetlenül használhatja.)
|
||||
public Task<int> CountByFilterAsync(EkaerHistoryFilter filter) => LinqToDB.AsyncExtensions.CountAsync(GetByFilter(filter));
|
||||
}
|
||||
|
|
@ -55,8 +55,6 @@ public class FruitBankDbContext : MgDbContextBase,
|
|||
public OrderItemDtoDbTable OrderItemDtos { get; set; }
|
||||
|
||||
public PartnerDbTable Partners { get; set; }
|
||||
public PartnerDepotDbTable PartnerDepots { get; set; }
|
||||
public EkaerHistoryDbTable EkaerHistories { get; set; }
|
||||
public CargoPartnerDbTable CargoPartners { get; set; }
|
||||
public CargoTruckDbTable CargoTrucks{ get; set; }
|
||||
|
||||
|
|
@ -83,7 +81,7 @@ public class FruitBankDbContext : MgDbContextBase,
|
|||
public IRepository<StockQuantityHistoryExt> StockQuantityHistoriesExt { get; set; }
|
||||
|
||||
public FruitBankDbContext(INopDataProvider dataProvider, ILockService lockService, FruitBankAttributeService fruitBankAttributeService, IStoreContext storeContext,
|
||||
CargoPartnerDbTable cargoPartnerDbTable, CargoTruckDbTable cargoTruckDbTable, PartnerDbTable partnerDbTable, PartnerDepotDbTable partnerDepotDbTable, EkaerHistoryDbTable ekaerHistoryDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, ShippingItemDbTable shippingItemDbTable,
|
||||
CargoPartnerDbTable cargoPartnerDbTable, CargoTruckDbTable cargoTruckDbTable, PartnerDbTable partnerDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, ShippingItemDbTable shippingItemDbTable,
|
||||
ShippingItemPalletDbTable shippingItemPalletDbTable, FilesDbTable filesDbTable, ShippingDocumentToFilesDbTable shippingDocumentToFilesDbTable,
|
||||
ProductDtoDbTable productDtoDbTable, OrderDtoDbTable orderDtoDbTable, OrderItemDtoDbTable orderItemDtoDbTable, OrderItemPalletDbTable orderItemPalletDbTable,
|
||||
StockQuantityHistoryDtoDbTable stockQuantityHistoryDtos, CustomerCreditDbTable customerCreditDbTable,
|
||||
|
|
@ -110,8 +108,6 @@ public class FruitBankDbContext : MgDbContextBase,
|
|||
|
||||
Files = filesDbTable;
|
||||
Partners = partnerDbTable;
|
||||
PartnerDepots = partnerDepotDbTable;
|
||||
EkaerHistories = ekaerHistoryDbTable;
|
||||
CargoPartners = cargoPartnerDbTable;
|
||||
CargoTrucks = cargoTruckDbTable;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,10 +22,9 @@ public class PartnerDbTable : MgDbTableBase<Partner>
|
|||
{
|
||||
return loadRelations
|
||||
? GetAll()
|
||||
.LoadWith(p => p.PartnerDepots)
|
||||
.LoadWith(p => p.ShippingDocuments).ThenLoad(sd => sd.Shipping)
|
||||
.LoadWith(p => p.ShippingDocuments).ThenLoad(sd => sd.ShippingItems).ThenLoad(si => si.ShippingItemPallets)
|
||||
.LoadWith(p => p.ShippingDocuments).ThenLoad(sd => sd.ShippingDocumentToFiles).ThenLoad(sdtof => sdtof.ShippingDocumentFile)
|
||||
.LoadWith(sd => sd.ShippingDocuments).ThenLoad(s => s.Shipping)
|
||||
.LoadWith(sd => sd.ShippingDocuments).ThenLoad(si => si.ShippingItems).ThenLoad(sip => sip.ShippingItemPallets)
|
||||
.LoadWith(sd => sd.ShippingDocuments).ThenLoad(sdtof => sdtof.ShippingDocumentToFiles).ThenLoad(f => f.ShippingDocumentFile)
|
||||
: GetAll();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
using FruitBank.Common.Entities;
|
||||
using LinqToDB;
|
||||
using Mango.Nop.Data.Repositories;
|
||||
using Nop.Core.Caching;
|
||||
using Nop.Core.Configuration;
|
||||
using Nop.Core.Events;
|
||||
using Nop.Data;
|
||||
|
||||
namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||
|
||||
public class PartnerDepotDbTable : MgDbTableBase<PartnerDepot>
|
||||
{
|
||||
public PartnerDepotDbTable(IEventPublisher eventPublisher, INopDataProvider dataProvider, IShortTermCacheManager shortTermCacheManager, IStaticCacheManager staticCacheManager, AppSettings appSettings)
|
||||
: base(eventPublisher, dataProvider, shortTermCacheManager, staticCacheManager, appSettings)
|
||||
{
|
||||
}
|
||||
|
||||
public override IOrderedQueryable<PartnerDepot> GetAll() => base.GetAll().OrderBy(p => p.PartnerId);
|
||||
|
||||
public IQueryable<PartnerDepot> GetAll(bool loadRelations)
|
||||
{
|
||||
return loadRelations ? GetAll().LoadWith(sd => sd.Partner) : GetAll();
|
||||
}
|
||||
|
||||
public Task<PartnerDepot> GetByIdAsync(int id, bool loadRelations) => GetAll(loadRelations).FirstOrDefaultAsync(p => p.Id == id);
|
||||
public IQueryable<PartnerDepot> GetByPartnerId(int partnerId, bool loadRelations) => GetAll(loadRelations).Where(p => p.PartnerId == partnerId);
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ using Microsoft.Extensions.Configuration;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Nop.Core;
|
||||
using Nop.Core.Domain.Orders;
|
||||
using Nop.Core.Infrastructure;
|
||||
using Nop.Data;
|
||||
|
|
@ -44,7 +43,6 @@ using System.Net.Http.Headers;
|
|||
using Mango.Nop.Core.Loggers;
|
||||
using AyCode.Services.Nav;
|
||||
using AyCode.Services.Nav.Ekaer;
|
||||
using AyCode.Services.Nav.Ekaer.Models;
|
||||
using FruitBank.Common.Services.Ekaer;
|
||||
using FruitBank.Common.Server.Services.Ekaer;
|
||||
using System.Security.Authentication;
|
||||
|
|
@ -85,8 +83,6 @@ public class PluginNopStartup : INopStartup
|
|||
services.AddScoped<OrderItemPalletDbTable>();
|
||||
|
||||
services.AddScoped<PartnerDbTable>();
|
||||
services.AddScoped<PartnerDepotDbTable>();
|
||||
services.AddScoped<EkaerHistoryDbTable>();
|
||||
services.AddScoped<CargoPartnerDbTable>();
|
||||
services.AddScoped<CargoTruckDbTable>();
|
||||
|
||||
|
|
@ -194,44 +190,18 @@ public class PluginNopStartup : INopStartup
|
|||
BaseUrl = c["BaseUrl"] ?? "https://import-test-b.ekaer.nav.gov.hu", // TEST; PROD: https://import.ekaer.nav.gov.hu
|
||||
};
|
||||
});
|
||||
|
||||
// EKÁER konfiguráció (cégadat + telephely + küszöb + árfolyam), configból (appsettings "Ekaer").
|
||||
// IEkaerSettings-ként kötjük be (AyCode-kontraktus) — a fogyasztók az interfészt kérik, mint az INavCredentials-t.
|
||||
services.AddSingleton<IEkaerSettings>(sp =>
|
||||
services.AddSingleton(sp =>
|
||||
{
|
||||
var c = sp.GetRequiredService<IConfiguration>().GetSection("Ekaer");
|
||||
var co = c.GetSection("Company");
|
||||
var site = co.GetSection("Site");
|
||||
return new EkaerSettings
|
||||
var c = sp.GetRequiredService<IConfiguration>().GetSection("Ekaer:Company");
|
||||
return new EkaerCompanyInfo
|
||||
{
|
||||
Company = new EkaerCompanyInfo
|
||||
{
|
||||
Name = co["Name"],
|
||||
TaxId = co["TaxId"],
|
||||
CountryCode = co["CountryCode"] ?? "HU", // a NAV EKÁER magyar
|
||||
PostalCode = co["PostalCode"],
|
||||
City = co["City"],
|
||||
Street = co["Street"],
|
||||
// Telephely = fizikai fel-/lerakodási hely (bejövőnél lerakodás, kimenőnél felrakodás).
|
||||
// Magyar címnél a NAV Name/VatNumber/Phone/Email-t is megköveteli. Ha = székhely, ugyanaz a cím.
|
||||
Site = new LocationType
|
||||
{
|
||||
Name = co["Name"],
|
||||
VatNumber = co["TaxId"],
|
||||
Country = co["CountryCode"] ?? "HU",
|
||||
ZipCode = site["PostalCode"],
|
||||
City = site["City"],
|
||||
Street = site["Street"],
|
||||
Phone = co["Phone"],
|
||||
Email = co["Email"],
|
||||
},
|
||||
},
|
||||
|
||||
// Default ÉRTÉK szándékosan nincs: be nem töltött config → 0. A 0 árfolyam a számításkor hibát dob,
|
||||
// a 0 küszöb „mindent jelentünk" — egyik sem számol elavult, kódba égetett értékkel.
|
||||
EurHufRate = c.GetValue<double>("ExchangeRate:EurHuf"),
|
||||
ThresholdWeightKg = c.GetValue<double>("Thresholds:WeightKg"),
|
||||
ThresholdValueHuf = c.GetValue<int>("Thresholds:ValueHuf"),
|
||||
Name = c["Name"],
|
||||
TaxId = c["TaxId"],
|
||||
CountryCode = c["CountryCode"] ?? "HU", // a NAV EKÁER magyar
|
||||
PostalCode = c["PostalCode"],
|
||||
City = c["City"],
|
||||
Street = c["Street"],
|
||||
// UnloadLocation (saját telephely): magyar címnél a NAV Name/VatNumber/Phone/Email-t is kér — TODO configból összeállítani.
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -259,10 +229,8 @@ public class PluginNopStartup : INopStartup
|
|||
})
|
||||
.AddAcBinaryProtocol(opts =>
|
||||
{
|
||||
//opts.ProtocolMode = BinaryProtocolMode.AsyncSegment;
|
||||
opts.ProtocolMode = BinaryProtocolMode.Bytes;
|
||||
opts.ProtocolMode = BinaryProtocolMode.AsyncSegment;
|
||||
opts.FlushPolicy = FlushPolicy.Coalesced;
|
||||
opts.FlushTimeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
// Explicit AcLogger instance (name-based category, matches the previous setup).
|
||||
// If omitted, the options extension falls back to ILogger<AcBinaryHubProtocol> from DI.
|
||||
|
|
|
|||
|
|
@ -49,9 +49,6 @@ public partial class NameCompatibility : INameCompatibility
|
|||
|
||||
{ typeof(CargoPartner), FruitBankConstClient.CargoPartnerDbTableName},
|
||||
{ typeof(CargoTruck), FruitBankConstClient.CargoTruckDbTableName},
|
||||
|
||||
{ typeof(PartnerDepot), FruitBankConstClient.PartnerDepotDbTableName},
|
||||
{ typeof(EkaerHistory), FruitBankConstClient.EkaerHistoryDbTableName},
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ Scope: a FruitBank EKÁER-bejelentés szerver-oldali / nopCommerce-integrációs
|
|||
|
||||
## MGFBANKPLUG-EKAER-I-T3X8: A `Product.Gtin` átmenetileg a VTSZ-t tárolja — szétválasztandó
|
||||
|
||||
**Status:** Open · **Priority:** P3 · **Type:** I (adatmodell / átmeneti megoldás)
|
||||
**Status:** Closed (2026-06-02) — superseded · **Priority:** P3 · **Type:** I (adatmodell / átmeneti megoldás)
|
||||
|
||||
> **Újranyitva (2026-06-15):** a probléma **továbbra is fennáll** — a `Gtin` jelenleg is a VTSZ-t tárolja, a szétválasztás nem történt meg. A 2026-06-02-i `FBANKAPP-DMODEL-I-P6X4`-be (DATAMODEL) helyezés visszavonva; a követés ITT (EKÁER) folytatódik. *(A `FBANKAPP-DMODEL-I-P6X4` ennek megfelelően lezárva — DATAMODEL.)*
|
||||
### Resolution
|
||||
**Áthelyezve** az általános adatmodell-topicba: **`FBANKAPP-DMODEL-I-P6X4`** (`FruitBank.Common/docs/DATAMODEL/DATAMODEL_ISSUES.md#fbankapp-dmodel-i-p6x4`). Indok: a GTIN/VTSZ szétválasztás **általános** adatmodell-kérdés, nem EKÁER-specifikus — az EKÁER csak felhasználója. A követés ott folytatódik; az alábbi leírás referencia.
|
||||
|
||||
Az EKÁER `tradeCardItem.productVtsz` (kötelező, 8 jegyű vámtarifaszám) forrása jelenleg a nopCommerce **`Product.Gtin`** oszlop (a `ProductDto.Gtin`-en keresztül). A GTIN és a VTSZ **fogalmilag különböző**:
|
||||
- **GTIN** — globális kereskedelmi cikkszám (vonalkód-azonosító, EAN/UPC).
|
||||
|
|
@ -26,18 +27,3 @@ Egy termékhez a kettő nem azonos; a `Gtin` oszlop VTSZ-ként való használata
|
|||
**Affected:**
|
||||
- `FruitBank.Common/Dtos/ProductDto.cs` → `Gtin` property (a `[Column(nameof(Product.Gtin))]` jelöléssel, summary-ban megjelölve)
|
||||
- jövőbeli: `ShippingToEkaerMapper` (`FruitBank.Common/Services/Ekaer/`) — a `productVtsz`-t a külön `Vtsz` mezőből olvassa
|
||||
|
||||
## MGFBANKPLUG-EKAER-I-M6Y2: A számla-pénznem a Partnerből jön — jobb lenne a ShippingDocument-en
|
||||
|
||||
**Status:** Open · **Priority:** P3 · **Type:** I (adatmodell)
|
||||
|
||||
Az EKÁER tétel-érték HUF-ra számításához a forrás-pénznem jelenleg a **`Partner.Currency`**-ből jön (a beszállító alap-devizája). **Most 100%-ban megbízható** — a partner egyértelműen meghatározza a devizát.
|
||||
|
||||
**Hatás:** működik; de a pénznem **konceptuálisan a szállítmányhoz / dokumentumhoz tartozik**, nem a partnerhez (egy partner elvileg többféle devizában is számlázhat).
|
||||
|
||||
**Javítási irány:** külön **`Currency` oszlop a `ShippingDocument`-en**, és az `EkaerValueCalculator` / `ShippingToEkaerMapper` onnan olvasson; a `Partner.Currency` maradjon fallback. **Nem prioritás** — a jelenlegi partner-alapú forrás elegendő.
|
||||
|
||||
**Affected:**
|
||||
- `FruitBank.Common/Entities/ShippingDocument.cs` — jövőbeli `Currency` oszlop
|
||||
- `FruitBank.Common/Services/Ekaer/EkaerValueCalculator.cs` + `ShippingToEkaerMapper.cs` — a deviza-forrás
|
||||
- jelenlegi forrás: `PartnerBase.Currency`
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -11,7 +11,7 @@ A FruitBank EKÁER-bejelentés **szerver-oldali** (a kliens már standalone WASM
|
|||
| Réteg | Hol | Mit ad |
|
||||
|---|---|---|
|
||||
| **NAV/EKÁER framework** | `AyCode.Services/Nav/` + `Nav/Ekaer/` (AyCode.Core) | transport, auth (SHA-512), generált modellek, **`EkaerTradeCardValidator`** (NAV-séma + üzleti szabályok), **`EkaerSubmitService`** (validate→send), `EkaerManageService` (HTTP). Általános — **nem** FruitBank-specifikus. |
|
||||
| **FruitBank leképezés** | `FruitBank.Common/Services/Ekaer/` | **`ShippingToEkaerMapper`** — a forrást (bejövő `ShippingDocument`-csoport / kimenő `OrderDto`) egy irány-független **`EkaerConsignment`**-re képezi (`ToConsignment`), abból épül a `tradeCard` (`BuildTradeCard`). **`EkaerReportability`** — bejelentés-kötelezettség (küszöb/külföld). **`EkaerValueCalculator`** — tétel-érték HUF-ban. |
|
||||
| **FruitBank leképezés** | `FruitBank.Common/Services/Ekaer/` | **`ShippingToEkaerMapper`** — `Shipping` → `tradeCard` ops (tiszta leképező, nem validál) |
|
||||
| **FruitBank fogyasztó** | `FruitBank.Common.Server/Services/Ekaer/` | **`FruitBankEkaerService`** — szerver-oldali orchestráció: credentials/options + map → submit |
|
||||
| **nopCommerce-integráció (ez a plugin)** | ez a folder + a plugin kód | DI-bekötés, NAV-fiók settings, a VTSZ-forrás (`Product.Gtin`), SignalR-trigger a kliens felől, és ez a doksi |
|
||||
|
||||
|
|
@ -26,10 +26,6 @@ FruitBankEkaerService.SubmitShippingAsync(shipping) // FruitBank.Common.S
|
|||
```
|
||||
Validációs hiba → **hibalista** (`EkaerSubmitResult.Invalid`), nem megy ki kérés. NAV-oldali hiba → `NavReportException` propagál.
|
||||
|
||||
**További belépési pontok** (a kézi NAV-beadás munkafolyamathoz — `FruitBankDataController` SignalR-endpointok, kliens felől triggerelve):
|
||||
- **`CreateMissingEkaerHistories(fromDate)`** — reconciliation-kapu: a dátumtól rekord nélküli szállítólevelekre/rendelésekre `Pending` `EkaerHistory` sorokat hoz létre, **csak ha kötelező** (lásd a kapu-szakaszt lentebb). `EkaerCreateResult`-ot ad vissza (létrehozott szám + a kihagyott tételek üzenetei).
|
||||
- **`GenerateEkaerXmlDocument(foreignKey, isOutgoing)`** — egy sorra legenerálja + validálja a tradeCard XML-t (NEM küld NAV-ot); a `Status` / `ErrorText` / `ConversionRate` töltődik, az XML vágólapra másolható a kézi beadáshoz.
|
||||
|
||||
## Companion fájlok
|
||||
|
||||
- [`EKAER_ISSUES.md`](EKAER_ISSUES.md) — ismert problémák (pl. `MGFBANKPLUG-EKAER-I-T3X8`: GTIN≠VTSZ).
|
||||
|
|
@ -40,32 +36,15 @@ Validációs hiba → **hibalista** (`EkaerSubmitResult.Invalid`), nem megy ki k
|
|||
| EKÁER | FruitBank forrás |
|
||||
|---|---|
|
||||
| `seller*` (feladó) | `ShippingDocument.Partner` (a beszállító) |
|
||||
| `carrierText` (szállítmányozó — **opcionális**, NAV nem követeli) | **bejövő:** `Shipping.CargoPartner.Name` (valós, külön fuvarozó); **kimenő:** — **üres** (a vevő maga viszi el, ő már a címzett; nincs külön fuvarozónk) |
|
||||
| `carrierText` (fuvarozó, szöveges) | `Shipping.CargoPartner.Name` |
|
||||
| `vehicle` / `vehicle2` | `Shipping.CargoTruck` / `CargoTrailer` (`LicencePlate` + `CountryCode`, normalizálva) |
|
||||
| tétel `productVtsz` | `ShippingItem.ProductDto.Gtin` (átmenetileg — lásd `EKAER_ISSUES.md`) |
|
||||
| tétel `productName` | `ShippingItem.ProductName` (a VTSZ-szerinti csoportosítás + a csoport-név `Category`-ből: TODO **T-V9G3**) |
|
||||
| tétel `productName` | `ShippingItem.ProductName` |
|
||||
| tétel `weight` | `ShippingItem.MeasuredGrossWeight` (bruttó) |
|
||||
| tétel `tradeReason` | `A` (beszerzés — bejövő áru). ⚠️ Az enum: `S`=értékesítés, `A`=beszerzés, `W`=bérmunka, `O`=egyéb. |
|
||||
|
||||
A nyitott pontok (tradeType, destination, value/deviza, granularitás, kimenő irány): [`EKAER_TODO.md`](EKAER_TODO.md).
|
||||
|
||||
## Reconciliation-kapu, kötelezettség, életciklus (2026-06)
|
||||
|
||||
A kézi NAV-beadás munkafolyamat: a **kapu** létrehozza a sorokat → a user **generál** + másol/beküld → kézzel rögzíti a NAV-választ.
|
||||
|
||||
**`EkaerHistory` életciklus** (egy sor = egy tradeCard; `ForeignKey` = bejövő `ShippingDocument.Id` / kimenő `Order.Id` + `IsOutgoing`; a `Status` int-oszlop + `[NotColumn]` enum-nézet):
|
||||
`Pending` (a kapu hozta létre) → **Generate** → `Generated` / `GeneratedWithWarning` (küldhető, de pótlandó — pl. hiányzó rendszám) / `ValidationError` (blokkoló) → kézi NAV-beadás után `Sent` / `SentWithMissingData` (pótlásra vár). A grid 3 tabja ezekre szűr (szerver-oldali `EkaerHistoryFilter`), a warning ⚠️ / error ⛔ ikonnal megkülönböztetve.
|
||||
|
||||
**Bejelentés-kötelezettség** — `EkaerReportability.Evaluate(consignment, settings)`, a kapu hívja egységenként:
|
||||
- **egység:** bejövőnél a `(Shipping, Partner)` csoport (= feladó→címzett→jármű) — a tömeg/érték a csoport **ÖSSZES** dokumentumára **aggregálva** (13/2020 PM rendelet); kimenőnél egy rendelés;
|
||||
- **küszöb felett** → kötelező, az országkódtól **függetlenül** (az országkód-hibát a generate-validálás jelzi);
|
||||
- **küszöb alatt** → csak a **külföldi** reláció kötelező (a feladó és a címzett országkódja **eltér**); belföld → nem; hiányzó/érvénytelen (nem ISO-2) országkód → **`DataError`** (a sor kimarad + üzenet a popupban);
|
||||
- **mentesség:** `Partner.IsEkaer = false`, és a Partner nélküli (félkész) szállítólevél — csendben kimaradnak.
|
||||
|
||||
> **A küszöb configból jön** (`Ekaer:Thresholds:WeightKg` / `:ValueHuf`) — a kódba **NINCS drótozva**, mert a NAV-jogszabály és a beállítás szerint **változhat**. A konkrét, dátumozott értékeket és a termékkategóriákat lásd [`EKAER_TODO.md`](EKAER_TODO.md) **T-W3R8**. A NAV **mindkét irányban büntet** (alul- ÉS túljelentés), ezért a küszöbnek pontosnak kell lennie.
|
||||
|
||||
**Egyesített leképezés:** a bejövő/kimenő forrás egy **`EkaerConsignment`** köztes modellre képződik (két adapter), és **abból** épül MIND a tradeCard (`BuildTradeCard`), MIND a kötelezettség-döntés — így az irányfüggő rész két adapterre szorul, a közös logika egy helyen van. A `MapDocument` / `MapOrder` / `MapShipping` ennek vékony wrappere.
|
||||
|
||||
## Bekötés a szerveren (DI)
|
||||
|
||||
A plugin (vagy a szerver-startup) köti be a láncot — a NAV-fiók titkos adatai a szerver config/secret store-jából:
|
||||
|
|
@ -75,6 +54,6 @@ services.AddHttpClient<EkaerManageService>(/* NAV TLS 1.2 */);
|
|||
services.AddScoped<IEkaerTradeCardValidator, EkaerTradeCardValidator>();
|
||||
services.AddScoped<IEkaerSubmitService, EkaerSubmitService>();
|
||||
services.AddScoped<IShippingToEkaerMapper, ShippingToEkaerMapper>();
|
||||
services.AddSingleton<IEkaerSettings>(/* EkaerSettings: Ekaer:Company + Site + küszöbök + EUR-HUF árfolyam — configból */);
|
||||
services.AddSingleton(/* EkaerMappingOptions: destination + saját raktár */);
|
||||
services.AddScoped<IFruitBankEkaerService, FruitBankEkaerService>();
|
||||
```
|
||||
|
|
|
|||
Loading…
Reference in New Issue