WorkOrderKittingCheckService.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. using Admin.NET.Plugin.AiDOP.Order;
  2. namespace Admin.NET.Plugin.AiDOP.WorkOrder;
  3. /// <summary>
  4. /// 工单齐套检查动作:重新执行资源检查(BOM 展开 + 库存/在途缺口计算),
  5. /// 将新结果写入 b_examine_result / b_bom_child_examine,并更新 mes_morder.MaterialSituation。
  6. /// </summary>
  7. public class WorkOrderKittingCheckService : ITransient
  8. {
  9. private readonly ISqlSugarClient _db;
  10. private readonly MaterialRequirementCalculator _calculator;
  11. private readonly ResourceCheckResultWriter _writer;
  12. private readonly WorkOrderMaterialDetailSyncService _materialDetailSync;
  13. public WorkOrderKittingCheckService(
  14. ISqlSugarClient db,
  15. MaterialRequirementCalculator calculator,
  16. ResourceCheckResultWriter writer,
  17. WorkOrderMaterialDetailSyncService materialDetailSync)
  18. {
  19. _db = db;
  20. _calculator = calculator;
  21. _writer = writer;
  22. _materialDetailSync = materialDetailSync;
  23. }
  24. /// <summary>批量齐套检查:租户下所有 Status='p'/'r' 的工单逐一重新检查。</summary>
  25. public async Task<KittingCheckResult> CheckTenantWorkOrdersAsync(long tenantId, string account)
  26. {
  27. var workOrds = await _db.Ado.SqlQueryAsync<string>(
  28. """
  29. SELECT WorkOrd FROM WorkOrdMaster
  30. WHERE tenant_id = @TenantId
  31. AND LOWER(TRIM(IFNULL(Status,''))) IN ('p', 'r')
  32. ORDER BY WorkOrd
  33. """,
  34. new SugarParameter("@TenantId", tenantId));
  35. var result = new KittingCheckResult();
  36. foreach (var wo in workOrds)
  37. {
  38. var one = await CheckSingleAsync(tenantId, wo, account);
  39. result.CheckedCount++;
  40. if (one.IsKitted) result.KittedCount++;
  41. else result.ShortageCount++;
  42. }
  43. return result;
  44. }
  45. /// <summary>
  46. /// 单工单齐套检查:以工单为主,重新 BOM 展开 + 库存计算 → 写入检查结果。
  47. /// BusinessID>0 时从订单明细行加载数据;BusinessID=0 时从工单/生产工单构造参数。
  48. /// </summary>
  49. public async Task<SingleKittingCheckResult> CheckSingleAsync(long tenantId, string workOrd, string account, long bangId = 0)
  50. {
  51. var wo = workOrd.Trim();
  52. // 1. 从 WorkOrdMaster + mes_morder 获取工单数据
  53. var wm = await LoadWorkOrdMasterAsync(tenantId, wo);
  54. if (wm is null)
  55. throw Oops.Oh($"工单 {wo} 不存在或已关闭");
  56. OrderWorkOrderGenerationService.OrderEntryLine entry;
  57. OrderWorkOrderGenerationService.OrderHeader order;
  58. if (wm.BusinessId > 0)
  59. {
  60. // 2a. 有关联订单明细行:从 crm_seorderentry / crm_seorder 加载
  61. var loadedEntry = await LoadOrderEntryAsync(wm.BusinessId, tenantId);
  62. if (loadedEntry is not null)
  63. {
  64. entry = loadedEntry;
  65. var loadedOrder = await LoadOrderHeaderAsync(entry.SeOrderId, tenantId);
  66. order = loadedOrder ?? BuildFallbackOrderFromWorkOrder(wm);
  67. }
  68. else
  69. {
  70. // 明细行已删除/不存在,回退到工单数据
  71. entry = BuildEntryFromWorkOrder(wm);
  72. order = BuildFallbackOrderFromWorkOrder(wm);
  73. }
  74. }
  75. else
  76. {
  77. // 2b. 无关联订单明细行(BusinessID=0):从工单构造检查参数
  78. entry = BuildEntryFromWorkOrder(wm);
  79. order = BuildFallbackOrderFromWorkOrder(wm);
  80. }
  81. // 3. 重新执行资源检查(BOM 展开 + 库存/在途缺口计算)
  82. var warnings = new List<string>();
  83. var lines = await _calculator.BuildLinesAsync(order, entry, warnings, bangId);
  84. // 4. 将新结果写入 b_examine_result / b_bom_child_examine 并更新 mes_morder
  85. var checkResult = await _writer.WriteAsync(order, entry, wo, wm.MorderId, lines, account, DateTime.Now);
  86. // 5. 同步工单物料明细(WorkOrdDetail)
  87. await _materialDetailSync.EnsureFromResourceCheckAsync(tenantId, wo, account);
  88. return new SingleKittingCheckResult
  89. {
  90. WorkOrd = wo,
  91. IsKitted = !checkResult.HasShortage,
  92. ShortageLineCount = checkResult.HasShortage
  93. ? lines.Count(x => x.IsUse == 1 && x.LackQty > 0)
  94. : 0,
  95. MaterialSituation = checkResult.HasShortage ? "缺料" : "齐套"
  96. };
  97. }
  98. /// <summary>从 WorkOrdMaster + mes_morder 数据构造 OrderEntryLine。</summary>
  99. private static OrderWorkOrderGenerationService.OrderEntryLine BuildEntryFromWorkOrder(WorkOrdMasterRow wm)
  100. {
  101. return new OrderWorkOrderGenerationService.OrderEntryLine
  102. {
  103. Id = wm.BusinessId,
  104. SeOrderId = 0,
  105. BillNo = wm.SalesJob,
  106. EntrySeq = 0,
  107. ItemNumber = wm.WoItemNum,
  108. ItemName = wm.WoItemName,
  109. Specification = wm.MorderModel,
  110. Unit = wm.MorderUnit,
  111. BomNumber = wm.MorderBomNumber ?? wm.WoBomFormula,
  112. Qty = wm.WoQtyOrded,
  113. PlanDate = wm.WoDueDate,
  114. SysCapacityDate = wm.WoDueDate,
  115. Progress = null,
  116. Urgent = wm.WoUrgent,
  117. FactoryId = null,
  118. CompanyId = null,
  119. TenantId = wm.WoTenantId
  120. };
  121. }
  122. /// <summary>从 WorkOrdMaster 数据构造兜底 OrderHeader。</summary>
  123. private static OrderWorkOrderGenerationService.OrderHeader BuildFallbackOrderFromWorkOrder(WorkOrdMasterRow wm)
  124. {
  125. return new OrderWorkOrderGenerationService.OrderHeader
  126. {
  127. Id = 0,
  128. BillNo = wm.SalesJob,
  129. CustomNo = wm.WoCustNo,
  130. Urgent = wm.WoUrgent,
  131. FactoryId = null,
  132. TenantId = wm.WoTenantId
  133. };
  134. }
  135. // ──────────────── 数据加载 ────────────────
  136. private async Task<WorkOrdMasterRow?> LoadWorkOrdMasterAsync(long tenantId, string workOrd)
  137. {
  138. var rows = await _db.Ado.SqlQueryAsync<WorkOrdMasterRow>(
  139. """
  140. SELECT wm.RecID, wm.WorkOrd, wm.BusinessID, wm.SalesJob,
  141. wm.ItemNum AS WoItemNum, wm.ItemName AS WoItemName,
  142. wm.QtyOrded AS WoQtyOrded, wm.DueDate AS WoDueDate,
  143. wm.Urgent AS WoUrgent, wm.CustNo AS WoCustNo,
  144. wm.BOMFormula AS WoBomFormula,
  145. wm.tenant_id AS WoTenantId,
  146. morder.Id AS MorderId,
  147. morder.bom_number AS MorderBomNumber,
  148. morder.product_code AS MorderProductCode,
  149. morder.product_name AS MorderProductName,
  150. morder.fmodel AS MorderModel,
  151. morder.unit AS MorderUnit
  152. FROM WorkOrdMaster wm
  153. LEFT JOIN mes_morder morder
  154. ON morder.morder_no = wm.WorkOrd
  155. AND morder.tenant_id = wm.tenant_id
  156. AND morder.IsDeleted = 0
  157. WHERE wm.tenant_id = @TenantId
  158. AND wm.WorkOrd = @WorkOrd
  159. AND LOWER(TRIM(IFNULL(wm.Status,''))) <> 'c'
  160. ORDER BY wm.RecID DESC
  161. LIMIT 1
  162. """,
  163. new SugarParameter("@TenantId", tenantId),
  164. new SugarParameter("@WorkOrd", workOrd));
  165. return rows.FirstOrDefault();
  166. }
  167. private async Task<OrderWorkOrderGenerationService.OrderEntryLine?> LoadOrderEntryAsync(long entryId, long tenantId)
  168. {
  169. var rows = await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderEntryLine>(
  170. """
  171. SELECT
  172. Id, seorder_id AS SeOrderId, bill_no AS BillNo, entry_seq AS EntrySeq,
  173. item_number AS ItemNumber, item_name AS ItemName, specification AS Specification,
  174. unit AS Unit, bom_number AS BomNumber, qty AS Qty,
  175. plan_date AS PlanDate, sys_capacity_date AS SysCapacityDate,
  176. progress AS Progress, urgent AS Urgent,
  177. factory_id AS FactoryId, company_id AS CompanyId, tenant_id AS TenantId
  178. FROM crm_seorderentry
  179. WHERE Id = @Id AND tenant_id = @TenantId AND IsDeleted = 0
  180. LIMIT 1
  181. """,
  182. new SugarParameter("@Id", entryId),
  183. new SugarParameter("@TenantId", tenantId));
  184. return rows.FirstOrDefault();
  185. }
  186. private async Task<OrderWorkOrderGenerationService.OrderHeader?> LoadOrderHeaderAsync(long orderId, long tenantId)
  187. {
  188. var rows = await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderHeader>(
  189. """
  190. SELECT Id, bill_no AS BillNo, custom_no AS CustomNo, urgent AS Urgent,
  191. factory_id AS FactoryId, tenant_id AS TenantId
  192. FROM crm_seorder
  193. WHERE Id = @Id AND tenant_id = @TenantId AND IsDeleted = 0
  194. LIMIT 1
  195. """,
  196. new SugarParameter("@Id", orderId),
  197. new SugarParameter("@TenantId", tenantId));
  198. return rows.FirstOrDefault();
  199. }
  200. // ──────────────── 内部类 ────────────────
  201. private sealed class WorkOrdMasterRow
  202. {
  203. public long RecID { get; set; }
  204. public string WorkOrd { get; set; } = string.Empty;
  205. public long BusinessId { get; set; }
  206. public string? SalesJob { get; set; }
  207. public long? MorderId { get; set; }
  208. // WorkOrdMaster 字段
  209. public string? WoItemNum { get; set; }
  210. public string? WoItemName { get; set; }
  211. public decimal WoQtyOrded { get; set; }
  212. public DateTime? WoDueDate { get; set; }
  213. public int WoUrgent { get; set; }
  214. public string? WoCustNo { get; set; }
  215. public string? WoBomFormula { get; set; }
  216. public long WoTenantId { get; set; }
  217. // mes_morder 字段
  218. public string? MorderBomNumber { get; set; }
  219. public string? MorderProductCode { get; set; }
  220. public string? MorderProductName { get; set; }
  221. public string? MorderModel { get; set; }
  222. public string? MorderUnit { get; set; }
  223. }
  224. public sealed class KittingCheckResult
  225. {
  226. public int CheckedCount { get; set; }
  227. public int KittedCount { get; set; }
  228. public int ShortageCount { get; set; }
  229. }
  230. public sealed class SingleKittingCheckResult
  231. {
  232. public string WorkOrd { get; set; } = string.Empty;
  233. public bool IsKitted { get; set; }
  234. public int ShortageLineCount { get; set; }
  235. public string MaterialSituation { get; set; } = string.Empty;
  236. }
  237. }