diff --git a/Distribution/Features/ProductReport/ProductReportRepository.cs b/Distribution/Features/ProductReport/ProductReportRepository.cs index a1a75e6..c4a79da 100644 --- a/Distribution/Features/ProductReport/ProductReportRepository.cs +++ b/Distribution/Features/ProductReport/ProductReportRepository.cs @@ -60,24 +60,24 @@ public class ProductReportRepository(AppDBContext db) : IProductReportRepository return (rows, total); } - // Sold = shop PO lines on non-cancelled POs whose invoice is Finalized, dated by Invoice.FinalizedAt. - private IQueryable SoldLines(ProductReportFilter f) + // Sold = billable (non-removed) invoice lines of Finalized invoices on non-cancelled POs, dated by Invoice.FinalizedAt. + private IQueryable SoldLines(ProductReportFilter f) { var lines = - from li in db.ShopPOItems.AsNoTracking() - join inv in db.Invoices.AsNoTracking() on li.FK_ShopPOId equals inv.FK_ShopPOId - where inv.Status == InvoiceStatus.Finalized - && li.ShopPO.Status != ShopPOStatus.Cancelled - && (!f.From.HasValue || inv.FinalizedAt >= f.From.Value) - && (!f.To.HasValue || inv.FinalizedAt < f.To.Value.Date.AddDays(1)) + from li in db.InvoiceItems.AsNoTracking() + where li.IsActive && !li.IsRemoved + && li.Invoice.Status == InvoiceStatus.Finalized + && li.Invoice.ShopPO.Status != ShopPOStatus.Cancelled + && (!f.From.HasValue || li.Invoice.FinalizedAt >= f.From.Value) + && (!f.To.HasValue || li.Invoice.FinalizedAt < f.To.Value.Date.AddDays(1)) select li; 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.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.RouteId.HasValue) lines = lines.Where(l => l.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.StoreId.HasValue) lines = lines.Where(l => l.Invoice.ShopPO.FK_StoreId == f.StoreId.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.Invoice.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value); if (!string.IsNullOrWhiteSpace(f.Search)) { var s = f.Search.Trim(); @@ -93,33 +93,36 @@ public class ProductReportRepository(AppDBContext db) : IProductReportRepository .Select(g => new { 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.Quantity), - QtyFulfilled = g.Sum(x => (decimal)x.QuantityFulfilled), - SalesValue = g.Sum(x => x.QuantityFulfilled * x.UnitPrice), + QtyOrdered = g.Sum(x => (decimal)(x.ShopPOItem != null ? x.ShopPOItem.Quantity : x.Quantity)), + QtyFulfilled = g.Sum(x => (decimal)x.Quantity), + SalesValue = g.Sum(x => x.Quantity * x.UnitPrice), }); var total = await query.CountAsync(); var page_ = await query.OrderByDescending(r => r.SalesValue).ThenBy(r => r.Name) .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. 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)) - .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() .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, - 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); } public Task GetTotalSalesAsync(ProductReportFilter f) => - SoldLines(f).SumAsync(x => x.QuantityFulfilled * x.UnitPrice); + SoldLines(f).SumAsync(x => x.Quantity * x.UnitPrice); public async Task> GetSoldQtyAsync(ProductReportFilter f, IEnumerable itemIds) { @@ -127,7 +130,7 @@ public class ProductReportRepository(AppDBContext db) : IProductReportRepository return await SoldLines(f with { Search = null }) .Where(l => ids.Contains(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); }