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:
@@ -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 1–12. 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user