feat: update SoldLines method to use InvoiceItem and improve sales calculations
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user