feat: update SoldLines method to use InvoiceItem and improve sales calculations

This commit is contained in:
2026-10-22 10:40:09 +05:30
parent 2be2928ad1
commit e84ce29064
@@ -60,24 +60,24 @@ public class ProductReportRepository(AppDBContext db) : IProductReportRepository
return (rows, total); return (rows, total);
} }
// Sold = shop PO lines on non-cancelled POs whose invoice is Finalized, dated by Invoice.FinalizedAt. // Sold = billable (non-removed) invoice lines of Finalized invoices on non-cancelled POs, dated by Invoice.FinalizedAt.
private IQueryable<ShopPOItem> SoldLines(ProductReportFilter f) private IQueryable<InvoiceItem> SoldLines(ProductReportFilter f)
{ {
var lines = var lines =
from li in db.ShopPOItems.AsNoTracking() from li in db.InvoiceItems.AsNoTracking()
join inv in db.Invoices.AsNoTracking() on li.FK_ShopPOId equals inv.FK_ShopPOId where li.IsActive && !li.IsRemoved
where inv.Status == InvoiceStatus.Finalized && li.Invoice.Status == InvoiceStatus.Finalized
&& li.ShopPO.Status != ShopPOStatus.Cancelled && li.Invoice.ShopPO.Status != ShopPOStatus.Cancelled
&& (!f.From.HasValue || inv.FinalizedAt >= f.From.Value) && (!f.From.HasValue || li.Invoice.FinalizedAt >= f.From.Value)
&& (!f.To.HasValue || inv.FinalizedAt < f.To.Value.Date.AddDays(1)) && (!f.To.HasValue || li.Invoice.FinalizedAt < f.To.Value.Date.AddDays(1))
select li; select li;
if (f.BrandId.HasValue) lines = lines.Where(l => l.Item.FK_BrandId == f.BrandId.Value); if (f.BrandId.HasValue) lines = lines.Where(l => l.Item.FK_BrandId == f.BrandId.Value);
if (f.CategoryId.HasValue) lines = lines.Where(l => l.Item.FK_CategoryId == f.CategoryId.Value); if (f.CategoryId.HasValue) lines = lines.Where(l => l.Item.FK_CategoryId == f.CategoryId.Value);
if (f.SubCategoryId.HasValue) lines = lines.Where(l => l.Item.FK_SubCategoryId == f.SubCategoryId.Value); if (f.SubCategoryId.HasValue) lines = lines.Where(l => l.Item.FK_SubCategoryId == f.SubCategoryId.Value);
if (f.StoreId.HasValue) lines = lines.Where(l => l.ShopPO.FK_StoreId == f.StoreId.Value); if (f.StoreId.HasValue) lines = lines.Where(l => l.Invoice.ShopPO.FK_StoreId == f.StoreId.Value);
if (f.RouteId.HasValue) lines = lines.Where(l => l.ShopPO.Store.FK_RouteId == f.RouteId.Value); if (f.RouteId.HasValue) lines = lines.Where(l => l.Invoice.ShopPO.Store.FK_RouteId == f.RouteId.Value);
if (f.RegionId.HasValue) lines = lines.Where(l => l.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value); if (f.RegionId.HasValue) lines = lines.Where(l => l.Invoice.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value);
if (!string.IsNullOrWhiteSpace(f.Search)) if (!string.IsNullOrWhiteSpace(f.Search))
{ {
var s = f.Search.Trim(); var s = f.Search.Trim();
@@ -93,33 +93,36 @@ public class ProductReportRepository(AppDBContext db) : IProductReportRepository
.Select(g => new .Select(g => new
{ {
g.Key.FK_ItemId, g.Key.Sku, g.Key.Name, g.Key.CostPrice, g.Key.FK_ItemId, g.Key.Sku, g.Key.Name, g.Key.CostPrice,
Orders = g.Select(x => x.FK_ShopPOId).Distinct().Count(), QtyOrdered = g.Sum(x => (decimal)(x.ShopPOItem != null ? x.ShopPOItem.Quantity : x.Quantity)),
QtyOrdered = g.Sum(x => (decimal)x.Quantity), QtyFulfilled = g.Sum(x => (decimal)x.Quantity),
QtyFulfilled = g.Sum(x => (decimal)x.QuantityFulfilled), SalesValue = g.Sum(x => x.Quantity * x.UnitPrice),
SalesValue = g.Sum(x => x.QuantityFulfilled * x.UnitPrice),
}); });
var total = await query.CountAsync(); var total = await query.CountAsync();
var page_ = await query.OrderByDescending(r => r.SalesValue).ThenBy(r => r.Name) var page_ = await query.OrderByDescending(r => r.SalesValue).ThenBy(r => r.Name)
.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
// Distinct stores per item is counted separately: MySQL can't run the correlated // Distinct orders and stores per item are counted separately: MySQL can't run the correlated
// "SELECT DISTINCT ... WHERE outer.col" derived table EF generates inside a GroupBy. // "SELECT DISTINCT ... WHERE outer.col" derived table EF generates inside a GroupBy.
var ids = page_.Select(r => r.FK_ItemId).ToList(); var ids = page_.Select(r => r.FK_ItemId).ToList();
var storePairs = await SoldLines(f) var pairs = await SoldLines(f)
.Where(l => ids.Contains(l.FK_ItemId)) .Where(l => ids.Contains(l.FK_ItemId))
.Select(l => new { l.FK_ItemId, l.ShopPO.FK_StoreId }) .Select(l => new { l.FK_ItemId, PoId = l.Invoice.FK_ShopPOId, l.Invoice.ShopPO.FK_StoreId })
.Distinct() .Distinct()
.ToListAsync(); .ToListAsync();
var storeCounts = storePairs.GroupBy(p => p.FK_ItemId).ToDictionary(g => g.Key, g => g.Count()); var orderCounts = pairs.GroupBy(p => p.FK_ItemId)
.ToDictionary(g => g.Key, g => g.Select(p => p.PoId).Distinct().Count());
var storeCounts = pairs.GroupBy(p => p.FK_ItemId)
.ToDictionary(g => g.Key, g => g.Select(p => p.FK_StoreId).Distinct().Count());
var rows = page_.Select(r => new SalesAggregate(r.FK_ItemId, r.Sku, r.Name, r.CostPrice, var rows = page_.Select(r => new SalesAggregate(r.FK_ItemId, r.Sku, r.Name, r.CostPrice,
r.Orders, storeCounts.GetValueOrDefault(r.FK_ItemId), r.QtyOrdered, r.QtyFulfilled, r.SalesValue)).ToList(); orderCounts.GetValueOrDefault(r.FK_ItemId), storeCounts.GetValueOrDefault(r.FK_ItemId),
r.QtyOrdered, r.QtyFulfilled, r.SalesValue)).ToList();
return (rows, total); return (rows, total);
} }
public Task<decimal> GetTotalSalesAsync(ProductReportFilter f) => public Task<decimal> GetTotalSalesAsync(ProductReportFilter f) =>
SoldLines(f).SumAsync(x => x.QuantityFulfilled * x.UnitPrice); SoldLines(f).SumAsync(x => x.Quantity * x.UnitPrice);
public async Task<Dictionary<Guid, decimal>> GetSoldQtyAsync(ProductReportFilter f, IEnumerable<Guid> itemIds) public async Task<Dictionary<Guid, decimal>> GetSoldQtyAsync(ProductReportFilter f, IEnumerable<Guid> itemIds)
{ {
@@ -127,7 +130,7 @@ public class ProductReportRepository(AppDBContext db) : IProductReportRepository
return await SoldLines(f with { Search = null }) return await SoldLines(f with { Search = null })
.Where(l => ids.Contains(l.FK_ItemId)) .Where(l => ids.Contains(l.FK_ItemId))
.GroupBy(l => l.FK_ItemId) .GroupBy(l => l.FK_ItemId)
.Select(g => new { g.Key, Qty = g.Sum(x => (decimal)x.QuantityFulfilled) }) .Select(g => new { g.Key, Qty = g.Sum(x => (decimal)x.Quantity) })
.ToDictionaryAsync(x => x.Key, x => x.Qty); .ToDictionaryAsync(x => x.Key, x => x.Qty);
} }