431 lines
14 KiB
C#
431 lines
14 KiB
C#
using System.Collections.Generic;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using CMSMicroservice.Application.Common.Interfaces;
|
|
using CMSMicroservice.Application.Common.Interfaces.Repositories;
|
|
using CMSMicroservice.Domain.Entities;
|
|
using CMSMicroservice.Domain.Enums;
|
|
|
|
namespace CMSMicroservice.Infrastructure.Persistence.Repositories;
|
|
|
|
/// <summary>
|
|
/// Repository implementation برای مدیریت حرکات موجودی
|
|
/// </summary>
|
|
public class StockMovementRepository : IStockMovementRepository
|
|
{
|
|
private readonly IApplicationDbContext _context;
|
|
|
|
public StockMovementRepository(IApplicationDbContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
#region Read Operations
|
|
|
|
public async Task<StockMovement?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
|
|
{
|
|
return await _context.StockMovements
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.Product)
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.DiscountProduct)
|
|
.FirstOrDefaultAsync(m => m.Id == id, cancellationToken);
|
|
}
|
|
|
|
public async Task<List<StockMovement>> GetByInventoryItemIdAsync(
|
|
long inventoryItemId,
|
|
StockMovementType? movementType = null,
|
|
DateTime? fromDate = null,
|
|
DateTime? toDate = null,
|
|
int skip = 0,
|
|
int take = 100,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = _context.StockMovements
|
|
.Include(m => m.InventoryItem)
|
|
.Where(m => m.InventoryItemId == inventoryItemId);
|
|
|
|
if (movementType.HasValue)
|
|
{
|
|
query = query.Where(m => m.MovementType == movementType.Value);
|
|
}
|
|
|
|
if (fromDate.HasValue)
|
|
{
|
|
query = query.Where(m => m.Created >= fromDate.Value);
|
|
}
|
|
|
|
if (toDate.HasValue)
|
|
{
|
|
query = query.Where(m => m.Created <= toDate.Value);
|
|
}
|
|
|
|
return await query
|
|
.OrderByDescending(m => m.Created)
|
|
.Skip(skip)
|
|
.Take(take)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<List<StockMovement>> GetByOrderIdAsync(long orderId, CancellationToken cancellationToken = default)
|
|
{
|
|
return await _context.StockMovements
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.Product)
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.DiscountProduct)
|
|
.Where(m => m.OrderId == orderId)
|
|
.OrderByDescending(m => m.Created)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<List<StockMovement>> GetByDiscountOrderIdAsync(long discountOrderId, CancellationToken cancellationToken = default)
|
|
{
|
|
return await _context.StockMovements
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.Product)
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.DiscountProduct)
|
|
.Where(m => m.DiscountOrderId == discountOrderId)
|
|
.OrderByDescending(m => m.Created)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<List<StockMovement>> GetByReferenceNumberAsync(string referenceNumber, CancellationToken cancellationToken = default)
|
|
{
|
|
return await _context.StockMovements
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.Product)
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.DiscountProduct)
|
|
.Where(m => m.ReferenceNumber == referenceNumber)
|
|
.OrderByDescending(m => m.Created)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<List<StockMovement>> GetByMovementTypeAsync(
|
|
StockMovementType movementType,
|
|
DateTime? fromDate = null,
|
|
DateTime? toDate = null,
|
|
int skip = 0,
|
|
int take = 100,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = _context.StockMovements
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.Product)
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.DiscountProduct)
|
|
.Where(m => m.MovementType == movementType);
|
|
|
|
if (fromDate.HasValue)
|
|
{
|
|
query = query.Where(m => m.Created >= fromDate.Value);
|
|
}
|
|
|
|
if (toDate.HasValue)
|
|
{
|
|
query = query.Where(m => m.Created <= toDate.Value);
|
|
}
|
|
|
|
return await query
|
|
.OrderByDescending(m => m.Created)
|
|
.Skip(skip)
|
|
.Take(take)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<List<StockMovement>> GetRecentMovementsAsync(
|
|
int count = 50,
|
|
StockMovementType? movementType = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = _context.StockMovements
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.Product)
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.DiscountProduct)
|
|
.AsQueryable();
|
|
|
|
if (movementType.HasValue)
|
|
{
|
|
query = query.Where(m => m.MovementType == movementType.Value);
|
|
}
|
|
|
|
return await query
|
|
.OrderByDescending(m => m.Created)
|
|
.Take(count)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<List<StockMovement>> SearchAsync(
|
|
long? inventoryItemId = null,
|
|
StockMovementType? movementType = null,
|
|
DateTime? fromDate = null,
|
|
DateTime? toDate = null,
|
|
string? referenceNumber = null,
|
|
long? orderId = null,
|
|
long? discountOrderId = null,
|
|
long? performedByUserId = null,
|
|
int skip = 0,
|
|
int take = 100,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = _context.StockMovements
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.Product)
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.DiscountProduct)
|
|
.AsQueryable();
|
|
|
|
if (inventoryItemId.HasValue)
|
|
{
|
|
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
|
|
}
|
|
|
|
if (movementType.HasValue)
|
|
{
|
|
query = query.Where(m => m.MovementType == movementType.Value);
|
|
}
|
|
|
|
if (fromDate.HasValue)
|
|
{
|
|
query = query.Where(m => m.Created >= fromDate.Value);
|
|
}
|
|
|
|
if (toDate.HasValue)
|
|
{
|
|
query = query.Where(m => m.Created <= toDate.Value);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(referenceNumber))
|
|
{
|
|
query = query.Where(m => m.ReferenceNumber != null && m.ReferenceNumber.Contains(referenceNumber));
|
|
}
|
|
|
|
if (orderId.HasValue)
|
|
{
|
|
query = query.Where(m => m.OrderId == orderId.Value);
|
|
}
|
|
|
|
if (discountOrderId.HasValue)
|
|
{
|
|
query = query.Where(m => m.DiscountOrderId == discountOrderId.Value);
|
|
}
|
|
|
|
if (performedByUserId.HasValue)
|
|
{
|
|
query = query.Where(m => m.PerformedByUserId == performedByUserId.Value);
|
|
}
|
|
|
|
return await query
|
|
.OrderByDescending(m => m.Created)
|
|
.Skip(skip)
|
|
.Take(take)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<int> CountAsync(
|
|
long? inventoryItemId = null,
|
|
StockMovementType? movementType = null,
|
|
DateTime? fromDate = null,
|
|
DateTime? toDate = null,
|
|
string? referenceNumber = null,
|
|
long? orderId = null,
|
|
long? discountOrderId = null,
|
|
long? performedByUserId = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = _context.StockMovements.AsQueryable();
|
|
|
|
if (inventoryItemId.HasValue)
|
|
{
|
|
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
|
|
}
|
|
|
|
if (movementType.HasValue)
|
|
{
|
|
query = query.Where(m => m.MovementType == movementType.Value);
|
|
}
|
|
|
|
if (fromDate.HasValue)
|
|
{
|
|
query = query.Where(m => m.Created >= fromDate.Value);
|
|
}
|
|
|
|
if (toDate.HasValue)
|
|
{
|
|
query = query.Where(m => m.Created <= toDate.Value);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(referenceNumber))
|
|
{
|
|
query = query.Where(m => m.ReferenceNumber != null && m.ReferenceNumber.Contains(referenceNumber));
|
|
}
|
|
|
|
if (orderId.HasValue)
|
|
{
|
|
query = query.Where(m => m.OrderId == orderId.Value);
|
|
}
|
|
|
|
if (discountOrderId.HasValue)
|
|
{
|
|
query = query.Where(m => m.DiscountOrderId == discountOrderId.Value);
|
|
}
|
|
|
|
if (performedByUserId.HasValue)
|
|
{
|
|
query = query.Where(m => m.PerformedByUserId == performedByUserId.Value);
|
|
}
|
|
|
|
return await query.CountAsync(cancellationToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Write Operations
|
|
|
|
public async Task<StockMovement> AddAsync(StockMovement stockMovement, CancellationToken cancellationToken = default)
|
|
{
|
|
_context.StockMovements.Add(stockMovement);
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
return stockMovement;
|
|
}
|
|
|
|
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
|
|
{
|
|
var stockMovement = await _context.StockMovements.FindAsync(new object[] { id }, cancellationToken);
|
|
if (stockMovement != null)
|
|
{
|
|
_context.StockMovements.Remove(stockMovement);
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
}
|
|
}
|
|
|
|
public async Task<List<StockMovement>> BulkAddAsync(List<StockMovement> stockMovements, CancellationToken cancellationToken = default)
|
|
{
|
|
_context.StockMovements.AddRange(stockMovements);
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
return stockMovements;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Analytics & Reports
|
|
|
|
public async Task<Dictionary<StockMovementType, int>> GetMovementSummaryAsync(
|
|
DateTime fromDate,
|
|
DateTime toDate,
|
|
long? inventoryItemId = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = _context.StockMovements
|
|
.Where(m => m.Created >= fromDate && m.Created <= toDate);
|
|
|
|
if (inventoryItemId.HasValue)
|
|
{
|
|
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
|
|
}
|
|
|
|
var movements = await query
|
|
.GroupBy(m => m.MovementType)
|
|
.Select(g => new { MovementType = g.Key, TotalQuantity = g.Sum(m => m.Quantity) })
|
|
.ToListAsync(cancellationToken);
|
|
|
|
return movements.ToDictionary(x => x.MovementType, x => x.TotalQuantity);
|
|
}
|
|
|
|
public async Task<List<(DateTime Date, int InboundQuantity, int OutboundQuantity)>> GetDailyMovementVolumeAsync(
|
|
DateTime fromDate,
|
|
DateTime toDate,
|
|
long? inventoryItemId = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = _context.StockMovements
|
|
.Where(m => m.Created >= fromDate && m.Created <= toDate);
|
|
|
|
if (inventoryItemId.HasValue)
|
|
{
|
|
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
|
|
}
|
|
|
|
var movements = await query.ToListAsync(cancellationToken);
|
|
|
|
// نوعهای ورودی (افزایش موجودی)
|
|
var inboundTypes = new[]
|
|
{
|
|
StockMovementType.InitialStock,
|
|
StockMovementType.Restock,
|
|
StockMovementType.Return,
|
|
StockMovementType.TransferIn,
|
|
StockMovementType.AdjustmentPlus,
|
|
StockMovementType.Released
|
|
};
|
|
|
|
// نوعهای خروجی (کاهش موجودی)
|
|
var outboundTypes = new[]
|
|
{
|
|
StockMovementType.Sale,
|
|
StockMovementType.Damaged,
|
|
StockMovementType.Lost,
|
|
StockMovementType.TransferOut,
|
|
StockMovementType.AdjustmentMinus,
|
|
StockMovementType.Reserved
|
|
};
|
|
|
|
var dailyVolumes = movements
|
|
.GroupBy(m => m.Created.Date)
|
|
.Select(g => (
|
|
Date: g.Key,
|
|
InboundQuantity: g.Where(m => inboundTypes.Contains(m.MovementType)).Sum(m => m.Quantity),
|
|
OutboundQuantity: g.Where(m => outboundTypes.Contains(m.MovementType)).Sum(m => m.Quantity)
|
|
))
|
|
.OrderBy(x => x.Date)
|
|
.ToList();
|
|
|
|
return dailyVolumes;
|
|
}
|
|
|
|
public async Task<List<(long InventoryItemId, string ProductName, int MovementCount, int TotalQuantityChange)>> GetTopMovingProductsAsync(
|
|
DateTime fromDate,
|
|
DateTime toDate,
|
|
int count = 10,
|
|
StockMovementType? movementType = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = _context.StockMovements
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.Product)
|
|
.Include(m => m.InventoryItem)
|
|
.ThenInclude(i => i.DiscountProduct)
|
|
.Where(m => m.Created >= fromDate && m.Created <= toDate);
|
|
|
|
if (movementType.HasValue)
|
|
{
|
|
query = query.Where(m => m.MovementType == movementType.Value);
|
|
}
|
|
|
|
var movements = await query.ToListAsync(cancellationToken);
|
|
|
|
var topProducts = movements
|
|
.GroupBy(m => m.InventoryItemId)
|
|
.Select(g =>
|
|
{
|
|
var firstItem = g.First().InventoryItem;
|
|
var productName = firstItem.Product?.Title ?? firstItem.DiscountProduct?.Title ?? "Unknown";
|
|
return (
|
|
InventoryItemId: g.Key,
|
|
ProductName: productName,
|
|
MovementCount: g.Count(),
|
|
TotalQuantityChange: g.Sum(m => m.Quantity)
|
|
);
|
|
})
|
|
.OrderByDescending(x => x.MovementCount)
|
|
.Take(count)
|
|
.ToList();
|
|
|
|
return topProducts;
|
|
}
|
|
|
|
#endregion
|
|
}
|