ProcurementPipelineService.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. using Admin.NET.Plugin.AiDOP.Infrastructure;
  2. using Admin.NET.Plugin.AiDOP.ProcurementExecution;
  3. using Yitter.IdGenerator;
  4. namespace Admin.NET.Plugin.AiDOP.Supply;
  5. /// <summary>
  6. /// S4 采购执行闭环:缺料生成 PR → 合并 → 要货令转 DO/PO → 外部推送日志。
  7. /// 对应旧系统 AutoTransferDoOrPo / AutomaticPrAdjustDate 业务意图。
  8. /// </summary>
  9. [ApiDescriptionSettings(Order = 309, Description = "采购执行闭环")]
  10. [Route("api/Supply")]
  11. [AllowAnonymous]
  12. [NonUnify]
  13. public class ProcurementPipelineService : IDynamicApiController, ITransient
  14. {
  15. private readonly ISqlSugarClient _db;
  16. private readonly UserManager _userManager;
  17. private readonly PurchaseRequestFromShortageService _shortageService;
  18. private readonly PurchaseRequestMergeService _mergeService;
  19. private readonly NumberRuleService _numberRuleService;
  20. private readonly PurchaseOrderTransferService _transferService;
  21. private readonly PurchaseRequestExternalPushService _pushService;
  22. private readonly AidopActionRunLogWriter _runLog;
  23. private readonly S4MdpSyncTransformService _s4MdpSync;
  24. private readonly S4ReadPathConsistencyService _readPathCheck;
  25. public ProcurementPipelineService(
  26. ISqlSugarClient db,
  27. UserManager userManager,
  28. PurchaseRequestFromShortageService shortageService,
  29. PurchaseRequestMergeService mergeService,
  30. NumberRuleService numberRuleService,
  31. PurchaseOrderTransferService transferService,
  32. PurchaseRequestExternalPushService pushService,
  33. AidopActionRunLogWriter runLog,
  34. S4MdpSyncTransformService s4MdpSync,
  35. S4ReadPathConsistencyService readPathCheck)
  36. {
  37. _db = db;
  38. _userManager = userManager;
  39. _shortageService = shortageService;
  40. _mergeService = mergeService;
  41. _numberRuleService = numberRuleService;
  42. _transferService = transferService;
  43. _pushService = pushService;
  44. _runLog = runLog;
  45. _s4MdpSync = s4MdpSync;
  46. _readPathCheck = readPathCheck;
  47. }
  48. /// <summary>执行采购闭环(合并待处理 PR、转单、写 QadTracking)。</summary>
  49. [DisplayName("采购执行闭环")]
  50. [HttpPost("procurement/execute-pipeline")]
  51. public async Task<ProcurementPipelineResult> ExecutePipeline(
  52. [FromQuery] string? domain,
  53. [FromQuery] bool createFromShortage = true,
  54. [FromQuery] bool mergeHistorical = true,
  55. [FromQuery] bool enableExternalPush = true)
  56. {
  57. var tenantId = ResolveTenantId(domain);
  58. var account = _userManager.Account ?? "system";
  59. var logId = await _runLog.StartAsync("S4_PROCUREMENT_PIPELINE", tenantId, "tenant", tenantId, domain ?? tenantId.ToString());
  60. try
  61. {
  62. var result = await ExecuteCoreAsync(tenantId, account, createFromShortage, mergeHistorical, enableExternalPush);
  63. result.ReadPathConsistency = await _readPathCheck.CheckTenantAsync(tenantId);
  64. result.Warnings.AddRange(await TryTriggerS4MdpRefreshAsync(result));
  65. await _runLog.SuccessAsync(logId, result.Message, result);
  66. return result;
  67. }
  68. catch (Exception ex)
  69. {
  70. await _runLog.FailedAsync(logId, ex.Message, new { tenantId, domain });
  71. throw Oops.Oh(ex.Message);
  72. }
  73. }
  74. /// <summary>S4 读路径一致性检查(运行表 vs DWD)。</summary>
  75. [DisplayName("S4读路径一致性检查")]
  76. [HttpGet("procurement/read-path-consistency")]
  77. public async Task<S4ReadPathConsistencyResult> ReadPathConsistency([FromQuery] string? domain)
  78. {
  79. var tenantId = ResolveTenantId(domain);
  80. return await _readPathCheck.CheckTenantAsync(tenantId);
  81. }
  82. public async Task<ProcurementPipelineResult> ExecuteCoreAsync(
  83. long tenantId,
  84. string account,
  85. bool createFromShortage,
  86. bool mergeHistorical = true,
  87. bool enableExternalPush = true)
  88. {
  89. var result = new ProcurementPipelineResult();
  90. var now = DateTime.Now;
  91. await _db.Ado.BeginTranAsync();
  92. try
  93. {
  94. var newRequests = new List<PurchaseRequestMain>();
  95. if (createFromShortage)
  96. {
  97. newRequests = await _shortageService.BuildFromResourceCheckAsync(tenantId, account);
  98. result.ShortageItemCount = newRequests.Count;
  99. }
  100. if (newRequests.Count > 0)
  101. {
  102. var mergeNew = _mergeService.MergeGeneratedRequests(newRequests);
  103. newRequests = mergeNew.Requests;
  104. result.PrMergeReducedCount += mergeNew.ReducedCount;
  105. await AssignPrNumbersAsync(newRequests, account);
  106. await InsertPurchaseRequestsAsync(newRequests);
  107. result.PrCreatedCount = newRequests.Count;
  108. }
  109. if (mergeHistorical)
  110. {
  111. var historicalMerge = await _mergeService.MergeTenantPendingAsync(tenantId, account);
  112. result.PrMergeReducedCount += historicalMerge.ReducedCount;
  113. result.HistoricalPrMergedGroups = historicalMerge.MergedGroupCount;
  114. result.PrCreatedCount += historicalMerge.CreatedPrCount;
  115. }
  116. var pending = await LoadPendingPurchaseRequestsAsync(tenantId);
  117. result.PendingPrCount = pending.Count;
  118. if (pending.Any(x => x.IsRequireGoods == 1))
  119. {
  120. var transfer = await _transferService.TransferGeneratedRequireGoodsAsync(
  121. pending.Where(x => x.IsRequireGoods == 1).ToList(), account);
  122. result.PoCreatedCount = transfer.CreatedOrderCount;
  123. result.PoTransferredPrCount = transfer.TransferredPrCount;
  124. result.PoOccupyRehangedCount = transfer.PoOccupyRehangedCount;
  125. result.CreatedPoNumbers = transfer.CreatedOrders;
  126. await PersistPrStateUpdatesAsync(transfer.TransferredPrIds, 4, account);
  127. }
  128. var pushCandidates = pending
  129. .Where(x => x.IsRequireGoods == 0 && (x.State ?? 0) == 1)
  130. .Concat(newRequests.Where(x => x.IsRequireGoods == 0))
  131. .GroupBy(x => x.Id)
  132. .Select(g => g.First())
  133. .ToList();
  134. if (enableExternalPush && pushCandidates.Count > 0)
  135. {
  136. var push = await _pushService.CreateQadTrackingForGeneratedRequestsAsync(pushCandidates, account);
  137. result.QadTrackingCount = push.TrackingCount;
  138. result.QadTrackingSkippedCount = push.SkippedCandidateCount;
  139. result.Warnings.AddRange(push.Warnings);
  140. await PersistPrStateUpdatesAsync(push.PushedPrIds, 2, account);
  141. }
  142. await _db.Ado.CommitTranAsync();
  143. }
  144. catch
  145. {
  146. await _db.Ado.RollbackTranAsync();
  147. throw;
  148. }
  149. result.Message = BuildMessage(result);
  150. return result;
  151. }
  152. private async Task<List<PurchaseRequestMain>> LoadPendingPurchaseRequestsAsync(long tenantId)
  153. {
  154. var windowEnd = DateTime.Today.AddDays(35);
  155. var rows = await _db.Ado.SqlQueryAsync<PurchaseRequestMain>(
  156. """
  157. SELECT
  158. Id, pr_billno AS PrBillNo, pr_purchaseid AS PrPurchaseId,
  159. pr_purchasenumber AS PrPurchaseNumber, pr_purchasename AS PrPurchaseName,
  160. pr_purchaser AS PrPurchaser, pr_purchaser_num AS PrPurchaserNum,
  161. pr_rqty AS PrRqty, pr_aqty AS PrAqty, pr_sqty AS PrSqty,
  162. icitem_id AS IcitemId, icitem_name AS IcitemName,
  163. pr_ssend_date AS PrSsendDate, pr_sarrive_date AS PrSarriveDate,
  164. pr_unit AS PrUnit, state AS State, pr_type AS PrType,
  165. currencytype AS CurrencyType, tenant_id AS TenantId,
  166. factory_id AS FactoryId, org_id AS OrgId, company_id AS CompanyId,
  167. IsRequireGoods, supplier_type AS SupplierType, IsDeleted
  168. FROM srm_pr_main
  169. WHERE tenant_id = @TenantId
  170. AND IFNULL(IsDeleted, 0) = 0
  171. AND IFNULL(state, 0) = 1
  172. AND IFNULL(analogcalcversion, '') = ''
  173. AND pr_ssend_date IS NOT NULL
  174. AND pr_ssend_date <= @WindowEnd
  175. ORDER BY pr_ssend_date, Id
  176. """,
  177. new SugarParameter("@TenantId", tenantId),
  178. new SugarParameter("@WindowEnd", windowEnd));
  179. return rows;
  180. }
  181. private async Task AssignPrNumbersAsync(List<PurchaseRequestMain> requests, string account)
  182. {
  183. foreach (var group in requests.GroupBy(x => x.TenantId.ToString()))
  184. {
  185. var rows = group.ToList();
  186. var numbers = await _numberRuleService.NextBatchInCurrentTransactionAsync("PR", group.Key, rows.Count, account);
  187. if (numbers.Count < rows.Count)
  188. throw Oops.Oh("采购申请编号生成失败");
  189. for (var i = 0; i < rows.Count; i++)
  190. rows[i].PrBillNo = numbers[i].Trim();
  191. }
  192. }
  193. private async Task InsertPurchaseRequestsAsync(List<PurchaseRequestMain> requests)
  194. {
  195. foreach (var pr in requests)
  196. {
  197. await _db.Ado.ExecuteCommandAsync(
  198. """
  199. INSERT INTO srm_pr_main
  200. (Id,pr_billno,pr_purchaseid,pr_purchasenumber,pr_purchasename,pr_purchaser,pr_purchaser_num,
  201. pr_rqty,pr_aqty,pr_sqty,icitem_id,icitem_name,pr_ssend_date,pr_sarrive_date,pr_unit,state,pr_type,currencytype,
  202. create_by_name,create_time,update_by_name,update_time,tenant_id,factory_id,org_id,IsDeleted,company_id,IsRequireGoods,supplier_type)
  203. VALUES
  204. (@Id,@PrBillNo,@PrPurchaseId,@PrPurchaseNumber,@PrPurchaseName,@PrPurchaser,@PrPurchaserNum,
  205. @PrRqty,@PrAqty,@PrSqty,@IcitemId,@IcitemName,@PrSsendDate,@PrSarriveDate,@PrUnit,@State,@PrType,@CurrencyType,
  206. @CreateByName,@CreateTime,@UpdateByName,@UpdateTime,@TenantId,@FactoryId,@OrgId,0,@CompanyId,@IsRequireGoods,@SupplierType)
  207. """,
  208. new SugarParameter("@Id", pr.Id),
  209. new SugarParameter("@PrBillNo", pr.PrBillNo),
  210. new SugarParameter("@PrPurchaseId", pr.PrPurchaseId),
  211. new SugarParameter("@PrPurchaseNumber", pr.PrPurchaseNumber),
  212. new SugarParameter("@PrPurchaseName", pr.PrPurchaseName),
  213. new SugarParameter("@PrPurchaser", pr.PrPurchaser),
  214. new SugarParameter("@PrPurchaserNum", pr.PrPurchaserNum),
  215. new SugarParameter("@PrRqty", pr.PrRqty),
  216. new SugarParameter("@PrAqty", pr.PrAqty),
  217. new SugarParameter("@PrSqty", pr.PrSqty),
  218. new SugarParameter("@IcitemId", pr.IcitemId),
  219. new SugarParameter("@IcitemName", pr.IcitemName),
  220. new SugarParameter("@PrSsendDate", pr.PrSsendDate),
  221. new SugarParameter("@PrSarriveDate", pr.PrSarriveDate),
  222. new SugarParameter("@PrUnit", pr.PrUnit),
  223. new SugarParameter("@State", pr.State ?? 1),
  224. new SugarParameter("@PrType", pr.PrType ?? 3),
  225. new SugarParameter("@CurrencyType", pr.CurrencyType),
  226. new SugarParameter("@CreateByName", pr.CreateByName),
  227. new SugarParameter("@CreateTime", pr.CreateTime),
  228. new SugarParameter("@UpdateByName", pr.UpdateByName),
  229. new SugarParameter("@UpdateTime", pr.UpdateTime),
  230. new SugarParameter("@TenantId", pr.TenantId),
  231. new SugarParameter("@FactoryId", pr.FactoryId),
  232. new SugarParameter("@OrgId", pr.OrgId),
  233. new SugarParameter("@CompanyId", pr.CompanyId ?? 1000),
  234. new SugarParameter("@IsRequireGoods", pr.IsRequireGoods),
  235. new SugarParameter("@SupplierType", pr.SupplierType));
  236. }
  237. }
  238. private async Task PersistPrStateUpdatesAsync(IReadOnlyList<long> prIds, int state, string account)
  239. {
  240. if (prIds.Count == 0) return;
  241. foreach (var id in prIds.Distinct())
  242. {
  243. await _db.Ado.ExecuteCommandAsync(
  244. """
  245. UPDATE srm_pr_main
  246. SET state = @State, update_by_name = @User, update_time = @Now
  247. WHERE Id = @Id
  248. """,
  249. new SugarParameter("@State", state),
  250. new SugarParameter("@User", account),
  251. new SugarParameter("@Now", DateTime.Now),
  252. new SugarParameter("@Id", id));
  253. }
  254. }
  255. private long ResolveTenantId(string? domain)
  256. {
  257. if (!string.IsNullOrWhiteSpace(domain) && long.TryParse(domain.Trim(), out var tid) && tid > 0)
  258. return tid;
  259. return AidopTenantHelper.Resolve(App.HttpContext);
  260. }
  261. private async Task<List<string>> TryTriggerS4MdpRefreshAsync(ProcurementPipelineResult result)
  262. {
  263. var warnings = new List<string>();
  264. if (result.PrCreatedCount == 0 && result.PoCreatedCount == 0 && result.QadTrackingCount == 0)
  265. return warnings;
  266. try
  267. {
  268. var mdp = await _s4MdpSync.RunFullAsync(triggerType: "PROCUREMENT_PIPELINE");
  269. result.S4MdpRefreshed = true;
  270. result.S4MdpBatchId = mdp.BatchId;
  271. }
  272. catch (Exception ex)
  273. {
  274. warnings.Add($"S4 MDP/DWD 刷新未完成:{ex.Message}");
  275. }
  276. return warnings;
  277. }
  278. private static string BuildMessage(ProcurementPipelineResult r)
  279. {
  280. var parts = new List<string>();
  281. if (r.PrCreatedCount > 0) parts.Add($"新建 PR {r.PrCreatedCount}");
  282. if (r.PoCreatedCount > 0) parts.Add($"转 DO/PO {r.PoCreatedCount}");
  283. if (r.QadTrackingCount > 0) parts.Add($"外部推送 {r.QadTrackingCount}");
  284. return parts.Count > 0 ? string.Join(",", parts) : "无待处理采购申请";
  285. }
  286. }
  287. public sealed class ProcurementPipelineResult
  288. {
  289. public int ShortageItemCount { get; set; }
  290. public int PrCreatedCount { get; set; }
  291. public int PrMergeReducedCount { get; set; }
  292. public int HistoricalPrMergedGroups { get; set; }
  293. public int PendingPrCount { get; set; }
  294. public int PoCreatedCount { get; set; }
  295. public int PoTransferredPrCount { get; set; }
  296. public int PoOccupyRehangedCount { get; set; }
  297. public int QadTrackingCount { get; set; }
  298. public int QadTrackingSkippedCount { get; set; }
  299. public List<string> CreatedPoNumbers { get; set; } = new();
  300. public List<string> Warnings { get; set; } = new();
  301. public bool S4MdpRefreshed { get; set; }
  302. public string? S4MdpBatchId { get; set; }
  303. public S4ReadPathConsistencyResult? ReadPathConsistency { get; set; }
  304. public string Message { get; set; } = string.Empty;
  305. }