PurchaseRequestMergeService.cs 16 KB

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