-
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
27
src/Apollinare.API/Modules/Production/Dtos/WorkCenterDto.cs
Normal file
27
src/Apollinare.API/Modules/Production/Dtos/WorkCenterDto.cs
Normal 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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
260
src/Apollinare.API/Modules/Production/Services/MrpService.cs
Normal file
260
src/Apollinare.API/Modules/Production/Services/MrpService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user