Az ÁFA-kimutatás kimenő oldala a lezárt rendelésekből számol

A fogyasztás-sorok csak az árrés-funkció élesítése óta léteznek, ezért a riport
augusztusra 12 napot mutatott 31 helyett: 126,3 M-ot a 309,7 M helyett. A forrás
mostantól a rendeléslista, a hónap kulcsa az átvétel időpontja.

ADR 0004-K9T3, horgony: MGFBANKPLUG-INNV-T-R6D2 (7-ből 6 lépés kész).

- populáció: minden lezárt, nem törölt rendelés; a be nem küldött is, számlálóval
- a hónap a DateOfReceipt, invariáns kultúrával parse-olva; fallback nincs, a
  datálatlan rendelés anomália-sorra kerül
- három anomália-számláló a felületen, a bejövő oldal mintájára
- a DataAvailableFrom padló megszűnt: a rendeléslista odáig megy vissza, ameddig
  az adat
- az InnVoice attribútum-kulcsok konstansok lettek, mind a nyolc hívási helyen

Hosszú id-lista nem megy át az SQL-nek: a NOT IN a teljes rendelés-történettel
időtúllépésbe futott. A halmaz-műveletek memóriában futnak, a tétel-lekérdezés
1000-es darabokban megy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 11:15:14 +02:00
co-authored by Claude Opus 5
parent 9f1ef5db04
commit 4b2e6f9d34
13 changed files with 174 additions and 110 deletions
@@ -277,7 +277,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// ─────────────────────────────────────────────────────────────────
var todayRows = todaysOrders.Take(20).Select(o =>
{
var hasInnVoice = o.GenericAttributes.Any(a => a.Key == "InnVoiceOrderTableId");
var hasInnVoice = o.GenericAttributes.Any(a => a.Key == FruitBankPluginConst.InnVoiceOrderTableIdAttribute);
return new
{
id = o.Id,
@@ -299,7 +299,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
audited = todaysOrders.Count(o => o.IsAllOrderItemAudited),
missingInnVoice = todaysOrders.Count(o =>
o.IsAllOrderItemAudited &&
!o.GenericAttributes.Any(a => a.Key == "InnVoiceOrderTableId")),
!o.GenericAttributes.Any(a => a.Key == FruitBankPluginConst.InnVoiceOrderTableIdAttribute)),
completed = todaysOrders.Count(o => o.OrderStatus == OrderStatus.Complete),
rows = todayRows
};
@@ -313,7 +313,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
foreach (var o in todaysOrders.Where(o =>
o.IsAllOrderItemAudited &&
o.OrderStatus != OrderStatus.Complete &&
!o.GenericAttributes.Any(a => a.Key == "InnVoiceOrderTableId")))
!o.GenericAttributes.Any(a => a.Key == FruitBankPluginConst.InnVoiceOrderTableIdAttribute)))
{
alerts.Add(new
{
@@ -2221,7 +2221,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{
var ga = o.GenericAttributes;
var dateOfReceipt = ga?.FirstOrDefault(a => a.Key == "DateOfReceipt")?.Value is string dv && DateTime.TryParse(dv, out var dp) ? dp : (DateTime?)null;
var innvoiceTechId = ga?.FirstOrDefault(a => a.Key == "InnVoiceOrderTechId")?.Value;
var innvoiceTechId = ga?.FirstOrDefault(a => a.Key == FruitBankPluginConst.InnVoiceOrderTechIdAttribute)?.Value;
var company = o.Customer != null
? $"{o.Customer.Company} {o.Customer.FirstName}_{o.Customer.LastName}".Trim()
: string.Empty;
@@ -213,27 +213,27 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// Save InnVoice order details as attributes
//await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync<Order, string>(
// order.Id,
// "InnVoiceOrderTechId",
// FruitBankPluginConst.InnVoiceOrderTechIdAttribute,
// response.TechId,
// storeId
//);
await _genericAttributeService.SaveAttributeAsync(
order,
"InnVoiceOrderTechId",
FruitBankPluginConst.InnVoiceOrderTechIdAttribute,
response.TechId,
storeId
);
await _genericAttributeService.SaveAttributeAsync(
order,
"InnVoiceOrderTableId",
FruitBankPluginConst.InnVoiceOrderTableIdAttribute,
response.TableId?.ToString() ?? string.Empty,
storeId
);
await _genericAttributeService.SaveAttributeAsync(
order,
"InnVoiceOrderPrintLink",
FruitBankPluginConst.InnVoiceOrderPrintLinkAttribute,
response.PrintUrl?.ToString() ?? string.Empty,
storeId
);
@@ -290,13 +290,13 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// Get saved InnVoice order TechId
var techId = await _genericAttributeService.GetAttributeAsync<string>(
order,
"InnVoiceOrderTechId",
FruitBankPluginConst.InnVoiceOrderTechIdAttribute,
storeId
);
var printLink = await _genericAttributeService.GetAttributeAsync<string>(
order,
"InnVoiceOrderPrintLink",
FruitBankPluginConst.InnVoiceOrderPrintLinkAttribute,
storeId
);
@@ -438,14 +438,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{
await _genericAttributeService.SaveAttributeAsync(
order,
"InnVoiceOrderTechId",
FruitBankPluginConst.InnVoiceOrderTechIdAttribute,
responses[i].TechId,
storeId
);
await _genericAttributeService.SaveAttributeAsync(
order,
"InnVoiceOrderTableId",
FruitBankPluginConst.InnVoiceOrderTableIdAttribute,
responses[i].TableId?.ToString(),
storeId
);
@@ -78,7 +78,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{
var tableIdStr = await _genericAttributeService.GetAttributeAsync<string>(
order,
"InnVoiceOrderTableId",
FruitBankPluginConst.InnVoiceOrderTableIdAttribute,
storeId
);
@@ -104,17 +104,13 @@ public class VatReportController(
var requestedFrom = (from ?? new DateTime(today.Year, today.Month, 1)).Date;
var toExclusive = to?.Date ?? new DateTime(today.Year, today.Month, 1).AddMonths(1);
// Az adathatár alá nem megyünk: a kimenő oldal ennél korábbra nem létezik, és a nulla kimenő áfa
// hiányzó adatot jelentene, nem valós nullát. Csendben felhúzzuk, de a felület megmondja, hogy tette.
var floor = VatReportService.DataAvailableFrom;
var fromInclusive = requestedFrom < floor ? floor : requestedFrom;
// Alsó adathatár nincs: mindkét oldal odáig megy vissza, ameddig az adat. A korábbi korlát a
// fogyasztás-sorok kezdetét takarta, az pedig már nem forrása a kimenő oldalnak (ADR 0004-K9T3).
var fromInclusive = requestedFrom;
if (toExclusive <= fromInclusive) toExclusive = fromInclusive.AddMonths(1);
var filter = new VatReportInvoiceFilter { Text = text, Currency = currency, OnlyWithVat = onlyWithVat };
var model = await vatReportService.CalculateAsync(fromInclusive, toExclusive, filter);
model.FromClamped = requestedFrom < floor;
return model;
return await vatReportService.CalculateAsync(fromInclusive, toExclusive, filter);
}
}
@@ -52,10 +52,10 @@
<form method="get" class="form-inline">
<label class="mr-2">Ettől:</label>
<input type="date" name="from" value="@Model.FromInclusive.ToString("yyyy-MM-dd")"
min="@Model.DataAvailableFrom.ToString("yyyy-MM-dd")" class="form-control mr-3" />
class="form-control mr-3" />
<label class="mr-2">Eddig:</label>
<input type="date" name="to" value="@Model.ToExclusive.ToString("yyyy-MM-dd")"
min="@Model.DataAvailableFrom.ToString("yyyy-MM-dd")" class="form-control mr-3" />
class="form-control mr-3" />
<label class="mr-2">Szállító / bizonylat:</label>
<input type="text" name="text" value="@Model.Filter.Text" placeholder="névrészlet vagy szám"
class="form-control mr-3" style="min-width:220px;" />
@@ -81,26 +81,19 @@
}
</form>
<p class="mb-0 mt-2 text-muted small">
A kimutatás <strong>@Model.DataAvailableFrom.ToString("yyyy.MM.dd.")</strong> előttre nem kérdezhető le.
A bejövő számlák ugyan korábbra is megvannak a tükörben, de az <strong>eladási oldal</strong> csak
ettől a naptól áll össze — korábbra a nulla kimenő áfa hiányzó adatot jelentene, nem valós nullát.
A <strong>bejövő</strong> oldal hónapja a számla teljesítés-kelte, a <strong>kimenő</strong> oldalé
az <strong>átvétel időpontja</strong> a rendelésen. Az árrés-kimutatás ettől eltérően a lezárás
hónapja szerint csoportosít — a két riport szándékosan más naptárt követ.
</p>
</div>
</div>
@* ── Figyelmeztetések ───────────────────────────────────────── *@
@if (Model.FromClamped)
{
<div class="alert alert-info">
A kért kezdő dátum korábbi volt az adathatárnál, ezért
<strong>@Model.DataAvailableFrom.ToString("yyyy.MM.dd.")</strong>-tól számoltunk.
</div>
}
@if (Model.WithoutPerformanceDate > 0 || Model.ImplausiblePerformanceDate > 0)
@if (Model.WithoutPerformanceDate > 0 || Model.ImplausiblePerformanceDate > 0
|| Model.WithoutPickupDate > 0 || Model.UnparsablePickupDate > 0)
{
<div class="alert alert-warning">
<strong>Egyik hónapba sem esik bele</strong> (a teljes tükörben, nem csak ebben az időszakban):
<strong>Egyik hónapba sem esik bele</strong> (a teljes adatállományban, nem csak ebben az időszakban):
@if (Model.WithoutPerformanceDate > 0)
{
<text>@Model.WithoutPerformanceDate számla teljesítés-dátum nélkül. </text>
@@ -109,8 +102,16 @@
{
<text>@Model.ImplausiblePerformanceDate számla értelmetlen teljesítés-dátummal (elgépelt évszám). </text>
}
Ezek a sorok <strong>egyetlen hónap összegében sem szerepelnek</strong> — az InnVoice-ban kell javítani a dátumot,
utána egy szinkron átveszi.
@if (Model.WithoutPickupDate > 0)
{
<text><strong>@Model.WithoutPickupDate lezárt rendelés átvételi időpont nélkül.</strong> </text>
}
@if (Model.UnparsablePickupDate > 0)
{
<text>@Model.UnparsablePickupDate rendelés értelmezhetetlen átvételi időponttal. </text>
}
Ezek a sorok <strong>egyetlen hónap összegében sem szerepelnek</strong>. A számla dátumát az InnVoice-ban
kell javítani (utána egy szinkron átveszi), az átvételi időpontot a rendelés lapján.
</div>
}
@@ -237,7 +238,7 @@
<strong class="@(row.Balance >= 0 ? "text-danger" : "text-success")">@Huf(row.Balance)</strong>
</td>
<td class="small text-muted">
@row.IncomingInvoiceCount forintos bejövő számla · @row.OutgoingOrderItemCount eladott tétel
@row.IncomingInvoiceCount forintos bejövő számla · @row.OutgoingOrderCount lezárt rendelés
@foreach (var currency in foreign)
{
<br />
@@ -252,14 +253,13 @@
{
<br /><span>@row.SetAsideCount félretett számla (benne van az összegben)</span>
}
@if (row.MissingOrderItemCount > 0)
@* A be nem küldött rendelés BENNE van az összegben — a számláló azt mondja
meg, mennyivel állhat a kimutatás a könyvelés kiszámlázott száma FÖLÖTT. *@
@if (row.NotSubmittedCount > 0)
{
<br /><span class="text-danger">@row.MissingOrderItemCount eladott tétel nem található — a kimenő ÁFA ennyivel kevesebb</span>
<br /><span class="text-danger">@row.NotSubmittedCount lezárt rendelés nem ment ki az InnVoice-ba — benne van az összegben</span>
}
@* A kimenő oldal a fogyasztás-sorokra épül, azok pedig csak az árrés-funkció
élesítése óta keletkeznek. Egy korábbi hónapban a bejövő valós, a kimenő
nulla — az egyenleg ilyenkor nem hiányzó áfa, hanem hiányzó ADAT. *@
@if (row.IncomingInvoiceCount > 0 && row.OutgoingOrderItemCount == 0)
@if (row.IncomingInvoiceCount > 0 && row.OutgoingOrderCount == 0)
{
<br /><span class="text-danger"><strong>Nincs eladási adat ebben a hónapban</strong> — az egyenleg félrevezető</span>
}
@@ -191,7 +191,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
var genericAttributes = orderDto.GenericAttributes;
if(genericAttributes != null)
{
var innvoiceTechIdAttribute = genericAttributes.FirstOrDefault(ga => ga.Key == "InnVoiceOrderTechId");
var innvoiceTechIdAttribute = genericAttributes.FirstOrDefault(ga => ga.Key == FruitBankPluginConst.InnVoiceOrderTechIdAttribute);
if (innvoiceTechIdAttribute != null)
{
orderModelExtended.InnvoiceTechId = innvoiceTechIdAttribute.Value;
@@ -57,6 +57,19 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
/// <summary>License plate (rendszám) of the vehicle picking up the order. Raw; the NAV mapper normalizes.</summary>
public const string OrderLicensePlateAttribute = "LicensePlate";
// ── InnVoice submission markers, written when an order is submitted ──────
// These say the order was SUBMITTED, never that an invoice exists: the outbound direction submits an
// order, not an invoice. A reader that treats the marker as proof of invoicing states more than the
// data holds.
/// <summary>InnVoice technical id of the submitted order.</summary>
public const string InnVoiceOrderTechIdAttribute = "InnVoiceOrderTechId";
/// <summary>InnVoice table id of the submitted order — the marker a report tests for.</summary>
public const string InnVoiceOrderTableIdAttribute = "InnVoiceOrderTableId";
/// <summary>Print (PDF) link InnVoice returned for the submitted order.</summary>
public const string InnVoiceOrderPrintLinkAttribute = "InnVoiceOrderPrintLink";
// ── Plugin-owned table names (NOT shared with the Hybrid client) ─────────
// The shared entities' table names live in FruitBankConstClient (FruitBank.Common), because those
// entities go over SignalR. Cost and margin data is admin-only (ADR 0002-R7Q2), so both the entity
@@ -154,7 +154,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
Total: order.OrderTotal,
Audited: audited,
WeightOk: audited ? order.IsValidMeasuringValues() : null,
InvoiceCreated: audited ? order.GenericAttributes.Any(a => a.Key == "InnVoiceOrderTableId") : null,
InvoiceCreated: audited ? order.GenericAttributes.Any(a => a.Key == FruitBankPluginConst.InnVoiceOrderTableIdAttribute) : null,
Completed: audited ? order.OrderStatus == OrderStatus.Complete : null);
}).ToList();
@@ -1,4 +1,8 @@
using LinqToDB;
using System.Globalization;
using FruitBank.Common.Dtos;
using FruitBank.Common.Interfaces;
using LinqToDB;
using Nop.Core.Domain.Orders;
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
using Nop.Plugin.Misc.FruitBankPlugin.Domains.Entities;
@@ -15,6 +19,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
/// <para><b>A bejövő oldal a tükör.</b> A szállítmány-költségek (<c>fbShippingCost</c>) és a beszállítói
/// szállítólevelek NEM számítanak bele: azok is az InnVoice-on át jönnek, és kétszer venni őket
/// duplázás lenne.</para>
/// <para><b>A kimenő oldal hónapja az ÁTVÉTEL</b> (<c>OrderDto.DateOfReceipt</c>), nem a rendelés kelte és
/// nem a lezárás pillanata — az árrés-kimutatás ezért szándékosan más naptárt követ (ADR 0004-K9T3).</para>
/// <para><b>A kimenő oldal a bruttóból számol</b>, egyetlen kulccsal
/// (<see cref="FruitBankPluginConst.OutgoingVatRate"/>) — ugyanazzal, amivel a számla kimegy. A rendelés-tétel
/// nettó oszlopa erre nem alkalmas, az ott áll, hogy miért.</para>
@@ -23,29 +29,20 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
public class VatReportService(
IncomingInvoiceDbTable incomingInvoices,
IncomingInvoiceItemDbTable incomingInvoiceItems,
CostConsumptionDbTable costConsumptions,
FruitBankDbContext dbContext)
{
/// <summary>Ennél korábbi vagy ennél későbbi teljesítés-dátum adathiba, nem valós hónap.</summary>
private static readonly DateTime PlausibleFrom = new(2015, 1, 1);
/// <summary>
/// A kimutatás alsó határa: ennél korábbi időszak nem kérdezhető le.
/// </summary>
/// <remarks>
/// A bejövő oldal ugyan öt évre visszamenőleg megvan a tükörben, a KIMENŐ oldal viszont nem: az a
/// fogyasztás-sorokra épül, azok pedig csak az árrés-funkció élesítése óta keletkeznek. Egy korábbi
/// hónapra a riport valós bejövő áfát és nulla kimenőt mutatna — az egyenleg nem hiányzó áfát jelezne,
/// hanem hiányzó adatot. Ezt nem magyarázzuk el minden sorban, hanem el sem engedjük odáig.
/// </remarks>
public static readonly DateTime DataAvailableFrom = new(2026, 8, 1);
/// <summary>A HUF megnevezései az InnVoice-ban. A tükör azt tárolja, amit a forrás ad ("Ft").</summary>
private static readonly HashSet<string> HufCodes = new(StringComparer.OrdinalIgnoreCase) { "Ft", "HUF" };
/// <summary>A tételes lista felső határa — egy éves időszak több ezer sort adna.</summary>
private const int InvoiceListLimit = 500;
/// <summary>Ennyi id mehet egy IN-listába; a hosszabb lista tervezési és paraméter-korlátba ütközik.</summary>
private const int IdChunkSize = 1000;
public async Task<VatReportModel> CalculateAsync(DateTime fromInclusive, DateTime toExclusive,
VatReportInvoiceFilter? filter = null)
{
@@ -211,59 +208,115 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
private async Task AddOutgoingAsync(VatReportModel model, DateTime fromInclusive, DateTime toExclusive)
{
// A rendelés lezárásának pillanata a fogyasztás-sor Created mezője — a nop rendelés-dátumai közül
// egyik sem jelöli a lezárást (ADR 0002-R7Q2, 10. pont). Így a havi bontás egyezik az árrés-riportéval.
var consumptions = await costConsumptions.GetAllByPeriod(fromInclusive, toExclusive, false)
.Where(consumption => consumption.ConsumerTypeId == (int)CostConsumerType.OrderItem)
.Select(consumption => new { consumption.ConsumerId, consumption.Created })
// Az átvétel időpontja GenericAttribute-ban, STRINGKÉNT áll, tehát a hónap SQL-ből nem szűrhető
// (ADR 0004-K9T3, 3. és 4. pont). Ezért a kulcs összes sorát behozzuk — rendelésenként egy sor, két
// oszlop —, és a hónapot memóriában döntjük el. Dátum szerinti SQL-előszűrő NEM kerülhet ide: egy
// rossz határ némán dobna el rendeléseket, és pont az ilyen néma hiány ellen készült a kimutatás.
var pickupValues = await dbContext.GenericAttributes.Table
.Where(attribute => attribute.KeyGroup == nameof(Order)
&& attribute.Key == nameof(IOrderDto.DateOfReceipt))
.Select(attribute => new { attribute.EntityId, attribute.Value })
.ToListAsync();
if (consumptions.Count == 0) return;
// ⚠️ Rendelésenként EGY érték. Az attribútum bolt-hatókörrel íródik, tehát két bolt két sort adna
// ugyanarra a rendelésre — a rendelés kétszer esne bele a hónapba, és a kimenő áfa csendben duplázna.
var pickupByOrder = pickupValues
.GroupBy(pickup => pickup.EntityId)
.ToDictionary(group => group.Key, group => group.Last().Value);
// ⚠️ EGY rendelés-tétel TÖBB rétegből is fogyhat, tehát több fogyasztás-sora van. A tétel áfáját
// soronként összegezve annyiszor számolnánk, ahány rétegből fogyott — ezért DISZTINKT tétel-id.
var itemIdsByMonth = consumptions
.GroupBy(consumption => (consumption.Created.Year, consumption.Created.Month))
.ToDictionary(g => g.Key, g => g.Select(x => x.ConsumerId).Distinct().ToList());
var allItemIds = itemIdsByMonth.Values.SelectMany(ids => ids).Distinct().ToList();
var orderItems = (await dbContext.OrderItemDtos.GetAll()
.Where(item => allItemIds.Contains(item.Id))
.Select(item => new { item.Id, item.PriceInclTax })
// ⚠️ Az adatbázisnak SOSEM adunk át hosszú id-listát szűrőnek. A rendelés-történet minden datált sora
// egy-egy paraméter lenne egy NOT IN-ben, és az a lekérdezés időtúllépésbe fut (mérve: 2026-09-01).
// Az adatbázistól oszlopokat kérünk, a halmaz-műveletek memóriában futnak.
var completedOrderIds = (await CompletedOrders()
.Select(order => order.Id)
.ToListAsync())
.ToDictionary(item => item.Id);
.ToHashSet();
foreach (var (period, itemIds) in itemIdsByMonth)
var orderIdsByMonth = new Dictionary<(int Year, int Month), List<int>>();
foreach (var (orderId, pickupValue) in pickupByOrder)
{
if (!completedOrderIds.Contains(orderId)) continue;
// ⚠️ INVARIÁNS kultúra. A mező "MM/dd/yyyy HH:mm:ss" alakban íródik; magyar locale-on a
// 08/05/2026 május 8-nak olvasódna, és a hiba a hónap 112. napjain néma.
if (!DateTime.TryParse(pickupValue, CultureInfo.InvariantCulture,
DateTimeStyles.None, out var pickedUpAt))
{
model.UnparsablePickupDate++;
continue;
}
if (pickedUpAt < fromInclusive || pickedUpAt >= toExclusive) continue;
var period = (pickedUpAt.Year, pickedUpAt.Month);
if (!orderIdsByMonth.TryGetValue(period, out var ids))
orderIdsByMonth[period] = ids = [];
ids.Add(orderId);
}
// Átvételi időpont nélküli lezárt rendelés egyetlen hónapba sem esik bele — ugyanaz a hiány, amit a
// bejövő oldalon a teljesítés-dátum nélküli számla jelent, tehát ugyanúgy számolni kell.
model.WithoutPickupDate = completedOrderIds.Count(orderId => !pickupByOrder.ContainsKey(orderId));
if (orderIdsByMonth.Count == 0) return;
// Egy hónap ~1200 rendelés. Ennyi id már átadható szűrőnek, de DARABOLVA: a lekérdezés-terv és a
// paraméter-korlát is a lista hosszával romlik.
var grossByOrder = new Dictionary<int, decimal>();
foreach (var chunk in orderIdsByMonth.Values.SelectMany(ids => ids).Chunk(IdChunkSize))
{
var itemRows = await dbContext.OrderItemDtos.GetAll()
.Where(item => chunk.Contains(item.OrderId))
.Select(item => new { item.OrderId, item.PriceInclTax })
.ToListAsync();
foreach (var itemRow in itemRows)
grossByOrder[itemRow.OrderId] =
grossByOrder.GetValueOrDefault(itemRow.OrderId) + itemRow.PriceInclTax;
}
// Eladás = lezárt rendelés. A be nem küldött rendelés BENNE marad az összegben és külön számlálót
// kap: a hiányzó számla üzleti kivétel, nem szűrési szempont (ADR 0004-K9T3, 1. pont). A jelölő is
// szűrő-lista nélkül jön, egy oszlopban — a metszet memóriában.
var submittedIds = (await dbContext.GenericAttributes.Table
.Where(attribute => attribute.KeyGroup == nameof(Order)
&& attribute.Key == FruitBankPluginConst.InnVoiceOrderTableIdAttribute)
.Select(attribute => attribute.EntityId)
.ToListAsync())
.ToHashSet();
foreach (var (period, orderIds) in orderIdsByMonth)
{
var row = model.RowFor(new DateTime(period.Year, period.Month, 1));
foreach (var itemId in itemIds)
foreach (var orderId in orderIds)
{
if (!orderItems.TryGetValue(itemId, out var item))
{
// A rendelés-tétel eltűnt a fogyasztás-sor alól. Nem hallgatjuk el: a kimenő áfa
// enélkül csendben kevesebb lenne.
row.MissingOrderItemCount++;
continue;
}
// ⚠️ Az áfa a BRUTTÓBÓL áll elő, soha nem a PriceInclTax PriceExclTax különbségből. A nop
// adószolgáltatása ezekre a termékekre változatlanul adja vissza az árat
// (FruitBankOrderItemService), tehát a két oszlop a tételek túlnyomó részén AZONOS, és a
// különbség nullát ad — a kimutatás így a valós áfa fél százalékát mutatta. A PriceInclTax
// viszont megbízható: ezzel az egységárral megy ki a számla is (InnVoiceOrderController).
var grossAmount = item.PriceInclTax;
var grossAmount = grossByOrder.TryGetValue(orderId, out var gross) ? gross : 0m;
var netAmount = decimal.Round(
grossAmount / (1m + FruitBankPluginConst.OutgoingVatRate), 2);
row.OutgoingOrderItemCount++;
row.OutgoingOrderCount++;
row.OutgoingNetAmount += netAmount;
row.OutgoingVatAmount += grossAmount - netAmount;
if (!submittedIds.Contains(orderId)) row.NotSubmittedCount++;
}
}
}
/// <summary>A lezárt, nem törölt rendelések — az eladási oldal populációja.</summary>
private IQueryable<OrderDto> CompletedOrders()
=> dbContext.OrderDtos.GetAllByOrderStatus(OrderStatus.Complete, false)
.Where(order => !order.Deleted);
// ── Ami egyik hónapba sem esik bele ──────────────────────────────────────
private async Task AddAnomaliesAsync(VatReportModel model)
@@ -301,15 +354,9 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
public DateTime FromInclusive { get; set; }
public DateTime ToExclusive { get; set; }
/// <summary>A lekérdezhető legkorábbi nap — a felület ezt teszi a dátummezők alsó határának.</summary>
public DateTime DataAvailableFrom => VatReportService.DataAvailableFrom;
/// <summary>A kimenő oldal áfakulcsa — a felület kiírja, mert a számból nem derül ki.</summary>
public decimal OutgoingVatRate => FruitBankPluginConst.OutgoingVatRate;
/// <summary>A kért kezdő dátum korábbi volt az adathatárnál, ezért felhúztuk.</summary>
public bool FromClamped { get; set; }
/// <summary>Mikor frissült utoljára a tükör az InnVoice-ból; a riport nem szinkronizál magától.</summary>
public DateTime? MirrorSyncedAt { get; set; }
@@ -345,6 +392,12 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
/// <summary>Értelmetlen teljesítés-dátumú számlák (elgépelt évszám) — szintén kimaradnak minden hónapból.</summary>
public int ImplausiblePerformanceDate { get; set; }
/// <summary>Átvételi időpont nélküli lezárt rendelések — egyik hónapba sem esnek bele.</summary>
public int WithoutPickupDate { get; set; }
/// <summary>Értelmezhetetlen átvételi időpont — a tárolt szöveg nem dátum, tehát a rendelés kimarad.</summary>
public int UnparsablePickupDate { get; set; }
public VatReportRow RowFor(DateTime date)
{
var row = Rows.FirstOrDefault(r => r.Year == date.Year && r.Month == date.Month);
@@ -398,12 +451,12 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
public int SetAsideCount { get; set; }
// ── Kimenő ──
public int OutgoingOrderItemCount { get; set; }
public int OutgoingOrderCount { get; set; }
public decimal OutgoingNetAmount { get; set; }
public decimal OutgoingVatAmount { get; set; }
/// <summary>Fogyasztás-sor, aminek a rendelés-tétele nem található.</summary>
public int MissingOrderItemCount { get; set; }
/// <summary>Lezárt rendelés, ami nem ment ki az InnVoice-ba — benne van az összegben, de látszik.</summary>
public int NotSubmittedCount { get; set; }
/// <summary>Devizánkénti bontás, saját pénznemben.</summary>
public List<VatCurrencyRow> CurrencyRows { get; set; } = [];
File diff suppressed because one or more lines are too long
@@ -79,7 +79,7 @@ következő friss adatbázison ugyanide jutnánk.
## MGFBANKPLUG-INNV-T-R6D2: The VAT report's outgoing side moves from cost-consumption rows to completed orders
**Status:** Open · **Priority:** P1 · **Type:** T · **2026-08-31**
**Status:** InProgress · **Priority:** P1 · **Type:** T · **2026-08-31**
The anchor entry of [ADR 0004-K9T3](../adr/0004-K9T3-vat-report-outgoing-from-orders.md). This entry carries the
implementation status; the decision, its alternatives and its consequences stay in the record.
@@ -90,18 +90,18 @@ around 2026-08-20.
### Steps
- [ ] **Read orders, not consumption rows.** `VatReportService.AddOutgoingAsync` takes every `Complete`,
- [x] **Read orders, not consumption rows.** `VatReportService.AddOutgoingAsync` takes every `Complete`,
non-deleted order instead of `CostConsumptionDbTable.GetAllByPeriod`. The order item totals stay the
revenue source; the VAT stays derived from the gross.
- [ ] **Group by `DateOfReceipt`**, parsed with invariant culture from the `GenericAttribute` string. Not
- [x] **Group by `DateOfReceipt`**, parsed with invariant culture from the `GenericAttribute` string. Not
`DateOfReceiptOrCreated` — the fallback would hide a missing date.
- [ ] **Anomaly counters, with the numbers on the page**: completed order without a pickup date; completed order
- [x] **Anomaly counters, with the numbers on the page**: completed order without a pickup date; completed order
with no `InnVoiceOrderTableId`; pickup date that does not parse.
- [ ] **Drop `DataAvailableFrom`** and the header sentence explaining the floor; the date pickers lose their
- [x] **Drop `DataAvailableFrom`** and the header sentence explaining the floor; the date pickers lose their
lower bound.
- [ ] **The InnVoice attribute keys** (`InnVoiceOrderTechId`, `InnVoiceOrderTableId`, `InnVoiceOrderPrintLink`)
- [x] **The InnVoice attribute keys** (`InnVoiceOrderTechId`, `InnVoiceOrderTableId`, `InnVoiceOrderPrintLink`)
move to `FruitBankPluginConst` before a second reader copies the literals.
- [ ] **Verify against the month already measured**: 2026-08 must come out at 309 693 697 Ft gross, and the
unsubmitted count must explain the distance from the invoiced figure.
- [ ] **Docs** — [`INNVOICE_INCOMING.md`](INNVOICE_INCOMING.md) → *Monthly VAT report* states the new outgoing
- [x] **Docs** — [`INNVOICE_INCOMING.md`](INNVOICE_INCOMING.md) → *Monthly VAT report* states the new outgoing
rules as current fact, and the calendar difference against the margin report.
+1 -1
View File
@@ -22,7 +22,7 @@ Az implementáció állapotát ADR-enként **pontosan egy** `_TODO` / `_ISSUES`
| [0001-V4T7](0001-V4T7-product-master-data-storage.md) | A termék mérési/logisztikai törzsadata elsőrendű, plugin-tulajdonú táblába kerül a `GenericAttribute`-ok helyett | Proposed (2026-08-11) | `MGFBANKPLUG-PMDATA-T-H3N8` ([PMDATA_TODO](../PMDATA/PMDATA_TODO.md)) — Open |
| [0002-R7Q2](0002-R7Q2-document-cost-layers-and-margin.md) | A nyereség-kimutatás a szállítólevél szintjén számol, FIFO-val fogyasztott költség-rétegekből, és minden pénzügyi értéket lekérdezéssel állít elő | Proposed (2026-08-13) | `MGFBANKPLUG-MARGIN-T-K5V9` ([MARGIN_TODO](../MARGIN/MARGIN_TODO.md)) — InProgress |
| [0003-H7C4](0003-H7C4-dto-event-bridge-wiring.md) | A DTO-repók esemény-hídja bekötésre kerül a keretben, a `Nop.Services` referencia árán | Accepted (2026-08-18) | `MGFBANKPLUG-NOTIF-I-Q3M8` ([NOTIFICATION_ISSUES](../NOTIFICATION/NOTIFICATION_ISSUES.md)) — Closed |
| [0004-K9T3](0004-K9T3-vat-report-outgoing-from-orders.md) | The VAT report's outgoing side reads completed orders, keyed on the pickup date | Accepted (2026-08-31) | `MGFBANKPLUG-INNV-T-R6D2` ([INNVOICE_TODO](../INNVOICE/INNVOICE_TODO.md)) — Open |
| [0004-K9T3](0004-K9T3-vat-report-outgoing-from-orders.md) | The VAT report's outgoing side reads completed orders, keyed on the pickup date | Accepted (2026-08-31) | `MGFBANKPLUG-INNV-T-R6D2` ([INNVOICE_TODO](../INNVOICE/INNVOICE_TODO.md)) — InProgress |
## Ezt a repót érintő döntések más rétegben