ProcurementPipelineService.cs 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894
  1. using Admin.NET.Plugin.AiDOP.Infrastructure;
  2. using Admin.NET.Plugin.AiDOP.ProcurementExecution;
  3. using Admin.NET.Plugin.AiDOP.WorkOrder;
  4. using Microsoft.Extensions.DependencyInjection;
  5. using Yitter.IdGenerator;
  6. namespace Admin.NET.Plugin.AiDOP.Supply;
  7. /// <summary>
  8. /// S4 采购执行闭环:缺料生成 PR → 合并 → 要货令转 DO/PO → 外部推送日志。
  9. /// 对应旧系统 AutoTransferDoOrPo / AutomaticPrAdjustDate 业务意图。
  10. /// </summary>
  11. [ApiDescriptionSettings(Order = 309, Description = "采购执行闭环")]
  12. [Route("api/Supply")]
  13. [AllowAnonymous]
  14. [NonUnify]
  15. public class ProcurementPipelineService : IDynamicApiController, ITransient
  16. {
  17. private readonly ISqlSugarClient _db;
  18. private readonly UserManager _userManager;
  19. private readonly PurchaseRequestFromShortageService _shortageService;
  20. private readonly PurchaseRequestMergeService _mergeService;
  21. private readonly NumberRuleService _numberRuleService;
  22. private readonly PurchaseOrderTransferService _transferService;
  23. private readonly PurchaseRequestExternalPushService _pushService;
  24. private readonly AidopActionRunLogWriter _runLog;
  25. private readonly MdpRefreshDispatcher _refreshDispatcher;
  26. private readonly S4ReadPathConsistencyService _readPathCheck;
  27. public ProcurementPipelineService(
  28. ISqlSugarClient db,
  29. UserManager userManager,
  30. PurchaseRequestFromShortageService shortageService,
  31. PurchaseRequestMergeService mergeService,
  32. NumberRuleService numberRuleService,
  33. PurchaseOrderTransferService transferService,
  34. PurchaseRequestExternalPushService pushService,
  35. AidopActionRunLogWriter runLog,
  36. MdpRefreshDispatcher refreshDispatcher,
  37. S4ReadPathConsistencyService readPathCheck)
  38. {
  39. _db = db;
  40. _userManager = userManager;
  41. _shortageService = shortageService;
  42. _mergeService = mergeService;
  43. _numberRuleService = numberRuleService;
  44. _transferService = transferService;
  45. _pushService = pushService;
  46. _runLog = runLog;
  47. _refreshDispatcher = refreshDispatcher;
  48. _readPathCheck = readPathCheck;
  49. }
  50. /// <summary>执行采购闭环(合并待处理 PR、转单)。</summary>
  51. [DisplayName("采购执行闭环")]
  52. [HttpPost("procurement/execute-pipeline")]
  53. public async Task<ProcurementPipelineResult> ExecutePipeline(
  54. [FromQuery] long tenantId = 0,
  55. [FromQuery] bool createFromShortage = true,
  56. [FromQuery] bool mergeHistorical = true,
  57. [FromQuery] bool enableExternalPush = true)
  58. {
  59. if (tenantId <= 0)
  60. tenantId = AidopTenantHelper.Resolve(App.HttpContext);
  61. var account = _userManager.Account ?? "system";
  62. var logId = await _runLog.StartAsync("S4_PROCUREMENT_PIPELINE", tenantId, "tenant", tenantId, tenantId.ToString());
  63. try
  64. {
  65. var result = await ExecuteCoreAsync(tenantId, account, createFromShortage, mergeHistorical, enableExternalPush);
  66. result.ReadPathConsistency = await _readPathCheck.CheckTenantAsync(tenantId);
  67. result.Warnings.AddRange(TryTriggerS4MdpRefresh(result));
  68. await _runLog.SuccessAsync(logId, result.Message, result);
  69. return result;
  70. }
  71. catch (Exception ex)
  72. {
  73. await _runLog.FailedAsync(logId, ex.Message, new { tenantId });
  74. throw Oops.Oh(ex.Message);
  75. }
  76. }
  77. /// <summary>S4 读路径一致性检查(运行表 vs DWD)。</summary>
  78. [DisplayName("S4读路径一致性检查")]
  79. [HttpGet("procurement/read-path-consistency")]
  80. public async Task<S4ReadPathConsistencyResult> ReadPathConsistency([FromQuery] long tenantId = 0)
  81. {
  82. if (tenantId <= 0)
  83. tenantId = AidopTenantHelper.Resolve(App.HttpContext);
  84. return await _readPathCheck.CheckTenantAsync(tenantId);
  85. }
  86. public async Task<ProcurementPipelineResult> ExecuteCoreAsync(
  87. long tenantId,
  88. string account,
  89. bool createFromShortage,
  90. bool mergeHistorical = true,
  91. bool enableExternalPush = true)
  92. {
  93. var result = new ProcurementPipelineResult();
  94. var now = DateTime.Now;
  95. await _db.Ado.BeginTranAsync();
  96. try
  97. {
  98. var newRequests = new List<PurchaseRequestMain>();
  99. var deleteVouchers = new List<long>();
  100. if (createFromShortage)
  101. {
  102. var candidates = await _shortageService.BuildFromResourceCheckAsync(tenantId, account);
  103. result.ShortageItemCount = candidates.Count;
  104. // 幂等处理:对比已有 PR,更新/删除/保留
  105. var (toAdd, toDelete) = await _shortageService.ReconcileAsync(tenantId, candidates, account);
  106. newRequests = toAdd;
  107. deleteVouchers = toDelete;
  108. result.PrUpdatedCount = candidates.Count - toAdd.Count - toDelete.Count;
  109. }
  110. if (newRequests.Count > 0)
  111. {
  112. // 保留各工单独立 PR + 创建合并 PR(数量重新计算)
  113. var split = await _mergeService.SplitMergeWithRecalcAsync(newRequests, account);
  114. var individualPrs = split.IndividualRequests;
  115. var mergedPrs = split.MergedRequests;
  116. // 分配编号
  117. await AssignPrNumbersAsync(individualPrs, account);
  118. await AssignPrNumbersAsync(mergedPrs, account);
  119. // 独立 PR: state=0(合并后旧PR关闭,不参与 DO/PO 转单)
  120. foreach (var pr in individualPrs) pr.State = 0;
  121. // 关联独立 PR 到合并 PR 编号
  122. foreach (var group in individualPrs.GroupBy(x => new { x.IcitemId, x.PrPurchaseId }))
  123. {
  124. var merged = mergedPrs.FirstOrDefault(m =>
  125. m.IcitemId == group.Key.IcitemId && m.PrPurchaseId == group.Key.PrPurchaseId);
  126. if (merged is not null)
  127. {
  128. foreach (var ind in group)
  129. ind.IndividualPrReferBillNo = merged.PrBillNo;
  130. }
  131. }
  132. // 插入独立 PR(参考)
  133. await InsertIndividualPurchaseRequestsAsync(individualPrs);
  134. // 插入合并 PR(可转单)
  135. await InsertPurchaseRequestsAsync(mergedPrs);
  136. result.PrCreatedCount = mergedPrs.Count;
  137. result.IndividualPrCount = individualPrs.Count;
  138. result.PrMergeReducedCount = Math.Max(0, individualPrs.Count - mergedPrs.Count);
  139. }
  140. if (mergeHistorical)
  141. {
  142. var historicalMerge = await _mergeService.MergeTenantPendingAsync(tenantId, account);
  143. result.PrMergeReducedCount += historicalMerge.ReducedCount;
  144. result.HistoricalPrMergedGroups = historicalMerge.MergedGroupCount;
  145. result.PrCreatedCount += historicalMerge.CreatedPrCount;
  146. }
  147. // 删除已无缺料的旧 PR
  148. if (deleteVouchers.Count > 0)
  149. {
  150. foreach (var v in deleteVouchers)
  151. {
  152. await _db.Ado.ExecuteCommandAsync(
  153. """
  154. UPDATE srm_pr_main
  155. SET IsDeleted = 1, update_by_name = @User, update_time = @Now
  156. WHERE voucher = @Voucher
  157. """,
  158. new SugarParameter("@User", account),
  159. new SugarParameter("@Now", DateTime.Now),
  160. new SugarParameter("@Voucher", v));
  161. }
  162. result.PrDeletedCount = deleteVouchers.Count;
  163. }
  164. // ── 生成物料交货计划(ic_demandschedule)──
  165. result.DemandScheduleCount = await GenerateDemandScheduleAsync(tenantId, account);
  166. var pending = await LoadPendingPurchaseRequestsAsync(tenantId);
  167. result.PendingPrCount = pending.Count;
  168. if (pending.Count > 0)
  169. {
  170. var transfer = await _transferService.TransferGeneratedRequireGoodsAsync(pending, account);
  171. result.PoCreatedCount = transfer.CreatedOrderCount;
  172. result.PoTransferredPrCount = transfer.TransferredPrCount;
  173. result.PoOccupyRehangedCount = transfer.PoOccupyRehangedCount;
  174. result.CreatedPoNumbers = transfer.CreatedOrders;
  175. await PersistPrStateUpdatesAsync(transfer.TransferredPrIds, 4, account);
  176. }
  177. await _db.Ado.CommitTranAsync();
  178. }
  179. catch
  180. {
  181. await _db.Ado.RollbackTranAsync();
  182. throw;
  183. }
  184. result.Message = BuildMessage(result);
  185. return result;
  186. }
  187. private async Task<List<PurchaseRequestMain>> LoadPendingPurchaseRequestsAsync(long tenantId)
  188. {
  189. var windowEnd = DateTime.Today.AddDays(35);
  190. var rows = await _db.Ado.SqlQueryAsync<PurchaseRequestMain>(
  191. """
  192. SELECT
  193. Id, pr_billno AS PrBillNo, pr_purchaseid AS PrPurchaseId,
  194. pr_purchasenumber AS PrPurchaseNumber, pr_purchasename AS PrPurchaseName,
  195. pr_purchaser AS PrPurchaser, pr_purchaser_num AS PrPurchaserNum,
  196. pr_rqty AS PrRqty, pr_aqty AS PrAqty, pr_sqty AS PrSqty,
  197. icitem_id AS IcitemId, icitem_name AS IcitemName,
  198. pr_ssend_date AS PrSsendDate, pr_sarrive_date AS PrSarriveDate,
  199. pr_unit AS PrUnit, state AS State, pr_type AS PrType,
  200. currencytype AS CurrencyType, tenant_id AS TenantId,
  201. factory_id AS FactoryId, org_id AS OrgId, company_id AS CompanyId,
  202. IsRequireGoods, supplier_type AS SupplierType, IsDeleted
  203. FROM srm_pr_main
  204. WHERE tenant_id = @TenantId
  205. AND IFNULL(IsDeleted, 0) = 0
  206. AND IFNULL(state, 0) = 1
  207. AND IFNULL(analogcalcversion, '') = ''
  208. AND pr_ssend_date IS NOT NULL
  209. AND pr_ssend_date <= @WindowEnd
  210. ORDER BY pr_ssend_date, Id
  211. """,
  212. new SugarParameter("@TenantId", tenantId),
  213. new SugarParameter("@WindowEnd", windowEnd));
  214. return rows;
  215. }
  216. private async Task AssignPrNumbersAsync(List<PurchaseRequestMain> requests, string account)
  217. {
  218. foreach (var group in requests.GroupBy(x => x.TenantId.ToString()))
  219. {
  220. var rows = group.ToList();
  221. var numbers = await _numberRuleService.NextBatchInCurrentTransactionAsync("PR", group.Key, rows.Count, account);
  222. if (numbers.Count < rows.Count)
  223. throw Oops.Oh("采购申请编号生成失败");
  224. for (var i = 0; i < rows.Count; i++)
  225. rows[i].PrBillNo = numbers[i].Trim();
  226. }
  227. }
  228. private async Task InsertPurchaseRequestsAsync(List<PurchaseRequestMain> requests)
  229. {
  230. foreach (var pr in requests)
  231. {
  232. await _db.Ado.ExecuteCommandAsync(
  233. """
  234. INSERT INTO srm_pr_main
  235. (Id,pr_billno,pr_mono,entity_id,pr_purchaseid,pr_purchasenumber,pr_purchasename,pr_purchaser,pr_purchaser_num,
  236. pr_rqty,pr_aqty,pr_sqty,icitem_id,icitem_name,pr_ssend_date,pr_sarrive_date,pr_unit,state,pr_type,currencytype,
  237. create_by_name,create_time,update_by_name,update_time,tenant_id,factory_id,org_id,IsDeleted,company_id,IsRequireGoods,supplier_type)
  238. VALUES
  239. (@Id,@PrBillNo,@PrMono,@EntityId,@PrPurchaseId,@PrPurchaseNumber,@PrPurchaseName,@PrPurchaser,@PrPurchaserNum,
  240. @PrRqty,@PrAqty,@PrSqty,@IcitemId,@IcitemName,@PrSsendDate,@PrSarriveDate,@PrUnit,@State,@PrType,@CurrencyType,
  241. @CreateByName,@CreateTime,@UpdateByName,@UpdateTime,@TenantId,@FactoryId,@OrgId,0,@CompanyId,@IsRequireGoods,@SupplierType)
  242. """,
  243. new SugarParameter("@Id", pr.Id),
  244. new SugarParameter("@PrBillNo", pr.PrBillNo),
  245. new SugarParameter("@PrMono", pr.PrMono ?? (object)DBNull.Value),
  246. new SugarParameter("@EntityId", pr.EntityId ?? (object)DBNull.Value),
  247. new SugarParameter("@PrPurchaseId", pr.PrPurchaseId),
  248. new SugarParameter("@PrPurchaseNumber", pr.PrPurchaseNumber),
  249. new SugarParameter("@PrPurchaseName", pr.PrPurchaseName),
  250. new SugarParameter("@PrPurchaser", pr.PrPurchaser),
  251. new SugarParameter("@PrPurchaserNum", pr.PrPurchaserNum),
  252. new SugarParameter("@PrRqty", pr.PrRqty),
  253. new SugarParameter("@PrAqty", pr.PrAqty),
  254. new SugarParameter("@PrSqty", pr.PrSqty),
  255. new SugarParameter("@IcitemId", pr.IcitemId),
  256. new SugarParameter("@IcitemName", pr.IcitemName),
  257. new SugarParameter("@PrSsendDate", pr.PrSsendDate),
  258. new SugarParameter("@PrSarriveDate", pr.PrSarriveDate),
  259. new SugarParameter("@PrUnit", pr.PrUnit),
  260. new SugarParameter("@State", pr.State ?? 1),
  261. new SugarParameter("@PrType", pr.PrType ?? 3),
  262. new SugarParameter("@CurrencyType", pr.CurrencyType),
  263. new SugarParameter("@CreateByName", pr.CreateByName),
  264. new SugarParameter("@CreateTime", pr.CreateTime),
  265. new SugarParameter("@UpdateByName", pr.UpdateByName),
  266. new SugarParameter("@UpdateTime", pr.UpdateTime),
  267. new SugarParameter("@TenantId", pr.TenantId),
  268. new SugarParameter("@FactoryId", pr.FactoryId),
  269. new SugarParameter("@OrgId", pr.OrgId),
  270. new SugarParameter("@CompanyId", pr.CompanyId ?? 1000),
  271. new SugarParameter("@IsRequireGoods", pr.IsRequireGoods),
  272. new SugarParameter("@SupplierType", pr.SupplierType));
  273. }
  274. }
  275. private async Task InsertIndividualPurchaseRequestsAsync(List<PurchaseRequestMain> requests)
  276. {
  277. foreach (var pr in requests)
  278. {
  279. await _db.Ado.ExecuteCommandAsync(
  280. """
  281. INSERT INTO srm_pr_main
  282. (Id,pr_billno,pr_mono,entity_id,pr_purchaseid,pr_purchasenumber,pr_purchasename,pr_purchaser,pr_purchaser_num,
  283. pr_rqty,pr_aqty,pr_sqty,icitem_id,icitem_name,pr_ssend_date,pr_sarrive_date,pr_unit,state,pr_type,currencytype,
  284. create_by_name,create_time,update_by_name,update_time,tenant_id,factory_id,org_id,IsDeleted,company_id,
  285. IsRequireGoods,supplier_type,refer_pr_billno)
  286. VALUES
  287. (@Id,@PrBillNo,@PrMono,@EntityId,@PrPurchaseId,@PrPurchaseNumber,@PrPurchaseName,@PrPurchaser,@PrPurchaserNum,
  288. @PrRqty,@PrAqty,@PrSqty,@IcitemId,@IcitemName,@PrSsendDate,@PrSarriveDate,@PrUnit,@State,@PrType,@CurrencyType,
  289. @CreateByName,@CreateTime,@UpdateByName,@UpdateTime,@TenantId,@FactoryId,@OrgId,0,@CompanyId,
  290. @IsRequireGoods,@SupplierType,@ReferPrBillNo)
  291. """,
  292. new SugarParameter("@Id", pr.Id),
  293. new SugarParameter("@PrBillNo", pr.PrBillNo),
  294. new SugarParameter("@PrMono", pr.PrMono ?? (object)DBNull.Value),
  295. new SugarParameter("@EntityId", pr.EntityId ?? (object)DBNull.Value),
  296. new SugarParameter("@PrPurchaseId", pr.PrPurchaseId),
  297. new SugarParameter("@PrPurchaseNumber", pr.PrPurchaseNumber),
  298. new SugarParameter("@PrPurchaseName", pr.PrPurchaseName),
  299. new SugarParameter("@PrPurchaser", pr.PrPurchaser),
  300. new SugarParameter("@PrPurchaserNum", pr.PrPurchaserNum),
  301. new SugarParameter("@PrRqty", pr.PrRqty),
  302. new SugarParameter("@PrAqty", pr.PrAqty),
  303. new SugarParameter("@PrSqty", pr.PrSqty),
  304. new SugarParameter("@IcitemId", pr.IcitemId),
  305. new SugarParameter("@IcitemName", pr.IcitemName),
  306. new SugarParameter("@PrSsendDate", pr.PrSsendDate),
  307. new SugarParameter("@PrSarriveDate", pr.PrSarriveDate),
  308. new SugarParameter("@PrUnit", pr.PrUnit),
  309. new SugarParameter("@State", pr.State ?? 0),
  310. new SugarParameter("@PrType", pr.PrType ?? 3),
  311. new SugarParameter("@CurrencyType", pr.CurrencyType),
  312. new SugarParameter("@CreateByName", pr.CreateByName),
  313. new SugarParameter("@CreateTime", pr.CreateTime),
  314. new SugarParameter("@UpdateByName", pr.UpdateByName),
  315. new SugarParameter("@UpdateTime", pr.UpdateTime),
  316. new SugarParameter("@TenantId", pr.TenantId),
  317. new SugarParameter("@FactoryId", pr.FactoryId),
  318. new SugarParameter("@OrgId", pr.OrgId),
  319. new SugarParameter("@CompanyId", pr.CompanyId ?? 1000),
  320. new SugarParameter("@IsRequireGoods", pr.IsRequireGoods),
  321. new SugarParameter("@SupplierType", pr.SupplierType),
  322. new SugarParameter("@ReferPrBillNo", pr.IndividualPrReferBillNo ?? (object)DBNull.Value));
  323. }
  324. }
  325. private async Task PersistPrStateUpdatesAsync(IReadOnlyList<long> prIds, int state, string account)
  326. {
  327. if (prIds.Count == 0) return;
  328. foreach (var id in prIds.Distinct())
  329. {
  330. await _db.Ado.ExecuteCommandAsync(
  331. """
  332. UPDATE srm_pr_main
  333. SET state = @State, update_by_name = @User, update_time = @Now
  334. WHERE Id = @Id
  335. """,
  336. new SugarParameter("@State", state),
  337. new SugarParameter("@User", account),
  338. new SugarParameter("@Now", DateTime.Now),
  339. new SugarParameter("@Id", id));
  340. }
  341. }
  342. private List<string> TryTriggerS4MdpRefresh(ProcurementPipelineResult result)
  343. {
  344. if (result.PrCreatedCount == 0 && result.PoCreatedCount == 0)
  345. return new List<string>();
  346. // 后台异步触发 S4 全量刷新(单飞+去抖),管线请求不再等待其完成
  347. _refreshDispatcher.Enqueue("S4", (sp, ct) =>
  348. sp.GetRequiredService<S4MdpSyncTransformService>().RunFullAsync(ct, "PROCUREMENT_PIPELINE"));
  349. result.S4MdpRefreshed = true; // 语义变更:true = 已派发后台刷新(非“已刷新完成”)
  350. result.S4MdpBatchId = null; // 不再同步等待,故无同步批次号
  351. return new List<string>();
  352. }
  353. /// <summary>
  354. /// 生成物料交货计划(ic_demandschedule):按 8 周窗口汇总工单缺料与领料单待发料,
  355. /// 扣减库存和已发布交货单在途后写入需求计划。
  356. /// </summary>
  357. private async Task<int> GenerateDemandScheduleAsync(long tenantId, string account)
  358. {
  359. var now = DateTime.Now;
  360. // ── 1. 软删除旧未发布需求计划 + 禁用旧未发布交货单 + 标记历史版本 ──
  361. await _db.Ado.ExecuteCommandAsync(
  362. """
  363. UPDATE ic_demandschedule
  364. SET IsDeleted = 1, update_by_name = @User, update_time = @Now,
  365. remarks = CONCAT(@NowStr, ',重新生成交货计划把未发布交货计划软删除')
  366. WHERE tenant_id = @TenantId
  367. AND IFNULL(IsDeleted, 0) = 0
  368. AND IFNULL(ishistoryversion, 'N') <> 'Y'
  369. AND IFNULL(status, '') <> 'P'
  370. """,
  371. new SugarParameter("@User", account),
  372. new SugarParameter("@Now", now),
  373. new SugarParameter("@NowStr", now.ToString("yyyy-MM-dd HH:mm:ss")),
  374. new SugarParameter("@TenantId", tenantId));
  375. await _db.Ado.ExecuteCommandAsync(
  376. """
  377. UPDATE srm_polist_ds
  378. SET isactive = 0, updateuser = @User, updatetime = @Now,
  379. remarks = CONCAT(@NowStr, ',重新生成交货计划把未发布交货单禁用')
  380. WHERE tenant_id = @TenantId AND isactive = 1 AND IFNULL(status, 'N') = 'N'
  381. """,
  382. new SugarParameter("@User", account),
  383. new SugarParameter("@Now", now),
  384. new SugarParameter("@NowStr", now.ToString("yyyy-MM-dd HH:mm:ss")),
  385. new SugarParameter("@TenantId", tenantId));
  386. await _db.Ado.ExecuteCommandAsync(
  387. """
  388. UPDATE ic_demandschedule
  389. SET ishistoryversion = 'Y', historyversionTime = @Now
  390. WHERE tenant_id = @TenantId
  391. AND IFNULL(IsDeleted, 0) = 0
  392. AND IFNULL(ishistoryversion, 'N') <> 'Y'
  393. """,
  394. new SugarParameter("@Now", now),
  395. new SugarParameter("@TenantId", tenantId));
  396. // ── 2. 计算 8 周窗口(从下周一开始) ──
  397. var weekday = (int)now.DayOfWeek;
  398. var addDays = weekday == 0 ? 1 : 8 - weekday;
  399. var beginTime = now.Date.AddDays(addDays);
  400. var endTime = beginTime.AddDays(21 + 28 + 6).Add(new TimeSpan(23, 59, 59));
  401. // ── 3. 加载窗口内工单(Status 为空或 P) ──
  402. var workOrds = await _db.Ado.SqlQueryAsync<WorkOrdRow>(
  403. """
  404. SELECT WorkOrd, OrdDate, Status
  405. FROM WorkOrdMaster
  406. WHERE tenant_id = @TenantId
  407. AND OrdDate >= @Begin AND OrdDate <= @End
  408. AND (Status IS NULL OR UPPER(Status) = 'P')
  409. """,
  410. new SugarParameter("@TenantId", tenantId),
  411. new SugarParameter("@Begin", beginTime),
  412. new SugarParameter("@End", endTime));
  413. // ── 4. 加载已下达/投产/暂停工单(提前开工,用于领料单) ──
  414. var pickBillWorkOrds = await _db.Ado.SqlQueryAsync<string>(
  415. """
  416. SELECT DISTINCT WorkOrd
  417. FROM WorkOrdMaster
  418. WHERE tenant_id = @TenantId
  419. AND Status IS NOT NULL AND UPPER(Status) NOT IN ('C', 'P')
  420. """,
  421. new SugarParameter("@TenantId", tenantId));
  422. var allWorkOrdList = workOrds.Select(w => w.WorkOrd!).ToList();
  423. var pickBillWorkOrdList = pickBillWorkOrds.ToList();
  424. allWorkOrdList = allWorkOrdList.Union(pickBillWorkOrdList).Distinct().ToList();
  425. if (allWorkOrdList.Count == 0)
  426. return 0;
  427. // ── 5. 加载每个工单的最新资源检查结果 ──
  428. var examineResults = await _db.Ado.SqlQueryAsync<ExamineResultRow>(
  429. """
  430. SELECT ber.morder_no AS MorderNo, MAX(ber.Id) AS ExamineId, MAX(ber.bangid) AS BangId
  431. FROM b_examine_result ber
  432. WHERE ber.tenant_id = @TenantId AND ber.IsDeleted = 0
  433. AND ber.morder_no IN (@WorkOrds)
  434. GROUP BY ber.morder_no
  435. """.Replace("@WorkOrds", string.Join(",", allWorkOrdList.Select((_, i) => $"@Wo{i}"))),
  436. BuildInParams(allWorkOrdList, tenantId));
  437. var examineIds = examineResults.Select(e => e.ExamineId).ToList();
  438. // ── 6. 加载 BOM 子件检查行(erp_cls=3 外购 或 2 委外) ──
  439. var examines = examineIds.Count == 0
  440. ? new List<BomChildExamineRow>()
  441. : await _db.Ado.SqlQueryAsync<BomChildExamineRow>(
  442. """
  443. SELECT item_number AS ItemNumber, examine_id AS ExamineId,
  444. IFNULL(lack_qty, 0) AS LackQty, IFNULL(needCount, 0) AS NeedCount,
  445. tenant_id AS TenantId, company_id AS CompanyId
  446. FROM b_bom_child_examine
  447. WHERE tenant_id = @TenantId AND is_use = 1
  448. AND (erp_cls = 3 OR erp_cls = 2)
  449. AND examine_id IN (@ExamineIds)
  450. """.Replace("@ExamineIds", string.Join(",", examineIds.Select((_, i) => $"@Ei{i}"))),
  451. BuildExamineParams(examineIds, tenantId));
  452. // ── 7. 加载领料单待发料明细(NbrDetail Type=SM, QtyOrd-QtyRec>0, Status!=C) ──
  453. var pickBills = await _db.Ado.SqlQueryAsync<PickBillRow>(
  454. """
  455. SELECT ItemNum, QtyOrd, QtyRec
  456. FROM NbrDetail
  457. WHERE tenant_id = @TenantId AND UPPER(Type) = 'SM'
  458. AND (QtyOrd - QtyRec) > 0 AND UPPER(IFNULL(Status, '')) <> 'C'
  459. AND WorkOrd IN (@PickWorkOrds)
  460. """.Replace("@PickWorkOrds", string.Join(",", pickBillWorkOrdList.Select((_, i) => $"@Pw{i}"))),
  461. BuildInParams(pickBillWorkOrdList, tenantId, "Pw"));
  462. // ── 8. 加载已发布交货单在途(srm_polist_ds isactive=1, status='P') ──
  463. var publishedDsList = await _db.Ado.SqlQueryAsync<PublishedDsRow>(
  464. """
  465. SELECT itemnum, IFNULL(SUM(schedqty - sentqty), 0) AS PendingQty
  466. FROM srm_polist_ds
  467. WHERE tenant_id = @TenantId AND isactive = 1 AND IFNULL(status, 'N') = 'P'
  468. GROUP BY itemnum
  469. """,
  470. new SugarParameter("@TenantId", tenantId));
  471. // ── 9. 收集所有涉及物料编号 ──
  472. var items = examines.Select(e => e.ItemNumber!).Distinct().ToList();
  473. items.AddRange(pickBills.Select(p => p.ItemNum!));
  474. items = items.Distinct().ToList();
  475. if (items.Count == 0)
  476. return 0;
  477. // ── 10. 加载物料主数据 ──
  478. var itemList = await _db.Ado.SqlQueryAsync<ItemMasterRow>(
  479. """
  480. SELECT ItemNum, IFNULL(Rev, '') AS Rev, IFNULL(Drawing, '') AS Drawing,
  481. IFNULL(InsLT, 0) AS InsLT, IFNULL(MFGMTTR, 0) AS MFGMTTR
  482. FROM ItemMaster
  483. WHERE tenant_id = @TenantId AND ItemNum IN (@Items)
  484. """.Replace("@Items", string.Join(",", items.Select((_, i) => $"@It{i}"))),
  485. BuildInParams(items, tenantId, "It"));
  486. // ── 11. 加载库存(InvMaster,按配置的库位范围) ──
  487. var locationList = new List<string> { "1001", "5007", "5008", "8000", "8001" };
  488. var locationInClause = string.Join(",", locationList.Select(l => $"'{l}'"));
  489. var stockRows = await _db.Ado.SqlQueryAsync<StockRow>(
  490. $"""
  491. SELECT ItemNum,
  492. IFNULL(SUM(IFNULL(AvailStatusQty, 0) + IFNULL(Assay, 0)), 0) AS Qty
  493. FROM InvMaster
  494. WHERE ItemNum IN ({string.Join(",", items.Select((_, i) => $"@Sk{i}"))})
  495. AND IsActive = 1
  496. AND Location IN ({locationInClause})
  497. AND tenant_id = @TenantId
  498. GROUP BY ItemNum
  499. """,
  500. BuildInParams(items, tenantId, "Sk"));
  501. var weekStockQty = stockRows.ToDictionary(s => s.ItemNum!, s => s.Qty, StringComparer.OrdinalIgnoreCase);
  502. var weekDsQty = publishedDsList.ToDictionary(d => d.ItemNum!, d => d.PendingQty, StringComparer.OrdinalIgnoreCase);
  503. // ── 12. 按 8 周循环生成需求计划 ──
  504. var dsRecords = new List<DemandScheduleInsertRow>();
  505. var remainingPickBills = pickBills.ToList();
  506. for (var i = 0; i < 8; i++)
  507. {
  508. var itemBegin = beginTime.AddDays(i * 7);
  509. var itemEnd = endTime.AddDays(7 * i - 21 - 28);
  510. var weekWorkOrds = workOrds.Where(a => a.OrdDate >= itemBegin && a.OrdDate <= itemEnd).ToList();
  511. var itemQtyList = new List<DemandItemDto>();
  512. if (weekWorkOrds.Count > 0)
  513. {
  514. foreach (var wo in weekWorkOrds)
  515. {
  516. // 已下达工单(R)的缺料已计入领料单,不在此重复
  517. if (!string.IsNullOrEmpty(wo.Status) && wo.Status.ToUpper() == "R")
  518. continue;
  519. var exam = examineResults.FirstOrDefault(e => e.MorderNo == wo.WorkOrd);
  520. if (exam is null || exam.ExamineId <= 0)
  521. continue;
  522. var itemLackList = examines.Where(a => a.ExamineId == exam.ExamineId).ToList();
  523. foreach (var lack in itemLackList)
  524. {
  525. var existing = itemQtyList.FirstOrDefault(x => x.ItemNum == lack.ItemNumber);
  526. if (existing != null)
  527. {
  528. existing.LackQty += lack.LackQty;
  529. existing.NeedQty += lack.NeedCount;
  530. if (!existing.WorkOrds.Contains(wo.WorkOrd!))
  531. existing.WorkOrds += "," + wo.WorkOrd;
  532. if (!existing.BangId.Contains(exam.BangId?.ToString() ?? ""))
  533. existing.BangId += "," + (exam.BangId?.ToString() ?? "");
  534. }
  535. else
  536. {
  537. itemQtyList.Add(new DemandItemDto
  538. {
  539. ItemNum = lack.ItemNumber!,
  540. LackQty = lack.LackQty,
  541. NeedQty = lack.NeedCount,
  542. WorkOrds = wo.WorkOrd!,
  543. BangId = exam.BangId?.ToString() ?? ""
  544. });
  545. }
  546. }
  547. }
  548. var requestDate = weekWorkOrds.Min(a => a.OrdDate) ?? DateTime.Today;
  549. foreach (var d in itemQtyList)
  550. {
  551. var im = itemList.FirstOrDefault(a => a.ItemNum == d.ItemNum);
  552. var arrivalDate = requestDate.AddDays(-1).AddDays(-(im?.InsLT ?? 0)).AddDays(-(im?.MFGMTTR ?? 0));
  553. decimal mesQty = d.NeedQty;
  554. // 第一周:工单需求 + 已下达工单领料单待发料(只算一次)
  555. if (i == 0)
  556. {
  557. var pickQty = remainingPickBills.Where(p => p.ItemNum == d.ItemNum).Sum(q => q.QtyOrd - q.QtyRec);
  558. mesQty = d.NeedQty + pickQty;
  559. remainingPickBills.RemoveAll(p => p.ItemNum == d.ItemNum);
  560. }
  561. // 库存扣减
  562. decimal stockDeduction = 0;
  563. weekStockQty.TryGetValue(d.ItemNum, out var stock);
  564. var locQty = stock;
  565. if (mesQty >= locQty)
  566. {
  567. weekStockQty[d.ItemNum] = 0;
  568. stockDeduction = locQty;
  569. }
  570. else
  571. {
  572. weekStockQty[d.ItemNum] = locQty - mesQty;
  573. stockDeduction = mesQty;
  574. }
  575. // 交货单在途扣减
  576. decimal sechedQty = 0;
  577. if (weekDsQty.TryGetValue(d.ItemNum, out var dsPending))
  578. {
  579. sechedQty = dsPending;
  580. if (mesQty - stockDeduction - sechedQty >= 0)
  581. weekDsQty[d.ItemNum] = 0;
  582. else
  583. weekDsQty[d.ItemNum] = dsPending - (mesQty - stockDeduction);
  584. }
  585. var toSechedQty = mesQty - (locQty + sechedQty);
  586. dsRecords.Add(new DemandScheduleInsertRow
  587. {
  588. ItemNum = d.ItemNum,
  589. FVersion = im?.Rev ?? "",
  590. Drawing = im?.Drawing ?? "",
  591. RequestDate = requestDate,
  592. ArrivalDate = arrivalDate,
  593. ShortQty = d.LackQty,
  594. MesQty = mesQty,
  595. LocQty = locQty,
  596. SechedQty = sechedQty,
  597. ToSechedQty = toSechedQty,
  598. WoList = d.WorkOrds,
  599. BangId = d.BangId
  600. });
  601. }
  602. }
  603. // 第一周额外处理:领料单中剩余的物料(不在资源检查缺料中的已下达工单待发料)
  604. if (i == 0 && remainingPickBills.Count > 0)
  605. {
  606. var pickbillItems = remainingPickBills.Select(a => a.ItemNum!).Distinct().ToList();
  607. var requestDate = itemBegin;
  608. foreach (var item in pickbillItems)
  609. {
  610. var im = itemList.FirstOrDefault(a => a.ItemNum == item);
  611. if (im is null) continue;
  612. var arrivalDate = requestDate.AddDays(-1).AddDays(-im.InsLT).AddDays(-im.MFGMTTR);
  613. var mesQty = remainingPickBills.Where(p => p.ItemNum == item).Sum(q => q.QtyOrd - q.QtyRec);
  614. decimal stockDeduction = 0;
  615. weekStockQty.TryGetValue(item, out var stock);
  616. var locQty = stock;
  617. if (mesQty >= locQty)
  618. {
  619. weekStockQty[item] = 0;
  620. stockDeduction = locQty;
  621. }
  622. else
  623. {
  624. weekStockQty[item] = locQty - mesQty;
  625. stockDeduction = mesQty;
  626. }
  627. decimal sechedQty = 0;
  628. if (weekDsQty.TryGetValue(item, out var dsPending))
  629. {
  630. sechedQty = dsPending;
  631. if (mesQty - stockDeduction - sechedQty >= 0)
  632. weekDsQty[item] = 0;
  633. else
  634. weekDsQty[item] = dsPending - (mesQty - stockDeduction);
  635. }
  636. var toSechedQty = mesQty - (locQty + sechedQty);
  637. dsRecords.Add(new DemandScheduleInsertRow
  638. {
  639. ItemNum = item,
  640. FVersion = im.Rev,
  641. Drawing = im.Drawing,
  642. RequestDate = requestDate,
  643. ArrivalDate = arrivalDate,
  644. ShortQty = 0,
  645. MesQty = mesQty,
  646. LocQty = locQty,
  647. SechedQty = sechedQty,
  648. ToSechedQty = toSechedQty,
  649. WoList = "",
  650. BangId = ""
  651. });
  652. }
  653. }
  654. }
  655. // ── 13. 批量插入 ic_demandschedule ──
  656. foreach (var ds in dsRecords)
  657. {
  658. await _db.Ado.ExecuteCommandAsync(
  659. """
  660. INSERT INTO ic_demandschedule
  661. (Id, itemnum, fversion, drawing, requestdate, arrivaldate,
  662. shortqty, mesqty, locqty, sechedqty, tosechedqty,
  663. status, remarks, ishistoryversion,
  664. create_by_name, create_time, update_by_name, update_time,
  665. tenant_id, factory_id, org_id, company_id, IsDeleted,
  666. wolist, bangid)
  667. VALUES
  668. (@Id, @ItemNum, @FVersion, @Drawing, @RequestDate, @ArrivalDate,
  669. @ShortQty, @MesQty, @LocQty, @SechedQty, @ToSechedQty,
  670. '', '', 'N',
  671. @User, @Now, @User, @Now,
  672. @TenantId, @TenantId, @TenantId, 1000, 0,
  673. @WoList, @BangId)
  674. """,
  675. new SugarParameter("@Id", YitIdHelper.NextId()),
  676. new SugarParameter("@ItemNum", ds.ItemNum),
  677. new SugarParameter("@FVersion", ds.FVersion ?? ""),
  678. new SugarParameter("@Drawing", ds.Drawing ?? ""),
  679. new SugarParameter("@RequestDate", ds.RequestDate),
  680. new SugarParameter("@ArrivalDate", ds.ArrivalDate),
  681. new SugarParameter("@ShortQty", ds.ShortQty),
  682. new SugarParameter("@MesQty", ds.MesQty),
  683. new SugarParameter("@LocQty", ds.LocQty),
  684. new SugarParameter("@SechedQty", ds.SechedQty),
  685. new SugarParameter("@ToSechedQty", ds.ToSechedQty),
  686. new SugarParameter("@User", account),
  687. new SugarParameter("@Now", now),
  688. new SugarParameter("@TenantId", tenantId),
  689. new SugarParameter("@WoList", ds.WoList ?? ""),
  690. new SugarParameter("@BangId", ds.BangId ?? ""));
  691. }
  692. return dsRecords.Count;
  693. }
  694. private static List<SugarParameter> BuildInParams(List<string> values, long tenantId, string prefix = "Wo")
  695. {
  696. var ps = new List<SugarParameter> { new("@TenantId", tenantId) };
  697. for (var i = 0; i < values.Count; i++)
  698. ps.Add(new SugarParameter($"@{prefix}{i}", values[i]));
  699. return ps;
  700. }
  701. private static List<SugarParameter> BuildExamineParams(List<long> ids, long tenantId)
  702. {
  703. var ps = new List<SugarParameter> { new("@TenantId", tenantId) };
  704. for (var i = 0; i < ids.Count; i++)
  705. ps.Add(new SugarParameter($"@Ei{i}", ids[i]));
  706. return ps;
  707. }
  708. // ── DTO for demand schedule generation ──
  709. private sealed class WorkOrdRow
  710. {
  711. public string? WorkOrd { get; set; }
  712. public DateTime? OrdDate { get; set; }
  713. public string? Status { get; set; }
  714. }
  715. private sealed class ExamineResultRow
  716. {
  717. public string? MorderNo { get; set; }
  718. public long ExamineId { get; set; }
  719. public long? BangId { get; set; }
  720. }
  721. private sealed class BomChildExamineRow
  722. {
  723. public string? ItemNumber { get; set; }
  724. public long? ExamineId { get; set; }
  725. public decimal LackQty { get; set; }
  726. public decimal NeedCount { get; set; }
  727. public long TenantId { get; set; }
  728. public long CompanyId { get; set; }
  729. }
  730. private sealed class PickBillRow
  731. {
  732. public string? ItemNum { get; set; }
  733. public decimal QtyOrd { get; set; }
  734. public decimal QtyRec { get; set; }
  735. }
  736. private sealed class PublishedDsRow
  737. {
  738. public string? ItemNum { get; set; }
  739. public decimal PendingQty { get; set; }
  740. }
  741. private sealed class ItemMasterRow
  742. {
  743. public string? ItemNum { get; set; }
  744. public string? Rev { get; set; }
  745. public string? Drawing { get; set; }
  746. public int InsLT { get; set; }
  747. public int MFGMTTR { get; set; }
  748. }
  749. private sealed class StockRow
  750. {
  751. public string? ItemNum { get; set; }
  752. public decimal Qty { get; set; }
  753. }
  754. private sealed class DemandItemDto
  755. {
  756. public string ItemNum { get; set; } = string.Empty;
  757. public decimal LackQty { get; set; }
  758. public decimal NeedQty { get; set; }
  759. public string WorkOrds { get; set; } = string.Empty;
  760. public string BangId { get; set; } = string.Empty;
  761. }
  762. private sealed class DemandScheduleInsertRow
  763. {
  764. public string? ItemNum { get; set; }
  765. public string? FVersion { get; set; }
  766. public string? Drawing { get; set; }
  767. public DateTime RequestDate { get; set; }
  768. public DateTime ArrivalDate { get; set; }
  769. public decimal ShortQty { get; set; }
  770. public decimal MesQty { get; set; }
  771. public decimal LocQty { get; set; }
  772. public decimal SechedQty { get; set; }
  773. public decimal ToSechedQty { get; set; }
  774. public string? WoList { get; set; }
  775. public string? BangId { get; set; }
  776. }
  777. private static string BuildMessage(ProcurementPipelineResult r)
  778. {
  779. var parts = new List<string>();
  780. if (r.IndividualPrCount > 0) parts.Add($"独立 PR {r.IndividualPrCount}");
  781. if (r.PrCreatedCount > 0) parts.Add($"合并 PR {r.PrCreatedCount}");
  782. if (r.PrUpdatedCount > 0) parts.Add($"更新 PR {r.PrUpdatedCount}");
  783. if (r.PrDeletedCount > 0) parts.Add($"删除 PR {r.PrDeletedCount}");
  784. if (r.PoCreatedCount > 0) parts.Add($"转 DO/PO {r.PoCreatedCount}");
  785. if (r.DemandScheduleCount > 0) parts.Add($"交货计划 {r.DemandScheduleCount}");
  786. return parts.Count > 0 ? string.Join(",", parts) : "无待处理采购申请";
  787. }
  788. }
  789. public sealed class ProcurementPipelineResult
  790. {
  791. public int ShortageItemCount { get; set; }
  792. public int IndividualPrCount { get; set; }
  793. public int PrCreatedCount { get; set; }
  794. public int PrUpdatedCount { get; set; }
  795. public int PrDeletedCount { get; set; }
  796. public int PrMergeReducedCount { get; set; }
  797. public int HistoricalPrMergedGroups { get; set; }
  798. public int PendingPrCount { get; set; }
  799. public int PoCreatedCount { get; set; }
  800. public int PoTransferredPrCount { get; set; }
  801. public int PoOccupyRehangedCount { get; set; }
  802. public int QadTrackingCount { get; set; }
  803. public int QadTrackingSkippedCount { get; set; }
  804. public int DemandScheduleCount { get; set; }
  805. public List<string> CreatedPoNumbers { get; set; } = new();
  806. public List<string> Warnings { get; set; } = new();
  807. public bool S4MdpRefreshed { get; set; }
  808. public string? S4MdpBatchId { get; set; }
  809. public S4ReadPathConsistencyResult? ReadPathConsistency { get; set; }
  810. public string Message { get; set; } = string.Empty;
  811. }