| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- using Admin.NET.Plugin.AiDOP.Infrastructure;
- using Admin.NET.Plugin.AiDOP.ProcurementExecution;
- using Yitter.IdGenerator;
- namespace Admin.NET.Plugin.AiDOP.Supply;
- /// <summary>
- /// S4 采购执行闭环:缺料生成 PR → 合并 → 要货令转 DO/PO → 外部推送日志。
- /// 对应旧系统 AutoTransferDoOrPo / AutomaticPrAdjustDate 业务意图。
- /// </summary>
- [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;
- }
- /// <summary>执行采购闭环(合并待处理 PR、转单、写 QadTracking)。</summary>
- [DisplayName("采购执行闭环")]
- [HttpPost("procurement/execute-pipeline")]
- public async Task<ProcurementPipelineResult> 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);
- }
- }
- /// <summary>S4 读路径一致性检查(运行表 vs DWD)。</summary>
- [DisplayName("S4读路径一致性检查")]
- [HttpGet("procurement/read-path-consistency")]
- public async Task<S4ReadPathConsistencyResult> ReadPathConsistency([FromQuery] string? domain)
- {
- var tenantId = ResolveTenantId(domain);
- return await _readPathCheck.CheckTenantAsync(tenantId);
- }
- public async Task<ProcurementPipelineResult> 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<PurchaseRequestMain>();
- 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<List<PurchaseRequestMain>> LoadPendingPurchaseRequestsAsync(long tenantId)
- {
- var windowEnd = DateTime.Today.AddDays(35);
- var rows = await _db.Ado.SqlQueryAsync<PurchaseRequestMain>(
- """
- 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<PurchaseRequestMain> 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<PurchaseRequestMain> 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<long> 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<List<string>> TryTriggerS4MdpRefreshAsync(ProcurementPipelineResult result)
- {
- var warnings = new List<string>();
- 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<string>();
- 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<string> CreatedPoNumbers { get; set; } = new();
- public List<string> 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;
- }
|