WorkOrderPurchaseConfirmService.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. using Admin.NET.Plugin.AiDOP.Infrastructure;
  2. using Microsoft.Extensions.Logging;
  3. namespace Admin.NET.Plugin.AiDOP.WorkOrder;
  4. /// <summary>
  5. /// 批量采购确认服务 — 列出初始(p)状态工单中未匹配到供应商的物料明细,
  6. /// 确认后把(物料 → 采购员 → 供应商)关系写入货源清单基础数据 srm_purchase。
  7. /// 路由前缀:/api/WorkOrder/purchase-confirm/...
  8. ///
  9. /// 【未匹配口径】物料号在 srm_purchase 中不存在「未删除且有供应商编码」的记录。
  10. /// 【写入约定】供应商编码/ID 从 SuppMaster 按名称回填(查不到编码为空);
  11. /// 物料 ID/工厂/公司从 ItemMaster 回填;同一(物料号+供应商)已存在则更新,否则新增。
  12. ///
  13. /// 【租户安全边界】需认证访问;租户一律经
  14. /// <c>AidopTenantScope.ResolveOrThrow</c> 从认证后 JWT 解析。
  15. /// </summary>
  16. [ApiDescriptionSettings(Order = 259, Description = "批量采购确认")]
  17. [Route("api/WorkOrder")]
  18. [NonUnify]
  19. public class WorkOrderPurchaseConfirmService : IDynamicApiController, ITransient
  20. {
  21. private readonly ISqlSugarClient _db;
  22. private readonly UserManager _userManager;
  23. private readonly ILogger<WorkOrderPurchaseConfirmService> _logger;
  24. public WorkOrderPurchaseConfirmService(
  25. ISqlSugarClient db,
  26. UserManager userManager,
  27. ILogger<WorkOrderPurchaseConfirmService> logger)
  28. {
  29. _db = db;
  30. _userManager = userManager;
  31. _logger = logger;
  32. }
  33. private long ResolveTenantId() => AidopTenantScope.ResolveOrThrow(_userManager);
  34. // ══════════════════════════════════════════════════════════════
  35. // GET /api/WorkOrder/purchase-confirm/unmatched-list
  36. // ══════════════════════════════════════════════════════════════
  37. /// <summary>
  38. /// 获取未匹配到供应商的工单物料明细(仅初始 p 状态工单;
  39. /// 物料号在 srm_purchase 中无「未删除且有供应商编码」的有效货源记录)。
  40. /// </summary>
  41. [DisplayName("批量采购确认未匹配列表")]
  42. [HttpGet("purchase-confirm/unmatched-list")]
  43. public async Task<object> GetUnmatchedList()
  44. {
  45. var tid = ResolveTenantId();
  46. var list = await _db.Ado.SqlQueryAsync<UnmatchedPurchaseRow>(
  47. """
  48. SELECT
  49. wd.RecID AS DetailRecId,
  50. wm.WorkOrd,
  51. wm.SalesJob AS SalesOrderNo,
  52. co.bill_from AS ContractNo,
  53. wm.ItemNum AS ParentItemNum,
  54. wm.ItemName AS ParentDescr,
  55. wm.Domain AS Factory,
  56. wd.ItemNum,
  57. im.Descr,
  58. im.Descr1,
  59. wd.QtyRequired,
  60. wd.UM AS Um,
  61. CAST(wd.Op AS CHAR(20)) AS ProjectText,
  62. IFNULL(TRIM(im.Buyer), '') AS Purchaser
  63. FROM WorkOrdDetail wd
  64. JOIN WorkOrdMaster wm
  65. ON wm.WorkOrd = wd.WorkOrd AND wm.tenant_id = wd.tenant_id
  66. LEFT JOIN ItemMaster im
  67. ON im.ItemNum = wd.ItemNum AND (im.tenant_id = wd.tenant_id OR im.tenant_id IS NULL)
  68. LEFT JOIN crm_seorder co
  69. ON co.Id = wm.BusinessID AND co.tenant_id = wm.tenant_id
  70. LEFT JOIN srm_purchase sp
  71. ON sp.number = wd.ItemNum AND sp.tenant_id = wd.tenant_id
  72. AND IFNULL(sp.IsDeleted, 0) = 0
  73. AND IFNULL(TRIM(sp.supplier_number), '') <> ''
  74. WHERE wd.tenant_id = @TenantId
  75. AND IFNULL(wd.IsActive, 0) = 1
  76. AND IFNULL(TRIM(wd.ItemNum), '') <> ''
  77. AND LOWER(TRIM(IFNULL(wm.Status, ''))) = 'p'
  78. AND sp.Id IS NULL
  79. ORDER BY wm.WorkOrd, wd.LineNum
  80. """,
  81. new SugarParameter("@TenantId", tid));
  82. return new { total = list.Count, list };
  83. }
  84. // ══════════════════════════════════════════════════════════════
  85. // POST /api/WorkOrder/purchase-confirm/confirm
  86. // ══════════════════════════════════════════════════════════════
  87. /// <summary>
  88. /// 批量确认:把(物料 → 采购员 → 供应商)关系写入货源清单 srm_purchase。
  89. /// 供应商为空的行跳过;按(物料号+供应商)去重合并。
  90. /// </summary>
  91. [DisplayName("批量采购确认写入货源清单")]
  92. [ApiDescriptionSettings(Name = "PurchaseConfirm"), HttpPost("purchase-confirm/confirm")]
  93. public async Task<PurchaseConfirmResult> Confirm([FromBody] PurchaseConfirmInput input)
  94. {
  95. var tid = ResolveTenantId();
  96. if (input.Items == null || input.Items.Count == 0)
  97. throw Oops.Oh("确认列表为空");
  98. var account = _userManager.Account ?? "system";
  99. var now = DateTime.Now;
  100. // 1) 供应商为空的行跳过;其余按(物料号+供应商)去重合并
  101. var groups = input.Items
  102. .Where(x => !string.IsNullOrWhiteSpace(x.ItemNum) && !string.IsNullOrWhiteSpace(x.Supplier))
  103. .GroupBy(x => (ItemNum: x.ItemNum!.Trim(), Supplier: x.Supplier!.Trim()))
  104. .ToList();
  105. var skipped = input.Items.Count - input.Items.Count(x =>
  106. !string.IsNullOrWhiteSpace(x.ItemNum) && !string.IsNullOrWhiteSpace(x.Supplier));
  107. var inserted = 0;
  108. var updated = 0;
  109. await _db.Ado.BeginTranAsync();
  110. try
  111. {
  112. foreach (var g in groups)
  113. {
  114. var itemNum = g.Key.ItemNum;
  115. var supplierName = g.Key.Supplier;
  116. var first = g.First();
  117. var purchaser = first.Purchaser?.Trim();
  118. var unitPrice = g.Select(x => x.UnitPrice).FirstOrDefault(x => x.HasValue);
  119. // 物料主数据回填(ID/工厂/公司/名称/单位)
  120. var item = await _db.Ado.SqlQuerySingleAsync<ItemRefRow>(
  121. """
  122. SELECT RecID AS Id, company_ref_id AS CompanyRefId, factory_ref_id AS FactoryRefId,
  123. Descr, UM
  124. FROM ItemMaster
  125. WHERE tenant_id = @TenantId AND TRIM(ItemNum) = @ItemNum
  126. LIMIT 1
  127. """,
  128. new SugarParameter("@TenantId", tid),
  129. new SugarParameter("@ItemNum", itemNum));
  130. var icitemId = item?.Id ?? 0;
  131. var companyRefId = item?.CompanyRefId ?? 0;
  132. var factoryRefId = item?.FactoryRefId ?? 0;
  133. var descr = first.Descr?.Trim() ?? item?.Descr ?? string.Empty;
  134. var um = first.Um?.Trim() ?? item?.UM ?? string.Empty;
  135. // 供应商主数据回填(ID/编码),按简称或编码匹配
  136. var supp = await _db.Ado.SqlQuerySingleAsync<SuppRefRow>(
  137. """
  138. SELECT RecID AS Id, Supp
  139. FROM SuppMaster
  140. WHERE tenant_id = @TenantId
  141. AND (TRIM(IFNULL(SortName, '')) = @SupplierName OR TRIM(IFNULL(Supp, '')) = @SupplierName)
  142. ORDER BY IFNULL(IsActive, 0) DESC
  143. LIMIT 1
  144. """,
  145. new SugarParameter("@TenantId", tid),
  146. new SugarParameter("@SupplierName", supplierName));
  147. var supplierId = supp?.Id ?? 0;
  148. var supplierNumber = supp?.Supp ?? string.Empty;
  149. // 已有货源记录?(同物料+同供应商)→ 更新,否则新增
  150. var existing = await _db.Ado.SqlQuerySingleAsync<ExistingSourceRow>(
  151. """
  152. SELECT Id, supplier_number AS SupplierNumber
  153. FROM srm_purchase
  154. WHERE tenant_id = @TenantId
  155. AND TRIM(IFNULL(number, '')) = @ItemNum
  156. AND TRIM(IFNULL(supplier_name, '')) = @SupplierName
  157. AND IFNULL(IsDeleted, 0) = 0
  158. LIMIT 1
  159. """,
  160. new SugarParameter("@TenantId", tid),
  161. new SugarParameter("@ItemNum", itemNum),
  162. new SugarParameter("@SupplierName", supplierName));
  163. if (existing != null)
  164. {
  165. await _db.Ado.ExecuteCommandAsync(
  166. """
  167. UPDATE srm_purchase
  168. SET order_rector_name = @Purchaser,
  169. purcher = @Purchaser,
  170. order_price = @UnitPrice,
  171. supplier_number = CASE
  172. WHEN IFNULL(TRIM(supplier_number), '') = '' AND TRIM(@SupplierNumber) <> '' THEN @SupplierNumber
  173. ELSE supplier_number
  174. END,
  175. supplier_id = CASE
  176. WHEN IFNULL(supplier_id, 0) = 0 AND @SupplierId > 0 THEN @SupplierId
  177. ELSE supplier_id
  178. END,
  179. update_user = @User,
  180. update_time = @Now
  181. WHERE Id = @Id AND tenant_id = @TenantId
  182. """,
  183. new SugarParameter("@Purchaser", string.IsNullOrEmpty(purchaser) ? (object)DBNull.Value : purchaser),
  184. new SugarParameter("@UnitPrice", unitPrice ?? (object)DBNull.Value),
  185. new SugarParameter("@SupplierNumber", supplierNumber),
  186. new SugarParameter("@SupplierId", supplierId),
  187. new SugarParameter("@User", account),
  188. new SugarParameter("@Now", now),
  189. new SugarParameter("@Id", existing.Id),
  190. new SugarParameter("@TenantId", tid));
  191. updated++;
  192. }
  193. else
  194. {
  195. await _db.Ado.ExecuteCommandAsync(
  196. """
  197. INSERT INTO srm_purchase (
  198. company_ref_id, factory_ref_id, icitem_id, icitem_name,
  199. is_active, supplier_id, supplier_name, supplier_number,
  200. order_price, quota_rate, order_rector_name, purcher,
  201. purchase_unit, IsRequireGoods, quota_priority,
  202. create_user, create_time, update_user, update_time,
  203. number, IsDeleted, tenant_id
  204. ) VALUES (
  205. @CompanyRefId, @FactoryRefId, @IcitemId, @IcitemName,
  206. '是', @SupplierId, @SupplierName, @SupplierNumber,
  207. @UnitPrice, 100, @Purchaser, @Purchaser,
  208. @PurchaseUnit, 0, 0,
  209. @User, @Now, @User, @Now,
  210. @ItemNum, 0, @TenantId
  211. )
  212. """,
  213. new SugarParameter("@CompanyRefId", companyRefId),
  214. new SugarParameter("@FactoryRefId", factoryRefId),
  215. new SugarParameter("@IcitemId", icitemId),
  216. new SugarParameter("@IcitemName", string.IsNullOrEmpty(descr) ? (object)DBNull.Value : descr),
  217. new SugarParameter("@SupplierId", supplierId),
  218. new SugarParameter("@SupplierName", supplierName),
  219. new SugarParameter("@SupplierNumber", string.IsNullOrEmpty(supplierNumber) ? string.Empty : supplierNumber),
  220. new SugarParameter("@UnitPrice", unitPrice ?? (object)DBNull.Value),
  221. new SugarParameter("@Purchaser", string.IsNullOrEmpty(purchaser) ? (object)DBNull.Value : purchaser),
  222. new SugarParameter("@PurchaseUnit", string.IsNullOrEmpty(um) ? (object)DBNull.Value : um),
  223. new SugarParameter("@User", account),
  224. new SugarParameter("@Now", now),
  225. new SugarParameter("@ItemNum", itemNum),
  226. new SugarParameter("@TenantId", tid));
  227. inserted++;
  228. }
  229. }
  230. await _db.Ado.CommitTranAsync();
  231. _logger.LogInformation(
  232. "[PurchaseConfirm] tenantId={TenantId} total={Total} inserted={Inserted} updated={Updated} skipped={Skipped}",
  233. tid, input.Items.Count, inserted, updated, skipped);
  234. return new PurchaseConfirmResult
  235. {
  236. Inserted = inserted,
  237. Updated = updated,
  238. Skipped = skipped,
  239. Total = input.Items.Count
  240. };
  241. }
  242. catch
  243. {
  244. await _db.Ado.RollbackTranAsync();
  245. throw;
  246. }
  247. }
  248. private sealed class ItemRefRow
  249. {
  250. public long Id { get; set; }
  251. public long CompanyRefId { get; set; }
  252. public long FactoryRefId { get; set; }
  253. public string? Descr { get; set; }
  254. public string? UM { get; set; }
  255. }
  256. private sealed class SuppRefRow
  257. {
  258. public long Id { get; set; }
  259. public string? Supp { get; set; }
  260. }
  261. private sealed class ExistingSourceRow
  262. {
  263. public long Id { get; set; }
  264. public string? SupplierNumber { get; set; }
  265. }
  266. }