Szállítólevél-törlés a tételeivel együtt, és az előrendelés-teljesítés nyilvántartása
A törlés eddig bezárt feltétel-láncon állt: beolvasott dokumentumnak mindig van tétele, tehát a "nincs tétele" feltétel miatt sosem volt törölhető — és vele a fájl-ág meg az újraimport is elérhetetlen maradt. - fbPreOrderFulfilment (új tábla, SQL: docs/PREORDER/PREORDER_fulfilment_schema.sql): melyik szállítólevél mennyit teljesített egy előrendelés-tételből, és melyik rendelés-tétel lett belőle. Futásonként egy sor a delta-mennyiséggel — nem oszlop a PreOrderItem-en, mert egy tételt több dokumentum is feltölthet - PreOrderConversionService: a közös InsertOrderItemsAsync írja a sort, tehát az első konverzió és a későbbi feltöltés is rögzül. Eddig ez az adat csak a rendelés-megjegyzés SZÖVEGÉBEN létezett - ShippingCleanupService: a "nincs tétele" helyére négy feltétel lépett — nincs lemért raklap, nincs kamion-allocation (ADR 0003), nincs előrendelés-teljesítés, nincs befelé irányuló EKÁER. A tételeket és a raklapjaikat a meglévő event-kaszkád viszi; a guard az egyetlen védelem, ezért a tranzakción belül újraértékel - CostAttributionService: konvertált rendelés-tételnél a teljesítő dokumentum rétegei fogynak először, a többi utánuk marad — ha a megjelölt dokumentum elfogyott, a maradék a valós készletre kerül, nem "ismeretlen forrásúra" - Entitás, DbTable, DbSet-interfész, DbContext-, DI- és NameCompatibility-bekötés - PREORDER/README, MARGIN/README, MARGIN_TODO átvezetve Korlát: teljesítés-sor csak a bevezetés óta keletkezik, a korábbi konverziókat sem a törlés-guard, sem az árrés forrás-választása nem látja.
This commit is contained in:
@@ -46,7 +46,8 @@ public class FruitBankDbContext : MgDbContextBase,
|
||||
IFilesDbSet<FilesDbTable>,
|
||||
ICustomerCreditDbSet<CustomerCreditDbTable>,
|
||||
IShippingCostDbSet<ShippingCostDbTable>,
|
||||
ICostConsumptionDbSet<CostConsumptionDbTable>
|
||||
ICostConsumptionDbSet<CostConsumptionDbTable>,
|
||||
IPreOrderFulfilmentDbSet<PreOrderFulfilmentDbTable>
|
||||
{
|
||||
private readonly FruitBankAttributeService _fruitBankAttributeService;
|
||||
private readonly CostAttributionService _costAttributionService;
|
||||
@@ -81,6 +82,9 @@ public class FruitBankDbContext : MgDbContextBase,
|
||||
/// <summary>Költség-réteg fogyasztások (ADR 0002-R7Q2) — az árrés-modell egyetlen tárolt adata.</summary>
|
||||
public CostConsumptionDbTable CostConsumptions { get; set; }
|
||||
|
||||
/// <summary>Előrendelés-teljesítések: melyik szállítólevél mennyit teljesített egy előrendelés-tételből.</summary>
|
||||
public PreOrderFulfilmentDbTable PreOrderFulfilments { get; set; }
|
||||
|
||||
public OrderItemPalletDbTable OrderItemPallets { get; set; }
|
||||
|
||||
public FilesDbTable Files { get; set; }
|
||||
@@ -111,6 +115,7 @@ public class FruitBankDbContext : MgDbContextBase,
|
||||
OrderDraftDbTable orderDraftDbTable, OrderDraftItemDbTable orderDraftItemDbTable,
|
||||
ExternalUserPartnerMapDbTable externalUserPartnerMapDbTable, ProductTextMappingDbTable productTextMappingDbTable,
|
||||
ShippingCostDbTable shippingCostDbTable, CostConsumptionDbTable costConsumptionDbTable,
|
||||
PreOrderFulfilmentDbTable preOrderFulfilmentDbTable,
|
||||
CostAttributionService costAttributionService,
|
||||
IProductService productService, IStaticCacheManager staticCacheManager,
|
||||
IRepository<Order> orderRepository,
|
||||
@@ -174,6 +179,7 @@ public class FruitBankDbContext : MgDbContextBase,
|
||||
|
||||
ShippingCosts = shippingCostDbTable;
|
||||
CostConsumptions = costConsumptionDbTable;
|
||||
PreOrderFulfilments = preOrderFulfilmentDbTable;
|
||||
_costAttributionService = costAttributionService;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using Mango.Nop.Data.Interfaces;
|
||||
using Nop.Data;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Domains.Entities;
|
||||
|
||||
namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer.Interfaces;
|
||||
|
||||
public interface IPreOrderFulfilmentDbSet<TDbTable> : IMgDbTableBase where TDbTable : IRepository<PreOrderFulfilment>
|
||||
{
|
||||
public TDbTable PreOrderFulfilments { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using LinqToDB;
|
||||
using Mango.Nop.Data.Repositories;
|
||||
using Nop.Core.Caching;
|
||||
using Nop.Core.Configuration;
|
||||
using Nop.Core.Events;
|
||||
using Nop.Data;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Domains.Entities;
|
||||
|
||||
namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||
|
||||
/// <summary>Előrendelés-teljesítések: melyik szállítólevél mennyit teljesített egy előrendelés-tételből.</summary>
|
||||
public class PreOrderFulfilmentDbTable(IEventPublisher eventPublisher, INopDataProvider dataProvider, IShortTermCacheManager shortTermCacheManager, IStaticCacheManager staticCacheManager, AppSettings appSettings)
|
||||
: MgDbTableBase<PreOrderFulfilment>(eventPublisher, dataProvider, shortTermCacheManager, staticCacheManager, appSettings)
|
||||
{
|
||||
public override IQueryable<PreOrderFulfilment> GetAll() => base.GetAll();
|
||||
|
||||
/// <summary>Egy szállítólevél teljesítései — a törlés-guard erre kérdez rá.</summary>
|
||||
public IQueryable<PreOrderFulfilment> GetAllByShippingDocumentId(int shippingDocumentId)
|
||||
=> GetAll().Where(f => f.ShippingDocumentId == shippingDocumentId);
|
||||
|
||||
/// <summary>Egy rendelés-tétel forrásai — az árrés ezen át jut el a forrás-dokumentumig.</summary>
|
||||
public IQueryable<PreOrderFulfilment> GetAllByOrderItemId(int orderItemId)
|
||||
=> GetAll().Where(f => f.OrderItemId == orderItemId);
|
||||
|
||||
/// <summary>Több rendelés-tétel forrásai egy menetben — a riport így kerüli az N+1-et.</summary>
|
||||
public IQueryable<PreOrderFulfilment> GetAllByOrderItemIds(IReadOnlyCollection<int> orderItemIds)
|
||||
=> GetAll().Where(f => f.OrderItemId != null && orderItemIds.Contains(f.OrderItemId.Value));
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using AyCode.Interfaces.TimeStampInfo;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.Entities;
|
||||
|
||||
/// <summary>Egy előrendelés-tétel teljesítése egy konkrét szállítólevélből.</summary>
|
||||
/// <remarks>
|
||||
/// Ma ez az adat sehol nem marad meg: a <c>PreOrderConversionService</c> a szállítólevél azonosítóját csak egy
|
||||
/// rendelés-megjegyzés SZÖVEGÉBE írja. Két fogyasztója van:
|
||||
/// <list type="number">
|
||||
/// <item>a szállítólevél törlése — blokkol, ha van rá teljesítés-sor;</item>
|
||||
/// <item>az árrés (ADR 0002-R7Q2) — a konvertált rendelés-tételnél a forrás így egzakt, nem FIFO-becslés.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>Miért külön sor és nem oszlop a <c>PreOrderItem</c>-en: egy tételt TÖBB dokumentum tölthet fel
|
||||
/// részletekben (a konverzió a <c>min(Requested − Fulfilled, available)</c> deltával dolgozik). Egy oszlop csak
|
||||
/// az elsőt vagy az utolsót őrizné, és a törlés-guard emiatt alul-blokkolna.</para>
|
||||
///
|
||||
/// <para>Nincs <c>[AcBinarySerializable]</c>: ez a típus nem megy a wire-en — admin- és riport-adat.</para>
|
||||
/// </remarks>
|
||||
[ToonDescription("Fulfilment of one preorder line from one delivery note",
|
||||
Purpose = "Records which shipping document fulfilled how much of a preorder item, and which order item it " +
|
||||
"produced. One preorder item may be topped up across several documents, so this is a row per " +
|
||||
"conversion run, never a column on the item.")]
|
||||
[Table(Name = FruitBankPluginConst.PreOrderFulfilmentDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankPluginConst.PreOrderFulfilmentDbTableName)]
|
||||
public sealed class PreOrderFulfilment : MgEntityBase, ITimeStampCreated
|
||||
{
|
||||
/// <summary>A teljesített előrendelés-tétel.</summary>
|
||||
[Column] public int PreOrderItemId { get; set; }
|
||||
|
||||
/// <summary>A szállítólevél, amiből a teljesítés történt — a törlés-guard erre kérdez rá.</summary>
|
||||
[Column] public int ShippingDocumentId { get; set; }
|
||||
|
||||
/// <summary>Az ebben a futásban keletkezett rendelés-tétel; ezen át jut el az árrés a forrás-dokumentumig.</summary>
|
||||
[Column] public int? OrderItemId { get; set; }
|
||||
|
||||
/// <summary>Az EBBEN a futásban teljesített mennyiség (delta), nem a kumulált <c>FulfilledQuantity</c>.</summary>
|
||||
[Column] public int Quantity { get; set; }
|
||||
|
||||
/// <summary>Létrehozás UTC időbélyege — a CRUD-hook tölti, kézzel nem állítandó.</summary>
|
||||
[Column] public DateTime Created { get; set; }
|
||||
}
|
||||
@@ -53,5 +53,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
|
||||
|
||||
/// <summary>Quantity consumed from one purchase (cost layer).</summary>
|
||||
public const string CostConsumptionDbTableName = "fbCostConsumption";
|
||||
|
||||
/// <summary>Fulfilment of one preorder line from one delivery note.</summary>
|
||||
public const string PreOrderFulfilmentDbTableName = "fbPreOrderFulfilment";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ public class PluginNopStartup : INopStartup
|
||||
services.AddScoped<ShippingItemToShippingDbTable>();
|
||||
services.AddScoped<ShippingCostDbTable>();
|
||||
services.AddScoped<CostConsumptionDbTable>();
|
||||
services.AddScoped<PreOrderFulfilmentDbTable>();
|
||||
services.AddScoped<CostAttributionService>();
|
||||
services.AddScoped<ShippingCostService>();
|
||||
services.AddScoped<MarginCalculationService>();
|
||||
|
||||
@@ -59,6 +59,7 @@ public partial class NameCompatibility : INameCompatibility
|
||||
// Plugin-tulajdonú, a klienssel NEM megosztott táblák (ADR 0002-R7Q2) — a nevük a FruitBankPluginConst-ban.
|
||||
{ typeof(ShippingCost), FruitBankPluginConst.ShippingCostDbTableName},
|
||||
{ typeof(CostConsumption), FruitBankPluginConst.CostConsumptionDbTableName},
|
||||
{ typeof(PreOrderFulfilment), FruitBankPluginConst.PreOrderFulfilmentDbTableName},
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||
public class CostAttributionService(
|
||||
CostConsumptionDbTable costConsumptions,
|
||||
ShippingItemDbTable shippingItems,
|
||||
PreOrderFulfilmentDbTable preOrderFulfilments,
|
||||
IEnumerable<IAcLogWriterBase> logWriters)
|
||||
{
|
||||
private readonly Logger<CostAttributionService> _logger = new(logWriters.ToArray());
|
||||
@@ -78,7 +79,17 @@ public class CostAttributionService(
|
||||
var needed = isMeasurable ? netWeight : quantity;
|
||||
if (needed <= 0) return 0;
|
||||
|
||||
var layers = await LoadLayersAsync(productId, isMeasurable);
|
||||
// Előrendelésből konvertált rendelés-tételnél a forrás NEM tipp: a konverzió rögzítette, melyik
|
||||
// szállítólevél teljesítette (ADR 0002-R7Q2, 2. pont). Ilyenkor annak a dokumentumnak a rétegei
|
||||
// fogynak először; a FIFO csak azon belül, illetve utánuk dönt.
|
||||
var preferredDocumentIds = consumerType == CostConsumerType.OrderItem
|
||||
? await preOrderFulfilments.GetAllByOrderItemId(consumerId)
|
||||
.Select(f => f.ShippingDocumentId)
|
||||
.Distinct()
|
||||
.ToListAsync()
|
||||
: [];
|
||||
|
||||
var layers = await LoadLayersAsync(productId, isMeasurable, preferredDocumentIds);
|
||||
|
||||
// A tétel összesenjei a MÁSIK dimenzió arányosításához; a vezérlő dimenzió mindig egzakt.
|
||||
var totals = new ItemTotals(needed, quantity, netWeight);
|
||||
@@ -113,14 +124,19 @@ public class CostAttributionService(
|
||||
}
|
||||
|
||||
/// <summary>A termék beszerzési rétegei, a legrégebbi elöl, a még ki nem adott mennyiségükkel.</summary>
|
||||
private async Task<List<LayerBalance>> LoadLayersAsync(int productId, bool isMeasurable)
|
||||
/// <param name="preferredDocumentIds">
|
||||
/// Előrendelésből konvertált tételnél a teljesítő szállítólevél(ek): ezek rétegei kerülnek előre, FIFO-sorrend
|
||||
/// szerint egymás között. A többi réteg utánuk marad — nem esik ki, mert ha a megjelölt dokumentum már
|
||||
/// elfogyott, a maradékot inkább a valós készletre osztjuk, mint „ismeretlen forrásúra".
|
||||
/// </param>
|
||||
private async Task<List<LayerBalance>> LoadLayersAsync(int productId, bool isMeasurable, IReadOnlyCollection<int> preferredDocumentIds)
|
||||
{
|
||||
// Csak a lemért (bevételezett) tétel lehet réteg: a fajlagos önköltséghez a MÉRT mennyiség az osztó.
|
||||
var candidates = await shippingItems.GetAll()
|
||||
.Where(si => si.ProductId == productId && si.IsMeasured)
|
||||
.OrderBy(si => si.ShippingDocument.ShippingDate)
|
||||
.ThenBy(si => si.Id)
|
||||
.Select(si => new { si.Id, si.MeasuredQuantity, si.MeasuredNetWeight })
|
||||
.Select(si => new { si.Id, si.ShippingDocumentId, si.MeasuredQuantity, si.MeasuredNetWeight })
|
||||
.ToListAsync();
|
||||
|
||||
if (candidates.Count == 0) return [];
|
||||
@@ -142,6 +158,10 @@ public class CostAttributionService(
|
||||
.ToDictionary(g => g.Key, g => isMeasurable ? g.Sum(x => x.NetWeight) : g.Sum(x => x.Quantity));
|
||||
|
||||
return candidates
|
||||
// A megjelölt dokumentum rétegei előre; a lekérdezés FIFO-sorrendje ezen belül és a többin is megmarad
|
||||
// (az OrderBy stabil). Egyetlen sorrend-döntés, nem külön ág — így a maradék automatikusan a normál
|
||||
// FIFO-n folytatódik.
|
||||
.OrderByDescending(c => preferredDocumentIds.Contains(c.ShippingDocumentId))
|
||||
.Select(c =>
|
||||
{
|
||||
var capacity = isMeasurable ? c.MeasuredNetWeight : c.MeasuredQuantity;
|
||||
|
||||
@@ -11,6 +11,7 @@ using Nop.Core.Domain.Payments;
|
||||
using Nop.Core.Domain.Shipping;
|
||||
using Nop.Core.Events;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Domains.Entities;
|
||||
using Nop.Services.Catalog;
|
||||
using Nop.Services.Customers;
|
||||
using Nop.Services.Orders;
|
||||
@@ -453,7 +454,7 @@ public partial class PreOrderConversionService
|
||||
CreatedOrUpdatedDateUTC = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await InsertOrderItemsAsync(order, fulfilledItems, gainedByItemId);
|
||||
await InsertOrderItemsAsync(order, fulfilledItems, gainedByItemId, shippingDocumentId);
|
||||
|
||||
// Recalculate header totals from the actual inserted order items rather than the
|
||||
// PreOrderItem snapshot price: that snapshot is 0 for OrderDraft-origin preorders
|
||||
@@ -506,7 +507,7 @@ public partial class PreOrderConversionService
|
||||
}
|
||||
|
||||
// Append new OrderItems for the quantity gained THIS run (delta) only
|
||||
await InsertOrderItemsAsync(order, newlyFulfilled, gainedByItemId);
|
||||
await InsertOrderItemsAsync(order, newlyFulfilled, gainedByItemId, shippingDocumentId);
|
||||
|
||||
// Recalculate order total from all order items
|
||||
var allItems = await _dbContext.OrderItems.Table
|
||||
@@ -535,7 +536,8 @@ public partial class PreOrderConversionService
|
||||
|
||||
// ── Shared helpers ────────────────────────────────────────────────────────
|
||||
|
||||
private async Task InsertOrderItemsAsync(Order order, List<PreOrderItem> items, IReadOnlyDictionary<int, int> gainedByItemId)
|
||||
private async Task InsertOrderItemsAsync(Order order, List<PreOrderItem> items, IReadOnlyDictionary<int, int> gainedByItemId,
|
||||
int shippingDocumentId)
|
||||
{
|
||||
var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId);
|
||||
var store = await _storeContext.GetCurrentStoreAsync();
|
||||
@@ -597,6 +599,18 @@ public partial class PreOrderConversionService
|
||||
// Use the service (fires NopCommerce events) instead of direct DB insert
|
||||
await _orderService.InsertOrderItemAsync(orderItem);
|
||||
|
||||
// A teljesítés ténye strukturáltan is rögzül — eddig csak a rendelés-megjegyzés SZÖVEGÉBE került.
|
||||
// Két fogyasztója van: a szállítólevél törlés-guardja, és az árrés (a konvertált tételnél a forrás
|
||||
// így egzakt, nem FIFO-becslés). A mennyiség a MOSTANI futás deltája, mert egy tételt több
|
||||
// dokumentum is feltölthet.
|
||||
await _dbContext.PreOrderFulfilments.InsertAsync(new PreOrderFulfilment
|
||||
{
|
||||
PreOrderItemId = item.Id,
|
||||
ShippingDocumentId = shippingDocumentId,
|
||||
OrderItemId = orderItem.Id,
|
||||
Quantity = quantityToAdd
|
||||
});
|
||||
|
||||
// Deduct from stock — same as CustomOrderController and FruitBankOrderItemService
|
||||
await _productService.AdjustInventoryAsync(
|
||||
product,
|
||||
|
||||
@@ -23,6 +23,13 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||
/// azok feltétel nélküliek, és mindegyik <b>saját</b> tranzakciót nyit, amin kívülre esne az ellenőrzés.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>A dokumentum törlése a tételeit is viszi</b> (2026-08-16), ha a kiolvasás eredménye érintetlen: nincs
|
||||
/// lemérve semmi, nincs kamionra kiosztva, nem teljesített előrendelést, nincs rá EKÁER. Enélkül a feltétel-lánc
|
||||
/// bezárt: beolvasott dokumentumnak mindig van tétele, tehát sosem lett volna törölhető — és vele a fájl-ág, az
|
||||
/// újraimport is elérhetetlen maradt.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>Későbbi kör: logikai törlés (Deleted flag) azoknak a tételeknek, amikhez van kapcsolt adat.</para>
|
||||
/// </summary>
|
||||
public sealed class ShippingCleanupService(FruitBankDbContext ctx, FileStorageService fileStorageService)
|
||||
@@ -67,7 +74,13 @@ public sealed class ShippingCleanupService(FruitBankDbContext ctx, FileStorageSe
|
||||
#region ShippingDocument
|
||||
|
||||
/// <summary>
|
||||
/// Dokumentum törölhető, ha nincs tétele ÉS nincs rá <b>befelé irányuló</b> EKÁER-bejelentés.
|
||||
/// Dokumentum törölhető, ha a kiolvasás eredménye még <b>érintetlen</b>: nincs lemérve semmi, nincs kamionra
|
||||
/// kiosztva, nem teljesített előrendelést, és nincs rá <b>befelé irányuló</b> EKÁER-bejelentés.
|
||||
/// <para>
|
||||
/// A törlés a <b>tételeit is elviszi</b> (a kaszkádon át). Ez nem mond ellent annak, hogy egy tétel magában
|
||||
/// nem törölhető: azt az AI a dokumentumból olvasta ki, egyet nem lehet újragenerálni — az egész
|
||||
/// kiolvasás-eredmény eldobása viszont értelmes művelet, és a fájl-ág is csak így válik elérhetővé.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Az <c>EkaerHistoryMapping.ForeignKey</c> polimorf: kifelé a <c>Order.Id</c>-t jelenti, ezért a
|
||||
/// szülő <c>EkaerHistory.IsOutgoing == false</c> szűrés nélkül egy azonos azonosítójú rendelés
|
||||
@@ -81,14 +94,48 @@ public sealed class ShippingCleanupService(FruitBankDbContext ctx, FileStorageSe
|
||||
var document = await ctx.ShippingDocuments.GetByIdAsync(shippingDocumentId, false);
|
||||
if (document == null) return DeleteCheckResult.Deny("A dokumentum nem található.");
|
||||
|
||||
var itemCount = await ctx.ShippingItems.GetAll()
|
||||
.CountAsync(si => si.ShippingDocumentId == shippingDocumentId);
|
||||
var itemIds = await ctx.ShippingItems.GetAll()
|
||||
.Where(si => si.ShippingDocumentId == shippingDocumentId)
|
||||
.Select(si => si.Id)
|
||||
.ToListAsync();
|
||||
|
||||
if (itemCount > 0)
|
||||
return DeleteCheckResult.Deny($"Nem törölhető: {itemCount} tétel tartozik hozzá.");
|
||||
if (itemIds.Count > 0)
|
||||
{
|
||||
// 1. Mérés. A raklap az igazságforrás, de a tétel saját jelzőjét is nézzük: ha bármelyik azt mondja,
|
||||
// hogy mérés történt, a kiolvasás-eredmény már nem érintetlen.
|
||||
var measuredPalletCount = await ctx.ShippingItemPallets.GetAll()
|
||||
.CountAsync(p => itemIds.Contains(p.ShippingItemId) && p.IsMeasured);
|
||||
|
||||
// Teljes névvel: a paraméter nélküli CountAsync kétértelmű a nop
|
||||
// System.Linq.AsyncIQueryableExtensions és a LinqToDB.AsyncExtensions között.
|
||||
if (measuredPalletCount > 0)
|
||||
return DeleteCheckResult.Deny($"Nem törölhető: {measuredPalletCount} lemért raklap tartozik a tételeihez.");
|
||||
|
||||
var measuredItemCount = await ctx.ShippingItems.GetAll()
|
||||
.CountAsync(si => itemIds.Contains(si.Id) && si.IsMeasured);
|
||||
|
||||
if (measuredItemCount > 0)
|
||||
return DeleteCheckResult.Deny($"Nem törölhető: {measuredItemCount} tétele le van mérve.");
|
||||
|
||||
// 2. Kamion-allocation (ADR 0003). A tervezőben már beosztott áru törlése a kamion rakományát
|
||||
// csendben megváltoztatná.
|
||||
var allocationCount = await ctx.ShippingItemAllocations.GetAll()
|
||||
.CountAsync(a => itemIds.Contains(a.ShippingItemId));
|
||||
|
||||
if (allocationCount > 0)
|
||||
return DeleteCheckResult.Deny($"Nem törölhető: {allocationCount} tétele kamionhoz van rendelve. Előbb vedd le a szállítmánytervezőben.");
|
||||
}
|
||||
|
||||
// 3. Előrendelés-teljesítés. A konverzió a beérkezett árut rendelésekre osztotta — a törlés ezt nem
|
||||
// fordítaná vissza. ⚠️ Csak a funkció bevezetése ÓTA keletkezett teljesítésekről van sorunk; a korábbi
|
||||
// konverziókról nincs, azokat ez a feltétel nem látja.
|
||||
// Teljes névvel, mint lent az EKÁER-nél: a paraméter nélküli CountAsync kétértelmű.
|
||||
var fulfilmentCount = await LinqToDB.AsyncExtensions.CountAsync(
|
||||
ctx.PreOrderFulfilments.GetAllByShippingDocumentId(shippingDocumentId));
|
||||
|
||||
if (fulfilmentCount > 0)
|
||||
return DeleteCheckResult.Deny($"Nem törölhető: {fulfilmentCount} előrendelés-teljesítés származik belőle.");
|
||||
|
||||
// 4. EKÁER. Teljes névvel: a paraméter nélküli CountAsync kétértelmű a nop
|
||||
// System.Linq.AsyncIQueryableExtensions és a LinqToDB.AsyncExtensions között.
|
||||
var ekaerCount = await LinqToDB.AsyncExtensions.CountAsync(
|
||||
from mapping in ctx.EkaerHistoryMappings.GetAll()
|
||||
join history in ctx.EkaerHistories.GetAll() on mapping.EkaerHistoryId equals history.Id
|
||||
@@ -108,6 +155,11 @@ public sealed class ShippingCleanupService(FruitBankDbContext ctx, FileStorageSe
|
||||
/// A fájl fizikai törlése tudatos: az újrafeldolgozás idempotencia-ellenőrzése a
|
||||
/// <c>Files.FileHash</c>-re kulcsol, tehát a sor bennhagyása véglegesen blokkolná az újraimportot.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A <b>tételeket és a raklapjaikat</b> nem itt töröljük: azokat a <c>FruitBankEventConsumer</c> kaszkádja
|
||||
/// viszi (dokumentum → tételek → raklapok), ezért megy a törlés <c>publishEvent: true</c>-val. A guard
|
||||
/// biztosítja, hogy a kaszkád csak érintetlen kiolvasás-eredményt vihessen.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public async Task<DeleteCheckResult> DeleteShippingDocumentAsync(int shippingDocumentId)
|
||||
{
|
||||
|
||||
@@ -56,8 +56,15 @@ lépések és az állapotuk.
|
||||
`AttributeStockTakingLossAsync`-et. Csak a **hiány** fogyaszt réteget — a leltári többlethez nem tartozik
|
||||
beszerzés, azt a modell nem tudja hova kötni. Menet közben javítva a riport `Maradt` oszlopa is: eddig az
|
||||
időszak eladásából számolt, ezért a korábbi hónapok fogyását és a leírást készletnek mutatta.*
|
||||
- [ ] **Előrendelés pontos forrása** — a `fbPreOrderItem` nem tárolja, melyik szállítólevél elégítette ki, ezért
|
||||
- [x] **Előrendelés pontos forrása** — a `fbPreOrderItem` nem tárolja, melyik szállítólevél elégítette ki, ezért
|
||||
ott is FIFO dönt. Egy oszlop + egy értékadás a `PreOrderConversionService`-ben.
|
||||
*Kész (2026-08-16), de **nem oszloppal**: egy tételt több dokumentum is feltölthet részletekben, ezért egy
|
||||
oszlop csak az elsőt vagy az utolsót őrizné. Új tábla: `fbPreOrderFulfilment` (SQL:
|
||||
[`PREORDER_fulfilment_schema.sql`](../PREORDER/PREORDER_fulfilment_schema.sql)), futásonként egy sor a
|
||||
delta-mennyiséggel. A `CostAttributionService` a teljesítő dokumentum rétegeit veszi előre; a többi réteg
|
||||
utánuk marad, tehát ha a megjelölt dokumentum elfogyott, a maradék a valós készletre kerül, nem
|
||||
„ismeretlen forrásúra". Ugyanez a tábla adja a szállítólevél-törlés guardját.*
|
||||
⚠️ Csak a bevezetés ÓTA keletkezett konverziókról van sor; a korábbiakra a forrás továbbra is FIFO.
|
||||
- [ ] **Futásidejű ellenőrzés: devizás beszerzés önköltsége** — egy ÚJ, EUR-os beszállítótól származó
|
||||
szállítólevélnél igazolni, hogy a dokumentum `FxRate`-je a nop `Currency` táblájából jön (≈357), és hogy az
|
||||
önköltség a várt nagyságrendbe esik. A régi dokumentumokat **nem** töltjük vissza (ADR 11. pont).
|
||||
|
||||
@@ -19,6 +19,9 @@ legrégebbitől. Ugyanabban a tranzakcióban fut, mint a készlet-levonás.
|
||||
|
||||
- A fogyasztás **vezérlő** mértékegysége terméktípus-függő, és követi az árazást: mérendőnél **kg**,
|
||||
nem mérendőnél **rekesz**. A sor a másik dimenziót is kitölti a tétel arányában.
|
||||
- **Előrendelésből konvertált tételnél a forrás nem tipp:** a konverzió rögzíti, melyik szállítólevél teljesítette
|
||||
(`fbPreOrderFulfilment`), és annak a dokumentumnak a rétegei fogynak először. A többi réteg utánuk marad, tehát
|
||||
ha a megjelölt dokumentum elfogyott, a maradék a valós készletre kerül, nem „ismeretlen forrásúra".
|
||||
- Réteg híján a sor **ismeretlen forrású** (`ShippingItemId = null`), és naplóba is kerül. Nulla önköltséggel
|
||||
beolvasztani tilos — a riport külön során jelenik meg.
|
||||
- Idempotens: ha egy fogyasztóhoz már van fogyasztás-sor, nem keletkezik újabb.
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/* =============================================================================
|
||||
ELŐRENDELÉS-TELJESÍTÉS — séma
|
||||
-----------------------------------------------------------------------------
|
||||
Egy tábla, amely rögzíti, hogy egy előrendelés-tételből MELYIK szállítólevél
|
||||
MENNYIT teljesített. Ma ez az adat sehol nem marad meg: a
|
||||
PreOrderConversionService a shippingDocumentId-t csak egy rendelés-megjegyzés
|
||||
SZÖVEGÉBE írja (InsertOrderNoteAsync).
|
||||
|
||||
Miért tábla és nem oszlop a fbPreOrderItem-en:
|
||||
egy tételt TÖBB dokumentum is feltölthet részletekben — a konverzió a
|
||||
`min(Requested - Fulfilled, available)` deltával dolgozik, és a kód kommentje
|
||||
külön kimondja, hogy „a partial item topped up across documents is not
|
||||
deducted/billed twice". Egy oszlop csak az első vagy az utolsó dokumentumot
|
||||
őrizné, és a törlés-guard emiatt ALUL-BLOKKOLNA: a korábbi dokumentum úgy
|
||||
tűnne, hogy nem hivatkozza semmi.
|
||||
|
||||
Két fogyasztója van:
|
||||
1. Szállítólevél törlése — blokkol, ha van rá teljesítés-sor.
|
||||
2. Árrés (ADR 0002-R7Q2) — a konvertált rendelés-tételnél a forrás így
|
||||
EGZAKT, nem FIFO-becslés.
|
||||
|
||||
🛡️ USE FruitBank_DEV;-re állítva. PROD-hoz írd át a USE-t a PROD adatbázis
|
||||
nevére. Séma-változás, ezért a futtatás előtt DB-mentés ajánlott.
|
||||
============================================================================= */
|
||||
|
||||
USE FruitBank_DEV; -- PROD-hoz írd át a PROD adatbázis nevére!
|
||||
SET NOCOUNT ON;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('dbo.fbPreOrderFulfilment', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.fbPreOrderFulfilment
|
||||
(
|
||||
Id int IDENTITY(1,1) NOT NULL,
|
||||
|
||||
/* Melyik előrendelés-tételt teljesítette. */
|
||||
PreOrderItemId int NOT NULL,
|
||||
|
||||
/* Melyik szállítólevélből. Ez az, amit ma csak a megjegyzés szövege őriz. */
|
||||
ShippingDocumentId int NOT NULL,
|
||||
|
||||
/* A rendelés-tétel, ami ebben a futásban keletkezett vagy bővült.
|
||||
Az árrés innen jut el a fogyasztótól a forrás-dokumentumig; NULL, ha a
|
||||
konverzió nem hozott létre rendelés-tételt (elvben nem fordul elő). */
|
||||
OrderItemId int NULL,
|
||||
|
||||
/* Az EBBEN a futásban teljesített mennyiség (delta), nem a kumulált
|
||||
FulfilledQuantity — különben a többszöri feltöltés duplán számolna. */
|
||||
Quantity int NOT NULL,
|
||||
|
||||
Created datetime2(7) NOT NULL CONSTRAINT DF_fbPreOrderFulfilment_Created DEFAULT (SYSUTCDATETIME()),
|
||||
|
||||
CONSTRAINT PK_fbPreOrderFulfilment PRIMARY KEY CLUSTERED (Id),
|
||||
CONSTRAINT CK_fbPreOrderFulfilment_Quantity CHECK (Quantity > 0),
|
||||
|
||||
/* A teljesítés ténye a tétellel együtt szűnik meg. */
|
||||
CONSTRAINT FK_fbPreOrderFulfilment_PreOrderItem FOREIGN KEY (PreOrderItemId)
|
||||
REFERENCES dbo.fbPreOrderItem (Id) ON DELETE CASCADE
|
||||
|
||||
/* A ShippingDocumentId-ra SZÁNDÉKOSAN NINCS FK: éppen az a dolga, hogy a
|
||||
dokumentum törlését MEGAKADÁLYOZZA az alkalmazás-oldali guard. Egy
|
||||
kaszkád csendben elvinné a bizonyítékot, egy NO ACTION FK pedig
|
||||
adatbázis-hibaként csapna le a beszédes üzenet helyett. */
|
||||
);
|
||||
|
||||
/* A törlés-guard kérdése: hivatkozza-e bármi ezt a dokumentumot. */
|
||||
CREATE INDEX IX_fbPreOrderFulfilment_ShippingDocumentId ON dbo.fbPreOrderFulfilment (ShippingDocumentId);
|
||||
|
||||
/* Az árrés kérdése: egy rendelés-tétel melyik dokumentumból jött. */
|
||||
CREATE INDEX IX_fbPreOrderFulfilment_OrderItemId ON dbo.fbPreOrderFulfilment (OrderItemId) WHERE OrderItemId IS NOT NULL;
|
||||
|
||||
CREATE INDEX IX_fbPreOrderFulfilment_PreOrderItemId ON dbo.fbPreOrderFulfilment (PreOrderItemId);
|
||||
END
|
||||
GO
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Ellenőrzés
|
||||
----------------------------------------------------------------------------- */
|
||||
SELECT 'fbPreOrderFulfilment' AS Objektum,
|
||||
CASE WHEN OBJECT_ID('dbo.fbPreOrderFulfilment','U') IS NULL THEN 'HIÁNYZIK' ELSE 'OK' END AS Allapot;
|
||||
GO
|
||||
@@ -71,6 +71,23 @@ Header status is **derived from item states** by `PreOrderDbContext.RefreshPreOr
|
||||
|
||||
`SweepExpiredPreordersAsync` runs at the start of every conversion run: for preorders past `DateOfReceipt` still holding `Pending`/`PartiallyFulfilled` items, any still-`Pending` item becomes `Dropped` and the header status is refreshed. Already-fulfilled quantities stay (they are already on a real order).
|
||||
|
||||
## Teljesítés-nyilvántartás (`fbPreOrderFulfilment`)
|
||||
|
||||
A konverzió minden futásban rögzíti, hogy melyik **szállítólevélből** mennyit teljesített egy előrendelés-tételre,
|
||||
és melyik rendelés-tétel keletkezett belőle. Egy sor = egy futás, a mennyiség az **adott futás deltája** — egy
|
||||
tételt több dokumentum is feltölthet, ezért ez nem lehet oszlop a `PreOrderItem`-en. Séma:
|
||||
[`PREORDER_fulfilment_schema.sql`](PREORDER_fulfilment_schema.sql).
|
||||
|
||||
Két fogyasztója van:
|
||||
|
||||
- **Szállítólevél törlése** — a `ShippingCleanupService` blokkol, ha a dokumentumból származik teljesítés: a
|
||||
konverzió szétosztotta a beérkezett árut, és a törlés ezt nem fordítaná vissza.
|
||||
- **Árrés** ([`MARGIN/README.md`](../MARGIN/README.md)) — a konvertált rendelés-tételnél a beszerzési forrás
|
||||
ebből egzakt, nem FIFO-becslés.
|
||||
|
||||
Korábban ez az adat csak a rendelés-megjegyzés **szövegében** szerepelt, tehát megbízhatóan nem volt olvasható.
|
||||
A bevezetés előtti konverziókról nincs sor.
|
||||
|
||||
## Pricing
|
||||
|
||||
| Product | Order-item price on conversion |
|
||||
|
||||
Reference in New Issue
Block a user