WorkOrderKittingCheckService.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  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, bangId: 0, checkScope: "BATCH");
  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. /// <param name="bangId">
  50. /// 排程版本。&gt; 0 表示由排产驱动(净口径 + 写占用);
  51. /// = 0 表示手动/下达触发,此时自动复用当前有效排程版本,保持口径一致但只读不写。
  52. /// </param>
  53. /// <param name="checkScope">触发来源,写入 b_examine_result.check_scope 供事后追溯。</param>
  54. public async Task<SingleKittingCheckResult> CheckSingleAsync(
  55. long tenantId, string workOrd, string account, long bangId = 0, string checkScope = "SINGLE")
  56. {
  57. var wo = workOrd.Trim();
  58. // 口径统一(C-D1):手动/下达触发时也按净口径算——复用当前有效排程版本的占用账,
  59. // 否则每个工单都以为自己能独占全部库存,多工单同时下达必然超发。
  60. var isScheduleDriven = bangId > 0;
  61. if (!isScheduleDriven)
  62. bangId = await ResolveCurrentScheduleBangIdAsync(tenantId) ?? 0;
  63. // 1. 从 WorkOrdMaster + mes_morder 获取工单数据
  64. var wm = await LoadWorkOrdMasterAsync(tenantId, wo);
  65. if (wm is null)
  66. throw Oops.Oh($"工单 {wo} 不存在或已关闭");
  67. OrderWorkOrderGenerationService.OrderEntryLine entry;
  68. OrderWorkOrderGenerationService.OrderHeader order;
  69. if (wm.BusinessId > 0)
  70. {
  71. // 2a. 有关联订单明细行:从 crm_seorderentry / crm_seorder 加载
  72. var loadedEntry = await LoadOrderEntryAsync(wm.BusinessId, tenantId);
  73. if (loadedEntry is not null)
  74. {
  75. entry = loadedEntry;
  76. var loadedOrder = await LoadOrderHeaderAsync(entry.SeOrderId, tenantId);
  77. order = loadedOrder ?? BuildFallbackOrderFromWorkOrder(wm);
  78. }
  79. else
  80. {
  81. // 明细行已删除/不存在,回退到工单数据
  82. entry = BuildEntryFromWorkOrder(wm);
  83. order = BuildFallbackOrderFromWorkOrder(wm);
  84. }
  85. }
  86. else
  87. {
  88. // 2b. 无关联订单明细行(BusinessID=0):从工单构造检查参数
  89. entry = BuildEntryFromWorkOrder(wm);
  90. order = BuildFallbackOrderFromWorkOrder(wm);
  91. }
  92. // 3. 重新执行资源检查(BOM 展开 + 库存/在途缺口计算)
  93. // 非排产触发:只读不写占用(C-D3),并排除本工单自身占用(C-D2)
  94. var warnings = new List<string>();
  95. var lines = await _calculator.BuildLinesAsync(
  96. order, entry, warnings, bangId, wo, wm.MorderId ?? 0,
  97. writeOccupy: isScheduleDriven,
  98. excludeOccupyWorkOrd: wo);
  99. // 4. 将新结果写入 b_examine_result / b_bom_child_examine 并更新 mes_morder
  100. var checkResult = await _writer.WriteAsync(
  101. order, entry, wo, wm.MorderId, lines, account, DateTime.Now,
  102. bangId: bangId, checkScope: checkScope);
  103. // 5. 同步工单物料明细(WorkOrdDetail)
  104. await _materialDetailSync.EnsureFromResourceCheckAsync(tenantId, wo, account);
  105. return new SingleKittingCheckResult
  106. {
  107. WorkOrd = wo,
  108. IsKitted = !checkResult.HasShortage,
  109. ShortageLineCount = checkResult.HasShortage
  110. ? lines.Count(x => x.IsUse == 1 && x.LackQty > 0)
  111. : 0,
  112. MaterialSituation = checkResult.HasShortage ? "缺料" : "齐套"
  113. };
  114. }
  115. /// <summary>从 WorkOrdMaster + mes_morder 数据构造 OrderEntryLine。</summary>
  116. private static OrderWorkOrderGenerationService.OrderEntryLine BuildEntryFromWorkOrder(WorkOrdMasterRow wm)
  117. {
  118. return new OrderWorkOrderGenerationService.OrderEntryLine
  119. {
  120. Id = wm.BusinessId,
  121. SeOrderId = 0,
  122. BillNo = wm.SalesJob,
  123. EntrySeq = 0,
  124. ItemNumber = wm.WoItemNum,
  125. ItemName = wm.WoItemName,
  126. Specification = wm.MorderModel,
  127. Unit = wm.MorderUnit,
  128. BomNumber = wm.MorderBomNumber ?? wm.WoBomFormula,
  129. Qty = wm.WoQtyOrded,
  130. PlanDate = wm.WoDueDate,
  131. SysCapacityDate = wm.WoDueDate,
  132. Progress = null,
  133. Urgent = wm.WoUrgent,
  134. FactoryId = null,
  135. CompanyId = null,
  136. TenantId = wm.WoTenantId
  137. };
  138. }
  139. /// <summary>从 WorkOrdMaster 数据构造兜底 OrderHeader。</summary>
  140. private static OrderWorkOrderGenerationService.OrderHeader BuildFallbackOrderFromWorkOrder(WorkOrdMasterRow wm)
  141. {
  142. return new OrderWorkOrderGenerationService.OrderHeader
  143. {
  144. Id = 0,
  145. BillNo = wm.SalesJob,
  146. CustomNo = wm.WoCustNo,
  147. Urgent = wm.WoUrgent,
  148. FactoryId = null,
  149. TenantId = wm.WoTenantId
  150. };
  151. }
  152. // ──────────────── 数据加载 ────────────────
  153. /// <summary>
  154. /// 取当前有效排程版本 id(= 最近一次成功排产的 aidop_action_run_log.id,与 bang_id 同源)。
  155. /// 与 ProductionScheduleGenerationService.ResolveCurrentScheduleBangIdAsync 同逻辑;
  156. /// 无成功排产记录时返回 null,口径退化为毛口径(与改动前行为一致,不会更差)。
  157. /// </summary>
  158. private async Task<long?> ResolveCurrentScheduleBangIdAsync(long tenantId)
  159. {
  160. var id = await _db.Ado.GetLongAsync(
  161. """
  162. SELECT id FROM aidop_action_run_log
  163. WHERE tenant_id = @TenantId AND action_code = @ActionCode AND status = 'SUCCESS'
  164. ORDER BY start_time DESC, id DESC
  165. LIMIT 1
  166. """,
  167. new List<SugarParameter>
  168. {
  169. new("@TenantId", tenantId),
  170. new("@ActionCode", "S2_SCHEDULE_GENERATE")
  171. });
  172. return id > 0 ? id : null;
  173. }
  174. private async Task<WorkOrdMasterRow?> LoadWorkOrdMasterAsync(long tenantId, string workOrd)
  175. {
  176. var rows = await _db.Ado.SqlQueryAsync<WorkOrdMasterRow>(
  177. """
  178. SELECT wm.RecID, wm.WorkOrd, wm.BusinessID, wm.SalesJob,
  179. wm.ItemNum AS WoItemNum, wm.ItemName AS WoItemName,
  180. wm.QtyOrded AS WoQtyOrded, wm.DueDate AS WoDueDate,
  181. wm.Urgent AS WoUrgent, wm.CustNo AS WoCustNo,
  182. wm.BOMFormula AS WoBomFormula,
  183. wm.tenant_id AS WoTenantId,
  184. morder.Id AS MorderId,
  185. morder.bom_number AS MorderBomNumber,
  186. morder.product_code AS MorderProductCode,
  187. morder.product_name AS MorderProductName,
  188. morder.fmodel AS MorderModel,
  189. morder.unit AS MorderUnit
  190. FROM WorkOrdMaster wm
  191. LEFT JOIN mes_morder morder
  192. ON morder.morder_no = wm.WorkOrd
  193. AND morder.tenant_id = wm.tenant_id
  194. AND morder.IsDeleted = 0
  195. WHERE wm.tenant_id = @TenantId
  196. AND wm.WorkOrd = @WorkOrd
  197. AND LOWER(TRIM(IFNULL(wm.Status,''))) <> 'c'
  198. ORDER BY wm.RecID DESC
  199. LIMIT 1
  200. """,
  201. new SugarParameter("@TenantId", tenantId),
  202. new SugarParameter("@WorkOrd", workOrd));
  203. return rows.FirstOrDefault();
  204. }
  205. private async Task<OrderWorkOrderGenerationService.OrderEntryLine?> LoadOrderEntryAsync(long entryId, long tenantId)
  206. {
  207. var rows = await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderEntryLine>(
  208. """
  209. SELECT
  210. Id, seorder_id AS SeOrderId, bill_no AS BillNo, entry_seq AS EntrySeq,
  211. item_number AS ItemNumber, item_name AS ItemName, specification AS Specification,
  212. unit AS Unit, bom_number AS BomNumber, qty AS Qty,
  213. plan_date AS PlanDate, sys_capacity_date AS SysCapacityDate,
  214. progress AS Progress, urgent AS Urgent,
  215. factory_id AS FactoryId, company_id AS CompanyId, tenant_id AS TenantId
  216. FROM crm_seorderentry
  217. WHERE Id = @Id AND tenant_id = @TenantId AND IsDeleted = 0
  218. LIMIT 1
  219. """,
  220. new SugarParameter("@Id", entryId),
  221. new SugarParameter("@TenantId", tenantId));
  222. return rows.FirstOrDefault();
  223. }
  224. private async Task<OrderWorkOrderGenerationService.OrderHeader?> LoadOrderHeaderAsync(long orderId, long tenantId)
  225. {
  226. var rows = await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderHeader>(
  227. """
  228. SELECT Id, bill_no AS BillNo, custom_no AS CustomNo, urgent AS Urgent,
  229. factory_id AS FactoryId, tenant_id AS TenantId
  230. FROM crm_seorder
  231. WHERE Id = @Id AND tenant_id = @TenantId AND IsDeleted = 0
  232. LIMIT 1
  233. """,
  234. new SugarParameter("@Id", orderId),
  235. new SugarParameter("@TenantId", tenantId));
  236. return rows.FirstOrDefault();
  237. }
  238. // ──────────────── 内部类 ────────────────
  239. private sealed class WorkOrdMasterRow
  240. {
  241. public long RecID { get; set; }
  242. public string WorkOrd { get; set; } = string.Empty;
  243. public long BusinessId { get; set; }
  244. public string? SalesJob { get; set; }
  245. public long? MorderId { get; set; }
  246. // WorkOrdMaster 字段
  247. public string? WoItemNum { get; set; }
  248. public string? WoItemName { get; set; }
  249. public decimal WoQtyOrded { get; set; }
  250. public DateTime? WoDueDate { get; set; }
  251. public int WoUrgent { get; set; }
  252. public string? WoCustNo { get; set; }
  253. public string? WoBomFormula { get; set; }
  254. public long WoTenantId { get; set; }
  255. // mes_morder 字段
  256. public string? MorderBomNumber { get; set; }
  257. public string? MorderProductCode { get; set; }
  258. public string? MorderProductName { get; set; }
  259. public string? MorderModel { get; set; }
  260. public string? MorderUnit { get; set; }
  261. }
  262. public sealed class KittingCheckResult
  263. {
  264. public int CheckedCount { get; set; }
  265. public int KittedCount { get; set; }
  266. public int ShortageCount { get; set; }
  267. }
  268. public sealed class SingleKittingCheckResult
  269. {
  270. public string WorkOrd { get; set; } = string.Empty;
  271. public bool IsKitted { get; set; }
  272. public int ShortageLineCount { get; set; }
  273. public string MaterialSituation { get; set; } = string.Empty;
  274. }
  275. }