Просмотр исходного кода

refactor(aidop): 工单齐套检查以工单为主,BusinessID=0时从WorkOrdMaster/mes_morder构造参数

Pengxy 1 неделя назад
Родитель
Сommit
cbaae39ed2

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.197</AssemblyVersion>
-    <FileVersion>1.0.197</FileVersion>
-    <Version>1.0.197</Version>
+    <AssemblyVersion>1.0.198</AssemblyVersion>
+    <FileVersion>1.0.198</FileVersion>
+    <Version>1.0.198</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 196 - 32
server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/WorkOrderKittingCheckService.cs

@@ -1,15 +1,28 @@
+using Admin.NET.Plugin.AiDOP.Order;
+
 namespace Admin.NET.Plugin.AiDOP.WorkOrder;
 
-/// <summary>工单齐套检查动作。</summary>
+/// <summary>
+/// 工单齐套检查动作:重新执行资源检查(BOM 展开 + 库存/在途缺口计算),
+/// 将新结果写入 b_examine_result / b_bom_child_examine,并更新 mes_morder.MaterialSituation。
+/// </summary>
 public class WorkOrderKittingCheckService : ITransient
 {
     private readonly ISqlSugarClient _db;
+    private readonly MaterialRequirementCalculator _calculator;
+    private readonly ResourceCheckResultWriter _writer;
 
-    public WorkOrderKittingCheckService(ISqlSugarClient db)
+    public WorkOrderKittingCheckService(
+        ISqlSugarClient db,
+        MaterialRequirementCalculator calculator,
+        ResourceCheckResultWriter writer)
     {
         _db = db;
+        _calculator = calculator;
+        _writer = writer;
     }
 
+    /// <summary>批量齐套检查:租户下所有 Status='p'/'r' 的工单逐一重新检查。</summary>
     public async Task<KittingCheckResult> CheckTenantWorkOrdersAsync(long tenantId, string account)
     {
         var workOrds = await _db.Ado.SqlQueryAsync<string>(
@@ -32,45 +45,196 @@ public class WorkOrderKittingCheckService : ITransient
         return result;
     }
 
+    /// <summary>
+    /// 单工单齐套检查:以工单为主,重新 BOM 展开 + 库存计算 → 写入检查结果。
+    /// BusinessID>0 时从订单明细行加载数据;BusinessID=0 时从工单/生产工单构造参数。
+    /// </summary>
     public async Task<SingleKittingCheckResult> CheckSingleAsync(long tenantId, string workOrd, string account)
     {
-        var now = DateTime.Now;
-        var shortage = await _db.Ado.GetIntAsync(
+        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<string>();
+        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 ? "缺料" : "齐套"
+        };
+    }
+
+    /// <summary>从 WorkOrdMaster + mes_morder 数据构造 OrderEntryLine。</summary>
+    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
+        };
+    }
+
+    /// <summary>从 WorkOrdMaster 数据构造兜底 OrderHeader。</summary>
+    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<WorkOrdMasterRow?> LoadWorkOrdMasterAsync(long tenantId, string workOrd)
+    {
+        var rows = await _db.Ado.SqlQueryAsync<WorkOrdMasterRow>(
             """
-            SELECT COUNT(*)
-            FROM b_examine_result ber
-            INNER JOIN b_bom_child_examine bce ON ber.Id = bce.examine_id AND bce.is_use = 1
-            WHERE ber.tenant_id = @TenantId
-              AND ber.IsDeleted = 0
-              AND ber.morder_no = @WorkOrd
-              AND IFNULL(bce.lack_qty, 0) > 0
+            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.Trim()));
+            new SugarParameter("@WorkOrd", workOrd));
+        return rows.FirstOrDefault();
+    }
 
-        var isKitted = shortage == 0;
-        var situation = isKitted ? "齐套" : "缺料";
-        await _db.Ado.ExecuteCommandAsync(
+    private async Task<OrderWorkOrderGenerationService.OrderEntryLine?> LoadOrderEntryAsync(long entryId, long tenantId)
+    {
+        var rows = await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderEntryLine>(
             """
-            UPDATE mes_morder
-            SET MaterialSituation = @Situation,
-                update_by_name = @User,
-                update_time = @Now
-            WHERE tenant_id = @TenantId AND morder_no = @WorkOrd AND IsDeleted = 0
+            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("@Situation", situation),
-            new SugarParameter("@User", account),
-            new SugarParameter("@Now", now),
-            new SugarParameter("@TenantId", tenantId),
-            new SugarParameter("@WorkOrd", workOrd.Trim()));
+            new SugarParameter("@Id", entryId),
+            new SugarParameter("@TenantId", tenantId));
+        return rows.FirstOrDefault();
+    }
 
-        return new SingleKittingCheckResult
-        {
-            WorkOrd = workOrd,
-            IsKitted = isKitted,
-            ShortageLineCount = shortage,
-            MaterialSituation = situation
-        };
+    private async Task<OrderWorkOrderGenerationService.OrderHeader?> LoadOrderHeaderAsync(long orderId, long tenantId)
+    {
+        var rows = await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderHeader>(
+            """
+            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