Browse Source

feat(s4): push purchase chain to WMS 165 so barcode receipt works (server 1.0.330)

拆库后 WMS 读 165,而自建采购单及其下游数据从未落到 165,导致 APP 扫码收货既选不到
采购单、也查不到装箱标签。改为在业务动作发生时经 outbox 回写:PR→PO 转单推
PurOrdMaster/Detail,交货计划发布推 srm_polist_ds,发货单生成标签推
scm_shd/shdzb/shdshph 与 MissedPrint。

- MdpDbPushExecutor 支持受限 resolve 子查询,用自然键在目标库解析 RecID 外键,
  避免直接搬运源库 IDENTITY 值;白名单补齐上述四张表
- 修正 scm_shbq 取数的列别名(po_billline / po_usage),此前映射失败使
  MissedPrint.PurLine 落 0,标签匹配不到订单项
- 推送范围限自建单(ReqBy in PO/DO)且排除委外 PW;不写 DO 的 B 状态

端到端已实测:PO202608110003 扫码收货生成 RC202608110001,PurOrdDetail.RctQty
+500,箱码转待检。收货结果回读 Ai-DOP 仍待做。
YY968XX 4 days ago
parent
commit
ef3d691fb2

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 閫氱敤鏉冮檺寮€鍙戝钩鍙?/Description>
     <Description>Admin.NET 閫氱敤鏉冮檺寮€鍙戝钩鍙?/Description>
-    <AssemblyVersion>1.0.329</AssemblyVersion>
-    <FileVersion>1.0.329</FileVersion>
-    <Version>1.0.329</Version>
+    <AssemblyVersion>1.0.330</AssemblyVersion>
+    <FileVersion>1.0.330</FileVersion>
+    <Version>1.0.330</Version>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 78 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/DataPlatform/Executors/MdpDbPushExecutor.cs

@@ -22,6 +22,7 @@ public sealed class MdpDbPushExecutor : IMdpTargetPushExecutor, ITransient
         "PurOrdMaster", "PurOrdDetail", "PurOrdDetailBatch",
         "PurOrdMaster", "PurOrdDetail", "PurOrdDetailBatch",
         "NbrMaster", "NbrDetail",
         "NbrMaster", "NbrDetail",
         "PeriodSequenceDet", "srm_polist_ds",
         "PeriodSequenceDet", "srm_polist_ds",
+        "scm_shd", "scm_shdzb", "scm_shdshph", "MissedPrint",
         "ScheduleResultOpMaster", "rf_serialnumber",
         "ScheduleResultOpMaster", "rf_serialnumber",
         // WP7 基础数据 + 任务(DOP 独占)
         // WP7 基础数据 + 任务(DOP 独占)
         "LocationMaster", "LocationShelfMaster", "DepartmentMaster",
         "LocationMaster", "LocationShelfMaster", "DepartmentMaster",
@@ -61,11 +62,19 @@ public sealed class MdpDbPushExecutor : IMdpTargetPushExecutor, ITransient
         ValidateColumns(payload.Insert, "insert");
         ValidateColumns(payload.Insert, "insert");
         ValidateColumns(payload.Update, "update");
         ValidateColumns(payload.Update, "update");
         ValidateColumns(payload.Expect, "expect");
         ValidateColumns(payload.Expect, "expect");
+        ValidateResolves(payload.Resolves);
 
 
         var op = (payload.Op ?? "").Trim().ToUpperInvariant();
         var op = (payload.Op ?? "").Trim().ToUpperInvariant();
         try
         try
         {
         {
             var db = await _scopeFactory.GetScopeAsync(source.SourceCode, ct);
             var db = await _scopeFactory.GetScopeAsync(source.SourceCode, ct);
+
+            if (payload.Resolves.Count > 0)
+            {
+                var resolveErr = await ApplyResolvesAsync(db, payload, ct);
+                if (resolveErr != null) return MdpPushResult.Fail(resolveErr);
+            }
+
             return op switch
             return op switch
             {
             {
                 "INSERT" => await ExecInsertAsync(db, payload, ct),
                 "INSERT" => await ExecInsertAsync(db, payload, ct),
@@ -239,6 +248,43 @@ public sealed class MdpDbPushExecutor : IMdpTargetPushExecutor, ITransient
         return null;
         return null;
     }
     }
 
 
+    /// <summary>
+    /// 目标库自增主键不能由本库 RecID 顶替,外键列须在 165 上现查现填。
+    /// 上游行还没到位时返回失败,交给 dispatcher 退避重试,等主表推成功后自然收敛。
+    /// </summary>
+    private static async Task<string?> ApplyResolvesAsync(
+        ISqlSugarClient db, DbPushPayload p, CancellationToken ct)
+    {
+        foreach (var r in p.Resolves)
+        {
+            var where = string.Join(" AND ", r.Match.Keys.Select((c, i) => $"[{c}]=@r{i}"));
+            var pars = r.Match.Select((kv, i) => new SugarParameter($"@r{i}", CoerceDbValue(kv.Value) ?? DBNull.Value)).ToArray();
+            var sql = $"SELECT TOP 1 [{r.Column}] FROM [{r.Table}] WHERE {where}";
+            var val = await db.Ado.GetScalarAsync(sql, pars);
+            if (val == null || val == DBNull.Value)
+                return $"resolve 未命中:{r.Table}.{r.Column}(目标列 {r.TargetColumn}),上游行未就绪";
+            p.Insert[r.TargetColumn] = val;
+        }
+        return null;
+    }
+
+    private static void ValidateResolves(List<ResolveSpec> resolves)
+    {
+        foreach (var r in resolves)
+        {
+            if (!ColNameRegex.IsMatch(r.TargetColumn))
+                throw new InvalidOperationException($"非法列名(resolve):{r.TargetColumn}");
+            ValidateTable(r.Table);
+            if (!ColNameRegex.IsMatch(r.Column))
+                throw new InvalidOperationException($"非法列名(resolve.column):{r.Column}");
+            ValidateColumns(r.Match, "resolve.match");
+            if (r.Match.Count == 0)
+                throw new InvalidOperationException($"resolve.{r.TargetColumn} 缺少 match,禁止无条件取值");
+            if (RejectArrayValues(r.Match, "resolve.match") is { } err)
+                throw new InvalidOperationException(err);
+        }
+    }
+
     private static void ValidateTable(string table)
     private static void ValidateTable(string table)
     {
     {
         if (string.IsNullOrWhiteSpace(table) || !ColNameRegex.IsMatch(table))
         if (string.IsNullOrWhiteSpace(table) || !ColNameRegex.IsMatch(table))
@@ -267,11 +313,33 @@ public sealed class MdpDbPushExecutor : IMdpTargetPushExecutor, ITransient
             Keys = ReadDict(root, "keys"),
             Keys = ReadDict(root, "keys"),
             Insert = ReadDict(root, "insert"),
             Insert = ReadDict(root, "insert"),
             Update = ReadDict(root, "update"),
             Update = ReadDict(root, "update"),
-            Expect = ReadDict(root, "expect")
+            Expect = ReadDict(root, "expect"),
+            Resolves = ReadResolves(root)
         };
         };
         return p;
         return p;
     }
     }
 
 
+    private static List<ResolveSpec> ReadResolves(JsonElement root)
+    {
+        var list = new List<ResolveSpec>();
+        if (!root.TryGetProperty("resolve", out var el) || el.ValueKind != JsonValueKind.Object)
+            return list;
+        foreach (var prop in el.EnumerateObject())
+        {
+            if (prop.Value.ValueKind != JsonValueKind.Object)
+                throw new InvalidOperationException($"resolve.{prop.Name} 必须是对象");
+            var spec = new ResolveSpec
+            {
+                TargetColumn = prop.Name,
+                Table = prop.Value.TryGetProperty("table", out var t) ? t.GetString() ?? "" : "",
+                Column = prop.Value.TryGetProperty("column", out var c) ? c.GetString() ?? "" : "",
+                Match = ReadDict(prop.Value, "match")
+            };
+            list.Add(spec);
+        }
+        return list;
+    }
+
     private static Dictionary<string, object?> ReadDict(JsonElement root, string name)
     private static Dictionary<string, object?> ReadDict(JsonElement root, string name)
     {
     {
         var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
         var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
@@ -302,5 +370,14 @@ public sealed class MdpDbPushExecutor : IMdpTargetPushExecutor, ITransient
         public Dictionary<string, object?> Insert { get; set; } = new();
         public Dictionary<string, object?> Insert { get; set; } = new();
         public Dictionary<string, object?> Update { get; set; } = new();
         public Dictionary<string, object?> Update { get; set; } = new();
         public Dictionary<string, object?> Expect { get; set; } = new();
         public Dictionary<string, object?> Expect { get; set; } = new();
+        public List<ResolveSpec> Resolves { get; set; } = new();
+    }
+
+    private sealed class ResolveSpec
+    {
+        public string TargetColumn { get; set; } = "";
+        public string Table { get; set; } = "";
+        public string Column { get; set; } = "";
+        public Dictionary<string, object?> Match { get; set; } = new();
     }
     }
 }
 }

+ 1080 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/DataPlatform/Wms/PurOrdWmsPushService.cs

@@ -0,0 +1,1080 @@
+using System.Text.Json;
+using Admin.NET.Plugin.AiDOP.DataPlatform.Executors;
+using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using SqlSugar;
+
+namespace Admin.NET.Plugin.AiDOP.DataPlatform.Wms;
+
+/// <summary>
+/// WP4 · 采购链入 165:自建采购单 / 交货计划 / 送货单 → Outbox UPSERT,供 WMS 扫码收货读取。
+/// 旧 DOP 与 WMS 同库,建单即可见;新架构拆库后必须补这段回写。
+/// 范围:Potype='po' 且 ReqBy in ('PO','DO') 的自建单,SAP 下发单不回推。
+/// </summary>
+[ApiDescriptionSettings(Order = 327, Description = "采购链推送 WMS")]
+[Route("api/aidop/wms-purord")]
+[AllowAnonymous]
+[NonUnify]
+public class PurOrdWmsPushService : IDynamicApiController, ITransient
+{
+    public const string TargetSource = "DOPDEMORQ_SQLSERVER";
+    public const string ActionPom = "PO_WMS_POM";
+    public const string ActionPod = "PO_WMS_POD";
+    public const string ActionDs = "PO_WMS_DS";
+    public const string ActionShd = "PO_WMS_SHD";
+    public const string ActionShdzb = "PO_WMS_SHDZB";
+    public const string ActionShph = "PO_WMS_SHPH";
+    public const string ActionMp = "PO_WMS_MP";
+
+    private readonly ISqlSugarClient _db;
+    private readonly MdpOutboxEnqueueService _enqueue;
+    private readonly MdpOutboxWakeSignal _wake;
+    private readonly UserManager _userManager;
+    private readonly IHttpContextAccessor _httpContextAccessor;
+    private readonly ILogger<PurOrdWmsPushService> _logger;
+
+    public PurOrdWmsPushService(
+        ISqlSugarClient db,
+        MdpOutboxEnqueueService enqueue,
+        MdpOutboxWakeSignal wake,
+        UserManager userManager,
+        IHttpContextAccessor httpContextAccessor,
+        ILogger<PurOrdWmsPushService> logger)
+    {
+        _db = db;
+        _enqueue = enqueue;
+        _wake = wake;
+        _userManager = userManager;
+        _httpContextAccessor = httpContextAccessor;
+        _logger = logger;
+    }
+
+    public sealed class PushInput
+    {
+        /// <summary>采购单号。与 Shddh 二选一。</summary>
+        public string? PurOrd { get; set; }
+
+        /// <summary>送货单号。给定时按「PO + 交货计划 + 送货单」整链补推。</summary>
+        public string? Shddh { get; set; }
+
+        public long TenantId { get; set; }
+    }
+
+    public sealed class PushResult
+    {
+        public bool Ok { get; set; }
+        public int Enqueued { get; set; }
+        public int PurOrdCount { get; set; }
+        public int DetailCount { get; set; }
+        public int ScheduleCount { get; set; }
+        public int ShipmentCount { get; set; }
+        public int LabelCount { get; set; }
+        public int BarcodeCount { get; set; }
+        public List<string> Skipped { get; set; } = new();
+        public string? Message { get; set; }
+    }
+
+    /// <summary>补偿 / 联调:按本库快照把采购链推 165。</summary>
+    [DisplayName("推送采购链到 WMS")]
+    [HttpPost("push")]
+    public async Task<PushResult> Push([FromBody] PushInput input, CancellationToken ct = default)
+    {
+        if (input == null) throw Oops.Oh("请求体不能为空");
+        if (string.IsNullOrWhiteSpace(input.PurOrd) && string.IsNullOrWhiteSpace(input.Shddh))
+            throw Oops.Oh("采购单号与送货单号至少给一个");
+        if (input.TenantId <= 0) throw Oops.Oh("租户号无效,请指定有效的 TenantId");
+
+        var jwtTenantId = _userManager.TenantId;
+        if (jwtTenantId > 0 && input.TenantId != jwtTenantId)
+            throw Oops.Oh("请求租户与当前登录租户不一致");
+        if (jwtTenantId <= 0)
+        {
+            var clientIp = _httpContextAccessor.HttpContext?.Connection?.RemoteIpAddress?.ToString() ?? "unknown";
+            _logger.LogInformation(
+                "[PurOrdWmsPush] 无登录态推送 audit ip={ClientIp} purOrd={PurOrd} shddh={Shddh} tenantId={TenantId}",
+                clientIp, input.PurOrd, input.Shddh, input.TenantId);
+        }
+
+        var result = new PushResult { Ok = true };
+        if (!string.IsNullOrWhiteSpace(input.Shddh))
+            await EnqueueShipmentChainAsync(input.TenantId, input.Shddh.Trim(), result, ct);
+        else
+            await EnqueuePurchaseOrderAsync(input.TenantId, input.PurOrd!.Trim(), result, ct);
+
+        _wake.Pulse();
+        result.Message = $"已入队 {result.Enqueued} 条(采购单 {result.PurOrdCount}/明细 {result.DetailCount}/"
+                         + $"交货计划 {result.ScheduleCount}/送货单 {result.ShipmentCount}/装箱标签 {result.LabelCount}/"
+                         + $"箱码 {result.BarcodeCount})";
+        return result;
+    }
+
+    /// <summary>业务挂接点用:失败只记日志与 warning,不拖垮本库事务。</summary>
+    public async Task TryEnqueuePurchaseOrderSafeAsync(long tenantId, string purOrd, List<string>? warnings = null)
+    {
+        if (string.IsNullOrWhiteSpace(purOrd)) return;
+        try
+        {
+            var r = new PushResult();
+            await EnqueuePurchaseOrderAsync(tenantId, purOrd.Trim(), r, CancellationToken.None);
+            _wake.Pulse();
+            if (r.Enqueued > 0)
+                warnings?.Add($"采购单 {purOrd} 已入队推送 WMS({r.Enqueued} 条)");
+        }
+        catch (Exception ex)
+        {
+            _logger.LogWarning(ex, "[PurOrdWmsPush] purOrd enqueue failed tenant={Tenant} po={PurOrd}", tenantId, purOrd);
+            warnings?.Add($"采购单 {purOrd} 推送 WMS 入队失败:{ex.Message}(可调用 /api/aidop/wms-purord/push 补偿)");
+        }
+    }
+
+    /// <summary>业务挂接点用:交货计划发布/取消后同步 165。</summary>
+    public async Task TryEnqueueSchedulesSafeAsync(long tenantId, List<long> ids, List<string>? warnings = null)
+    {
+        if (ids == null || ids.Count == 0) return;
+        try
+        {
+            var r = new PushResult();
+            await EnqueueSchedulesByIdsAsync(tenantId, ids, r, CancellationToken.None);
+            _wake.Pulse();
+            if (r.Enqueued > 0)
+                warnings?.Add($"交货计划已入队推送 WMS({r.Enqueued} 条)");
+        }
+        catch (Exception ex)
+        {
+            _logger.LogWarning(ex, "[PurOrdWmsPush] ds enqueue failed tenant={Tenant} ids={Count}", tenantId, ids.Count);
+            warnings?.Add($"交货计划推送 WMS 入队失败:{ex.Message}");
+        }
+    }
+
+    /// <summary>业务挂接点用:发货单生成标签后推整链(PO → 交货计划 → 送货单 → 装箱标签)。</summary>
+    public async Task TryEnqueueShipmentChainSafeAsync(long tenantId, string shddh, List<string>? warnings = null)
+    {
+        if (string.IsNullOrWhiteSpace(shddh)) return;
+        try
+        {
+            var r = new PushResult();
+            await EnqueueShipmentChainAsync(tenantId, shddh.Trim(), r, CancellationToken.None);
+            _wake.Pulse();
+            if (r.Enqueued > 0)
+                warnings?.Add($"送货单 {shddh} 及采购链已入队推送 WMS({r.Enqueued} 条)");
+            foreach (var s in r.Skipped) warnings?.Add(s);
+        }
+        catch (Exception ex)
+        {
+            _logger.LogWarning(ex, "[PurOrdWmsPush] shipment enqueue failed tenant={Tenant} shddh={Shddh}", tenantId, shddh);
+            warnings?.Add($"送货单 {shddh} 推送 WMS 入队失败:{ex.Message}(可调用 /api/aidop/wms-purord/push 补偿)");
+        }
+    }
+
+    // ── 采购单主表 + 明细 ──────────────────────────────────────────────
+
+    private async Task EnqueuePurchaseOrderAsync(
+        long tenantId, string purOrd, PushResult result, CancellationToken ct)
+    {
+        var masters = await _db.Ado.SqlQueryAsync<PurOrdMasterRow>(
+            """
+            SELECT
+                TRIM(IFNULL(`Domain`, '')) AS Domain,
+                TRIM(IFNULL(PurOrd, '')) AS PurOrd,
+                TRIM(IFNULL(Potype, '')) AS Potype,
+                TRIM(IFNULL(Typed, '')) AS Typed,
+                TRIM(IFNULL(Supp, '')) AS Supp,
+                TRIM(IFNULL(Buyer, '')) AS Buyer,
+                TRIM(IFNULL(ReqBy, '')) AS ReqBy,
+                TRIM(IFNULL(`Usage`, '')) AS UsageText,
+                TRIM(IFNULL(FSTID, '')) AS FSTID,
+                TRIM(IFNULL(Remark, '')) AS Remark,
+                OrdDate, DueDate,
+                IFNULL(IsActive, 1) AS IsActive,
+                IFNULL(IsConfirm, 0) AS IsConfirm,
+                CreateUser, CreateTime, UpdateUser, UpdateTime
+            FROM PurOrdMaster
+            WHERE tenant_id = @TenantId AND PurOrd = @PurOrd
+            LIMIT 1
+            """,
+            new SugarParameter("@TenantId", tenantId),
+            new SugarParameter("@PurOrd", purOrd));
+        if (masters.Count == 0)
+            throw Oops.Oh($"本库未找到采购单 {purOrd}");
+        var m = masters[0];
+
+        if (!IsSelfBuilt(m.Potype, m.ReqBy))
+        {
+            result.Skipped.Add($"采购单 {purOrd} 非自建单(Potype={m.Potype} / ReqBy={m.ReqBy}),按约定不回推 165");
+            return;
+        }
+
+        var domain = string.IsNullOrWhiteSpace(m.Domain) ? "8010" : m.Domain;
+        var potype = string.IsNullOrWhiteSpace(m.Potype) ? "po" : m.Potype;
+        var now = DateTime.Now;
+
+        var keys = new Dictionary<string, object?>
+        {
+            ["Domain"] = domain,
+            ["PurOrd"] = purOrd,
+            ["Potype"] = potype
+        };
+        var insert = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
+        {
+            ["Domain"] = domain,
+            ["PurOrd"] = Trunc(purOrd, 48),
+            ["Potype"] = Trunc(potype, 8),
+            ["Typed"] = Trunc(m.Typed, 18),
+            ["Supp"] = Trunc(m.Supp, 20),
+            ["Buyer"] = Trunc(m.Buyer, 30),
+            ["ReqBy"] = Trunc(m.ReqBy, 8),
+            // Status 只建行写空串;收货态由 WMS 维护
+            ["Status"] = "",
+            ["Usage"] = Trunc(m.UsageText, 30),
+            ["FSTID"] = Trunc(m.FSTID, 24),
+            ["Remark"] = Trunc(m.Remark, 200),
+            ["IsActive"] = m.IsActive != 0,
+            ["IsConfirm"] = m.IsConfirm != 0,
+            ["CreateUser"] = Trunc(m.CreateUser ?? "aidop", 24),
+            ["UpdateUser"] = Trunc(m.UpdateUser ?? m.CreateUser ?? "aidop", 24),
+            ["CreateTime"] = Fmt(m.CreateTime ?? now),
+            ["UpdateTime"] = Fmt(m.UpdateTime ?? now)
+        };
+        if (m.OrdDate != null) insert["OrdDate"] = Fmt(m.OrdDate);
+        if (m.DueDate != null) insert["DueDate"] = Fmt(m.DueDate);
+
+        var update = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
+        {
+            ["Typed"] = Trunc(m.Typed, 18),
+            ["Supp"] = Trunc(m.Supp, 20),
+            ["Buyer"] = Trunc(m.Buyer, 30),
+            ["ReqBy"] = Trunc(m.ReqBy, 8),
+            ["Usage"] = Trunc(m.UsageText, 30),
+            ["FSTID"] = Trunc(m.FSTID, 24),
+            ["Remark"] = Trunc(m.Remark, 200),
+            ["IsActive"] = m.IsActive != 0,
+            ["UpdateUser"] = Trunc(m.UpdateUser ?? "aidop", 24),
+            ["UpdateTime"] = Fmt(now)
+        };
+        if (m.DueDate != null) update["DueDate"] = Fmt(m.DueDate);
+
+        if (await EnqueueRowAsync(tenantId, $"po|{domain}|{potype}|{purOrd}|pom",
+                ActionPom, "PurOrdMaster", keys, insert, update, null, ct))
+            result.Enqueued++;
+        result.PurOrdCount++;
+
+        var details = await _db.Ado.SqlQueryAsync<PurOrdDetailRow>(
+            """
+            SELECT
+                TRIM(IFNULL(`Domain`, '')) AS Domain,
+                IFNULL(Line, 0) AS Line,
+                TRIM(IFNULL(ItemNum, '')) AS ItemNum,
+                TRIM(IFNULL(Descr, '')) AS Descr,
+                TRIM(IFNULL(UM, '')) AS UM,
+                TRIM(IFNULL(Rev, '')) AS Rev,
+                TRIM(IFNULL(Drawing, '')) AS Drawing,
+                TRIM(IFNULL(Location, '')) AS Location,
+                TRIM(IFNULL(LotSerial, '')) AS LotSerial,
+                TRIM(IFNULL(Req, '')) AS Req,
+                IFNULL(QtyOrded, 0) AS QtyOrded,
+                DueDate, NeedDate,
+                IFNULL(IsActive, 1) AS IsActive,
+                IFNULL(IsConfirm, 0) AS IsConfirm,
+                CreateUser, CreateTime, UpdateUser, UpdateTime
+            FROM PurOrdDetail
+            WHERE tenant_id = @TenantId AND PurOrd = @PurOrd
+            ORDER BY Line
+            """,
+            new SugarParameter("@TenantId", tenantId),
+            new SugarParameter("@PurOrd", purOrd));
+
+        foreach (var d in details)
+        {
+            var dKeys = new Dictionary<string, object?>
+            {
+                ["Domain"] = domain,
+                ["PurOrd"] = purOrd,
+                ["Potype"] = potype,
+                ["Line"] = d.Line,
+                ["BlanketLine"] = 0
+            };
+            var dInsert = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
+            {
+                ["Domain"] = domain,
+                ["PurOrd"] = Trunc(purOrd, 48),
+                ["Potype"] = Trunc(potype, 8),
+                ["Line"] = d.Line,
+                ["BlanketLine"] = 0,
+                ["ItemNum"] = Trunc(d.ItemNum, 60),
+                ["Descr"] = Trunc(d.Descr, 255),
+                ["UM"] = Trunc(d.UM, 8),
+                ["Rev"] = Trunc(d.Rev, 8),
+                ["Drawing"] = Trunc(d.Drawing, 24),
+                ["Location"] = Trunc(d.Location, 8),
+                ["LotSerial"] = Trunc(d.LotSerial, 120),
+                ["Req"] = Trunc(d.Req, 20),
+                ["QtyOrded"] = d.QtyOrded,
+                // 收货累计列建行给 0,之后归 WMS
+                ["RctQty"] = 0m,
+                ["ReceiptQty"] = 0m,
+                ["QtyReturned"] = 0m,
+                ["Status"] = "",
+                ["IsActive"] = d.IsActive != 0,
+                ["IsConfirm"] = d.IsConfirm != 0,
+                ["CreateUser"] = Trunc(d.CreateUser ?? "aidop", 24),
+                ["UpdateUser"] = Trunc(d.UpdateUser ?? d.CreateUser ?? "aidop", 24),
+                ["CreateTime"] = Fmt(d.CreateTime ?? now),
+                ["UpdateTime"] = Fmt(d.UpdateTime ?? now)
+            };
+            if (d.DueDate != null) dInsert["DueDate"] = Fmt(d.DueDate);
+            if (d.NeedDate != null) dInsert["NeedDate"] = Fmt(d.NeedDate);
+
+            var dUpdate = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
+            {
+                ["ItemNum"] = Trunc(d.ItemNum, 60),
+                ["Descr"] = Trunc(d.Descr, 255),
+                ["UM"] = Trunc(d.UM, 8),
+                ["Rev"] = Trunc(d.Rev, 8),
+                ["Drawing"] = Trunc(d.Drawing, 24),
+                ["Location"] = Trunc(d.Location, 8),
+                ["QtyOrded"] = d.QtyOrded,
+                ["IsActive"] = d.IsActive != 0,
+                ["UpdateUser"] = Trunc(d.UpdateUser ?? "aidop", 24),
+                ["UpdateTime"] = Fmt(now)
+            };
+            if (d.DueDate != null) dUpdate["DueDate"] = Fmt(d.DueDate);
+            if (d.NeedDate != null) dUpdate["NeedDate"] = Fmt(d.NeedDate);
+
+            // 165 的 RecID 是 IDENTITY,明细外键必须在目标库现查主表 RecID
+            var resolve = new Dictionary<string, object?>
+            {
+                ["PurOrdRecID"] = new Dictionary<string, object?>
+                {
+                    ["table"] = "PurOrdMaster",
+                    ["column"] = "RecID",
+                    ["match"] = new Dictionary<string, object?>
+                    {
+                        ["Domain"] = domain,
+                        ["PurOrd"] = purOrd,
+                        ["Potype"] = potype
+                    }
+                }
+            };
+
+            if (await EnqueueRowAsync(tenantId, $"po|{domain}|{potype}|{purOrd}|pod|{d.Line}",
+                    ActionPod, "PurOrdDetail", dKeys, dInsert, dUpdate, resolve, ct))
+                result.Enqueued++;
+            result.DetailCount++;
+        }
+    }
+
+    // ── 交货计划 ──────────────────────────────────────────────────────
+
+    private async Task EnqueueSchedulesByIdsAsync(
+        long tenantId, List<long> ids, PushResult result, CancellationToken ct)
+    {
+        var rows = await _db.Ado.SqlQueryAsync<DeliveryScheduleRow>(
+            $"""
+            SELECT
+                Id, TRIM(IFNULL(domain, '')) AS Domain, IFNULL(icdsid, 0) AS Icdsid,
+                TRIM(IFNULL(dsnum, '')) AS Dsnum, TRIM(IFNULL(status, '')) AS Status,
+                TRIM(IFNULL(itemnum, '')) AS Itemnum, TRIM(IFNULL(um, '')) AS Um,
+                TRIM(IFNULL(purgroup, '')) AS Purgroup,
+                TRIM(IFNULL(suppliercode, '')) AS Suppliercode, TRIM(IFNULL(supplier, '')) AS Supplier,
+                submitdate, requestdate, needdate,
+                TRIM(IFNULL(ponumber, '')) AS Ponumber, IFNULL(poline, 0) AS Poline,
+                IFNULL(schedqty, 0) AS Schedqty,
+                TRIM(IFNULL(remarks, '')) AS Remarks, IFNULL(isactive, 1) AS Isactive,
+                createuser, createtime, updateuser, updatetime
+            FROM srm_polist_ds
+            WHERE tenant_id = @TenantId AND Id IN ({string.Join(",", ids.Select((_, i) => $"@i{i}"))})
+            """,
+            ids.Select((v, i) => new SugarParameter($"@i{i}", v))
+               .Append(new SugarParameter("@TenantId", tenantId))
+               .ToList());
+
+        foreach (var ds in rows)
+            await EnqueueScheduleRowAsync(tenantId, ds, result, ct);
+    }
+
+    private async Task EnqueueSchedulesByPurOrdAsync(
+        long tenantId, string purOrd, PushResult result, CancellationToken ct)
+    {
+        var rows = await _db.Ado.SqlQueryAsync<DeliveryScheduleRow>(
+            """
+            SELECT
+                Id, TRIM(IFNULL(domain, '')) AS Domain, IFNULL(icdsid, 0) AS Icdsid,
+                TRIM(IFNULL(dsnum, '')) AS Dsnum, TRIM(IFNULL(status, '')) AS Status,
+                TRIM(IFNULL(itemnum, '')) AS Itemnum, TRIM(IFNULL(um, '')) AS Um,
+                TRIM(IFNULL(purgroup, '')) AS Purgroup,
+                TRIM(IFNULL(suppliercode, '')) AS Suppliercode, TRIM(IFNULL(supplier, '')) AS Supplier,
+                submitdate, requestdate, needdate,
+                TRIM(IFNULL(ponumber, '')) AS Ponumber, IFNULL(poline, 0) AS Poline,
+                IFNULL(schedqty, 0) AS Schedqty,
+                TRIM(IFNULL(remarks, '')) AS Remarks, IFNULL(isactive, 1) AS Isactive,
+                createuser, createtime, updateuser, updatetime
+            FROM srm_polist_ds
+            WHERE tenant_id = @TenantId AND ponumber = @PurOrd AND IFNULL(isactive, 1) = 1
+            ORDER BY poline
+            """,
+            new SugarParameter("@TenantId", tenantId),
+            new SugarParameter("@PurOrd", purOrd));
+
+        foreach (var ds in rows)
+            await EnqueueScheduleRowAsync(tenantId, ds, result, ct);
+    }
+
+    private async Task EnqueueScheduleRowAsync(
+        long tenantId, DeliveryScheduleRow ds, PushResult result, CancellationToken ct)
+    {
+        var domain = string.IsNullOrWhiteSpace(ds.Domain) ? "8010" : ds.Domain;
+        var now = DateTime.Now;
+
+        var keys = new Dictionary<string, object?>
+        {
+            ["domain"] = domain,
+            ["dsnum"] = ds.Dsnum
+        };
+        var insert = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
+        {
+            ["Id"] = ds.Id,
+            ["domain"] = domain,
+            ["icdsid"] = ds.Icdsid,
+            ["dsnum"] = Trunc(ds.Dsnum, 128),
+            ["status"] = Trunc(ds.Status, 10),
+            ["itemnum"] = Trunc(ds.Itemnum, 128),
+            ["um"] = Trunc(ds.Um, 124),
+            ["purgroup"] = Trunc(ds.Purgroup, 50),
+            ["suppliercode"] = Trunc(ds.Suppliercode, 50),
+            ["supplier"] = Trunc(ds.Supplier, 50),
+            ["ponumber"] = Trunc(ds.Ponumber, 50),
+            ["poline"] = ds.Poline,
+            ["schedqty"] = ds.Schedqty,
+            // 收货累计列建行给 0 / 全额待交,之后归 WMS
+            ["lastsentqty"] = 0m,
+            ["sentqty"] = 0m,
+            ["restqty"] = ds.Schedqty,
+            ["remarks"] = Trunc(ds.Remarks, 500),
+            ["isactive"] = ds.Isactive,
+            ["createuser"] = Trunc(ds.Createuser ?? "aidop", 24),
+            ["updateuser"] = Trunc(ds.Updateuser ?? ds.Createuser ?? "aidop", 24),
+            ["createtime"] = Fmt(ds.Createtime ?? now),
+            ["updatetime"] = Fmt(ds.Updatetime ?? now)
+        };
+        if (ds.Submitdate != null) insert["submitdate"] = Fmt(ds.Submitdate);
+        if (ds.Requestdate != null) insert["requestdate"] = Fmt(ds.Requestdate);
+        if (ds.Needdate != null) insert["needdate"] = Fmt(ds.Needdate);
+
+        // sentqty / restqty / lastsentdate 归 WMS,更新时一律不碰
+        var update = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
+        {
+            ["status"] = Trunc(ds.Status, 10),
+            ["schedqty"] = ds.Schedqty,
+            ["isactive"] = ds.Isactive,
+            ["remarks"] = Trunc(ds.Remarks, 500),
+            ["updateuser"] = Trunc(ds.Updateuser ?? "aidop", 24),
+            ["updatetime"] = Fmt(now)
+        };
+        if (ds.Needdate != null) update["needdate"] = Fmt(ds.Needdate);
+        if (ds.Submitdate != null) update["submitdate"] = Fmt(ds.Submitdate);
+
+        if (await EnqueueRowAsync(tenantId, $"po|{domain}|ds|{ds.Dsnum}",
+                ActionDs, "srm_polist_ds", keys, insert, update, null, ct))
+            result.Enqueued++;
+        result.ScheduleCount++;
+    }
+
+    // ── 送货单 + 明细 + 装箱标签 ───────────────────────────────────────
+
+    private async Task EnqueueShipmentChainAsync(
+        long tenantId, string shddh, PushResult result, CancellationToken ct)
+    {
+        var masters = await _db.Ado.SqlQueryAsync<ShipmentRow>(
+            """
+            SELECT
+                id AS Id,
+                IFNULL(sh_purchase_id, 0) AS ShPurchaseId,
+                TRIM(IFNULL(sh_purchase_name, '')) AS ShPurchaseName,
+                TRIM(IFNULL(sh_purchase_num, '')) AS ShPurchaseNum,
+                TRIM(IFNULL(sh_purchase_address, '')) AS ShPurchaseAddress,
+                TRIM(IFNULL(sh_purchase_lxr, '')) AS ShPurchaseLxr,
+                TRIM(IFNULL(sh_purchase_phone, '')) AS ShPurchasePhone,
+                TRIM(IFNULL(delivery_Address, '')) AS DeliveryAddress,
+                TRIM(IFNULL(expected_consignee, '')) AS ExpectedConsignee,
+                TRIM(IFNULL(consignee_phone, '')) AS ConsigneePhone,
+                estimated_delivery_date AS EstimatedDeliveryDate,
+                TRIM(IFNULL(po_billno, '')) AS PoBillno,
+                TRIM(IFNULL(shddh, '')) AS Shddh,
+                TRIM(IFNULL(jhshrq, '')) AS Jhshrq,
+                TRIM(IFNULL(tjrid, '')) AS Tjrid,
+                TRIM(IFNULL(tjrxm, '')) AS Tjrxm,
+                TRIM(IFNULL(tjrq, '')) AS Tjrq,
+                IFNULL(scbq, 0) AS Scbq,
+                TRIM(IFNULL(chbg, '')) AS Chbg,
+                IFNULL(sfpc, 0) AS Sfpc,
+                TRIM(IFNULL(pcsm, '')) AS Pcsm,
+                TRIM(IFNULL(wlsc, '')) AS Wlsc,
+                TRIM(IFNULL(yjdhrq, '')) AS Yjdhrq,
+                IFNULL(state, 0) AS State,
+                TRIM(IFNULL(shzt, '')) AS Shzt,
+                TRIM(IFNULL(wldh, '')) AS Wldh,
+                IFNULL(dycs, 0) AS Dycs
+            FROM scm_shd
+            WHERE tenant_id = @TenantId AND shddh = @Shddh
+            LIMIT 1
+            """,
+            new SugarParameter("@TenantId", tenantId),
+            new SugarParameter("@Shddh", shddh));
+        if (masters.Count == 0)
+            throw Oops.Oh($"本库未找到送货单 {shddh}");
+        var s = masters[0];
+
+        var lines = await _db.Ado.SqlQueryAsync<ShipmentLineRow>(
+            """
+            SELECT
+                id AS Id, TRIM(IFNULL(glid, '')) AS Glid,
+                TRIM(IFNULL(sh_material_code, '')) AS ShMaterialCode,
+                TRIM(IFNULL(sh_material_name, '')) AS ShMaterialName,
+                TRIM(IFNULL(sh_material_ggxh, '')) AS ShMaterialGgxh,
+                IFNULL(sh_delivery_quantity, 0) AS ShDeliveryQuantity,
+                TRIM(IFNULL(sh_material_dw, '')) AS ShMaterialDw,
+                TRIM(IFNULL(remarks, '')) AS Remarks,
+                IFNULL(bzsl, 0) AS Bzsl, IFNULL(bqsl, 0) AS Bqsl,
+                TRIM(IFNULL(order_type, '')) AS OrderType,
+                TRIM(IFNULL(po_bill, '')) AS PoBill, TRIM(IFNULL(po_billline, '')) AS PoBillline,
+                IFNULL(hh, 0) AS Hh,
+                TRIM(IFNULL(scrq, '')) AS Scrq, TRIM(IFNULL(scph, '')) AS Scph,
+                TRIM(IFNULL(th, '')) AS Th, TRIM(IFNULL(bbh, '')) AS Bbh,
+                IFNULL(djsl, 0) AS Djsl,
+                TRIM(IFNULL(ccrq, '')) AS Ccrq, TRIM(IFNULL(cgyt, '')) AS Cgyt,
+                TRIM(IFNULL(jybb, '')) AS Jybb,
+                TRIM(IFNULL(jhdbh, '')) AS Jhdbh, TRIM(IFNULL(jhdhh, '')) AS Jhdhh,
+                TRIM(IFNULL(shpc, '')) AS Shpc, TRIM(IFNULL(shzt, '')) AS Shzt,
+                IFNULL(rksl, 0) AS Rksl, IFNULL(thsl, 0) AS Thsl
+            FROM scm_shdzb
+            WHERE glid = @Glid
+            ORDER BY hh, id
+            """,
+            new SugarParameter("@Glid", s.Id.ToString()));
+
+        var labels = await _db.Ado.SqlQueryAsync<ShipmentLabelRow>(
+            """
+            SELECT
+                id AS Id, TRIM(IFNULL(xh, '')) AS Xh, TRIM(IFNULL(wlbm, '')) AS Wlbm,
+                TRIM(IFNULL(scph, '')) AS Scph, TRIM(IFNULL(shdh, '')) AS Shdh,
+                TRIM(IFNULL(shpc, '')) AS Shpc, TRIM(IFNULL(gysbm, '')) AS Gysbm,
+                TRIM(IFNULL(csrq, '')) AS Csrq
+            FROM scm_shdshph
+            WHERE shdh = @Shddh
+            ORDER BY id
+            """,
+            new SugarParameter("@Shddh", shddh));
+
+        // 先补上游:送货明细引用的采购单与交货计划,否则 WMS 查不到可收行
+        var purOrds = lines.Select(x => (x.PoBill ?? "").Trim())
+            .Where(x => x.Length > 0)
+            .Distinct(StringComparer.OrdinalIgnoreCase)
+            .ToList();
+        foreach (var po in purOrds)
+        {
+            await EnqueuePurchaseOrderAsync(tenantId, po, result, ct);
+            await EnqueueSchedulesByPurOrdAsync(tenantId, po, result, ct);
+        }
+
+        var now = DateTime.Now;
+        var sInsert = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
+        {
+            ["id"] = s.Id,
+            ["sh_purchase_id"] = s.ShPurchaseId,
+            ["sh_purchase_name"] = Trunc(s.ShPurchaseName, 255),
+            ["sh_purchase_num"] = Trunc(s.ShPurchaseNum, 255),
+            ["sh_purchase_address"] = Trunc(s.ShPurchaseAddress, 255),
+            ["sh_purchase_lxr"] = Trunc(s.ShPurchaseLxr, 255),
+            ["sh_purchase_phone"] = Trunc(s.ShPurchasePhone, 255),
+            ["delivery_Address"] = Trunc(s.DeliveryAddress, 255),
+            ["expected_consignee"] = Trunc(s.ExpectedConsignee, 255),
+            ["consignee_phone"] = Trunc(s.ConsigneePhone, 255),
+            ["po_billno"] = Trunc(s.PoBillno, 255),
+            ["shddh"] = Trunc(s.Shddh, 255),
+            ["jhshrq"] = Trunc(s.Jhshrq, 50),
+            ["tjrid"] = Trunc(s.Tjrid, 50),
+            ["tjrxm"] = Trunc(s.Tjrxm, 50),
+            ["tjrq"] = Trunc(s.Tjrq, 50),
+            ["scbq"] = s.Scbq,
+            ["chbg"] = Trunc(s.Chbg, 255),
+            ["sfpc"] = s.Sfpc,
+            ["pcsm"] = Trunc(s.Pcsm, 255),
+            ["wlsc"] = Trunc(s.Wlsc, 500),
+            ["yjdhrq"] = Trunc(s.Yjdhrq, 500),
+            ["state"] = s.State,
+            ["shzt"] = Trunc(s.Shzt, 50),
+            ["wldh"] = Trunc(s.Wldh, 50),
+            ["dycs"] = s.Dycs
+        };
+        if (s.EstimatedDeliveryDate != null) sInsert["estimated_delivery_date"] = Fmt(s.EstimatedDeliveryDate);
+
+        // shzt / state 收货开始后归 WMS('收货中'),更新时不回写覆盖
+        var sUpdate = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
+        {
+            ["sh_purchase_name"] = Trunc(s.ShPurchaseName, 255),
+            ["sh_purchase_num"] = Trunc(s.ShPurchaseNum, 255),
+            ["delivery_Address"] = Trunc(s.DeliveryAddress, 255),
+            ["expected_consignee"] = Trunc(s.ExpectedConsignee, 255),
+            ["consignee_phone"] = Trunc(s.ConsigneePhone, 255),
+            ["po_billno"] = Trunc(s.PoBillno, 255),
+            ["jhshrq"] = Trunc(s.Jhshrq, 50),
+            ["wlsc"] = Trunc(s.Wlsc, 500),
+            ["yjdhrq"] = Trunc(s.Yjdhrq, 500)
+        };
+        if (s.EstimatedDeliveryDate != null) sUpdate["estimated_delivery_date"] = Fmt(s.EstimatedDeliveryDate);
+
+        if (await EnqueueRowAsync(tenantId, $"po|shd|{shddh}", ActionShd, "scm_shd",
+                new Dictionary<string, object?> { ["id"] = s.Id }, sInsert, sUpdate, null, ct))
+            result.Enqueued++;
+        result.ShipmentCount++;
+
+        foreach (var l in lines)
+        {
+            var lInsert = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
+            {
+                ["id"] = l.Id,
+                ["glid"] = Trunc(l.Glid, 255),
+                ["sh_material_code"] = Trunc(l.ShMaterialCode, 255),
+                ["sh_material_name"] = Trunc(l.ShMaterialName, 255),
+                ["sh_material_ggxh"] = Trunc(l.ShMaterialGgxh, 255),
+                ["sh_delivery_quantity"] = l.ShDeliveryQuantity,
+                ["sh_material_dw"] = Trunc(l.ShMaterialDw, 255),
+                ["remarks"] = Trunc(l.Remarks, 255),
+                ["bzsl"] = l.Bzsl,
+                ["bqsl"] = l.Bqsl,
+                ["order_type"] = Trunc(l.OrderType, 255),
+                ["po_bill"] = Trunc(l.PoBill, 255),
+                ["po_billline"] = Trunc(l.PoBillline, 50),
+                ["hh"] = l.Hh,
+                ["scrq"] = Trunc(l.Scrq, 255),
+                ["scph"] = Trunc(l.Scph, 255),
+                ["th"] = Trunc(l.Th, 255),
+                ["bbh"] = Trunc(l.Bbh, 255),
+                ["djsl"] = l.Djsl,
+                ["ccrq"] = Trunc(l.Ccrq, 255),
+                ["cgyt"] = Trunc(l.Cgyt, 255),
+                ["jybb"] = Trunc(l.Jybb, 255),
+                ["jhdbh"] = Trunc(l.Jhdbh, 50),
+                ["jhdhh"] = Trunc(l.Jhdhh, 50),
+                ["shpc"] = Trunc(l.Shpc, 50),
+                ["shzt"] = Trunc(l.Shzt, 255),
+                // 入库 / 退货数量归 WMS
+                ["rksl"] = 0m,
+                ["thsl"] = 0m
+            };
+            var lUpdate = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
+            {
+                ["sh_material_code"] = Trunc(l.ShMaterialCode, 255),
+                ["sh_material_name"] = Trunc(l.ShMaterialName, 255),
+                ["sh_material_ggxh"] = Trunc(l.ShMaterialGgxh, 255),
+                ["sh_delivery_quantity"] = l.ShDeliveryQuantity,
+                ["sh_material_dw"] = Trunc(l.ShMaterialDw, 255),
+                ["bzsl"] = l.Bzsl,
+                ["bqsl"] = l.Bqsl,
+                ["po_bill"] = Trunc(l.PoBill, 255),
+                ["po_billline"] = Trunc(l.PoBillline, 50),
+                ["jhdbh"] = Trunc(l.Jhdbh, 50),
+                ["jhdhh"] = Trunc(l.Jhdhh, 50),
+                ["shpc"] = Trunc(l.Shpc, 50)
+            };
+            if (await EnqueueRowAsync(tenantId, $"po|shdzb|{shddh}|{l.Id}", ActionShdzb, "scm_shdzb",
+                    new Dictionary<string, object?> { ["id"] = l.Id }, lInsert, lUpdate, null, ct))
+                result.Enqueued++;
+        }
+
+        foreach (var lb in labels)
+        {
+            // 165 的 scm_shdshph.id 是 IDENTITY,只能按 (shdh, xh) 自然键幂等
+            var bKeys = new Dictionary<string, object?>
+            {
+                ["shdh"] = Trunc(lb.Shdh, 500),
+                ["xh"] = Trunc(lb.Xh, 50)
+            };
+            var bInsert = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
+            {
+                ["xh"] = Trunc(lb.Xh, 50),
+                ["wlbm"] = Trunc(lb.Wlbm, 500),
+                ["scph"] = Trunc(lb.Scph, 500),
+                ["shdh"] = Trunc(lb.Shdh, 500),
+                ["shpc"] = Trunc(lb.Shpc, 500),
+                ["gysbm"] = Trunc(lb.Gysbm, 500),
+                ["csrq"] = Trunc(lb.Csrq, 50)
+            };
+            if (await EnqueueRowAsync(tenantId, $"po|shph|{shddh}|{lb.Xh}", ActionShph, "scm_shdshph",
+                    bKeys, bInsert, bInsert, null, ct))
+                result.Enqueued++;
+            result.LabelCount++;
+        }
+
+        await EnqueueBarcodesAsync(tenantId, shddh, result, ct);
+    }
+
+    /// <summary>
+    /// 箱码入 165 MissedPrint:WMS 扫码就是拿箱码查这张表(pr_WMS_GetBarCodes),
+    /// 查不到即报「该标签不存在」。唯一索引 IX_MissedPrint=(Domain,BarCode),RecID 是 IDENTITY 不推。
+    /// </summary>
+    private async Task EnqueueBarcodesAsync(
+        long tenantId, string shddh, PushResult result, CancellationToken ct)
+    {
+        var rows = await _db.Ado.SqlQueryAsync<BarcodeRow>(
+            """
+            SELECT
+                TRIM(IFNULL(`Domain`, '')) AS Domain,
+                TRIM(IFNULL(Site, '')) AS Site,
+                TRIM(IFNULL(BarCode, '')) AS BarCode,
+                TRIM(IFNULL(ItemNum, '')) AS ItemNum,
+                TRIM(IFNULL(Descr, '')) AS Descr,
+                TRIM(IFNULL(Product, '')) AS Product,
+                TRIM(IFNULL(Carton, '')) AS Carton,
+                TRIM(IFNULL(OrdNbr, '')) AS OrdNbr,
+                IFNULL(PackingQty, 0) AS PackingQty,
+                IFNULL(Qty, 0) AS Qty,
+                TRIM(IFNULL(Status, '')) AS Status,
+                TRIM(IFNULL(Supply, '')) AS Supply,
+                TRIM(IFNULL(LotSerial, '')) AS LotSerial,
+                IFNULL(CartonQty, 1) AS CartonQty,
+                TRIM(IFNULL(SuppLotSerial, '')) AS SuppLotSerial,
+                TRIM(IFNULL(ShipperNbr, '')) AS ShipperNbr,
+                IFNULL(ShipperLine, 0) AS ShipperLine,
+                ProdDate, ExpireDate,
+                TRIM(IFNULL(PurOrd, '')) AS PurOrd,
+                IFNULL(PurLine, 0) AS PurLine,
+                IFNULL(PurQty, 0) AS PurQty,
+                TRIM(IFNULL(LabelFormat, '')) AS LabelFormat,
+                TRIM(IFNULL(StandItem, '')) AS StandItem,
+                TRIM(IFNULL(EffSize, '')) AS EffSize,
+                IFNULL(GP12CheckedQty, 0) AS GP12CheckedQty,
+                IFNULL(NetWeight, 0) AS NetWeight,
+                TRIM(IFNULL(Remark, '')) AS Remark,
+                TRIM(IFNULL(LevelChar, '')) AS LevelChar,
+                TRIM(IFNULL(PurOrdDetBatchNbr, '')) AS PurOrdDetBatchNbr,
+                TRIM(IFNULL(FirmString5, '')) AS FirmString5,
+                CreateUser, CreateTime, UpdateUser, UpdateTime
+            FROM MissedPrint
+            WHERE ShipperNbr = @Shddh
+              AND IFNULL(Status, '') = 'U'
+              AND IFNULL(PurOrd, '') NOT LIKE '作废%'
+            ORDER BY ShipperLine, Carton
+            """,
+            new SugarParameter("@Shddh", shddh));
+
+        var now = DateTime.Now;
+        foreach (var b in rows)
+        {
+            var domain = string.IsNullOrWhiteSpace(b.Domain) ? "8010" : b.Domain;
+            var keys = new Dictionary<string, object?>
+            {
+                ["Domain"] = domain,
+                ["BarCode"] = b.BarCode
+            };
+            var insert = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
+            {
+                ["Domain"] = domain,
+                ["Site"] = Trunc(b.Site, 24),
+                ["BarCode"] = Trunc(b.BarCode, 250),
+                ["ItemNum"] = Trunc(b.ItemNum, 60),
+                ["Descr"] = Trunc(b.Descr, 255),
+                ["Product"] = Trunc(b.Product, 255),
+                ["Carton"] = Trunc(b.Carton, 20),
+                ["OrdNbr"] = Trunc(b.OrdNbr, 48),
+                ["PackingQty"] = b.PackingQty,
+                ["Qty"] = b.Qty,
+                // 标签状态归 WMS:建行给待收货 U,收货后由 WMS 改,更新时不回写
+                ["Status"] = "U",
+                ["RelatedBarCode"] = "",
+                ["Supply"] = Trunc(b.Supply, 40),
+                ["LotSerial"] = Trunc(b.LotSerial, 120),
+                ["CartonQty"] = b.CartonQty,
+                ["SuppLotSerial"] = Trunc(b.SuppLotSerial, 120),
+                ["ShipperNbr"] = Trunc(b.ShipperNbr, 60),
+                ["ShipperLine"] = b.ShipperLine,
+                ["PurOrd"] = Trunc(b.PurOrd, 48),
+                ["PurLine"] = b.PurLine,
+                ["PurQty"] = b.PurQty,
+                ["LabelFormat"] = Trunc(b.LabelFormat, 20),
+                ["StandItem"] = Trunc(b.StandItem, 60),
+                ["EffSize"] = Trunc(b.EffSize, 20),
+                ["GP12CheckedQty"] = b.GP12CheckedQty,
+                ["NetWeight"] = b.NetWeight,
+                ["Remark"] = Trunc(b.Remark, 200),
+                ["LevelChar"] = Trunc(b.LevelChar, 20),
+                ["PurOrdDetBatchNbr"] = Trunc(b.PurOrdDetBatchNbr, 48),
+                ["FirmString5"] = Trunc(b.FirmString5, 48),
+                ["CreateUser"] = Trunc(b.CreateUser ?? "aidop", 24),
+                ["UpdateUser"] = Trunc(b.UpdateUser ?? b.CreateUser ?? "aidop", 24),
+                ["CreateTime"] = Fmt(b.CreateTime ?? now),
+                ["UpdateTime"] = Fmt(b.UpdateTime ?? now)
+            };
+            if (b.ProdDate != null) insert["ProdDate"] = Fmt(b.ProdDate);
+            if (b.ExpireDate != null) insert["ExpireDate"] = Fmt(b.ExpireDate);
+
+            var update = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
+            {
+                ["ItemNum"] = Trunc(b.ItemNum, 60),
+                ["Descr"] = Trunc(b.Descr, 255),
+                ["Product"] = Trunc(b.Product, 255),
+                ["PackingQty"] = b.PackingQty,
+                ["Qty"] = b.Qty,
+                ["LotSerial"] = Trunc(b.LotSerial, 120),
+                ["SuppLotSerial"] = Trunc(b.SuppLotSerial, 120),
+                ["ShipperNbr"] = Trunc(b.ShipperNbr, 60),
+                ["ShipperLine"] = b.ShipperLine,
+                ["PurOrd"] = Trunc(b.PurOrd, 48),
+                ["PurLine"] = b.PurLine,
+                ["PurQty"] = b.PurQty,
+                ["StandItem"] = Trunc(b.StandItem, 60),
+                ["EffSize"] = Trunc(b.EffSize, 20),
+                ["LevelChar"] = Trunc(b.LevelChar, 20),
+                ["PurOrdDetBatchNbr"] = Trunc(b.PurOrdDetBatchNbr, 48),
+                ["Remark"] = Trunc(b.Remark, 200),
+                ["UpdateUser"] = Trunc(b.UpdateUser ?? "aidop", 24),
+                ["UpdateTime"] = Fmt(now)
+            };
+            if (b.ProdDate != null) update["ProdDate"] = Fmt(b.ProdDate);
+
+            if (await EnqueueRowAsync(tenantId, $"po|mp|{domain}|{b.BarCode}", ActionMp, "MissedPrint",
+                    keys, insert, update, null, ct))
+                result.Enqueued++;
+            result.BarcodeCount++;
+        }
+    }
+
+    // ── 基础设施 ──────────────────────────────────────────────────────
+
+    /// <summary>自建单口径:SAP 下发单不回推 165(决策:SAP 维持现状)。</summary>
+    private static bool IsSelfBuilt(string? potype, string? reqBy)
+    {
+        var p = (potype ?? "").Trim();
+        var r = (reqBy ?? "").Trim();
+        return string.Equals(p, "po", StringComparison.OrdinalIgnoreCase)
+               && (string.Equals(r, "PO", StringComparison.OrdinalIgnoreCase)
+                   || string.Equals(r, "DO", StringComparison.OrdinalIgnoreCase));
+    }
+
+    private async Task<bool> EnqueueRowAsync(
+        long tenantId,
+        string idem,
+        string action,
+        string table,
+        Dictionary<string, object?> keys,
+        Dictionary<string, object?> insert,
+        Dictionary<string, object?> update,
+        Dictionary<string, object?>? resolve,
+        CancellationToken ct)
+    {
+        var payload = new Dictionary<string, object?>
+        {
+            ["op"] = "UPSERT",
+            ["table"] = table,
+            ["keys"] = keys,
+            ["insert"] = insert,
+            ["update"] = update,
+            ["expect"] = new Dictionary<string, object?>()
+        };
+        if (resolve != null && resolve.Count > 0) payload["resolve"] = resolve;
+
+        var item = new MdpOutbox
+        {
+            TenantId = tenantId,
+            TargetSourceCode = TargetSource,
+            ActionCode = action,
+            IdemKey = idem.Length > 200 ? idem[..200] : idem,
+            PayloadJson = JsonSerializer.Serialize(payload)
+        };
+        return await _enqueue.TryEnqueueOrRefreshAsync(item, ct, pulse: false);
+    }
+
+    private static string Fmt(DateTime? dt) => (dt ?? DateTime.Now).ToString("yyyy-MM-dd HH:mm:ss");
+
+    private static string Trunc(string? s, int max)
+    {
+        if (string.IsNullOrEmpty(s)) return "";
+        return s.Length <= max ? s : s[..max];
+    }
+
+    private sealed class PurOrdMasterRow
+    {
+        public string? Domain { get; set; }
+        public string? PurOrd { get; set; }
+        public string? Potype { get; set; }
+        public string? Typed { get; set; }
+        public string? Supp { get; set; }
+        public string? Buyer { get; set; }
+        public string? ReqBy { get; set; }
+        public string? UsageText { get; set; }
+        public string? FSTID { get; set; }
+        public string? Remark { get; set; }
+        public DateTime? OrdDate { get; set; }
+        public DateTime? DueDate { get; set; }
+        public int IsActive { get; set; }
+        public int IsConfirm { get; set; }
+        public string? CreateUser { get; set; }
+        public DateTime? CreateTime { get; set; }
+        public string? UpdateUser { get; set; }
+        public DateTime? UpdateTime { get; set; }
+    }
+
+    private sealed class PurOrdDetailRow
+    {
+        public string? Domain { get; set; }
+        public int Line { get; set; }
+        public string? ItemNum { get; set; }
+        public string? Descr { get; set; }
+        public string? UM { get; set; }
+        public string? Rev { get; set; }
+        public string? Drawing { get; set; }
+        public string? Location { get; set; }
+        public string? LotSerial { get; set; }
+        public string? Req { get; set; }
+        public decimal QtyOrded { get; set; }
+        public DateTime? DueDate { get; set; }
+        public DateTime? NeedDate { get; set; }
+        public int IsActive { get; set; }
+        public int IsConfirm { get; set; }
+        public string? CreateUser { get; set; }
+        public DateTime? CreateTime { get; set; }
+        public string? UpdateUser { get; set; }
+        public DateTime? UpdateTime { get; set; }
+    }
+
+    private sealed class DeliveryScheduleRow
+    {
+        public long Id { get; set; }
+        public string? Domain { get; set; }
+        public long Icdsid { get; set; }
+        public string? Dsnum { get; set; }
+        public string? Status { get; set; }
+        public string? Itemnum { get; set; }
+        public string? Um { get; set; }
+        public string? Purgroup { get; set; }
+        public string? Suppliercode { get; set; }
+        public string? Supplier { get; set; }
+        public DateTime? Submitdate { get; set; }
+        public DateTime? Requestdate { get; set; }
+        public DateTime? Needdate { get; set; }
+        public string? Ponumber { get; set; }
+        public int Poline { get; set; }
+        public decimal Schedqty { get; set; }
+        public string? Remarks { get; set; }
+        public int Isactive { get; set; }
+        public string? Createuser { get; set; }
+        public DateTime? Createtime { get; set; }
+        public string? Updateuser { get; set; }
+        public DateTime? Updatetime { get; set; }
+    }
+
+    private sealed class ShipmentRow
+    {
+        public long Id { get; set; }
+        public long ShPurchaseId { get; set; }
+        public string? ShPurchaseName { get; set; }
+        public string? ShPurchaseNum { get; set; }
+        public string? ShPurchaseAddress { get; set; }
+        public string? ShPurchaseLxr { get; set; }
+        public string? ShPurchasePhone { get; set; }
+        public string? DeliveryAddress { get; set; }
+        public string? ExpectedConsignee { get; set; }
+        public string? ConsigneePhone { get; set; }
+        public DateTime? EstimatedDeliveryDate { get; set; }
+        public string? PoBillno { get; set; }
+        public string? Shddh { get; set; }
+        public string? Jhshrq { get; set; }
+        public string? Tjrid { get; set; }
+        public string? Tjrxm { get; set; }
+        public string? Tjrq { get; set; }
+        public int Scbq { get; set; }
+        public string? Chbg { get; set; }
+        public int Sfpc { get; set; }
+        public string? Pcsm { get; set; }
+        public string? Wlsc { get; set; }
+        public string? Yjdhrq { get; set; }
+        public int State { get; set; }
+        public string? Shzt { get; set; }
+        public string? Wldh { get; set; }
+        public int Dycs { get; set; }
+    }
+
+    private sealed class ShipmentLineRow
+    {
+        public long Id { get; set; }
+        public string? Glid { get; set; }
+        public string? ShMaterialCode { get; set; }
+        public string? ShMaterialName { get; set; }
+        public string? ShMaterialGgxh { get; set; }
+        public decimal ShDeliveryQuantity { get; set; }
+        public string? ShMaterialDw { get; set; }
+        public string? Remarks { get; set; }
+        public decimal Bzsl { get; set; }
+        public decimal Bqsl { get; set; }
+        public string? OrderType { get; set; }
+        public string? PoBill { get; set; }
+        public string? PoBillline { get; set; }
+        public int Hh { get; set; }
+        public string? Scrq { get; set; }
+        public string? Scph { get; set; }
+        public string? Th { get; set; }
+        public string? Bbh { get; set; }
+        public decimal Djsl { get; set; }
+        public string? Ccrq { get; set; }
+        public string? Cgyt { get; set; }
+        public string? Jybb { get; set; }
+        public string? Jhdbh { get; set; }
+        public string? Jhdhh { get; set; }
+        public string? Shpc { get; set; }
+        public string? Shzt { get; set; }
+        public decimal Rksl { get; set; }
+        public decimal Thsl { get; set; }
+    }
+
+    private sealed class BarcodeRow
+    {
+        public string? Domain { get; set; }
+        public string? Site { get; set; }
+        public string? BarCode { get; set; }
+        public string? ItemNum { get; set; }
+        public string? Descr { get; set; }
+        public string? Product { get; set; }
+        public string? Carton { get; set; }
+        public string? OrdNbr { get; set; }
+        public decimal PackingQty { get; set; }
+        public decimal Qty { get; set; }
+        public string? Status { get; set; }
+        public string? Supply { get; set; }
+        public string? LotSerial { get; set; }
+        public int CartonQty { get; set; }
+        public string? SuppLotSerial { get; set; }
+        public string? ShipperNbr { get; set; }
+        public int ShipperLine { get; set; }
+        public DateTime? ProdDate { get; set; }
+        public DateTime? ExpireDate { get; set; }
+        public string? PurOrd { get; set; }
+        public int PurLine { get; set; }
+        public decimal PurQty { get; set; }
+        public string? LabelFormat { get; set; }
+        public string? StandItem { get; set; }
+        public string? EffSize { get; set; }
+        public decimal GP12CheckedQty { get; set; }
+        public decimal NetWeight { get; set; }
+        public string? Remark { get; set; }
+        public string? LevelChar { get; set; }
+        public string? PurOrdDetBatchNbr { get; set; }
+        public string? FirmString5 { get; set; }
+        public string? CreateUser { get; set; }
+        public DateTime? CreateTime { get; set; }
+        public string? UpdateUser { get; set; }
+        public DateTime? UpdateTime { get; set; }
+    }
+
+    private sealed class ShipmentLabelRow
+    {
+        public long Id { get; set; }
+        public string? Xh { get; set; }
+        public string? Wlbm { get; set; }
+        public string? Scph { get; set; }
+        public string? Shdh { get; set; }
+        public string? Shpc { get; set; }
+        public string? Gysbm { get; set; }
+        public string? Csrq { get; set; }
+    }
+}

+ 11 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/SupplierShipmentService.cs

@@ -17,6 +17,7 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
     private readonly SqlSugarRepository<ScmShd> _masterRep;
     private readonly SqlSugarRepository<ScmShd> _masterRep;
     private readonly SqlSugarRepository<ScmShdzb> _detailRep;
     private readonly SqlSugarRepository<ScmShdzb> _detailRep;
     private readonly UserManager _userManager;
     private readonly UserManager _userManager;
+    private readonly DataPlatform.Wms.PurOrdWmsPushService _purOrdWmsPush;
     private const int ShpcSerialWidth = 3; // yyMMdd + 3位流水:260508001
     private const int ShpcSerialWidth = 3; // yyMMdd + 3位流水:260508001
     private const int LabelXhStart = 10001;
     private const int LabelXhStart = 10001;
 
 
@@ -24,12 +25,14 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
         ISqlSugarClient db,
         ISqlSugarClient db,
         SqlSugarRepository<ScmShd> masterRep,
         SqlSugarRepository<ScmShd> masterRep,
         SqlSugarRepository<ScmShdzb> detailRep,
         SqlSugarRepository<ScmShdzb> detailRep,
-        UserManager userManager)
+        UserManager userManager,
+        DataPlatform.Wms.PurOrdWmsPushService purOrdWmsPush)
     {
     {
         _db = db;
         _db = db;
         _masterRep = masterRep;
         _masterRep = masterRep;
         _detailRep = detailRep;
         _detailRep = detailRep;
         _userManager = userManager;
         _userManager = userManager;
+        _purOrdWmsPush = purOrdWmsPush;
     }
     }
 
 
     [DisplayName("供应商发货单列表")]
     [DisplayName("供应商发货单列表")]
@@ -457,7 +460,7 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
                         b.bqsl AS bqsl,
                         b.bqsl AS bqsl,
                         b.order_type AS ddlx,
                         b.order_type AS ddlx,
                         b.po_bill AS ddh,
                         b.po_bill AS ddh,
-                        CAST(IFNULL(b.po_billline, '0') AS SIGNED) AS po_billline,
+                        CAST(IFNULL(b.po_billline, '0') AS SIGNED) AS PoBillLine,
                         b.hh AS hh,
                         b.hh AS hh,
                         b.scrq AS scrq,
                         b.scrq AS scrq,
                         b.scph AS scph,
                         b.scph AS scph,
@@ -465,7 +468,7 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
                         IFNULL(im.Rev, pd.Rev) AS bbh,
                         IFNULL(im.Rev, pd.Rev) AS bbh,
                         a.sh_purchase_name AS gysmc,
                         a.sh_purchase_name AS gysmc,
                         a.sh_purchase_num AS gysdm,
                         a.sh_purchase_num AS gysdm,
-                        pm.`Usage` AS po_usage,
+                        pm.`Usage` AS PoUsage,
                         b.ccrq AS ccrq,
                         b.ccrq AS ccrq,
                         a.jhshrq AS jhshrq,
                         a.jhshrq AS jhshrq,
                         b.jhdbh AS jhdbh,
                         b.jhdbh AS jhdbh,
@@ -622,8 +625,12 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
                 return new { success = false, msg, message = msg };
                 return new { success = false, msg, message = msg };
             }
             }
 
 
+            // 拆库后 WMS 读 165,采购链须随标签一并回写,否则扫码收货查不到订单
+            var warnings = new List<string>();
+            await _purOrdWmsPush.TryEnqueueShipmentChainSafeAsync(tenantId, shddh, warnings);
+
             var ok = "生成标签成功!";
             var ok = "生成标签成功!";
-            return new { success = true, msg = ok, message = ok };
+            return new { success = true, msg = ok, message = ok, warnings };
         }
         }
         finally
         finally
         {
         {

+ 10 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Supply/DeliveryScheduleService.cs

@@ -23,6 +23,7 @@ public class DeliveryScheduleService : IDynamicApiController, ITransient
     private readonly S3OperationLogService _operationLogService;
     private readonly S3OperationLogService _operationLogService;
     private readonly RuleConfigService _ruleConfigService;
     private readonly RuleConfigService _ruleConfigService;
     private readonly AidopActionRunLogWriter _runLog;
     private readonly AidopActionRunLogWriter _runLog;
+    private readonly DataPlatform.Wms.PurOrdWmsPushService _purOrdWmsPush;
 
 
     public DeliveryScheduleService(
     public DeliveryScheduleService(
         ISqlSugarClient db,
         ISqlSugarClient db,
@@ -34,8 +35,10 @@ public class DeliveryScheduleService : IDynamicApiController, ITransient
         PurchaseRequestExternalPushService purchaseRequestExternalPushService,
         PurchaseRequestExternalPushService purchaseRequestExternalPushService,
         S3OperationLogService operationLogService,
         S3OperationLogService operationLogService,
         RuleConfigService ruleConfigService,
         RuleConfigService ruleConfigService,
-        AidopActionRunLogWriter runLog)
+        AidopActionRunLogWriter runLog,
+        DataPlatform.Wms.PurOrdWmsPushService purOrdWmsPush)
     {
     {
+        _purOrdWmsPush = purOrdWmsPush;
         _db = db;
         _db = db;
         _rep = rep;
         _rep = rep;
         _userManager = userManager;
         _userManager = userManager;
@@ -311,7 +314,12 @@ public class DeliveryScheduleService : IDynamicApiController, ITransient
             })
             })
             .Where(x => ids.Contains(x.Id) && x.IsActive == 1 && x.Status == "N" && x.TenantId == tenantId)
             .Where(x => ids.Contains(x.Id) && x.IsActive == 1 && x.Status == "N" && x.TenantId == tenantId)
             .ExecuteCommandAsync();
             .ExecuteCommandAsync();
-        return new { affected, message = affected > 0 ? "发布成功" : "无可发布数据" };
+
+        var warnings = new List<string>();
+        if (affected > 0)
+            await _purOrdWmsPush.TryEnqueueSchedulesSafeAsync(tenantId, ids, warnings);
+
+        return new { affected, message = affected > 0 ? "发布成功" : "无可发布数据", warnings };
     }
     }
 
 
     [DisplayName("取消发布交货单")]
     [DisplayName("取消发布交货单")]

+ 13 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Supply/PurchaseOrderTransferService.cs

@@ -8,12 +8,18 @@ public class PurchaseOrderTransferService : ITransient
     private readonly ISqlSugarClient _db;
     private readonly ISqlSugarClient _db;
     private readonly NumberRuleService _numberRuleService;
     private readonly NumberRuleService _numberRuleService;
     private readonly UserManager _userManager;
     private readonly UserManager _userManager;
+    private readonly DataPlatform.Wms.PurOrdWmsPushService _purOrdWmsPush;
 
 
-    public PurchaseOrderTransferService(ISqlSugarClient db, NumberRuleService numberRuleService, UserManager userManager)
+    public PurchaseOrderTransferService(
+        ISqlSugarClient db,
+        NumberRuleService numberRuleService,
+        UserManager userManager,
+        DataPlatform.Wms.PurOrdWmsPushService purOrdWmsPush)
     {
     {
         _db = db;
         _db = db;
         _numberRuleService = numberRuleService;
         _numberRuleService = numberRuleService;
         _userManager = userManager;
         _userManager = userManager;
+        _purOrdWmsPush = purOrdWmsPush;
     }
     }
 
 
     public async Task<PurchaseOrderTransferResult> TransferGeneratedRequireGoodsAsync(List<PurchaseRequestMain> requests, string account)
     public async Task<PurchaseOrderTransferResult> TransferGeneratedRequireGoodsAsync(List<PurchaseRequestMain> requests, string account)
@@ -25,6 +31,7 @@ public class PurchaseOrderTransferService : ITransient
         if (candidates.Count == 0) return new PurchaseOrderTransferResult();
         if (candidates.Count == 0) return new PurchaseOrderTransferResult();
 
 
         var createdOrders = new List<string>();
         var createdOrders = new List<string>();
+        var createdOrderTenants = new List<(long TenantId, string PurOrd)>();
         var transferredPrIds = new List<long>();
         var transferredPrIds = new List<long>();
         var poOccupyRehangedCount = 0;
         var poOccupyRehangedCount = 0;
         var groups = candidates
         var groups = candidates
@@ -104,8 +111,13 @@ public class PurchaseOrderTransferService : ITransient
             }
             }
 
 
             createdOrders.Add(purOrd);
             createdOrders.Add(purOrd);
+            createdOrderTenants.Add((group.Key.TenantId, purOrd));
         }
         }
 
 
+        // 拆库后 WMS 读 165,自建采购单须回写,否则收货侧查不到
+        foreach (var (poTenantId, poNbr) in createdOrderTenants)
+            await _purOrdWmsPush.TryEnqueuePurchaseOrderSafeAsync(poTenantId, poNbr);
+
         return new PurchaseOrderTransferResult
         return new PurchaseOrderTransferResult
         {
         {
             CreatedOrderCount = createdOrders.Count,
             CreatedOrderCount = createdOrders.Count,