OrderWorkOrderGenerationService.cs 16 KB

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