|
@@ -129,6 +129,30 @@ public sealed class MdpHotWatchService : ITransient
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 装箱标签推入 165 后登记采购单热关注,用于回读 WMS 扫码收货结果。
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// 挂在标签环节而非建单环节:标签生成意味着即将到货,此时开始轮询窗口最短。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public async Task EnrollPurOrderAsync(
|
|
|
|
|
+ string domain, string purOrd, long tenantId = 0, CancellationToken ct = default)
|
|
|
|
|
+ {
|
|
|
|
|
+ purOrd = (purOrd ?? "").Trim();
|
|
|
|
|
+ if (string.IsNullOrWhiteSpace(purOrd)) return;
|
|
|
|
|
+ domain = string.IsNullOrWhiteSpace(domain) ? "8010" : domain.Trim();
|
|
|
|
|
+
|
|
|
|
|
+ if (tenantId <= 0)
|
|
|
|
|
+ tenantId = await ResolveLocalPurOrdTenantAsync(purOrd, domain, ct);
|
|
|
|
|
+
|
|
|
|
|
+ await EnrollAsync(
|
|
|
|
|
+ "PUR_ORDER",
|
|
|
|
|
+ purOrd,
|
|
|
|
|
+ domain,
|
|
|
|
|
+ new[] { "PurOrdMaster", "PurOrdDetail", "MissedPrint" },
|
|
|
|
|
+ tenantId,
|
|
|
|
|
+ ct: ct);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static bool TryParseWoIdem(string? idem, out string domain, out string workOrd)
|
|
private static bool TryParseWoIdem(string? idem, out string domain, out string workOrd)
|
|
|
{
|
|
{
|
|
|
domain = "8010";
|
|
domain = "8010";
|
|
@@ -246,6 +270,18 @@ public sealed class MdpHotWatchService : ITransient
|
|
|
effectiveTenantId, watch.Domain, watch.BizKey, periodDets, routings, ct);
|
|
effectiveTenantId, watch.Domain, watch.BizKey, periodDets, routings, ct);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 采购收货结果写回本库业务表,供发货单列表与齐套口径直接读取
|
|
|
|
|
+ if (string.Equals(watch.BizType, "PUR_ORDER", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
+ {
|
|
|
|
|
+ tableRows.TryGetValue("PurOrdDetail", out var purDetails);
|
|
|
|
|
+ tableRows.TryGetValue("MissedPrint", out var barcodes);
|
|
|
|
|
+ var effectiveTenantId = watch.TenantId;
|
|
|
|
|
+ if (effectiveTenantId <= 0)
|
|
|
|
|
+ effectiveTenantId = await ResolveLocalPurOrdTenantAsync(watch.BizKey, watch.Domain, ct);
|
|
|
|
|
+ await ApplyPurchaseReceiptAsync(
|
|
|
|
|
+ effectiveTenantId, watch.Domain, watch.BizKey, purDetails, barcodes, ct);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (await ShouldTerminateAsync(remote, watch, ct))
|
|
if (await ShouldTerminateAsync(remote, watch, ct))
|
|
|
{
|
|
{
|
|
|
watch.Status = 1;
|
|
watch.Status = 1;
|
|
@@ -542,6 +578,189 @@ public sealed class MdpHotWatchService : ITransient
|
|
|
workOrd, domain, grouped.Count, periodDets.Count);
|
|
workOrd, domain, grouped.Count, periodDets.Count);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 把 165 的采购收货结果回读本库:明细收货数、箱码状态,并据箱码推进送货单状态。
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// 口径(2026-08-11 定):WMS 扫码收货只入待检仓(InvTransHist 落 rct-po-ins / Loc=1000),
|
|
|
|
|
+ /// 不等于合格入库,所以这里只推进 scm_shd/scm_shdzb.shzt,不写 rksl——合格入库数留给检验环节。
|
|
|
|
|
+ /// 165 的 scm_shdzb.rksl / shzt 在收货时并不更新,送货单进度只能由箱码状态反推。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private async Task ApplyPurchaseReceiptAsync(
|
|
|
|
|
+ long tenantId,
|
|
|
|
|
+ string domain,
|
|
|
|
|
+ string purOrd,
|
|
|
|
|
+ List<Dictionary<string, object>>? purDetails,
|
|
|
|
|
+ List<Dictionary<string, object>>? barcodes,
|
|
|
|
|
+ CancellationToken ct)
|
|
|
|
|
+ {
|
|
|
|
|
+ var now = DateTime.Now;
|
|
|
|
|
+ var updatedLines = 0;
|
|
|
|
|
+ var updatedBarcodes = 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (purDetails != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ foreach (var row in purDetails)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!TryGetInt(row, "Line", out var line))
|
|
|
|
|
+ continue;
|
|
|
|
|
+
|
|
|
|
|
+ // 自建单本库不填 Domain(165/MES 概念),所以空 Domain 也算命中;采购单号本库唯一
|
|
|
|
|
+ updatedLines += await _db.Ado.ExecuteCommandAsync(
|
|
|
|
|
+ """
|
|
|
|
|
+ UPDATE PurOrdDetail
|
|
|
|
|
+ SET RctQty = @RctQty,
|
|
|
|
|
+ ReceiptQty = @ReceiptQty,
|
|
|
|
|
+ QtyReturned = @QtyReturned,
|
|
|
|
|
+ UpdateUser = 'MDP_HOT',
|
|
|
|
|
+ UpdateTime = @Now
|
|
|
|
|
+ WHERE PurOrd = @PurOrd
|
|
|
|
|
+ AND Line = @Line
|
|
|
|
|
+ AND IFNULL(Domain, '') IN ('', @Domain)
|
|
|
|
|
+ AND (@TenantId = 0 OR IFNULL(tenant_id, 0) = @TenantId)
|
|
|
|
|
+ """,
|
|
|
|
|
+ new SugarParameter("@RctQty", GetDecimal(row, "RctQty")),
|
|
|
|
|
+ new SugarParameter("@ReceiptQty", GetDecimal(row, "ReceiptQty")),
|
|
|
|
|
+ new SugarParameter("@QtyReturned", GetDecimal(row, "QtyReturned")),
|
|
|
|
|
+ new SugarParameter("@Now", now),
|
|
|
|
|
+ new SugarParameter("@PurOrd", purOrd),
|
|
|
|
|
+ new SugarParameter("@Line", line),
|
|
|
|
|
+ new SugarParameter("@Domain", domain),
|
|
|
|
|
+ new SugarParameter("@TenantId", tenantId));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 本库 MissedPrint 无 tenant_id,按 Domain + BarCode 定位;作废行 BarCode 带 RecID 前缀不会误匹配
|
|
|
|
|
+ var shippers = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
+ if (barcodes != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ foreach (var row in barcodes)
|
|
|
|
|
+ {
|
|
|
|
|
+ var barCode = GetString(row, "BarCode")?.Trim();
|
|
|
|
|
+ if (string.IsNullOrWhiteSpace(barCode))
|
|
|
|
|
+ continue;
|
|
|
|
|
+
|
|
|
|
|
+ updatedBarcodes += await _db.Ado.ExecuteCommandAsync(
|
|
|
|
|
+ """
|
|
|
|
|
+ UPDATE MissedPrint
|
|
|
|
|
+ SET Status = CASE WHEN IFNULL(@Status, '') = '' THEN Status ELSE @Status END,
|
|
|
|
|
+ RctNbr = @RctNbr,
|
|
|
|
|
+ Location = @Location,
|
|
|
|
|
+ Shelf = @Shelf,
|
|
|
|
|
+ InvStatus = @InvStatus,
|
|
|
|
|
+ UpdateUser = 'MDP_HOT',
|
|
|
|
|
+ UpdateTime = @Now
|
|
|
|
|
+ WHERE BarCode = @BarCode
|
|
|
|
|
+ AND IFNULL(Domain, '') IN ('', @Domain)
|
|
|
|
|
+ """,
|
|
|
|
|
+ new SugarParameter("@Status", Trunc(GetString(row, "Status"), 4) ?? ""),
|
|
|
|
|
+ new SugarParameter("@RctNbr", Trunc(GetString(row, "RctNbr"), 48) ?? ""),
|
|
|
|
|
+ new SugarParameter("@Location", Trunc(GetString(row, "Location"), 24) ?? ""),
|
|
|
|
|
+ new SugarParameter("@Shelf", Trunc(GetString(row, "Shelf"), 40) ?? ""),
|
|
|
|
|
+ new SugarParameter("@InvStatus", Trunc(GetString(row, "InvStatus"), 24) ?? ""),
|
|
|
|
|
+ new SugarParameter("@Now", now),
|
|
|
|
|
+ new SugarParameter("@BarCode", barCode),
|
|
|
|
|
+ new SugarParameter("@Domain", domain));
|
|
|
|
|
+
|
|
|
|
|
+ var shipper = GetString(row, "ShipperNbr")?.Trim();
|
|
|
|
|
+ if (!string.IsNullOrWhiteSpace(shipper))
|
|
|
|
|
+ shippers.Add(shipper!);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var shipper in shippers)
|
|
|
|
|
+ await ApplyShipmentStatusAsync(tenantId, domain, shipper, ct);
|
|
|
|
|
+
|
|
|
|
|
+ _logger.LogInformation(
|
|
|
|
|
+ "[MdpHotWatch] applied PUR_ORDER receipt po={PurOrd} domain={Domain} lines={Lines} barcodes={Barcodes} shipments={Shipments}",
|
|
|
|
|
+ purOrd, domain, updatedLines, updatedBarcodes, shippers.Count);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 按箱码待收数推进送货单状态:全部待收=待收,全部离开待收=完成,其余=收货中。
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// 完成判据取箱码而非数量(决策:少收/超收不影响状态),列表读的是 scm_shd.shzt。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private async Task ApplyShipmentStatusAsync(
|
|
|
|
|
+ long tenantId, string domain, string shddh, CancellationToken ct)
|
|
|
|
|
+ {
|
|
|
|
|
+ var stats = await _db.Ado.SqlQueryAsync<ShipmentBarcodeStat>(
|
|
|
|
|
+ """
|
|
|
|
|
+ SELECT
|
|
|
|
|
+ COUNT(*) AS Total,
|
|
|
|
|
+ SUM(CASE WHEN IFNULL(Status, '') = 'U' THEN 1 ELSE 0 END) AS Pending
|
|
|
|
|
+ FROM MissedPrint
|
|
|
|
|
+ WHERE ShipperNbr = @Shddh
|
|
|
|
|
+ AND IFNULL(Domain, '') = @Domain
|
|
|
|
|
+ AND IFNULL(PurOrd, '') NOT LIKE '作废%'
|
|
|
|
|
+ """,
|
|
|
|
|
+ new SugarParameter("@Shddh", shddh),
|
|
|
|
|
+ new SugarParameter("@Domain", domain));
|
|
|
|
|
+
|
|
|
|
|
+ var stat = stats?.FirstOrDefault();
|
|
|
|
|
+ if (stat == null || stat.Total <= 0)
|
|
|
|
|
+ return;
|
|
|
|
|
+
|
|
|
|
|
+ var shzt = stat.Pending >= stat.Total ? "待收" : (stat.Pending == 0 ? "完成" : "收货中");
|
|
|
|
|
+
|
|
|
|
|
+ // 先定位表头再按主键更新:scm_shd.id 与 scm_shdzb.glid 排序规则不同(0900_ai_ci vs unicode_ci),
|
|
|
|
|
+ // 直接 JOIN 会报 Illegal mix of collations,改由参数传 glid 规避
|
|
|
|
|
+ var heads = await _db.Ado.SqlQueryAsync<ShipmentHeadRow>(
|
|
|
|
|
+ """
|
|
|
|
|
+ SELECT id AS Id
|
|
|
|
|
+ FROM scm_shd
|
|
|
|
|
+ WHERE shddh = @Shddh
|
|
|
|
|
+ AND (@TenantId = 0 OR IFNULL(tenant_id, 0) = @TenantId)
|
|
|
|
|
+ ORDER BY id
|
|
|
|
|
+ LIMIT 1
|
|
|
|
|
+ """,
|
|
|
|
|
+ new SugarParameter("@Shddh", shddh),
|
|
|
|
|
+ new SugarParameter("@TenantId", tenantId));
|
|
|
|
|
+
|
|
|
|
|
+ var head = heads?.FirstOrDefault();
|
|
|
|
|
+ if (head == null)
|
|
|
|
|
+ {
|
|
|
|
|
+ _logger.LogWarning(
|
|
|
|
|
+ "[MdpHotWatch] 送货单在本库缺失,跳过状态推进 shddh={Shddh} tenant={Tenant}", shddh, tenantId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
|
|
+ "UPDATE scm_shd SET shzt = @Shzt WHERE id = @Id",
|
|
|
|
|
+ new SugarParameter("@Shzt", shzt),
|
|
|
|
|
+ new SugarParameter("@Id", head.Id));
|
|
|
|
|
+
|
|
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
|
|
+ "UPDATE scm_shdzb SET shzt = @Shzt WHERE glid = @Glid",
|
|
|
|
|
+ new SugarParameter("@Shzt", shzt),
|
|
|
|
|
+ new SugarParameter("@Glid", head.Id.ToString()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private sealed class ShipmentBarcodeStat
|
|
|
|
|
+ {
|
|
|
|
|
+ public int Total { get; set; }
|
|
|
|
|
+ public int Pending { get; set; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private sealed class ShipmentHeadRow
|
|
|
|
|
+ {
|
|
|
|
|
+ public long Id { get; set; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private async Task<long> ResolveLocalPurOrdTenantAsync(string purOrd, string domain, CancellationToken ct)
|
|
|
|
|
+ {
|
|
|
|
|
+ var tid = await _db.Ado.SqlQuerySingleAsync<long?>(
|
|
|
|
|
+ """
|
|
|
|
|
+ SELECT IFNULL(tenant_id, 0)
|
|
|
|
|
+ FROM PurOrdDetail
|
|
|
|
|
+ WHERE PurOrd = @PurOrd AND IFNULL(Domain, '') IN ('', @Domain)
|
|
|
|
|
+ ORDER BY Line
|
|
|
|
|
+ LIMIT 1
|
|
|
|
|
+ """,
|
|
|
|
|
+ new SugarParameter("@PurOrd", purOrd),
|
|
|
|
|
+ new SugarParameter("@Domain", domain));
|
|
|
|
|
+ return tid ?? 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private sealed class PsdOpActual
|
|
private sealed class PsdOpActual
|
|
|
{
|
|
{
|
|
|
public int Op { get; set; }
|
|
public int Op { get; set; }
|
|
@@ -621,6 +840,29 @@ public sealed class MdpHotWatchService : ITransient
|
|
|
new SugarParameter("@k", watch.BizKey));
|
|
new SugarParameter("@k", watch.BizKey));
|
|
|
return string.Equals(status?.ToString()?.Trim(), "C", StringComparison.OrdinalIgnoreCase);
|
|
return string.Equals(status?.ToString()?.Trim(), "C", StringComparison.OrdinalIgnoreCase);
|
|
|
}
|
|
}
|
|
|
|
|
+ if (string.Equals(watch.BizType, "PUR_ORDER", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
+ {
|
|
|
|
|
+ var status = await remote.Ado.GetScalarAsync(
|
|
|
|
|
+ "SELECT TOP 1 Status FROM PurOrdMaster WHERE Domain=@d AND PurOrd=@k",
|
|
|
|
|
+ new SugarParameter("@d", watch.Domain),
|
|
|
|
|
+ new SugarParameter("@k", watch.BizKey));
|
|
|
|
|
+ if (string.Equals(status?.ToString()?.Trim(), "C", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
+ return true;
|
|
|
|
|
+
|
|
|
|
|
+ // 箱码全部离开待收即收货结束;标签尚未推达(total=0)时不能终结
|
|
|
|
|
+ var total = Convert.ToInt32(await remote.Ado.GetScalarAsync(
|
|
|
|
|
+ "SELECT COUNT(*) FROM MissedPrint WHERE Domain=@d AND OrdNbr=@k",
|
|
|
|
|
+ new SugarParameter("@d", watch.Domain),
|
|
|
|
|
+ new SugarParameter("@k", watch.BizKey)) ?? 0);
|
|
|
|
|
+ if (total > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ var pending = Convert.ToInt32(await remote.Ado.GetScalarAsync(
|
|
|
|
|
+ "SELECT COUNT(*) FROM MissedPrint WHERE Domain=@d AND OrdNbr=@k AND IsNull(Status,'')='U'",
|
|
|
|
|
+ new SugarParameter("@d", watch.Domain),
|
|
|
|
|
+ new SugarParameter("@k", watch.BizKey)) ?? 0);
|
|
|
|
|
+ if (pending == 0) return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
// 兜底:超过 7 天强制终结
|
|
// 兜底:超过 7 天强制终结
|
|
|
return watch.EnrollTime < DateTime.Now.AddDays(-7);
|
|
return watch.EnrollTime < DateTime.Now.AddDays(-7);
|
|
|
}
|
|
}
|