PurchaseOrderTransferService.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. namespace Admin.NET.Plugin.AiDOP.Supply;
  2. /// <summary>
  3. /// 采购申请转 PO 服务。处理待处理 PR,生成 PurOrdMaster/Detail 并改挂 srm_po_occupy。
  4. /// </summary>
  5. public class PurchaseOrderTransferService : ITransient
  6. {
  7. private readonly ISqlSugarClient _db;
  8. private readonly NumberRuleService _numberRuleService;
  9. private readonly UserManager _userManager;
  10. private readonly DataPlatform.Wms.PurOrdWmsPushService _purOrdWmsPush;
  11. public PurchaseOrderTransferService(
  12. ISqlSugarClient db,
  13. NumberRuleService numberRuleService,
  14. UserManager userManager,
  15. DataPlatform.Wms.PurOrdWmsPushService purOrdWmsPush)
  16. {
  17. _db = db;
  18. _numberRuleService = numberRuleService;
  19. _userManager = userManager;
  20. _purOrdWmsPush = purOrdWmsPush;
  21. }
  22. public async Task<PurchaseOrderTransferResult> TransferGeneratedRequireGoodsAsync(List<PurchaseRequestMain> requests, string account)
  23. {
  24. var candidates = requests
  25. .Where(x => (x.State ?? 0) == 1
  26. && !string.IsNullOrWhiteSpace(x.PrBillNo))
  27. .ToList();
  28. if (candidates.Count == 0) return new PurchaseOrderTransferResult();
  29. var createdOrders = new List<string>();
  30. var createdOrderTenants = new List<(long TenantId, string PurOrd)>();
  31. var transferredPrIds = new List<long>();
  32. var poOccupyRehangedCount = 0;
  33. var groups = candidates
  34. .GroupBy(x => new
  35. {
  36. x.TenantId,
  37. x.CompanyId,
  38. x.FactoryId,
  39. x.PrPurchaseId,
  40. x.PrPurchaseNumber,
  41. x.PrPurchaseName,
  42. x.IsRequireGoods,
  43. SupplierType = x.SupplierType ?? string.Empty
  44. })
  45. .ToList();
  46. foreach (var group in groups)
  47. {
  48. var now = DateTime.Now;
  49. var rows = group.OrderBy(x => x.PrSarriveDate).ThenBy(x => x.PrBillNo).ToList();
  50. var supplier = await LoadSupplierContextAsync(group.Key.PrPurchaseId, rows[0].IcitemId, group.Key.TenantId);
  51. var supplierCode = supplier?.SupplierNumber ?? group.Key.PrPurchaseNumber;
  52. var supplierType = string.IsNullOrWhiteSpace(group.Key.SupplierType)
  53. ? supplier?.SupplierType ?? string.Empty
  54. : group.Key.SupplierType;
  55. var isOutsource = string.Equals(supplierType, "委外", StringComparison.OrdinalIgnoreCase);
  56. var isRequireGoods = group.Key.IsRequireGoods == 1;
  57. // 三类型分流:IsRequireGoods=0→PO(采购订单);IsRequireGoods=1+委外→PW(委外加工订单);IsRequireGoods=1+非委外→DO(要货令)
  58. string ruleCode, poType, reqBy, usage;
  59. if (!isRequireGoods)
  60. {
  61. ruleCode = "PO"; poType = "po"; reqBy = "PO"; usage = supplierType;
  62. }
  63. else if (isOutsource)
  64. {
  65. ruleCode = "PW"; poType = "PW"; reqBy = "DO"; usage = "委外加工";
  66. }
  67. else
  68. {
  69. ruleCode = "DO"; poType = "po"; reqBy = "DO"; usage = supplierType;
  70. }
  71. var buyer = ResolveBuyer(supplierType);
  72. var purOrdNumbers = await _numberRuleService.NextBatchInCurrentTransactionAsync(
  73. ruleCode, group.Key.TenantId.ToString(), 1, account);
  74. if (purOrdNumbers.Count == 0 || string.IsNullOrWhiteSpace(purOrdNumbers[0]))
  75. throw Oops.Oh($"当前{ruleCode}单号生成失败,请检查{ruleCode}编号规则维护。Domain={group.Key.TenantId}");
  76. var purOrd = purOrdNumbers[0].Trim();
  77. await InsertPurchaseOrderMasterAsync(purOrd, poType, reqBy, usage, buyer, supplierCode, now, account, group.Key.TenantId);
  78. var masterId = await _db.Ado.GetIntAsync(
  79. "SELECT IFNULL(MAX(RecID),0) FROM PurOrdMaster WHERE PurOrd=@PurOrd",
  80. new SugarParameter("@PurOrd", purOrd));
  81. if (masterId <= 0) throw Oops.Oh("采购订单主表生成失败。");
  82. for (var lineIndex = 0; lineIndex < rows.Count; lineIndex++)
  83. {
  84. var pr = rows[lineIndex];
  85. var line = lineIndex + 1;
  86. var (detailRecId, itemNum) = await InsertPurchaseOrderDetailAsync(purOrd, poType, masterId, line, pr, account);
  87. if (detailRecId > 0)
  88. poOccupyRehangedCount += await RehangPoOccupyFromPrToDetailAsync(pr.Id, detailRecId, pr.TenantId, account);
  89. // 委外加工订单:BOM展开写入PurOrdDetailBatch
  90. if (isOutsource && detailRecId > 0 && !string.IsNullOrWhiteSpace(itemNum))
  91. {
  92. await InsertPurOrdDetailBatchAsync(purOrd, poType, line, detailRecId, itemNum,
  93. pr.PrAqty ?? pr.PrSqty ?? pr.PrRqty ?? 0, pr.TenantId, account, now);
  94. }
  95. pr.State = 4;
  96. pr.UpdateByName = account;
  97. pr.UpdateTime = now;
  98. transferredPrIds.Add(pr.Id);
  99. }
  100. createdOrders.Add(purOrd);
  101. createdOrderTenants.Add((group.Key.TenantId, purOrd));
  102. }
  103. // 拆库后 WMS 读 165,自建采购单须回写,否则收货侧查不到
  104. foreach (var (poTenantId, poNbr) in createdOrderTenants)
  105. await _purOrdWmsPush.TryEnqueuePurchaseOrderSafeAsync(poTenantId, poNbr);
  106. return new PurchaseOrderTransferResult
  107. {
  108. CreatedOrderCount = createdOrders.Count,
  109. TransferredPrCount = transferredPrIds.Count,
  110. PoOccupyRehangedCount = poOccupyRehangedCount,
  111. CreatedOrders = createdOrders,
  112. TransferredPrIds = transferredPrIds
  113. };
  114. }
  115. private async Task<SupplierContextRow?> LoadSupplierContextAsync(long? supplierId, long? icitemId, long tenantId)
  116. {
  117. if (supplierId is null or <= 0) return null;
  118. var rows = await _db.Ado.SqlQueryAsync<SupplierContextRow>(
  119. """
  120. SELECT
  121. IFNULL(sp.supplier_number, '') AS SupplierNumber,
  122. IFNULL(sp.supplier_type, '') AS SupplierType
  123. FROM srm_purchase sp
  124. WHERE sp.tenant_id = @TenantId
  125. AND sp.supplier_id = @SupplierId
  126. AND (@IcitemId IS NULL OR sp.icitem_id = @IcitemId)
  127. ORDER BY sp.Id
  128. LIMIT 1
  129. """,
  130. new SugarParameter("@TenantId", tenantId),
  131. new SugarParameter("@SupplierId", supplierId),
  132. new SugarParameter("@IcitemId", icitemId));
  133. return rows.FirstOrDefault();
  134. }
  135. private async Task<int> RehangPoOccupyFromPrToDetailAsync(long prId, int detailRecId, long tenantId, string account)
  136. {
  137. return await _db.Ado.ExecuteCommandAsync(
  138. """
  139. UPDATE srm_po_occupy
  140. SET polist_id = @DetailRecId,
  141. update_by_name = @User,
  142. update_time = @Now
  143. WHERE tenant_id = @TenantId
  144. AND polist_id = @PrId
  145. """,
  146. new SugarParameter("@DetailRecId", detailRecId),
  147. new SugarParameter("@User", account),
  148. new SugarParameter("@Now", DateTime.Now),
  149. new SugarParameter("@TenantId", tenantId),
  150. new SugarParameter("@PrId", prId));
  151. }
  152. private async Task InsertPurchaseOrderMasterAsync(
  153. string purOrd,
  154. string poType,
  155. string reqBy,
  156. string? usage,
  157. string buyer,
  158. string? supplierCode,
  159. DateTime now,
  160. string account,
  161. long tenantId)
  162. {
  163. await _db.Ado.ExecuteCommandAsync(
  164. """
  165. INSERT INTO PurOrdMaster
  166. (
  167. Confirming, CreditTermsInt, Disc, ExchRate, EstVal, ExchRate1, ExchRate2,
  168. FixedPrice, FixedRate, Frt, PartialOK, AmtPrepaid, PrintPO, PST, Recurr,
  169. `Release`, Revision, Scheduled, ServiceCharge, SpecialCharge, Taxable,
  170. Tax1, Tax2, Tax3, TransportDays, IsActive, IsConfirm, Potype, IsChanged,
  171. TaxIn, Amt, IsPriceChanged,
  172. Buyer, Domain, PurOrd, OrdDate, ReqBy, Status, Supp, CreateUser, CreateTime,
  173. UpdateUser, UpdateTime, `Usage`, FSTID, Typed, tenant_id
  174. )
  175. VALUES
  176. (
  177. 0, 0, 0, 1, 0, 1, 1,
  178. 0, 0, 0, 1, 0, 0, 0, 0,
  179. 0, 0, 0, 0, 0, 1,
  180. 0, 0, 0, 0, 1, 1, @PoType, 0,
  181. 1, 0, 0,
  182. @Buyer, @Domain, @PurOrd, @Now, @ReqBy, '', @Supp, @CreateUser, @Now,
  183. @UpdateUser, @Now, @Usage, @FSTID, @Typed, @TenantId
  184. )
  185. """,
  186. new SugarParameter("@PoType", poType),
  187. new SugarParameter("@ReqBy", reqBy),
  188. new SugarParameter("@Buyer", buyer),
  189. new SugarParameter("@Domain", "8010"),
  190. new SugarParameter("@PurOrd", purOrd),
  191. new SugarParameter("@Now", now),
  192. new SugarParameter("@Supp", supplierCode),
  193. new SugarParameter("@CreateUser", account),
  194. new SugarParameter("@UpdateUser", account),
  195. new SugarParameter("@Usage", usage),
  196. new SugarParameter("@FSTID", string.Equals(usage, "VMI", StringComparison.OrdinalIgnoreCase) ? "3" : string.Empty),
  197. new SugarParameter("@Typed", poType == "PW" ? "s" : string.Empty),
  198. new SugarParameter("@TenantId", tenantId <= 0 ? null : tenantId));
  199. }
  200. private async Task<(int detailRecId, string itemNum)> InsertPurchaseOrderDetailAsync(
  201. string purOrd, string poType, int masterId, int line, PurchaseRequestMain pr, string account)
  202. {
  203. var item = (await _db.Ado.SqlQueryAsync<ItemLookupRow>(
  204. """
  205. SELECT
  206. COALESCE(NULLIF(im.ItemNum,''), ic.number) AS ItemNum,
  207. COALESCE(NULLIF(im.Descr,''), ic.name) AS Descr,
  208. COALESCE(NULLIF(im.UM,''), ic.unit) AS UM,
  209. IFNULL(im.Location, '') AS Location,
  210. IFNULL(im.Rev, '') AS Rev,
  211. IFNULL(im.Drawing, '') AS Drawing
  212. FROM ic_item ic
  213. LEFT JOIN ItemMaster im ON ic.number = im.ItemNum
  214. WHERE ic.Id = @IcitemId
  215. LIMIT 1
  216. """,
  217. new SugarParameter("@IcitemId", pr.IcitemId))).FirstOrDefault();
  218. await _db.Ado.ExecuteCommandAsync(
  219. """
  220. INSERT INTO PurOrdDetail
  221. (
  222. QtyBO, RctCost, CreditTermsInt, UpdateCurrentCost, CumReceived1, CumReceived2,
  223. CumReceived3, CumReceived4, Disc, FixedPrice, InspectReq, SingleLot, SupplyPer,
  224. PurOrd, PST, PackingSlipQty, PayUMConv, PurCost, RctQty, QtyOrded, QtyReceived,
  225. QtyReturned, Active, QtyReleased, RctUMConversion, Scheduled, ScheduledChanged,
  226. SchedMRPReq, SafetyDays, SafetyHours, StdCost, Taxable, TaxIn, MaxTaxableAmt,
  227. TransportHours, UMConversion, VAT, IsActive, IsConfirm, Potype, IsChanged,
  228. TaxRate, IsRounding, ReceiptQty, BarCodeQty, IsClosed, QtyReturnedRefund, CumQtyBO,
  229. Line, ItemNum, Descr, UM, Rev, Drawing, Location, DueDate, NeedDate, LotSerial, PurOrdRecID, Status, Req,
  230. CreateUser, CreateTime, UpdateUser, UpdateTime, tenant_id
  231. )
  232. VALUES
  233. (
  234. 0, 0, 0, 0, 0, 0,
  235. 0, 0, 0, 0, 0, 0, 0,
  236. @PurOrd, 0, 0, 1, 0, 0, @QtyOrded, 0,
  237. 0, 1, 0, 1, 0, 0,
  238. 0, 0, 0, 0, 1, 1, 0,
  239. 0, 1, 0, 1, 1, @PoType, 0,
  240. 0, 0, 0, 0, 0, 0, 0,
  241. @Line, @ItemNum, @Descr, @UM, @Rev, @Drawing, @Location, @DueDate, @NeedDate, '', @PurOrdRecID, 'R', @Req,
  242. @CreateUser, @Now, @UpdateUser, @Now, @TenantId
  243. )
  244. """,
  245. new SugarParameter("@PurOrd", purOrd),
  246. new SugarParameter("@PoType", poType),
  247. new SugarParameter("@QtyOrded", pr.PrAqty ?? pr.PrSqty ?? pr.PrRqty ?? 0),
  248. new SugarParameter("@Line", line),
  249. new SugarParameter("@ItemNum", item?.ItemNum ?? pr.IcitemName ?? string.Empty),
  250. new SugarParameter("@Descr", item?.Descr ?? pr.IcitemName ?? string.Empty),
  251. new SugarParameter("@UM", item?.UM ?? pr.PrUnit ?? string.Empty),
  252. new SugarParameter("@Rev", item?.Rev ?? string.Empty),
  253. new SugarParameter("@Drawing", item?.Drawing ?? string.Empty),
  254. new SugarParameter("@Location", string.IsNullOrWhiteSpace(item?.Location) ? "1001" : item.Location),
  255. new SugarParameter("@DueDate", pr.PrSarriveDate),
  256. new SugarParameter("@NeedDate", pr.PrSarriveDate),
  257. new SugarParameter("@PurOrdRecID", masterId),
  258. new SugarParameter("@Req", pr.PrBillNo),
  259. new SugarParameter("@CreateUser", account),
  260. new SugarParameter("@UpdateUser", account),
  261. new SugarParameter("@Now", DateTime.Now),
  262. new SugarParameter("@TenantId", pr.TenantId <= 0 ? null : pr.TenantId));
  263. var detailRecId = await _db.Ado.GetIntAsync(
  264. "SELECT IFNULL(MAX(RecID),0) FROM PurOrdDetail WHERE PurOrd=@PurOrd AND Line=@Line",
  265. new SugarParameter("@PurOrd", purOrd),
  266. new SugarParameter("@Line", line));
  267. return (detailRecId, item?.ItemNum ?? pr.IcitemName ?? string.Empty);
  268. }
  269. /// <summary>委外加工订单BOM展开:查询ProductStructureMaster并写入PurOrdDetailBatch。</summary>
  270. private async Task InsertPurOrdDetailBatchAsync(
  271. string purOrd, string poType, int line, int detailRecId, string parentItem,
  272. decimal prQty, long tenantId, string account, DateTime now)
  273. {
  274. var components = await _db.Ado.SqlQueryAsync<BomComponentRow>(
  275. """
  276. SELECT
  277. psm.ComponentItem AS SuppItem,
  278. CAST(psm.Qty AS DECIMAL(18,5)) AS QtyPerUnit,
  279. IFNULL(psm.UM, im.UM) AS UM,
  280. IFNULL(im.ItemNum, psm.ComponentItem) AS ItemNum,
  281. IFNULL(im.Location, '') AS Location
  282. FROM ProductStructureMaster psm
  283. LEFT JOIN ItemMaster im ON psm.ComponentItem = im.ItemNum
  284. WHERE psm.ParentItem = @ParentItem
  285. ORDER BY psm.ComponentItem
  286. """,
  287. new SugarParameter("@ParentItem", parentItem));
  288. var batchNo = 1;
  289. foreach (var comp in components)
  290. {
  291. if (string.IsNullOrWhiteSpace(comp.SuppItem)) continue;
  292. var batchQty = Math.Round(prQty * comp.QtyPerUnit, 5);
  293. await _db.Ado.ExecuteCommandAsync(
  294. """
  295. INSERT INTO PurOrdDetailBatch
  296. (
  297. Domain, PurOrd, Potype, Line, Batch, ItemNum, SuppItem, UM, Location,
  298. QtyOrded, QtyBO, QtyReleased, QtyReceived, QtyReturned, LotSerial, PurOrdDetailRecID,
  299. CreateUser, CreateTime, UpdateUser, UpdateTime, tenant_id
  300. )
  301. VALUES
  302. (
  303. @Domain, @PurOrd, @Potype, @Line, @Batch, @ItemNum, @SuppItem, @UM, @Location,
  304. @QtyOrded, @QtyBO, @QtyReleased, 0, 0, '', @PurOrdDetailRecID,
  305. @CreateUser, @CreateTime, @UpdateUser, @UpdateTime, @TenantId
  306. )
  307. """,
  308. new SugarParameter("@Domain", "8010"),
  309. new SugarParameter("@PurOrd", purOrd),
  310. new SugarParameter("@Potype", poType),
  311. new SugarParameter("@Line", line),
  312. new SugarParameter("@Batch", batchNo),
  313. new SugarParameter("@ItemNum", comp.ItemNum ?? comp.SuppItem),
  314. new SugarParameter("@SuppItem", comp.SuppItem),
  315. new SugarParameter("@UM", comp.UM ?? string.Empty),
  316. new SugarParameter("@Location", string.IsNullOrWhiteSpace(comp.Location) ? "1001" : comp.Location),
  317. new SugarParameter("@QtyOrded", batchQty),
  318. new SugarParameter("@QtyBO", comp.QtyPerUnit),
  319. new SugarParameter("@QtyReleased", batchQty),
  320. new SugarParameter("@PurOrdDetailRecID", detailRecId),
  321. new SugarParameter("@CreateUser", account),
  322. new SugarParameter("@CreateTime", now),
  323. new SugarParameter("@UpdateUser", account),
  324. new SugarParameter("@UpdateTime", now),
  325. new SugarParameter("@TenantId", tenantId <= 0 ? null : tenantId));
  326. batchNo++;
  327. }
  328. }
  329. private static string ResolveBuyer(string? supplierType)
  330. {
  331. return supplierType switch
  332. {
  333. "研发" => "130",
  334. "ECR" => "170",
  335. _ => "110"
  336. };
  337. }
  338. private sealed class SupplierContextRow
  339. {
  340. public string? SupplierNumber { get; set; }
  341. public string? SupplierType { get; set; }
  342. }
  343. private sealed class ItemLookupRow
  344. {
  345. public string? ItemNum { get; set; }
  346. public string? Descr { get; set; }
  347. public string? UM { get; set; }
  348. public string? Location { get; set; }
  349. public string? Rev { get; set; }
  350. public string? Drawing { get; set; }
  351. }
  352. private sealed class BomComponentRow
  353. {
  354. public string? SuppItem { get; set; }
  355. public decimal QtyPerUnit { get; set; }
  356. public string? UM { get; set; }
  357. public string? ItemNum { get; set; }
  358. public string? Location { get; set; }
  359. }
  360. }
  361. public sealed class PurchaseOrderTransferResult
  362. {
  363. public int CreatedOrderCount { get; set; }
  364. public int TransferredPrCount { get; set; }
  365. public int PoOccupyRehangedCount { get; set; }
  366. public List<string> CreatedOrders { get; set; } = new();
  367. public List<long> TransferredPrIds { get; set; } = new();
  368. }