Files
zentral/src/backend/Zentral.API/Modules/Purchases/Services/PurchaseService.cs

341 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Zentral.API.Modules.Purchases.Dtos;
using Zentral.API.Modules.Warehouse.Services;
using Zentral.API.Services;
using Zentral.Domain.Entities.Purchases;
using Zentral.Domain.Entities.Warehouse;
using Zentral.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Zentral.API.Modules.Purchases.Services;
public class PurchaseService
{
private readonly ZentralDbContext _db;
private readonly AutoCodeService _autoCodeService;
private readonly IWarehouseService _warehouseService;
public PurchaseService(ZentralDbContext db, AutoCodeService autoCodeService, IWarehouseService warehouseService)
{
_db = db;
_autoCodeService = autoCodeService;
_warehouseService = warehouseService;
}
public async Task<List<PurchaseOrderDto>> GetAllAsync()
{
return await _db.PurchaseOrders
.AsNoTracking()
.Include(o => o.Supplier)
.Include(o => o.DestinationWarehouse)
.OrderByDescending(o => o.OrderDate)
.Select(o => new PurchaseOrderDto
{
Id = o.Id,
OrderNumber = o.OrderNumber,
OrderDate = o.OrderDate,
ExpectedDeliveryDate = o.ExpectedDeliveryDate,
SupplierId = o.SupplierId,
SupplierName = o.Supplier!.Name,
Status = o.Status,
DestinationWarehouseId = o.DestinationWarehouseId,
DestinationWarehouseName = o.DestinationWarehouse != null ? o.DestinationWarehouse.Name : null,
Notes = o.Notes,
TotalNet = o.TotalNet,
TotalTax = o.TotalTax,
TotalGross = o.TotalGross,
CreatedAt = o.CreatedAt,
UpdatedAt = o.UpdatedAt
})
.ToListAsync();
}
public async Task<PurchaseOrderDto?> GetByIdAsync(int id)
{
var order = await _db.PurchaseOrders
.AsNoTracking()
.Include(o => o.Supplier)
.Include(o => o.DestinationWarehouse)
.Include(o => o.Lines)
.ThenInclude(l => l.WarehouseArticle)
.FirstOrDefaultAsync(o => o.Id == id);
if (order == null) return null;
return new PurchaseOrderDto
{
Id = order.Id,
OrderNumber = order.OrderNumber,
OrderDate = order.OrderDate,
ExpectedDeliveryDate = order.ExpectedDeliveryDate,
SupplierId = order.SupplierId,
SupplierName = order.Supplier!.Name,
Status = order.Status,
DestinationWarehouseId = order.DestinationWarehouseId,
DestinationWarehouseName = order.DestinationWarehouse?.Name,
Notes = order.Notes,
TotalNet = order.TotalNet,
TotalTax = order.TotalTax,
TotalGross = order.TotalGross,
CreatedAt = order.CreatedAt,
UpdatedAt = order.UpdatedAt,
Lines = order.Lines.Select(l => new PurchaseOrderLineDto
{
Id = l.Id,
PurchaseOrderId = l.PurchaseOrderId,
WarehouseArticleId = l.WarehouseArticleId,
ArticleCode = l.WarehouseArticle!.Code,
ArticleDescription = l.WarehouseArticle.Description,
Description = l.Description,
Quantity = l.Quantity,
ReceivedQuantity = l.ReceivedQuantity,
UnitPrice = l.UnitPrice,
TaxRate = l.TaxRate,
DiscountPercent = l.DiscountPercent,
LineTotal = l.LineTotal
}).ToList()
};
}
public async Task<PurchaseOrderDto> CreateAsync(CreatePurchaseOrderDto dto)
{
var code = await _autoCodeService.GenerateNextCodeAsync("purchase_order");
if (string.IsNullOrEmpty(code))
{
code = $"ODA{DateTime.Now:yyyy}-{Guid.NewGuid().ToString().Substring(0, 5).ToUpper()}";
}
var order = new PurchaseOrder
{
OrderNumber = code,
OrderDate = dto.OrderDate,
ExpectedDeliveryDate = dto.ExpectedDeliveryDate,
SupplierId = dto.SupplierId,
DestinationWarehouseId = dto.DestinationWarehouseId,
Notes = dto.Notes,
Status = PurchaseOrderStatus.Draft
};
foreach (var lineDto in dto.Lines)
{
var line = new PurchaseOrderLine
{
WarehouseArticleId = lineDto.WarehouseArticleId,
Description = lineDto.Description ?? string.Empty,
Quantity = lineDto.Quantity,
UnitPrice = lineDto.UnitPrice,
TaxRate = lineDto.TaxRate,
DiscountPercent = lineDto.DiscountPercent
};
// If description is empty, fetch from article
if (string.IsNullOrEmpty(line.Description))
{
var article = await _db.WarehouseArticles.FindAsync(line.WarehouseArticleId);
if (article != null) line.Description = article.Description;
}
// Calculate totals
var netPrice = line.UnitPrice * (1 - line.DiscountPercent / 100);
line.LineTotal = Math.Round(netPrice * line.Quantity, 2);
order.Lines.Add(line);
}
CalculateOrderTotals(order);
_db.PurchaseOrders.Add(order);
await _db.SaveChangesAsync();
return await GetByIdAsync(order.Id) ?? throw new InvalidOperationException("Failed to retrieve created order");
}
public async Task<PurchaseOrderDto?> UpdateAsync(int id, UpdatePurchaseOrderDto dto)
{
var order = await _db.PurchaseOrders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == id);
if (order == null) return null;
if (order.Status != PurchaseOrderStatus.Draft)
throw new InvalidOperationException("Solo gli ordini in bozza possono essere modificati");
order.OrderDate = dto.OrderDate;
order.ExpectedDeliveryDate = dto.ExpectedDeliveryDate;
order.DestinationWarehouseId = dto.DestinationWarehouseId;
order.Notes = dto.Notes;
order.UpdatedAt = DateTime.Now;
// Update lines
foreach (var lineDto in dto.Lines)
{
if (lineDto.IsDeleted)
{
if (lineDto.Id.HasValue)
{
var lineToDelete = order.Lines.FirstOrDefault(l => l.Id == lineDto.Id.Value);
if (lineToDelete != null) order.Lines.Remove(lineToDelete);
}
continue;
}
PurchaseOrderLine line;
if (lineDto.Id.HasValue)
{
line = order.Lines.FirstOrDefault(l => l.Id == lineDto.Id.Value)
?? throw new KeyNotFoundException($"Line {lineDto.Id} not found");
}
else
{
line = new PurchaseOrderLine();
order.Lines.Add(line);
}
line.WarehouseArticleId = lineDto.WarehouseArticleId;
line.Description = lineDto.Description ?? string.Empty;
line.Quantity = lineDto.Quantity;
line.UnitPrice = lineDto.UnitPrice;
line.TaxRate = lineDto.TaxRate;
line.DiscountPercent = lineDto.DiscountPercent;
if (string.IsNullOrEmpty(line.Description))
{
var article = await _db.WarehouseArticles.FindAsync(line.WarehouseArticleId);
if (article != null) line.Description = article.Description;
}
var netPrice = line.UnitPrice * (1 - line.DiscountPercent / 100);
line.LineTotal = Math.Round(netPrice * line.Quantity, 2);
}
CalculateOrderTotals(order);
await _db.SaveChangesAsync();
return await GetByIdAsync(id);
}
public async Task<bool> DeleteAsync(int id)
{
var order = await _db.PurchaseOrders.FindAsync(id);
if (order == null) return false;
if (order.Status != PurchaseOrderStatus.Draft && order.Status != PurchaseOrderStatus.Cancelled)
throw new InvalidOperationException("Solo gli ordini in bozza o annullati possono essere eliminati");
_db.PurchaseOrders.Remove(order);
await _db.SaveChangesAsync();
return true;
}
public async Task<PurchaseOrderDto> ConfirmOrderAsync(int id)
{
var order = await _db.PurchaseOrders.FindAsync(id);
if (order == null) throw new KeyNotFoundException("Order not found");
if (order.Status != PurchaseOrderStatus.Draft) throw new InvalidOperationException("Solo gli ordini in bozza possono essere confermati");
order.Status = PurchaseOrderStatus.Confirmed;
order.UpdatedAt = DateTime.Now;
await _db.SaveChangesAsync();
return await GetByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve order");
}
public async Task<PurchaseOrderDto> ReceiveOrderAsync(int id)
{
var order = await _db.PurchaseOrders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == id);
if (order == null) throw new KeyNotFoundException("Order not found");
if (order.Status != PurchaseOrderStatus.Confirmed && order.Status != PurchaseOrderStatus.PartiallyReceived)
throw new InvalidOperationException("L'ordine deve essere confermato per essere ricevuto");
// Create Stock Movement (Inbound)
var warehouseId = order.DestinationWarehouseId;
if (!warehouseId.HasValue)
{
var defaultWarehouse = await _warehouseService.GetDefaultWarehouseAsync();
warehouseId = defaultWarehouse?.Id;
}
if (!warehouseId.HasValue) throw new InvalidOperationException("Nessun magazzino di destinazione specificato o di default");
// Genera numero documento movimento
var docNumber = await _warehouseService.GenerateDocumentNumberAsync(MovementType.Inbound);
// Trova causale di default per acquisto (se esiste, altrimenti null o crea)
var reason = (await _warehouseService.GetMovementReasonsAsync(MovementType.Inbound))
.FirstOrDefault(r => r.Code == "ACQ" || r.Description.Contains("Acquisto"));
var movement = new StockMovement
{
DocumentNumber = docNumber,
MovementDate = DateTime.Now,
Type = MovementType.Inbound,
Status = MovementStatus.Draft,
DestinationWarehouseId = warehouseId,
ReasonId = reason?.Id,
ExternalReference = order.OrderNumber,
Notes = $"Ricevimento merce da ordine {order.OrderNumber}"
};
movement = await _warehouseService.CreateMovementAsync(movement);
// Add lines to movement
foreach (var line in order.Lines)
{
var remainingQty = line.Quantity - line.ReceivedQuantity;
if (remainingQty <= 0) continue;
// Update received quantity on order line
line.ReceivedQuantity += remainingQty;
// Add movement line directly via DbContext since IWarehouseService doesn't expose AddLine (it exposes UpdateMovement)
// Or better, construct the movement with lines initially if possible.
// Since CreateMovementAsync saves, we need to add lines and save again.
var movementLine = new StockMovementLine
{
MovementId = movement.Id,
ArticleId = line.WarehouseArticleId,
Quantity = remainingQty,
UnitCost = line.UnitPrice * (1 - line.DiscountPercent / 100),
LineValue = Math.Round(remainingQty * (line.UnitPrice * (1 - line.DiscountPercent / 100)), 2)
};
_db.StockMovementLines.Add(movementLine);
}
await _db.SaveChangesAsync();
// Confirm movement to update stock
await _warehouseService.ConfirmMovementAsync(movement.Id);
// Update order status
var allReceived = order.Lines.All(l => l.ReceivedQuantity >= l.Quantity);
order.Status = allReceived ? PurchaseOrderStatus.Received : PurchaseOrderStatus.PartiallyReceived;
order.UpdatedAt = DateTime.Now;
await _db.SaveChangesAsync();
return await GetByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve order");
}
private void CalculateOrderTotals(PurchaseOrder order)
{
order.TotalNet = 0;
order.TotalTax = 0;
order.TotalGross = 0;
foreach (var line in order.Lines)
{
order.TotalNet += line.LineTotal;
var taxAmount = line.LineTotal * (line.TaxRate / 100);
order.TotalTax += taxAmount;
}
order.TotalGross = order.TotalNet + order.TotalTax;
}
}