PurchaseRequestMergeService.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  2. using Yitter.IdGenerator;
  3. namespace Admin.NET.Plugin.AiDOP.Supply;
  4. /// <summary>
  5. /// 采购申请合并服务。支持本次生成 PR 合并与租户待处理历史 PR 合并(对齐旧 PrAutoMerge2)。
  6. /// </summary>
  7. public class PurchaseRequestMergeService : ITransient
  8. {
  9. private readonly ISqlSugarClient _db;
  10. private readonly NumberRuleService _numberRuleService;
  11. private readonly IInventoryBalanceReader _balanceReader;
  12. public PurchaseRequestMergeService(
  13. ISqlSugarClient db,
  14. NumberRuleService numberRuleService,
  15. IInventoryBalanceReader balanceReader)
  16. {
  17. _db = db;
  18. _numberRuleService = numberRuleService;
  19. _balanceReader = balanceReader;
  20. }
  21. public PurchaseRequestMergeResult MergeGeneratedRequests(List<PurchaseRequestMain> requests) =>
  22. MergeInMemory(requests);
  23. /// <summary>合并租户内待处理历史 PR(state=1,5 周窗口)。</summary>
  24. public async Task<PurchaseRequestHistoricalMergeResult> MergeTenantPendingAsync(long tenantId, string account)
  25. {
  26. var tomorrow = DateTime.Today.AddDays(1);
  27. var windowEnd = GetWeekStart(DateTime.Today)?.AddDays(35) ?? DateTime.Today.AddDays(35);
  28. var pending = await _db.Ado.SqlQueryAsync<PurchaseRequestMain>(
  29. """
  30. SELECT
  31. Id, pr_billno AS PrBillNo, pr_purchaseid AS PrPurchaseId,
  32. pr_purchasenumber AS PrPurchaseNumber, pr_purchasename AS PrPurchaseName,
  33. pr_purchaser AS PrPurchaser, pr_purchaser_num AS PrPurchaserNum,
  34. pr_rqty AS PrRqty, pr_aqty AS PrAqty, pr_sqty AS PrSqty,
  35. icitem_id AS IcitemId, icitem_name AS IcitemName,
  36. pr_ssend_date AS PrSsendDate, pr_sarrive_date AS PrSarriveDate,
  37. pr_unit AS PrUnit, state AS State, pr_type AS PrType,
  38. currencytype AS CurrencyType, tenant_id AS TenantId,
  39. factory_id AS FactoryId, org_id AS OrgId, company_id AS CompanyId,
  40. IsRequireGoods, supplier_type AS SupplierType, IsDeleted
  41. FROM srm_pr_main
  42. WHERE tenant_id = @TenantId
  43. AND IFNULL(IsDeleted, 0) = 0
  44. AND IFNULL(state, 0) = 1
  45. AND IFNULL(analogcalcversion, '') = ''
  46. AND IFNULL(refer_pr_billno, '') = ''
  47. AND pr_ssend_date IS NOT NULL
  48. AND pr_ssend_date <= @WindowEnd
  49. ORDER BY pr_ssend_date, Id
  50. """,
  51. new SugarParameter("@TenantId", tenantId),
  52. new SugarParameter("@WindowEnd", windowEnd));
  53. if (pending.Count <= 1)
  54. {
  55. return new PurchaseRequestHistoricalMergeResult
  56. {
  57. PendingCount = pending.Count,
  58. MergedGroupCount = 0
  59. };
  60. }
  61. foreach (var pr in pending)
  62. {
  63. if (pr.PrSsendDate.HasValue && pr.PrSsendDate.Value.Date < tomorrow)
  64. {
  65. var shift = (tomorrow - pr.PrSsendDate.Value.Date).Days;
  66. pr.PrSsendDate = tomorrow;
  67. if (pr.PrSarriveDate.HasValue)
  68. pr.PrSarriveDate = pr.PrSarriveDate.Value.AddDays(shift);
  69. }
  70. }
  71. var groups = pending
  72. .GroupBy(x => new
  73. {
  74. x.TenantId,
  75. x.CompanyId,
  76. x.FactoryId,
  77. x.IcitemId,
  78. x.PrPurchaseId,
  79. x.IsRequireGoods,
  80. SupplierType = x.SupplierType ?? string.Empty,
  81. WeekStart = GetWeekStart(x.PrSsendDate)
  82. })
  83. .Where(g => g.Count() > 1)
  84. .ToList();
  85. if (groups.Count == 0)
  86. {
  87. return new PurchaseRequestHistoricalMergeResult
  88. {
  89. PendingCount = pending.Count,
  90. MergedGroupCount = 0
  91. };
  92. }
  93. var now = DateTime.Now;
  94. var createdCount = 0;
  95. var closedCount = 0;
  96. var reducedCount = 0;
  97. foreach (var group in groups)
  98. {
  99. var rows = group.OrderBy(x => x.PrSsendDate).ThenBy(x => x.PrSarriveDate).ThenBy(x => x.Id).ToList();
  100. var merged = BuildMergedRow(rows, account, now);
  101. var numbers = await _numberRuleService.NextBatchInCurrentTransactionAsync(
  102. "PR", merged.TenantId.ToString(), 1, account);
  103. merged.PrBillNo = numbers.FirstOrDefault()?.Trim()
  104. ?? throw Oops.Oh("历史 PR 合并编号生成失败");
  105. merged.Id = YitIdHelper.NextId();
  106. await InsertMergedPurchaseRequestAsync(merged);
  107. foreach (var old in rows)
  108. {
  109. await _db.Ado.ExecuteCommandAsync(
  110. """
  111. UPDATE srm_pr_main
  112. SET state = 0,
  113. refer_pr_billno = @NewBillNo,
  114. update_by_name = @User,
  115. update_time = @Now
  116. WHERE Id = @Id AND tenant_id = @TenantId
  117. """,
  118. new SugarParameter("@NewBillNo", merged.PrBillNo),
  119. new SugarParameter("@User", account),
  120. new SugarParameter("@Now", now),
  121. new SugarParameter("@Id", old.Id),
  122. new SugarParameter("@TenantId", tenantId));
  123. await _db.Ado.ExecuteCommandAsync(
  124. """
  125. UPDATE srm_po_occupy
  126. SET polist_id = @NewPrId,
  127. update_by_name = @User,
  128. update_time = @Now
  129. WHERE tenant_id = @TenantId AND polist_id = @OldPrId
  130. """,
  131. new SugarParameter("@NewPrId", merged.Id),
  132. new SugarParameter("@User", account),
  133. new SugarParameter("@Now", now),
  134. new SugarParameter("@TenantId", tenantId),
  135. new SugarParameter("@OldPrId", old.Id));
  136. }
  137. createdCount++;
  138. closedCount += rows.Count;
  139. reducedCount += rows.Count - 1;
  140. }
  141. return new PurchaseRequestHistoricalMergeResult
  142. {
  143. PendingCount = pending.Count,
  144. MergedGroupCount = createdCount,
  145. ClosedPrCount = closedCount,
  146. CreatedPrCount = createdCount,
  147. ReducedCount = reducedCount
  148. };
  149. }
  150. private static PurchaseRequestMergeResult MergeInMemory(List<PurchaseRequestMain> requests)
  151. {
  152. if (requests.Count <= 1)
  153. {
  154. return new PurchaseRequestMergeResult
  155. {
  156. Requests = requests,
  157. OriginalCount = requests.Count,
  158. MergedCount = requests.Count
  159. };
  160. }
  161. var merged = requests
  162. .GroupBy(x => new
  163. {
  164. x.TenantId,
  165. x.CompanyId,
  166. x.FactoryId,
  167. x.IcitemId,
  168. x.PrPurchaseId,
  169. x.IsRequireGoods,
  170. SupplierType = x.SupplierType ?? string.Empty,
  171. WeekStart = GetWeekStart(x.PrSsendDate)
  172. })
  173. .Select(group =>
  174. {
  175. var rows = group.OrderBy(x => x.PrSsendDate).ThenBy(x => x.PrSarriveDate).ThenBy(x => x.Id).ToList();
  176. return BuildMergedRow(rows, rows.First().CreateByName ?? "system", DateTime.Now);
  177. })
  178. .ToList();
  179. return new PurchaseRequestMergeResult
  180. {
  181. Requests = merged,
  182. OriginalCount = requests.Count,
  183. MergedCount = merged.Count
  184. };
  185. }
  186. private static PurchaseRequestMain BuildMergedRow(List<PurchaseRequestMain> rows, string account, DateTime now)
  187. {
  188. var first = rows.First();
  189. return new PurchaseRequestMain
  190. {
  191. Id = first.Id,
  192. PrMono = first.PrMono,
  193. EntityId = first.EntityId,
  194. PrPurchaseId = first.PrPurchaseId,
  195. PrPurchaseNumber = first.PrPurchaseNumber,
  196. PrPurchaseName = first.PrPurchaseName,
  197. PrPurchaser = first.PrPurchaser,
  198. PrPurchaserNum = first.PrPurchaserNum,
  199. PrRqty = rows.Sum(x => x.PrRqty ?? 0),
  200. PrAqty = rows.Sum(x => x.PrAqty ?? 0),
  201. PrSqty = rows.Sum(x => x.PrSqty ?? 0),
  202. IcitemId = first.IcitemId,
  203. IcitemName = first.IcitemName,
  204. PrSsendDate = rows.Min(x => x.PrSsendDate),
  205. PrSarriveDate = rows.Min(x => x.PrSarriveDate),
  206. PrUnit = first.PrUnit,
  207. State = 1,
  208. PrType = first.PrType ?? 3,
  209. CurrencyType = first.CurrencyType,
  210. CreateByName = account,
  211. CreateTime = now,
  212. UpdateByName = account,
  213. UpdateTime = now,
  214. TenantId = first.TenantId,
  215. FactoryId = first.FactoryId,
  216. OrgId = first.OrgId,
  217. CompanyId = first.CompanyId,
  218. IsDeleted = false,
  219. IsRequireGoods = first.IsRequireGoods,
  220. SupplierType = first.SupplierType
  221. };
  222. }
  223. private async Task InsertMergedPurchaseRequestAsync(PurchaseRequestMain pr)
  224. {
  225. await _db.Ado.ExecuteCommandAsync(
  226. """
  227. INSERT INTO srm_pr_main
  228. (Id,pr_billno,pr_mono,entity_id,pr_purchaseid,pr_purchasenumber,pr_purchasename,pr_purchaser,pr_purchaser_num,
  229. pr_rqty,pr_aqty,pr_sqty,icitem_id,icitem_name,pr_ssend_date,pr_sarrive_date,pr_unit,state,pr_type,currencytype,
  230. create_by_name,create_time,update_by_name,update_time,tenant_id,factory_id,org_id,IsDeleted,company_id,IsRequireGoods,supplier_type)
  231. VALUES
  232. (@Id,@PrBillNo,@PrMono,@EntityId,@PrPurchaseId,@PrPurchaseNumber,@PrPurchaseName,@PrPurchaser,@PrPurchaserNum,
  233. @PrRqty,@PrAqty,@PrSqty,@IcitemId,@IcitemName,@PrSsendDate,@PrSarriveDate,@PrUnit,@State,@PrType,@CurrencyType,
  234. @CreateByName,@CreateTime,@UpdateByName,@UpdateTime,@TenantId,@FactoryId,@OrgId,0,@CompanyId,@IsRequireGoods,@SupplierType)
  235. """,
  236. new SugarParameter("@Id", pr.Id),
  237. new SugarParameter("@PrBillNo", pr.PrBillNo),
  238. new SugarParameter("@PrMono", pr.PrMono ?? (object)DBNull.Value),
  239. new SugarParameter("@EntityId", pr.EntityId ?? (object)DBNull.Value),
  240. new SugarParameter("@PrPurchaseId", pr.PrPurchaseId),
  241. new SugarParameter("@PrPurchaseNumber", pr.PrPurchaseNumber),
  242. new SugarParameter("@PrPurchaseName", pr.PrPurchaseName),
  243. new SugarParameter("@PrPurchaser", pr.PrPurchaser),
  244. new SugarParameter("@PrPurchaserNum", pr.PrPurchaserNum),
  245. new SugarParameter("@PrRqty", pr.PrRqty),
  246. new SugarParameter("@PrAqty", pr.PrAqty),
  247. new SugarParameter("@PrSqty", pr.PrSqty),
  248. new SugarParameter("@IcitemId", pr.IcitemId),
  249. new SugarParameter("@IcitemName", pr.IcitemName),
  250. new SugarParameter("@PrSsendDate", pr.PrSsendDate),
  251. new SugarParameter("@PrSarriveDate", pr.PrSarriveDate),
  252. new SugarParameter("@PrUnit", pr.PrUnit),
  253. new SugarParameter("@State", pr.State ?? 1),
  254. new SugarParameter("@PrType", pr.PrType ?? 3),
  255. new SugarParameter("@CurrencyType", pr.CurrencyType),
  256. new SugarParameter("@CreateByName", pr.CreateByName),
  257. new SugarParameter("@CreateTime", pr.CreateTime),
  258. new SugarParameter("@UpdateByName", pr.UpdateByName),
  259. new SugarParameter("@UpdateTime", pr.UpdateTime),
  260. new SugarParameter("@TenantId", pr.TenantId),
  261. new SugarParameter("@FactoryId", pr.FactoryId),
  262. new SugarParameter("@OrgId", pr.OrgId),
  263. new SugarParameter("@CompanyId", pr.CompanyId ?? 1000),
  264. new SugarParameter("@IsRequireGoods", pr.IsRequireGoods),
  265. new SugarParameter("@SupplierType", pr.SupplierType));
  266. }
  267. private static DateTime? GetWeekStart(DateTime? value)
  268. {
  269. if (!value.HasValue) return null;
  270. var date = value.Value.Date;
  271. var diff = ((int)date.DayOfWeek + 6) % 7;
  272. return date.AddDays(-diff);
  273. }
  274. /// <summary>
  275. /// 保留各工单独立 PR + 创建合并 PR(数量 = 总需求 - 库存 - 在途)。
  276. /// 仅当同组有 2 条以上 PR 时才创建合并 PR。
  277. /// </summary>
  278. public async Task<SplitMergeResult> SplitMergeWithRecalcAsync(
  279. List<PurchaseRequestMain> requests, string account)
  280. {
  281. var result = new SplitMergeResult();
  282. if (requests.Count == 0) return result;
  283. var groups = requests
  284. .GroupBy(x => new
  285. {
  286. x.TenantId,
  287. x.CompanyId,
  288. x.FactoryId,
  289. x.IcitemId,
  290. x.PrPurchaseId,
  291. x.IsRequireGoods,
  292. SupplierType = x.SupplierType ?? string.Empty,
  293. WeekStart = GetWeekStart(x.PrSsendDate)
  294. })
  295. .ToList();
  296. var now = DateTime.Now;
  297. var mergedList = new List<PurchaseRequestMain>();
  298. foreach (var group in groups)
  299. {
  300. var rows = group.OrderBy(x => x.PrSsendDate).ThenBy(x => x.PrSarriveDate).ThenBy(x => x.Id).ToList();
  301. // ① 保留各工单独立 PR
  302. result.IndividualRequests.AddRange(rows);
  303. // ② 仅当同组有 2+ 条时才创建合并 PR
  304. if (rows.Count >= 2)
  305. {
  306. var merged = BuildMergedRow(rows, account, now);
  307. // 重新计算合并 PR 数量:总需求 - 库存 - 在途
  308. var totalNeed = rows.Sum(x => x.PrRqty ?? 0);
  309. var stock = await QueryStockQtyAsync(rows[0].IcitemId, rows[0].TenantId);
  310. var transit = await QueryOpenPurchaseQtyAsync(rows[0].IcitemId, rows[0].TenantId);
  311. var actualShortage = Math.Max(0, totalNeed - stock - transit);
  312. merged.PrRqty = actualShortage;
  313. merged.PrAqty = actualShortage;
  314. merged.PrSqty = actualShortage;
  315. mergedList.Add(merged);
  316. }
  317. }
  318. result.MergedRequests = mergedList;
  319. return result;
  320. }
  321. private async Task<decimal> QueryStockQtyAsync(long icitemId, long tenantId)
  322. {
  323. if (tenantId <= 0)
  324. throw Oops.Oh("数据异常:无法确定租户,无法查询库存");
  325. var itemNumber = await _db.Ado.GetStringAsync(
  326. "SELECT number FROM ic_item WHERE Id = @Id LIMIT 1",
  327. new SugarParameter("@Id", icitemId));
  328. if (string.IsNullOrWhiteSpace(itemNumber)) return 0;
  329. var stockMap = await _balanceReader.LoadAvailableQtyAsync(tenantId, new[] { itemNumber });
  330. return stockMap.TryGetValue(itemNumber, out var qty) ? qty : 0m;
  331. }
  332. private async Task<decimal> QueryOpenPurchaseQtyAsync(long icitemId, long tenantId)
  333. {
  334. var itemNumber = await _db.Ado.GetStringAsync(
  335. "SELECT number FROM ic_item WHERE Id = @Id LIMIT 1",
  336. new SugarParameter("@Id", icitemId));
  337. if (string.IsNullOrWhiteSpace(itemNumber)) return 0;
  338. return await _db.Ado.GetDecimalAsync(
  339. """
  340. SELECT COALESCE(SUM(
  341. GREATEST(IFNULL(d.QtyOrded, 0) - IFNULL(d.QtyReceived, 0), 0)
  342. ), 0)
  343. FROM PurOrdDetail d
  344. INNER JOIN PurOrdMaster m ON m.RecID = d.PurOrdRecID
  345. WHERE d.ItemNum = @ItemNum AND m.tenant_id = @TenantId
  346. AND IFNULL(d.IsActive, 1) = 1 AND IFNULL(m.IsActive, 1) = 1
  347. """,
  348. new SugarParameter("@ItemNum", itemNumber),
  349. new SugarParameter("@TenantId", tenantId));
  350. }
  351. }
  352. public sealed class SplitMergeResult
  353. {
  354. public List<PurchaseRequestMain> IndividualRequests { get; set; } = new();
  355. public List<PurchaseRequestMain> MergedRequests { get; set; } = new();
  356. }
  357. public sealed class PurchaseRequestMergeResult
  358. {
  359. public List<PurchaseRequestMain> Requests { get; set; } = new();
  360. public int OriginalCount { get; set; }
  361. public int MergedCount { get; set; }
  362. public int ReducedCount => Math.Max(OriginalCount - MergedCount, 0);
  363. }
  364. public sealed class PurchaseRequestHistoricalMergeResult
  365. {
  366. public int PendingCount { get; set; }
  367. public int MergedGroupCount { get; set; }
  368. public int ClosedPrCount { get; set; }
  369. public int CreatedPrCount { get; set; }
  370. public int ReducedCount { get; set; }
  371. }