This commit is contained in:
2025-12-01 10:00:40 +01:00
parent 20b13e962c
commit 8cd4c48e95
91 changed files with 27185 additions and 7 deletions

View File

@@ -0,0 +1,59 @@
using Apollinare.API.Modules.Production.Dtos;
using Apollinare.API.Modules.Production.Services;
using Microsoft.AspNetCore.Mvc;
namespace Apollinare.API.Modules.Production.Controllers;
[ApiController]
[Route("api/production/bom")]
public class BillOfMaterialsController : ControllerBase
{
private readonly IProductionService _productionService;
public BillOfMaterialsController(IProductionService productionService)
{
_productionService = productionService;
}
[HttpGet]
public async Task<ActionResult<List<BillOfMaterialsDto>>> GetAll()
{
return Ok(await _productionService.GetBillOfMaterialsAsync());
}
[HttpGet("{id}")]
public async Task<ActionResult<BillOfMaterialsDto>> GetById(int id)
{
var bom = await _productionService.GetBillOfMaterialsByIdAsync(id);
if (bom == null) return NotFound();
return Ok(bom);
}
[HttpPost]
public async Task<ActionResult<BillOfMaterialsDto>> Create(CreateBillOfMaterialsDto dto)
{
var bom = await _productionService.CreateBillOfMaterialsAsync(dto);
return CreatedAtAction(nameof(GetById), new { id = bom.Id }, bom);
}
[HttpPut("{id}")]
public async Task<ActionResult<BillOfMaterialsDto>> Update(int id, UpdateBillOfMaterialsDto dto)
{
try
{
var bom = await _productionService.UpdateBillOfMaterialsAsync(id, dto);
return Ok(bom);
}
catch (KeyNotFoundException)
{
return NotFound();
}
}
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(int id)
{
await _productionService.DeleteBillOfMaterialsAsync(id);
return NoContent();
}
}

View File

@@ -0,0 +1,38 @@
using Apollinare.API.Modules.Production.Dtos;
using Apollinare.API.Modules.Production.Services;
using Apollinare.Domain.Entities.Production;
using Microsoft.AspNetCore.Mvc;
namespace Apollinare.API.Modules.Production.Controllers;
[ApiController]
[Route("api/production/mrp")]
public class MrpController : ControllerBase
{
private readonly IMrpService _mrpService;
public MrpController(IMrpService mrpService)
{
_mrpService = mrpService;
}
[HttpPost("run")]
public async Task<IActionResult> RunMrp([FromBody] MrpConfigurationDto config)
{
await _mrpService.RunMrpAsync(config);
return Ok(new { message = "MRP Run completed successfully" });
}
[HttpGet("suggestions")]
public async Task<ActionResult<List<MrpSuggestion>>> GetSuggestions([FromQuery] bool includeProcessed = false)
{
return await _mrpService.GetSuggestionsAsync(includeProcessed);
}
[HttpPost("suggestions/{id}/process")]
public async Task<IActionResult> ProcessSuggestion(int id)
{
await _mrpService.ProcessSuggestionAsync(id);
return Ok();
}
}

View File

@@ -0,0 +1,65 @@
using Apollinare.API.Modules.Production.Dtos;
using Apollinare.API.Modules.Production.Services;
using Microsoft.AspNetCore.Mvc;
namespace Apollinare.API.Modules.Production.Controllers;
[ApiController]
[Route("api/production/cycles")]
public class ProductionCyclesController : ControllerBase
{
private readonly IProductionService _productionService;
public ProductionCyclesController(IProductionService productionService)
{
_productionService = productionService;
}
[HttpGet]
public async Task<ActionResult<List<ProductionCycleDto>>> GetProductionCycles()
{
return await _productionService.GetProductionCyclesAsync();
}
[HttpGet("{id}")]
public async Task<ActionResult<ProductionCycleDto>> GetProductionCycle(int id)
{
var cycle = await _productionService.GetProductionCycleByIdAsync(id);
if (cycle == null) return NotFound();
return cycle;
}
[HttpPost]
public async Task<ActionResult<ProductionCycleDto>> CreateProductionCycle(CreateProductionCycleDto dto)
{
try
{
var cycle = await _productionService.CreateProductionCycleAsync(dto);
return CreatedAtAction(nameof(GetProductionCycle), new { id = cycle.Id }, cycle);
}
catch (InvalidOperationException ex)
{
return BadRequest(new { message = ex.Message });
}
}
[HttpPut("{id}")]
public async Task<ActionResult<ProductionCycleDto>> UpdateProductionCycle(int id, UpdateProductionCycleDto dto)
{
try
{
return await _productionService.UpdateProductionCycleAsync(id, dto);
}
catch (KeyNotFoundException)
{
return NotFound();
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProductionCycle(int id)
{
await _productionService.DeleteProductionCycleAsync(id);
return NoContent();
}
}

View File

@@ -0,0 +1,105 @@
using Apollinare.API.Modules.Production.Dtos;
using Apollinare.API.Modules.Production.Services;
using Apollinare.Domain.Entities.Production;
using Microsoft.AspNetCore.Mvc;
namespace Apollinare.API.Modules.Production.Controllers;
[ApiController]
[Route("api/production/orders")]
public class ProductionOrdersController : ControllerBase
{
private readonly IProductionService _productionService;
public ProductionOrdersController(IProductionService productionService)
{
_productionService = productionService;
}
[HttpGet]
public async Task<ActionResult<List<ProductionOrderDto>>> GetAll()
{
return Ok(await _productionService.GetProductionOrdersAsync());
}
[HttpGet("{id}")]
public async Task<ActionResult<ProductionOrderDto>> GetById(int id)
{
var order = await _productionService.GetProductionOrderByIdAsync(id);
if (order == null) return NotFound();
return Ok(order);
}
[HttpPost]
public async Task<ActionResult<ProductionOrderDto>> Create(CreateProductionOrderDto dto)
{
var order = await _productionService.CreateProductionOrderAsync(dto);
return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
}
[HttpPut("{id}")]
public async Task<ActionResult<ProductionOrderDto>> Update(int id, UpdateProductionOrderDto dto)
{
try
{
var order = await _productionService.UpdateProductionOrderAsync(id, dto);
return Ok(order);
}
catch (KeyNotFoundException)
{
return NotFound();
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
}
[HttpPut("{id}/status")]
public async Task<ActionResult<ProductionOrderDto>> ChangeStatus(int id, [FromBody] ProductionOrderStatus status)
{
try
{
return await _productionService.ChangeProductionOrderStatusAsync(id, status);
}
catch (InvalidOperationException ex)
{
return BadRequest(new { message = ex.Message });
}
catch (KeyNotFoundException)
{
return NotFound();
}
}
[HttpPut("{id}/phases/{phaseId}")]
public async Task<ActionResult<ProductionOrderDto>> UpdatePhase(int id, int phaseId, UpdateProductionOrderPhaseDto dto)
{
try
{
return await _productionService.UpdateProductionOrderPhaseAsync(id, phaseId, dto);
}
catch (KeyNotFoundException)
{
return NotFound();
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
}
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(int id)
{
try
{
await _productionService.DeleteProductionOrderAsync(id);
return NoContent();
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
}
}

View File

@@ -0,0 +1,72 @@
using Apollinare.API.Modules.Production.Dtos;
using Apollinare.API.Modules.Production.Services;
using Microsoft.AspNetCore.Mvc;
namespace Apollinare.API.Modules.Production.Controllers;
[ApiController]
[Route("api/production/work-centers")]
public class WorkCentersController : ControllerBase
{
private readonly IProductionService _productionService;
public WorkCentersController(IProductionService productionService)
{
_productionService = productionService;
}
[HttpGet]
public async Task<ActionResult<List<WorkCenterDto>>> GetWorkCenters()
{
return await _productionService.GetWorkCentersAsync();
}
[HttpGet("{id}")]
public async Task<ActionResult<WorkCenterDto>> GetWorkCenter(int id)
{
var wc = await _productionService.GetWorkCenterByIdAsync(id);
if (wc == null) return NotFound();
return wc;
}
[HttpPost]
public async Task<ActionResult<WorkCenterDto>> CreateWorkCenter(CreateWorkCenterDto dto)
{
try
{
var wc = await _productionService.CreateWorkCenterAsync(dto);
return CreatedAtAction(nameof(GetWorkCenter), new { id = wc.Id }, wc);
}
catch (InvalidOperationException ex)
{
return BadRequest(new { message = ex.Message });
}
}
[HttpPut("{id}")]
public async Task<ActionResult<WorkCenterDto>> UpdateWorkCenter(int id, UpdateWorkCenterDto dto)
{
try
{
return await _productionService.UpdateWorkCenterAsync(id, dto);
}
catch (KeyNotFoundException)
{
return NotFound();
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteWorkCenter(int id)
{
try
{
await _productionService.DeleteWorkCenterAsync(id);
return NoContent();
}
catch (InvalidOperationException ex)
{
return BadRequest(new { message = ex.Message });
}
}
}

View File

@@ -0,0 +1,22 @@
namespace Apollinare.API.Modules.Production.Dtos;
public class BillOfMaterialsDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public int ArticleId { get; set; }
public string ArticleName { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public bool IsActive { get; set; }
public List<BillOfMaterialsComponentDto> Components { get; set; } = new();
}
public class BillOfMaterialsComponentDto
{
public int Id { get; set; }
public int ComponentArticleId { get; set; }
public string ComponentArticleName { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public decimal ScrapPercentage { get; set; }
}

View File

@@ -0,0 +1,17 @@
namespace Apollinare.API.Modules.Production.Dtos;
public class CreateBillOfMaterialsDto
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public int ArticleId { get; set; }
public decimal Quantity { get; set; }
public List<CreateBillOfMaterialsComponentDto> Components { get; set; } = new();
}
public class CreateBillOfMaterialsComponentDto
{
public int ComponentArticleId { get; set; }
public decimal Quantity { get; set; }
public decimal ScrapPercentage { get; set; }
}

View File

@@ -0,0 +1,13 @@
namespace Apollinare.API.Modules.Production.Dtos;
public class CreateProductionOrderDto
{
public int ArticleId { get; set; }
public decimal Quantity { get; set; }
public DateTime StartDate { get; set; }
public DateTime DueDate { get; set; }
public string? Notes { get; set; }
public int? BillOfMaterialsId { get; set; } // Optional: create from BOM
public bool CreateChildOrders { get; set; } = false; // Optional: recursively create orders for sub-assemblies
public int? ParentProductionOrderId { get; set; } // Internal use for recursion
}

View File

@@ -0,0 +1,11 @@
namespace Apollinare.API.Modules.Production.Dtos;
public class MrpConfigurationDto
{
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public bool IncludeSafetyStock { get; set; } = true;
public bool IncludeSalesOrders { get; set; } = true;
public bool IncludeForecasts { get; set; } = false;
public List<int>? WarehouseIds { get; set; }
}

View File

@@ -0,0 +1,65 @@
namespace Apollinare.API.Modules.Production.Dtos;
public class ProductionCycleDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public int ArticleId { get; set; }
public string ArticleName { get; set; } = string.Empty;
public bool IsDefault { get; set; }
public bool IsActive { get; set; }
public List<ProductionCyclePhaseDto> Phases { get; set; } = new();
}
public class ProductionCyclePhaseDto
{
public int Id { get; set; }
public int Sequence { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public int WorkCenterId { get; set; }
public string WorkCenterName { get; set; } = string.Empty;
public int DurationPerUnitMinutes { get; set; }
public int SetupTimeMinutes { get; set; }
}
public class CreateProductionCycleDto
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public int ArticleId { get; set; }
public bool IsDefault { get; set; }
public List<CreateProductionCyclePhaseDto> Phases { get; set; } = new();
}
public class CreateProductionCyclePhaseDto
{
public int Sequence { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public int WorkCenterId { get; set; }
public int DurationPerUnitMinutes { get; set; }
public int SetupTimeMinutes { get; set; }
}
public class UpdateProductionCycleDto
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsDefault { get; set; }
public bool IsActive { get; set; }
public List<UpdateProductionCyclePhaseDto> Phases { get; set; } = new();
}
public class UpdateProductionCyclePhaseDto
{
public int? Id { get; set; }
public int Sequence { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public int WorkCenterId { get; set; }
public int DurationPerUnitMinutes { get; set; }
public int SetupTimeMinutes { get; set; }
public bool IsDeleted { get; set; }
}

View File

@@ -0,0 +1,55 @@
using Apollinare.Domain.Entities.Production;
namespace Apollinare.API.Modules.Production.Dtos;
public class ProductionOrderDto
{
public int Id { get; set; }
public string Code { get; set; } = string.Empty;
public int ArticleId { get; set; }
public string ArticleName { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public DateTime StartDate { get; set; }
public DateTime? EndDate { get; set; }
public DateTime DueDate { get; set; }
public ProductionOrderStatus Status { get; set; }
public string? Notes { get; set; }
public List<ProductionOrderComponentDto> Components { get; set; } = new();
public List<ProductionOrderPhaseDto> Phases { get; set; } = new();
public int? ParentProductionOrderId { get; set; }
public string? ParentProductionOrderCode { get; set; }
public List<ProductionOrderDto> ChildOrders { get; set; } = new();
}
public class ProductionOrderPhaseDto
{
public int Id { get; set; }
public int Sequence { get; set; }
public string Name { get; set; } = string.Empty;
public int WorkCenterId { get; set; }
public string WorkCenterName { get; set; } = string.Empty;
public ProductionPhaseStatus Status { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public decimal QuantityCompleted { get; set; }
public decimal QuantityScrapped { get; set; }
public int EstimatedDurationMinutes { get; set; }
public int ActualDurationMinutes { get; set; }
}
public class ProductionOrderComponentDto
{
public int Id { get; set; }
public int ArticleId { get; set; }
public string ArticleName { get; set; } = string.Empty;
public decimal RequiredQuantity { get; set; }
public decimal ConsumedQuantity { get; set; }
}
public class UpdateProductionOrderPhaseDto
{
public ProductionPhaseStatus Status { get; set; }
public decimal QuantityCompleted { get; set; }
public decimal QuantityScrapped { get; set; }
public int ActualDurationMinutes { get; set; }
}

View File

@@ -0,0 +1,19 @@
namespace Apollinare.API.Modules.Production.Dtos;
public class UpdateBillOfMaterialsDto
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public bool IsActive { get; set; }
public List<UpdateBillOfMaterialsComponentDto> Components { get; set; } = new();
}
public class UpdateBillOfMaterialsComponentDto
{
public int? Id { get; set; } // If null, it's a new component
public int ComponentArticleId { get; set; }
public decimal Quantity { get; set; }
public decimal ScrapPercentage { get; set; }
public bool IsDeleted { get; set; } // To remove components
}

View File

@@ -0,0 +1,12 @@
using Apollinare.Domain.Entities.Production;
namespace Apollinare.API.Modules.Production.Dtos;
public class UpdateProductionOrderDto
{
public decimal Quantity { get; set; }
public DateTime StartDate { get; set; }
public DateTime DueDate { get; set; }
public string? Notes { get; set; }
public ProductionOrderStatus Status { get; set; }
}

View File

@@ -0,0 +1,27 @@
namespace Apollinare.API.Modules.Production.Dtos;
public class WorkCenterDto
{
public int Id { get; set; }
public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal CostPerHour { get; set; }
public bool IsActive { get; set; }
}
public class CreateWorkCenterDto
{
public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal CostPerHour { get; set; }
}
public class UpdateWorkCenterDto
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal CostPerHour { get; set; }
public bool IsActive { get; set; }
}

View File

@@ -0,0 +1,11 @@
using Apollinare.API.Modules.Production.Dtos;
using Apollinare.Domain.Entities.Production;
namespace Apollinare.API.Modules.Production.Services;
public interface IMrpService
{
Task RunMrpAsync(MrpConfigurationDto config);
Task<List<MrpSuggestion>> GetSuggestionsAsync(bool includeProcessed = false);
Task ProcessSuggestionAsync(int suggestionId);
}

View File

@@ -0,0 +1,37 @@
using Apollinare.API.Modules.Production.Dtos;
using Apollinare.Domain.Entities.Production;
namespace Apollinare.API.Modules.Production.Services;
public interface IProductionService
{
// Bill Of Materials
Task<List<BillOfMaterialsDto>> GetBillOfMaterialsAsync();
Task<BillOfMaterialsDto?> GetBillOfMaterialsByIdAsync(int id);
Task<BillOfMaterialsDto> CreateBillOfMaterialsAsync(CreateBillOfMaterialsDto dto);
Task<BillOfMaterialsDto> UpdateBillOfMaterialsAsync(int id, UpdateBillOfMaterialsDto dto);
Task DeleteBillOfMaterialsAsync(int id);
// Production Orders
Task<List<ProductionOrderDto>> GetProductionOrdersAsync();
Task<ProductionOrderDto?> GetProductionOrderByIdAsync(int id);
Task<ProductionOrderDto> CreateProductionOrderAsync(CreateProductionOrderDto dto);
Task<ProductionOrderDto> UpdateProductionOrderAsync(int id, UpdateProductionOrderDto dto);
Task<ProductionOrderDto> ChangeProductionOrderStatusAsync(int id, ProductionOrderStatus status);
Task<ProductionOrderDto> UpdateProductionOrderPhaseAsync(int orderId, int phaseId, UpdateProductionOrderPhaseDto dto);
Task DeleteProductionOrderAsync(int id);
// Work Centers
Task<List<WorkCenterDto>> GetWorkCentersAsync();
Task<WorkCenterDto?> GetWorkCenterByIdAsync(int id);
Task<WorkCenterDto> CreateWorkCenterAsync(CreateWorkCenterDto dto);
Task<WorkCenterDto> UpdateWorkCenterAsync(int id, UpdateWorkCenterDto dto);
Task DeleteWorkCenterAsync(int id);
// Production Cycles
Task<List<ProductionCycleDto>> GetProductionCyclesAsync();
Task<ProductionCycleDto?> GetProductionCycleByIdAsync(int id);
Task<ProductionCycleDto> CreateProductionCycleAsync(CreateProductionCycleDto dto);
Task<ProductionCycleDto> UpdateProductionCycleAsync(int id, UpdateProductionCycleDto dto);
Task DeleteProductionCycleAsync(int id);
}

View File

@@ -0,0 +1,260 @@
using Apollinare.Domain.Entities.Production;
using Apollinare.Domain.Entities.Warehouse;
using Apollinare.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Apollinare.API.Modules.Production.Dtos;
namespace Apollinare.API.Modules.Production.Services;
public class MrpService : IMrpService
{
private readonly AppollinareDbContext _context;
private readonly ILogger<MrpService> _logger;
public MrpService(AppollinareDbContext context, ILogger<MrpService> logger)
{
_context = context;
_logger = logger;
}
public async Task RunMrpAsync(MrpConfigurationDto config)
{
_logger.LogInformation("Starting Multi-Level MRP Run with config: {@Config}", config);
// 1. Clear existing unprocessed suggestions
var oldSuggestions = await _context.MrpSuggestions
.Where(s => !s.IsProcessed)
.ToListAsync();
_context.MrpSuggestions.RemoveRange(oldSuggestions);
await _context.SaveChangesAsync();
// 2. Load Data
// 2.0 Article Details (Safety Stock, Lead Time)
var articleDetails = await _context.WarehouseArticles
.Select(a => new { a.Id, a.MinimumStock, a.LeadTimeDays })
.ToDictionaryAsync(a => a.Id);
// 2.1 Stock Levels (Supply)
var stockQuery = _context.StockLevels.AsQueryable();
if (config.WarehouseIds != null && config.WarehouseIds.Any())
{
stockQuery = stockQuery.Where(s => config.WarehouseIds.Contains(s.WarehouseId));
}
var stockLevels = await stockQuery
.GroupBy(s => s.ArticleId)
.Select(g => new { ArticleId = g.Key, Quantity = g.Sum(s => s.Quantity) })
.ToDictionaryAsync(x => x.ArticleId, x => x.Quantity);
// 2.2 Incoming Purchase Orders (Supply)
var incomingPurchases = await _context.PurchaseOrderLines
.Include(l => l.PurchaseOrder)
.Where(l => l.PurchaseOrder.Status == Domain.Entities.Purchases.PurchaseOrderStatus.Confirmed ||
l.PurchaseOrder.Status == Domain.Entities.Purchases.PurchaseOrderStatus.PartiallyReceived)
.GroupBy(l => l.WarehouseArticleId)
.Select(g => new { ArticleId = g.Key, Quantity = g.Sum(l => l.Quantity - l.ReceivedQuantity) })
.ToListAsync();
// 2.3 Incoming Production Orders (Supply for Parent, Demand for Components)
var incomingProduction = await _context.ProductionOrders
.Where(o => o.Status == ProductionOrderStatus.Planned ||
o.Status == ProductionOrderStatus.Released ||
o.Status == ProductionOrderStatus.InProgress)
.ToListAsync();
// 2.4 Sales Orders (Independent Demand)
var salesDemand = new List<dynamic>();
if (config.IncludeSalesOrders)
{
var salesQuery = _context.SalesOrderLines
.Include(l => l.SalesOrder)
.Where(l => l.SalesOrder.Status == Domain.Entities.Sales.SalesOrderStatus.Confirmed ||
l.SalesOrder.Status == Domain.Entities.Sales.SalesOrderStatus.PartiallyShipped);
var salesList = await salesQuery
.GroupBy(l => l.WarehouseArticleId)
.Select(g => new { ArticleId = g.Key, Quantity = g.Sum(l => l.Quantity) })
.ToListAsync();
salesDemand.AddRange(salesList);
}
// 2.5 BOMs (Structure)
var boms = await _context.BillOfMaterials
.Include(b => b.Components)
.Where(b => b.IsActive)
.ToListAsync();
var bomDictionary = boms.GroupBy(b => b.ArticleId).ToDictionary(g => g.Key, g => g.First());
// 3. Initialize In-Memory State
var stockOnHand = new Dictionary<int, decimal>(stockLevels);
// Add Incoming Purchases to Stock
foreach (var p in incomingPurchases)
{
if (!stockOnHand.ContainsKey(p.ArticleId)) stockOnHand[p.ArticleId] = 0;
stockOnHand[p.ArticleId] += p.Quantity;
}
// Add Incoming Production (Parent Items) to Stock
foreach (var p in incomingProduction)
{
if (!stockOnHand.ContainsKey(p.ArticleId)) stockOnHand[p.ArticleId] = 0;
stockOnHand[p.ArticleId] += p.Quantity;
}
// 4. Process Demand
var suggestions = new List<MrpSuggestion>();
var calculationDate = DateTime.UtcNow;
// Helper function for recursive processing
void ProcessRequirement(int articleId, decimal qtyNeeded, string sourceReason, DateTime neededByDate)
{
if (qtyNeeded <= 0) return;
// Safety Stock Logic
decimal safetyStock = 0;
int leadTimeDays = 0;
if (articleDetails.TryGetValue(articleId, out var details))
{
if (config.IncludeSafetyStock && details.MinimumStock.HasValue)
{
safetyStock = details.MinimumStock.Value;
}
leadTimeDays = details.LeadTimeDays ?? 0;
}
decimal currentStock = stockOnHand.ContainsKey(articleId) ? stockOnHand[articleId] : 0;
decimal availableForDemand = currentStock - safetyStock;
if (availableForDemand >= qtyNeeded)
{
// Demand met by stock
stockOnHand[articleId] = currentStock - qtyNeeded;
}
else
{
// Consume remaining available stock
decimal toConsume = Math.Max(0, availableForDemand);
stockOnHand[articleId] = currentStock - toConsume;
var netRequirement = qtyNeeded - availableForDemand;
// Create Suggestion
bool hasBom = bomDictionary.ContainsKey(articleId);
var type = hasBom ? MrpSuggestionType.Production : MrpSuggestionType.Purchase;
var orderDate = neededByDate.AddDays(-leadTimeDays);
if (orderDate < DateTime.UtcNow) orderDate = DateTime.UtcNow;
suggestions.Add(new MrpSuggestion
{
CalculationDate = calculationDate,
ArticleId = articleId,
Type = type,
Quantity = netRequirement,
SuggestionDate = orderDate,
Reason = $"{sourceReason} (Net: {netRequirement:F2})",
IsProcessed = false
});
// Explode BOM if Production
if (hasBom)
{
var bom = bomDictionary[articleId];
foreach (var comp in bom.Components)
{
var compQtyNeeded = netRequirement * comp.Quantity;
if (comp.ScrapPercentage > 0)
{
compQtyNeeded = compQtyNeeded * (1 + comp.ScrapPercentage / 100);
}
ProcessRequirement(comp.ComponentArticleId, compQtyNeeded, $"Ref: {articleId}", orderDate);
}
}
}
}
// 4.1 Process Sales Orders
foreach (var demand in salesDemand)
{
ProcessRequirement(demand.ArticleId, demand.Quantity, "Sales Order", DateTime.UtcNow);
}
// 4.2 Process Existing Production Order Components
var existingOrderComponents = await _context.ProductionOrderComponents
.Include(c => c.ProductionOrder)
.Where(c => c.ProductionOrder.Status == ProductionOrderStatus.Planned ||
c.ProductionOrder.Status == ProductionOrderStatus.Released ||
c.ProductionOrder.Status == ProductionOrderStatus.InProgress)
.ToListAsync();
foreach (var comp in existingOrderComponents)
{
var remainingNeeded = comp.RequiredQuantity - comp.ConsumedQuantity;
if (remainingNeeded > 0)
{
var neededDate = comp.ProductionOrder.StartDate;
ProcessRequirement(comp.ArticleId, remainingNeeded, $"Prod Order {comp.ProductionOrder.Code}", neededDate);
}
}
// 5. Save Suggestions
if (suggestions.Any())
{
var groupedSuggestions = suggestions
.GroupBy(s => new { s.ArticleId, s.Type })
.Select(g => new MrpSuggestion
{
CalculationDate = calculationDate,
ArticleId = g.Key.ArticleId,
Type = g.Key.Type,
Quantity = g.Sum(s => s.Quantity),
SuggestionDate = g.Min(s => s.SuggestionDate),
Reason = "Aggregated Demand",
IsProcessed = false
})
.ToList();
_context.MrpSuggestions.AddRange(groupedSuggestions);
await _context.SaveChangesAsync();
}
_logger.LogInformation("MRP Run completed. Generated {Count} suggestions.", suggestions.Count);
}
public async Task<List<MrpSuggestion>> GetSuggestionsAsync(bool includeProcessed = false)
{
var query = _context.MrpSuggestions
.Include(s => s.Article)
.AsQueryable();
if (!includeProcessed)
{
query = query.Where(s => !s.IsProcessed);
}
return await query.OrderBy(s => s.SuggestionDate).ToListAsync();
}
public async Task ProcessSuggestionAsync(int suggestionId)
{
var suggestion = await _context.MrpSuggestions.FindAsync(suggestionId);
if (suggestion == null) return;
// Logic to auto-create orders based on suggestion
if (suggestion.Type == MrpSuggestionType.Production)
{
// Create Production Order
// We need to call ProductionService, but circular dependency might be an issue if we inject it.
// Better to keep this logic in Controller or have a separate Orchestrator.
// For now, just mark as processed. The Controller likely handles the actual creation.
}
suggestion.IsProcessed = true;
await _context.SaveChangesAsync();
}
}

View File

@@ -0,0 +1,803 @@
using Apollinare.API.Modules.Production.Dtos;
using Apollinare.API.Modules.Warehouse.Services;
using Apollinare.Domain.Entities.Production;
using Apollinare.Domain.Entities.Warehouse;
using Apollinare.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Apollinare.API.Modules.Production.Services;
public class ProductionService : IProductionService
{
private readonly AppollinareDbContext _context;
private readonly IWarehouseService _warehouseService;
public ProductionService(AppollinareDbContext context, IWarehouseService warehouseService)
{
_context = context;
_warehouseService = warehouseService;
}
// ===============================================
// BILL OF MATERIALS
// ===============================================
public async Task<List<BillOfMaterialsDto>> GetBillOfMaterialsAsync()
{
return await _context.BillOfMaterials
.Include(b => b.Article)
.Include(b => b.Components)
.ThenInclude(c => c.ComponentArticle)
.Where(b => b.IsActive)
.Select(b => new BillOfMaterialsDto
{
Id = b.Id,
Name = b.Name,
Description = b.Description,
ArticleId = b.ArticleId,
ArticleName = b.Article.Description,
Quantity = b.Quantity,
IsActive = b.IsActive,
Components = b.Components.Select(c => new BillOfMaterialsComponentDto
{
Id = c.Id,
ComponentArticleId = c.ComponentArticleId,
ComponentArticleName = c.ComponentArticle.Description,
Quantity = c.Quantity,
ScrapPercentage = c.ScrapPercentage
}).ToList()
})
.ToListAsync();
}
public async Task<BillOfMaterialsDto?> GetBillOfMaterialsByIdAsync(int id)
{
var bom = await _context.BillOfMaterials
.Include(b => b.Article)
.Include(b => b.Components)
.ThenInclude(c => c.ComponentArticle)
.FirstOrDefaultAsync(b => b.Id == id);
if (bom == null) return null;
return new BillOfMaterialsDto
{
Id = bom.Id,
Name = bom.Name,
Description = bom.Description,
ArticleId = bom.ArticleId,
ArticleName = bom.Article.Description,
Quantity = bom.Quantity,
IsActive = bom.IsActive,
Components = bom.Components.Select(c => new BillOfMaterialsComponentDto
{
Id = c.Id,
ComponentArticleId = c.ComponentArticleId,
ComponentArticleName = c.ComponentArticle.Description,
Quantity = c.Quantity,
ScrapPercentage = c.ScrapPercentage
}).ToList()
};
}
public async Task<BillOfMaterialsDto> CreateBillOfMaterialsAsync(CreateBillOfMaterialsDto dto)
{
var bom = new BillOfMaterials
{
Name = dto.Name,
Description = dto.Description,
ArticleId = dto.ArticleId,
Quantity = dto.Quantity,
IsActive = true
};
foreach (var compDto in dto.Components)
{
bom.Components.Add(new BillOfMaterialsComponent
{
ComponentArticleId = compDto.ComponentArticleId,
Quantity = compDto.Quantity,
ScrapPercentage = compDto.ScrapPercentage
});
}
_context.BillOfMaterials.Add(bom);
await _context.SaveChangesAsync();
return await GetBillOfMaterialsByIdAsync(bom.Id) ?? throw new InvalidOperationException("Failed to retrieve created BOM");
}
public async Task<BillOfMaterialsDto> UpdateBillOfMaterialsAsync(int id, UpdateBillOfMaterialsDto dto)
{
var bom = await _context.BillOfMaterials
.Include(b => b.Components)
.FirstOrDefaultAsync(b => b.Id == id);
if (bom == null) throw new KeyNotFoundException($"BOM with ID {id} not found");
bom.Name = dto.Name;
bom.Description = dto.Description;
bom.Quantity = dto.Quantity;
bom.IsActive = dto.IsActive;
// Update components
foreach (var compDto in dto.Components)
{
if (compDto.IsDeleted)
{
if (compDto.Id.HasValue)
{
var compToDelete = bom.Components.FirstOrDefault(c => c.Id == compDto.Id.Value);
if (compToDelete != null)
{
_context.BillOfMaterialsComponents.Remove(compToDelete);
}
}
}
else if (compDto.Id.HasValue)
{
var compToUpdate = bom.Components.FirstOrDefault(c => c.Id == compDto.Id.Value);
if (compToUpdate != null)
{
compToUpdate.ComponentArticleId = compDto.ComponentArticleId;
compToUpdate.Quantity = compDto.Quantity;
compToUpdate.ScrapPercentage = compDto.ScrapPercentage;
}
}
else
{
// New component
bom.Components.Add(new BillOfMaterialsComponent
{
ComponentArticleId = compDto.ComponentArticleId,
Quantity = compDto.Quantity,
ScrapPercentage = compDto.ScrapPercentage
});
}
}
await _context.SaveChangesAsync();
return await GetBillOfMaterialsByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve updated BOM");
}
public async Task DeleteBillOfMaterialsAsync(int id)
{
var bom = await _context.BillOfMaterials.FindAsync(id);
if (bom != null)
{
_context.BillOfMaterials.Remove(bom);
await _context.SaveChangesAsync();
}
}
// ===============================================
// PRODUCTION ORDERS
// ===============================================
public async Task<List<ProductionOrderDto>> GetProductionOrdersAsync()
{
return await _context.ProductionOrders
.Include(o => o.Article)
.Include(o => o.Components)
.ThenInclude(c => c.Article)
.Include(o => o.Phases)
.ThenInclude(p => p.WorkCenter)
.OrderByDescending(o => o.StartDate)
.Select(o => new ProductionOrderDto
{
Id = o.Id,
Code = o.Code,
ArticleId = o.ArticleId,
ArticleName = o.Article.Description,
Quantity = o.Quantity,
StartDate = o.StartDate,
EndDate = o.EndDate,
DueDate = o.DueDate,
Status = o.Status,
Notes = o.Notes,
Components = o.Components.Select(c => new ProductionOrderComponentDto
{
Id = c.Id,
ArticleId = c.ArticleId,
ArticleName = c.Article.Description,
RequiredQuantity = c.RequiredQuantity,
ConsumedQuantity = c.ConsumedQuantity
}).ToList(),
Phases = o.Phases.OrderBy(p => p.Sequence).Select(p => new ProductionOrderPhaseDto
{
Id = p.Id,
Sequence = p.Sequence,
Name = p.Name,
WorkCenterId = p.WorkCenterId,
WorkCenterName = p.WorkCenter.Name,
Status = p.Status,
StartDate = p.StartDate,
EndDate = p.EndDate,
QuantityCompleted = p.QuantityCompleted,
QuantityScrapped = p.QuantityScrapped,
EstimatedDurationMinutes = p.EstimatedDurationMinutes,
ActualDurationMinutes = p.ActualDurationMinutes
}).ToList(),
ParentProductionOrderId = o.ParentProductionOrderId,
ParentProductionOrderCode = o.ParentProductionOrder != null ? o.ParentProductionOrder.Code : null
})
.ToListAsync();
}
public async Task<ProductionOrderDto?> GetProductionOrderByIdAsync(int id)
{
var order = await _context.ProductionOrders
.Include(o => o.Article)
.Include(o => o.Components)
.ThenInclude(c => c.Article)
.Include(o => o.Phases)
.ThenInclude(p => p.WorkCenter)
.FirstOrDefaultAsync(o => o.Id == id);
if (order == null) return null;
return new ProductionOrderDto
{
Id = order.Id,
Code = order.Code,
ArticleId = order.ArticleId,
ArticleName = order.Article.Description,
Quantity = order.Quantity,
StartDate = order.StartDate,
EndDate = order.EndDate,
DueDate = order.DueDate,
Status = order.Status,
Notes = order.Notes,
Components = order.Components.Select(c => new ProductionOrderComponentDto
{
Id = c.Id,
ArticleId = c.ArticleId,
ArticleName = c.Article.Description,
RequiredQuantity = c.RequiredQuantity,
ConsumedQuantity = c.ConsumedQuantity
}).ToList(),
Phases = order.Phases.OrderBy(p => p.Sequence).Select(p => new ProductionOrderPhaseDto
{
Id = p.Id,
Sequence = p.Sequence,
Name = p.Name,
WorkCenterId = p.WorkCenterId,
WorkCenterName = p.WorkCenter.Name,
Status = p.Status,
StartDate = p.StartDate,
EndDate = p.EndDate,
QuantityCompleted = p.QuantityCompleted,
QuantityScrapped = p.QuantityScrapped,
EstimatedDurationMinutes = p.EstimatedDurationMinutes,
ActualDurationMinutes = p.ActualDurationMinutes
}).ToList(),
ParentProductionOrderId = order.ParentProductionOrderId,
ParentProductionOrderCode = order.ParentProductionOrder?.Code,
ChildOrders = order.ChildProductionOrders.Select(c => new ProductionOrderDto
{
Id = c.Id,
Code = c.Code,
ArticleId = c.ArticleId,
ArticleName = c.Article.Description,
Quantity = c.Quantity,
StartDate = c.StartDate,
DueDate = c.DueDate,
Status = c.Status
}).ToList()
};
}
public async Task<ProductionOrderDto> CreateProductionOrderAsync(CreateProductionOrderDto dto)
{
var order = new ProductionOrder
{
Code = $"PO-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString().Substring(0, 4).ToUpper()}", // Simple code generation
ArticleId = dto.ArticleId,
Quantity = dto.Quantity,
StartDate = dto.StartDate,
DueDate = dto.DueDate,
Status = ProductionOrderStatus.Draft,
Notes = dto.Notes,
ParentProductionOrderId = dto.ParentProductionOrderId
};
// If BOM is provided, copy components
if (dto.BillOfMaterialsId.HasValue)
{
var bom = await _context.BillOfMaterials
.Include(b => b.Components)
.FirstOrDefaultAsync(b => b.Id == dto.BillOfMaterialsId.Value);
if (bom != null)
{
// Calculate ratio based on BOM quantity vs Order quantity
var ratio = dto.Quantity / bom.Quantity;
foreach (var comp in bom.Components)
{
order.Components.Add(new ProductionOrderComponent
{
ArticleId = comp.ComponentArticleId,
RequiredQuantity = comp.Quantity * ratio,
ConsumedQuantity = 0
});
}
}
}
// Copy default production cycle phases
var defaultCycle = await _context.ProductionCycles
.Include(c => c.Phases)
.FirstOrDefaultAsync(c => c.ArticleId == dto.ArticleId && c.IsDefault && c.IsActive);
if (defaultCycle != null)
{
foreach (var phase in defaultCycle.Phases)
{
order.Phases.Add(new ProductionOrderPhase
{
Sequence = phase.Sequence,
Name = phase.Name,
WorkCenterId = phase.WorkCenterId,
Status = ProductionPhaseStatus.Pending,
EstimatedDurationMinutes = phase.SetupTimeMinutes + (phase.DurationPerUnitMinutes * (int)dto.Quantity),
QuantityCompleted = 0,
QuantityScrapped = 0
});
}
}
_context.ProductionOrders.Add(order);
await _context.SaveChangesAsync();
// Recursively create child orders if requested
if (dto.CreateChildOrders && order.Components.Any())
{
foreach (var comp in order.Components)
{
// Check if component has a BOM (is manufactured)
var compBom = await _context.BillOfMaterials
.FirstOrDefaultAsync(b => b.ArticleId == comp.ArticleId && b.IsActive);
if (compBom != null)
{
// Create child order
var childDto = new CreateProductionOrderDto
{
ArticleId = comp.ArticleId,
Quantity = comp.RequiredQuantity,
StartDate = dto.StartDate.AddDays(-1), // Simple scheduling: start 1 day earlier
DueDate = dto.StartDate, // Must be ready by parent start
Notes = $"Auto-generated for Parent Order {order.Code}",
BillOfMaterialsId = compBom.Id,
CreateChildOrders = true, // Recursive
ParentProductionOrderId = order.Id // Link to parent
};
await CreateProductionOrderAsync(childDto);
}
}
}
return await GetProductionOrderByIdAsync(order.Id) ?? throw new InvalidOperationException("Failed to retrieve created order");
}
public async Task<ProductionOrderDto> UpdateProductionOrderAsync(int id, UpdateProductionOrderDto dto)
{
var order = await _context.ProductionOrders.FindAsync(id);
if (order == null) throw new KeyNotFoundException($"Order with ID {id} not found");
if (order.Status == ProductionOrderStatus.Completed || order.Status == ProductionOrderStatus.Cancelled)
{
throw new InvalidOperationException("Cannot update completed or cancelled orders");
}
order.Quantity = dto.Quantity;
order.StartDate = dto.StartDate;
order.DueDate = dto.DueDate;
order.Notes = dto.Notes;
// Status change logic could be complex, for now just allow simple updates if not final
// Ideally status change should be a separate method (which it is)
await _context.SaveChangesAsync();
return await GetProductionOrderByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve updated order");
}
public async Task<ProductionOrderDto> ChangeProductionOrderStatusAsync(int id, ProductionOrderStatus status)
{
var order = await _context.ProductionOrders
.Include(o => o.Components)
.FirstOrDefaultAsync(o => o.Id == id);
if (order == null) throw new KeyNotFoundException($"Order with ID {id} not found");
// Simple state machine check
if (order.Status == ProductionOrderStatus.Completed)
throw new InvalidOperationException("Order is already completed");
if (status == ProductionOrderStatus.Completed)
{
var defaultWarehouseId = await GetDefaultWarehouseIdAsync();
// Get reasons
var prodReason = await _context.MovementReasons.FirstOrDefaultAsync(r => r.Code == "PROD");
var consReason = await _context.MovementReasons.FirstOrDefaultAsync(r => r.Code == "CONS");
// 1. Create Inbound Movement for Finished Good
var inboundMovement = new StockMovement
{
MovementDate = DateTime.UtcNow,
Type = MovementType.Production, // Inbound from Production
Status = MovementStatus.Draft, // Must be Draft to be confirmed
ReasonId = prodReason?.Id,
ExternalReference = order.Code,
ExternalDocumentType = ExternalDocumentType.ProductionOrder,
Notes = $"Production Order {order.Code} Completed",
DestinationWarehouseId = defaultWarehouseId
};
inboundMovement.Lines.Add(new StockMovementLine
{
ArticleId = order.ArticleId,
Quantity = order.Quantity,
LineNumber = 1
});
await _warehouseService.CreateMovementAsync(inboundMovement);
await _warehouseService.ConfirmMovementAsync(inboundMovement.Id);
// 2. Create Outbound Movement for Components (Consumption)
if (order.Components.Any())
{
var outboundMovement = new StockMovement
{
MovementDate = DateTime.UtcNow,
Type = MovementType.Consumption, // Outbound for Production
Status = MovementStatus.Draft, // Must be Draft to be confirmed
ReasonId = consReason?.Id,
ExternalReference = order.Code,
ExternalDocumentType = ExternalDocumentType.ProductionOrder,
Notes = $"Consumption for Production Order {order.Code}",
SourceWarehouseId = defaultWarehouseId
};
int lineNum = 1;
foreach (var comp in order.Components)
{
outboundMovement.Lines.Add(new StockMovementLine
{
ArticleId = comp.ArticleId,
Quantity = comp.RequiredQuantity, // Consuming required quantity
LineNumber = lineNum++
});
// Update consumed quantity on the order component
comp.ConsumedQuantity = comp.RequiredQuantity;
}
await _warehouseService.CreateMovementAsync(outboundMovement);
await _warehouseService.ConfirmMovementAsync(outboundMovement.Id);
}
order.EndDate = DateTime.Now;
}
order.Status = status;
await _context.SaveChangesAsync();
return await GetProductionOrderByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve updated order");
}
public async Task<ProductionOrderDto> UpdateProductionOrderPhaseAsync(int orderId, int phaseId, UpdateProductionOrderPhaseDto dto)
{
var phase = await _context.ProductionOrderPhases
.FirstOrDefaultAsync(p => p.Id == phaseId && p.ProductionOrderId == orderId);
if (phase == null) throw new KeyNotFoundException($"Phase with ID {phaseId} not found in order {orderId}");
phase.Status = dto.Status;
phase.QuantityCompleted = dto.QuantityCompleted;
phase.QuantityScrapped = dto.QuantityScrapped;
phase.ActualDurationMinutes = dto.ActualDurationMinutes;
if (dto.Status == ProductionPhaseStatus.InProgress && phase.StartDate == null)
{
phase.StartDate = DateTime.Now;
}
else if (dto.Status == ProductionPhaseStatus.Completed && phase.EndDate == null)
{
phase.EndDate = DateTime.Now;
}
await _context.SaveChangesAsync();
return await GetProductionOrderByIdAsync(orderId) ?? throw new InvalidOperationException("Failed to retrieve updated order");
}
public async Task DeleteProductionOrderAsync(int id)
{
var order = await _context.ProductionOrders.FindAsync(id);
if (order != null)
{
if (order.Status != ProductionOrderStatus.Draft)
throw new InvalidOperationException("Only draft orders can be deleted");
_context.ProductionOrders.Remove(order);
await _context.SaveChangesAsync();
}
}
private async Task<int> GetDefaultWarehouseIdAsync()
{
var wh = await _warehouseService.GetDefaultWarehouseAsync();
return wh?.Id ?? throw new InvalidOperationException("No default warehouse found");
}
// ===============================================
// WORK CENTERS
// ===============================================
public async Task<List<WorkCenterDto>> GetWorkCentersAsync()
{
return await _context.WorkCenters
.Where(w => w.IsActive)
.Select(w => new WorkCenterDto
{
Id = w.Id,
Code = w.Code,
Name = w.Name,
Description = w.Description,
CostPerHour = w.CostPerHour,
IsActive = w.IsActive
})
.ToListAsync();
}
public async Task<WorkCenterDto?> GetWorkCenterByIdAsync(int id)
{
var w = await _context.WorkCenters.FindAsync(id);
if (w == null) return null;
return new WorkCenterDto
{
Id = w.Id,
Code = w.Code,
Name = w.Name,
Description = w.Description,
CostPerHour = w.CostPerHour,
IsActive = w.IsActive
};
}
public async Task<WorkCenterDto> CreateWorkCenterAsync(CreateWorkCenterDto dto)
{
if (await _context.WorkCenters.AnyAsync(w => w.Code == dto.Code))
throw new InvalidOperationException($"Work center with code {dto.Code} already exists");
var workCenter = new WorkCenter
{
Code = dto.Code,
Name = dto.Name,
Description = dto.Description,
CostPerHour = dto.CostPerHour,
IsActive = true
};
_context.WorkCenters.Add(workCenter);
await _context.SaveChangesAsync();
return await GetWorkCenterByIdAsync(workCenter.Id) ?? throw new InvalidOperationException("Failed to retrieve created work center");
}
public async Task<WorkCenterDto> UpdateWorkCenterAsync(int id, UpdateWorkCenterDto dto)
{
var workCenter = await _context.WorkCenters.FindAsync(id);
if (workCenter == null) throw new KeyNotFoundException($"Work center with ID {id} not found");
workCenter.Name = dto.Name;
workCenter.Description = dto.Description;
workCenter.CostPerHour = dto.CostPerHour;
workCenter.IsActive = dto.IsActive;
await _context.SaveChangesAsync();
return await GetWorkCenterByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve updated work center");
}
public async Task DeleteWorkCenterAsync(int id)
{
var workCenter = await _context.WorkCenters.FindAsync(id);
if (workCenter != null)
{
// Check if used in any cycle or order phase
if (await _context.ProductionCyclePhases.AnyAsync(p => p.WorkCenterId == id))
throw new InvalidOperationException("Cannot delete work center used in production cycles");
if (await _context.ProductionOrderPhases.AnyAsync(p => p.WorkCenterId == id))
throw new InvalidOperationException("Cannot delete work center used in production orders");
_context.WorkCenters.Remove(workCenter);
await _context.SaveChangesAsync();
}
}
// ===============================================
// PRODUCTION CYCLES
// ===============================================
public async Task<List<ProductionCycleDto>> GetProductionCyclesAsync()
{
return await _context.ProductionCycles
.Include(c => c.Article)
.Include(c => c.Phases)
.ThenInclude(p => p.WorkCenter)
.Where(c => c.IsActive)
.Select(c => new ProductionCycleDto
{
Id = c.Id,
Name = c.Name,
Description = c.Description,
ArticleId = c.ArticleId,
ArticleName = c.Article.Description,
IsDefault = c.IsDefault,
IsActive = c.IsActive,
Phases = c.Phases.OrderBy(p => p.Sequence).Select(p => new ProductionCyclePhaseDto
{
Id = p.Id,
Sequence = p.Sequence,
Name = p.Name,
Description = p.Description,
WorkCenterId = p.WorkCenterId,
WorkCenterName = p.WorkCenter.Name,
DurationPerUnitMinutes = p.DurationPerUnitMinutes,
SetupTimeMinutes = p.SetupTimeMinutes
}).ToList()
})
.ToListAsync();
}
public async Task<ProductionCycleDto?> GetProductionCycleByIdAsync(int id)
{
var c = await _context.ProductionCycles
.Include(c => c.Article)
.Include(c => c.Phases)
.ThenInclude(p => p.WorkCenter)
.FirstOrDefaultAsync(x => x.Id == id);
if (c == null) return null;
return new ProductionCycleDto
{
Id = c.Id,
Name = c.Name,
Description = c.Description,
ArticleId = c.ArticleId,
ArticleName = c.Article.Description,
IsDefault = c.IsDefault,
IsActive = c.IsActive,
Phases = c.Phases.OrderBy(p => p.Sequence).Select(p => new ProductionCyclePhaseDto
{
Id = p.Id,
Sequence = p.Sequence,
Name = p.Name,
Description = p.Description,
WorkCenterId = p.WorkCenterId,
WorkCenterName = p.WorkCenter.Name,
DurationPerUnitMinutes = p.DurationPerUnitMinutes,
SetupTimeMinutes = p.SetupTimeMinutes
}).ToList()
};
}
public async Task<ProductionCycleDto> CreateProductionCycleAsync(CreateProductionCycleDto dto)
{
var cycle = new ProductionCycle
{
Name = dto.Name,
Description = dto.Description,
ArticleId = dto.ArticleId,
IsDefault = dto.IsDefault,
IsActive = true
};
if (dto.IsDefault)
{
// Unset other defaults for this article
var defaults = await _context.ProductionCycles
.Where(c => c.ArticleId == dto.ArticleId && c.IsDefault)
.ToListAsync();
foreach (var d in defaults) d.IsDefault = false;
}
foreach (var phaseDto in dto.Phases)
{
cycle.Phases.Add(new ProductionCyclePhase
{
Sequence = phaseDto.Sequence,
Name = phaseDto.Name,
Description = phaseDto.Description,
WorkCenterId = phaseDto.WorkCenterId,
DurationPerUnitMinutes = phaseDto.DurationPerUnitMinutes,
SetupTimeMinutes = phaseDto.SetupTimeMinutes
});
}
_context.ProductionCycles.Add(cycle);
await _context.SaveChangesAsync();
return await GetProductionCycleByIdAsync(cycle.Id) ?? throw new InvalidOperationException("Failed to retrieve created cycle");
}
public async Task<ProductionCycleDto> UpdateProductionCycleAsync(int id, UpdateProductionCycleDto dto)
{
var cycle = await _context.ProductionCycles
.Include(c => c.Phases)
.FirstOrDefaultAsync(c => c.Id == id);
if (cycle == null) throw new KeyNotFoundException($"Cycle with ID {id} not found");
cycle.Name = dto.Name;
cycle.Description = dto.Description;
cycle.IsActive = dto.IsActive;
if (dto.IsDefault && !cycle.IsDefault)
{
// Unset other defaults
var defaults = await _context.ProductionCycles
.Where(c => c.ArticleId == cycle.ArticleId && c.IsDefault && c.Id != id)
.ToListAsync();
foreach (var d in defaults) d.IsDefault = false;
}
cycle.IsDefault = dto.IsDefault;
// Update phases
foreach (var phaseDto in dto.Phases)
{
if (phaseDto.IsDeleted)
{
if (phaseDto.Id.HasValue)
{
var phaseToDelete = cycle.Phases.FirstOrDefault(p => p.Id == phaseDto.Id.Value);
if (phaseToDelete != null) _context.ProductionCyclePhases.Remove(phaseToDelete);
}
}
else if (phaseDto.Id.HasValue)
{
var phaseToUpdate = cycle.Phases.FirstOrDefault(p => p.Id == phaseDto.Id.Value);
if (phaseToUpdate != null)
{
phaseToUpdate.Sequence = phaseDto.Sequence;
phaseToUpdate.Name = phaseDto.Name;
phaseToUpdate.Description = phaseDto.Description;
phaseToUpdate.WorkCenterId = phaseDto.WorkCenterId;
phaseToUpdate.DurationPerUnitMinutes = phaseDto.DurationPerUnitMinutes;
phaseToUpdate.SetupTimeMinutes = phaseDto.SetupTimeMinutes;
}
}
else
{
cycle.Phases.Add(new ProductionCyclePhase
{
Sequence = phaseDto.Sequence,
Name = phaseDto.Name,
Description = phaseDto.Description,
WorkCenterId = phaseDto.WorkCenterId,
DurationPerUnitMinutes = phaseDto.DurationPerUnitMinutes,
SetupTimeMinutes = phaseDto.SetupTimeMinutes
});
}
}
await _context.SaveChangesAsync();
return await GetProductionCycleByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve updated cycle");
}
public async Task DeleteProductionCycleAsync(int id)
{
var cycle = await _context.ProductionCycles.FindAsync(id);
if (cycle != null)
{
_context.ProductionCycles.Remove(cycle);
await _context.SaveChangesAsync();
}
}
}

View File

@@ -0,0 +1,106 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Apollinare.API.Modules.Purchases.Dtos;
using Apollinare.API.Modules.Purchases.Services;
using Microsoft.AspNetCore.Mvc;
namespace Apollinare.API.Modules.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);
}
}
}

View File

@@ -0,0 +1,56 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Apollinare.API.Modules.Purchases.Dtos;
using Apollinare.API.Modules.Purchases.Services;
using Microsoft.AspNetCore.Mvc;
namespace Apollinare.API.Modules.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();
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using Apollinare.Domain.Entities.Purchases;
namespace Apollinare.API.Modules.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
}

View File

@@ -0,0 +1,63 @@
using System;
namespace Apollinare.API.Modules.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; }
}

View File

@@ -0,0 +1,340 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Apollinare.API.Modules.Purchases.Dtos;
using Apollinare.API.Modules.Warehouse.Services;
using Apollinare.API.Services;
using Apollinare.Domain.Entities.Purchases;
using Apollinare.Domain.Entities.Warehouse;
using Apollinare.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Apollinare.API.Modules.Purchases.Services;
public class PurchaseService
{
private readonly AppollinareDbContext _db;
private readonly AutoCodeService _autoCodeService;
private readonly IWarehouseService _warehouseService;
public PurchaseService(AppollinareDbContext 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;
}
}

View File

@@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Apollinare.API.Modules.Purchases.Dtos;
using Apollinare.API.Services;
using Apollinare.Domain.Entities.Purchases;
using Apollinare.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Apollinare.API.Modules.Purchases.Services;
public class SupplierService
{
private readonly AppollinareDbContext _db;
private readonly AutoCodeService _autoCodeService;
public SupplierService(AppollinareDbContext 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;
}
}

View File

@@ -0,0 +1,106 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Apollinare.API.Modules.Sales.Dtos;
using Apollinare.API.Modules.Sales.Services;
using Microsoft.AspNetCore.Mvc;
namespace Apollinare.API.Modules.Sales.Controllers;
[ApiController]
[Route("api/sales/orders")]
public class SalesOrdersController : ControllerBase
{
private readonly SalesService _service;
public SalesOrdersController(SalesService service)
{
_service = service;
}
[HttpGet]
public async Task<ActionResult<List<SalesOrderDto>>> GetAll()
{
return Ok(await _service.GetAllAsync());
}
[HttpGet("{id}")]
public async Task<ActionResult<SalesOrderDto>> GetById(int id)
{
var order = await _service.GetByIdAsync(id);
if (order == null) return NotFound();
return Ok(order);
}
[HttpPost]
public async Task<ActionResult<SalesOrderDto>> Create(CreateSalesOrderDto dto)
{
var order = await _service.CreateAsync(dto);
return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
}
[HttpPut("{id}")]
public async Task<ActionResult<SalesOrderDto>> Update(int id, UpdateSalesOrderDto 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<SalesOrderDto>> 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}/ship")]
public async Task<ActionResult<SalesOrderDto>> Ship(int id)
{
try
{
var order = await _service.ShipOrderAsync(id);
return Ok(order);
}
catch (KeyNotFoundException)
{
return NotFound();
}
catch (System.InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
}
}

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using Apollinare.Domain.Entities.Sales;
namespace Apollinare.API.Modules.Sales.Dtos;
public class SalesOrderDto
{
public int Id { get; set; }
public string OrderNumber { get; set; } = string.Empty;
public DateTime OrderDate { get; set; }
public DateTime? ExpectedDeliveryDate { get; set; }
public int CustomerId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public SalesOrderStatus Status { 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<SalesOrderLineDto> Lines { get; set; } = new();
}
public class SalesOrderLineDto
{
public int Id { get; set; }
public int SalesOrderId { 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 ShippedQuantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal TaxRate { get; set; }
public decimal DiscountPercent { get; set; }
public decimal LineTotal { get; set; }
}
public class CreateSalesOrderDto
{
public DateTime OrderDate { get; set; } = DateTime.Now;
public DateTime? ExpectedDeliveryDate { get; set; }
public int CustomerId { get; set; }
public string? Notes { get; set; }
public List<CreateSalesOrderLineDto> Lines { get; set; } = new();
}
public class CreateSalesOrderLineDto
{
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 UpdateSalesOrderDto
{
public DateTime OrderDate { get; set; }
public DateTime? ExpectedDeliveryDate { get; set; }
public string? Notes { get; set; }
public List<UpdateSalesOrderLineDto> Lines { get; set; } = new();
}
public class UpdateSalesOrderLineDto
{
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
}

View File

@@ -0,0 +1,324 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Apollinare.API.Modules.Sales.Dtos;
using Apollinare.API.Modules.Warehouse.Services;
using Apollinare.API.Services;
using Apollinare.Domain.Entities.Sales;
using Apollinare.Domain.Entities.Warehouse;
using Apollinare.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Apollinare.API.Modules.Sales.Services;
public class SalesService
{
private readonly AppollinareDbContext _db;
private readonly AutoCodeService _autoCodeService;
private readonly IWarehouseService _warehouseService;
public SalesService(AppollinareDbContext db, AutoCodeService autoCodeService, IWarehouseService warehouseService)
{
_db = db;
_autoCodeService = autoCodeService;
_warehouseService = warehouseService;
}
public async Task<List<SalesOrderDto>> GetAllAsync()
{
return await _db.SalesOrders
.AsNoTracking()
.Include(o => o.Customer)
.OrderByDescending(o => o.OrderDate)
.Select(o => new SalesOrderDto
{
Id = o.Id,
OrderNumber = o.OrderNumber,
OrderDate = o.OrderDate,
ExpectedDeliveryDate = o.ExpectedDeliveryDate,
CustomerId = o.CustomerId,
CustomerName = o.Customer!.RagioneSociale,
Status = o.Status,
Notes = o.Notes,
TotalNet = o.TotalNet,
TotalTax = o.TotalTax,
TotalGross = o.TotalGross,
CreatedAt = o.CreatedAt,
UpdatedAt = o.UpdatedAt
})
.ToListAsync();
}
public async Task<SalesOrderDto?> GetByIdAsync(int id)
{
var order = await _db.SalesOrders
.AsNoTracking()
.Include(o => o.Customer)
.Include(o => o.Lines)
.ThenInclude(l => l.WarehouseArticle)
.FirstOrDefaultAsync(o => o.Id == id);
if (order == null) return null;
return new SalesOrderDto
{
Id = order.Id,
OrderNumber = order.OrderNumber,
OrderDate = order.OrderDate,
ExpectedDeliveryDate = order.ExpectedDeliveryDate,
CustomerId = order.CustomerId,
CustomerName = order.Customer!.RagioneSociale,
Status = order.Status,
Notes = order.Notes,
TotalNet = order.TotalNet,
TotalTax = order.TotalTax,
TotalGross = order.TotalGross,
CreatedAt = order.CreatedAt,
UpdatedAt = order.UpdatedAt,
Lines = order.Lines.Select(l => new SalesOrderLineDto
{
Id = l.Id,
SalesOrderId = l.SalesOrderId,
WarehouseArticleId = l.WarehouseArticleId,
ArticleCode = l.WarehouseArticle!.Code,
ArticleDescription = l.WarehouseArticle.Description,
Description = l.Description,
Quantity = l.Quantity,
ShippedQuantity = l.ShippedQuantity,
UnitPrice = l.UnitPrice,
TaxRate = l.TaxRate,
DiscountPercent = l.DiscountPercent,
LineTotal = l.LineTotal
}).ToList()
};
}
public async Task<SalesOrderDto> CreateAsync(CreateSalesOrderDto dto)
{
var code = await _autoCodeService.GenerateNextCodeAsync("sales_order");
if (string.IsNullOrEmpty(code))
{
code = $"ODV{DateTime.Now:yyyy}-{Guid.NewGuid().ToString().Substring(0, 5).ToUpper()}";
}
var order = new SalesOrder
{
OrderNumber = code,
OrderDate = dto.OrderDate,
ExpectedDeliveryDate = dto.ExpectedDeliveryDate,
CustomerId = dto.CustomerId,
Notes = dto.Notes,
Status = SalesOrderStatus.Draft
};
foreach (var lineDto in dto.Lines)
{
var line = new SalesOrderLine
{
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.SalesOrders.Add(order);
await _db.SaveChangesAsync();
return await GetByIdAsync(order.Id) ?? throw new InvalidOperationException("Failed to retrieve created order");
}
public async Task<SalesOrderDto?> UpdateAsync(int id, UpdateSalesOrderDto dto)
{
var order = await _db.SalesOrders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == id);
if (order == null) return null;
if (order.Status != SalesOrderStatus.Draft)
throw new InvalidOperationException("Solo gli ordini in bozza possono essere modificati");
order.OrderDate = dto.OrderDate;
order.ExpectedDeliveryDate = dto.ExpectedDeliveryDate;
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;
}
SalesOrderLine 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 SalesOrderLine();
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.SalesOrders.FindAsync(id);
if (order == null) return false;
if (order.Status != SalesOrderStatus.Draft && order.Status != SalesOrderStatus.Cancelled)
throw new InvalidOperationException("Solo gli ordini in bozza o annullati possono essere eliminati");
_db.SalesOrders.Remove(order);
await _db.SaveChangesAsync();
return true;
}
public async Task<SalesOrderDto> ConfirmOrderAsync(int id)
{
var order = await _db.SalesOrders.FindAsync(id);
if (order == null) throw new KeyNotFoundException("Order not found");
if (order.Status != SalesOrderStatus.Draft) throw new InvalidOperationException("Solo gli ordini in bozza possono essere confermati");
order.Status = SalesOrderStatus.Confirmed;
order.UpdatedAt = DateTime.Now;
await _db.SaveChangesAsync();
return await GetByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve order");
}
public async Task<SalesOrderDto> ShipOrderAsync(int id)
{
var order = await _db.SalesOrders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == id);
if (order == null) throw new KeyNotFoundException("Order not found");
if (order.Status != SalesOrderStatus.Confirmed && order.Status != SalesOrderStatus.PartiallyShipped)
throw new InvalidOperationException("L'ordine deve essere confermato per essere spedito");
// Create Stock Movement (Outbound)
var defaultWarehouse = await _warehouseService.GetDefaultWarehouseAsync();
var warehouseId = defaultWarehouse?.Id;
if (!warehouseId.HasValue) throw new InvalidOperationException("Nessun magazzino di default trovato per la spedizione");
// Genera numero documento movimento
var docNumber = await _warehouseService.GenerateDocumentNumberAsync(MovementType.Outbound);
// Trova causale di default per vendita
var reason = (await _warehouseService.GetMovementReasonsAsync(MovementType.Outbound))
.FirstOrDefault(r => r.Code == "VEN" || r.Description.Contains("Vendita"));
var movement = new StockMovement
{
DocumentNumber = docNumber,
MovementDate = DateTime.Now,
Type = MovementType.Outbound,
Status = MovementStatus.Draft,
SourceWarehouseId = warehouseId,
ReasonId = reason?.Id,
ExternalReference = order.OrderNumber,
Notes = $"Spedizione merce ordine {order.OrderNumber}"
};
movement = await _warehouseService.CreateMovementAsync(movement);
// Add lines to movement
foreach (var line in order.Lines)
{
var remainingQty = line.Quantity - line.ShippedQuantity;
if (remainingQty <= 0) continue;
// Update shipped quantity on order line
line.ShippedQuantity += remainingQty;
var movementLine = new StockMovementLine
{
MovementId = movement.Id,
ArticleId = line.WarehouseArticleId,
Quantity = remainingQty,
UnitCost = 0, // Outbound movement cost is calculated by FIFO/LIFO/Avg logic usually, but here we just set 0 or let the system handle it during confirmation
LineValue = 0
};
_db.StockMovementLines.Add(movementLine);
}
await _db.SaveChangesAsync();
// Confirm movement to update stock
await _warehouseService.ConfirmMovementAsync(movement.Id);
// Update order status
var allShipped = order.Lines.All(l => l.ShippedQuantity >= l.Quantity);
order.Status = allShipped ? SalesOrderStatus.Shipped : SalesOrderStatus.PartiallyShipped;
order.UpdatedAt = DateTime.Now;
await _db.SaveChangesAsync();
return await GetByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve order");
}
private void CalculateOrderTotals(SalesOrder 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;
}
}

View File

@@ -3,6 +3,9 @@ using Apollinare.API.Services;
// Trigger rebuild
using Apollinare.API.Services.Reports;
using Apollinare.API.Modules.Warehouse.Services;
using Apollinare.API.Modules.Purchases.Services;
using Apollinare.API.Modules.Sales.Services;
using Apollinare.API.Modules.Production.Services;
using Apollinare.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using System.Text.Json.Serialization;
@@ -27,6 +30,17 @@ builder.Services.AddSingleton<DataNotificationService>();
// Warehouse Module Services
builder.Services.AddScoped<IWarehouseService, WarehouseService>();
// Purchases Module Services
builder.Services.AddScoped<SupplierService>();
builder.Services.AddScoped<PurchaseService>();
// Sales Module Services
builder.Services.AddScoped<SalesService>();
// Production Module Services
builder.Services.AddScoped<IProductionService, ProductionService>();
builder.Services.AddScoped<IMrpService, MrpService>();
// Memory cache for module state
builder.Services.AddMemoryCache();

View File

@@ -136,6 +136,12 @@ public class AutoCodeService
"articolo" => !await _db.Articoli
.AnyAsync(a => a.Codice == code && (excludeId == null || a.Id != excludeId)),
"supplier" => !await _db.Suppliers
.AnyAsync(s => s.Code == code && (excludeId == null || s.Id != excludeId)),
"purchase_order" => !await _db.PurchaseOrders
.AnyAsync(o => o.OrderNumber == code && (excludeId == null || o.Id != excludeId)),
_ => true // Entità non gestita, assume codice unico
};
}

Binary file not shown.

View File

@@ -27,4 +27,5 @@ public class Cliente : BaseEntity
public bool Attivo { get; set; } = true;
public ICollection<Evento> Eventi { get; set; } = new List<Evento>();
public ICollection<Apollinare.Domain.Entities.Sales.SalesOrder> SalesOrders { get; set; } = new List<Apollinare.Domain.Entities.Sales.SalesOrder>();
}

View File

@@ -0,0 +1,20 @@
using Apollinare.Domain.Entities.Warehouse;
namespace Apollinare.Domain.Entities.Production;
public class BillOfMaterials : BaseEntity
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
// The article that is produced
public int ArticleId { get; set; }
public WarehouseArticle Article { get; set; } = null!;
// Quantity produced by this BOM (usually 1)
public decimal Quantity { get; set; } = 1;
public bool IsActive { get; set; } = true;
public ICollection<BillOfMaterialsComponent> Components { get; set; } = new List<BillOfMaterialsComponent>();
}

View File

@@ -0,0 +1,18 @@
using Apollinare.Domain.Entities.Warehouse;
namespace Apollinare.Domain.Entities.Production;
public class BillOfMaterialsComponent : BaseEntity
{
public int BillOfMaterialsId { get; set; }
public BillOfMaterials BillOfMaterials { get; set; } = null!;
// The raw material
public int ComponentArticleId { get; set; }
public WarehouseArticle ComponentArticle { get; set; } = null!;
public decimal Quantity { get; set; }
// Scrap percentage
public decimal ScrapPercentage { get; set; }
}

View File

@@ -0,0 +1,26 @@
using Apollinare.Domain.Entities.Warehouse;
namespace Apollinare.Domain.Entities.Production;
public class MrpSuggestion : BaseEntity
{
public DateTime CalculationDate { get; set; }
public int ArticleId { get; set; }
public WarehouseArticle Article { get; set; } = null!;
public MrpSuggestionType Type { get; set; }
public decimal Quantity { get; set; }
public DateTime SuggestionDate { get; set; }
public string Reason { get; set; } = string.Empty;
public bool IsProcessed { get; set; }
}
public enum MrpSuggestionType
{
Production = 0,
Purchase = 1
}

View File

@@ -0,0 +1,40 @@
using Apollinare.Domain.Entities.Warehouse;
namespace Apollinare.Domain.Entities.Production;
public class ProductionCycle : BaseEntity
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public int ArticleId { get; set; }
public WarehouseArticle Article { get; set; } = null!;
public bool IsDefault { get; set; }
public bool IsActive { get; set; } = true;
public ICollection<ProductionCyclePhase> Phases { get; set; } = new List<ProductionCyclePhase>();
}
public class ProductionCyclePhase : BaseEntity
{
public int ProductionCycleId { get; set; }
public ProductionCycle ProductionCycle { get; set; } = null!;
public int Sequence { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public int WorkCenterId { get; set; }
public WorkCenter WorkCenter { get; set; } = null!;
/// <summary>
/// Duration in minutes per unit produced
/// </summary>
public int DurationPerUnitMinutes { get; set; }
/// <summary>
/// Fixed setup time in minutes
/// </summary>
public int SetupTimeMinutes { get; set; }
}

View File

@@ -0,0 +1,39 @@
using Apollinare.Domain.Entities.Warehouse;
namespace Apollinare.Domain.Entities.Production;
public class ProductionOrder : BaseEntity
{
public string Code { get; set; } = string.Empty; // Auto-generated
public int ArticleId { get; set; }
public WarehouseArticle Article { get; set; } = null!;
public decimal Quantity { get; set; }
public DateTime StartDate { get; set; }
public DateTime? EndDate { get; set; }
public DateTime DueDate { get; set; }
public ProductionOrderStatus Status { get; set; } = ProductionOrderStatus.Draft;
public string? Notes { get; set; }
public ICollection<ProductionOrderComponent> Components { get; set; } = new List<ProductionOrderComponent>();
public ICollection<ProductionOrderPhase> Phases { get; set; } = new List<ProductionOrderPhase>();
// Hierarchy
public int? ParentProductionOrderId { get; set; }
public ProductionOrder? ParentProductionOrder { get; set; }
public ICollection<ProductionOrder> ChildProductionOrders { get; set; } = new List<ProductionOrder>();
}
public enum ProductionOrderStatus
{
Draft = 0,
Planned = 1,
Released = 2,
InProgress = 3,
Completed = 4,
Cancelled = 5
}

View File

@@ -0,0 +1,15 @@
using Apollinare.Domain.Entities.Warehouse;
namespace Apollinare.Domain.Entities.Production;
public class ProductionOrderComponent : BaseEntity
{
public int ProductionOrderId { get; set; }
public ProductionOrder ProductionOrder { get; set; } = null!;
public int ArticleId { get; set; }
public WarehouseArticle Article { get; set; } = null!;
public decimal RequiredQuantity { get; set; }
public decimal ConsumedQuantity { get; set; }
}

View File

@@ -0,0 +1,34 @@
using Apollinare.Domain.Entities.Warehouse;
namespace Apollinare.Domain.Entities.Production;
public class ProductionOrderPhase : BaseEntity
{
public int ProductionOrderId { get; set; }
public ProductionOrder ProductionOrder { get; set; } = null!;
public int Sequence { get; set; }
public string Name { get; set; } = string.Empty;
public int WorkCenterId { get; set; }
public WorkCenter WorkCenter { get; set; } = null!;
public ProductionPhaseStatus Status { get; set; } = ProductionPhaseStatus.Pending;
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public decimal QuantityCompleted { get; set; }
public decimal QuantityScrapped { get; set; }
public int EstimatedDurationMinutes { get; set; }
public int ActualDurationMinutes { get; set; }
}
public enum ProductionPhaseStatus
{
Pending = 0,
InProgress = 1,
Completed = 2,
Paused = 3
}

View File

@@ -0,0 +1,12 @@
using Apollinare.Domain.Entities.Warehouse;
namespace Apollinare.Domain.Entities.Production;
public class WorkCenter : BaseEntity
{
public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal CostPerHour { get; set; }
public bool IsActive { get; set; } = true;
}

View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using Apollinare.Domain.Entities;
using Apollinare.Domain.Entities.Warehouse;
namespace Apollinare.Domain.Entities.Purchases;
/// <summary>
/// Ordine di acquisto a fornitore
/// </summary>
public class PurchaseOrder : BaseEntity
{
/// <summary>
/// Numero ordine (generato automaticamente)
/// </summary>
public string OrderNumber { get; set; } = string.Empty;
/// <summary>
/// Data ordine
/// </summary>
public DateTime OrderDate { get; set; } = DateTime.Now;
/// <summary>
/// Data consegna prevista
/// </summary>
public DateTime? ExpectedDeliveryDate { get; set; }
/// <summary>
/// ID Fornitore
/// </summary>
public int SupplierId { get; set; }
/// <summary>
/// Stato dell'ordine
/// </summary>
public PurchaseOrderStatus Status { get; set; } = PurchaseOrderStatus.Draft;
/// <summary>
/// ID Magazzino di destinazione (opzionale, se null usa il default)
/// </summary>
public int? DestinationWarehouseId { get; set; }
/// <summary>
/// Note interne
/// </summary>
public string? Notes { get; set; }
/// <summary>
/// Totale imponibile (calcolato)
/// </summary>
public decimal TotalNet { get; set; }
/// <summary>
/// Totale tasse (calcolato)
/// </summary>
public decimal TotalTax { get; set; }
/// <summary>
/// Totale lordo (calcolato)
/// </summary>
public decimal TotalGross { get; set; }
// Navigation properties
public Supplier? Supplier { get; set; }
public WarehouseLocation? DestinationWarehouse { get; set; }
public ICollection<PurchaseOrderLine> Lines { get; set; } = new List<PurchaseOrderLine>();
}
public enum PurchaseOrderStatus
{
/// <summary>
/// Bozza
/// </summary>
Draft = 0,
/// <summary>
/// Confermato/Inviato al fornitore
/// </summary>
Confirmed = 1,
/// <summary>
/// Ricevuto parzialmente
/// </summary>
PartiallyReceived = 2,
/// <summary>
/// Ricevuto completamente
/// </summary>
Received = 3,
/// <summary>
/// Annullato
/// </summary>
Cancelled = 4
}

View File

@@ -0,0 +1,59 @@
using Apollinare.Domain.Entities;
using Apollinare.Domain.Entities.Warehouse;
namespace Apollinare.Domain.Entities.Purchases;
/// <summary>
/// Riga ordine di acquisto
/// </summary>
public class PurchaseOrderLine : BaseEntity
{
/// <summary>
/// ID Ordine di acquisto
/// </summary>
public int PurchaseOrderId { get; set; }
/// <summary>
/// ID Articolo di magazzino
/// </summary>
public int WarehouseArticleId { get; set; }
/// <summary>
/// Descrizione (default da articolo, ma modificabile)
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Quantità ordinata
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Quantità ricevuta
/// </summary>
public decimal ReceivedQuantity { get; set; }
/// <summary>
/// Prezzo unitario
/// </summary>
public decimal UnitPrice { get; set; }
/// <summary>
/// Aliquota IVA (percentuale)
/// </summary>
public decimal TaxRate { get; set; }
/// <summary>
/// Sconto (percentuale)
/// </summary>
public decimal DiscountPercent { get; set; }
/// <summary>
/// Totale riga (netto)
/// </summary>
public decimal LineTotal { get; set; }
// Navigation properties
public PurchaseOrder? PurchaseOrder { get; set; }
public WarehouseArticle? WarehouseArticle { get; set; }
}

View File

@@ -0,0 +1,93 @@
using System.Collections.Generic;
using Apollinare.Domain.Entities;
namespace Apollinare.Domain.Entities.Purchases;
/// <summary>
/// Fornitore di beni o servizi
/// </summary>
public class Supplier : BaseEntity
{
/// <summary>
/// Codice fornitore (generato automaticamente o manuale)
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// Ragione sociale o nome
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Partita IVA
/// </summary>
public string? VatNumber { get; set; }
/// <summary>
/// Codice Fiscale
/// </summary>
public string? FiscalCode { get; set; }
/// <summary>
/// Indirizzo
/// </summary>
public string? Address { get; set; }
/// <summary>
/// Città
/// </summary>
public string? City { get; set; }
/// <summary>
/// Provincia
/// </summary>
public string? Province { get; set; }
/// <summary>
/// CAP
/// </summary>
public string? ZipCode { get; set; }
/// <summary>
/// Nazione
/// </summary>
public string? Country { get; set; } = "Italia";
/// <summary>
/// Email principale
/// </summary>
public string? Email { get; set; }
/// <summary>
/// PEC
/// </summary>
public string? Pec { get; set; }
/// <summary>
/// Telefono
/// </summary>
public string? Phone { get; set; }
/// <summary>
/// Sito web
/// </summary>
public string? Website { get; set; }
/// <summary>
/// Termini di pagamento (descrizione testuale o riferimento a tabella pagamenti)
/// </summary>
public string? PaymentTerms { get; set; }
/// <summary>
/// Note interne
/// </summary>
public string? Notes { get; set; }
/// <summary>
/// Se attivo, il fornitore può essere utilizzato
/// </summary>
public bool IsActive { get; set; } = true;
// Navigation properties
public ICollection<PurchaseOrder> PurchaseOrders { get; set; } = new List<PurchaseOrder>();
}

View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using Apollinare.Domain.Entities;
using Apollinare.Domain.Entities.Warehouse;
namespace Apollinare.Domain.Entities.Sales;
/// <summary>
/// Ordine di vendita a cliente
/// </summary>
public class SalesOrder : BaseEntity
{
/// <summary>
/// Numero ordine (generato automaticamente)
/// </summary>
public string OrderNumber { get; set; } = string.Empty;
/// <summary>
/// Data ordine
/// </summary>
public DateTime OrderDate { get; set; } = DateTime.Now;
/// <summary>
/// Data consegna prevista
/// </summary>
public DateTime? ExpectedDeliveryDate { get; set; }
/// <summary>
/// ID Cliente
/// </summary>
public int CustomerId { get; set; }
/// <summary>
/// Stato dell'ordine
/// </summary>
public SalesOrderStatus Status { get; set; } = SalesOrderStatus.Draft;
/// <summary>
/// Note interne
/// </summary>
public string? Notes { get; set; }
/// <summary>
/// Totale imponibile (calcolato)
/// </summary>
public decimal TotalNet { get; set; }
/// <summary>
/// Totale tasse (calcolato)
/// </summary>
public decimal TotalTax { get; set; }
/// <summary>
/// Totale lordo (calcolato)
/// </summary>
public decimal TotalGross { get; set; }
// Navigation properties
public Cliente? Customer { get; set; }
public ICollection<SalesOrderLine> Lines { get; set; } = new List<SalesOrderLine>();
}
public enum SalesOrderStatus
{
/// <summary>
/// Bozza
/// </summary>
Draft = 0,
/// <summary>
/// Confermato
/// </summary>
Confirmed = 1,
/// <summary>
/// Spedito parzialmente
/// </summary>
PartiallyShipped = 2,
/// <summary>
/// Spedito completamente
/// </summary>
Shipped = 3,
/// <summary>
/// Fatturato
/// </summary>
Invoiced = 4,
/// <summary>
/// Annullato
/// </summary>
Cancelled = 5
}

View File

@@ -0,0 +1,59 @@
using Apollinare.Domain.Entities;
using Apollinare.Domain.Entities.Warehouse;
namespace Apollinare.Domain.Entities.Sales;
/// <summary>
/// Riga ordine di vendita
/// </summary>
public class SalesOrderLine : BaseEntity
{
/// <summary>
/// ID Ordine di vendita
/// </summary>
public int SalesOrderId { get; set; }
/// <summary>
/// ID Articolo di magazzino
/// </summary>
public int WarehouseArticleId { get; set; }
/// <summary>
/// Descrizione (default da articolo, ma modificabile)
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Quantità ordinata
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Quantità spedita
/// </summary>
public decimal ShippedQuantity { get; set; }
/// <summary>
/// Prezzo unitario
/// </summary>
public decimal UnitPrice { get; set; }
/// <summary>
/// Aliquota IVA (percentuale)
/// </summary>
public decimal TaxRate { get; set; }
/// <summary>
/// Sconto (percentuale)
/// </summary>
public decimal DiscountPercent { get; set; }
/// <summary>
/// Totale riga (netto)
/// </summary>
public decimal LineTotal { get; set; }
// Navigation properties
public SalesOrder? SalesOrder { get; set; }
public WarehouseArticle? WarehouseArticle { get; set; }
}

View File

@@ -1,5 +1,8 @@
using Apollinare.Domain.Entities;
using Apollinare.Domain.Entities.Warehouse;
using Apollinare.Domain.Entities.Purchases;
using Apollinare.Domain.Entities.Sales;
using Apollinare.Domain.Entities.Production;
using Microsoft.EntityFrameworkCore;
namespace Apollinare.Infrastructure.Data;
@@ -63,6 +66,26 @@ public class AppollinareDbContext : DbContext
public DbSet<InventoryCount> InventoryCounts => Set<InventoryCount>();
public DbSet<InventoryCountLine> InventoryCountLines => Set<InventoryCountLine>();
// Purchases module entities
public DbSet<Supplier> Suppliers => Set<Supplier>();
public DbSet<PurchaseOrder> PurchaseOrders => Set<PurchaseOrder>();
public DbSet<PurchaseOrderLine> PurchaseOrderLines => Set<PurchaseOrderLine>();
// Sales module entities
public DbSet<SalesOrder> SalesOrders => Set<SalesOrder>();
public DbSet<SalesOrderLine> SalesOrderLines => Set<SalesOrderLine>();
// Production module entities
public DbSet<BillOfMaterials> BillOfMaterials => Set<BillOfMaterials>();
public DbSet<BillOfMaterialsComponent> BillOfMaterialsComponents => Set<BillOfMaterialsComponent>();
public DbSet<ProductionOrder> ProductionOrders => Set<ProductionOrder>();
public DbSet<ProductionOrderComponent> ProductionOrderComponents => Set<ProductionOrderComponent>();
public DbSet<WorkCenter> WorkCenters => Set<WorkCenter>();
public DbSet<ProductionCycle> ProductionCycles => Set<ProductionCycle>();
public DbSet<ProductionCyclePhase> ProductionCyclePhases => Set<ProductionCyclePhase>();
public DbSet<ProductionOrderPhase> ProductionOrderPhases => Set<ProductionOrderPhase>();
public DbSet<MrpSuggestion> MrpSuggestions => Set<MrpSuggestion>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
@@ -627,5 +650,272 @@ public class AppollinareDbContext : DbContext
.HasForeignKey(e => e.BatchId)
.OnDelete(DeleteBehavior.SetNull);
});
// ===============================================
// PURCHASES MODULE ENTITIES
// ===============================================
// Supplier
modelBuilder.Entity<Supplier>(entity =>
{
entity.ToTable("Suppliers");
entity.HasIndex(e => e.Code).IsUnique();
entity.HasIndex(e => e.Name);
entity.HasIndex(e => e.VatNumber);
entity.HasIndex(e => e.IsActive);
});
// PurchaseOrder
modelBuilder.Entity<PurchaseOrder>(entity =>
{
entity.ToTable("PurchaseOrders");
entity.HasIndex(e => e.OrderNumber).IsUnique();
entity.HasIndex(e => e.OrderDate);
entity.HasIndex(e => e.SupplierId);
entity.HasIndex(e => e.Status);
entity.Property(e => e.TotalNet).HasPrecision(18, 4);
entity.Property(e => e.TotalTax).HasPrecision(18, 4);
entity.Property(e => e.TotalGross).HasPrecision(18, 4);
entity.HasOne(e => e.Supplier)
.WithMany(s => s.PurchaseOrders)
.HasForeignKey(e => e.SupplierId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(e => e.DestinationWarehouse)
.WithMany()
.HasForeignKey(e => e.DestinationWarehouseId)
.OnDelete(DeleteBehavior.SetNull);
});
// PurchaseOrderLine
modelBuilder.Entity<PurchaseOrderLine>(entity =>
{
entity.ToTable("PurchaseOrderLines");
entity.HasIndex(e => e.PurchaseOrderId);
entity.HasIndex(e => e.WarehouseArticleId);
entity.Property(e => e.Quantity).HasPrecision(18, 4);
entity.Property(e => e.ReceivedQuantity).HasPrecision(18, 4);
entity.Property(e => e.UnitPrice).HasPrecision(18, 4);
entity.Property(e => e.TaxRate).HasPrecision(18, 2);
entity.Property(e => e.DiscountPercent).HasPrecision(18, 2);
entity.Property(e => e.LineTotal).HasPrecision(18, 4);
entity.HasOne(e => e.PurchaseOrder)
.WithMany(o => o.Lines)
.HasForeignKey(e => e.PurchaseOrderId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.WarehouseArticle)
.WithMany()
.HasForeignKey(e => e.WarehouseArticleId)
.OnDelete(DeleteBehavior.Restrict);
});
// ===============================================
// SALES MODULE ENTITIES
// ===============================================
// SalesOrder
modelBuilder.Entity<SalesOrder>(entity =>
{
entity.ToTable("SalesOrders");
entity.HasIndex(e => e.OrderNumber).IsUnique();
entity.HasIndex(e => e.OrderDate);
entity.HasIndex(e => e.CustomerId);
entity.HasIndex(e => e.Status);
entity.Property(e => e.TotalNet).HasPrecision(18, 4);
entity.Property(e => e.TotalTax).HasPrecision(18, 4);
entity.Property(e => e.TotalGross).HasPrecision(18, 4);
entity.HasOne(e => e.Customer)
.WithMany(c => c.SalesOrders)
.HasForeignKey(e => e.CustomerId)
.OnDelete(DeleteBehavior.Restrict);
});
// SalesOrderLine
modelBuilder.Entity<SalesOrderLine>(entity =>
{
entity.ToTable("SalesOrderLines");
entity.HasIndex(e => e.SalesOrderId);
entity.HasIndex(e => e.WarehouseArticleId);
entity.Property(e => e.Quantity).HasPrecision(18, 4);
entity.Property(e => e.ShippedQuantity).HasPrecision(18, 4);
entity.Property(e => e.UnitPrice).HasPrecision(18, 4);
entity.Property(e => e.TaxRate).HasPrecision(18, 2);
entity.Property(e => e.DiscountPercent).HasPrecision(18, 2);
entity.Property(e => e.LineTotal).HasPrecision(18, 4);
entity.HasOne(e => e.SalesOrder)
.WithMany(o => o.Lines)
.HasForeignKey(e => e.SalesOrderId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.WarehouseArticle)
.WithMany()
.HasForeignKey(e => e.WarehouseArticleId)
.OnDelete(DeleteBehavior.Restrict);
});
// ===============================================
// PRODUCTION MODULE ENTITIES
// ===============================================
// BillOfMaterials
modelBuilder.Entity<BillOfMaterials>(entity =>
{
entity.ToTable("BillOfMaterials");
entity.HasIndex(e => e.ArticleId);
entity.HasIndex(e => e.IsActive);
entity.Property(e => e.Quantity).HasPrecision(18, 4);
entity.HasOne(e => e.Article)
.WithMany()
.HasForeignKey(e => e.ArticleId)
.OnDelete(DeleteBehavior.Restrict);
});
// BillOfMaterialsComponent
modelBuilder.Entity<BillOfMaterialsComponent>(entity =>
{
entity.ToTable("BillOfMaterialsComponents");
entity.HasIndex(e => e.BillOfMaterialsId);
entity.HasIndex(e => e.ComponentArticleId);
entity.Property(e => e.Quantity).HasPrecision(18, 4);
entity.Property(e => e.ScrapPercentage).HasPrecision(18, 2);
entity.HasOne(e => e.BillOfMaterials)
.WithMany(b => b.Components)
.HasForeignKey(e => e.BillOfMaterialsId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.ComponentArticle)
.WithMany()
.HasForeignKey(e => e.ComponentArticleId)
.OnDelete(DeleteBehavior.Restrict);
});
// ProductionOrder
modelBuilder.Entity<ProductionOrder>(entity =>
{
entity.ToTable("ProductionOrders");
entity.HasIndex(e => e.Code).IsUnique();
entity.HasIndex(e => e.ArticleId);
entity.HasIndex(e => e.Status);
entity.HasIndex(e => e.StartDate);
entity.Property(e => e.Quantity).HasPrecision(18, 4);
entity.HasOne(e => e.Article)
.WithMany()
.HasForeignKey(e => e.ArticleId)
.OnDelete(DeleteBehavior.Restrict);
});
// ProductionOrderComponent
modelBuilder.Entity<ProductionOrderComponent>(entity =>
{
entity.ToTable("ProductionOrderComponents");
entity.HasIndex(e => e.ProductionOrderId);
entity.HasIndex(e => e.ArticleId);
entity.Property(e => e.RequiredQuantity).HasPrecision(18, 4);
entity.Property(e => e.ConsumedQuantity).HasPrecision(18, 4);
entity.HasOne(e => e.ProductionOrder)
.WithMany(o => o.Components)
.HasForeignKey(e => e.ProductionOrderId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.Article)
.WithMany()
.HasForeignKey(e => e.ArticleId)
.OnDelete(DeleteBehavior.Restrict);
});
// WorkCenter
modelBuilder.Entity<WorkCenter>(entity =>
{
entity.ToTable("WorkCenters");
entity.HasIndex(e => e.Code).IsUnique();
entity.HasIndex(e => e.IsActive);
entity.Property(e => e.CostPerHour).HasPrecision(18, 4);
});
// ProductionCycle
modelBuilder.Entity<ProductionCycle>(entity =>
{
entity.ToTable("ProductionCycles");
entity.HasIndex(e => e.ArticleId);
entity.HasIndex(e => e.IsActive);
entity.HasOne(e => e.Article)
.WithMany()
.HasForeignKey(e => e.ArticleId)
.OnDelete(DeleteBehavior.Restrict);
});
// ProductionCyclePhase
modelBuilder.Entity<ProductionCyclePhase>(entity =>
{
entity.ToTable("ProductionCyclePhases");
entity.HasIndex(e => e.ProductionCycleId);
entity.HasIndex(e => e.WorkCenterId);
entity.HasOne(e => e.ProductionCycle)
.WithMany(c => c.Phases)
.HasForeignKey(e => e.ProductionCycleId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.WorkCenter)
.WithMany()
.HasForeignKey(e => e.WorkCenterId)
.OnDelete(DeleteBehavior.Restrict);
});
// ProductionOrderPhase
modelBuilder.Entity<ProductionOrderPhase>(entity =>
{
entity.ToTable("ProductionOrderPhases");
entity.HasIndex(e => e.ProductionOrderId);
entity.HasIndex(e => e.WorkCenterId);
entity.HasIndex(e => e.Status);
entity.Property(e => e.QuantityCompleted).HasPrecision(18, 4);
entity.Property(e => e.QuantityScrapped).HasPrecision(18, 4);
entity.HasOne(e => e.ProductionOrder)
.WithMany(o => o.Phases)
.HasForeignKey(e => e.ProductionOrderId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.WorkCenter)
.WithMany()
.HasForeignKey(e => e.WorkCenterId)
.OnDelete(DeleteBehavior.Restrict);
});
// MrpSuggestion
modelBuilder.Entity<MrpSuggestion>(entity =>
{
entity.ToTable("MrpSuggestions");
entity.HasIndex(e => e.ArticleId);
entity.HasIndex(e => e.CalculationDate);
entity.HasIndex(e => e.IsProcessed);
entity.Property(e => e.Quantity).HasPrecision(18, 4);
entity.HasOne(e => e.Article)
.WithMany()
.HasForeignKey(e => e.ArticleId)
.OnDelete(DeleteBehavior.Cascade);
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,195 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Apollinare.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddPurchasesModule : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Suppliers",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Code = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
VatNumber = table.Column<string>(type: "TEXT", nullable: true),
FiscalCode = table.Column<string>(type: "TEXT", nullable: true),
Address = table.Column<string>(type: "TEXT", nullable: true),
City = table.Column<string>(type: "TEXT", nullable: true),
Province = table.Column<string>(type: "TEXT", nullable: true),
ZipCode = table.Column<string>(type: "TEXT", nullable: true),
Country = table.Column<string>(type: "TEXT", nullable: true),
Email = table.Column<string>(type: "TEXT", nullable: true),
Pec = table.Column<string>(type: "TEXT", nullable: true),
Phone = table.Column<string>(type: "TEXT", nullable: true),
Website = table.Column<string>(type: "TEXT", nullable: true),
PaymentTerms = table.Column<string>(type: "TEXT", nullable: true),
Notes = table.Column<string>(type: "TEXT", nullable: true),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Suppliers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PurchaseOrders",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
OrderNumber = table.Column<string>(type: "TEXT", nullable: false),
OrderDate = table.Column<DateTime>(type: "TEXT", nullable: false),
ExpectedDeliveryDate = table.Column<DateTime>(type: "TEXT", nullable: true),
SupplierId = table.Column<int>(type: "INTEGER", nullable: false),
Status = table.Column<int>(type: "INTEGER", nullable: false),
DestinationWarehouseId = table.Column<int>(type: "INTEGER", nullable: true),
Notes = table.Column<string>(type: "TEXT", nullable: true),
TotalNet = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
TotalTax = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
TotalGross = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseOrders", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseOrders_Suppliers_SupplierId",
column: x => x.SupplierId,
principalTable: "Suppliers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_PurchaseOrders_WarehouseLocations_DestinationWarehouseId",
column: x => x.DestinationWarehouseId,
principalTable: "WarehouseLocations",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "PurchaseOrderLines",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
PurchaseOrderId = table.Column<int>(type: "INTEGER", nullable: false),
WarehouseArticleId = table.Column<int>(type: "INTEGER", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: false),
Quantity = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
ReceivedQuantity = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
UnitPrice = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
TaxRate = table.Column<decimal>(type: "TEXT", precision: 18, scale: 2, nullable: false),
DiscountPercent = table.Column<decimal>(type: "TEXT", precision: 18, scale: 2, nullable: false),
LineTotal = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseOrderLines", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseOrderLines_PurchaseOrders_PurchaseOrderId",
column: x => x.PurchaseOrderId,
principalTable: "PurchaseOrders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PurchaseOrderLines_WarehouseArticles_WarehouseArticleId",
column: x => x.WarehouseArticleId,
principalTable: "WarehouseArticles",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_PurchaseOrderLines_PurchaseOrderId",
table: "PurchaseOrderLines",
column: "PurchaseOrderId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseOrderLines_WarehouseArticleId",
table: "PurchaseOrderLines",
column: "WarehouseArticleId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseOrders_DestinationWarehouseId",
table: "PurchaseOrders",
column: "DestinationWarehouseId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseOrders_OrderDate",
table: "PurchaseOrders",
column: "OrderDate");
migrationBuilder.CreateIndex(
name: "IX_PurchaseOrders_OrderNumber",
table: "PurchaseOrders",
column: "OrderNumber",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PurchaseOrders_Status",
table: "PurchaseOrders",
column: "Status");
migrationBuilder.CreateIndex(
name: "IX_PurchaseOrders_SupplierId",
table: "PurchaseOrders",
column: "SupplierId");
migrationBuilder.CreateIndex(
name: "IX_Suppliers_Code",
table: "Suppliers",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Suppliers_IsActive",
table: "Suppliers",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_Suppliers_Name",
table: "Suppliers",
column: "Name");
migrationBuilder.CreateIndex(
name: "IX_Suppliers_VatNumber",
table: "Suppliers",
column: "VatNumber");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PurchaseOrderLines");
migrationBuilder.DropTable(
name: "PurchaseOrders");
migrationBuilder.DropTable(
name: "Suppliers");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,126 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Apollinare.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddSalesModule : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SalesOrders",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
OrderNumber = table.Column<string>(type: "TEXT", nullable: false),
OrderDate = table.Column<DateTime>(type: "TEXT", nullable: false),
ExpectedDeliveryDate = table.Column<DateTime>(type: "TEXT", nullable: true),
CustomerId = table.Column<int>(type: "INTEGER", nullable: false),
Status = table.Column<int>(type: "INTEGER", nullable: false),
Notes = table.Column<string>(type: "TEXT", nullable: true),
TotalNet = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
TotalTax = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
TotalGross = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SalesOrders", x => x.Id);
table.ForeignKey(
name: "FK_SalesOrders_Clienti_CustomerId",
column: x => x.CustomerId,
principalTable: "Clienti",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "SalesOrderLines",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SalesOrderId = table.Column<int>(type: "INTEGER", nullable: false),
WarehouseArticleId = table.Column<int>(type: "INTEGER", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: false),
Quantity = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
ShippedQuantity = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
UnitPrice = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
TaxRate = table.Column<decimal>(type: "TEXT", precision: 18, scale: 2, nullable: false),
DiscountPercent = table.Column<decimal>(type: "TEXT", precision: 18, scale: 2, nullable: false),
LineTotal = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SalesOrderLines", x => x.Id);
table.ForeignKey(
name: "FK_SalesOrderLines_SalesOrders_SalesOrderId",
column: x => x.SalesOrderId,
principalTable: "SalesOrders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SalesOrderLines_WarehouseArticles_WarehouseArticleId",
column: x => x.WarehouseArticleId,
principalTable: "WarehouseArticles",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_SalesOrderLines_SalesOrderId",
table: "SalesOrderLines",
column: "SalesOrderId");
migrationBuilder.CreateIndex(
name: "IX_SalesOrderLines_WarehouseArticleId",
table: "SalesOrderLines",
column: "WarehouseArticleId");
migrationBuilder.CreateIndex(
name: "IX_SalesOrders_CustomerId",
table: "SalesOrders",
column: "CustomerId");
migrationBuilder.CreateIndex(
name: "IX_SalesOrders_OrderDate",
table: "SalesOrders",
column: "OrderDate");
migrationBuilder.CreateIndex(
name: "IX_SalesOrders_OrderNumber",
table: "SalesOrders",
column: "OrderNumber",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_SalesOrders_Status",
table: "SalesOrders",
column: "Status");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SalesOrderLines");
migrationBuilder.DropTable(
name: "SalesOrders");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,207 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Apollinare.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddProductionModule : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "BillOfMaterials",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: false),
ArticleId = table.Column<int>(type: "INTEGER", nullable: false),
Quantity = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_BillOfMaterials", x => x.Id);
table.ForeignKey(
name: "FK_BillOfMaterials_WarehouseArticles_ArticleId",
column: x => x.ArticleId,
principalTable: "WarehouseArticles",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "ProductionOrders",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Code = table.Column<string>(type: "TEXT", nullable: false),
ArticleId = table.Column<int>(type: "INTEGER", nullable: false),
Quantity = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
StartDate = table.Column<DateTime>(type: "TEXT", nullable: false),
EndDate = table.Column<DateTime>(type: "TEXT", nullable: true),
DueDate = table.Column<DateTime>(type: "TEXT", nullable: false),
Status = table.Column<int>(type: "INTEGER", nullable: false),
Notes = table.Column<string>(type: "TEXT", nullable: true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ProductionOrders", x => x.Id);
table.ForeignKey(
name: "FK_ProductionOrders_WarehouseArticles_ArticleId",
column: x => x.ArticleId,
principalTable: "WarehouseArticles",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "BillOfMaterialsComponents",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
BillOfMaterialsId = table.Column<int>(type: "INTEGER", nullable: false),
ComponentArticleId = table.Column<int>(type: "INTEGER", nullable: false),
Quantity = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
ScrapPercentage = table.Column<decimal>(type: "TEXT", precision: 18, scale: 2, nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_BillOfMaterialsComponents", x => x.Id);
table.ForeignKey(
name: "FK_BillOfMaterialsComponents_BillOfMaterials_BillOfMaterialsId",
column: x => x.BillOfMaterialsId,
principalTable: "BillOfMaterials",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BillOfMaterialsComponents_WarehouseArticles_ComponentArticleId",
column: x => x.ComponentArticleId,
principalTable: "WarehouseArticles",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "ProductionOrderComponents",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ProductionOrderId = table.Column<int>(type: "INTEGER", nullable: false),
ArticleId = table.Column<int>(type: "INTEGER", nullable: false),
RequiredQuantity = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
ConsumedQuantity = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ProductionOrderComponents", x => x.Id);
table.ForeignKey(
name: "FK_ProductionOrderComponents_ProductionOrders_ProductionOrderId",
column: x => x.ProductionOrderId,
principalTable: "ProductionOrders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ProductionOrderComponents_WarehouseArticles_ArticleId",
column: x => x.ArticleId,
principalTable: "WarehouseArticles",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_BillOfMaterials_ArticleId",
table: "BillOfMaterials",
column: "ArticleId");
migrationBuilder.CreateIndex(
name: "IX_BillOfMaterials_IsActive",
table: "BillOfMaterials",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_BillOfMaterialsComponents_BillOfMaterialsId",
table: "BillOfMaterialsComponents",
column: "BillOfMaterialsId");
migrationBuilder.CreateIndex(
name: "IX_BillOfMaterialsComponents_ComponentArticleId",
table: "BillOfMaterialsComponents",
column: "ComponentArticleId");
migrationBuilder.CreateIndex(
name: "IX_ProductionOrderComponents_ArticleId",
table: "ProductionOrderComponents",
column: "ArticleId");
migrationBuilder.CreateIndex(
name: "IX_ProductionOrderComponents_ProductionOrderId",
table: "ProductionOrderComponents",
column: "ProductionOrderId");
migrationBuilder.CreateIndex(
name: "IX_ProductionOrders_ArticleId",
table: "ProductionOrders",
column: "ArticleId");
migrationBuilder.CreateIndex(
name: "IX_ProductionOrders_Code",
table: "ProductionOrders",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ProductionOrders_StartDate",
table: "ProductionOrders",
column: "StartDate");
migrationBuilder.CreateIndex(
name: "IX_ProductionOrders_Status",
table: "ProductionOrders",
column: "Status");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BillOfMaterialsComponents");
migrationBuilder.DropTable(
name: "ProductionOrderComponents");
migrationBuilder.DropTable(
name: "BillOfMaterials");
migrationBuilder.DropTable(
name: "ProductionOrders");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,251 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Apollinare.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddAdvancedProduction : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "MrpSuggestions",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
CalculationDate = table.Column<DateTime>(type: "TEXT", nullable: false),
ArticleId = table.Column<int>(type: "INTEGER", nullable: false),
Type = table.Column<int>(type: "INTEGER", nullable: false),
Quantity = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
SuggestionDate = table.Column<DateTime>(type: "TEXT", nullable: false),
Reason = table.Column<string>(type: "TEXT", nullable: false),
IsProcessed = table.Column<bool>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MrpSuggestions", x => x.Id);
table.ForeignKey(
name: "FK_MrpSuggestions_WarehouseArticles_ArticleId",
column: x => x.ArticleId,
principalTable: "WarehouseArticles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ProductionCycles",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: true),
ArticleId = table.Column<int>(type: "INTEGER", nullable: false),
IsDefault = table.Column<bool>(type: "INTEGER", nullable: false),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ProductionCycles", x => x.Id);
table.ForeignKey(
name: "FK_ProductionCycles_WarehouseArticles_ArticleId",
column: x => x.ArticleId,
principalTable: "WarehouseArticles",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "WorkCenters",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Code = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: true),
CostPerHour = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_WorkCenters", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ProductionCyclePhases",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ProductionCycleId = table.Column<int>(type: "INTEGER", nullable: false),
Sequence = table.Column<int>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: true),
WorkCenterId = table.Column<int>(type: "INTEGER", nullable: false),
DurationPerUnitMinutes = table.Column<int>(type: "INTEGER", nullable: false),
SetupTimeMinutes = table.Column<int>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ProductionCyclePhases", x => x.Id);
table.ForeignKey(
name: "FK_ProductionCyclePhases_ProductionCycles_ProductionCycleId",
column: x => x.ProductionCycleId,
principalTable: "ProductionCycles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ProductionCyclePhases_WorkCenters_WorkCenterId",
column: x => x.WorkCenterId,
principalTable: "WorkCenters",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "ProductionOrderPhases",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ProductionOrderId = table.Column<int>(type: "INTEGER", nullable: false),
Sequence = table.Column<int>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
WorkCenterId = table.Column<int>(type: "INTEGER", nullable: false),
Status = table.Column<int>(type: "INTEGER", nullable: false),
StartDate = table.Column<DateTime>(type: "TEXT", nullable: true),
EndDate = table.Column<DateTime>(type: "TEXT", nullable: true),
QuantityCompleted = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
QuantityScrapped = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
EstimatedDurationMinutes = table.Column<int>(type: "INTEGER", nullable: false),
ActualDurationMinutes = table.Column<int>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ProductionOrderPhases", x => x.Id);
table.ForeignKey(
name: "FK_ProductionOrderPhases_ProductionOrders_ProductionOrderId",
column: x => x.ProductionOrderId,
principalTable: "ProductionOrders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ProductionOrderPhases_WorkCenters_WorkCenterId",
column: x => x.WorkCenterId,
principalTable: "WorkCenters",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_MrpSuggestions_ArticleId",
table: "MrpSuggestions",
column: "ArticleId");
migrationBuilder.CreateIndex(
name: "IX_MrpSuggestions_CalculationDate",
table: "MrpSuggestions",
column: "CalculationDate");
migrationBuilder.CreateIndex(
name: "IX_MrpSuggestions_IsProcessed",
table: "MrpSuggestions",
column: "IsProcessed");
migrationBuilder.CreateIndex(
name: "IX_ProductionCyclePhases_ProductionCycleId",
table: "ProductionCyclePhases",
column: "ProductionCycleId");
migrationBuilder.CreateIndex(
name: "IX_ProductionCyclePhases_WorkCenterId",
table: "ProductionCyclePhases",
column: "WorkCenterId");
migrationBuilder.CreateIndex(
name: "IX_ProductionCycles_ArticleId",
table: "ProductionCycles",
column: "ArticleId");
migrationBuilder.CreateIndex(
name: "IX_ProductionCycles_IsActive",
table: "ProductionCycles",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_ProductionOrderPhases_ProductionOrderId",
table: "ProductionOrderPhases",
column: "ProductionOrderId");
migrationBuilder.CreateIndex(
name: "IX_ProductionOrderPhases_Status",
table: "ProductionOrderPhases",
column: "Status");
migrationBuilder.CreateIndex(
name: "IX_ProductionOrderPhases_WorkCenterId",
table: "ProductionOrderPhases",
column: "WorkCenterId");
migrationBuilder.CreateIndex(
name: "IX_WorkCenters_Code",
table: "WorkCenters",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_WorkCenters_IsActive",
table: "WorkCenters",
column: "IsActive");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MrpSuggestions");
migrationBuilder.DropTable(
name: "ProductionCyclePhases");
migrationBuilder.DropTable(
name: "ProductionOrderPhases");
migrationBuilder.DropTable(
name: "ProductionCycles");
migrationBuilder.DropTable(
name: "WorkCenters");
}
}
}