ProductionSchedulingActionService.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. using System.Globalization;
  2. using Admin.NET.Plugin.AiDOP.Infrastructure;
  3. using Admin.NET.Plugin.AiDOP.Order;
  4. using Admin.NET.Plugin.AiDOP.WorkOrder;
  5. namespace Admin.NET.Plugin.AiDOP.Production;
  6. /// <summary>工单排产动作:生成排程、优先级调整并重检。</summary>
  7. [ApiDescriptionSettings(Order = 264, Description = "工单排产动作")]
  8. [Route("api/Production")]
  9. [AllowAnonymous]
  10. [NonUnify]
  11. public class ProductionSchedulingActionService : IDynamicApiController, ITransient
  12. {
  13. private readonly ISqlSugarClient _db;
  14. private readonly UserManager _userManager;
  15. private readonly ProductionScheduleGenerationService _scheduleGen;
  16. private readonly WorkOrderRoutingSyncService _routingSync;
  17. private readonly AidopActionRunLogWriter _runLog;
  18. public ProductionSchedulingActionService(
  19. ISqlSugarClient db,
  20. UserManager userManager,
  21. ProductionScheduleGenerationService scheduleGen,
  22. WorkOrderRoutingSyncService routingSync,
  23. AidopActionRunLogWriter runLog)
  24. {
  25. _db = db;
  26. _userManager = userManager;
  27. _scheduleGen = scheduleGen;
  28. _routingSync = routingSync;
  29. _runLog = runLog;
  30. }
  31. /// <summary>生成生产排程计划(写入 PeriodSequenceDet)。</summary>
  32. [DisplayName("生成生产排程")]
  33. [HttpPost("scheduling/generate")]
  34. public async Task<object> GenerateSchedule([FromQuery] string domain, [FromQuery] bool enableCapacityConstraint = false)
  35. {
  36. var tenantId = ResolveTenantId(domain);
  37. var account = _userManager.Account ?? "system";
  38. var logId = await _runLog.StartAsync("S2_SCHEDULE_GENERATE", tenantId, "WorkOrdMaster", null, $"domain={domain}");
  39. try
  40. {
  41. var result = await _scheduleGen.GenerateAsync(tenantId, domain, account, enableCapacityConstraint);
  42. await _runLog.SuccessAsync(logId, result.Message, new
  43. {
  44. tenantId,
  45. domain,
  46. result.WorkOrderCount,
  47. result.ScheduleRowCount,
  48. result.UsedWorkCenterCalendar,
  49. result.SkippedWorkOrders
  50. });
  51. return new
  52. {
  53. message = result.Message,
  54. workOrderCount = result.WorkOrderCount,
  55. scheduleRowCount = result.ScheduleRowCount,
  56. usedWorkCenterCalendar = result.UsedWorkCenterCalendar,
  57. skippedWorkOrders = result.SkippedWorkOrders
  58. };
  59. }
  60. catch (Exception ex)
  61. {
  62. await _runLog.FailedAsync(logId, ex.Message, new { tenantId, domain });
  63. throw;
  64. }
  65. }
  66. /// <summary>优先级/数量/交期调整并重做资源检查。</summary>
  67. [DisplayName("优先级调整并重检")]
  68. [HttpPost("scheduling/update-priority-and-recheck")]
  69. public async Task<object> UpdatePriorityAndRecheck([FromBody] WorkOrderPriorityRecheckInput input)
  70. {
  71. var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
  72. var account = string.IsNullOrWhiteSpace(input.UserAccount)
  73. ? (_userManager.Account ?? "system")
  74. : input.UserAccount.Trim();
  75. var workOrd = input.Workord.Trim();
  76. var logId = await _runLog.StartAsync("S2_PRIORITY_RECHECK", tenantId, "WorkOrdMaster", null, workOrd);
  77. try
  78. {
  79. var before = await LoadWorkOrderSnapshotAsync(tenantId, workOrd);
  80. DateTime? dueDate = null;
  81. if (!string.IsNullOrWhiteSpace(input.Instockdate))
  82. dueDate = DateTime.Parse(input.Instockdate.Trim(), CultureInfo.InvariantCulture).Date;
  83. decimal? qty = null;
  84. if (!string.IsNullOrWhiteSpace(input.Qty) && decimal.TryParse(input.Qty.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out var q))
  85. qty = q;
  86. var pars = new List<SugarParameter>
  87. {
  88. new("@WorkOrd", workOrd),
  89. new("@TenantId", tenantId),
  90. new("@Priority", string.IsNullOrWhiteSpace(input.Priority) ? (object)DBNull.Value : input.Priority.Trim()),
  91. new("@LotSerial", string.IsNullOrWhiteSpace(input.LotSerial) ? (object)DBNull.Value : input.LotSerial.Trim()),
  92. new("@Qty", qty ?? (object)DBNull.Value),
  93. new("@DueDate", dueDate ?? (object)DBNull.Value),
  94. new("@User", account),
  95. new("@Now", DateTime.Now)
  96. };
  97. var affected = await _db.Ado.ExecuteCommandAsync(
  98. """
  99. UPDATE WorkOrdMaster
  100. SET Priority = COALESCE(@Priority, Priority),
  101. LotSerial = COALESCE(@LotSerial, LotSerial),
  102. QtyOrded = COALESCE(@Qty, QtyOrded),
  103. DueDate = COALESCE(@DueDate, DueDate),
  104. UpdateUser = @User,
  105. UpdateTime = @Now
  106. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
  107. """,
  108. pars);
  109. if (affected == 0)
  110. throw Oops.Oh("工单不存在或未更新");
  111. var qtyChanged = qty.HasValue && before?.QtyOrded != qty;
  112. var dueChanged = dueDate.HasValue && before?.DueDate?.Date != dueDate.Value.Date;
  113. var priorityChanged = !string.IsNullOrWhiteSpace(input.Priority)
  114. && !string.Equals(before?.Priority?.Trim(), input.Priority.Trim(), StringComparison.Ordinal);
  115. var warnings = new List<string>();
  116. // ── 同步 crm_seorderentry,避免后续齐套检查重新加载时读到旧值 ──
  117. if (qtyChanged || dueChanged)
  118. {
  119. var link = await LoadWorkOrderEntryLinkAsync(tenantId, workOrd);
  120. if (link is not null)
  121. {
  122. await _db.Ado.ExecuteCommandAsync(
  123. """
  124. UPDATE crm_seorderentry
  125. SET qty = COALESCE(@Qty, qty),
  126. sys_capacity_date = COALESCE(@SysCapacityDate, sys_capacity_date),
  127. update_time = NOW()
  128. WHERE Id = @EntryId AND tenant_id = @TenantId AND IsDeleted = 0
  129. """,
  130. new SugarParameter("@Qty", qty ?? (object)DBNull.Value),
  131. new SugarParameter("@SysCapacityDate", dueDate ?? (object)DBNull.Value),
  132. new SugarParameter("@EntryId", link.Entry.Id),
  133. new SugarParameter("@TenantId", tenantId));
  134. }
  135. }
  136. // 数量变更时同步更新 mes_morder 和 mes_moentry
  137. if (qtyChanged && qty.HasValue)
  138. {
  139. await UpdateMesOrderQuantityAsync(tenantId, workOrd, qty.Value, account);
  140. // 同步更新 WorkOrdRouting.QtyOrded(EnsureFromRoutingAsync 已有数据时不会更新数量)
  141. await _db.Ado.ExecuteCommandAsync(
  142. """
  143. UPDATE WorkOrdRouting
  144. SET QtyOrded = @Qty,
  145. UpdateUser = @User, UpdateTime = @Now
  146. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
  147. AND IFNULL(IsActive, 0) = 1
  148. """,
  149. new SugarParameter("@Qty", qty.Value),
  150. new SugarParameter("@User", account),
  151. new SugarParameter("@Now", DateTime.Now),
  152. new SugarParameter("@TenantId", tenantId),
  153. new SugarParameter("@WorkOrd", workOrd));
  154. }
  155. // 数量变更时同步更新领料单明细(NbrDetail)
  156. if (qtyChanged)
  157. {
  158. await SyncPickingListDetailsAsync(tenantId, workOrd, account, warnings);
  159. }
  160. ProductionScheduleGenerationService.ScheduleGenerationResult? reschedule = null;
  161. if (qtyChanged || dueChanged)
  162. {
  163. reschedule = await _scheduleGen.RegenerateForWorkOrderAsync(tenantId, workOrd, input.Domain, account, input.EnableCapacityConstraint);
  164. warnings.Add(reschedule.Message);
  165. // 排程后确保工艺路线数据存在
  166. await _routingSync.EnsureFromRoutingAsync(tenantId, workOrd, account);
  167. }
  168. else if (priorityChanged)
  169. {
  170. await _scheduleGen.DeactivateExistingScheduleAsync(tenantId, workOrd);
  171. warnings.Add("优先级已变更,原排程已失效,请重新执行批量排程");
  172. }
  173. await _runLog.SuccessAsync(logId, "优先级调整并重检完成", new
  174. {
  175. workOrd,
  176. before,
  177. after = new
  178. {
  179. qty,
  180. dueDate,
  181. priority = input.Priority,
  182. lotSerial = input.LotSerial
  183. },
  184. qtyChanged,
  185. dueChanged,
  186. priorityChanged,
  187. resourceRechecked = qtyChanged || dueChanged,
  188. reschedule = reschedule is null
  189. ? null
  190. : new
  191. {
  192. reschedule.WorkOrderCount,
  193. reschedule.ScheduleRowCount,
  194. reschedule.UsedWorkCenterCalendar
  195. },
  196. warnings
  197. });
  198. return new
  199. {
  200. message = "ok",
  201. warnings,
  202. resourceRechecked = qtyChanged || dueChanged,
  203. rescheduleTriggered = reschedule is not null,
  204. scheduleRowCount = reschedule?.ScheduleRowCount ?? 0
  205. };
  206. }
  207. catch (Exception ex)
  208. {
  209. await _runLog.FailedAsync(logId, ex.Message, new { workOrd, tenantId });
  210. throw;
  211. }
  212. }
  213. private long ResolveTenantId(string? domain)
  214. {
  215. if (!string.IsNullOrWhiteSpace(domain) && long.TryParse(domain.Trim(), out var tid) && tid > 0)
  216. return tid;
  217. return AidopTenantHelper.Resolve(App.HttpContext);
  218. }
  219. /// <summary>
  220. /// 数量变更时同步更新 mes_morder(制造工单)和 mes_moentry(工单明细)的数量字段。
  221. /// </summary>
  222. private async Task UpdateMesOrderQuantityAsync(long tenantId, string workOrd, decimal newQty, string account)
  223. {
  224. var now = DateTime.Now;
  225. // 更新 mes_morder
  226. await _db.Ado.ExecuteCommandAsync(
  227. """
  228. UPDATE mes_morder
  229. SET need_number = @Qty,
  230. morder_production_number = @Qty,
  231. update_by_name = @User,
  232. update_time = @Now
  233. WHERE morder_no = @MorderNo
  234. AND tenant_id = @TenantId
  235. AND IsDeleted = 0
  236. """,
  237. new SugarParameter("@Qty", newQty),
  238. new SugarParameter("@User", account),
  239. new SugarParameter("@Now", now),
  240. new SugarParameter("@MorderNo", workOrd),
  241. new SugarParameter("@TenantId", tenantId));
  242. // 更新 mes_moentry(remaining_number 仅在未开始生产时才更新)
  243. await _db.Ado.ExecuteCommandAsync(
  244. """
  245. UPDATE mes_moentry
  246. SET need_number = @Qty,
  247. morder_production_number = @Qty,
  248. remaining_number = CASE
  249. WHEN IFNULL(remaining_number, 0) = IFNULL(need_number, 0)
  250. OR IFNULL(morder_production_number, 0) = 0
  251. THEN @Qty
  252. ELSE remaining_number
  253. END,
  254. update_by_name = @User,
  255. update_time = @Now
  256. WHERE moentry_mono = @Mono
  257. AND tenant_id = @TenantId
  258. AND IsDeleted = 0
  259. """,
  260. new SugarParameter("@Qty", newQty),
  261. new SugarParameter("@User", account),
  262. new SugarParameter("@Now", now),
  263. new SugarParameter("@Mono", workOrd),
  264. new SugarParameter("@TenantId", tenantId));
  265. }
  266. /// <summary>
  267. /// 数量变更后同步领料单明细(NbrDetail),按物料逐行比对更新。
  268. /// 规则:
  269. /// 已发料(QtyRec>0) + 新需求>已发料 → 更新 QtyOrd/CurrQtyOpened
  270. /// 已发料(QtyRec>0) + 新需求≤已发料 → 关闭该行
  271. /// 未发料 → 直接修改 QtyOrd/CurrQtyOpened
  272. /// 工单明细有但领料单无 → 新增
  273. /// 领料单有但工单明细无 → 关闭
  274. /// </summary>
  275. private async Task SyncPickingListDetailsAsync(
  276. long tenantId, string workOrd, string account, List<string> warnings)
  277. {
  278. // 获取工单状态
  279. var status = await _db.Ado.GetStringAsync(
  280. """
  281. SELECT IFNULL(LOWER(TRIM(Status)), '') FROM WorkOrdMaster
  282. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
  283. LIMIT 1
  284. """,
  285. new SugarParameter("@TenantId", tenantId),
  286. new SugarParameter("@WorkOrd", workOrd));
  287. // 仅处理 下达(R)/投产(W)/暂停(S) 状态的工单
  288. if (status is not ("r" or "w" or "s"))
  289. {
  290. // 初始(P)状态无领料单,无需同步
  291. return;
  292. }
  293. // 查找领料单主记录
  294. var nbrRows = await _db.Ado.SqlQueryAsync<NbrMasterRow>(
  295. """
  296. SELECT RecID, Nbr, `Domain` FROM NbrMaster
  297. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd AND Type = 'SM'
  298. AND IFNULL(TransType, '') = ''
  299. AND IFNULL(IsActive, 0) = 1
  300. """,
  301. new SugarParameter("@TenantId", tenantId),
  302. new SugarParameter("@WorkOrd", workOrd));
  303. if (nbrRows.Count == 0)
  304. {
  305. warnings.Add($"工单 {workOrd} 状态为 {status.ToUpper()} 但无领料单,跳过领料单同步");
  306. return;
  307. }
  308. var now = DateTime.Now;
  309. // 从 WorkOrdDetail 汇总最新物料需求
  310. var details = await _db.Ado.SqlQueryAsync<PickDetailRow>(
  311. """
  312. SELECT
  313. d.ItemNum,
  314. SUM(d.QtyRequired) AS QtyRequired,
  315. MAX(IFNULL(d.UM, im.Um)) AS Unit,
  316. MAX(im.Descr) AS ItemName
  317. FROM WorkOrdDetail d
  318. LEFT JOIN ItemMaster im ON d.ItemNum = im.ItemNum AND im.tenant_id = d.tenant_id
  319. WHERE d.tenant_id = @TenantId AND d.WorkOrd = @WorkOrd AND IFNULL(d.IsActive, 0) = 1
  320. GROUP BY d.ItemNum
  321. HAVING SUM(d.QtyRequired) > 0
  322. ORDER BY d.ItemNum
  323. """,
  324. new SugarParameter("@TenantId", tenantId),
  325. new SugarParameter("@WorkOrd", workOrd));
  326. foreach (var nbr in nbrRows)
  327. {
  328. var domain = string.IsNullOrWhiteSpace(nbr.Domain) ? tenantId.ToString() : nbr.Domain!.Trim();
  329. if (domain.Length > 8) domain = domain[..8];
  330. // 加载当前领料单明细行(仅未关闭的)
  331. var existingDetails = await _db.Ado.SqlQueryAsync<NbrDetailRow>(
  332. """
  333. SELECT RecID, ItemNum, QtyOrd, QtyRec, CurrQtyOpened, Line
  334. FROM NbrDetail
  335. WHERE tenant_id = @TenantId AND Nbr = @Nbr AND Type = 'SM'
  336. AND IFNULL(IsActive, 0) = 1
  337. """,
  338. new SugarParameter("@TenantId", tenantId),
  339. new SugarParameter("@Nbr", nbr.Nbr));
  340. var detailMap = details.ToDictionary(d => d.ItemNum.Trim(), d => d);
  341. var existingMap = existingDetails.ToDictionary(d => (d.ItemNum ?? "").Trim(), d => d);
  342. // ── 1. 处理已有明细行:按 ItemNum 匹配 ──
  343. foreach (var existing in existingDetails)
  344. {
  345. var key = (existing.ItemNum ?? "").Trim();
  346. if (detailMap.TryGetValue(key, out var newDetail))
  347. {
  348. var newQty = newDetail.QtyRequired;
  349. if (existing.QtyRec > 0)
  350. {
  351. if (newQty > existing.QtyRec)
  352. {
  353. // 新需求 > 已发料 → 更新需求数
  354. await _db.Ado.ExecuteCommandAsync(
  355. """
  356. UPDATE NbrDetail
  357. SET QtyOrd = @QtyOrd, CurrQtyOpened = @CurrQtyOpened,
  358. UM = @UM, ItemName = @ItemName,
  359. UpdateUser = @User, UpdateTime = @Now
  360. WHERE RecID = @RecId
  361. """,
  362. new SugarParameter("@QtyOrd", newQty),
  363. new SugarParameter("@CurrQtyOpened", newQty),
  364. new SugarParameter("@UM", (object?)newDetail.Unit ?? DBNull.Value),
  365. new SugarParameter("@ItemName", (object?)newDetail.ItemName ?? DBNull.Value),
  366. new SugarParameter("@User", account),
  367. new SugarParameter("@Now", now),
  368. new SugarParameter("@RecId", existing.RecID));
  369. }
  370. else
  371. {
  372. // 新需求 <= 已发料 → 关闭当前行
  373. await _db.Ado.ExecuteCommandAsync(
  374. """
  375. UPDATE NbrDetail
  376. SET Status = 'C', IsActive = 0,
  377. UpdateUser = @User, UpdateTime = @Now
  378. WHERE RecID = @RecId
  379. """,
  380. new SugarParameter("@User", account),
  381. new SugarParameter("@Now", now),
  382. new SugarParameter("@RecId", existing.RecID));
  383. }
  384. }
  385. else
  386. {
  387. // 未发料 → 直接修改需求数
  388. await _db.Ado.ExecuteCommandAsync(
  389. """
  390. UPDATE NbrDetail
  391. SET QtyOrd = @QtyOrd, CurrQtyOpened = @CurrQtyOpened,
  392. UM = @UM, ItemName = @ItemName,
  393. UpdateUser = @User, UpdateTime = @Now
  394. WHERE RecID = @RecId
  395. """,
  396. new SugarParameter("@QtyOrd", newQty),
  397. new SugarParameter("@CurrQtyOpened", newQty),
  398. new SugarParameter("@UM", (object?)newDetail.Unit ?? DBNull.Value),
  399. new SugarParameter("@ItemName", (object?)newDetail.ItemName ?? DBNull.Value),
  400. new SugarParameter("@User", account),
  401. new SugarParameter("@Now", now),
  402. new SugarParameter("@RecId", existing.RecID));
  403. }
  404. }
  405. else
  406. {
  407. // 物料明细中没有,但领料单明细有 → 关闭当前领料单明细行
  408. await _db.Ado.ExecuteCommandAsync(
  409. """
  410. UPDATE NbrDetail
  411. SET Status = 'C', IsActive = 0,
  412. UpdateUser = @User, UpdateTime = @Now
  413. WHERE RecID = @RecId
  414. """,
  415. new SugarParameter("@User", account),
  416. new SugarParameter("@Now", now),
  417. new SugarParameter("@RecId", existing.RecID));
  418. }
  419. }
  420. // ── 2. 新增:物料明细中有,领料单明细没有的 ──
  421. var nextDetailId = await _db.Ado.GetIntAsync("SELECT IFNULL(MAX(RecID), 0) + 1 FROM NbrDetail");
  422. short newLine = (short)(existingDetails.Count > 0 ? existingDetails.Max(d => d.Line) + 1 : 1);
  423. foreach (var d in details)
  424. {
  425. var key = d.ItemNum.Trim();
  426. if (existingMap.ContainsKey(key))
  427. continue;
  428. await _db.Ado.ExecuteCommandAsync(
  429. """
  430. INSERT INTO NbrDetail (
  431. RecID, `Domain`, Type, Nbr, Line, ItemNum, Dimension1, Dimension2,
  432. LocationFrom, LocationTo, QtyFrom, QtyTo, QtyOrd, QtyRec,
  433. CurrQtyOpened, UM, WorkOrd, ItemName, Status,
  434. IsActive, IsConfirm, IsChanged, BusinessID, NbrRecID,
  435. CreateUser, CreateTime, UpdateUser, UpdateTime, tenant_id
  436. ) VALUES (
  437. @RecId, @Domain, 'SM', @Nbr, @Line, @ItemNum, '', '',
  438. '', '', 0, 0, @QtyOrd, 0,
  439. @CurrQtyOpened, @UM, @WorkOrd, @ItemName, '',
  440. 1, 0, 1, 0, @NbrRecId,
  441. @User, @Now, @User, @Now, @TenantId
  442. )
  443. """,
  444. new SugarParameter("@RecId", nextDetailId++),
  445. new SugarParameter("@Domain", domain),
  446. new SugarParameter("@Nbr", nbr.Nbr),
  447. new SugarParameter("@Line", newLine++),
  448. new SugarParameter("@ItemNum", d.ItemNum),
  449. new SugarParameter("@QtyOrd", d.QtyRequired),
  450. new SugarParameter("@CurrQtyOpened", d.QtyRequired),
  451. new SugarParameter("@UM", (object?)d.Unit ?? DBNull.Value),
  452. new SugarParameter("@WorkOrd", workOrd),
  453. new SugarParameter("@ItemName", (object?)d.ItemName ?? DBNull.Value),
  454. new SugarParameter("@NbrRecId", nbr.RecID),
  455. new SugarParameter("@User", account.Length > 24 ? account[..24] : account),
  456. new SugarParameter("@Now", now),
  457. new SugarParameter("@TenantId", tenantId));
  458. }
  459. // ── 3. 更新领料单主记录的更新时间和数量 ──
  460. await _db.Ado.ExecuteCommandAsync(
  461. """
  462. UPDATE NbrMaster
  463. SET QtyOrd = (SELECT IFNULL(SUM(QtyOrd), 0) FROM NbrDetail
  464. WHERE Nbr = @Nbr AND Type = 'SM' AND IFNULL(IsActive, 1) = 1),
  465. UpdateUser = @User, UpdateTime = @Now
  466. WHERE RecID = @RecId
  467. """,
  468. new SugarParameter("@Nbr", nbr.Nbr),
  469. new SugarParameter("@User", account),
  470. new SugarParameter("@Now", now),
  471. new SugarParameter("@RecId", nbr.RecID));
  472. }
  473. }
  474. // ══════════════════════════════════════════════════════════════
  475. // 内部 DTO
  476. // ══════════════════════════════════════════════════════════════
  477. private sealed class NbrMasterRow
  478. {
  479. public int RecID { get; set; }
  480. public string Nbr { get; set; } = string.Empty;
  481. public string? Domain { get; set; }
  482. }
  483. private sealed class NbrDetailRow
  484. {
  485. public int RecID { get; set; }
  486. public string? ItemNum { get; set; }
  487. public decimal QtyOrd { get; set; }
  488. public decimal QtyRec { get; set; }
  489. public decimal CurrQtyOpened { get; set; }
  490. public short Line { get; set; }
  491. }
  492. private sealed class PickDetailRow
  493. {
  494. public string ItemNum { get; set; } = string.Empty;
  495. public decimal QtyRequired { get; set; }
  496. public string? Unit { get; set; }
  497. public string? ItemName { get; set; }
  498. }
  499. private async Task<WorkOrderSnapshotRow?> LoadWorkOrderSnapshotAsync(long tenantId, string workOrd)
  500. {
  501. var rows = await _db.Ado.SqlQueryAsync<WorkOrderSnapshotRow>(
  502. """
  503. SELECT WorkOrd, Priority, QtyOrded, DueDate, LotSerial, `Domain`
  504. FROM WorkOrdMaster
  505. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
  506. LIMIT 1
  507. """,
  508. new SugarParameter("@TenantId", tenantId),
  509. new SugarParameter("@WorkOrd", workOrd));
  510. return rows.FirstOrDefault();
  511. }
  512. private async Task<WorkOrderEntryLink?> LoadWorkOrderEntryLinkAsync(long tenantId, string workOrd)
  513. {
  514. var rows = await _db.Ado.SqlQueryAsync<WorkOrderEntryLinkRow>(
  515. """
  516. SELECT
  517. w.BusinessID AS EntryId,
  518. e.seorder_id AS SeOrderId,
  519. e.bill_no AS BillNo,
  520. e.entry_seq AS EntrySeq,
  521. e.item_number AS ItemNumber,
  522. e.item_name AS ItemName,
  523. e.specification AS Specification,
  524. e.unit AS Unit,
  525. e.bom_number AS BomNumber,
  526. e.qty AS Qty,
  527. e.plan_date AS PlanDate,
  528. e.sys_capacity_date AS SysCapacityDate,
  529. e.progress AS Progress,
  530. e.urgent AS Urgent,
  531. e.factory_id AS FactoryId,
  532. e.company_id AS CompanyId,
  533. e.tenant_id AS TenantId,
  534. o.Id AS OrderId,
  535. o.bill_no AS OrderBillNo,
  536. o.custom_no AS CustomNo,
  537. o.urgent AS OrderUrgent,
  538. o.factory_id AS OrderFactoryId
  539. FROM WorkOrdMaster w
  540. INNER JOIN crm_seorderentry e ON e.Id = w.BusinessID AND e.tenant_id = w.tenant_id AND e.IsDeleted = 0
  541. INNER JOIN crm_seorder o ON o.Id = e.seorder_id AND o.tenant_id = e.tenant_id AND o.IsDeleted = 0
  542. WHERE w.tenant_id = @TenantId AND w.WorkOrd = @WorkOrd
  543. LIMIT 1
  544. """,
  545. new SugarParameter("@TenantId", tenantId),
  546. new SugarParameter("@WorkOrd", workOrd));
  547. var row = rows.FirstOrDefault();
  548. if (row is null || row.EntryId <= 0)
  549. return null;
  550. return new WorkOrderEntryLink
  551. {
  552. Order = new OrderWorkOrderGenerationService.OrderHeader
  553. {
  554. Id = row.OrderId,
  555. BillNo = row.OrderBillNo ?? row.BillNo,
  556. CustomNo = row.CustomNo,
  557. Urgent = row.OrderUrgent,
  558. FactoryId = row.OrderFactoryId,
  559. TenantId = tenantId
  560. },
  561. Entry = new OrderWorkOrderGenerationService.OrderEntryLine
  562. {
  563. Id = row.EntryId,
  564. SeOrderId = row.SeOrderId,
  565. BillNo = row.BillNo,
  566. EntrySeq = row.EntrySeq,
  567. ItemNumber = row.ItemNumber,
  568. ItemName = row.ItemName,
  569. Specification = row.Specification,
  570. Unit = row.Unit,
  571. BomNumber = row.BomNumber,
  572. Qty = row.Qty,
  573. PlanDate = row.PlanDate,
  574. SysCapacityDate = row.SysCapacityDate,
  575. Progress = row.Progress,
  576. Urgent = row.Urgent,
  577. FactoryId = row.FactoryId,
  578. CompanyId = row.CompanyId,
  579. TenantId = row.TenantId
  580. }
  581. };
  582. }
  583. private sealed class WorkOrderSnapshotRow
  584. {
  585. public string? WorkOrd { get; set; }
  586. public string? Priority { get; set; }
  587. public decimal? QtyOrded { get; set; }
  588. public DateTime? DueDate { get; set; }
  589. public string? LotSerial { get; set; }
  590. public string? Domain { get; set; }
  591. }
  592. private sealed class WorkOrderEntryLink
  593. {
  594. public OrderWorkOrderGenerationService.OrderHeader Order { get; set; } = new();
  595. public OrderWorkOrderGenerationService.OrderEntryLine Entry { get; set; } = new();
  596. }
  597. private sealed class WorkOrderEntryLinkRow
  598. {
  599. public long EntryId { get; set; }
  600. public long SeOrderId { get; set; }
  601. public string? BillNo { get; set; }
  602. public int? EntrySeq { get; set; }
  603. public string? ItemNumber { get; set; }
  604. public string? ItemName { get; set; }
  605. public string? Specification { get; set; }
  606. public string? Unit { get; set; }
  607. public string? BomNumber { get; set; }
  608. public decimal? Qty { get; set; }
  609. public DateTime? PlanDate { get; set; }
  610. public DateTime? SysCapacityDate { get; set; }
  611. public string? Progress { get; set; }
  612. public int? Urgent { get; set; }
  613. public long? FactoryId { get; set; }
  614. public long? CompanyId { get; set; }
  615. public long TenantId { get; set; }
  616. public long OrderId { get; set; }
  617. public string? OrderBillNo { get; set; }
  618. public string? CustomNo { get; set; }
  619. public int? OrderUrgent { get; set; }
  620. public long? OrderFactoryId { get; set; }
  621. }
  622. }
  623. public class WorkOrderPriorityRecheckInput
  624. {
  625. public string Workord { get; set; } = string.Empty;
  626. public string? Qty { get; set; }
  627. public string? Instockdate { get; set; }
  628. public string? Priority { get; set; }
  629. public string Domain { get; set; } = string.Empty;
  630. public string? UserAccount { get; set; }
  631. public string? LotSerial { get; set; }
  632. public bool EnableCapacityConstraint { get; set; }
  633. }