OrderWorkOrderGenerationService.cs 17 KB

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