ProcurementPipelineService.cs 40 KB

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