WorkOrderPickBillService.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. using Admin.NET.Plugin.AiDOP.Supply;
  2. using Yitter.IdGenerator;
  3. namespace Admin.NET.Plugin.AiDOP.WorkOrder;
  4. /// <summary>
  5. /// 本库领料单生成(NbrMaster / NbrDetail,Type=SM)。WP9 Local 路径 / LocalPickBillCreator。
  6. /// </summary>
  7. public class WorkOrderPickBillService : ITransient
  8. {
  9. private readonly ISqlSugarClient _db;
  10. private readonly NumberRuleService _numberRule;
  11. private readonly WorkOrderMaterialDetailSyncService _detailSync;
  12. public WorkOrderPickBillService(
  13. ISqlSugarClient db,
  14. NumberRuleService numberRule,
  15. WorkOrderMaterialDetailSyncService detailSync)
  16. {
  17. _db = db;
  18. _numberRule = numberRule;
  19. _detailSync = detailSync;
  20. }
  21. /// <summary>兼容旧调用;等价于 CreateAsync(domain 由工单推导)。</summary>
  22. public Task<PickBillResult> CreateForWorkOrderAsync(long tenantId, string workOrd, string account)
  23. => CreateAsync(tenantId, domain: "", workOrd, account);
  24. public async Task<PickBillResult> CreateAsync(
  25. long tenantId,
  26. string domain,
  27. string workOrd,
  28. string account,
  29. CancellationToken ct = default)
  30. {
  31. workOrd = workOrd.Trim();
  32. var existing = await FindExistingPickBillAsync(tenantId, workOrd);
  33. if (!string.IsNullOrWhiteSpace(existing))
  34. return new PickBillResult { Nbr = existing, Created = false, Message = "领料单已存在", Source = "Local" };
  35. var wo = await LoadWorkOrderAsync(tenantId, workOrd)
  36. ?? throw Oops.Oh($"工单 {workOrd} 不存在");
  37. await _detailSync.EnsureFromResourceCheckAsync(tenantId, workOrd, account);
  38. var details = await LoadPickDetailsAsync(tenantId, workOrd);
  39. if (details.Count == 0)
  40. throw Oops.Oh($"工单 {workOrd} 无物料明细,请先完成订单评审资源检查");
  41. var resolvedDomain = !string.IsNullOrWhiteSpace(domain)
  42. ? domain.Trim()
  43. : (string.IsNullOrWhiteSpace(wo.Domain) ? tenantId.ToString() : wo.Domain.Trim());
  44. var lineTo = await ResolveLineSideLocationAsync(tenantId, wo.ItemNum);
  45. var now = DateTime.Now;
  46. var effDate = wo.OrdDate?.Date ?? now.Date;
  47. var pickDate = effDate < now.Date ? now.Date : effDate;
  48. string nbrNo;
  49. var remark = "下达自动领料";
  50. var source = "Local";
  51. try
  52. {
  53. var numbers = await _numberRule.NextBatchInCurrentTransactionAsync("SM", resolvedDomain, 1, account);
  54. nbrNo = numbers.FirstOrDefault() ?? throw Oops.Oh("领料单号生成失败");
  55. }
  56. catch
  57. {
  58. // WP9 S2:降级号与 165 号段可区分
  59. nbrNo = "SMX" + now.ToString("yyyyMMddHHmmss") + YitIdHelper.NextId().ToString()[^5..];
  60. remark = "本地降级";
  61. source = "LocalFallback";
  62. }
  63. var masterRecId = await NextRecIdAsync("NbrMaster");
  64. await _db.Ado.ExecuteCommandAsync(
  65. """
  66. INSERT INTO NbrMaster (
  67. RecID, `Domain`, Type, Nbr, Status, Remark, Date, EffDate,
  68. WorkOrd, QtyOrd, QtyRec, ProdLine, Department, Name,
  69. TransType, IsActive, IsChanged, IsConfirm, BusinessID,
  70. CreateUser, CreateTime, UpdateUser, UpdateTime, tenant_id
  71. ) VALUES (
  72. @RecId, @Domain, 'SM', @Nbr, '', @Remark, @PickDate, @EffDate,
  73. @WorkOrd, @QtyOrd, 0, @ProdLine, '101', @Name,
  74. '', 1, 1, 0, 0,
  75. @User, @Now, @User, @Now, @TenantId
  76. )
  77. """,
  78. new SugarParameter("@RecId", masterRecId),
  79. new SugarParameter("@Domain", resolvedDomain.Length > 8 ? resolvedDomain[..8] : resolvedDomain),
  80. new SugarParameter("@Nbr", nbrNo),
  81. new SugarParameter("@Remark", remark),
  82. new SugarParameter("@PickDate", pickDate),
  83. new SugarParameter("@EffDate", effDate),
  84. new SugarParameter("@WorkOrd", workOrd),
  85. new SugarParameter("@QtyOrd", wo.QtyOrded ?? 0),
  86. new SugarParameter("@ProdLine", wo.ProdLine ?? (object)DBNull.Value),
  87. new SugarParameter("@Name", account.Length > 12 ? account[..12] : account),
  88. new SugarParameter("@User", account.Length > 24 ? account[..24] : account),
  89. new SugarParameter("@Now", now),
  90. new SugarParameter("@TenantId", tenantId));
  91. short line = 1;
  92. foreach (var d in details)
  93. {
  94. var detailRecId = await NextRecIdAsync("NbrDetail");
  95. await _db.Ado.ExecuteCommandAsync(
  96. """
  97. INSERT INTO NbrDetail (
  98. RecID, `Domain`, Type, Nbr, Line, ItemNum, Dimension1, Dimension2,
  99. LocationFrom, LocationTo, QtyFrom, QtyTo, QtyOrd, QtyRec,
  100. CurrQtyOpened, UM, WorkOrd, ItemName, Status,
  101. IsActive, IsConfirm, IsChanged, BusinessID, NbrRecID,
  102. CreateUser, CreateTime, UpdateUser, UpdateTime, tenant_id
  103. ) VALUES (
  104. @RecId, @Domain, 'SM', @Nbr, @Line, @ItemNum, '', '',
  105. @LocationFrom, @LocationTo, 0, 0, @QtyOrd, 0,
  106. @CurrQtyOpened, @UM, @WorkOrd, @ItemName, '',
  107. 1, 0, 1, 0, @NbrRecId,
  108. @User, @Now, @User, @Now, @TenantId
  109. )
  110. """,
  111. new SugarParameter("@RecId", detailRecId),
  112. new SugarParameter("@Domain", resolvedDomain.Length > 8 ? resolvedDomain[..8] : resolvedDomain),
  113. new SugarParameter("@Nbr", nbrNo),
  114. new SugarParameter("@Line", line),
  115. new SugarParameter("@ItemNum", d.ItemNum),
  116. new SugarParameter("@LocationFrom", d.LocationFrom ?? (object)DBNull.Value),
  117. new SugarParameter("@LocationTo", lineTo ?? (object)DBNull.Value),
  118. new SugarParameter("@QtyOrd", d.QtyRequired),
  119. new SugarParameter("@CurrQtyOpened", d.QtyRequired),
  120. new SugarParameter("@UM", d.Unit ?? (object)DBNull.Value),
  121. new SugarParameter("@WorkOrd", workOrd),
  122. new SugarParameter("@ItemName", d.ItemName ?? (object)DBNull.Value),
  123. new SugarParameter("@NbrRecId", masterRecId),
  124. new SugarParameter("@User", account.Length > 24 ? account[..24] : account),
  125. new SugarParameter("@Now", now),
  126. new SugarParameter("@TenantId", tenantId));
  127. line++;
  128. }
  129. await _db.Ado.ExecuteCommandAsync(
  130. """
  131. UPDATE mes_morder
  132. SET morder_state = '下达',
  133. MaterialSituation = CASE
  134. WHEN MaterialSituation = '缺料' THEN '未备料'
  135. ELSE IFNULL(MaterialSituation, '齐套')
  136. END,
  137. update_by_name = @User,
  138. update_time = @Now
  139. WHERE tenant_id = @TenantId AND morder_no = @WorkOrd AND IsDeleted = 0
  140. """,
  141. new SugarParameter("@User", account),
  142. new SugarParameter("@Now", now),
  143. new SugarParameter("@TenantId", tenantId),
  144. new SugarParameter("@WorkOrd", workOrd));
  145. return new PickBillResult
  146. {
  147. Nbr = nbrNo,
  148. Created = true,
  149. LineCount = details.Count,
  150. Message = source == "LocalFallback" ? "领料单已生成本地降级号" : "领料单已生成",
  151. Source = source
  152. };
  153. }
  154. private async Task<string?> FindExistingPickBillAsync(long tenantId, string workOrd)
  155. {
  156. return await _db.Ado.GetStringAsync(
  157. """
  158. SELECT Nbr FROM NbrMaster
  159. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd AND Type = 'SM'
  160. AND IFNULL(TransType, '') = ''
  161. AND IFNULL(IsActive, 1) = 1
  162. ORDER BY RecID DESC LIMIT 1
  163. """,
  164. new List<SugarParameter>
  165. {
  166. new("@TenantId", tenantId),
  167. new("@WorkOrd", workOrd)
  168. });
  169. }
  170. private async Task<WorkOrderPickRow?> LoadWorkOrderAsync(long tenantId, string workOrd)
  171. {
  172. var rows = await _db.Ado.SqlQueryAsync<WorkOrderPickRow>(
  173. """
  174. SELECT
  175. w.RecID AS RecId,
  176. w.`Domain`,
  177. w.ItemNum,
  178. w.QtyOrded,
  179. w.OrdDate,
  180. (
  181. SELECT LEFT(IFNULL(r.ProdLine, ''), 8)
  182. FROM WorkOrdRouting r
  183. WHERE r.WorkOrd = w.WorkOrd AND r.tenant_id = w.tenant_id AND IFNULL(r.IsActive, 0) = 1
  184. ORDER BY (r.OP + 0) LIMIT 1
  185. ) AS ProdLine
  186. FROM WorkOrdMaster w
  187. WHERE w.tenant_id = @TenantId AND w.WorkOrd = @WorkOrd
  188. LIMIT 1
  189. """,
  190. new SugarParameter("@TenantId", tenantId),
  191. new SugarParameter("@WorkOrd", workOrd));
  192. return rows.FirstOrDefault();
  193. }
  194. private async Task<List<PickDetailRow>> LoadPickDetailsAsync(long tenantId, string workOrd)
  195. {
  196. return await _db.Ado.SqlQueryAsync<PickDetailRow>(
  197. """
  198. SELECT
  199. d.ItemNum,
  200. SUM(d.QtyRequired) AS QtyRequired,
  201. MAX(IFNULL(d.Location, im.Location)) AS LocationFrom,
  202. MAX(IFNULL(d.UM, im.Um)) AS Unit,
  203. MAX(im.Descr) AS ItemName
  204. FROM WorkOrdDetail d
  205. LEFT JOIN ItemMaster im ON d.ItemNum = im.ItemNum
  206. WHERE d.tenant_id = @TenantId AND d.WorkOrd = @WorkOrd AND IFNULL(d.IsActive, 0) = 1
  207. GROUP BY d.ItemNum
  208. HAVING SUM(d.QtyRequired) > 0
  209. ORDER BY d.ItemNum
  210. """,
  211. new SugarParameter("@TenantId", tenantId),
  212. new SugarParameter("@WorkOrd", workOrd));
  213. }
  214. private async Task<string?> ResolveLineSideLocationAsync(long tenantId, string? itemNum)
  215. {
  216. if (string.IsNullOrWhiteSpace(itemNum))
  217. return null;
  218. var loc = await _db.Ado.GetStringAsync(
  219. """
  220. SELECT LEFT(IFNULL(lm.Location, lm.PickingLocation), 8)
  221. FROM ProdLineDetail pld
  222. INNER JOIN LineMaster lm ON pld.Line = lm.Line AND lm.tenant_id = pld.tenant_id
  223. WHERE pld.Part = @ItemNum AND pld.tenant_id = @TenantId AND IFNULL(pld.IsActive, 1) = 1
  224. ORDER BY IFNULL(pld.Sequence, 0)
  225. LIMIT 1
  226. """,
  227. new List<SugarParameter>
  228. {
  229. new("@ItemNum", itemNum.Trim()),
  230. new("@TenantId", tenantId)
  231. });
  232. return string.IsNullOrWhiteSpace(loc) ? null : loc;
  233. }
  234. private async Task<int> NextRecIdAsync(string table)
  235. {
  236. return await _db.Ado.GetIntAsync($"SELECT IFNULL(MAX(RecID), 0) + 1 FROM {table}");
  237. }
  238. private sealed class WorkOrderPickRow
  239. {
  240. public long RecId { get; set; }
  241. public string? Domain { get; set; }
  242. public string? ItemNum { get; set; }
  243. public decimal? QtyOrded { get; set; }
  244. public DateTime? OrdDate { get; set; }
  245. public string? ProdLine { get; set; }
  246. }
  247. private sealed class PickDetailRow
  248. {
  249. public string ItemNum { get; set; } = string.Empty;
  250. public decimal QtyRequired { get; set; }
  251. public string? LocationFrom { get; set; }
  252. public string? Unit { get; set; }
  253. public string? ItemName { get; set; }
  254. }
  255. }