refactor: Migrate backend and frontend architecture from a module-based to an app-based structure.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Zentral.API.Apps.Purchases.Dtos;
|
||||
using Zentral.API.Apps.Purchases.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Zentral.API.Apps.Purchases.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/purchases/orders")]
|
||||
public class PurchaseOrdersController : ControllerBase
|
||||
{
|
||||
private readonly PurchaseService _service;
|
||||
|
||||
public PurchaseOrdersController(PurchaseService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<PurchaseOrderDto>>> GetAll()
|
||||
{
|
||||
return Ok(await _service.GetAllAsync());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> GetById(int id)
|
||||
{
|
||||
var order = await _service.GetByIdAsync(id);
|
||||
if (order == null) return NotFound();
|
||||
return Ok(order);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> Create(CreatePurchaseOrderDto dto)
|
||||
{
|
||||
var order = await _service.CreateAsync(dto);
|
||||
return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> Update(int id, UpdatePurchaseOrderDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var order = await _service.UpdateAsync(id, dto);
|
||||
if (order == null) return NotFound();
|
||||
return Ok(order);
|
||||
}
|
||||
catch (System.InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult> Delete(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _service.DeleteAsync(id);
|
||||
if (!result) return NotFound();
|
||||
return NoContent();
|
||||
}
|
||||
catch (System.InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id}/confirm")]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> Confirm(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var order = await _service.ConfirmOrderAsync(id);
|
||||
return Ok(order);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
catch (System.InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id}/receive")]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> Receive(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var order = await _service.ReceiveOrderAsync(id);
|
||||
return Ok(order);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
catch (System.InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Zentral.API.Apps.Purchases.Dtos;
|
||||
using Zentral.API.Apps.Purchases.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Zentral.API.Apps.Purchases.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/purchases/suppliers")]
|
||||
public class SuppliersController : ControllerBase
|
||||
{
|
||||
private readonly SupplierService _service;
|
||||
|
||||
public SuppliersController(SupplierService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<SupplierDto>>> GetAll()
|
||||
{
|
||||
return Ok(await _service.GetAllAsync());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<SupplierDto>> GetById(int id)
|
||||
{
|
||||
var supplier = await _service.GetByIdAsync(id);
|
||||
if (supplier == null) return NotFound();
|
||||
return Ok(supplier);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<SupplierDto>> Create(CreateSupplierDto dto)
|
||||
{
|
||||
var supplier = await _service.CreateAsync(dto);
|
||||
return CreatedAtAction(nameof(GetById), new { id = supplier.Id }, supplier);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<SupplierDto>> Update(int id, UpdateSupplierDto dto)
|
||||
{
|
||||
var supplier = await _service.UpdateAsync(id, dto);
|
||||
if (supplier == null) return NotFound();
|
||||
return Ok(supplier);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult> Delete(int id)
|
||||
{
|
||||
var result = await _service.DeleteAsync(id);
|
||||
if (!result) return NotFound();
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Zentral.Domain.Entities.Purchases;
|
||||
|
||||
namespace Zentral.API.Apps.Purchases.Dtos;
|
||||
|
||||
public class PurchaseOrderDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string OrderNumber { get; set; } = string.Empty;
|
||||
public DateTime OrderDate { get; set; }
|
||||
public DateTime? ExpectedDeliveryDate { get; set; }
|
||||
public int SupplierId { get; set; }
|
||||
public string SupplierName { get; set; } = string.Empty;
|
||||
public PurchaseOrderStatus Status { get; set; }
|
||||
public int? DestinationWarehouseId { get; set; }
|
||||
public string? DestinationWarehouseName { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public decimal TotalNet { get; set; }
|
||||
public decimal TotalTax { get; set; }
|
||||
public decimal TotalGross { get; set; }
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public List<PurchaseOrderLineDto> Lines { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PurchaseOrderLineDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int PurchaseOrderId { get; set; }
|
||||
public int WarehouseArticleId { get; set; }
|
||||
public string ArticleCode { get; set; } = string.Empty;
|
||||
public string ArticleDescription { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public decimal Quantity { get; set; }
|
||||
public decimal ReceivedQuantity { get; set; }
|
||||
public decimal UnitPrice { get; set; }
|
||||
public decimal TaxRate { get; set; }
|
||||
public decimal DiscountPercent { get; set; }
|
||||
public decimal LineTotal { get; set; }
|
||||
}
|
||||
|
||||
public class CreatePurchaseOrderDto
|
||||
{
|
||||
public DateTime OrderDate { get; set; } = DateTime.Now;
|
||||
public DateTime? ExpectedDeliveryDate { get; set; }
|
||||
public int SupplierId { get; set; }
|
||||
public int? DestinationWarehouseId { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public List<CreatePurchaseOrderLineDto> Lines { get; set; } = new();
|
||||
}
|
||||
|
||||
public class CreatePurchaseOrderLineDto
|
||||
{
|
||||
public int WarehouseArticleId { get; set; }
|
||||
public string? Description { get; set; } // Optional override
|
||||
public decimal Quantity { get; set; }
|
||||
public decimal UnitPrice { get; set; }
|
||||
public decimal TaxRate { get; set; }
|
||||
public decimal DiscountPercent { get; set; }
|
||||
}
|
||||
|
||||
public class UpdatePurchaseOrderDto
|
||||
{
|
||||
public DateTime OrderDate { get; set; }
|
||||
public DateTime? ExpectedDeliveryDate { get; set; }
|
||||
public int? DestinationWarehouseId { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public List<UpdatePurchaseOrderLineDto> Lines { get; set; } = new();
|
||||
}
|
||||
|
||||
public class UpdatePurchaseOrderLineDto
|
||||
{
|
||||
public int? Id { get; set; } // Null if new line
|
||||
public int WarehouseArticleId { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public decimal Quantity { get; set; }
|
||||
public decimal UnitPrice { get; set; }
|
||||
public decimal TaxRate { get; set; }
|
||||
public decimal DiscountPercent { get; set; }
|
||||
public bool IsDeleted { get; set; } // To mark for deletion
|
||||
}
|
||||
63
src/backend/Zentral.API/Apps/Purchases/Dtos/SupplierDtos.cs
Normal file
63
src/backend/Zentral.API/Apps/Purchases/Dtos/SupplierDtos.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
|
||||
namespace Zentral.API.Apps.Purchases.Dtos;
|
||||
|
||||
public class SupplierDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? VatNumber { get; set; }
|
||||
public string? FiscalCode { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? Province { get; set; }
|
||||
public string? ZipCode { get; set; }
|
||||
public string? Country { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Pec { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? Website { get; set; }
|
||||
public string? PaymentTerms { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class CreateSupplierDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? VatNumber { get; set; }
|
||||
public string? FiscalCode { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? Province { get; set; }
|
||||
public string? ZipCode { get; set; }
|
||||
public string? Country { get; set; } = "Italia";
|
||||
public string? Email { get; set; }
|
||||
public string? Pec { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? Website { get; set; }
|
||||
public string? PaymentTerms { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateSupplierDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? VatNumber { get; set; }
|
||||
public string? FiscalCode { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? Province { get; set; }
|
||||
public string? ZipCode { get; set; }
|
||||
public string? Country { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Pec { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? Website { get; set; }
|
||||
public string? PaymentTerms { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Zentral.API.Apps.Purchases.Dtos;
|
||||
using Zentral.API.Apps.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.Apps.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Zentral.API.Apps.Purchases.Dtos;
|
||||
using Zentral.API.Services;
|
||||
using Zentral.Domain.Entities.Purchases;
|
||||
using Zentral.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Zentral.API.Apps.Purchases.Services;
|
||||
|
||||
public class SupplierService
|
||||
{
|
||||
private readonly ZentralDbContext _db;
|
||||
private readonly AutoCodeService _autoCodeService;
|
||||
|
||||
public SupplierService(ZentralDbContext db, AutoCodeService autoCodeService)
|
||||
{
|
||||
_db = db;
|
||||
_autoCodeService = autoCodeService;
|
||||
}
|
||||
|
||||
public async Task<List<SupplierDto>> GetAllAsync()
|
||||
{
|
||||
return await _db.Suppliers
|
||||
.AsNoTracking()
|
||||
.OrderBy(s => s.Name)
|
||||
.Select(s => new SupplierDto
|
||||
{
|
||||
Id = s.Id,
|
||||
Code = s.Code,
|
||||
Name = s.Name,
|
||||
VatNumber = s.VatNumber,
|
||||
FiscalCode = s.FiscalCode,
|
||||
Address = s.Address,
|
||||
City = s.City,
|
||||
Province = s.Province,
|
||||
ZipCode = s.ZipCode,
|
||||
Country = s.Country,
|
||||
Email = s.Email,
|
||||
Pec = s.Pec,
|
||||
Phone = s.Phone,
|
||||
Website = s.Website,
|
||||
PaymentTerms = s.PaymentTerms,
|
||||
Notes = s.Notes,
|
||||
IsActive = s.IsActive,
|
||||
CreatedAt = s.CreatedAt,
|
||||
UpdatedAt = s.UpdatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<SupplierDto?> GetByIdAsync(int id)
|
||||
{
|
||||
var supplier = await _db.Suppliers
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(s => s.Id == id);
|
||||
|
||||
if (supplier == null) return null;
|
||||
|
||||
return new SupplierDto
|
||||
{
|
||||
Id = supplier.Id,
|
||||
Code = supplier.Code,
|
||||
Name = supplier.Name,
|
||||
VatNumber = supplier.VatNumber,
|
||||
FiscalCode = supplier.FiscalCode,
|
||||
Address = supplier.Address,
|
||||
City = supplier.City,
|
||||
Province = supplier.Province,
|
||||
ZipCode = supplier.ZipCode,
|
||||
Country = supplier.Country,
|
||||
Email = supplier.Email,
|
||||
Pec = supplier.Pec,
|
||||
Phone = supplier.Phone,
|
||||
Website = supplier.Website,
|
||||
PaymentTerms = supplier.PaymentTerms,
|
||||
Notes = supplier.Notes,
|
||||
IsActive = supplier.IsActive,
|
||||
CreatedAt = supplier.CreatedAt,
|
||||
UpdatedAt = supplier.UpdatedAt
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<SupplierDto> CreateAsync(CreateSupplierDto dto)
|
||||
{
|
||||
// Genera codice automatico
|
||||
var code = await _autoCodeService.GenerateNextCodeAsync("supplier");
|
||||
if (string.IsNullOrEmpty(code))
|
||||
{
|
||||
// Fallback se disabilitato
|
||||
code = $"FOR-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString().Substring(0, 4).ToUpper()}";
|
||||
}
|
||||
|
||||
var supplier = new Supplier
|
||||
{
|
||||
Code = code,
|
||||
Name = dto.Name,
|
||||
VatNumber = dto.VatNumber,
|
||||
FiscalCode = dto.FiscalCode,
|
||||
Address = dto.Address,
|
||||
City = dto.City,
|
||||
Province = dto.Province,
|
||||
ZipCode = dto.ZipCode,
|
||||
Country = dto.Country,
|
||||
Email = dto.Email,
|
||||
Pec = dto.Pec,
|
||||
Phone = dto.Phone,
|
||||
Website = dto.Website,
|
||||
PaymentTerms = dto.PaymentTerms,
|
||||
Notes = dto.Notes,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.Now
|
||||
};
|
||||
|
||||
_db.Suppliers.Add(supplier);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return await GetByIdAsync(supplier.Id) ?? throw new InvalidOperationException("Failed to retrieve created supplier");
|
||||
}
|
||||
|
||||
public async Task<SupplierDto?> UpdateAsync(int id, UpdateSupplierDto dto)
|
||||
{
|
||||
var supplier = await _db.Suppliers.FindAsync(id);
|
||||
if (supplier == null) return null;
|
||||
|
||||
supplier.Name = dto.Name;
|
||||
supplier.VatNumber = dto.VatNumber;
|
||||
supplier.FiscalCode = dto.FiscalCode;
|
||||
supplier.Address = dto.Address;
|
||||
supplier.City = dto.City;
|
||||
supplier.Province = dto.Province;
|
||||
supplier.ZipCode = dto.ZipCode;
|
||||
supplier.Country = dto.Country;
|
||||
supplier.Email = dto.Email;
|
||||
supplier.Pec = dto.Pec;
|
||||
supplier.Phone = dto.Phone;
|
||||
supplier.Website = dto.Website;
|
||||
supplier.PaymentTerms = dto.PaymentTerms;
|
||||
supplier.Notes = dto.Notes;
|
||||
supplier.IsActive = dto.IsActive;
|
||||
supplier.UpdatedAt = DateTime.Now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return await GetByIdAsync(id);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id)
|
||||
{
|
||||
var supplier = await _db.Suppliers.FindAsync(id);
|
||||
if (supplier == null) return false;
|
||||
|
||||
// Check if used in purchase orders
|
||||
var hasOrders = await _db.PurchaseOrders.AnyAsync(o => o.SupplierId == id);
|
||||
if (hasOrders)
|
||||
{
|
||||
throw new InvalidOperationException("Impossibile eliminare il fornitore perché ha ordini associati.");
|
||||
}
|
||||
|
||||
_db.Suppliers.Remove(supplier);
|
||||
await _db.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user