361 lines
11 KiB
C#
361 lines
11 KiB
C#
using Zentral.API.Modules.Warehouse.Services;
|
|
using Zentral.Domain.Entities.Warehouse;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace Zentral.API.Modules.Warehouse.Controllers;
|
|
|
|
/// <summary>
|
|
/// Controller per la gestione delle giacenze e valorizzazione
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/warehouse/stock")]
|
|
public class StockLevelsController : ControllerBase
|
|
{
|
|
private readonly IWarehouseService _warehouseService;
|
|
private readonly ILogger<StockLevelsController> _logger;
|
|
|
|
public StockLevelsController(
|
|
IWarehouseService warehouseService,
|
|
ILogger<StockLevelsController> logger)
|
|
{
|
|
_warehouseService = warehouseService;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene le giacenze con filtri opzionali
|
|
/// </summary>
|
|
[HttpGet]
|
|
public async Task<ActionResult<List<StockLevelDto>>> GetStockLevels([FromQuery] StockLevelFilterDto? filter)
|
|
{
|
|
var stockFilter = filter != null ? new StockLevelFilter
|
|
{
|
|
ArticleId = filter.ArticleId,
|
|
WarehouseId = filter.WarehouseId,
|
|
BatchId = filter.BatchId,
|
|
CategoryId = filter.CategoryId,
|
|
OnlyWithStock = filter.OnlyWithStock,
|
|
OnlyLowStock = filter.OnlyLowStock,
|
|
Skip = filter.Skip,
|
|
Take = filter.Take
|
|
} : null;
|
|
|
|
var stockLevels = await _warehouseService.GetStockLevelsAsync(stockFilter);
|
|
return Ok(stockLevels.Select(MapToDto));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene la giacenza per articolo/magazzino/batch
|
|
/// </summary>
|
|
[HttpGet("{articleId}/{warehouseId}")]
|
|
public async Task<ActionResult<StockLevelDto>> GetStockLevel(int articleId, int warehouseId, [FromQuery] int? batchId = null)
|
|
{
|
|
var stockLevel = await _warehouseService.GetStockLevelAsync(articleId, warehouseId, batchId);
|
|
if (stockLevel == null)
|
|
return NotFound();
|
|
|
|
return Ok(MapToDto(stockLevel));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene gli articoli sotto scorta
|
|
/// </summary>
|
|
[HttpGet("low-stock")]
|
|
public async Task<ActionResult<List<StockLevelDto>>> GetLowStockArticles()
|
|
{
|
|
var lowStock = await _warehouseService.GetLowStockArticlesAsync();
|
|
return Ok(lowStock.Select(MapToDto));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene il riepilogo giacenze per articolo
|
|
/// </summary>
|
|
[HttpGet("summary/{articleId}")]
|
|
public async Task<ActionResult<StockSummaryDto>> GetStockSummary(int articleId)
|
|
{
|
|
var article = await _warehouseService.GetArticleByIdAsync(articleId);
|
|
if (article == null)
|
|
return NotFound();
|
|
|
|
var totalStock = await _warehouseService.GetTotalStockAsync(articleId);
|
|
var availableStock = await _warehouseService.GetAvailableStockAsync(articleId);
|
|
var stockLevels = await _warehouseService.GetStockLevelsAsync(new StockLevelFilter { ArticleId = articleId });
|
|
|
|
return Ok(new StockSummaryDto(
|
|
articleId,
|
|
article.Code,
|
|
article.Description,
|
|
article.UnitOfMeasure,
|
|
totalStock,
|
|
availableStock,
|
|
article.MinimumStock,
|
|
article.MaximumStock,
|
|
article.ReorderPoint,
|
|
article.MinimumStock.HasValue && totalStock <= article.MinimumStock.Value,
|
|
stockLevels.Sum(s => s.StockValue ?? 0),
|
|
stockLevels.Count,
|
|
stockLevels.Select(MapToDto).ToList()
|
|
));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calcola la valorizzazione di un articolo
|
|
/// </summary>
|
|
[HttpGet("valuation/{articleId}")]
|
|
public async Task<ActionResult<ArticleValuationDto>> GetArticleValuation(int articleId, [FromQuery] ValuationMethod? method = null)
|
|
{
|
|
try
|
|
{
|
|
var article = await _warehouseService.GetArticleByIdAsync(articleId);
|
|
if (article == null)
|
|
return NotFound();
|
|
|
|
var effectiveMethod = method ?? article.ValuationMethod ?? ValuationMethod.WeightedAverage;
|
|
var totalValue = await _warehouseService.CalculateArticleValueAsync(articleId, effectiveMethod);
|
|
var totalStock = await _warehouseService.GetTotalStockAsync(articleId);
|
|
var avgCost = await _warehouseService.GetWeightedAverageCostAsync(articleId);
|
|
|
|
return Ok(new ArticleValuationDto(
|
|
articleId,
|
|
article.Code,
|
|
article.Description,
|
|
effectiveMethod,
|
|
totalStock,
|
|
article.UnitOfMeasure,
|
|
avgCost,
|
|
article.StandardCost,
|
|
article.LastPurchaseCost,
|
|
totalValue
|
|
));
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
return NotFound(new { error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calcola la valorizzazione di periodo
|
|
/// </summary>
|
|
[HttpGet("valuation/period/{period}")]
|
|
public async Task<ActionResult<List<PeriodValuationDto>>> GetPeriodValuation(int period, [FromQuery] int? warehouseId = null)
|
|
{
|
|
var valuations = await _warehouseService.GetValuationsAsync(period, warehouseId);
|
|
return Ok(valuations.Select(v => new PeriodValuationDto(
|
|
v.Id,
|
|
v.Period,
|
|
v.ValuationDate,
|
|
v.ArticleId,
|
|
v.Article?.Code ?? "",
|
|
v.Article?.Description ?? "",
|
|
v.WarehouseId,
|
|
v.Warehouse?.Code,
|
|
v.Warehouse?.Name,
|
|
v.Quantity,
|
|
v.Method,
|
|
v.UnitCost,
|
|
v.TotalValue,
|
|
v.InboundQuantity,
|
|
v.InboundValue,
|
|
v.OutboundQuantity,
|
|
v.OutboundValue,
|
|
v.IsClosed
|
|
)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Genera la valorizzazione per un articolo e periodo
|
|
/// </summary>
|
|
[HttpPost("valuation/calculate")]
|
|
public async Task<ActionResult<PeriodValuationDto>> CalculatePeriodValuation([FromBody] CalculateValuationDto dto)
|
|
{
|
|
try
|
|
{
|
|
var valuation = await _warehouseService.CalculatePeriodValuationAsync(dto.ArticleId, dto.Period, dto.WarehouseId);
|
|
return Ok(new PeriodValuationDto(
|
|
valuation.Id,
|
|
valuation.Period,
|
|
valuation.ValuationDate,
|
|
valuation.ArticleId,
|
|
valuation.Article?.Code ?? "",
|
|
valuation.Article?.Description ?? "",
|
|
valuation.WarehouseId,
|
|
valuation.Warehouse?.Code,
|
|
valuation.Warehouse?.Name,
|
|
valuation.Quantity,
|
|
valuation.Method,
|
|
valuation.UnitCost,
|
|
valuation.TotalValue,
|
|
valuation.InboundQuantity,
|
|
valuation.InboundValue,
|
|
valuation.OutboundQuantity,
|
|
valuation.OutboundValue,
|
|
valuation.IsClosed
|
|
));
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
return NotFound(new { error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Chiude un periodo (blocca modifiche)
|
|
/// </summary>
|
|
[HttpPost("valuation/close-period/{period}")]
|
|
public async Task<ActionResult> ClosePeriod(int period)
|
|
{
|
|
await _warehouseService.ClosePeriodAsync(period);
|
|
return Ok(new { message = $"Periodo {period} chiuso correttamente" });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ricalcola il costo medio ponderato di un articolo
|
|
/// </summary>
|
|
[HttpPost("recalculate-average/{articleId}")]
|
|
public async Task<ActionResult> RecalculateAverageCost(int articleId)
|
|
{
|
|
try
|
|
{
|
|
await _warehouseService.UpdateWeightedAverageCostAsync(articleId);
|
|
var avgCost = await _warehouseService.GetWeightedAverageCostAsync(articleId);
|
|
return Ok(new { articleId, weightedAverageCost = avgCost });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest(new { error = ex.Message });
|
|
}
|
|
}
|
|
|
|
#region DTOs
|
|
|
|
public record StockLevelFilterDto(
|
|
int? ArticleId,
|
|
int? WarehouseId,
|
|
int? BatchId,
|
|
int? CategoryId,
|
|
bool? OnlyWithStock,
|
|
bool? OnlyLowStock,
|
|
int Skip = 0,
|
|
int Take = 100
|
|
);
|
|
|
|
public record StockLevelDto(
|
|
int Id,
|
|
int ArticleId,
|
|
string ArticleCode,
|
|
string ArticleDescription,
|
|
string? CategoryName,
|
|
int WarehouseId,
|
|
string WarehouseCode,
|
|
string WarehouseName,
|
|
int? BatchId,
|
|
string? BatchNumber,
|
|
DateTime? BatchExpiryDate,
|
|
decimal Quantity,
|
|
decimal ReservedQuantity,
|
|
decimal AvailableQuantity,
|
|
decimal OnOrderQuantity,
|
|
decimal? UnitCost,
|
|
decimal? StockValue,
|
|
string? LocationCode,
|
|
DateTime? LastMovementDate,
|
|
DateTime? LastInventoryDate,
|
|
decimal? MinimumStock,
|
|
bool IsLowStock
|
|
);
|
|
|
|
public record StockSummaryDto(
|
|
int ArticleId,
|
|
string ArticleCode,
|
|
string ArticleDescription,
|
|
string UnitOfMeasure,
|
|
decimal TotalStock,
|
|
decimal AvailableStock,
|
|
decimal? MinimumStock,
|
|
decimal? MaximumStock,
|
|
decimal? ReorderPoint,
|
|
bool IsLowStock,
|
|
decimal TotalValue,
|
|
int WarehouseCount,
|
|
List<StockLevelDto> StockByWarehouse
|
|
);
|
|
|
|
public record ArticleValuationDto(
|
|
int ArticleId,
|
|
string ArticleCode,
|
|
string ArticleDescription,
|
|
ValuationMethod Method,
|
|
decimal TotalQuantity,
|
|
string UnitOfMeasure,
|
|
decimal WeightedAverageCost,
|
|
decimal? StandardCost,
|
|
decimal? LastPurchaseCost,
|
|
decimal TotalValue
|
|
);
|
|
|
|
public record PeriodValuationDto(
|
|
int Id,
|
|
int Period,
|
|
DateTime ValuationDate,
|
|
int ArticleId,
|
|
string ArticleCode,
|
|
string ArticleDescription,
|
|
int? WarehouseId,
|
|
string? WarehouseCode,
|
|
string? WarehouseName,
|
|
decimal Quantity,
|
|
ValuationMethod Method,
|
|
decimal UnitCost,
|
|
decimal TotalValue,
|
|
decimal InboundQuantity,
|
|
decimal InboundValue,
|
|
decimal OutboundQuantity,
|
|
decimal OutboundValue,
|
|
bool IsClosed
|
|
);
|
|
|
|
public record CalculateValuationDto(
|
|
int ArticleId,
|
|
int Period,
|
|
int? WarehouseId
|
|
);
|
|
|
|
#endregion
|
|
|
|
#region Mapping
|
|
|
|
private static StockLevelDto MapToDto(StockLevel stock)
|
|
{
|
|
var isLowStock = stock.Article?.MinimumStock.HasValue == true &&
|
|
stock.Quantity <= stock.Article.MinimumStock.Value;
|
|
|
|
return new StockLevelDto(
|
|
stock.Id,
|
|
stock.ArticleId,
|
|
stock.Article?.Code ?? "",
|
|
stock.Article?.Description ?? "",
|
|
stock.Article?.Category?.Name,
|
|
stock.WarehouseId,
|
|
stock.Warehouse?.Code ?? "",
|
|
stock.Warehouse?.Name ?? "",
|
|
stock.BatchId,
|
|
stock.Batch?.BatchNumber,
|
|
stock.Batch?.ExpiryDate,
|
|
stock.Quantity,
|
|
stock.ReservedQuantity,
|
|
stock.AvailableQuantity,
|
|
stock.OnOrderQuantity,
|
|
stock.UnitCost,
|
|
stock.StockValue,
|
|
stock.LocationCode,
|
|
stock.LastMovementDate,
|
|
stock.LastInventoryDate,
|
|
stock.Article?.MinimumStock,
|
|
isLowStock
|
|
);
|
|
}
|
|
|
|
#endregion
|
|
}
|