feat: Implement inventory and warehouse management features
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m33s
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m33s
- Add GetLowStockItemsResponseDto and LowStockItemDto for low stock item queries. - Create CreateStockMovementCommand and its handler for managing stock movements. - Implement CreateStockMovementCommandValidator for validating stock movement commands. - Add GetStockMovementsQuery and its handler to retrieve stock movement records. - Create GetStockMovementsResponseDto and StockMovementListDto for stock movement responses. - Implement GetStockMovementsByInventoryItemQuery and its handler for fetching movements by inventory item. - Add CreateWarehouseCommand and its handler for creating new warehouses. - Implement CreateWarehouseCommandValidator for warehouse creation validation. - Add DeleteWarehouseCommand and its handler for removing warehouses. - Implement SetDefaultWarehouseCommand and its handler for setting a default warehouse. - Create UpdateWarehouseCommand and its handler for updating warehouse details. - Implement GetAllWarehousesQuery and its handler to retrieve all warehouses. - Add GetWarehouseQuery and its handler for fetching a specific warehouse by ID. - Implement SearchWarehousesQuery and its handler for searching warehouses based on criteria.
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Interfaces.Repositories;
|
||||
using CMSMicroservice.Application.DayaLoanCQ.Services;
|
||||
using CMSMicroservice.Infrastructure.Persistence;
|
||||
using CMSMicroservice.Infrastructure.Persistence.Interceptors;
|
||||
using CMSMicroservice.Infrastructure.Persistence.Repositories;
|
||||
using CMSMicroservice.Infrastructure.BackgroundJobs;
|
||||
using CMSMicroservice.Infrastructure.Services.Monitoring;
|
||||
using CMSMicroservice.Infrastructure.Configuration;
|
||||
@@ -121,10 +119,7 @@ public static class ConfigureServices
|
||||
builder => builder.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
|
||||
}
|
||||
|
||||
// Repository Pattern Registration
|
||||
services.AddScoped<IInventoryItemRepository, InventoryItemRepository>();
|
||||
services.AddScoped<IStockMovementRepository, StockMovementRepository>();
|
||||
services.AddScoped<IWarehouseRepository, WarehouseRepository>();
|
||||
// Inventory Business Service
|
||||
services.AddScoped<IInventoryService, InventoryService>();
|
||||
|
||||
#region AddAuthentication
|
||||
|
||||
@@ -2,9 +2,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Interfaces.Repositories;
|
||||
using CMSMicroservice.Infrastructure.Persistence;
|
||||
using CMSMicroservice.Infrastructure.Persistence.Repositories;
|
||||
using CMSMicroservice.Infrastructure.Services;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure;
|
||||
@@ -31,11 +29,6 @@ public static class DependencyInjection
|
||||
// Application Context Interface
|
||||
services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
|
||||
|
||||
// Repository Pattern Registration
|
||||
services.AddScoped<IInventoryItemRepository, InventoryItemRepository>();
|
||||
services.AddScoped<IStockMovementRepository, StockMovementRepository>();
|
||||
services.AddScoped<IWarehouseRepository, WarehouseRepository>();
|
||||
|
||||
// Business Services
|
||||
services.AddScoped<IInventoryService, InventoryService>();
|
||||
|
||||
|
||||
-454
@@ -1,454 +0,0 @@
|
||||
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 InventoryItemRepository : IInventoryItemRepository
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public InventoryItemRepository(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
#region Read Operations
|
||||
|
||||
public async Task<InventoryItem?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Include(i => i.Warehouse)
|
||||
.FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<InventoryItem?> GetByProductIdAsync(long productId, long warehouseId = 1, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.Warehouse)
|
||||
.FirstOrDefaultAsync(i => i.ProductId == productId && i.WarehouseId == warehouseId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<InventoryItem?> GetByDiscountProductIdAsync(long discountProductId, long warehouseId = 1, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InventoryItems
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Include(i => i.Warehouse)
|
||||
.FirstOrDefaultAsync(i => i.DiscountProductId == discountProductId && i.WarehouseId == warehouseId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<InventoryItem>> GetByWarehouseIdAsync(long warehouseId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Where(i => i.WarehouseId == warehouseId)
|
||||
.OrderBy(i => i.Product != null ? i.Product.Title : i.DiscountProduct != null ? i.DiscountProduct.Title : "")
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<InventoryItem>> GetLowStockItemsAsync(ProductType? productType = null, long warehouseId = 1, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Where(i => i.WarehouseId == warehouseId && i.Quantity <= i.LowStockThreshold && i.Quantity > 0);
|
||||
|
||||
if (productType.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.ProductType == productType.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(i => i.Quantity)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<InventoryItem>> GetOutOfStockItemsAsync(ProductType? productType = null, long warehouseId = 1, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Where(i => i.WarehouseId == warehouseId && i.Quantity == 0);
|
||||
|
||||
if (productType.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.ProductType == productType.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(i => i.Product != null ? i.Product.Title : i.DiscountProduct != null ? i.DiscountProduct.Title : "")
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<InventoryItem>> SearchAsync(
|
||||
string? searchTerm = null,
|
||||
ProductType? productType = null,
|
||||
long? warehouseId = null,
|
||||
int? minQuantity = null,
|
||||
int? maxQuantity = null,
|
||||
int skip = 0,
|
||||
int take = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Include(i => i.Warehouse)
|
||||
.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
var term = searchTerm.ToLower();
|
||||
query = query.Where(i =>
|
||||
(i.Product != null && i.Product.Title.ToLower().Contains(term)) ||
|
||||
(i.DiscountProduct != null && i.DiscountProduct.Title.ToLower().Contains(term)));
|
||||
}
|
||||
|
||||
if (productType.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.ProductType == productType.Value);
|
||||
}
|
||||
|
||||
if (warehouseId.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.WarehouseId == warehouseId.Value);
|
||||
}
|
||||
|
||||
if (minQuantity.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.Quantity >= minQuantity.Value);
|
||||
}
|
||||
|
||||
if (maxQuantity.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.Quantity <= maxQuantity.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(i => i.Product != null ? i.Product.Title : i.DiscountProduct != null ? i.DiscountProduct.Title : "")
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> CountAsync(
|
||||
string? searchTerm = null,
|
||||
ProductType? productType = null,
|
||||
long? warehouseId = null,
|
||||
int? minQuantity = null,
|
||||
int? maxQuantity = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.InventoryItems.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
var term = searchTerm.ToLower();
|
||||
query = query.Where(i =>
|
||||
(i.Product != null && i.Product.Title.ToLower().Contains(term)) ||
|
||||
(i.DiscountProduct != null && i.DiscountProduct.Title.ToLower().Contains(term)));
|
||||
}
|
||||
|
||||
if (productType.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.ProductType == productType.Value);
|
||||
}
|
||||
|
||||
if (warehouseId.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.WarehouseId == warehouseId.Value);
|
||||
}
|
||||
|
||||
if (minQuantity.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.Quantity >= minQuantity.Value);
|
||||
}
|
||||
|
||||
if (maxQuantity.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.Quantity <= maxQuantity.Value);
|
||||
}
|
||||
|
||||
return await query.CountAsync(cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Write Operations
|
||||
|
||||
public async Task<InventoryItem> AddAsync(InventoryItem inventoryItem, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.InventoryItems.Add(inventoryItem);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return inventoryItem;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(InventoryItem inventoryItem, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.InventoryItems.Update(inventoryItem);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var item = await _context.InventoryItems.FindAsync(new object[] { id }, cancellationToken);
|
||||
if (item != null)
|
||||
{
|
||||
_context.InventoryItems.Remove(item);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateQuantityAsync(
|
||||
long inventoryItemId,
|
||||
int quantityChange,
|
||||
StockMovementType movementType,
|
||||
string? note = null,
|
||||
string? referenceNumber = null,
|
||||
long? orderId = null,
|
||||
long? discountOrderId = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var item = await _context.InventoryItems.FindAsync(new object[] { inventoryItemId }, cancellationToken);
|
||||
if (item == null) return false;
|
||||
|
||||
// بررسی اینکه موجودی کافی برای کاهش موجود باشد
|
||||
if (quantityChange < 0 && item.Quantity + quantityChange < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// بروزرسانی موجودی
|
||||
item.Quantity += quantityChange;
|
||||
|
||||
// بروزرسانی تاریخ آخرین فعالیت
|
||||
if (movementType == StockMovementType.Sale)
|
||||
{
|
||||
item.LastSoldAt = DateTime.UtcNow;
|
||||
}
|
||||
else if (movementType == StockMovementType.Restock || movementType == StockMovementType.InitialStock)
|
||||
{
|
||||
item.LastRestockedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// ثبت حرکت موجودی
|
||||
var stockMovement = new StockMovement
|
||||
{
|
||||
InventoryItemId = inventoryItemId,
|
||||
MovementType = movementType,
|
||||
Quantity = Math.Abs(quantityChange),
|
||||
Note = note,
|
||||
ReferenceNumber = referenceNumber,
|
||||
OrderId = orderId,
|
||||
DiscountOrderId = discountOrderId,
|
||||
PerformedByUserId = performedByUserId
|
||||
};
|
||||
|
||||
_context.StockMovements.Add(stockMovement);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ReserveQuantityAsync(
|
||||
long inventoryItemId,
|
||||
int quantity,
|
||||
string? note = null,
|
||||
string? referenceNumber = null,
|
||||
long? orderId = null,
|
||||
long? discountOrderId = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var item = await _context.InventoryItems.FindAsync(new object[] { inventoryItemId }, cancellationToken);
|
||||
if (item == null) return false;
|
||||
|
||||
// بررسی موجودی قابل دسترس
|
||||
if (item.AvailableQuantity < quantity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// رزرو موجودی
|
||||
item.ReservedQuantity += quantity;
|
||||
|
||||
// ثبت حرکت رزرو
|
||||
var stockMovement = new StockMovement
|
||||
{
|
||||
InventoryItemId = inventoryItemId,
|
||||
MovementType = StockMovementType.Reserved,
|
||||
Quantity = quantity,
|
||||
Note = note ?? "Quantity reserved",
|
||||
ReferenceNumber = referenceNumber,
|
||||
OrderId = orderId,
|
||||
DiscountOrderId = discountOrderId,
|
||||
PerformedByUserId = performedByUserId
|
||||
};
|
||||
|
||||
_context.StockMovements.Add(stockMovement);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ReleaseReservedQuantityAsync(
|
||||
long inventoryItemId,
|
||||
int quantity,
|
||||
string? note = null,
|
||||
string? referenceNumber = null,
|
||||
long? orderId = null,
|
||||
long? discountOrderId = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var item = await _context.InventoryItems.FindAsync(new object[] { inventoryItemId }, cancellationToken);
|
||||
if (item == null) return false;
|
||||
|
||||
// بررسی اینکه مقدار رزرو شده کافی باشد
|
||||
if (item.ReservedQuantity < quantity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// آزاد کردن رزرو
|
||||
item.ReservedQuantity -= quantity;
|
||||
|
||||
// ثبت حرکت آزادسازی
|
||||
var stockMovement = new StockMovement
|
||||
{
|
||||
InventoryItemId = inventoryItemId,
|
||||
MovementType = StockMovementType.Released,
|
||||
Quantity = quantity,
|
||||
Note = note ?? "Reserved quantity released",
|
||||
ReferenceNumber = referenceNumber,
|
||||
OrderId = orderId,
|
||||
DiscountOrderId = discountOrderId,
|
||||
PerformedByUserId = performedByUserId
|
||||
};
|
||||
|
||||
_context.StockMovements.Add(stockMovement);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bulk Operations
|
||||
|
||||
public async Task<bool> BulkUpdateQuantityAsync(
|
||||
List<(long InventoryItemId, int QuantityChange, string? Note)> updates,
|
||||
StockMovementType movementType,
|
||||
string? referenceNumber = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inventoryItemIds = updates.Select(u => u.InventoryItemId).ToList();
|
||||
var items = await _context.InventoryItems
|
||||
.Where(i => inventoryItemIds.Contains(i.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (items.Count != updates.Count)
|
||||
{
|
||||
return false; // برخی آیتمها پیدا نشدند
|
||||
}
|
||||
|
||||
var stockMovements = new List<StockMovement>();
|
||||
|
||||
foreach (var update in updates)
|
||||
{
|
||||
var item = items.First(i => i.Id == update.InventoryItemId);
|
||||
|
||||
// بررسی موجودی کافی
|
||||
if (update.QuantityChange < 0 && item.Quantity + update.QuantityChange < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
item.Quantity += update.QuantityChange;
|
||||
|
||||
if (movementType == StockMovementType.Sale)
|
||||
{
|
||||
item.LastSoldAt = DateTime.UtcNow;
|
||||
}
|
||||
else if (movementType == StockMovementType.Restock || movementType == StockMovementType.InitialStock)
|
||||
{
|
||||
item.LastRestockedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
stockMovements.Add(new StockMovement
|
||||
{
|
||||
InventoryItemId = update.InventoryItemId,
|
||||
MovementType = movementType,
|
||||
Quantity = Math.Abs(update.QuantityChange),
|
||||
Note = update.Note,
|
||||
ReferenceNumber = referenceNumber,
|
||||
PerformedByUserId = performedByUserId
|
||||
});
|
||||
}
|
||||
|
||||
_context.StockMovements.AddRange(stockMovements);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> BulkReserveQuantityAsync(
|
||||
List<(long InventoryItemId, int Quantity, string? Note)> reservations,
|
||||
string? referenceNumber = null,
|
||||
long? orderId = null,
|
||||
long? discountOrderId = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inventoryItemIds = reservations.Select(r => r.InventoryItemId).ToList();
|
||||
var items = await _context.InventoryItems
|
||||
.Where(i => inventoryItemIds.Contains(i.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (items.Count != reservations.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stockMovements = new List<StockMovement>();
|
||||
|
||||
foreach (var reservation in reservations)
|
||||
{
|
||||
var item = items.First(i => i.Id == reservation.InventoryItemId);
|
||||
|
||||
// بررسی موجودی قابل دسترس
|
||||
if (item.AvailableQuantity < reservation.Quantity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
item.ReservedQuantity += reservation.Quantity;
|
||||
|
||||
stockMovements.Add(new StockMovement
|
||||
{
|
||||
InventoryItemId = reservation.InventoryItemId,
|
||||
MovementType = StockMovementType.Reserved,
|
||||
Quantity = reservation.Quantity,
|
||||
Note = reservation.Note ?? "Bulk reservation",
|
||||
ReferenceNumber = referenceNumber,
|
||||
OrderId = orderId,
|
||||
DiscountOrderId = discountOrderId,
|
||||
PerformedByUserId = performedByUserId
|
||||
});
|
||||
}
|
||||
|
||||
_context.StockMovements.AddRange(stockMovements);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
-430
@@ -1,430 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
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 WarehouseRepository : IWarehouseRepository
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public WarehouseRepository(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
#region Read Operations
|
||||
|
||||
public async Task<Warehouse?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Warehouses
|
||||
.Include(w => w.InventoryItems)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(w => w.InventoryItems)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.FirstOrDefaultAsync(w => w.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Warehouse?> GetByCodeAsync(string code, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Warehouses
|
||||
.Include(w => w.InventoryItems)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(w => w.InventoryItems)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.FirstOrDefaultAsync(w => w.Code == code, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Warehouse?> GetDefaultWarehouseAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Warehouses
|
||||
.Include(w => w.InventoryItems)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(w => w.InventoryItems)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.FirstOrDefaultAsync(w => w.IsDefault, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Warehouse>> GetActiveWarehousesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Warehouses
|
||||
.Where(w => w.IsActive)
|
||||
.OrderBy(w => w.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Warehouse>> GetAllAsync(
|
||||
bool includeInactive = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.Warehouses.AsQueryable();
|
||||
|
||||
if (!includeInactive)
|
||||
{
|
||||
query = query.Where(w => w.IsActive);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(w => w.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Warehouse>> SearchAsync(
|
||||
string? searchTerm = null,
|
||||
bool? isActive = null,
|
||||
int skip = 0,
|
||||
int take = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.Warehouses.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
var term = searchTerm.ToLower();
|
||||
query = query.Where(w =>
|
||||
w.Name.ToLower().Contains(term) ||
|
||||
w.Code.ToLower().Contains(term) ||
|
||||
(w.Address != null && w.Address.ToLower().Contains(term)));
|
||||
}
|
||||
|
||||
if (isActive.HasValue)
|
||||
{
|
||||
query = query.Where(w => w.IsActive == isActive.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(w => w.Name)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> CountAsync(
|
||||
string? searchTerm = null,
|
||||
bool? isActive = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.Warehouses.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
var term = searchTerm.ToLower();
|
||||
query = query.Where(w =>
|
||||
w.Name.ToLower().Contains(term) ||
|
||||
w.Code.ToLower().Contains(term) ||
|
||||
(w.Address != null && w.Address.ToLower().Contains(term)));
|
||||
}
|
||||
|
||||
if (isActive.HasValue)
|
||||
{
|
||||
query = query.Where(w => w.IsActive == isActive.Value);
|
||||
}
|
||||
|
||||
return await query.CountAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsByCodeAsync(string code, long? excludeId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.Warehouses.Where(w => w.Code == code);
|
||||
|
||||
if (excludeId.HasValue)
|
||||
{
|
||||
query = query.Where(w => w.Id != excludeId.Value);
|
||||
}
|
||||
|
||||
return await query.AnyAsync(cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Write Operations
|
||||
|
||||
public async Task<Warehouse> AddAsync(Warehouse warehouse, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// اگر این انبار پیشفرض است، سایر انبارها را غیرپیشفرض کن
|
||||
if (warehouse.IsDefault)
|
||||
{
|
||||
await RemoveDefaultFromAllWarehousesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
_context.Warehouses.Add(warehouse);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return warehouse;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Warehouse warehouse, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// اگر این انبار پیشفرض شده، سایر انبارها را غیرپیشفرض کن
|
||||
if (warehouse.IsDefault)
|
||||
{
|
||||
await RemoveDefaultFromAllWarehousesAsync(warehouse.Id, cancellationToken);
|
||||
}
|
||||
|
||||
_context.Warehouses.Update(warehouse);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var warehouse = await _context.Warehouses.FindAsync(new object[] { id }, cancellationToken);
|
||||
if (warehouse != null)
|
||||
{
|
||||
warehouse.IsActive = false;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetActiveStatusAsync(long id, bool isActive, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var warehouse = await _context.Warehouses.FindAsync(new object[] { id }, cancellationToken);
|
||||
if (warehouse != null)
|
||||
{
|
||||
warehouse.IsActive = isActive;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetAsDefaultAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// ابتدا همه انبارها را غیرپیشفرض کن
|
||||
await RemoveDefaultFromAllWarehousesAsync(cancellationToken);
|
||||
|
||||
// سپس انبار مورد نظر را پیشفرض کن
|
||||
var warehouse = await _context.Warehouses.FindAsync(new object[] { id }, cancellationToken);
|
||||
if (warehouse != null)
|
||||
{
|
||||
warehouse.IsDefault = true;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Analytics
|
||||
|
||||
public async Task<(int TotalProducts, int LowStockProducts, int OutOfStockProducts, decimal TotalValue)> GetWarehouseStatisticsAsync(
|
||||
long warehouseId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inventoryItems = await _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Where(i => i.WarehouseId == warehouseId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var totalProducts = inventoryItems.Count;
|
||||
var lowStockProducts = inventoryItems.Count(i => i.Quantity <= i.LowStockThreshold && i.Quantity > 0);
|
||||
var outOfStockProducts = inventoryItems.Count(i => i.Quantity == 0);
|
||||
|
||||
// محاسبه ارزش کل بر اساس قیمت محصولات
|
||||
decimal totalValue = 0;
|
||||
foreach (var item in inventoryItems)
|
||||
{
|
||||
if (item.Product != null)
|
||||
{
|
||||
totalValue += item.Quantity * item.Product.Price;
|
||||
}
|
||||
else if (item.DiscountProduct != null)
|
||||
{
|
||||
totalValue += item.Quantity * item.DiscountProduct.Price;
|
||||
}
|
||||
}
|
||||
|
||||
return (totalProducts, lowStockProducts, outOfStockProducts, totalValue);
|
||||
}
|
||||
|
||||
public async Task<List<(long ProductId, string ProductName, int TotalSold, int CurrentStock)>> GetTopSellingProductsAsync(
|
||||
long warehouseId,
|
||||
DateTime fromDate,
|
||||
DateTime toDate,
|
||||
int count = 10,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// دریافت حرکات فروش برای این انبار در بازه زمانی مشخص
|
||||
var salesMovements = await _context.StockMovements
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.Where(m => m.InventoryItem.WarehouseId == warehouseId &&
|
||||
m.MovementType == StockMovementType.Sale &&
|
||||
m.Created >= fromDate &&
|
||||
m.Created <= toDate)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// گروهبندی بر اساس محصول و محاسبه تعداد فروش
|
||||
var topProducts = salesMovements
|
||||
.GroupBy(m => m.InventoryItemId)
|
||||
.Select(g =>
|
||||
{
|
||||
var firstItem = g.First().InventoryItem;
|
||||
var productId = firstItem.ProductId ?? firstItem.DiscountProductId ?? 0;
|
||||
var productName = firstItem.Product?.Title ?? firstItem.DiscountProduct?.Title ?? "Unknown";
|
||||
var totalSold = g.Sum(m => m.Quantity);
|
||||
var currentStock = firstItem.Quantity;
|
||||
return (ProductId: productId, ProductName: productName, TotalSold: totalSold, CurrentStock: currentStock);
|
||||
})
|
||||
.OrderByDescending(x => x.TotalSold)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
|
||||
return topProducts;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private async Task RemoveDefaultFromAllWarehousesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var defaultWarehouses = await _context.Warehouses
|
||||
.Where(w => w.IsDefault)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var warehouse in defaultWarehouses)
|
||||
{
|
||||
warehouse.IsDefault = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveDefaultFromAllWarehousesAsync(long excludeId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var defaultWarehouses = await _context.Warehouses
|
||||
.Where(w => w.IsDefault && w.Id != excludeId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var warehouse in defaultWarehouses)
|
||||
{
|
||||
warehouse.IsDefault = false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user