using Admin.NET.Plugin.AiDOP.Order; namespace Admin.NET.Plugin.AiDOP.WorkOrder; /// /// 工单齐套检查动作:重新执行资源检查(BOM 展开 + 库存/在途缺口计算), /// 将新结果写入 b_examine_result / b_bom_child_examine,并更新 mes_morder.MaterialSituation。 /// public class WorkOrderKittingCheckService : ITransient { private readonly ISqlSugarClient _db; private readonly MaterialRequirementCalculator _calculator; private readonly ResourceCheckResultWriter _writer; public WorkOrderKittingCheckService( ISqlSugarClient db, MaterialRequirementCalculator calculator, ResourceCheckResultWriter writer) { _db = db; _calculator = calculator; _writer = writer; } /// 批量齐套检查:租户下所有 Status='p'/'r' 的工单逐一重新检查。 public async Task CheckTenantWorkOrdersAsync(long tenantId, string account) { var workOrds = await _db.Ado.SqlQueryAsync( """ SELECT WorkOrd FROM WorkOrdMaster WHERE tenant_id = @TenantId AND LOWER(TRIM(IFNULL(Status,''))) IN ('p', 'r') ORDER BY WorkOrd """, new SugarParameter("@TenantId", tenantId)); var result = new KittingCheckResult(); foreach (var wo in workOrds) { var one = await CheckSingleAsync(tenantId, wo, account); result.CheckedCount++; if (one.IsKitted) result.KittedCount++; else result.ShortageCount++; } return result; } /// /// 单工单齐套检查:以工单为主,重新 BOM 展开 + 库存计算 → 写入检查结果。 /// BusinessID>0 时从订单明细行加载数据;BusinessID=0 时从工单/生产工单构造参数。 /// public async Task CheckSingleAsync(long tenantId, string workOrd, string account) { var wo = workOrd.Trim(); // 1. 从 WorkOrdMaster + mes_morder 获取工单数据 var wm = await LoadWorkOrdMasterAsync(tenantId, wo); if (wm is null) throw Oops.Oh($"工单 {wo} 不存在或已关闭"); OrderWorkOrderGenerationService.OrderEntryLine entry; OrderWorkOrderGenerationService.OrderHeader order; if (wm.BusinessId > 0) { // 2a. 有关联订单明细行:从 crm_seorderentry / crm_seorder 加载 var loadedEntry = await LoadOrderEntryAsync(wm.BusinessId, tenantId); if (loadedEntry is not null) { entry = loadedEntry; var loadedOrder = await LoadOrderHeaderAsync(entry.SeOrderId, tenantId); order = loadedOrder ?? BuildFallbackOrderFromWorkOrder(wm); } else { // 明细行已删除/不存在,回退到工单数据 entry = BuildEntryFromWorkOrder(wm); order = BuildFallbackOrderFromWorkOrder(wm); } } else { // 2b. 无关联订单明细行(BusinessID=0):从工单构造检查参数 entry = BuildEntryFromWorkOrder(wm); order = BuildFallbackOrderFromWorkOrder(wm); } // 3. 重新执行资源检查(BOM 展开 + 库存/在途缺口计算) var warnings = new List(); var lines = await _calculator.BuildLinesAsync(order, entry, warnings); // 4. 将新结果写入 b_examine_result / b_bom_child_examine 并更新 mes_morder var checkResult = await _writer.WriteAsync(order, entry, wo, wm.MorderId, lines, account, DateTime.Now); return new SingleKittingCheckResult { WorkOrd = wo, IsKitted = !checkResult.HasShortage, ShortageLineCount = checkResult.HasShortage ? lines.Count(x => x.IsUse == 1 && x.LackQty > 0) : 0, MaterialSituation = checkResult.HasShortage ? "缺料" : "齐套" }; } /// 从 WorkOrdMaster + mes_morder 数据构造 OrderEntryLine。 private static OrderWorkOrderGenerationService.OrderEntryLine BuildEntryFromWorkOrder(WorkOrdMasterRow wm) { return new OrderWorkOrderGenerationService.OrderEntryLine { Id = wm.BusinessId, SeOrderId = 0, BillNo = wm.SalesJob, EntrySeq = 0, ItemNumber = wm.WoItemNum, ItemName = wm.WoItemName, Specification = wm.MorderModel, Unit = wm.MorderUnit, BomNumber = wm.MorderBomNumber ?? wm.WoBomFormula, Qty = wm.WoQtyOrded, PlanDate = wm.WoDueDate, SysCapacityDate = wm.WoDueDate, Progress = null, Urgent = wm.WoUrgent, FactoryId = null, CompanyId = null, TenantId = wm.WoTenantId }; } /// 从 WorkOrdMaster 数据构造兜底 OrderHeader。 private static OrderWorkOrderGenerationService.OrderHeader BuildFallbackOrderFromWorkOrder(WorkOrdMasterRow wm) { return new OrderWorkOrderGenerationService.OrderHeader { Id = 0, BillNo = wm.SalesJob, CustomNo = wm.WoCustNo, Urgent = wm.WoUrgent, FactoryId = null, TenantId = wm.WoTenantId }; } // ──────────────── 数据加载 ──────────────── private async Task LoadWorkOrdMasterAsync(long tenantId, string workOrd) { var rows = await _db.Ado.SqlQueryAsync( """ SELECT wm.RecID, wm.WorkOrd, wm.BusinessID, wm.SalesJob, wm.ItemNum AS WoItemNum, wm.ItemName AS WoItemName, wm.QtyOrded AS WoQtyOrded, wm.DueDate AS WoDueDate, wm.Urgent AS WoUrgent, wm.CustNo AS WoCustNo, wm.BOMFormula AS WoBomFormula, wm.tenant_id AS WoTenantId, morder.Id AS MorderId, morder.bom_number AS MorderBomNumber, morder.product_code AS MorderProductCode, morder.product_name AS MorderProductName, morder.fmodel AS MorderModel, morder.unit AS MorderUnit FROM WorkOrdMaster wm LEFT JOIN mes_morder morder ON morder.morder_no = wm.WorkOrd AND morder.tenant_id = wm.tenant_id AND morder.IsDeleted = 0 WHERE wm.tenant_id = @TenantId AND wm.WorkOrd = @WorkOrd AND LOWER(TRIM(IFNULL(wm.Status,''))) <> 'c' ORDER BY wm.RecID DESC LIMIT 1 """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@WorkOrd", workOrd)); return rows.FirstOrDefault(); } private async Task LoadOrderEntryAsync(long entryId, long tenantId) { var rows = await _db.Ado.SqlQueryAsync( """ SELECT Id, seorder_id AS SeOrderId, bill_no AS BillNo, entry_seq AS EntrySeq, item_number AS ItemNumber, item_name AS ItemName, specification AS Specification, unit AS Unit, bom_number AS BomNumber, qty AS Qty, plan_date AS PlanDate, sys_capacity_date AS SysCapacityDate, progress AS Progress, urgent AS Urgent, factory_id AS FactoryId, company_id AS CompanyId, tenant_id AS TenantId FROM crm_seorderentry WHERE Id = @Id AND tenant_id = @TenantId AND IsDeleted = 0 LIMIT 1 """, new SugarParameter("@Id", entryId), new SugarParameter("@TenantId", tenantId)); return rows.FirstOrDefault(); } private async Task LoadOrderHeaderAsync(long orderId, long tenantId) { var rows = await _db.Ado.SqlQueryAsync( """ SELECT Id, bill_no AS BillNo, custom_no AS CustomNo, urgent AS Urgent, factory_id AS FactoryId, tenant_id AS TenantId FROM crm_seorder WHERE Id = @Id AND tenant_id = @TenantId AND IsDeleted = 0 LIMIT 1 """, new SugarParameter("@Id", orderId), new SugarParameter("@TenantId", tenantId)); return rows.FirstOrDefault(); } // ──────────────── 内部类 ──────────────── private sealed class WorkOrdMasterRow { public long RecID { get; set; } public string WorkOrd { get; set; } = string.Empty; public long BusinessId { get; set; } public string? SalesJob { get; set; } public long? MorderId { get; set; } // WorkOrdMaster 字段 public string? WoItemNum { get; set; } public string? WoItemName { get; set; } public decimal WoQtyOrded { get; set; } public DateTime? WoDueDate { get; set; } public int WoUrgent { get; set; } public string? WoCustNo { get; set; } public string? WoBomFormula { get; set; } public long WoTenantId { get; set; } // mes_morder 字段 public string? MorderBomNumber { get; set; } public string? MorderProductCode { get; set; } public string? MorderProductName { get; set; } public string? MorderModel { get; set; } public string? MorderUnit { get; set; } } public sealed class KittingCheckResult { public int CheckedCount { get; set; } public int KittedCount { get; set; } public int ShortageCount { get; set; } } public sealed class SingleKittingCheckResult { public string WorkOrd { get; set; } = string.Empty; public bool IsKitted { get; set; } public int ShortageLineCount { get; set; } public string MaterialSituation { get; set; } = string.Empty; } }