OrderWorkOrderGenerationService.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. using System.Globalization;
  2. using Yitter.IdGenerator;
  3. namespace Admin.NET.Plugin.AiDOP.Order;
  4. /// <summary>
  5. /// 订单评审通过后生成/更新工单(WorkOrdMaster、mes_morder、mes_moentry)。
  6. /// </summary>
  7. public class OrderWorkOrderGenerationService : ITransient
  8. {
  9. private readonly ISqlSugarClient _db;
  10. public OrderWorkOrderGenerationService(ISqlSugarClient db)
  11. {
  12. _db = db;
  13. }
  14. public sealed class OrderHeader
  15. {
  16. public long Id { get; set; }
  17. public string? BillNo { get; set; }
  18. public string? CustomNo { get; set; }
  19. public int? Urgent { get; set; }
  20. public long? FactoryId { get; set; }
  21. public long TenantId { get; set; }
  22. }
  23. public sealed class OrderEntryLine
  24. {
  25. public long Id { get; set; }
  26. public long SeOrderId { get; set; }
  27. public string? BillNo { get; set; }
  28. public int? EntrySeq { get; set; }
  29. public string? ItemNumber { get; set; }
  30. public string? ItemName { get; set; }
  31. public string? Specification { get; set; }
  32. public string? Unit { get; set; }
  33. public string? BomNumber { get; set; }
  34. public decimal? Qty { get; set; }
  35. public DateTime? PlanDate { get; set; }
  36. public DateTime? SysCapacityDate { get; set; }
  37. public string? Progress { get; set; }
  38. public int? Urgent { get; set; }
  39. public long? FactoryId { get; set; }
  40. public long? CompanyId { get; set; }
  41. public long TenantId { get; set; }
  42. }
  43. public sealed class WorkOrderUpsertResult
  44. {
  45. public string WorkOrd { get; set; } = string.Empty;
  46. public bool Created { get; set; }
  47. public List<string> Warnings { get; set; } = new();
  48. }
  49. public async Task<WorkOrderUpsertResult> CreateOrUpdateForEntryAsync(
  50. OrderHeader order,
  51. OrderEntryLine entry,
  52. string account,
  53. List<string> warnings)
  54. {
  55. var domain = await ResolveDomainAsync(order, entry);
  56. var dueDate = entry.SysCapacityDate ?? entry.PlanDate
  57. ?? throw Oops.Oh($"订单行 {entry.EntrySeq} 缺少计划交期,无法生成工单");
  58. var qty = entry.Qty ?? throw Oops.Oh($"订单行 {entry.EntrySeq} 数量无效");
  59. if (qty <= 0)
  60. throw Oops.Oh($"订单行 {entry.EntrySeq} 数量必须大于 0");
  61. var itemNum = entry.ItemNumber?.Trim()
  62. ?? throw Oops.Oh($"订单行 {entry.EntrySeq} 物料编码不能为空");
  63. var itemCnt = await _db.Ado.GetIntAsync(
  64. "SELECT COUNT(*) FROM ItemMaster WHERE ItemNum = @ItemNum AND (tenant_id = @TenantId OR @TenantId = 0)",
  65. new SugarParameter("@ItemNum", itemNum),
  66. new SugarParameter("@TenantId", entry.TenantId));
  67. if (itemCnt == 0)
  68. throw Oops.Oh($"物料 {itemNum} 不存在于 ItemMaster,订单行 {entry.EntrySeq} 无法评审");
  69. // 扣减已有成品库存,工单数量 = lack_qty(订单数量 - 在库数量)
  70. var stockQty = await _db.Ado.GetDecimalAsync(
  71. """
  72. SELECT COALESCE(SUM(
  73. CASE
  74. WHEN AvailStatusQty IS NOT NULL THEN AvailStatusQty
  75. WHEN QtyOnHand IS NOT NULL THEN QtyOnHand
  76. ELSE 0
  77. END
  78. ), 0)
  79. FROM InvMaster
  80. WHERE ItemNum = @ItemNum
  81. AND (tenant_id = @TenantId OR @TenantId = 0)
  82. """,
  83. new SugarParameter("@ItemNum", itemNum),
  84. new SugarParameter("@TenantId", entry.TenantId));
  85. var lackQty = Math.Max(0m, qty - stockQty);
  86. if (stockQty > 0)
  87. {
  88. warnings.Add($"订单行 {entry.EntrySeq}({itemNum})成品库存 {stockQty},工单数量 {qty} → {lackQty}");
  89. }
  90. if (lackQty > 0)
  91. qty = lackQty;
  92. if (string.IsNullOrWhiteSpace(entry.BomNumber))
  93. warnings.Add($"订单行 {entry.EntrySeq}({itemNum})无 BOM 编号,工单已生成但未展开物料明细");
  94. var urgent = entry.Urgent ?? order.Urgent ?? 0;
  95. var priority = urgent switch
  96. {
  97. 2 => 20m,
  98. 1 => 10m,
  99. _ => 0m
  100. };
  101. var existing = await FindOpenWorkOrderByEntryAsync(entry.TenantId, entry.Id);
  102. var now = DateTime.Now;
  103. var workOrd = existing?.WorkOrd ?? await GenerateWorkOrdNoAsync(entry.TenantId);
  104. var created = existing is null;
  105. if (existing is null)
  106. {
  107. await InsertWorkOrdMasterAsync(order, entry, workOrd, domain, itemNum, qty, dueDate, priority, urgent, account, now);
  108. var morderId = await InsertMesMorderAsync(order, entry, workOrd, domain, itemNum, qty, urgent, account, now);
  109. await InsertMesMoentryAsync(order, entry, workOrd, morderId, qty, account, now);
  110. }
  111. else
  112. {
  113. await UpdateWorkOrdMasterAsync(existing, entry, itemNum, qty, dueDate, priority, urgent, account, now);
  114. await UpdateMesMorderAsync(existing.WorkOrd, domain, entry, itemNum, qty, urgent, account, now);
  115. }
  116. return new WorkOrderUpsertResult
  117. {
  118. WorkOrd = workOrd,
  119. Created = created,
  120. Warnings = warnings
  121. };
  122. }
  123. /// <summary>
  124. /// WorkOrdMaster.Domain 为 varchar(8) 工厂编码(如 8010)。
  125. /// UAT 订单行 factory_id 可能误存租户 ID,不可直接 ToString 写入 Domain。
  126. /// </summary>
  127. private async Task<string> ResolveDomainAsync(OrderHeader order, OrderEntryLine entry)
  128. {
  129. var fid = entry.FactoryId ?? order.FactoryId;
  130. if (fid is > 0)
  131. {
  132. var fidText = fid.Value.ToString(CultureInfo.InvariantCulture);
  133. if (fidText.Length <= 8)
  134. return fidText;
  135. }
  136. var fallback = await _db.Ado.GetStringAsync(
  137. """
  138. SELECT `Domain` FROM WorkOrdMaster
  139. WHERE tenant_id = @TenantId
  140. AND `Domain` IS NOT NULL AND TRIM(`Domain`) <> ''
  141. GROUP BY `Domain`
  142. ORDER BY COUNT(*) DESC
  143. LIMIT 1
  144. """,
  145. new SugarParameter("@TenantId", entry.TenantId));
  146. if (!string.IsNullOrWhiteSpace(fallback))
  147. return fallback.Trim();
  148. return "8010";
  149. }
  150. private async Task<ExistingWorkOrderRow?> FindOpenWorkOrderByEntryAsync(long tenantId, long entryId)
  151. {
  152. var rows = await _db.Ado.SqlQueryAsync<ExistingWorkOrderRow>(
  153. """
  154. SELECT RecID AS RecId, WorkOrd, `Domain`, Status
  155. FROM WorkOrdMaster
  156. WHERE tenant_id = @TenantId
  157. AND BusinessID = @EntryId
  158. AND LOWER(TRIM(IFNULL(Status,''))) <> 'c'
  159. ORDER BY RecID DESC
  160. LIMIT 1
  161. """,
  162. new SugarParameter("@TenantId", tenantId),
  163. new SugarParameter("@EntryId", entryId));
  164. return rows.FirstOrDefault();
  165. }
  166. private async Task<string> GenerateWorkOrdNoAsync(long tenantId)
  167. {
  168. const string sql = """
  169. SELECT IFNULL(MAX(CAST(SUBSTRING(TRIM(WorkOrd), 3) AS UNSIGNED)), 0)
  170. FROM WorkOrdMaster
  171. WHERE tenant_id = @TenantId
  172. AND TRIM(WorkOrd) REGEXP '^M5[0-9]+$'
  173. """;
  174. var maxSeq = await _db.Ado.GetIntAsync(sql, new SugarParameter("@TenantId", tenantId));
  175. for (var attempt = 0; attempt < 5; attempt++)
  176. {
  177. var next = maxSeq + 1 + attempt;
  178. var candidate = "M5" + next.ToString("D8", CultureInfo.InvariantCulture);
  179. var dup = await _db.Ado.GetIntAsync(
  180. "SELECT COUNT(*) FROM WorkOrdMaster WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd",
  181. new SugarParameter("@TenantId", tenantId),
  182. new SugarParameter("@WorkOrd", candidate));
  183. if (dup == 0)
  184. return candidate;
  185. }
  186. throw Oops.Oh("生成工单号失败:流水号冲突,请重试");
  187. }
  188. private async Task InsertWorkOrdMasterAsync(
  189. OrderHeader order,
  190. OrderEntryLine entry,
  191. string workOrd,
  192. string domain,
  193. string itemNum,
  194. decimal qty,
  195. DateTime dueDate,
  196. decimal priority,
  197. int urgent,
  198. string account,
  199. DateTime now)
  200. {
  201. await _db.Ado.ExecuteCommandAsync(
  202. """
  203. INSERT INTO WorkOrdMaster (
  204. `Domain`, WorkOrd, ItemNum, ItemName, Status, Typed,
  205. QtyOrded, QtyCompleted, LbrVar, OrdDate, DueDate, Priority, Urgent,
  206. CustNo, SalesJob, BusinessID, CreateUser, CreateTime, UpdateUser, UpdateTime,
  207. IsActive, IsConfirm, tenant_id, Remark
  208. ) VALUES (
  209. @Domain, @WorkOrd, @ItemNum, @ItemName, '', 'M',
  210. @Qty, 0, 0, @OrdDate, @DueDate, @Priority, @Urgent,
  211. @CustNo, @SalesJob, @BusinessId, @User, @Now, @User, @Now,
  212. 1, 0, @TenantId, @Remark
  213. )
  214. """,
  215. new SugarParameter("@Domain", domain),
  216. new SugarParameter("@WorkOrd", workOrd),
  217. new SugarParameter("@ItemNum", itemNum),
  218. new SugarParameter("@ItemName", entry.ItemName ?? string.Empty),
  219. new SugarParameter("@Qty", qty),
  220. new SugarParameter("@OrdDate", now.Date),
  221. new SugarParameter("@DueDate", dueDate.Date),
  222. new SugarParameter("@Priority", priority),
  223. new SugarParameter("@Urgent", urgent),
  224. new SugarParameter("@CustNo", order.CustomNo ?? (object)DBNull.Value),
  225. new SugarParameter("@SalesJob", order.BillNo ?? (object)DBNull.Value),
  226. new SugarParameter("@BusinessId", entry.Id),
  227. new SugarParameter("@User", account),
  228. new SugarParameter("@Now", now),
  229. new SugarParameter("@TenantId", entry.TenantId),
  230. new SugarParameter("@Remark", $"S1评审生成 订单行{entry.EntrySeq}"));
  231. }
  232. private async Task UpdateWorkOrdMasterAsync(
  233. ExistingWorkOrderRow existing,
  234. OrderEntryLine entry,
  235. string itemNum,
  236. decimal qty,
  237. DateTime dueDate,
  238. decimal priority,
  239. int urgent,
  240. string account,
  241. DateTime now)
  242. {
  243. await _db.Ado.ExecuteCommandAsync(
  244. """
  245. UPDATE WorkOrdMaster
  246. SET ItemNum = @ItemNum,
  247. ItemName = @ItemName,
  248. QtyOrded = @Qty,
  249. DueDate = @DueDate,
  250. Priority = @Priority,
  251. Urgent = @Urgent,
  252. UpdateUser = @User,
  253. UpdateTime = @Now
  254. WHERE RecID = @RecId
  255. """,
  256. new SugarParameter("@ItemNum", itemNum),
  257. new SugarParameter("@ItemName", entry.ItemName ?? string.Empty),
  258. new SugarParameter("@Qty", qty),
  259. new SugarParameter("@DueDate", dueDate.Date),
  260. new SugarParameter("@Priority", priority),
  261. new SugarParameter("@Urgent", urgent),
  262. new SugarParameter("@User", account),
  263. new SugarParameter("@Now", now),
  264. new SugarParameter("@RecId", existing.RecId));
  265. }
  266. private async Task<long> InsertMesMorderAsync(
  267. OrderHeader order,
  268. OrderEntryLine entry,
  269. string workOrd,
  270. string domain,
  271. string itemNum,
  272. decimal qty,
  273. int urgent,
  274. string account,
  275. DateTime now)
  276. {
  277. var factoryId = entry.FactoryId ?? order.FactoryId ?? entry.TenantId;
  278. var companyId = entry.CompanyId ?? factoryId;
  279. var morderId = YitIdHelper.NextId();
  280. await _db.Ado.ExecuteCommandAsync(
  281. """
  282. INSERT INTO mes_morder (
  283. Id, morder_no, morder_type, morder_date, morder_state,
  284. product_code, product_name, fmodel, bom_number, unit,
  285. need_number, morder_production_number, MaterialSituation, urgent,
  286. create_by_name, create_time, update_by_name, update_time,
  287. tenant_id, factory_id, company_id, IsDeleted
  288. ) VALUES (
  289. @Id, @MorderNo, '生产工单', @Now, '新建',
  290. @ProductCode, @ProductName, @Fmodel, @BomNumber, @Unit,
  291. @Qty, @Qty, '待检查', @Urgent,
  292. @User, @Now, @User, @Now,
  293. @TenantId, @FactoryId, @CompanyId, 0
  294. )
  295. """,
  296. new SugarParameter("@Id", morderId),
  297. new SugarParameter("@MorderNo", workOrd),
  298. new SugarParameter("@Now", now),
  299. new SugarParameter("@ProductCode", itemNum),
  300. new SugarParameter("@ProductName", entry.ItemName ?? string.Empty),
  301. new SugarParameter("@Fmodel", entry.Specification ?? (object)DBNull.Value),
  302. new SugarParameter("@BomNumber", entry.BomNumber ?? (object)DBNull.Value),
  303. new SugarParameter("@Unit", entry.Unit ?? (object)DBNull.Value),
  304. new SugarParameter("@Qty", qty),
  305. new SugarParameter("@Urgent", urgent),
  306. new SugarParameter("@User", account),
  307. new SugarParameter("@TenantId", entry.TenantId),
  308. new SugarParameter("@FactoryId", factoryId),
  309. new SugarParameter("@CompanyId", companyId));
  310. return morderId;
  311. }
  312. private async Task InsertMesMoentryAsync(
  313. OrderHeader order,
  314. OrderEntryLine entry,
  315. string workOrd,
  316. long morderId,
  317. decimal qty,
  318. string account,
  319. DateTime now)
  320. {
  321. var factoryId = entry.FactoryId ?? order.FactoryId ?? entry.TenantId;
  322. var companyId = entry.CompanyId ?? factoryId;
  323. var moentryId = YitIdHelper.NextId();
  324. await _db.Ado.ExecuteCommandAsync(
  325. """
  326. INSERT INTO mes_moentry (
  327. Id, moentry_moid, moentry_mono, soentry_id, fbill_no, unit,
  328. need_number, morder_production_number, remaining_number,
  329. create_by_name, create_time, update_by_name, update_time,
  330. tenant_id, factory_id, company_id, IsDeleted
  331. ) VALUES (
  332. @Id, @Moid, @Mono, @SoentryId, @BillNo, @Unit,
  333. @Qty, @Qty, @Qty,
  334. @User, @Now, @User, @Now,
  335. @TenantId, @FactoryId, @CompanyId, 0
  336. )
  337. """,
  338. new SugarParameter("@Id", moentryId),
  339. new SugarParameter("@Moid", morderId),
  340. new SugarParameter("@Mono", workOrd),
  341. new SugarParameter("@SoentryId", entry.Id),
  342. new SugarParameter("@BillNo", order.BillNo ?? entry.BillNo ?? string.Empty),
  343. new SugarParameter("@Unit", entry.Unit ?? (object)DBNull.Value),
  344. new SugarParameter("@Qty", qty),
  345. new SugarParameter("@User", account),
  346. new SugarParameter("@Now", now),
  347. new SugarParameter("@TenantId", entry.TenantId),
  348. new SugarParameter("@FactoryId", factoryId),
  349. new SugarParameter("@CompanyId", companyId));
  350. }
  351. private async Task UpdateMesMorderAsync(
  352. string workOrd,
  353. string domain,
  354. OrderEntryLine entry,
  355. string itemNum,
  356. decimal qty,
  357. int urgent,
  358. string account,
  359. DateTime now)
  360. {
  361. var factoryId = entry.FactoryId ?? entry.TenantId;
  362. await _db.Ado.ExecuteCommandAsync(
  363. """
  364. UPDATE mes_morder
  365. SET product_code = @ProductCode,
  366. product_name = @ProductName,
  367. fmodel = @Fmodel,
  368. bom_number = @BomNumber,
  369. unit = @Unit,
  370. need_number = @Qty,
  371. morder_production_number = @Qty,
  372. urgent = @Urgent,
  373. update_by_name = @User,
  374. update_time = @Now
  375. WHERE morder_no = @MorderNo
  376. AND tenant_id = @TenantId
  377. AND IsDeleted = 0
  378. """,
  379. new SugarParameter("@ProductCode", itemNum),
  380. new SugarParameter("@ProductName", entry.ItemName ?? string.Empty),
  381. new SugarParameter("@Fmodel", entry.Specification ?? (object)DBNull.Value),
  382. new SugarParameter("@BomNumber", entry.BomNumber ?? (object)DBNull.Value),
  383. new SugarParameter("@Unit", entry.Unit ?? (object)DBNull.Value),
  384. new SugarParameter("@Qty", qty),
  385. new SugarParameter("@Urgent", urgent),
  386. new SugarParameter("@User", account),
  387. new SugarParameter("@Now", now),
  388. new SugarParameter("@MorderNo", workOrd),
  389. new SugarParameter("@TenantId", entry.TenantId));
  390. }
  391. private sealed class ExistingWorkOrderRow
  392. {
  393. public long RecId { get; set; }
  394. public string? WorkOrd { get; set; }
  395. public string? Domain { get; set; }
  396. public string? Status { get; set; }
  397. }
  398. }