Add stock report features r
This commit is contained in:
@@ -7,16 +7,39 @@ namespace Distribution.Features.Stock;
|
||||
public class StockRepository(AppDBContext db) : IStockRepository
|
||||
{
|
||||
public Task<StockBalance?> GetBalanceAsync(Guid itemId, Guid locationId) =>
|
||||
db.StockBalances
|
||||
.Include(b => b.Item)
|
||||
.FirstOrDefaultAsync(b => b.FK_ItemId == itemId && b.LocationId == locationId
|
||||
&& b.FK_ItemBatchId == null && b.FK_ItemSerialNumberId == null);
|
||||
GetBalanceAsync(itemId, locationId, null, null);
|
||||
|
||||
public Task<StockBalance?> GetBalanceAsync(Guid itemId, Guid locationId, Guid? itemBatchId, Guid? itemSerialNumberId) =>
|
||||
db.StockBalances
|
||||
// Looks at rows added earlier in the same unit of work first — they aren't in the database yet,
|
||||
// so a second movement for the same (item, location, batch, serial) would otherwise insert a duplicate.
|
||||
public async Task<StockBalance?> GetBalanceAsync(Guid itemId, Guid locationId, Guid? itemBatchId, Guid? itemSerialNumberId)
|
||||
{
|
||||
var pending = db.StockBalances.Local.FirstOrDefault(b => b.FK_ItemId == itemId && b.LocationId == locationId
|
||||
&& b.FK_ItemBatchId == itemBatchId && b.FK_ItemSerialNumberId == itemSerialNumberId);
|
||||
if (pending is not null) return pending;
|
||||
|
||||
return await db.StockBalances
|
||||
.Include(b => b.Item)
|
||||
.FirstOrDefaultAsync(b => b.FK_ItemId == itemId && b.LocationId == locationId
|
||||
&& b.FK_ItemBatchId == itemBatchId && b.FK_ItemSerialNumberId == itemSerialNumberId);
|
||||
}
|
||||
|
||||
public async Task<List<StockBalance>> GetAllocatableBalancesAsync(Guid itemId, Guid locationId)
|
||||
{
|
||||
var balances = await db.StockBalances
|
||||
.Include(b => b.Item)
|
||||
.Include(b => b.ItemBatch)
|
||||
.Include(b => b.ItemSerialNumber)
|
||||
.Where(b => b.FK_ItemId == itemId && b.LocationId == locationId && b.Quantity > 0)
|
||||
.ToListAsync();
|
||||
|
||||
// Tracked stock first: earliest expiry (FEFO), then oldest received serial (FIFO); untracked rows last.
|
||||
return balances
|
||||
.OrderBy(b => b.FK_ItemBatchId == null && b.FK_ItemSerialNumberId == null ? 1 : 0)
|
||||
.ThenBy(b => b.ItemBatch?.ExpiryDate ?? DateTime.MaxValue)
|
||||
.ThenBy(b => b.ItemSerialNumber?.ReceivedAt ?? DateTime.MaxValue)
|
||||
.ThenBy(b => b.UpdatedAt)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public Task<List<StockBalance>> GetBalancesByLocationAsync(Guid locationId, LocationType locationType) =>
|
||||
db.StockBalances
|
||||
@@ -69,41 +92,88 @@ public class StockRepository(AppDBContext db) : IStockRepository
|
||||
await db.StockMovements.AddAsync(movement);
|
||||
|
||||
public Task<StockMovement?> GetMovementByIdAsync(Guid id) =>
|
||||
db.StockMovements.Include(m => m.Item).FirstOrDefaultAsync(m => m.Id == id);
|
||||
db.StockMovements.Include(m => m.Item).Include(m => m.ItemBatch).Include(m => m.ItemSerialNumber).FirstOrDefaultAsync(m => m.Id == id);
|
||||
|
||||
public void UpdateMovement(StockMovement movement) =>
|
||||
db.StockMovements.Update(movement);
|
||||
|
||||
public Task<List<StockMovement>> GetMovementsAsync(Guid? locationId, Guid? itemId, MovementType? type, DateTime? from, DateTime? to, int page, int pageSize)
|
||||
{
|
||||
var query = db.StockMovements.Include(m => m.Item).AsQueryable();
|
||||
if (locationId.HasValue)
|
||||
query = query.Where(m => m.FromLocationId == locationId.Value || m.ToLocationId == locationId.Value);
|
||||
if (itemId.HasValue) query = query.Where(m => m.FK_ItemId == itemId.Value);
|
||||
if (type.HasValue) query = query.Where(m => m.Type == type.Value);
|
||||
if (from.HasValue) query = query.Where(m => m.CreatedAt >= from.Value);
|
||||
if (to.HasValue) query = query.Where(m => m.CreatedAt <= to.Value);
|
||||
return query.OrderByDescending(m => m.CreatedAt).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
|
||||
}
|
||||
|
||||
public Task<int> GetMovementsCountAsync(Guid? locationId, Guid? itemId, MovementType? type, DateTime? from, DateTime? to)
|
||||
private IQueryable<StockMovement> FilterMovements(MovementFilter f)
|
||||
{
|
||||
var query = db.StockMovements.AsQueryable();
|
||||
if (locationId.HasValue)
|
||||
query = query.Where(m => m.FromLocationId == locationId.Value || m.ToLocationId == locationId.Value);
|
||||
if (itemId.HasValue) query = query.Where(m => m.FK_ItemId == itemId.Value);
|
||||
if (type.HasValue) query = query.Where(m => m.Type == type.Value);
|
||||
if (from.HasValue) query = query.Where(m => m.CreatedAt >= from.Value);
|
||||
if (to.HasValue) query = query.Where(m => m.CreatedAt <= to.Value);
|
||||
return query.CountAsync();
|
||||
if (f.LocationId.HasValue)
|
||||
query = query.Where(m => m.FromLocationId == f.LocationId.Value || m.ToLocationId == f.LocationId.Value);
|
||||
if (f.ItemId.HasValue) query = query.Where(m => m.FK_ItemId == f.ItemId.Value);
|
||||
if (f.Type.HasValue) query = query.Where(m => m.Type == f.Type.Value);
|
||||
if (f.ReferenceId.HasValue) query = query.Where(m => m.ReferenceId == f.ReferenceId.Value);
|
||||
if (f.BatchId.HasValue) query = query.Where(m => m.FK_ItemBatchId == f.BatchId.Value);
|
||||
if (f.SerialId.HasValue) query = query.Where(m => m.FK_ItemSerialNumberId == f.SerialId.Value);
|
||||
if (f.CreatedBy.HasValue) query = query.Where(m => m.CreatedBy == f.CreatedBy.Value);
|
||||
if (!string.IsNullOrWhiteSpace(f.ReasonCode)) query = query.Where(m => m.ReasonCode == f.ReasonCode);
|
||||
if (f.From.HasValue) query = query.Where(m => m.CreatedAt >= f.From.Value);
|
||||
if (f.To.HasValue)
|
||||
{
|
||||
// A bare date (midnight) means "through the end of that day".
|
||||
var to = f.To.Value;
|
||||
var toExclusive = to.TimeOfDay == TimeSpan.Zero ? to.AddDays(1) : to;
|
||||
query = query.Where(m => m.CreatedAt < toExclusive);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
public Task<List<StockMovement>> GetMovementsAsync(MovementFilter filter, int page, int pageSize) =>
|
||||
FilterMovements(filter)
|
||||
.Include(m => m.Item)
|
||||
.Include(m => m.ItemBatch)
|
||||
.Include(m => m.ItemSerialNumber)
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
.Skip((page - 1) * pageSize).Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
public Task<int> GetMovementsCountAsync(MovementFilter filter) =>
|
||||
FilterMovements(filter).CountAsync();
|
||||
|
||||
public async Task<MovementSummary> GetMovementSummaryAsync(Guid itemId, Guid locationId, DateTime from, DateTime toExclusive)
|
||||
{
|
||||
var rows = db.StockMovements.AsNoTracking()
|
||||
.Where(m => m.FK_ItemId == itemId && (m.FromLocationId == locationId || m.ToLocationId == locationId));
|
||||
|
||||
var before = await rows.Where(m => m.CreatedAt < from)
|
||||
.GroupBy(_ => 1)
|
||||
.Select(g => g.Sum(m => m.ToLocationId == locationId ? m.Quantity : 0)
|
||||
- g.Sum(m => m.FromLocationId == locationId ? m.Quantity : 0))
|
||||
.FirstOrDefaultAsync();
|
||||
var inRange = rows.Where(m => m.CreatedAt >= from && m.CreatedAt < toExclusive);
|
||||
var received = await inRange.Where(m => m.ToLocationId == locationId).SumAsync(m => (decimal?)m.Quantity) ?? 0;
|
||||
var issued = await inRange.Where(m => m.FromLocationId == locationId).SumAsync(m => (decimal?)m.Quantity) ?? 0;
|
||||
var current = await db.StockBalances.AsNoTracking()
|
||||
.Where(b => b.FK_ItemId == itemId && b.LocationId == locationId)
|
||||
.SumAsync(b => (decimal?)b.Quantity) ?? 0;
|
||||
|
||||
return new MovementSummary(before, received, issued, before + received - issued, current);
|
||||
}
|
||||
|
||||
public Task<List<StockMovement>> GetMovementsByReferenceAsync(Guid referenceId, Guid itemId, MovementType type) =>
|
||||
db.StockMovements.AsNoTracking()
|
||||
.Where(m => m.ReferenceId == referenceId && m.FK_ItemId == itemId && m.Type == type)
|
||||
.ToListAsync();
|
||||
|
||||
public Task<Guid?> GetLastWarehouseSourceAsync(Guid vehicleId, Guid itemId) =>
|
||||
db.StockMovements.AsNoTracking()
|
||||
.Where(m => m.ToLocationId == vehicleId && m.FK_ItemId == itemId
|
||||
&& m.FromLocationType == LocationType.Warehouse)
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
.Select(m => m.FromLocationId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
public Task<ItemBatch?> FindBatchAsync(Guid itemId, string batchNumber) =>
|
||||
db.ItemBatches.FirstOrDefaultAsync(b => b.FK_ItemId == itemId && b.BatchNumber == batchNumber);
|
||||
|
||||
public async Task AddItemBatchAsync(ItemBatch batch) =>
|
||||
await db.ItemBatches.AddAsync(batch);
|
||||
|
||||
public Task<ItemSerialNumber?> GetSerialAsync(Guid serialId) =>
|
||||
db.ItemSerialNumbers.FirstOrDefaultAsync(s => s.Id == serialId);
|
||||
|
||||
public Task<bool> SerialNumberExistsAsync(Guid itemId, string serialNumber) =>
|
||||
db.ItemSerialNumbers.AnyAsync(s => s.FK_ItemId == itemId && s.SerialNumber == serialNumber);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user