OrderReviewOrchestrationService.cs 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048
  1. using Admin.NET.Plugin.AiDOP.DataPlatform.S1Refresh;
  2. using Admin.NET.Plugin.AiDOP.DataPlatform.Wms;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure;
  4. using Admin.NET.Plugin.AiDOP.Production;
  5. using Admin.NET.Plugin.AiDOP.Supply;
  6. using Admin.NET.Plugin.AiDOP.WorkOrder;
  7. using Microsoft.Extensions.DependencyInjection;
  8. using Yitter.IdGenerator;
  9. namespace Admin.NET.Plugin.AiDOP.Order;
  10. /// <summary>
  11. /// S1 订单评审编排:资源检查、状态更新、工单生成、运行日志。
  12. /// </summary>
  13. public class OrderReviewOrchestrationService : ITransient
  14. {
  15. public const string ActionReview = "S1_ORDER_REVIEW";
  16. public const string ActionConfirm = "S1_DELIVERY_CONFIRM";
  17. public const string ActionRefresh = "S1_ORDER_REFRESH_PLAN";
  18. /// <summary>当前评审批次 run_id(雪花),用于跨工单库存占用递减;每次评审/刷新重新分配。</summary>
  19. private long _currentReviewBangId;
  20. private readonly ISqlSugarClient _db;
  21. private readonly UserManager _userManager;
  22. private readonly OrderWorkOrderGenerationService _workOrderGen;
  23. private readonly OrderResourceCheckService _resourceCheck;
  24. private readonly WorkOrderMaterialDetailSyncService _materialDetailSync;
  25. private readonly WorkOrderRoutingSyncService _routingSync;
  26. private readonly MdpRefreshDispatcher _refreshDispatcher;
  27. private readonly AidopActionRunLogWriter _runLog;
  28. private readonly ProductionScheduleGenerationService _scheduleGen;
  29. private readonly ProcurementPipelineService _pipeline;
  30. private readonly WorkOrdMesPushService _workOrdMesPush;
  31. private readonly MaterialRequirementCalculator _calculator;
  32. private readonly WorkOrderKittingCheckService _kittingCheckService;
  33. private readonly PickBillNbrSyncService _pickNbrSync;
  34. public OrderReviewOrchestrationService(
  35. ISqlSugarClient db,
  36. UserManager userManager,
  37. OrderWorkOrderGenerationService workOrderGen,
  38. OrderResourceCheckService resourceCheck,
  39. WorkOrderMaterialDetailSyncService materialDetailSync,
  40. WorkOrderRoutingSyncService routingSync,
  41. MdpRefreshDispatcher refreshDispatcher,
  42. AidopActionRunLogWriter runLog,
  43. ProductionScheduleGenerationService scheduleGen,
  44. ProcurementPipelineService pipeline,
  45. WorkOrdMesPushService workOrdMesPush,
  46. MaterialRequirementCalculator calculator,
  47. WorkOrderKittingCheckService kittingCheckService,
  48. PickBillNbrSyncService pickNbrSync)
  49. {
  50. _db = db;
  51. _userManager = userManager;
  52. _workOrderGen = workOrderGen;
  53. _resourceCheck = resourceCheck;
  54. _materialDetailSync = materialDetailSync;
  55. _routingSync = routingSync;
  56. _refreshDispatcher = refreshDispatcher;
  57. _runLog = runLog;
  58. _scheduleGen = scheduleGen;
  59. _pipeline = pipeline;
  60. _workOrdMesPush = workOrdMesPush;
  61. _calculator = calculator;
  62. _kittingCheckService = kittingCheckService;
  63. _pickNbrSync = pickNbrSync;
  64. }
  65. public Task<SeOrderReviewExecuteResult> ReviewAsync(IReadOnlyList<long> orderIds) =>
  66. ExecuteBatchAsync(ActionReview, orderIds, ReviewOneOrderAsync);
  67. public Task<SeOrderReviewExecuteResult> ConfirmDeliveryAsync(IReadOnlyList<long> orderIds) =>
  68. ExecuteBatchAsync(ActionConfirm, orderIds, ConfirmOneOrderAsync);
  69. public Task<SeOrderReviewExecuteResult> RefreshPlanAsync(long orderId, string? reason) =>
  70. ExecuteSingleAsync(ActionRefresh, orderId, async (order, result, warnings, account) =>
  71. {
  72. // ── 第0步:物料编码变更校验 ──
  73. await ValidateMaterialNotChangedAsync(order.Id, order.TenantId);
  74. // ── 第1步:使用新 run_id 写入占用(旧版本在批次结束后清理,崩溃安全) ──
  75. EnsureReviewBangId();
  76. // ── 第2步:加载可重排明细行(确认 / 再评审) ──
  77. var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["3", "0"]);
  78. if (entries.Count == 0)
  79. throw Oops.Oh("订单没有可重排的确认/再评审明细行");
  80. // ── 第3步:逐条处理(工单 + 资源检查 + 领料单 + 交期更新) ──
  81. foreach (var entry in entries)
  82. {
  83. ValidateEntryForResourceCheck(entry);
  84. var wo = await _workOrderGen.CreateOrUpdateForEntryAsync(order, entry, account, warnings);
  85. if (wo.Created) result.WorkOrderCreatedCount++;
  86. else result.WorkOrderUpdatedCount++;
  87. result.WorkOrders.Add(wo.WorkOrd);
  88. var check = await _resourceCheck.RunForEntryAsync(order, entry, wo.WorkOrd, account, warnings, _currentReviewBangId);
  89. result.ResourceCheckCount++;
  90. result.ResourceCheckLineCount += check.LineCount;
  91. await _materialDetailSync.EnsureFromResourceCheckAsync(entry.TenantId, wo.WorkOrd, account);
  92. await _routingSync.EnsureFromRoutingAsync(entry.TenantId, wo.WorkOrd, account);
  93. await _workOrdMesPush.TryEnqueueSnapshotSafeAsync(entry.TenantId, wo.WorkOrd, warnings);
  94. // 当工单状态为下达/投产/暂停(R、W、S)时,更新对应领料单数据
  95. await UpdatePickingListForActiveWorkOrderAsync(entry.TenantId, wo.WorkOrd, account, warnings);
  96. // 计算齐套率并回写到领料单(NbrMaster.MFRate / NbrDetail.SatisfyQty)
  97. await _kittingCheckService.WriteBackMFRateAsync(entry.TenantId, wo.WorkOrd);
  98. // 根据资源检查结果更新明细行系统建议交期
  99. await UpdateEntrySysCapacityDateAsync(entry.Id, check.KittingTime, account);
  100. }
  101. // ── 第4步:更新明细行进度为3 ──
  102. await UpdateEntriesProgressAsync(entries.Select(e => e.Id).ToList(), "3", account);
  103. // ── 第4.5步:重新进行生产排程(在事务提交前) ──
  104. try
  105. {
  106. var scheduleResult = await _scheduleGen.GenerateAsync(order.TenantId, order.TenantId.ToString(), account);
  107. if (!string.IsNullOrWhiteSpace(scheduleResult.Message))
  108. warnings.Add($"生产排程:{scheduleResult.Message}");
  109. }
  110. catch (Exception ex)
  111. {
  112. warnings.Add($"生产排程失败:{ex.Message}");
  113. }
  114. // ── 第4.5步:同步物料需求(MRP → PR → 采购闭环) ──
  115. try
  116. {
  117. var mrResult = await _pipeline.ExecuteCoreAsync(order.TenantId, account, createFromShortage: true);
  118. if (!string.IsNullOrWhiteSpace(mrResult.Message))
  119. warnings.Add($"物料需求同步:{mrResult.Message}");
  120. }
  121. catch (Exception ex)
  122. {
  123. warnings.Add($"物料需求同步失败:{ex.Message}");
  124. }
  125. result.EntryCount = entries.Count;
  126. result.Message = string.IsNullOrWhiteSpace(reason)
  127. ? "3级计划重排完成"
  128. : $"3级计划重排完成:{reason.Trim()}";
  129. });
  130. private async Task<SeOrderReviewExecuteResult> ExecuteBatchAsync(
  131. string actionCode,
  132. IReadOnlyList<long> orderIds,
  133. Func<OrderWorkOrderGenerationService.OrderHeader, SeOrderReviewExecuteResult, List<string>, string, Task> handler)
  134. {
  135. if (orderIds is null || orderIds.Count == 0)
  136. throw Oops.Oh("至少选择一条订单");
  137. var tenantId = _userManager.TenantId > 0
  138. ? _userManager.TenantId
  139. : AidopTenantHelper.Resolve(App.HttpContext);
  140. var account = _userManager.Account ?? "system";
  141. var distinctIds = orderIds.Distinct().ToList();
  142. var aggregate = new SeOrderReviewExecuteResult
  143. {
  144. ActionCode = actionCode,
  145. OrderCount = distinctIds.Count,
  146. Message = "执行成功"
  147. };
  148. var allWarnings = new List<string>();
  149. long? firstLogId = null;
  150. // 评审/重排:分配新 run_id,跨本批次订单共享以正确递减库存
  151. if (actionCode == ActionReview || actionCode == ActionRefresh)
  152. EnsureReviewBangId();
  153. foreach (var orderId in distinctIds)
  154. {
  155. var order = await LoadOrderAsync(orderId, tenantId)
  156. ?? throw Oops.Oh($"订单 {orderId} 不存在或不属于当前租户");
  157. var runLogId = await _runLog.StartAsync(actionCode, tenantId, "crm_seorder", order.Id, order.BillNo);
  158. if (firstLogId is null)
  159. firstLogId = runLogId;
  160. var perOrder = new SeOrderReviewExecuteResult { ActionCode = actionCode };
  161. var warnings = new List<string>();
  162. try
  163. {
  164. await _db.Ado.BeginTranAsync();
  165. await handler(order, perOrder, warnings, account);
  166. await _db.Ado.CommitTranAsync();
  167. aggregate.EntryCount += perOrder.EntryCount;
  168. aggregate.WorkOrderCreatedCount += perOrder.WorkOrderCreatedCount;
  169. aggregate.WorkOrderUpdatedCount += perOrder.WorkOrderUpdatedCount;
  170. aggregate.WorkOrderClosedCount += perOrder.WorkOrderClosedCount;
  171. aggregate.ResourceCheckCount += perOrder.ResourceCheckCount;
  172. aggregate.ResourceCheckLineCount += perOrder.ResourceCheckLineCount;
  173. aggregate.WorkOrders.AddRange(perOrder.WorkOrders);
  174. allWarnings.AddRange(warnings);
  175. await _runLog.SuccessAsync(runLogId, perOrder.Message, new
  176. {
  177. orderId = order.Id,
  178. billNo = order.BillNo,
  179. perOrder.EntryCount,
  180. perOrder.WorkOrderCreatedCount,
  181. perOrder.WorkOrderUpdatedCount,
  182. workOrders = perOrder.WorkOrders,
  183. warnings
  184. });
  185. }
  186. catch (Exception ex)
  187. {
  188. await _db.Ado.RollbackTranAsync();
  189. await _runLog.FailedAsync(runLogId, ex.Message, new { orderId = order.Id, billNo = order.BillNo });
  190. throw Oops.Oh(ex.Message);
  191. }
  192. }
  193. aggregate.RunLogId = firstLogId ?? 0;
  194. aggregate.Warnings = allWarnings.Distinct().ToList();
  195. aggregate.Message = BuildAggregateMessage(actionCode, aggregate);
  196. if ((actionCode == ActionReview || actionCode == ActionRefresh) && _currentReviewBangId > 0)
  197. await CleanupStaleReviewOccupyAsync(tenantId, _currentReviewBangId);
  198. if (actionCode == ActionReview && aggregate.ResourceCheckCount > 0)
  199. aggregate.Warnings.AddRange(TryTriggerMdpRefresh());
  200. return aggregate;
  201. }
  202. private async Task<SeOrderReviewExecuteResult> ExecuteSingleAsync(
  203. string actionCode,
  204. long orderId,
  205. Func<OrderWorkOrderGenerationService.OrderHeader, SeOrderReviewExecuteResult, List<string>, string, Task> handler)
  206. {
  207. var tenantId = _userManager.TenantId > 0
  208. ? _userManager.TenantId
  209. : AidopTenantHelper.Resolve(App.HttpContext);
  210. var account = _userManager.Account ?? "system";
  211. var order = await LoadOrderAsync(orderId, tenantId)
  212. ?? throw Oops.Oh("订单不存在或不属于当前租户");
  213. var result = new SeOrderReviewExecuteResult
  214. {
  215. ActionCode = actionCode,
  216. OrderCount = 1
  217. };
  218. var warnings = new List<string>();
  219. var runLogId = await _runLog.StartAsync(actionCode, tenantId, "crm_seorder", order.Id, order.BillNo);
  220. result.RunLogId = runLogId;
  221. try
  222. {
  223. await _db.Ado.BeginTranAsync();
  224. await handler(order, result, warnings, account);
  225. await _db.Ado.CommitTranAsync();
  226. result.Warnings = warnings;
  227. result.Message = BuildSingleMessage(result);
  228. await _runLog.SuccessAsync(runLogId, result.Message, new
  229. {
  230. orderId = order.Id,
  231. billNo = order.BillNo,
  232. result.EntryCount,
  233. result.WorkOrderCreatedCount,
  234. result.WorkOrderUpdatedCount,
  235. workOrders = result.WorkOrders,
  236. warnings
  237. });
  238. if (actionCode == ActionRefresh && _currentReviewBangId > 0)
  239. await CleanupStaleReviewOccupyAsync(tenantId, _currentReviewBangId);
  240. if (actionCode == ActionRefresh && result.ResourceCheckCount > 0)
  241. result.Warnings.AddRange(TryTriggerMdpRefresh());
  242. return result;
  243. }
  244. catch (Exception ex)
  245. {
  246. await _db.Ado.RollbackTranAsync();
  247. await _runLog.FailedAsync(runLogId, ex.Message, new { orderId = order.Id, billNo = order.BillNo });
  248. throw Oops.Oh(ex.Message);
  249. }
  250. }
  251. private void EnsureReviewBangId()
  252. {
  253. if (_currentReviewBangId <= 0)
  254. _currentReviewBangId = YitIdHelper.NextId();
  255. }
  256. /// <summary>
  257. /// 清理本租户非当前 S1 版本、且非当前 S2 版本的占用行(保留 legacy S2 bang_id=1 直至首次 S2 全量排程切换)。
  258. /// </summary>
  259. private async Task CleanupStaleReviewOccupyAsync(long tenantId, long currentS1BangId)
  260. {
  261. var s2BangId = await _db.Ado.GetLongAsync(
  262. """
  263. SELECT id FROM aidop_action_run_log
  264. WHERE tenant_id = @TenantId AND action_code = 'S2_SCHEDULE_GENERATE' AND status = 'SUCCESS'
  265. ORDER BY start_time DESC, id DESC
  266. LIMIT 1
  267. """,
  268. new List<SugarParameter> { new("@TenantId", tenantId) });
  269. const long legacyS2BangId = 1;
  270. await _db.Ado.ExecuteCommandAsync(
  271. """
  272. DELETE FROM ic_item_stockoccupy
  273. WHERE tenant_id = @TenantId
  274. AND bang_id <> @S1BangId
  275. AND bang_id <> @S2BangId
  276. AND bang_id <> @LegacyS2
  277. """,
  278. new SugarParameter("@TenantId", tenantId),
  279. new SugarParameter("@S1BangId", currentS1BangId),
  280. new SugarParameter("@S2BangId", s2BangId > 0 ? s2BangId : -1L),
  281. new SugarParameter("@LegacyS2", legacyS2BangId));
  282. await _db.Ado.ExecuteCommandAsync(
  283. """
  284. DELETE FROM srm_po_occupy
  285. WHERE tenant_id = @TenantId
  286. AND bang_id <> @S1BangId
  287. AND bang_id <> @S2BangId
  288. AND bang_id <> @LegacyS2
  289. """,
  290. new SugarParameter("@TenantId", tenantId),
  291. new SugarParameter("@S1BangId", currentS1BangId),
  292. new SugarParameter("@S2BangId", s2BangId > 0 ? s2BangId : -1L),
  293. new SugarParameter("@LegacyS2", legacyS2BangId));
  294. }
  295. /// <summary>
  296. /// 清理本订单所有明细行的旧 status=0 预占记录(b_stock_occupy / b_purchase_occupy / b_mo_occupy)。
  297. /// 每次评审前调用,确保幂等。
  298. /// </summary>
  299. private async Task CleanupReviewOccupyAsync(long tenantId, List<long> entryIds)
  300. {
  301. if (entryIds.Count == 0) return;
  302. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  303. var placeholders = new List<string>();
  304. for (var i = 0; i < entryIds.Count; i++)
  305. {
  306. placeholders.Add($"@e{i}");
  307. pars.Add(new SugarParameter($"@e{i}", entryIds[i]));
  308. }
  309. var inClause = string.Join(",", placeholders);
  310. // b_stock_occupy(无 IsDeleted 字段)
  311. try { await _db.Ado.ExecuteCommandAsync(
  312. $"DELETE FROM b_stock_occupy WHERE tenant_id = @TenantId AND status = 0 AND entry_id IN ({inClause})",
  313. pars); } catch { /* 表可能缺少 status 列,跳过 */ }
  314. // b_purchase_occupy(有 IsDeleted 字段)
  315. try { await _db.Ado.ExecuteCommandAsync(
  316. $"DELETE FROM b_purchase_occupy WHERE tenant_id = @TenantId AND status = 0 AND IFNULL(IsDeleted, 0) = 0 AND entry_id IN ({inClause})",
  317. pars); } catch { /* 表可能缺少列,跳过 */ }
  318. // b_mo_occupy(有 IsDeleted 字段)
  319. try { await _db.Ado.ExecuteCommandAsync(
  320. $"DELETE FROM b_mo_occupy WHERE tenant_id = @TenantId AND status = 0 AND IFNULL(IsDeleted, 0) = 0 AND entry_id IN ({inClause})",
  321. pars); } catch { /* 表可能缺少列,跳过 */ }
  322. }
  323. /// <summary>
  324. /// 交期确认时将指定明细行的三张占用表 status 从 0 更新为 1。
  325. /// </summary>
  326. private async Task ConfirmReviewOccupyAsync(long tenantId, long entryId)
  327. {
  328. var now = DateTime.Now;
  329. var pars = new List<SugarParameter>
  330. {
  331. new("@TenantId", tenantId),
  332. new("@EntryId", entryId),
  333. new("@Now", now)
  334. };
  335. // b_stock_occupy(无 IsDeleted 字段)
  336. try { await _db.Ado.ExecuteCommandAsync(
  337. "UPDATE b_stock_occupy SET status = 1, update_time = @Now WHERE tenant_id = @TenantId AND entry_id = @EntryId AND status = 0",
  338. pars); } catch { /* 表可能缺少 status 列,跳过 */ }
  339. // b_purchase_occupy(有 IsDeleted 字段)
  340. try { await _db.Ado.ExecuteCommandAsync(
  341. "UPDATE b_purchase_occupy SET status = 1, update_time = @Now WHERE tenant_id = @TenantId AND entry_id = @EntryId AND status = 0 AND IFNULL(IsDeleted, 0) = 0",
  342. pars); } catch { /* 表可能缺少列,跳过 */ }
  343. // b_mo_occupy(有 IsDeleted 字段)
  344. try { await _db.Ado.ExecuteCommandAsync(
  345. "UPDATE b_mo_occupy SET status = 1, update_time = @Now WHERE tenant_id = @TenantId AND entry_id = @EntryId AND status = 0 AND IFNULL(IsDeleted, 0) = 0",
  346. pars); } catch { /* 表可能缺少列,跳过 */ }
  347. }
  348. private async Task ReviewOneOrderAsync(
  349. OrderWorkOrderGenerationService.OrderHeader order,
  350. SeOrderReviewExecuteResult result,
  351. List<string> warnings,
  352. string account)
  353. {
  354. var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["1", "0", "2"]);
  355. if (entries.Count == 0)
  356. throw Oops.Oh($"订单 {order.BillNo} 没有可评审的明细行(须为新建,评审,再评审状态)");
  357. // 清理本订单所有明细行的旧 status=0 预占记录(三张表)
  358. await CleanupReviewOccupyAsync(order.TenantId, entries.Select(e => e.Id).ToList());
  359. foreach (var entry in entries)
  360. {
  361. ValidateEntryForResourceCheck(entry);
  362. if (entry.PlanDate is null)
  363. throw Oops.Oh($"订单行 {entry.EntrySeq} 缺少客户要求交期(plan_date)");
  364. // 1. 先做资源检查(纯 BOM 展开 + 库存计算,不依赖工单),记录库存占用
  365. EnsureReviewBangId();
  366. var (check, lines) = await _resourceCheck.CheckOnlyAsync(order, entry, warnings, _currentReviewBangId);
  367. result.ResourceCheckCount++;
  368. result.ResourceCheckLineCount += check.LineCount;
  369. long? examineId = null;
  370. // 2. 有缺料 → 才生成工单并写入资源检查结果;库存可满足则跳过工单
  371. if (check.HasShortage)
  372. {
  373. var wo = await _workOrderGen.CreateOrUpdateForEntryAsync(order, entry, account, warnings);
  374. if (wo.Created) result.WorkOrderCreatedCount++;
  375. else result.WorkOrderUpdatedCount++;
  376. result.WorkOrders.Add(wo.WorkOrd);
  377. var checkResult = await _resourceCheck.WriteResultAsync(order, entry, wo.WorkOrd, lines, account);
  378. examineId = checkResult.ExamineId;
  379. await _materialDetailSync.EnsureFromResourceCheckAsync(entry.TenantId, wo.WorkOrd, account);
  380. await _routingSync.EnsureFromRoutingAsync(entry.TenantId, wo.WorkOrd, account);
  381. await _workOrdMesPush.TryEnqueueSnapshotSafeAsync(entry.TenantId, wo.WorkOrd, warnings);
  382. warnings.Add($"订单行 {entry.EntrySeq} 存在缺料(工单 {wo.WorkOrd})");
  383. }
  384. else
  385. {
  386. // 3. 不缺料 → 若之前已生成工单则关闭(Status='C', IsActive=0)
  387. var closedCount = await CloseExistingWorkOrdersAsync(entry.TenantId, entry.Id, account);
  388. if (closedCount > 0)
  389. {
  390. result.WorkOrderClosedCount += closedCount;
  391. warnings.Add($"订单行 {entry.EntrySeq} 库存可满足,已关闭 {closedCount} 个历史工单");
  392. }
  393. }
  394. // 写入订单评审三表占用追踪(status=0 预占)
  395. await _calculator.WriteReviewOccupyAsync(
  396. lines, entry.TenantId, entry.Id, order.BillNo ?? "", examineId);
  397. await UpdateEntryAfterReviewAsync(entry.Id, check.KittingTime, account);
  398. }
  399. result.EntryCount = entries.Count;
  400. result.Message = $"订单 {order.BillNo} 评审完成(资源检查 {result.ResourceCheckCount} 条)";
  401. }
  402. private async Task ConfirmOneOrderAsync(
  403. OrderWorkOrderGenerationService.OrderHeader order,
  404. SeOrderReviewExecuteResult result,
  405. List<string> warnings,
  406. string account)
  407. {
  408. var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["2"]);
  409. if (entries.Count == 0)
  410. throw Oops.Oh($"订单 {order.BillNo} 没有处于评审状态的明细行,请先完成订单评审");
  411. foreach (var entry in entries)
  412. {
  413. var woCnt = await _db.Ado.GetIntAsync(
  414. """
  415. SELECT COUNT(*) FROM WorkOrdMaster
  416. WHERE tenant_id = @TenantId AND BusinessID = @EntryId
  417. AND LOWER(TRIM(IFNULL(Status,''))) <> 'c'
  418. """,
  419. new SugarParameter("@TenantId", entry.TenantId),
  420. new SugarParameter("@EntryId", entry.Id));
  421. // 无活跃工单:可能从未生成(库存可满足)或已关闭 → 仍允许交期确认
  422. var confirmDate = entry.SysCapacityDate ?? entry.PlanDate;
  423. // 交期确认:三张占用表 status 0 -> 1
  424. await ConfirmReviewOccupyAsync(entry.TenantId, entry.Id);
  425. await UpdateEntryAfterConfirmAsync(entry.Id, confirmDate, account);
  426. result.EntryCount++;
  427. if (woCnt > 0)
  428. {
  429. var workOrd = await LoadWorkOrdForEntryAsync(entry.TenantId, entry.Id);
  430. result.WorkOrders.Add(workOrd);
  431. // 交期确认后将工单状态设为 p,同步 OrdDate 和 DueDate
  432. if (!string.IsNullOrWhiteSpace(workOrd))
  433. {
  434. var confirmDateForWo = confirmDate ?? DateTime.Now;
  435. await _db.Ado.ExecuteCommandAsync(
  436. """
  437. UPDATE WorkOrdMaster
  438. SET Status = 'p',
  439. OrdDate = COALESCE(OrdDate, @OrdDate),
  440. DueDate = @DueDate,
  441. UpdateUser = @User, UpdateTime = @Now
  442. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
  443. AND IFNULL(TRIM(Status), '') = ''
  444. """,
  445. new SugarParameter("@TenantId", entry.TenantId),
  446. new SugarParameter("@WorkOrd", workOrd),
  447. new SugarParameter("@OrdDate", DateTime.Now.Date),
  448. new SugarParameter("@DueDate", confirmDateForWo.Date),
  449. new SugarParameter("@User", account),
  450. new SugarParameter("@Now", DateTime.Now));
  451. // 交期确认后同步工单物料明细,使其与资源检查子物料一致
  452. await _materialDetailSync.EnsureFromResourceCheckAsync(entry.TenantId, workOrd, account);
  453. await _workOrdMesPush.TryEnqueueSnapshotSafeAsync(entry.TenantId, workOrd, warnings);
  454. }
  455. }
  456. else
  457. warnings.Add($"订单行 {entry.EntrySeq} 无活跃工单(库存可满足或已关闭),已直接确认交期");
  458. }
  459. result.WorkOrders = result.WorkOrders.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList();
  460. result.Message = $"订单 {order.BillNo} 交期确认完成";
  461. }
  462. private async Task<string> LoadWorkOrdForEntryAsync(long tenantId, long entryId)
  463. {
  464. var rows = await _db.Ado.SqlQueryAsync<string>(
  465. """
  466. SELECT WorkOrd FROM WorkOrdMaster
  467. WHERE tenant_id = @TenantId AND BusinessID = @EntryId
  468. ORDER BY RecID DESC LIMIT 1
  469. """,
  470. new SugarParameter("@TenantId", tenantId),
  471. new SugarParameter("@EntryId", entryId));
  472. return rows.FirstOrDefault() ?? string.Empty;
  473. }
  474. /// <summary>
  475. /// 关闭指定订单行已有的未关闭工单(WorkOrdMaster.Status='C', IsActive=0;mes_morder.morder_state='关闭')。
  476. /// 返回被关闭的工单数量。
  477. /// </summary>
  478. private async Task<int> CloseExistingWorkOrdersAsync(long tenantId, long entryId, string account)
  479. {
  480. // 查找该订单行下所有未关闭的工单号
  481. var openWorkOrds = await _db.Ado.SqlQueryAsync<string>(
  482. """
  483. SELECT WorkOrd FROM WorkOrdMaster
  484. WHERE tenant_id = @TenantId AND BusinessID = @EntryId
  485. AND LOWER(TRIM(IFNULL(Status,''))) <> 'c'
  486. AND IFNULL(IsActive, 0) = 1
  487. """,
  488. new SugarParameter("@TenantId", tenantId),
  489. new SugarParameter("@EntryId", entryId));
  490. if (openWorkOrds.Count == 0) return 0;
  491. var workOrdList = string.Join(",", openWorkOrds.Select(w => $"'{w}'"));
  492. var now = DateTime.Now;
  493. // 关闭 WorkOrdMaster
  494. await _db.Ado.ExecuteCommandAsync(
  495. $"""
  496. UPDATE WorkOrdMaster
  497. SET Status = 'C', IsActive = 0,
  498. UpdateUser = @User, UpdateTime = @Now
  499. WHERE tenant_id = @TenantId AND BusinessID = @EntryId
  500. AND LOWER(TRIM(IFNULL(Status,''))) <> 'c'
  501. AND IFNULL(IsActive, 0) = 1
  502. """,
  503. new SugarParameter("@TenantId", tenantId),
  504. new SugarParameter("@EntryId", entryId),
  505. new SugarParameter("@User", account),
  506. new SugarParameter("@Now", now));
  507. // 同步关闭 mes_morder
  508. await _db.Ado.ExecuteCommandAsync(
  509. $"""
  510. UPDATE mes_morder
  511. SET morder_state = '关闭',
  512. update_by_name = @User, update_time = @Now
  513. WHERE tenant_id = @TenantId AND morder_no IN ({workOrdList})
  514. AND IFNULL(morder_state, '') <> '关闭'
  515. """,
  516. new SugarParameter("@TenantId", tenantId),
  517. new SugarParameter("@User", account),
  518. new SugarParameter("@Now", now));
  519. return openWorkOrds.Count;
  520. }
  521. private static void ValidateEntryForResourceCheck(OrderWorkOrderGenerationService.OrderEntryLine entry)
  522. {
  523. if (string.IsNullOrWhiteSpace(entry.ItemNumber))
  524. throw Oops.Oh($"订单行 {entry.EntrySeq} 物料编码不能为空");
  525. if (entry.Qty is null or <= 0)
  526. throw Oops.Oh($"订单行 {entry.EntrySeq} 数量必须大于 0");
  527. if (entry.PlanDate is null && entry.SysCapacityDate is null)
  528. throw Oops.Oh($"订单行 {entry.EntrySeq} 缺少计划交期");
  529. }
  530. private async Task<OrderWorkOrderGenerationService.OrderHeader?> LoadOrderAsync(long orderId, long tenantId)
  531. {
  532. var rows = await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderHeader>(
  533. """
  534. SELECT Id, bill_no AS BillNo, custom_no AS CustomNo, urgent AS Urgent,
  535. factory_id AS FactoryId, tenant_id AS TenantId
  536. FROM crm_seorder
  537. WHERE Id = @Id AND tenant_id = @TenantId AND IsDeleted = 0
  538. LIMIT 1
  539. """,
  540. new SugarParameter("@Id", orderId),
  541. new SugarParameter("@TenantId", tenantId));
  542. return rows.FirstOrDefault();
  543. }
  544. private async Task<List<OrderWorkOrderGenerationService.OrderEntryLine>> LoadReviewableEntriesAsync(
  545. long orderId,
  546. long tenantId,
  547. IReadOnlyList<string> progressList)
  548. {
  549. if (progressList.Count == 0)
  550. return new List<OrderWorkOrderGenerationService.OrderEntryLine>();
  551. var inClause = string.Join(", ", progressList.Select((_, i) => $"@P{i}"));
  552. var pars = new List<SugarParameter>
  553. {
  554. new("@OrderId", orderId),
  555. new("@TenantId", tenantId)
  556. };
  557. for (var i = 0; i < progressList.Count; i++)
  558. pars.Add(new SugarParameter($"@P{i}", progressList[i]));
  559. return await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderEntryLine>(
  560. $"""
  561. SELECT
  562. Id, seorder_id AS SeOrderId, bill_no AS BillNo, entry_seq AS EntrySeq,
  563. item_number AS ItemNumber, item_name AS ItemName, specification AS Specification,
  564. unit AS Unit, bom_number AS BomNumber, qty AS Qty,
  565. plan_date AS PlanDate, sys_capacity_date AS SysCapacityDate,
  566. progress AS Progress, urgent AS Urgent,
  567. factory_id AS FactoryId, company_id AS CompanyId, tenant_id AS TenantId
  568. FROM crm_seorderentry
  569. WHERE seorder_id = @OrderId AND tenant_id = @TenantId AND IsDeleted = 0
  570. AND COALESCE(NULLIF(progress, ''), '1') IN ({inClause})
  571. ORDER BY entry_seq, Id
  572. """,
  573. pars);
  574. }
  575. private async Task UpdateEntryAfterReviewAsync(long entryId, DateTime? capacityDate, string account)
  576. {
  577. await _db.Ado.ExecuteCommandAsync(
  578. """
  579. UPDATE crm_seorderentry
  580. SET progress = '2',
  581. sys_capacity_date = @CapacityDate,
  582. update_time = @Now
  583. WHERE Id = @Id AND IsDeleted = 0
  584. """,
  585. new SugarParameter("@CapacityDate", capacityDate ?? (object)DBNull.Value),
  586. new SugarParameter("@Now", DateTime.Now),
  587. new SugarParameter("@Id", entryId));
  588. }
  589. private async Task UpdateEntryAfterConfirmAsync(long entryId, DateTime? confirmDate, string account)
  590. {
  591. await _db.Ado.ExecuteCommandAsync(
  592. """
  593. UPDATE crm_seorderentry
  594. SET progress = '3',
  595. date = COALESCE(date, @ConfirmDate),
  596. update_time = @Now
  597. WHERE Id = @Id AND IsDeleted = 0
  598. """,
  599. new SugarParameter("@ConfirmDate", confirmDate ?? (object)DBNull.Value),
  600. new SugarParameter("@Now", DateTime.Now),
  601. new SugarParameter("@Id", entryId));
  602. }
  603. private async Task UpdateEntriesProgressAsync(IReadOnlyList<long> entryIds, string progress, string account)
  604. {
  605. if (entryIds.Count == 0) return;
  606. var idList = string.Join(",", entryIds);
  607. await _db.Ado.ExecuteCommandAsync(
  608. $"""
  609. UPDATE crm_seorderentry
  610. SET progress = @Progress, update_time = @Now
  611. WHERE Id IN ({idList}) AND IsDeleted = 0
  612. """,
  613. new SugarParameter("@Progress", progress),
  614. new SugarParameter("@Now", DateTime.Now));
  615. }
  616. private static string BuildAggregateMessage(string actionCode, SeOrderReviewExecuteResult r)
  617. {
  618. var woPart = r.WorkOrders.Count > 0
  619. ? $",工单:{string.Join("、", r.WorkOrders.Distinct())}"
  620. : string.Empty;
  621. var closedPart = r.WorkOrderClosedCount > 0
  622. ? $"、关闭 {r.WorkOrderClosedCount}"
  623. : string.Empty;
  624. return actionCode switch
  625. {
  626. ActionReview => $"评审完成 {r.OrderCount} 单、{r.EntryCount} 行,新建工单 {r.WorkOrderCreatedCount}、更新 {r.WorkOrderUpdatedCount}{closedPart}、资源检查 {r.ResourceCheckCount} 条{woPart}",
  627. ActionConfirm => $"交期确认完成 {r.OrderCount} 单、{r.EntryCount} 行{woPart}",
  628. _ => r.Message
  629. };
  630. }
  631. private static string BuildSingleMessage(SeOrderReviewExecuteResult r)
  632. {
  633. if (r.WorkOrders.Count == 0)
  634. return r.Message;
  635. return $"{r.Message},工单:{string.Join("、", r.WorkOrders.Distinct())}";
  636. }
  637. private List<string> TryTriggerMdpRefresh()
  638. {
  639. if (_userManager.TenantId <= 0)
  640. return new List<string>();
  641. var tenantId = _userManager.TenantId;
  642. var factoryId = 1L;
  643. _refreshDispatcher.Enqueue("S1", async (sp, ct) =>
  644. {
  645. await sp.GetRequiredService<Admin.NET.Plugin.AiDOP.DataPlatform.MdpRebuild.ModuleRebuildService>()
  646. .EnqueueAsync("S1", tenantId, factoryId, null, "ORDER_REVIEW", ct);
  647. });
  648. return new List<string>();
  649. }
  650. // ══════════════════════════════════════════════════════════════
  651. // 3级计划重排 — 辅助方法
  652. // ══════════════════════════════════════════════════════════════
  653. /// <summary>
  654. /// 校验订单明细行的物料编码是否与已生成工单的物料编码一致。
  655. /// 若不一致则说明物料信息已变更,不允许重排。
  656. /// </summary>
  657. private async Task ValidateMaterialNotChangedAsync(long orderId, long tenantId)
  658. {
  659. var mismatches = await _db.Ado.SqlQueryAsync<dynamic>(
  660. """
  661. SELECT e.entry_seq AS EntrySeq,
  662. e.item_number AS EntryItemNum,
  663. w.ItemNum AS WoItemNum
  664. FROM crm_seorderentry e
  665. INNER JOIN WorkOrdMaster w
  666. ON w.BusinessID = e.Id AND w.tenant_id = e.tenant_id
  667. AND IFNULL(w.IsActive, 0) = 1
  668. AND LOWER(TRIM(IFNULL(w.Status, ''))) <> 'c'
  669. WHERE e.seorder_id = @OrderId AND e.tenant_id = @TenantId AND e.IsDeleted = 0
  670. AND TRIM(IFNULL(e.item_number, '')) <> ''
  671. AND TRIM(IFNULL(w.ItemNum, '')) <> ''
  672. AND TRIM(e.item_number) <> TRIM(w.ItemNum)
  673. """,
  674. new SugarParameter("@OrderId", orderId),
  675. new SugarParameter("@TenantId", tenantId));
  676. if (mismatches.Count > 0)
  677. throw Oops.Oh("此订单行的物料信息有变更无法重排");
  678. }
  679. /// <summary>
  680. /// 当工单状态为下达(R)/投产(W)/暂停(S)时,更新对应领料单明细数据。
  681. /// 重新从 WorkOrdDetail 汇总物料需求,覆盖 NbrDetail 中的 QtyOrd。
  682. /// </summary>
  683. private async Task UpdatePickingListForActiveWorkOrderAsync(
  684. long tenantId, string workOrd, string account, List<string> warnings)
  685. {
  686. // 获取工单状态
  687. var status = await _db.Ado.GetStringAsync(
  688. """
  689. SELECT IFNULL(LOWER(TRIM(Status)), '') FROM WorkOrdMaster
  690. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
  691. LIMIT 1
  692. """,
  693. new SugarParameter("@TenantId", tenantId),
  694. new SugarParameter("@WorkOrd", workOrd));
  695. // 仅处理 下达(R)/投产(W)/暂停(S) 状态的工单
  696. if (status is not ("r" or "w" or "s"))
  697. return;
  698. var woDomain = await _db.Ado.GetStringAsync(
  699. """
  700. SELECT IFNULL(`Domain`, '') FROM WorkOrdMaster
  701. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
  702. LIMIT 1
  703. """,
  704. new SugarParameter("@TenantId", tenantId),
  705. new SugarParameter("@WorkOrd", workOrd));
  706. if (await _pickNbrSync.TryEnqueueAfterPlanChangeAsync(tenantId, workOrd, woDomain, account))
  707. {
  708. warnings.Add($"工单 {workOrd} 领料单已入队回写 165");
  709. return;
  710. }
  711. // 查找领料单主记录
  712. var nbrRows = await _db.Ado.SqlQueryAsync<NbrMasterRow>(
  713. """
  714. SELECT RecID, Nbr, `Domain` FROM NbrMaster
  715. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd AND Type = 'SM'
  716. AND IFNULL(TransType, '') = ''
  717. AND IFNULL(IsActive, 0) = 1
  718. """,
  719. new SugarParameter("@TenantId", tenantId),
  720. new SugarParameter("@WorkOrd", workOrd));
  721. if (nbrRows.Count == 0)
  722. {
  723. warnings.Add($"工单 {workOrd} 状态为 {status.ToUpper()} 但无领料单,跳过领料单更新");
  724. return;
  725. }
  726. var now = DateTime.Now;
  727. // 从 WorkOrdDetail 汇总最新物料需求
  728. var details = await _db.Ado.SqlQueryAsync<PickDetailRow>(
  729. """
  730. SELECT
  731. d.ItemNum,
  732. SUM(d.QtyRequired) AS QtyRequired,
  733. MAX(IFNULL(d.UM, im.Um)) AS Unit,
  734. MAX(im.Descr) AS ItemName
  735. FROM WorkOrdDetail d
  736. LEFT JOIN ItemMaster im ON d.ItemNum = im.ItemNum AND im.tenant_id = d.tenant_id
  737. WHERE d.tenant_id = @TenantId AND d.WorkOrd = @WorkOrd AND IFNULL(d.IsActive, 0) = 1
  738. GROUP BY d.ItemNum
  739. HAVING SUM(d.QtyRequired) > 0
  740. ORDER BY d.ItemNum
  741. """,
  742. new SugarParameter("@TenantId", tenantId),
  743. new SugarParameter("@WorkOrd", workOrd));
  744. foreach (var nbr in nbrRows)
  745. {
  746. var domain = string.IsNullOrWhiteSpace(nbr.Domain) ? tenantId.ToString() : nbr.Domain!.Trim();
  747. if (domain.Length > 8) domain = domain[..8];
  748. // 加载当前领料单明细行(仅未关闭的)
  749. var existingDetails = await _db.Ado.SqlQueryAsync<NbrDetailRow>(
  750. """
  751. SELECT RecID, ItemNum, QtyOrd, QtyRec, CurrQtyOpened, Line
  752. FROM NbrDetail
  753. WHERE tenant_id = @TenantId AND Nbr = @Nbr AND Type = 'SM'
  754. AND IFNULL(IsActive, 0) = 1
  755. """,
  756. new SugarParameter("@TenantId", tenantId),
  757. new SugarParameter("@Nbr", nbr.Nbr));
  758. var detailMap = details.ToDictionary(d => d.ItemNum.Trim(), d => d);
  759. var existingMap = existingDetails.ToDictionary(d => (d.ItemNum ?? "").Trim(), d => d);
  760. // ── 1. 处理已有明细行:按 ItemNum 匹配 ──
  761. foreach (var existing in existingDetails)
  762. {
  763. var key = (existing.ItemNum ?? "").Trim();
  764. if (detailMap.TryGetValue(key, out var newDetail))
  765. {
  766. // 物料在工单明细中存在
  767. var newQty = newDetail.QtyRequired;
  768. if (existing.QtyRec > 0)
  769. {
  770. // 已发料:判断新需求数是否大于已发料数
  771. if (newQty > existing.QtyRec)
  772. {
  773. // 新需求 > 已发料 → 更新需求数
  774. await _db.Ado.ExecuteCommandAsync(
  775. """
  776. UPDATE NbrDetail
  777. SET QtyOrd = @QtyOrd, CurrQtyOpened = @CurrQtyOpened,
  778. UM = @UM, ItemName = @ItemName,
  779. UpdateUser = @User, UpdateTime = @Now
  780. WHERE RecID = @RecId
  781. """,
  782. new SugarParameter("@QtyOrd", newQty),
  783. new SugarParameter("@CurrQtyOpened", newQty),
  784. new SugarParameter("@UM", (object?)newDetail.Unit ?? DBNull.Value),
  785. new SugarParameter("@ItemName", (object?)newDetail.ItemName ?? DBNull.Value),
  786. new SugarParameter("@User", account),
  787. new SugarParameter("@Now", now),
  788. new SugarParameter("@RecId", existing.RecID));
  789. }
  790. else
  791. {
  792. // 新需求 <= 已发料 → 关闭当前行
  793. await _db.Ado.ExecuteCommandAsync(
  794. """
  795. UPDATE NbrDetail
  796. SET Status = 'C', IsActive = 0,
  797. UpdateUser = @User, UpdateTime = @Now
  798. WHERE RecID = @RecId
  799. """,
  800. new SugarParameter("@User", account),
  801. new SugarParameter("@Now", now),
  802. new SugarParameter("@RecId", existing.RecID));
  803. }
  804. }
  805. else
  806. {
  807. // 未发料 → 直接修改需求数
  808. await _db.Ado.ExecuteCommandAsync(
  809. """
  810. UPDATE NbrDetail
  811. SET QtyOrd = @QtyOrd, CurrQtyOpened = @CurrQtyOpened,
  812. UM = @UM, ItemName = @ItemName,
  813. UpdateUser = @User, UpdateTime = @Now
  814. WHERE RecID = @RecId
  815. """,
  816. new SugarParameter("@QtyOrd", newQty),
  817. new SugarParameter("@CurrQtyOpened", newQty),
  818. new SugarParameter("@UM", (object?)newDetail.Unit ?? DBNull.Value),
  819. new SugarParameter("@ItemName", (object?)newDetail.ItemName ?? DBNull.Value),
  820. new SugarParameter("@User", account),
  821. new SugarParameter("@Now", now),
  822. new SugarParameter("@RecId", existing.RecID));
  823. }
  824. }
  825. else
  826. {
  827. // 物料明细中没有,但领料单明细有 → 关闭当前领料单明细行
  828. await _db.Ado.ExecuteCommandAsync(
  829. """
  830. UPDATE NbrDetail
  831. SET Status = 'C', IsActive = 0,
  832. UpdateUser = @User, UpdateTime = @Now
  833. WHERE RecID = @RecId
  834. """,
  835. new SugarParameter("@User", account),
  836. new SugarParameter("@Now", now),
  837. new SugarParameter("@RecId", existing.RecID));
  838. }
  839. }
  840. // ── 2. 新增:物料明细中有,领料单明细没有的 ──
  841. var nextDetailId = await _db.Ado.GetIntAsync("SELECT IFNULL(MAX(RecID), 0) + 1 FROM NbrDetail");
  842. short newLine = (short)(existingDetails.Count > 0 ? existingDetails.Max(d => d.Line) + 1 : 1);
  843. foreach (var d in details)
  844. {
  845. var key = d.ItemNum.Trim();
  846. if (existingMap.ContainsKey(key))
  847. continue; // 已处理
  848. await _db.Ado.ExecuteCommandAsync(
  849. """
  850. INSERT INTO NbrDetail (
  851. RecID, `Domain`, Type, Nbr, Line, ItemNum, Dimension1, Dimension2,
  852. LocationFrom, LocationTo, QtyFrom, QtyTo, QtyOrd, QtyRec,
  853. CurrQtyOpened, UM, WorkOrd, ItemName, Status,
  854. IsActive, IsConfirm, IsChanged, BusinessID, NbrRecID,
  855. CreateUser, CreateTime, UpdateUser, UpdateTime, tenant_id
  856. ) VALUES (
  857. @RecId, @Domain, 'SM', @Nbr, @Line, @ItemNum, '', '',
  858. '', '', 0, 0, @QtyOrd, 0,
  859. @CurrQtyOpened, @UM, @WorkOrd, @ItemName, '',
  860. 1, 0, 1, 0, @NbrRecId,
  861. @User, @Now, @User, @Now, @TenantId
  862. )
  863. """,
  864. new SugarParameter("@RecId", nextDetailId++),
  865. new SugarParameter("@Domain", domain),
  866. new SugarParameter("@Nbr", nbr.Nbr),
  867. new SugarParameter("@Line", newLine++),
  868. new SugarParameter("@ItemNum", d.ItemNum),
  869. new SugarParameter("@QtyOrd", d.QtyRequired),
  870. new SugarParameter("@CurrQtyOpened", d.QtyRequired),
  871. new SugarParameter("@UM", (object?)d.Unit ?? DBNull.Value),
  872. new SugarParameter("@WorkOrd", workOrd),
  873. new SugarParameter("@ItemName", (object?)d.ItemName ?? DBNull.Value),
  874. new SugarParameter("@NbrRecId", nbr.RecID),
  875. new SugarParameter("@User", account.Length > 24 ? account[..24] : account),
  876. new SugarParameter("@Now", now),
  877. new SugarParameter("@TenantId", tenantId));
  878. }
  879. // ── 3. 更新领料单主记录的更新时间和数量 ──
  880. await _db.Ado.ExecuteCommandAsync(
  881. """
  882. UPDATE NbrMaster
  883. SET QtyOrd = (SELECT IFNULL(SUM(QtyOrd), 0) FROM NbrDetail
  884. WHERE Nbr = @Nbr AND Type = 'SM' AND IFNULL(IsActive, 1) = 1),
  885. UpdateUser = @User, UpdateTime = @Now
  886. WHERE RecID = @RecId
  887. """,
  888. new SugarParameter("@Nbr", nbr.Nbr),
  889. new SugarParameter("@User", account),
  890. new SugarParameter("@Now", now),
  891. new SugarParameter("@RecId", nbr.RecID));
  892. }
  893. }
  894. /// <summary>
  895. /// 根据资源检查结果更新明细行系统建议交期(sys_capacity_date)。
  896. /// </summary>
  897. private async Task UpdateEntrySysCapacityDateAsync(long entryId, DateTime? kittingTime, string account)
  898. {
  899. await _db.Ado.ExecuteCommandAsync(
  900. """
  901. UPDATE crm_seorderentry
  902. SET sys_capacity_date = @CapacityDate,
  903. update_time = @Now
  904. WHERE Id = @Id AND IsDeleted = 0
  905. """,
  906. new SugarParameter("@CapacityDate", kittingTime ?? (object)DBNull.Value),
  907. new SugarParameter("@Now", DateTime.Now),
  908. new SugarParameter("@Id", entryId));
  909. }
  910. // ══════════════════════════════════════════════════════════════
  911. // 3级计划重排 — 内部 DTO
  912. // ══════════════════════════════════════════════════════════════
  913. private sealed class NbrMasterRow
  914. {
  915. public int RecID { get; set; }
  916. public string Nbr { get; set; } = string.Empty;
  917. public string? Domain { get; set; }
  918. }
  919. private sealed class NbrDetailRow
  920. {
  921. public int RecID { get; set; }
  922. public string? ItemNum { get; set; }
  923. public decimal QtyOrd { get; set; }
  924. public decimal QtyRec { get; set; }
  925. public decimal CurrQtyOpened { get; set; }
  926. public short Line { get; set; }
  927. }
  928. private sealed class PickDetailRow
  929. {
  930. public string ItemNum { get; set; } = string.Empty;
  931. public decimal QtyRequired { get; set; }
  932. public string? Unit { get; set; }
  933. public string? ItemName { get; set; }
  934. }
  935. }