Files
zentral/src/backend/Zentral.API/Modules/Warehouse/Controllers/StockLevelsController.cs

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
}