using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; using MMDAL.CustomCode.Indent; using MMDAL.CustomCode.MRPRun; using MMDAL.CustomCode.PartLevel; using MMDAL.DTO.Indent; using MMDAL.DTO.PartLevel; using MMDAL.Query.Indent; using Newtonsoft.Json; using static GB5Shared.GB5Constant.Constant; namespace MMBLL.Indent { public class IndentBLL : IIndentBLL { private readonly IIndentDAL _IndentDAL; private readonly IPartDetailDAL _PartDetailDAL; private readonly IPartPositionDAL _PartPositionDAL; private readonly IMRPRunDAL _MRPRunDAL; private readonly AutoNumber _AutoNumber; private readonly IQueryExecutor _QueryExecutor; private readonly KeyInvalidate _KeyInvalidate; private readonly BaseEntityAppService _BaseEntityAppService; private readonly ILogger _Logger; private readonly VoucherNumberService _voucherNumberService; public IndentBLL( IIndentDAL indentDAL, IPartDetailDAL partDetailDAL, IPartPositionDAL partPositionDAL, IMRPRunDAL mrpRunDAL, AutoNumber autoNumber, IQueryExecutor queryExecutor, KeyInvalidate keyInvalidate, BaseEntityAppService baseEntityAppService, ILogger logger, VoucherNumberService voucherNumberService) { _IndentDAL = indentDAL; _PartDetailDAL = partDetailDAL; _PartPositionDAL = partPositionDAL; _MRPRunDAL = mrpRunDAL; _AutoNumber = autoNumber; _QueryExecutor = queryExecutor; _KeyInvalidate = keyInvalidate; _BaseEntityAppService = baseEntityAppService; _Logger = logger; _voucherNumberService = voucherNumberService; } // ── Picklist (existing) ─────────────────────────────────────────────── public async Task GetSelectListIndent(int FirstNumber, int MaxResult, CriteriaDTO CriteriaDTO, LoginDTO LoginDTO) { try { return await _IndentDAL.GetSelectListIndent(FirstNumber, MaxResult, CriteriaDTO, LoginDTO); } catch (Exception ex) { throw new Exception($"Error in IndentBLL.GetSelectListIndent: {ex.Message}", ex); } } public async Task AmendmentSelectList(int FirstNumber, int MaxResult, CriteriaDTO CriteriaDTO, LoginDTO LoginDTO) { try { return await _IndentDAL.AmendmentSelectList(FirstNumber, MaxResult, CriteriaDTO, LoginDTO); } catch (Exception ex) { throw new Exception($"Error in IndentBLL.AmendmentSelectList: {ex.Message}", ex); } } public async Task GetCombineIndentSelectList(int FirstNumber, int MaxResult, CriteriaDTO CriteriaDTO, LoginDTO LoginDTO) { try { return await _IndentDAL.GetCombineIndentSelectList(FirstNumber, MaxResult, CriteriaDTO, LoginDTO); } catch (Exception ex) { throw new Exception($"Error in IndentBLL.GetCombineIndentSelectList: {ex.Message}", ex); } } public async Task GetIndentReportSelectList(int FirstNumber, int MaxResult, CriteriaDTO CriteriaDTO, LoginDTO LoginDTO) { try { return await _IndentDAL.GetIndentReportSelectList(FirstNumber, MaxResult, CriteriaDTO, LoginDTO); } catch (Exception ex) { throw new Exception($"Error in IndentBLL.GetIndentReportSelectList: {ex.Message}", ex); } } // ── Phase 1: Get single indent ──────────────────────────────────────── public async Task GetIndent(int indentId, LoginDTO login, CancellationToken ct) { var header = await _IndentDAL.GetIndentHeader(indentId, login, ct).ConfigureAwait(false); if (header == null) return JsonConvert.SerializeObject(null); header.Details = (await _IndentDAL.GetIndentDetails(indentId, login, ct).ConfigureAwait(false)).ToList(); return JsonConvert.SerializeObject(header); } // ── Phase 1: Paged list ─────────────────────────────────────────────── public async Task GetIndentList(int ouId, int firstNumber, int maxResult, LoginDTO login, CancellationToken ct) { var items = await _IndentDAL.GetIndentList(ouId, firstNumber, maxResult, login, ct).ConfigureAwait(false); return JsonConvert.SerializeObject(items); } // ── Phase 1: Save (create + update) ────────────────────────────────── public async Task SaveIndent(IndentSaveDTO dto, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-indent", new { dto.IndentId }); if (dto.BizTransactionTypeId == 0) throw new ArgumentException("BizTransactionTypeId is required."); if (dto.OUId == 0) throw new ArgumentException("OUId is required."); if (dto.PeriodId == 0) throw new ArgumentException("PeriodId is required."); if (dto.Details.Count == 0) throw new ArgumentException("At least one work order detail is required."); foreach (var d in dto.Details) { if (d.IndentDetailQuantity <= 0) throw new ArgumentException($"Detail SlNo {d.SlNo}: Quantity must be > 0."); if (d.ItemId == 0 && d.RoutingId == 0) throw new ArgumentException($"Detail SlNo {d.SlNo}: ItemId or RoutingId is required."); } bool isNew = dto.IndentId == 0; if (isNew) { string? indentNumber = await _voucherNumberService.GetNextVoucherNumberAsync( dto.BizTransactionTypeId, dto.IndentDate, login, transaction: null); if (string.IsNullOrWhiteSpace(indentNumber)) { throw new InvalidOperationException( "Indent number could not be generated."); } dto.IndentNumber = indentNumber; } // Phase 4: auto-assign routing resources BEFORE counting await AssignResourcesFromRoutingAsync(dto.Details, login, ct).ConfigureAwait(false); // Pre-assign all IDs before transaction int detailCount = dto.Details.Count; int materialCount = dto.Details.Sum(d => d.Materials.Count); int resourceCount = dto.Details.Sum(d => d.Resources.Count); if (isNew) { var headerNum = await _AutoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.INDENT, login).ConfigureAwait(false); dto.IndentId = headerNum.StartNumber; } var detailNum = await _AutoNumber.GetNumberAsync(detailCount, AUTONUMBERCONSTANT.INDENT, login).ConfigureAwait(false); int nextDetailId = detailNum.StartNumber; AutoNumberDTO? materialNum = materialCount > 0 ? await _AutoNumber.GetNumberAsync(materialCount, AUTONUMBERCONSTANT.INDENT, login).ConfigureAwait(false) : null; int nextMaterialId = materialNum?.StartNumber ?? 0; AutoNumberDTO? resourceNum = resourceCount > 0 ? await _AutoNumber.GetNumberAsync(resourceCount, AUTONUMBERCONSTANT.INDENTRESOURCE, login).ConfigureAwait(false) : null; int nextResourceId = resourceNum?.StartNumber ?? 0; var partDetailNum = await _AutoNumber.GetNumberAsync(detailCount, AUTONUMBERCONSTANT.INDENTRESOURCE, login).ConfigureAwait(false); int nextPartDetailId = partDetailNum.StartNumber; // Build InsertDTOs with pre-assigned IDs var detailRows = new List(detailCount); var materialRows = new List(materialCount); var resourceRows = new List(resourceCount); var partDetailDTOs = new List<(PartDetailDTO Dto, int SlNo)>(detailCount); for (short i = 0; i < dto.Details.Count; i++) { var d = dto.Details[i]; d.IndentDetailId = nextDetailId++; d.IndentId = dto.IndentId; d.SlNo = (short)(i + 1); detailRows.Add(new IndentDetailInsertDTO { IndentDetailId = d.IndentDetailId, IndentId = dto.IndentId, SlNo = d.SlNo, LinkTypeId = d.LinkTypeId, MMDetailDocumentDetailId = d.MMDetailDocumentDetailId, IndentDetailDocumentId = d.IndentDetailDocumentId, ProcessGroupId = d.ProcessGroupId, WorkPlanDetailId = d.WorkPlanDetailId, ItemId = d.ItemId, SkuId = d.SkuId, RoutingId = d.RoutingId, RoutingDetailId = d.RoutingDetailId, BomId = d.BomId, MachineId = d.MachineId, MachineTypeId = d.MachineTypeId, NestingPlanId = d.NestingPlanId, AllocationId = d.AllocationId, CallId = d.CallId, TaskId = d.TaskId, GenerateAllocationId = d.GenerateAllocationId, BeamLotId = d.BeamLotId, IndentDetailQuantity = d.IndentDetailQuantity, PlannedQuantity = d.PlannedQuantity, Rate = d.Rate, Priority = d.Priority, Remarks = d.Remarks, IndentDetailReleaseDate = d.IndentDetailReleaseDate, ReleaseStatus = d.ReleaseStatus, IndentBeamType = d.IndentBeamType, RevisedWidth = d.RevisedWidth, MetersPerPiece = d.MetersPerPiece, WeavePick = d.WeavePick, WeaveWidth = d.WeaveWidth, AllotedGoodQuantity = d.AllotedGoodQuantity, AllotedReworkQuantity = d.AllotedReworkQuantity, NextLevelQuantity = d.NextLevelQuantity, PerQuantity = d.PerQuantity }); foreach (var m in d.Materials) { materialRows.Add(new IndentMaterialInsertDTO { IndentMaterialId = nextMaterialId++, IndentDetailId = d.IndentDetailId, SlNo = m.SlNo, ProcessId = m.ProcessId, Level = m.Level, ProcessForItemId = m.ProcessForItemId, ProcessForQty = m.ProcessForQty, MaterialSlNo = m.MaterialSlNo, ItemId = m.ItemId, Type = m.Type, SkuId = m.SkuId, RequiredQuantity = m.RequiredQuantity, MaterialRequiredPer = m.MaterialRequiredPer, TotalQuantity = m.TotalQuantity, WastePercentage = m.WastePercentage, NetQuantity = m.NetQuantity, UOMId = m.UOMId, Remarks = m.Remarks, AdjustQuantity = m.AdjustQuantity, FinalQuantity = m.FinalQuantity, TypeOfData = m.TypeOfData, Ends = m.Ends, RejectedQuantity = m.RejectedQuantity, ReworkQuantity = m.ReworkQuantity, OtherQuantity = m.OtherQuantity }); } foreach (var r in d.Resources) { resourceRows.Add(new IndentResourceInsertDTO { IndentResourceId = nextResourceId++, IndentDetailId = d.IndentDetailId, SlNo = r.SlNo, BillOfResourceId = r.BillOfResourceId, ResourceTypeId = r.ResourceTypeId, MachineId = r.MachineId, EmployeeId = r.EmployeeId, PlanDuration = r.PlanDuration, PlanStartDate = r.PlanStartDate, PlanStartTime = r.PlanStartTime, PlanEndDate = r.PlanEndDate, PlanEndTime = r.PlanEndTime, ResourceId = r.ResourceId, CalendarDetailId = r.CalendarDetailId, PlannedQuantity = r.PlannedQuantity, PerQuantity = r.PerQuantity, TimeUOM = r.TimeUOM, Remarks = r.Remarks }); } partDetailDTOs.Add((new PartDetailDTO { PartDetailId = nextPartDetailId++, EntityTypeId = 2, EntityId = dto.IndentId, EntityDetailId = d.IndentDetailId, ParentPartDetailId = d.ParentPartDetailId, RootPartDetailId = d.RootPartDetailId, ItemId = d.ItemId, RequiredQty = d.IndentDetailQuantity, AllocationId = d.AllocationId, PlannedDueDate = d.IndentDetailReleaseDate, TenantId = login.ClientId }, d.SlNo)); } GB5Trace.Step("save-indent", new { dto.IndentId, isNew }); var trans = await _QueryExecutor.BeginTransactionAsync(login); try { if (isNew) await _IndentDAL.SaveIndentHeader(dto, login, trans, ct).ConfigureAwait(false); else { await _IndentDAL.UpdateIndentHeader(dto, login, trans, ct).ConfigureAwait(false); await _IndentDAL.DeleteIndentDetails(dto.IndentId, login, trans, ct).ConfigureAwait(false); await _PartDetailDAL.DeletePartDetailsByEntity(2, dto.IndentId, login, trans, ct).ConfigureAwait(false); } await _IndentDAL.BulkSaveIndentDetails(detailRows, login, trans, ct).ConfigureAwait(false); if (materialRows.Count > 0) await _IndentDAL.BulkSaveIndentMaterials(materialRows, login, trans, ct).ConfigureAwait(false); if (resourceRows.Count > 0) await _IndentDAL.BulkSaveIndentResources(resourceRows, login, trans, ct).ConfigureAwait(false); //foreach (var (pdDto, _) in partDetailDTOs) // await _PartDetailDAL.SavePartDetail(pdDto, login, trans, ct).ConfigureAwait(false); await _QueryExecutor.CommitAsync(trans).ConfigureAwait(false); } catch { await _QueryExecutor.RollbackAsync(trans).ConfigureAwait(false); throw; } GB5Trace.Step("event-publish", new { WebServiceConstant.MMSINDENTSAVE }); await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTINDENT, WebServiceConstant.MMSINDENTSAVE, dto, login, _ => Task.FromResult(dto.IndentId), new Dictionary { { "IndentId", dto.IndentId } }); var keyGen = new CacheKeyGeneration(); var cacheKey = keyGen.KeyGeneration(dto.IndentId, EntityConstant.OBJECTINDENT, CacheKeyLevel.CLIENT_LEVEL, login); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.IndentId}" : SuccessResponse.UpdateSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("save-indent-failed", ex); _Logger.LogError(ex, "SaveIndent failed for IndentId {IndentId}", dto.IndentId); throw; } } // ── Phase 1: Delete ─────────────────────────────────────────────────── public async Task DeleteIndent(int indentId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("delete-indent", new { indentId }); var trans = await _QueryExecutor.BeginTransactionAsync(login); try { await _IndentDAL.DeleteIndentDetails(indentId, login, trans, ct).ConfigureAwait(false); await _PartDetailDAL.DeletePartDetailsByEntity(2, indentId, login, trans, ct).ConfigureAwait(false); await _IndentDAL.SoftDeleteIndent(indentId, login, trans, ct).ConfigureAwait(false); await _QueryExecutor.CommitAsync(trans).ConfigureAwait(false); } catch { await _QueryExecutor.RollbackAsync(trans).ConfigureAwait(false); throw; } GB5Trace.Step("event-publish", new { WebServiceConstant.MMSINDENTDELETE }); await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTINDENT, WebServiceConstant.MMSINDENTDELETE, new IndentSaveDTO { IndentId = indentId }, login, _ => Task.FromResult(indentId), new Dictionary { { "IndentId", indentId } }) .ConfigureAwait(false); var keyGen = new CacheKeyGeneration(); var cacheKey = keyGen.KeyGeneration(indentId, EntityConstant.OBJECTINDENT, CacheKeyLevel.CLIENT_LEVEL, login); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("delete-indent-failed", ex); _Logger.LogError(ex, "DeleteIndent failed for IndentId {IndentId}", indentId); throw; } } // ── Phase 2: SO line lookup ──────────────────────────────────────────── public async Task GetSOLineForWO(int mmDetailId, LoginDTO login, CancellationToken ct) { var result = await _IndentDAL.GetSOLineForWO(mmDetailId, login, ct).ConfigureAwait(false); return JsonConvert.SerializeObject(result); } public async Task GenerateWorkOrderFromSO(int mmDetailId, IndentGenerationParamsDTO genParams, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("generate-wo-from-so", new { mmDetailId }); var soLine = await _IndentDAL.GetSOLineForWO(mmDetailId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"SO line {mmDetailId} not found or fully consumed."); if (soLine.PendingQty <= 0) throw new InvalidOperationException($"SO line {mmDetailId} has no pending quantity."); int routingId = await _IndentDAL.GetItemDefaultRoutingId(soLine.ItemId, login, ct).ConfigureAwait(false); var dto = new IndentSaveDTO { BizTransactionTypeId = genParams.BizTransactionTypeId, OUId = genParams.OUId, PeriodId = genParams.PeriodId, IndentDate = DateTime.UtcNow, //SourceType = 1, // from SO ExpectedCompletionDate = genParams.ExpectedCompletionDate, Details = [ new IndentDetailSaveDTO { ItemId = soLine.ItemId, SkuId = soLine.SkuId, RoutingId = routingId, IndentDetailQuantity = soLine.PendingQty, IndentDetailReleaseDate = genParams.ExpectedCompletionDate, AllocationId = soLine.AllocationId } ] }; return await SaveIndent(dto, login, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("generate-wo-from-so-failed", ex); _Logger.LogError(ex, "GenerateWorkOrderFromSO failed for MMDetailId {MMDetailId}", mmDetailId); throw; } } // ── Phase 3: Production plan line lookup ────────────────────────────── public async Task GetProductionPlanLineForWO(int productionPlanDetailId, LoginDTO login, CancellationToken ct) { var result = await _IndentDAL.GetProductionPlanLineForWO(productionPlanDetailId, login, ct).ConfigureAwait(false); return JsonConvert.SerializeObject(result); } public async Task GenerateWorkOrderFromProductionPlan(int productionPlanDetailId, IndentGenerationParamsDTO genParams, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("generate-wo-from-plan", new { productionPlanDetailId }); var planLine = await _IndentDAL.GetProductionPlanLineForWO(productionPlanDetailId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"Production plan detail {productionPlanDetailId} not found."); if (planLine.PendingQuantity <= 0) throw new InvalidOperationException($"Production plan detail {productionPlanDetailId} has no pending quantity."); int routingId = await _IndentDAL.GetItemDefaultRoutingId(planLine.ItemId, login, ct).ConfigureAwait(false); var dto = new IndentSaveDTO { BizTransactionTypeId = genParams.BizTransactionTypeId, OUId = genParams.OUId, PeriodId = genParams.PeriodId, IndentDate = DateTime.UtcNow, //SourceType = 2, // from production plan ExpectedCompletionDate = genParams.ExpectedCompletionDate, Details = [ new IndentDetailSaveDTO { ItemId = planLine.ItemId, SkuId = planLine.SkuId, RoutingId = routingId, IndentDetailQuantity = planLine.PendingQuantity, IndentDetailReleaseDate = planLine.RequiredDate } ] }; return await SaveIndent(dto, login, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("generate-wo-from-plan-failed", ex); _Logger.LogError(ex, "GenerateWorkOrderFromProductionPlan failed for PlanDetailId {PlanDetailId}", productionPlanDetailId); throw; } } // ── Phase 6: Multi-level BOM WO generation ─────────────────────────── public async Task PreviewMultiLevelWO(MultiLevelWORequestDTO request, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("preview-multi-level-wo", new { request.ItemId, request.Quantity }); var nodes = await BuildAndNetWONodesAsync(request, login, ct).ConfigureAwait(false); var results = nodes.Select(n => new MultiLevelWOResultDTO { ItemId = n.ItemId, ItemName = n.ItemName, PartDimId = n.PartDimId, BomLevel = n.BomLevel, RequiredQty = n.RequiredQty, StockQty = n.StockQty, ReservedQty = n.ReservedQty, NetQty = n.NetQty, IsSkipped = n.NetQty <= 0 }).ToList(); return JsonConvert.SerializeObject(results); } catch (Exception ex) { GB5Trace.MarkFailed("preview-multi-level-wo-failed", ex); _Logger.LogError(ex, "PreviewMultiLevelWO failed for ItemId {ItemId}", request.ItemId); throw; } } public async Task GenerateMultiLevelWO(MultiLevelWORequestDTO request, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("generate-multi-level-wo", new { request.ItemId, request.Quantity }); var nodes = await BuildAndNetWONodesAsync(request, login, ct).ConfigureAwait(false); if (request.UseCpmPriority) ApplyCpmPriorities(nodes, request.GenParams); var nodesToSave = nodes.Where(n => n.NetQty > 0).ToList(); if (nodesToSave.Count == 0) return JsonConvert.SerializeObject(nodes.Select(n => new MultiLevelWOResultDTO { ItemId = n.ItemId, ItemName = n.ItemName, BomLevel = n.BomLevel, RequiredQty = n.RequiredQty, NetQty = 0, IsSkipped = true }).ToList()); // Save root product first (BomLevel=0, IsRootProduct=true), then sub-items deepest-first var rootNode = nodesToSave.FirstOrDefault(n => n.IsRootProduct); var subNodes = nodesToSave.Where(n => !n.IsRootProduct) .OrderByDescending(n => n.BomLevel).ToList(); var results = new List(); int rootPartDetailId = 0; if (rootNode != null) { var rootIndent = BuildIndentFromNode(rootNode, request.GenParams, login); await SaveIndent(rootIndent, login, ct).ConfigureAwait(false); // Get root's PartDetailId and set self-referential RootPartDetailId rootPartDetailId = await _PartDetailDAL .GetFirstPartDetailIdByEntity(2, rootIndent.IndentId, login, ct) .ConfigureAwait(false); if (rootPartDetailId > 0) { var tx = await _QueryExecutor.BeginTransactionAsync(login); try { await _PartDetailDAL.UpdatePartDetailLineage( 2, rootIndent.IndentId, null, rootPartDetailId, login, tx, ct) .ConfigureAwait(false); await _QueryExecutor.CommitAsync(tx); } catch { await _QueryExecutor.RollbackAsync(tx); throw; } } results.Add(new MultiLevelWOResultDTO { ItemId = rootNode.ItemId, ItemName = rootNode.ItemName, //PartDimId = rootNode.PartDimId, BomLevel = 0, RequiredQty = rootNode.RequiredQty, StockQty = rootNode.StockQty, ReservedQty = rootNode.ReservedQty, NetQty = rootNode.NetQty, IndentId = rootIndent.IndentId, PartDetailId = rootPartDetailId, Priority = rootNode.CpmPriority }); } foreach (var node in subNodes) { var subIndent = BuildIndentFromNode(node, request.GenParams, login); if (subIndent.Details.Count > 0 && rootPartDetailId > 0) { subIndent.Details[0].ParentPartDetailId = rootPartDetailId; subIndent.Details[0].RootPartDetailId = rootPartDetailId; } await SaveIndent(subIndent, login, ct).ConfigureAwait(false); var subPdId = rootPartDetailId > 0 ? await _PartDetailDAL.GetFirstPartDetailIdByEntity(2, subIndent.IndentId, login, ct).ConfigureAwait(false) : 0; results.Add(new MultiLevelWOResultDTO { ItemId = node.ItemId, ItemName = node.ItemName, PartDimId = node.PartDimId, BomLevel = node.BomLevel, RequiredQty = node.RequiredQty, StockQty = node.StockQty, ReservedQty = node.ReservedQty, NetQty = node.NetQty, IndentId = subIndent.IndentId, PartDetailId = subPdId, Priority = node.CpmPriority }); } // Deduct allocated stock from TPARTPOSITION for netted items. // PartPositionId was cached in the netting pass — no second DB round-trip needed. if (request.ConsiderStock) { foreach (var node in nodesToSave.Where(n => n.StockQty > 0 && n.PartPositionId > 0)) { var tx = await _QueryExecutor.BeginTransactionAsync(login); try { var netted = Math.Min(node.StockQty, node.RequiredQty); await _PartPositionDAL.UpdateAllocate(node.PartPositionId, netted, login, tx, ct) .ConfigureAwait(false); await _QueryExecutor.CommitAsync(tx); } catch { await _QueryExecutor.RollbackAsync(tx); throw; } } } // Append skipped nodes (no WO created because netted to zero) foreach (var node in nodes.Where(n => n.NetQty <= 0)) results.Add(new MultiLevelWOResultDTO { ItemId = node.ItemId, ItemName = node.ItemName, PartDimId = node.PartDimId, BomLevel = node.BomLevel, RequiredQty = node.RequiredQty, StockQty = node.StockQty, ReservedQty = node.ReservedQty, NetQty = 0, IsSkipped = true }); GB5Trace.Step("multi-level-wo-complete", new { count = nodesToSave.Count }); return JsonConvert.SerializeObject(results); } catch (Exception ex) { GB5Trace.MarkFailed("generate-multi-level-wo-failed", ex); _Logger.LogError(ex, "GenerateMultiLevelWO failed for ItemId {ItemId}", request.ItemId); throw; } } private async Task> BuildAndNetWONodesAsync( MultiLevelWORequestDTO request, LoginDTO login, CancellationToken ct) { // BOM explosion — only Make components (ISMAKEORBUY = 0) var bomRows = (await _MRPRunDAL.GetBOMExplosion(new[] { request.ItemId }, login, ct) .ConfigureAwait(false)) .Where(r => r.MakeOrBuy == 0) .ToList(); var nodes = new List { // Root product new() { ItemId = request.ItemId, ItemName = "", PartDimId = request.PartDimId, BomLevel = 0, ParentItemId = 0, IsRootProduct = true, RequiredQty = request.Quantity, SkuId = -1 } }; foreach (var row in bomRows) { if (request.MaxBomLevel.HasValue && row.DetailItemLowLevelCode >= request.MaxBomLevel.Value) continue; if (request.IncludeItemIds is { Count: > 0 } && !request.IncludeItemIds.Contains(row.DetailItemId)) continue; nodes.Add(new WONode { ItemId = row.DetailItemId, ItemName = row.DetailItemName, PartDimId = request.PartDimId, // propagate root dimension to sub-items BomLevel = row.DetailItemLowLevelCode + 1, ParentItemId = row.ItemId, IsRootProduct = false, RequiredQty = row.FinalQuantity * request.Quantity, SkuId = row.SKUId }); } // Stock/reservation netting — fetch all positions in parallel, cache PartPositionId if (request.ConsiderStock || request.ConsiderReservation) { var posTasks = nodes .Select(n => _PartPositionDAL.GetPartPositionByKey(n.ItemId, n.PartDimId, 0, null, login, ct)) .ToList(); var positions = await Task.WhenAll(posTasks).ConfigureAwait(false); for (int i = 0; i < nodes.Count; i++) { var pos = positions[i]; nodes[i].StockQty = request.ConsiderStock ? (pos?.Available ?? 0) : 0; nodes[i].ReservedQty = request.ConsiderReservation ? (pos?.Reserved ?? 0) : 0; nodes[i].PartPositionId = pos?.PartPositionId ?? 0; } } foreach (var node in nodes) node.NetQty = Math.Max(0, node.RequiredQty - node.StockQty - node.ReservedQty); return nodes; } private static IndentSaveDTO BuildIndentFromNode(WONode node, IndentGenerationParamsDTO genParams, LoginDTO login) { return new IndentSaveDTO { BizTransactionTypeId = genParams.BizTransactionTypeId, OUId = genParams.OUId, PeriodId = genParams.PeriodId, IndentDate = DateTime.UtcNow, ReferenceDate = DateTime.UtcNow, //SourceType = 0, // manual (multi-level WO is user-initiated) ExpectedFirstDeliveryDate = genParams.ExpectedCompletionDate, ExpectedCompletionDate = genParams.ExpectedCompletionDate, ReceivedOn = DateTime.UtcNow, ChangeRemarks = string.Empty, Details = [ new IndentDetailSaveDTO { ItemId = node.ItemId, SkuId = node.SkuId > 0 ? node.SkuId : -1, IndentDetailQuantity = node.NetQty, IndentDetailReleaseDate = genParams.ExpectedCompletionDate, LinkTypeId = 0, MMDetailDocumentDetailId = 0, IndentDetailDocumentId = 0, ProcessGroupId = 0, WorkPlanDetailId = 0, RoutingId = -1, RoutingDetailId = -1, BomId = -1, MachineTypeId = -1, MachineId = -1, Priority = 0, ReleaseStatus = 0, Rate = 0, Remarks = string.Empty, IndentBeamType = 0, BeamLotId = 0, MetersPerPiece = 0, WeavePick = 0, WeaveWidth = 0, AllotedGoodQuantity = 0, AllotedReworkQuantity = 0, NextLevelQuantity = 0, PerQuantity = 0, NestingPlanId = 0, AllocationId = 0, CallId = 0, TaskId = 0, GenerateAllocationId = 0 } ] }; } // Applies CPM priorities to a list of nodes before saving. private static void ApplyCpmPriorities(List nodes, IndentGenerationParamsDTO genParams) { var daysToDeadline = (decimal)(genParams.ExpectedCompletionDate - DateTime.UtcNow).TotalDays; var cpmNodes = nodes.Select(n => new IndentDependencyGraph.Node { ItemId = n.ItemId, BomLevel = n.BomLevel, Duration = 1, // default 1 day per level until routing durations are loaded }).ToList(); IndentDependencyGraph.AssignPriorities(cpmNodes, Math.Max(1, daysToDeadline)); // An item can appear at multiple BOM levels; pick the highest urgency (lowest rank = 1). var priorityByItemId = cpmNodes .GroupBy(c => c.ItemId) .ToDictionary(g => g.Key, g => g.Min(c => c.Priority)); foreach (var node in nodes) { if (priorityByItemId.TryGetValue(node.ItemId, out var p)) node.CpmPriority = p; } } // Internal node used only during multi-level WO generation private sealed class WONode { public int ItemId { get; set; } public string ItemName { get; set; } = ""; public int? PartDimId { get; set; } public int BomLevel { get; set; } public int ParentItemId { get; set; } public bool IsRootProduct { get; set; } public decimal RequiredQty { get; set; } public decimal StockQty { get; set; } public decimal ReservedQty { get; set; } public decimal NetQty { get; set; } public int SkuId { get; set; } = -1; public short CpmPriority { get; set; } public int PartPositionId { get; set; } // cached from netting pass; avoids second DB round-trip } // ── Phase 2 (Production): WO Lifecycle ──────────────────────────────── public async Task SplitWorkOrder(SplitWorkOrderDTO dto, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-split-wo", new { dto.IndentDetailId, dto.SplitQuantity }); if (dto.SplitQuantity <= 0) throw new ArgumentException("Split quantity must be greater than zero."); var source = await _IndentDAL.GetIndentDetailById(dto.IndentDetailId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"IndentDetailId {dto.IndentDetailId} not found."); decimal remainingQty = source.IndentDetailQuantity - dto.SplitQuantity; if (remainingQty <= 0) throw new InvalidOperationException("Split quantity must be less than the current work order quantity."); var newNum = await _AutoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.INDENTDETAIL, login).ConfigureAwait(false); var newDetail = new IndentDetailInsertDTO { IndentDetailId = newNum.StartNumber, IndentId = source.IndentId, SlNo = source.SlNo, ItemId = source.ItemId, SkuId = source.SkuId, RoutingId = source.RoutingId, RoutingDetailId = source.RoutingDetailId, BomId = source.BomId, MachineId = dto.SplitWorkCenterId > 0 ? source.MachineId : source.MachineId, MachineTypeId = source.MachineTypeId, NestingPlanId = source.NestingPlanId, IndentDetailQuantity = dto.SplitQuantity, IndentDetailReleaseDate = source.IndentDetailReleaseDate, AllocationId = source.AllocationId }; GB5Trace.Step("save-split-wo", new { dto.IndentDetailId, newDetailId = newDetail.IndentDetailId }); await using var tx = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _IndentDAL.UpdateIndentDetailQuantity(dto.IndentDetailId, remainingQty, login, tx, ct).ConfigureAwait(false); await _IndentDAL.BulkSaveIndentDetails(new[] { newDetail }, login, tx, ct).ConfigureAwait(false); await tx.CommitAsync(ct).ConfigureAwait(false); } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } return $"{SuccessResponse.SaveSuccessMessage} {newDetail.IndentDetailId}"; } catch (Exception ex) { GB5Trace.MarkFailed("split-wo-failed", ex); _Logger.LogError(ex, "SplitWorkOrder failed for IndentDetailId {IndentDetailId}", dto.IndentDetailId); throw; } } public async Task CloseWorkOrder(int indentDetailId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("close-wo", new { indentDetailId }); await using var tx = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _IndentDAL.CloseWorkOrder(indentDetailId, login, tx, ct).ConfigureAwait(false); await tx.CommitAsync(ct).ConfigureAwait(false); } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } return SuccessResponse.UpdateSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("close-wo-failed", ex); _Logger.LogError(ex, "CloseWorkOrder failed for IndentDetailId {IndentDetailId}", indentDetailId); throw; } } public async Task ShortCloseWorkOrder(int indentDetailId, decimal closedQuantity, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("short-close-wo", new { indentDetailId, closedQuantity }); if (closedQuantity <= 0) throw new ArgumentException("Closed quantity must be greater than zero."); await using var tx = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _IndentDAL.ShortCloseWorkOrder(indentDetailId, closedQuantity, login, tx, ct).ConfigureAwait(false); await tx.CommitAsync(ct).ConfigureAwait(false); } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } return SuccessResponse.UpdateSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("short-close-wo-failed", ex); _Logger.LogError(ex, "ShortCloseWorkOrder failed for IndentDetailId {IndentDetailId}", indentDetailId); throw; } } public async Task AmendWorkOrder(IndentSaveDTO dto, int originalIndentId, LoginDTO login, CancellationToken ct) { dto.IndentId = 0; return await SaveIndent(dto, login, ct).ConfigureAwait(false); } public async Task CheckBOMFeasibility(int indentDetailId, LoginDTO login, CancellationToken ct) { var items = await _IndentDAL.GetBOMFeasibility(indentDetailId, -1, login, ct).ConfigureAwait(false); return Newtonsoft.Json.JsonConvert.SerializeObject(items); } public async Task GetSupervisorWorkbenchKPI(int ouId, LoginDTO login, CancellationToken ct) { var kpi = await _IndentDAL.GetSupervisorWorkbenchKPI(ouId, login, ct).ConfigureAwait(false); return Newtonsoft.Json.JsonConvert.SerializeObject(kpi); } // ── Production Phase 6: Supervisor Workbench ────────────────────────── public async Task AssignResources(ResourceAssignmentDTO dto, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("assign-resources", new { dto.IndentDetailId, dto.WorkCenterId }); if (dto.IndentDetailId <= 0) throw new ArgumentException("IndentDetailId is required."); var resources = dto.Resources.Select((r, idx) => new IndentResourceInsertDTO { IndentResourceId = 0, // Will be replaced by AutoNumber IndentDetailId = dto.IndentDetailId, SlNo = (short)(idx + 1), BillOfResourceId = r.BillOfResourceId, ResourceTypeId = r.ResourceTypeId, MachineId = r.MachineId, EmployeeId = r.EmployeeId, PlanDuration = r.PlanDuration, PlanStartDate = r.PlanStartDate, PlanStartTime = r.PlanStartTime, PlanEndDate = r.PlanEndDate, PlanEndTime = r.PlanEndTime, ResourceId = r.ResourceId, CalendarDetailId = r.CalendarDetailId, PlannedQuantity = r.PlannedQuantity, PerQuantity = r.PerQuantity, TimeUOM = r.TimeUOM, Remarks = r.Remarks, // Extra DTO properties (not inserted into TINDENTRESOURCE) WorkCenterId = dto.WorkCenterId, MachineTypeId = r.MachineTypeId, Duration = r.Duration, TenantId = login.ClientId }).ToList(); if (resources.Count > 0) { var nums = await _AutoNumber.GetNumberAsync(resources.Count, AUTONUMBERCONSTANT.INDENTRESOURCE, login).ConfigureAwait(false); for (int i = 0; i < resources.Count; i++) resources[i].IndentResourceId = nums.StartNumber + i; } await using var tx = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _IndentDAL.DeleteIndentResources(dto.IndentDetailId, login, tx, ct).ConfigureAwait(false); if (resources.Count > 0) await _IndentDAL.BulkSaveIndentResourceAssignments(resources, login, tx).ConfigureAwait(false); await tx.CommitAsync(ct).ConfigureAwait(false); } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } return SuccessResponse.UpdateSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("assign-resources-failed", ex); _Logger.LogError(ex, "AssignResources failed for IndentDetailId {IndentDetailId}", dto.IndentDetailId); throw; } } public async Task ReleaseWorkOrder(int indentDetailId, byte releaseMode, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("release-wo", new { indentDetailId, releaseMode }); await using var tx = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _IndentDAL.ReleaseWorkOrder(indentDetailId, login, tx, ct).ConfigureAwait(false); await tx.CommitAsync(ct).ConfigureAwait(false); } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } return SuccessResponse.UpdateSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("release-wo-failed", ex); _Logger.LogError(ex, "ReleaseWorkOrder failed for IndentDetailId {IndentDetailId}", indentDetailId); throw; } } public async Task GetSupervisorWOList(int ouId, int statusFilter, int firstNumber, int maxResult, LoginDTO login, CancellationToken ct) { var (items, total) = await _IndentDAL.GetSupervisorWOList(ouId, statusFilter, firstNumber, maxResult, login, ct).ConfigureAwait(false); return Newtonsoft.Json.JsonConvert.SerializeObject(new { Items = items, TotalCount = total }); } // ── Phase 4: Routing resource auto-assignment ───────────────────────── private async Task AssignResourcesFromRoutingAsync(List details, LoginDTO login, CancellationToken ct) { var routingDetailIds = details .Where(d => d.RoutingDetailId > 0) .Select(d => d.RoutingDetailId) .Distinct() .ToList(); if (routingDetailIds.Count == 0) return; var resources = (await _IndentDAL.GetRoutingDetailResources(routingDetailIds, login, ct).ConfigureAwait(false)) .ToLookup(r => r.RoutingDetailId); foreach (var d in details) { if (d.RoutingDetailId <= 0 || d.Resources.Count > 0) continue; foreach (var r in resources[d.RoutingDetailId]) { d.Resources.Add(new IndentResourceSaveDTO { WorkCenterId = r.WorkCenterId, MachineId = r.MachineId, MachineTypeId = r.MachineTypeId, Duration = r.Duration }); } } } public async Task GetIndentById(int indentId, LoginDTO loginDTO, CancellationToken ct) { try { // Prepare SQL and params string sql = IndentQB.GET_INDENT_BY_ID; var parameters = new { IndentId = indentId }; // Call DAL var result = await _IndentDAL.GetIndentById( loginDTO, sql, parameters); return JsonConvert.SerializeObject(result); } catch (Exception) { throw; } } public async Task SaveWorkOrder(IndentSaveDTO IndentSaveDTO, LoginDTO loginDTO) { if (IndentSaveDTO == null) throw new ArgumentNullException(nameof(IndentSaveDTO)); if (string.IsNullOrWhiteSpace(IndentSaveDTO.IndentNumber)) throw new ArgumentException("Indent Number is required.", nameof(IndentSaveDTO.IndentNumber)); if (IndentSaveDTO.IndentId <= 0) // Insert { // Audit Fields IndentSaveDTO.CreatedById = loginDTO.UserId; IndentSaveDTO.CreatedOn = DateTime.UtcNow; IndentSaveDTO.ModifiedById = loginDTO.UserId; IndentSaveDTO.ModifiedOn = DateTime.UtcNow; int indentId = await _IndentDAL.SaveWorkOrder(IndentSaveDTO, loginDTO); if (indentId <= 0) throw new Exception("Failed to save Indent."); return $"Indent saved successfully. IndentId: {indentId}"; } else // Update { if (IndentSaveDTO.IndentId <= 0) throw new ArgumentException("Invalid IndentId for update."); // Audit Fields IndentSaveDTO.ModifiedById = loginDTO.UserId; IndentSaveDTO.ModifiedOn = DateTime.UtcNow; await _IndentDAL.UpdateWorkOrder(IndentSaveDTO, loginDTO); return $"Indent updated successfully. IndentId: {IndentSaveDTO.IndentId}"; } } public async Task> GetListOfIndent(CriteriaDTO criteriaDTO,LoginDTO loginDTO) { try { return await _IndentDAL.GetListOfIndent(criteriaDTO,loginDTO); } catch (Exception) { throw; } } // ── Phase 7: Project-wise shortage report (GB4 port) ─────────────────── public async Task GetProjectWiseShortage(int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO login, CancellationToken ct) { try { var (items, totalCount) = await _IndentDAL .GetProjectWiseShortage(firstNumber, maxResult, criteriaDTO, login, ct) .ConfigureAwait(false); return JsonConvert.SerializeObject(new { Items = items, TotalCount = totalCount }); } catch (Exception ex) { throw new Exception($"Error in IndentBLL.GetProjectWiseShortage: {ex.Message}", ex); } } // ── Phase 7: Indent reference-number picklist (GB4 port) ─────────────── public async Task GetSelectListIndentRefNumber(int firstNumber, int maxResult, LoginDTO login, CancellationToken ct) { try { var items = await _IndentDAL .GetSelectListIndentRefNumber(firstNumber, maxResult, login, ct) .ConfigureAwait(false); return JsonConvert.SerializeObject(items); } catch (Exception ex) { throw new Exception($"Error in IndentBLL.GetSelectListIndentRefNumber: {ex.Message}", ex); } } } }