using Admin.NET.Plugin.AiDOP.Infrastructure; using Admin.NET.Plugin.AiDOP.ProcurementExecution; using Yitter.IdGenerator; namespace Admin.NET.Plugin.AiDOP.Supply; /// /// S4 采购执行闭环:缺料生成 PR → 合并 → 要货令转 DO/PO → 外部推送日志。 /// 对应旧系统 AutoTransferDoOrPo / AutomaticPrAdjustDate 业务意图。 /// [ApiDescriptionSettings(Order = 309, Description = "采购执行闭环")] [Route("api/Supply")] [AllowAnonymous] [NonUnify] public class ProcurementPipelineService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly UserManager _userManager; private readonly PurchaseRequestFromShortageService _shortageService; private readonly PurchaseRequestMergeService _mergeService; private readonly NumberRuleService _numberRuleService; private readonly PurchaseOrderTransferService _transferService; private readonly PurchaseRequestExternalPushService _pushService; private readonly AidopActionRunLogWriter _runLog; private readonly S4MdpSyncTransformService _s4MdpSync; private readonly S4ReadPathConsistencyService _readPathCheck; public ProcurementPipelineService( ISqlSugarClient db, UserManager userManager, PurchaseRequestFromShortageService shortageService, PurchaseRequestMergeService mergeService, NumberRuleService numberRuleService, PurchaseOrderTransferService transferService, PurchaseRequestExternalPushService pushService, AidopActionRunLogWriter runLog, S4MdpSyncTransformService s4MdpSync, S4ReadPathConsistencyService readPathCheck) { _db = db; _userManager = userManager; _shortageService = shortageService; _mergeService = mergeService; _numberRuleService = numberRuleService; _transferService = transferService; _pushService = pushService; _runLog = runLog; _s4MdpSync = s4MdpSync; _readPathCheck = readPathCheck; } /// 执行采购闭环(合并待处理 PR、转单、写 QadTracking)。 [DisplayName("采购执行闭环")] [HttpPost("procurement/execute-pipeline")] public async Task ExecutePipeline( [FromQuery] string? domain, [FromQuery] bool createFromShortage = true, [FromQuery] bool mergeHistorical = true, [FromQuery] bool enableExternalPush = true) { var tenantId = ResolveTenantId(domain); var account = _userManager.Account ?? "system"; var logId = await _runLog.StartAsync("S4_PROCUREMENT_PIPELINE", tenantId, "tenant", tenantId, domain ?? tenantId.ToString()); try { var result = await ExecuteCoreAsync(tenantId, account, createFromShortage, mergeHistorical, enableExternalPush); result.ReadPathConsistency = await _readPathCheck.CheckTenantAsync(tenantId); result.Warnings.AddRange(await TryTriggerS4MdpRefreshAsync(result)); await _runLog.SuccessAsync(logId, result.Message, result); return result; } catch (Exception ex) { await _runLog.FailedAsync(logId, ex.Message, new { tenantId, domain }); throw Oops.Oh(ex.Message); } } /// S4 读路径一致性检查(运行表 vs DWD)。 [DisplayName("S4读路径一致性检查")] [HttpGet("procurement/read-path-consistency")] public async Task ReadPathConsistency([FromQuery] string? domain) { var tenantId = ResolveTenantId(domain); return await _readPathCheck.CheckTenantAsync(tenantId); } public async Task ExecuteCoreAsync( long tenantId, string account, bool createFromShortage, bool mergeHistorical = true, bool enableExternalPush = true) { var result = new ProcurementPipelineResult(); var now = DateTime.Now; await _db.Ado.BeginTranAsync(); try { var newRequests = new List(); if (createFromShortage) { newRequests = await _shortageService.BuildFromResourceCheckAsync(tenantId, account); result.ShortageItemCount = newRequests.Count; } if (newRequests.Count > 0) { var mergeNew = _mergeService.MergeGeneratedRequests(newRequests); newRequests = mergeNew.Requests; result.PrMergeReducedCount += mergeNew.ReducedCount; await AssignPrNumbersAsync(newRequests, account); await InsertPurchaseRequestsAsync(newRequests); result.PrCreatedCount = newRequests.Count; } if (mergeHistorical) { var historicalMerge = await _mergeService.MergeTenantPendingAsync(tenantId, account); result.PrMergeReducedCount += historicalMerge.ReducedCount; result.HistoricalPrMergedGroups = historicalMerge.MergedGroupCount; result.PrCreatedCount += historicalMerge.CreatedPrCount; } var pending = await LoadPendingPurchaseRequestsAsync(tenantId); result.PendingPrCount = pending.Count; if (pending.Any(x => x.IsRequireGoods == 1)) { var transfer = await _transferService.TransferGeneratedRequireGoodsAsync( pending.Where(x => x.IsRequireGoods == 1).ToList(), account); result.PoCreatedCount = transfer.CreatedOrderCount; result.PoTransferredPrCount = transfer.TransferredPrCount; result.PoOccupyRehangedCount = transfer.PoOccupyRehangedCount; result.CreatedPoNumbers = transfer.CreatedOrders; await PersistPrStateUpdatesAsync(transfer.TransferredPrIds, 4, account); } var pushCandidates = pending .Where(x => x.IsRequireGoods == 0 && (x.State ?? 0) == 1) .Concat(newRequests.Where(x => x.IsRequireGoods == 0)) .GroupBy(x => x.Id) .Select(g => g.First()) .ToList(); if (enableExternalPush && pushCandidates.Count > 0) { var push = await _pushService.CreateQadTrackingForGeneratedRequestsAsync(pushCandidates, account); result.QadTrackingCount = push.TrackingCount; result.QadTrackingSkippedCount = push.SkippedCandidateCount; result.Warnings.AddRange(push.Warnings); await PersistPrStateUpdatesAsync(push.PushedPrIds, 2, account); } await _db.Ado.CommitTranAsync(); } catch { await _db.Ado.RollbackTranAsync(); throw; } result.Message = BuildMessage(result); return result; } private async Task> LoadPendingPurchaseRequestsAsync(long tenantId) { var windowEnd = DateTime.Today.AddDays(35); var rows = await _db.Ado.SqlQueryAsync( """ SELECT Id, pr_billno AS PrBillNo, pr_purchaseid AS PrPurchaseId, pr_purchasenumber AS PrPurchaseNumber, pr_purchasename AS PrPurchaseName, pr_purchaser AS PrPurchaser, pr_purchaser_num AS PrPurchaserNum, pr_rqty AS PrRqty, pr_aqty AS PrAqty, pr_sqty AS PrSqty, icitem_id AS IcitemId, icitem_name AS IcitemName, pr_ssend_date AS PrSsendDate, pr_sarrive_date AS PrSarriveDate, pr_unit AS PrUnit, state AS State, pr_type AS PrType, currencytype AS CurrencyType, tenant_id AS TenantId, factory_id AS FactoryId, org_id AS OrgId, company_id AS CompanyId, IsRequireGoods, supplier_type AS SupplierType, IsDeleted FROM srm_pr_main WHERE tenant_id = @TenantId AND IFNULL(IsDeleted, 0) = 0 AND IFNULL(state, 0) = 1 AND IFNULL(analogcalcversion, '') = '' AND pr_ssend_date IS NOT NULL AND pr_ssend_date <= @WindowEnd ORDER BY pr_ssend_date, Id """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@WindowEnd", windowEnd)); return rows; } private async Task AssignPrNumbersAsync(List requests, string account) { foreach (var group in requests.GroupBy(x => x.TenantId.ToString())) { var rows = group.ToList(); var numbers = await _numberRuleService.NextBatchInCurrentTransactionAsync("PR", group.Key, rows.Count, account); if (numbers.Count < rows.Count) throw Oops.Oh("采购申请编号生成失败"); for (var i = 0; i < rows.Count; i++) rows[i].PrBillNo = numbers[i].Trim(); } } private async Task InsertPurchaseRequestsAsync(List requests) { foreach (var pr in requests) { await _db.Ado.ExecuteCommandAsync( """ INSERT INTO srm_pr_main (Id,pr_billno,pr_purchaseid,pr_purchasenumber,pr_purchasename,pr_purchaser,pr_purchaser_num, pr_rqty,pr_aqty,pr_sqty,icitem_id,icitem_name,pr_ssend_date,pr_sarrive_date,pr_unit,state,pr_type,currencytype, create_by_name,create_time,update_by_name,update_time,tenant_id,factory_id,org_id,IsDeleted,company_id,IsRequireGoods,supplier_type) VALUES (@Id,@PrBillNo,@PrPurchaseId,@PrPurchaseNumber,@PrPurchaseName,@PrPurchaser,@PrPurchaserNum, @PrRqty,@PrAqty,@PrSqty,@IcitemId,@IcitemName,@PrSsendDate,@PrSarriveDate,@PrUnit,@State,@PrType,@CurrencyType, @CreateByName,@CreateTime,@UpdateByName,@UpdateTime,@TenantId,@FactoryId,@OrgId,0,@CompanyId,@IsRequireGoods,@SupplierType) """, new SugarParameter("@Id", pr.Id), new SugarParameter("@PrBillNo", pr.PrBillNo), new SugarParameter("@PrPurchaseId", pr.PrPurchaseId), new SugarParameter("@PrPurchaseNumber", pr.PrPurchaseNumber), new SugarParameter("@PrPurchaseName", pr.PrPurchaseName), new SugarParameter("@PrPurchaser", pr.PrPurchaser), new SugarParameter("@PrPurchaserNum", pr.PrPurchaserNum), new SugarParameter("@PrRqty", pr.PrRqty), new SugarParameter("@PrAqty", pr.PrAqty), new SugarParameter("@PrSqty", pr.PrSqty), new SugarParameter("@IcitemId", pr.IcitemId), new SugarParameter("@IcitemName", pr.IcitemName), new SugarParameter("@PrSsendDate", pr.PrSsendDate), new SugarParameter("@PrSarriveDate", pr.PrSarriveDate), new SugarParameter("@PrUnit", pr.PrUnit), new SugarParameter("@State", pr.State ?? 1), new SugarParameter("@PrType", pr.PrType ?? 3), new SugarParameter("@CurrencyType", pr.CurrencyType), new SugarParameter("@CreateByName", pr.CreateByName), new SugarParameter("@CreateTime", pr.CreateTime), new SugarParameter("@UpdateByName", pr.UpdateByName), new SugarParameter("@UpdateTime", pr.UpdateTime), new SugarParameter("@TenantId", pr.TenantId), new SugarParameter("@FactoryId", pr.FactoryId), new SugarParameter("@OrgId", pr.OrgId), new SugarParameter("@CompanyId", pr.CompanyId ?? 1000), new SugarParameter("@IsRequireGoods", pr.IsRequireGoods), new SugarParameter("@SupplierType", pr.SupplierType)); } } private async Task PersistPrStateUpdatesAsync(IReadOnlyList prIds, int state, string account) { if (prIds.Count == 0) return; foreach (var id in prIds.Distinct()) { await _db.Ado.ExecuteCommandAsync( """ UPDATE srm_pr_main SET state = @State, update_by_name = @User, update_time = @Now WHERE Id = @Id """, new SugarParameter("@State", state), new SugarParameter("@User", account), new SugarParameter("@Now", DateTime.Now), new SugarParameter("@Id", id)); } } private long ResolveTenantId(string? domain) { if (!string.IsNullOrWhiteSpace(domain) && long.TryParse(domain.Trim(), out var tid) && tid > 0) return tid; return AidopTenantHelper.Resolve(App.HttpContext); } private async Task> TryTriggerS4MdpRefreshAsync(ProcurementPipelineResult result) { var warnings = new List(); if (result.PrCreatedCount == 0 && result.PoCreatedCount == 0 && result.QadTrackingCount == 0) return warnings; try { var mdp = await _s4MdpSync.RunFullAsync(triggerType: "PROCUREMENT_PIPELINE"); result.S4MdpRefreshed = true; result.S4MdpBatchId = mdp.BatchId; } catch (Exception ex) { warnings.Add($"S4 MDP/DWD 刷新未完成:{ex.Message}"); } return warnings; } private static string BuildMessage(ProcurementPipelineResult r) { var parts = new List(); if (r.PrCreatedCount > 0) parts.Add($"新建 PR {r.PrCreatedCount}"); if (r.PoCreatedCount > 0) parts.Add($"转 DO/PO {r.PoCreatedCount}"); if (r.QadTrackingCount > 0) parts.Add($"外部推送 {r.QadTrackingCount}"); return parts.Count > 0 ? string.Join(",", parts) : "无待处理采购申请"; } } public sealed class ProcurementPipelineResult { public int ShortageItemCount { get; set; } public int PrCreatedCount { get; set; } public int PrMergeReducedCount { get; set; } public int HistoricalPrMergedGroups { get; set; } public int PendingPrCount { get; set; } public int PoCreatedCount { get; set; } public int PoTransferredPrCount { get; set; } public int PoOccupyRehangedCount { get; set; } public int QadTrackingCount { get; set; } public int QadTrackingSkippedCount { get; set; } public List CreatedPoNumbers { get; set; } = new(); public List Warnings { get; set; } = new(); public bool S4MdpRefreshed { get; set; } public string? S4MdpBatchId { get; set; } public S4ReadPathConsistencyResult? ReadPathConsistency { get; set; } public string Message { get; set; } = string.Empty; }