changed name from Apollinare to Zentral
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user