OrderWorkOrderGenerationService.cs 16 KB

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