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;
}