Просмотр исходного кода

feat: 计划联动看板SP转C#代码+供应商欠料看板InvTransHist/NbrMaster/NbrDetail改SQL Server中台读取+临时表改普通表修复连接池问题+前端超时配置+nginx超时对齐

Pengxy 9 часов назад
Родитель
Сommit
6f15dad615

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.339",
+	"version": "2.4.340",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 2 - 2
Web/src/views/aidop/api/linkagePlan.ts

@@ -77,12 +77,12 @@ export function fetchLinkagePlanDetail(id: number | string) {
 
 export function refreshLinkagePlanData() {
 	return service
-		.post<unknown>('/api/Order/linkageplan/refresh')
+		.post<unknown>('/api/Order/linkageplan/refresh', undefined, { timeout: 180000 })
 		.then((r) => unwrapFurionBody<unknown>(r.data));
 }
 
 export function runLinkagePlanProcedure() {
 	return service
-		.post<unknown>('/api/Order/linkageplan/run')
+		.post<unknown>('/api/Order/linkageplan/run', undefined, { timeout: 180000 })
 		.then((r) => unwrapFurionBody<unknown>(r.data));
 }

+ 7 - 1
Web/src/views/aidop/s4/api/procurementExecution.ts

@@ -343,7 +343,13 @@ export function fetchSupplierShortageKanbanList(params: any) {
 }
 
 export function refreshSupplierShortageKanban() {
-	return service.post<{ ok: boolean; elapsedMs?: number; message?: string }>('/api/ProcurementExecution/supplier-shortage-kanban/refresh').then((r) => r.data);
+	return service
+		.post<{ ok: boolean; elapsedMs?: number; message?: string }>(
+			'/api/ProcurementExecution/supplier-shortage-kanban/refresh',
+			undefined,
+			{ timeout: 180000 }
+		)
+		.then((r) => r.data);
 }
 
 // ── 催料待办(MATERIAL_SHORTAGE 审批流)──

+ 45 - 0
doc/migrations/2026-08-31_copy_print_template_to_tenant.sql

@@ -0,0 +1,45 @@
+-- 将 AIDOP 租户的【送货标签】和【送货单】打印模板复制到租户 838257186181189
+-- SysPrint 表:主键 Id(雪花ID),租户字段 TenantId
+
+SET @src_tenant = (SELECT t.Id FROM SysTenant t JOIN SysOrg o ON o.Id = t.OrgId WHERE o.Code = 'AIDOP' AND t.Status = 1 LIMIT 1);
+SET @dst_tenant = 838257186181189;
+
+-- 校验源租户存在
+SELECT IF(@src_tenant IS NULL, ERROR('AIDOP 租户未找到'), 'OK: source tenant_id = ') AS chk, @src_tenant;
+-- 校验目标租户存在
+SELECT IF((SELECT COUNT(*) FROM SysTenant WHERE Id = @dst_tenant) = 0, ERROR('目标租户不存在'), 'OK: target tenant_id = ') AS chk, @dst_tenant;
+
+-- 查看源模板(执行前先确认)
+SELECT Id, Name, PrintType, LENGTH(Template) AS tpl_len
+FROM SysPrint
+WHERE TenantId = @src_tenant
+  AND Name IN ('送货标签', '送货单');
+
+-- 复制:生成新 Id(基于 UNIX_TIMESTAMP 微秒 + 随机偏移,避免冲突)
+INSERT INTO SysPrint (Id, Name, Template, PrintType, ClientServiceAddress, PrintParam, OrderNo, Status, Remark, PrintDataDemo, TenantId, CreateTime)
+SELECT
+    FLOOR(UNIX_TIMESTAMP(NOW(6)) * 1000000) + FLOOR(RAND() * 99999) AS Id,
+    p.Name,
+    p.Template,
+    p.PrintType,
+    p.ClientServiceAddress,
+    p.PrintParam,
+    p.OrderNo,
+    p.Status,
+    p.Remark,
+    p.PrintDataDemo,
+    @dst_tenant,
+    NOW()
+FROM SysPrint p
+WHERE p.TenantId = @src_tenant
+  AND p.Name IN ('送货标签', '送货单')
+  AND NOT EXISTS (
+      SELECT 1 FROM SysPrint p2
+      WHERE p2.TenantId = @dst_tenant AND p2.Name = p.Name
+  );
+
+-- 验证复制结果
+SELECT Id, Name, PrintType, LENGTH(Template) AS tpl_len, TenantId
+FROM SysPrint
+WHERE TenantId = @dst_tenant
+  AND Name IN ('送货标签', '送货单');

+ 2 - 0
doc/migrations/2026-08-31_missedprint_add_tenant_id.sql

@@ -0,0 +1,2 @@
+-- MissedPrint(漏打补打表)补充 tenant_id 字段,用于租户隔离
+ALTER TABLE `MissedPrint` ADD COLUMN `tenant_id` BIGINT NULL COMMENT '租户Id';

+ 4 - 4
docker/nginx/default.conf

@@ -21,8 +21,8 @@ server {
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_connect_timeout 60s;
-        proxy_send_timeout 120s;
-        proxy_read_timeout 120s;
+        proxy_send_timeout 180s;
+        proxy_read_timeout 180s;
         client_max_body_size 100m;
     }
 }
@@ -53,8 +53,8 @@ server {
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_connect_timeout 60s;
-        proxy_send_timeout 120s;
-        proxy_read_timeout 120s;
+        proxy_send_timeout 180s;
+        proxy_read_timeout 180s;
         client_max_body_size 100m;
     }
 }

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

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.421</AssemblyVersion>
-    <FileVersion>1.0.421</FileVersion>
-    <Version>1.0.421</Version>
+    <AssemblyVersion>1.0.422</AssemblyVersion>
+    <FileVersion>1.0.422</FileVersion>
+    <Version>1.0.422</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 423 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/Order/LinkagePlanService.cs

@@ -1,3 +1,4 @@
+using Admin.NET.Plugin.AiDOP.DataPlatform;
 using Admin.NET.Plugin.AiDOP.DataPlatform.S1Refresh;
 using Admin.NET.Plugin.AiDOP.Infrastructure;
 
@@ -13,21 +14,26 @@ namespace Admin.NET.Plugin.AiDOP.Order;
 [NonUnify]
 public class LinkagePlanService : IDynamicApiController, ITransient
 {
+    private const string SqlServerSourceCode = "DOPDEMORQ_SQLSERVER";
+
     private readonly ISqlSugarClient _db;
     private readonly SqlSugarRepository<LinkagePlan> _linkagePlanRep;
     private readonly UserManager _userManager;
     private readonly S1MdpSyncTransformService _s1MdpSyncTransformService;
+    private readonly MdpSourceScopeFactory _scopeFactory;
 
     public LinkagePlanService(
         ISqlSugarClient db,
         SqlSugarRepository<LinkagePlan> linkagePlanRep,
         UserManager userManager,
-        S1MdpSyncTransformService s1MdpSyncTransformService)
+        S1MdpSyncTransformService s1MdpSyncTransformService,
+        MdpSourceScopeFactory scopeFactory)
     {
         _db = db;
         _linkagePlanRep = linkagePlanRep;
         _userManager = userManager;
         _s1MdpSyncTransformService = s1MdpSyncTransformService;
+        _scopeFactory = scopeFactory;
     }
 
     [DisplayName("获取计划联动看板列表")]
@@ -166,20 +172,431 @@ public class LinkagePlanService : IDynamicApiController, ITransient
         }
     }
 
-    [DisplayName("执行计划联动存储过程")]
+    [DisplayName("执行计划联动刷新")]
     [HttpPost("linkageplan/run")]
     public async Task<object> RunLinkagePlanProcedure()
     {
         try
         {
-            var tenantId = _userManager.TenantId;
-            await _db.Ado.ExecuteCommandAsync("CALL pr_Mes_LinkagePlan(@TenantId)",
-                new SugarParameter("@TenantId", tenantId));
+            var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
+            var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
+
+            // ── Step 0: 从 SQL Server 读取 InvTransHist / NbrMaster / NbrDetail,写入 MySQL 辅助表 ──
+            await PrepareInvTransHistTempAsync();
+            await PrepareNbrMasterTempAsync();
+            await PrepareNbrDetailTempAsync();
+
+            // ── Step 1: 清空全部数据 ──
+            await _db.Ado.ExecuteCommandAsync("DELETE FROM LinkagePlan");
+
+            // ── Step 2: 插入计划 + 实际数据(IGNORE 跳过重复主键) ──
+            await _db.Ado.ExecuteCommandAsync("""
+                INSERT IGNORE INTO LinkagePlan(
+                    id, bill_no, custom_no, order_type, item_number, Descr, Descr1, ItemType,
+                    qty, update_time, sys_capacity_date, bomstart, bomend, linestart, lineend,
+                    productstart, productend, needtime, sys_material_date, cgneedtime, cgend,
+                    blstart, blend, starttime, endtime, ipqcjystart, ipqcjyend, fqcjystart,
+                    fqcjyend, rkstart, rkend, fystarttime, fyendtime, `type`, levelnum, isuse, tenant_id
+                )
+                SELECT
+                    id, bill_no, custom_no, order_type, item_number, Descr, Descr1, ItemType,
+                    qty, update_time, sys_capacity_date, bomstart, bomend, linestart, lineend,
+                    productstart, productend, needtime, sys_material_date, cgneedtime, cgend,
+                    blstart, blend, starttime, endtime, ipqcjystart, ipqcjyend, fqcjystart,
+                    fqcjyend, rkstart, rkend, fystarttime, fyendtime, `type`, levelnum, isuse, @TenantId
+                FROM (
+                    -- 计划数据
+                    SELECT
+                        b.id, a.bill_no, a.custom_no,
+                        IF(a.order_type='1', '销售', '计划') AS order_type,
+                        b.item_number, c.Descr, c.Descr1, c.ownerapplication AS ItemType,
+                        b.qty, h.create_time AS update_time, b.sys_capacity_date,
+                        h.create_time AS bomstart,
+                        DATE_ADD(h.create_time, INTERVAL IFNULL(c.BOMDesignTime, 0) DAY) AS bomend,
+                        DATE_ADD(h.create_time, INTERVAL IFNULL(c.BOMDesignTime, 0) DAY) AS linestart,
+                        DATE_ADD(h.create_time, INTERVAL (IFNULL(c.BOMDesignTime, 0) + IFNULL(c.RoutingDesignTime, 0)) DAY) AS lineend,
+                        DATE_ADD(h.create_time, INTERVAL (IFNULL(c.BOMDesignTime, 0) + IFNULL(c.RoutingDesignTime, 0)) DAY) AS productstart,
+                        DATE_ADD(h.create_time, INTERVAL (IFNULL(c.BOMDesignTime, 0) + IFNULL(c.RoutingDesignTime, 0) +
+                            CEILING(IFNULL(((b.qty-1)*d.maxruntime+d.runtime)/8, 0))) DAY) AS productend,
+                        h.create_time AS needtime, b.sys_material_date,
+                        h.create_time AS cgneedtime, e.kitting_time AS cgend,
+                        e.kitting_time AS blstart,
+                        DATE_ADD(e.kitting_time, INTERVAL IFNULL(c.mfgmttr, 0) DAY) AS blend,
+                        g.starttime, g.endtime,
+                        g.endtime AS ipqcjystart,
+                        DATE_ADD(g.endtime, INTERVAL IFNULL(c.InsLT, 0) DAY) AS ipqcjyend,
+                        g.endtime AS fqcjystart,
+                        DATE_ADD(g.endtime, INTERVAL IFNULL(c.InsLT, 0) DAY) AS fqcjyend,
+                        DATE_ADD(g.endtime, INTERVAL IFNULL(c.InsLT, 0) DAY) AS rkstart,
+                        DATE_ADD(g.endtime, INTERVAL (IFNULL(c.InsLT, 0) + 1) DAY) AS rkend,
+                        DATE_ADD(g.endtime, INTERVAL (IFNULL(c.InsLT, 0) + 1) DAY) AS fystarttime,
+                        DATE_ADD(g.endtime, INTERVAL (IFNULL(c.InsLT, 0) + 2) DAY) AS fyendtime,
+                        '计划' AS `type`, 2 AS levelnum, 0 AS isuse
+                    FROM crm_seorder a
+                    LEFT JOIN crm_seorderentry b ON a.bill_no = b.bill_no AND b.tenant_id = a.tenant_id
+                    LEFT JOIN ItemMaster c ON b.item_number = c.ItemNum AND (c.tenant_id = a.tenant_id OR a.tenant_id IS NULL)
+                    LEFT JOIN (
+                        SELECT RoutingCode, SUM(runtime) AS runtime, MAX(runtime) AS maxruntime
+                        FROM RoutingOpDetail GROUP BY RoutingCode
+                    ) d ON c.ItemNum = d.RoutingCode
+                    LEFT JOIN (
+                        SELECT sentry_id, MAX(kitting_time) AS kitting_time
+                        FROM (
+                            SELECT ar.sentry_id, bce.kitting_time,
+                                   ROW_NUMBER() OVER (PARTITION BY ar.sentry_id ORDER BY ar.create_time DESC) AS rn
+                            FROM b_examine_result ar
+                            JOIN b_bom_child_examine bce ON ar.id = bce.examine_id
+                            WHERE ar.sorderid IS NOT NULL AND bce.erp_cls IN ('2','3')
+                              AND ar.tenant_id = @TenantId
+                        ) ex WHERE rn = 1
+                        GROUP BY sentry_id
+                    ) e ON b.id = e.sentry_id
+                    LEFT JOIN mes_moentry f ON b.id = f.soentry_id AND f.tenant_id = a.tenant_id
+                    LEFT JOIN mes_morder h ON f.moentry_mono = h.morder_no AND h.parent_id IS NULL AND h.tenant_id = a.tenant_id
+                    LEFT JOIN (
+                        SELECT workords, MIN(plandate) AS starttime, MAX(plandate) AS endtime
+                        FROM PeriodSequenceDet GROUP BY workords
+                    ) g ON f.moentry_mono = g.workords
+                    WHERE a.IsDeleted = 0 AND h.create_time IS NOT NULL
+                      AND a.tenant_id = @TenantId
+
+                    UNION ALL
+
+                    -- 实际数据(InvTransHist 从 SQL Server 临时表读取)
+                    SELECT
+                        b.id, a.bill_no, a.custom_no,
+                        IF(a.order_type='1', '销售', '计划') AS order_type,
+                        b.item_number, c.Descr, c.Descr1, c.ownerapplication AS ItemType,
+                        b.qty, h.create_time AS update_time, b.sys_capacity_date,
+                        h.create_time AS bomstart, nb.CreateTime AS bomend,
+                        h.create_time AS linestart, nb.CreateTime AS lineend,
+                        d.starttime AS productstart, d.endtime AS productend,
+                        h.create_time AS needtime, b.sys_material_date,
+                        e.cgneedtime, e.kitting_time AS cgend,
+                        nb.CreateTime AS blstart, nd.blend,
+                        g.starttime, g.endtime,
+                        e.FAPPLYTIME AS ipqcjystart, e.FINSPESTARTDATE AS ipqcjyend,
+                        g.endtime AS fqcjystart, fqc.fqcDate AS fqcjyend,
+                        i.starttime AS rkstart, i.endtime AS rkend,
+                        asn.starttime AS fystarttime, asn.endtime AS fyendtime,
+                        '实际' AS `type`, 1 AS levelnum, 0 AS isuse
+                    FROM crm_seorder a
+                    LEFT JOIN crm_seorderentry b ON a.bill_no = b.bill_no AND b.tenant_id = a.tenant_id
+                    LEFT JOIN ItemMaster c ON b.item_number = c.ItemNum AND (c.tenant_id = a.tenant_id OR a.tenant_id IS NULL)
+                    LEFT JOIN (
+                        SELECT pr.sentry_id,
+                               MAX(sh.createtime) AS kitting_time,
+                               MIN(jq.jqhf) AS cgneedtime,
+                               MIN(iqcjy.FINSPESTARTDATE) AS FINSPESTARTDATE,
+                               MAX(iqcjy.FAPPLYTIME) AS FAPPLYTIME
+                        FROM srm_pr_main pr
+                        LEFT JOIN PurOrdDetail pod ON pr.pr_billno = pod.Req AND pod.tenant_id = pr.tenant_id
+                        LEFT JOIN vscm_cgshrk sh ON pod.PurOrd = sh.OrdNbr AND pod.Line = sh.OrdLine
+                        LEFT JOIN scm_jhjh_jq jq ON pod.PurOrd = jq.cgdd AND pod.Line = jq.ddhh
+                        LEFT JOIN (
+                            SELECT f.po_bill, f.po_billline,
+                                   MIN(a2.FINSPESTARTDATE) AS FINSPESTARTDATE,
+                                   MAX(b2.FAPPLYTIME) AS FAPPLYTIME
+                            FROM qms_qcp_inspbill a2
+                            LEFT JOIN qms_qcp_inspecapplyn b2 ON a2.lydjbh = b2.FBILLNO
+                            LEFT JOIN qms_qcp_insappnentry c2 ON b2.id = c2.glid
+                            JOIN scm_shd cd ON c2.shdh = cd.shddh
+                            LEFT JOIN scm_shdzb f ON f.shpc = c2.FLOTNUMBER
+                                AND f.sh_material_code = c2.FMATERIALCFG
+                                AND f.glid = cd.id
+                            GROUP BY f.po_bill, f.po_billline
+                        ) iqcjy ON pod.PurOrd = iqcjy.po_bill AND pod.Line = iqcjy.po_billline
+                        WHERE jq.flag = '1' AND pr.sentry_id IS NOT NULL
+                          AND pr.tenant_id = @TenantId
+                        GROUP BY pr.sentry_id
+                    ) e ON e.sentry_id = b.id
+                    LEFT JOIN mes_moentry f ON b.id = f.soentry_id AND f.tenant_id = a.tenant_id
+                    LEFT JOIN mes_morder h ON f.moentry_mono = h.morder_no AND h.parent_id IS NULL AND h.tenant_id = a.tenant_id
+                    LEFT JOIN (
+                        SELECT workords, MIN(plandate) AS starttime, MAX(plandate) AS endtime
+                        FROM PeriodSequenceDet GROUP BY workords
+                    ) d ON h.morder_no = d.workords
+                    LEFT JOIN _tmp_nbr_master nb ON h.morder_no = nb.WorkOrd
+                    LEFT JOIN _tmp_nbr_detail nd ON h.morder_no = nd.WorkOrd
+                    LEFT JOIN (
+                        SELECT workords, MIN(plandate) AS starttime, MAX(plandate) AS endtime
+                        FROM PeriodSequenceDet WHERE CompQty > 0 GROUP BY workords
+                    ) g ON f.moentry_mono = g.workords
+                    LEFT JOIN _tmp_inv_trans_hist i ON h.morder_no = i.WorkOrd
+                    LEFT JOIN (
+                        SELECT ad.OrdNbr, ad.ContainerItem, ad.OrdLine,
+                               MIN(ad.CreateTime) AS starttime, MAX(ad.UpdateTime) AS endtime
+                        FROM ASNBOLShipperDetail ad
+                        INNER JOIN ASNBOLShipperMaster am ON ad.ASNBOLShipperRecID = am.RecID
+                        WHERE am.shtype = 'SH' AND am.typed <> 'S'
+                        GROUP BY ad.OrdNbr, ad.ContainerItem, ad.OrdLine
+                    ) asn ON b.bill_no = asn.OrdNbr AND b.item_number = asn.ContainerItem
+                    LEFT JOIN WorkOrdMaster wm ON h.morder_no = wm.WorkOrd AND wm.tenant_id = a.tenant_id
+                    LEFT JOIN (
+                        SELECT sczld, MAX(FINSPEENDDATE) AS fqcDate
+                        FROM qms_qcpp_inspbill GROUP BY sczld
+                    ) fqc ON wm.Batch = fqc.sczld
+                    WHERE a.IsDeleted = 0 AND h.create_time IS NOT NULL
+                      AND a.tenant_id = @TenantId
+                ) AS viewall
+                """, pars);
+
+            // ── Step 3: 插入偏差数据 ──
+            await _db.Ado.ExecuteCommandAsync("""
+                INSERT IGNORE INTO LinkagePlan(
+                    id, bill_no, custom_no, order_type, item_number, Descr, Descr1, ItemType,
+                    qty, update_time, sys_capacity_date, bomstart, bomend, linestart, lineend,
+                    productstart, productend, needtime, sys_material_date, cgneedtime, cgend,
+                    blstart, blend, starttime, endtime, ipqcjystart, ipqcjyend, fqcjystart,
+                    fqcjyend, rkstart, rkend, fystarttime, fyendtime, `type`, levelnum, isuse, tenant_id
+                )
+                SELECT
+                    a.id, a.bill_no, a.custom_no, a.order_type, b.item_number,
+                    a.Descr, a.Descr1, a.ItemType, b.qty, a.update_time, b.sys_capacity_date,
+                    DATEDIFF(b.bomstart, a.bomstart), DATEDIFF(b.bomend, a.bomend),
+                    DATEDIFF(b.linestart, a.linestart), DATEDIFF(b.lineend, a.lineend),
+                    DATEDIFF(b.productstart, a.productstart), DATEDIFF(b.productend, a.productend),
+                    DATEDIFF(b.needtime, a.needtime), DATEDIFF(b.sys_material_date, a.sys_material_date),
+                    DATEDIFF(b.cgneedtime, a.cgneedtime), DATEDIFF(b.cgend, a.cgend),
+                    DATEDIFF(b.blstart, a.blstart), DATEDIFF(b.blend, a.blend),
+                    DATEDIFF(b.starttime, a.starttime), DATEDIFF(b.endtime, a.endtime),
+                    DATEDIFF(b.ipqcjystart, a.ipqcjystart), DATEDIFF(b.ipqcjyend, a.ipqcjyend),
+                    DATEDIFF(b.fqcjystart, a.fqcjystart), DATEDIFF(b.fqcjyend, a.fqcjyend),
+                    DATEDIFF(b.rkstart, a.rkstart), DATEDIFF(b.rkend, a.rkend),
+                    DATEDIFF(b.fystarttime, a.fystarttime), DATEDIFF(b.fyendtime, a.fyendtime),
+                    '偏差', 0, 0, @TenantId
+                FROM (SELECT * FROM LinkagePlan WHERE `type` = '计划' AND tenant_id = @TenantId) a
+                LEFT JOIN (SELECT * FROM LinkagePlan WHERE `type` = '实际' AND tenant_id = @TenantId) b ON a.id = b.id
+                """, pars);
+
+            // ── Step 4: 更新背景色标记 ──
+            await _db.Ado.ExecuteCommandAsync("""
+                UPDATE LinkagePlan a
+                LEFT JOIN (
+                    SELECT a2.id, a2.`type`,
+                        CONCAT(
+                            CASE WHEN a2.`type` = '计划' THEN
+                                CASE WHEN a2.bomstart IS NOT NULL AND b2.bomstart IS NULL AND DATEDIFF(NOW(), a2.bomstart) > 7 THEN 'bomstart:red;'
+                                     WHEN a2.bomstart IS NOT NULL AND b2.bomstart IS NULL AND DATEDIFF(NOW(), a2.bomstart) > 0 THEN 'bomstart:yellow;'
+                                     ELSE '' END
+                            WHEN a2.`type` = '偏差' THEN
+                                CASE WHEN IFNULL(CAST(a2.bomstart AS SIGNED), 0) > 7 THEN 'bomstart:red;'
+                                     WHEN IFNULL(CAST(a2.bomstart AS SIGNED), 0) > 0 THEN 'bomstart:yellow;'
+                                     ELSE '' END
+                            ELSE '' END,
+                            CASE WHEN a2.`type` = '计划' THEN
+                                CASE WHEN a2.bomend IS NOT NULL AND b2.bomend IS NULL AND DATEDIFF(NOW(), a2.bomend) > 7 THEN 'bomend:red;'
+                                     WHEN a2.bomend IS NOT NULL AND b2.bomend IS NULL AND DATEDIFF(NOW(), a2.bomend) > 0 THEN 'bomend:yellow;'
+                                     ELSE '' END
+                            WHEN a2.`type` = '偏差' THEN
+                                CASE WHEN IFNULL(CAST(a2.bomend AS SIGNED), 0) > 7 THEN 'bomend:red;'
+                                     WHEN IFNULL(CAST(a2.bomend AS SIGNED), 0) > 0 THEN 'bomend:yellow;'
+                                     ELSE '' END
+                            ELSE '' END,
+                            CASE WHEN a2.`type` = '计划' THEN
+                                CASE WHEN a2.starttime IS NOT NULL AND b2.starttime IS NULL AND DATEDIFF(NOW(), a2.starttime) > 7 THEN 'starttime:red;'
+                                     WHEN a2.starttime IS NOT NULL AND b2.starttime IS NULL AND DATEDIFF(NOW(), a2.starttime) > 0 THEN 'starttime:yellow;'
+                                     ELSE '' END
+                            WHEN a2.`type` = '偏差' THEN
+                                CASE WHEN IFNULL(CAST(a2.starttime AS SIGNED), 0) > 7 THEN 'starttime:red;'
+                                     WHEN IFNULL(CAST(a2.starttime AS SIGNED), 0) > 0 THEN 'starttime:yellow;'
+                                     ELSE '' END
+                            ELSE '' END,
+                            CASE WHEN a2.`type` = '计划' THEN
+                                CASE WHEN a2.endtime IS NOT NULL AND b2.endtime IS NULL AND DATEDIFF(NOW(), a2.endtime) > 7 THEN 'endtime:red;'
+                                     WHEN a2.endtime IS NOT NULL AND b2.endtime IS NULL AND DATEDIFF(NOW(), a2.endtime) > 0 THEN 'endtime:yellow;'
+                                     ELSE '' END
+                            WHEN a2.`type` = '偏差' THEN
+                                CASE WHEN IFNULL(CAST(a2.endtime AS SIGNED), 0) > 7 THEN 'endtime:red;'
+                                     WHEN IFNULL(CAST(a2.endtime AS SIGNED), 0) > 0 THEN 'endtime:yellow;'
+                                     ELSE '' END
+                            ELSE '' END
+                        ) AS background
+                    FROM LinkagePlan a2
+                    LEFT JOIN (SELECT * FROM LinkagePlan WHERE `type` = '实际' AND tenant_id = @TenantId) b2 ON a2.id = b2.id
+                    LEFT JOIN (SELECT * FROM LinkagePlan WHERE `type` = '偏差' AND tenant_id = @TenantId) c2 ON a2.id = c2.id
+                    WHERE a2.tenant_id = @TenantId
+                ) AS bg ON a.id = bg.id AND a.`type` = bg.`type`
+                SET a.background = bg.background
+                WHERE a.tenant_id = @TenantId
+                """, pars);
+
+            // ── 清理辅助表 ──
+            await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_inv_trans_hist");
+            await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_nbr_master");
+            await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_nbr_detail");
+
             return new { message = "计划联动刷新成功" };
         }
         catch (Exception ex)
         {
-            throw Oops.Oh($"执行存储过程失败: {ex.Message}");
+            throw Oops.Oh($"执行计划联动刷新失败: {ex.Message}");
+        }
+    }
+
+    /// <summary>
+    /// 从 SQL Server 读取 InvTransHist,写入 MySQL 临时表 _tmp_inv_trans_hist。
+    /// </summary>
+    private async Task PrepareInvTransHistTempAsync()
+    {
+        await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_inv_trans_hist");
+        await _db.Ado.ExecuteCommandAsync("""
+            CREATE TABLE IF NOT EXISTS _tmp_inv_trans_hist (
+                WorkOrd VARCHAR(64) PRIMARY KEY,
+                starttime DATETIME NULL,
+                endtime DATETIME NULL
+            )
+            """);
+
+        var remote = await _scopeFactory.GetScopeAsync(SqlServerSourceCode);
+        var rows = await remote.Ado.SqlQueryAsync<InvTransHistRow>("""
+            SELECT WorkOrd,
+                   MIN(CreateTime) AS starttime,
+                   MAX(CreateTime) AS endtime
+            FROM InvTransHist
+            WHERE TransType = 'rct-wo' AND QtyChange > 0
+            GROUP BY WorkOrd
+            """);
+
+        if (rows.Count == 0)
+            return;
+
+        // 分批 INSERT(每批 500 条)
+        var insertPars = new List<SugarParameter>();
+        for (var i = 0; i < rows.Count; i++)
+        {
+            var r = rows[i];
+            if (string.IsNullOrWhiteSpace(r.WorkOrd)) continue;
+            insertPars.Add(new SugarParameter($"@wo{i}", r.WorkOrd));
+            insertPars.Add(new SugarParameter($"@st{i}", r.StartTime));
+            insertPars.Add(new SugarParameter($"@et{i}", r.EndTime));
+        }
+
+        const int batchSize = 500;
+        for (var offset = 0; offset < insertPars.Count; offset += batchSize * 3)
+        {
+            var batchEnd = Math.Min(offset + batchSize * 3, insertPars.Count);
+            var batchPars = insertPars.GetRange(offset, batchEnd - offset);
+            var batchValues = new List<string>();
+            for (var j = offset; j < batchEnd; j += 3)
+                batchValues.Add($"({insertPars[j].ParameterName}, {insertPars[j+1].ParameterName}, {insertPars[j+2].ParameterName})");
+
+            await _db.Ado.ExecuteCommandAsync(
+                $"INSERT INTO _tmp_inv_trans_hist (WorkOrd, starttime, endtime) VALUES {string.Join(",", batchValues)}",
+                batchPars);
+        }
+    }
+
+    private sealed class InvTransHistRow
+    {
+        public string? WorkOrd { get; set; }
+        public DateTime? StartTime { get; set; }
+        public DateTime? EndTime { get; set; }
+    }
+
+    /// <summary>
+    /// 从 SQL Server 读取 NbrMaster (Type='SM'),写入 MySQL 临时表 _tmp_nbr_master。
+    /// </summary>
+    private async Task PrepareNbrMasterTempAsync()
+    {
+        await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_nbr_master");
+        await _db.Ado.ExecuteCommandAsync("""
+            CREATE TABLE IF NOT EXISTS _tmp_nbr_master (
+                WorkOrd VARCHAR(64) PRIMARY KEY,
+                CreateTime DATETIME NULL
+            )
+            """);
+
+        var remote = await _scopeFactory.GetScopeAsync(SqlServerSourceCode);
+        var rows = await remote.Ado.SqlQueryAsync<NbrMasterRow>("""
+            SELECT WorkOrd, MIN(CreateTime) AS CreateTime
+            FROM NbrMaster WHERE Type = 'SM'
+            GROUP BY WorkOrd
+            """);
+
+        if (rows.Count == 0)
+            return;
+
+        var insertPars = new List<SugarParameter>();
+        for (var i = 0; i < rows.Count; i++)
+        {
+            var r = rows[i];
+            if (string.IsNullOrWhiteSpace(r.WorkOrd)) continue;
+            insertPars.Add(new SugarParameter($"@wo{i}", r.WorkOrd));
+            insertPars.Add(new SugarParameter($"@ct{i}", r.CreateTime));
+        }
+
+        const int batchSize = 500;
+        for (var offset = 0; offset < insertPars.Count; offset += batchSize * 2)
+        {
+            var batchEnd = Math.Min(offset + batchSize * 2, insertPars.Count);
+            var batchPars = insertPars.GetRange(offset, batchEnd - offset);
+            var batchValues = new List<string>();
+            for (var j = offset; j < batchEnd; j += 2)
+                batchValues.Add($"({insertPars[j].ParameterName}, {insertPars[j+1].ParameterName})");
+
+            await _db.Ado.ExecuteCommandAsync(
+                $"INSERT INTO _tmp_nbr_master (WorkOrd, CreateTime) VALUES {string.Join(",", batchValues)}",
+                batchPars);
+        }
+    }
+
+    /// <summary>
+    /// 从 SQL Server 读取 NbrDetail,写入 MySQL 临时表 _tmp_nbr_detail。
+    /// </summary>
+    private async Task PrepareNbrDetailTempAsync()
+    {
+        await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_nbr_detail");
+        await _db.Ado.ExecuteCommandAsync("""
+            CREATE TABLE IF NOT EXISTS _tmp_nbr_detail (
+                WorkOrd VARCHAR(64) PRIMARY KEY,
+                blend DATETIME NULL
+            )
+            """);
+
+        var remote = await _scopeFactory.GetScopeAsync(SqlServerSourceCode);
+        var rows = await remote.Ado.SqlQueryAsync<NbrDetailRow>("""
+            SELECT WorkOrd, MIN(UpdateTime) AS blend
+            FROM NbrDetail
+            GROUP BY WorkOrd
+            """);
+
+        if (rows.Count == 0)
+            return;
+
+        var insertPars = new List<SugarParameter>();
+        for (var i = 0; i < rows.Count; i++)
+        {
+            var r = rows[i];
+            if (string.IsNullOrWhiteSpace(r.WorkOrd)) continue;
+            insertPars.Add(new SugarParameter($"@wo{i}", r.WorkOrd));
+            insertPars.Add(new SugarParameter($"@bl{i}", r.Blend));
+        }
+
+        const int batchSize = 500;
+        for (var offset = 0; offset < insertPars.Count; offset += batchSize * 2)
+        {
+            var batchEnd = Math.Min(offset + batchSize * 2, insertPars.Count);
+            var batchPars = insertPars.GetRange(offset, batchEnd - offset);
+            var batchValues = new List<string>();
+            for (var j = offset; j < batchEnd; j += 2)
+                batchValues.Add($"({insertPars[j].ParameterName}, {insertPars[j+1].ParameterName})");
+
+            await _db.Ado.ExecuteCommandAsync(
+                $"INSERT INTO _tmp_nbr_detail (WorkOrd, blend) VALUES {string.Join(",", batchValues)}",
+                batchPars);
         }
     }
+
+    private sealed class NbrMasterRow
+    {
+        public string? WorkOrd { get; set; }
+        public DateTime? CreateTime { get; set; }
+    }
+
+    private sealed class NbrDetailRow
+    {
+        public string? WorkOrd { get; set; }
+        public DateTime? Blend { get; set; }
+    }
 }

+ 18 - 16
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/SupplierDeliveryManagementService.cs

@@ -464,35 +464,37 @@ public class SupplierDeliveryManagementService : IDynamicApiController, ITransie
                     ELSE 'wait'
                 END AS sfyqnew
             FROM PurOrdDetail p
-            LEFT JOIN PurOrdMaster pm ON p.PurOrd = pm.PurOrd
+            LEFT JOIN PurOrdMaster pm ON p.PurOrd = pm.PurOrd AND pm.tenant_id = p.tenant_id
             LEFT JOIN (
-                SELECT SUM(sh_delivery_quantity) AS fhsl, po_bill, po_billline
+                SELECT SUM(sh_delivery_quantity) AS fhsl, po_bill, po_billline, tenant_id
                 FROM scm_shdzb
-                GROUP BY po_bill, po_billline
+                WHERE tenant_id = @TenantId
+                GROUP BY po_bill, po_billline, tenant_id
             ) s ON s.po_bill = p.PurOrd AND s.po_billline = p.Line
             LEFT JOIN (
-                SELECT SUM(Qty) AS zjsl, PurOrd, PurLine
+                SELECT SUM(Qty) AS zjsl, PurOrd, PurLine, tenant_id
                 FROM MissedPrint
-                WHERE Status = 'I'
-                GROUP BY PurOrd, PurLine
+                WHERE Status = 'I' AND tenant_id = @TenantId
+                GROUP BY PurOrd, PurLine, tenant_id
             ) i ON i.PurOrd = p.PurOrd AND i.PurLine = p.Line
             LEFT JOIN (
-                SELECT SUM(QtyReturn) AS thsl, OrdNbr, OrdLine
+                SELECT SUM(QtyReturn) AS thsl, OrdNbr, OrdLine, tenant_id
                 FROM PurOrdRctDetail
-                WHERE rcttype IN ('pt', 'temp')
-                GROUP BY OrdNbr, OrdLine
+                WHERE rcttype IN ('pt', 'temp') AND tenant_id = @TenantId
+                GROUP BY OrdNbr, OrdLine, tenant_id
             ) dor ON dor.OrdNbr = p.PurOrd AND dor.OrdLine = p.Line
-            LEFT JOIN srm_polist_ds ds ON p.PurOrd = ds.ponumber AND p.Line = ds.poline
+            LEFT JOIN srm_polist_ds ds ON p.PurOrd = ds.ponumber AND p.Line = ds.poline AND ds.tenant_id = p.tenant_id
             LEFT JOIN (
-                SELECT SUM(sh_delivery_quantity) AS jhdyj, jhdbh
+                SELECT SUM(sh_delivery_quantity) AS jhdyj, jhdbh, tenant_id
                 FROM scm_shdzb
-                GROUP BY jhdbh
+                WHERE tenant_id = @TenantId
+                GROUP BY jhdbh, tenant_id
             ) sh ON ds.dsnum = sh.jhdbh
             LEFT JOIN (
-                SELECT SUM(IFNULL(Qty, 0)) AS jhdzj, PurOrdDetBatchNbr
+                SELECT SUM(IFNULL(Qty, 0)) AS jhdzj, PurOrdDetBatchNbr, tenant_id
                 FROM MissedPrint
-                WHERE Status = 'I'
-                GROUP BY PurOrdDetBatchNbr
+                WHERE Status = 'I' AND tenant_id = @TenantId
+                GROUP BY PurOrdDetBatchNbr, tenant_id
             ) jy ON ds.dsnum = jy.PurOrdDetBatchNbr
             LEFT JOIN (
                 SELECT glid1, MIN(id) AS id, MIN(DATE_FORMAT(hfsj, '%Y.%m.%d')) AS hfsj
@@ -500,7 +502,7 @@ public class SupplierDeliveryManagementService : IDynamicApiController, ITransie
                 WHERE flag = 2
                 GROUP BY glid1
             ) b2 ON ds.id = b2.glid1
-            LEFT JOIN ItemMaster im ON p.ItemNum = im.ItemNum
+            LEFT JOIN ItemMaster im ON p.ItemNum = im.ItemNum AND (im.tenant_id = p.tenant_id OR p.tenant_id IS NULL)
             LEFT JOIN (
                 SELECT
                     glid,

+ 6 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/SupplierShipmentService.cs

@@ -169,8 +169,8 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
                 ds.DSNum AS jhdbh,
                 im.Descr1 AS shMaterialGgxh
             FROM PurOrdDetail p
-            LEFT JOIN srm_polist_ds ds ON p.PurOrd = ds.ponumber AND p.Line = ds.poline
-            LEFT JOIN ItemMaster im ON p.ItemNum = im.ItemNum
+            LEFT JOIN srm_polist_ds ds ON p.PurOrd = ds.ponumber AND p.Line = ds.poline AND ds.tenant_id = p.tenant_id
+            LEFT JOIN ItemMaster im ON p.ItemNum = im.ItemNum AND (im.tenant_id = p.tenant_id OR p.tenant_id IS NULL)
             WHERE FIND_IN_SET(CAST(IFNULL(ds.id, p.RecID) AS CHAR(50)), REPLACE(IFNULL(@ids, ''), ' ', '')) > 0
               AND p.tenant_id = @TenantId
             ORDER BY p.PurOrd, p.Line
@@ -930,7 +930,7 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
                 b.scph,
                 b.th
             FROM scm_shd a
-            INNER JOIN scm_shdzb b ON CAST(a.id AS CHAR) = b.glid
+            INNER JOIN scm_shdzb b ON CAST(a.id AS CHAR) = b.glid AND b.tenant_id = a.tenant_id
             LEFT JOIN (
                 SELECT
                     i.Drawing AS th,
@@ -939,9 +939,10 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
                     p.PurOrd,
                     p.`Usage`
                 FROM PurOrdMaster p
-                LEFT JOIN PurOrdDetail d ON p.Potype = d.Potype AND p.PurOrd = d.PurOrd
-                LEFT JOIN ItemMaster i ON d.ItemNum = i.ItemNum
+                LEFT JOIN PurOrdDetail d ON p.Potype = d.Potype AND p.PurOrd = d.PurOrd AND d.tenant_id = p.tenant_id
+                LEFT JOIN ItemMaster i ON d.ItemNum = i.ItemNum AND (i.tenant_id = p.tenant_id OR p.tenant_id IS NULL)
                 WHERE p.IsActive = 1
+                  AND p.tenant_id = @TenantId
             ) c ON c.PurOrd = b.po_bill AND CAST(c.Line AS CHAR) = TRIM(CAST(IFNULL(b.po_billline, '') AS CHAR))
             WHERE a.shddh = @shddh
               AND a.tenant_id = @TenantId

+ 310 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/SupplierShortageKanbanService.cs

@@ -1,4 +1,5 @@
 using System.Text.Json;
+using Admin.NET.Plugin.AiDOP.DataPlatform;
 using Admin.NET.Plugin.AiDOP.ProcurementExecution.Dto;
 using Admin.NET.Plugin.AiDOP.ProcurementExecution.Entity;
 
@@ -6,8 +7,7 @@ namespace Admin.NET.Plugin.AiDOP.ProcurementExecution;
 
 /// <summary>
 /// S4 供应商欠料看板(只读列表 + 刷新)
-/// 数据表:WorkOrdDetailTotalKB
-/// 存储过程:pr_MES_Report_DayWorkOrdDetailTotal_New
+/// 数据表:WorkOrdDetailTotalKB(结果)/ WorkOrdDetailTotal(中间)
 /// 路由:/api/ProcurementExecution/supplier-shortage-kanban/...
 /// </summary>
 [ApiDescriptionSettings(Order = 323, Description = "S4供应商欠料看板")]
@@ -16,10 +16,21 @@ namespace Admin.NET.Plugin.AiDOP.ProcurementExecution;
 [NonUnify]
 public class SupplierShortageKanbanService : IDynamicApiController, ITransient
 {
+    private const string SqlServerSourceCode = "DOPDEMORQ_SQLSERVER";
+
     private readonly ISqlSugarClient _db;
     private readonly UserManager _userManager;
+    private readonly MdpSourceScopeFactory _scopeFactory;
 
-    public SupplierShortageKanbanService(ISqlSugarClient db, UserManager userManager) { _db = db; _userManager = userManager; }
+    public SupplierShortageKanbanService(
+        ISqlSugarClient db,
+        UserManager userManager,
+        MdpSourceScopeFactory scopeFactory)
+    {
+        _db = db;
+        _userManager = userManager;
+        _scopeFactory = scopeFactory;
+    }
 
     private static string NormalizeOrderBy(string field, string order)
     {
@@ -135,8 +146,212 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
         var sw = System.Diagnostics.Stopwatch.StartNew();
         try
         {
-            // 说明:存储过程签名原为 @Params varchar(max) = '',MySQL 侧通常为一个 IN 参数;这里按空参数调用。
-            await _db.Ado.ExecuteCommandAsync("CALL pr_MES_Report_DayWorkOrdDetailTotal_New('')");
+            var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
+
+            // ── Step 0: 取公司域 ──
+            var domain = await _db.Ado.GetStringAsync(
+                "SELECT Domain FROM GeneralizedCodeMaster WHERE FldName='SystemConfig' AND Val='CompanyCode' LIMIT 1");
+            domain = string.IsNullOrWhiteSpace(domain) ? "1001" : domain.Trim();
+
+            // ── 清空中间表与结果表 ──
+            await _db.Ado.ExecuteCommandAsync("DELETE FROM WorkOrdDetailTotal");
+            await _db.Ado.ExecuteCommandAsync("DELETE FROM WorkOrdDetailTotalKB");
+
+            // ── Step 1: 基础数据写入 WorkOrdDetailTotal ──
+            await _db.Ado.ExecuteCommandAsync("""
+                INSERT INTO WorkOrdDetailTotal(
+                    RowNum, Status, workord, Priority, OrdDate, morder_type, bom_number,
+                    Descr, Descr1, production_number, QtyRequired, supplier_name, ItemNum,
+                    supplier_number, ljsl
+                )
+                SELECT
+                    viewql.*,
+                    SUM(QtyRequired) OVER (
+                        PARTITION BY viewql.ItemNum
+                        ORDER BY viewql.OrdDate, viewql.RowNum, viewql.Priority
+                        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
+                    ) AS ljsl
+                FROM (
+                    SELECT
+                        a.*, b.morder_type, c.ItemNum AS bom_number, c.Descr, c.Descr1,
+                        b.morder_production_number, d.QtyRequired, e.supplier_name,
+                        d.ItemNum, e.supplier_number
+                    FROM (
+                        SELECT RowNum, Status, workord, Priority, OrdDate
+                        FROM (
+                            SELECT CONCAT('1.', CAST(ROW_NUMBER() OVER (PARTITION BY Status ORDER BY Priority ASC) AS CHAR(50))) AS RowNum,
+                                   Status, workord, Priority, OrdDate
+                            FROM WorkOrdMaster WHERE Status = 'W' AND tenant_id = @TenantId
+                            UNION ALL
+                            SELECT CONCAT('2.', CAST(ROW_NUMBER() OVER (PARTITION BY Status ORDER BY Priority ASC) AS CHAR(50))),
+                                   Status, workord, Priority, OrdDate
+                            FROM WorkOrdMaster WHERE Status = 'S' AND tenant_id = @TenantId
+                            UNION ALL
+                            SELECT CONCAT('3.', CAST(ROW_NUMBER() OVER (PARTITION BY Status ORDER BY Priority ASC) AS CHAR(50))),
+                                   Status, workord, Priority, OrdDate
+                            FROM WorkOrdMaster WHERE Status = 'r' AND tenant_id = @TenantId
+                            UNION ALL
+                            SELECT CONCAT('4.', CAST(ROW_NUMBER() OVER (PARTITION BY Status ORDER BY Priority ASC) AS CHAR(50))),
+                                   Status, workord, Priority, OrdDate
+                            FROM WorkOrdMaster WHERE Status = 'p' AND tenant_id = @TenantId
+                        ) AS workview
+                        WHERE DATE(OrdDate) >= DATE(NOW())
+                          AND DATE(OrdDate) <= DATE(DATE_ADD(NOW(), INTERVAL 14 DAY))
+                          AND (
+                              WorkOrd IN (SELECT WorkOrd FROM NbrMaster WHERE Type = 'SM' AND Status = '' GROUP BY WorkOrd)
+                              OR Status = 'p'
+                          )
+                    ) a
+                    LEFT JOIN mes_morder b ON a.workord = b.morder_no
+                    LEFT JOIN ItemMaster c ON b.product_code = c.ItemNum AND (c.tenant_id = @TenantId OR @TenantId IS NULL)
+                    LEFT JOIN WorkOrdDetail d ON a.WorkOrd = d.WorkOrd AND d.tenant_id = @TenantId
+                    LEFT JOIN (
+                        SELECT a0.number,
+                               GROUP_CONCAT(b0.SortName ORDER BY b0.SortName SEPARATOR ',') AS supplier_name,
+                               GROUP_CONCAT(b0.Cust    ORDER BY b0.Cust    SEPARATOR ',') AS supplier_number
+                        FROM srm_purchase a0
+                        JOIN CustMaster b0 ON a0.supplier_number = b0.Cust AND b0.tenant_id = a0.tenant_id
+                        WHERE a0.quota_rate > 0 AND a0.tenant_id = @TenantId
+                        GROUP BY a0.number
+                    ) e ON d.ItemNum = e.number
+                ) AS viewql
+                """, new List<SugarParameter> { new("@TenantId", tenantId) });
+
+            // ── Step 2: 在库 locStock(InvMaster 从 SQL Server 读取)──
+            await RefreshLocStockAsync(domain, tenantId);
+
+            // ── Step 3: 在途 ztsl / 在检 zjsl ──
+            await _db.Ado.ExecuteCommandAsync("""
+                UPDATE WorkOrdDetailTotal a
+                JOIN (
+                    SELECT ItemNum, SUM(ztsl) AS ztsl, SUM(zsl1) AS zjsl
+                    FROM (
+                        SELECT p.ItemNum,
+                               CASE WHEN IFNULL(p.zfhl, 0) <= IFNULL(p.zshl, 0) THEN 0
+                                    ELSE IFNULL(p.zfhl, 0) - IFNULL(p.zshl, 0) END AS ztsl,
+                               IFNULL(p.zsl, 0) AS zsl1
+                        FROM (
+                            SELECT p0.PurOrd, p0.Line, p0.ItemNum,
+                                   IFNULL(s0.fhsl, 0) - IFNULL(Do0.thsl, 0) AS zfhl,
+                                   p0.ReceiptQty + p0.RctQty AS zshl,
+                                   IFNULL(i0.zjsl, 0) AS zsl
+                            FROM PurOrdDetail p0
+                            LEFT JOIN LATERAL (
+                                SELECT SUM(sh_delivery_quantity) AS fhsl
+                                FROM scm_shdzb
+                                WHERE po_bill = p0.PurOrd AND po_billline = p0.Line
+                                  AND tenant_id = p0.tenant_id
+                            ) s0 ON TRUE
+                            LEFT JOIN LATERAL (
+                                SELECT SUM(Qty) AS zjsl
+                                FROM MissedPrint m
+                                WHERE m.Status = 'I' AND m.PurOrd = p0.PurOrd AND m.PurLine = p0.Line
+                            ) i0 ON TRUE
+                            LEFT JOIN LATERAL (
+                                SELECT SUM(t.QtyReturn) AS thsl
+                                FROM PurOrdRctDetail t
+                                WHERE t.OrdNbr = p0.PurOrd AND t.OrdLine = p0.Line
+                                  AND t.rcttype IN ('pt','temp')
+                                  AND t.tenant_id = p0.tenant_id
+                            ) Do0 ON TRUE
+                            WHERE p0.Status <> 'C' AND p0.tenant_id = @TenantId
+                        ) p
+                    ) viewpur
+                    GROUP BY ItemNum
+                ) b ON a.ItemNum = b.ItemNum
+                SET a.ztsl = IFNULL(b.ztsl, 0),
+                    a.zjsl = IFNULL(b.zjsl, 0)
+                """, new List<SugarParameter> { new("@TenantId", tenantId) });
+
+            // ── Step 4: 承诺 jqsl ──
+            await _db.Ado.ExecuteCommandAsync("""
+                UPDATE WorkOrdDetailTotal a
+                JOIN (
+                    SELECT IF(jqsl > schedqty, schedqty, jqsl) AS jqsl, ItemNum, jqhf
+                    FROM (
+                        SELECT SUM(CAST(IFNULL(jq.jhdsl, 0) AS DECIMAL(18,6))) AS jqsl,
+                               MAX(ds.schedqty) AS schedqty,
+                               ds.ItemNum, DATE(jq.jqhf) AS jqhf
+                        FROM scm_jhjh_jq jq
+                        INNER JOIN srm_polist_ds ds ON jq.glid = ds.id AND ds.tenant_id = jq.tenant_id
+                        LEFT JOIN (
+                            SELECT SUM(sh_delivery_quantity) AS sh_delivery_quantity, jhdbh
+                            FROM scm_shdzb
+                            GROUP BY jhdbh
+                        ) c ON ds.dsnum = c.jhdbh
+                        WHERE jq.flag = 0
+                          AND IFNULL(c.sh_delivery_quantity, 0) < ds.schedqty
+                          AND ds.status = 'P'
+                          AND ds.schedqty - ds.sentqty > 0
+                          AND DATE(jq.jqhf) > DATE(NOW())
+                        GROUP BY ds.ItemNum, DATE(jq.jqhf)
+                    ) jqview
+                ) b ON a.ItemNum = b.ItemNum AND DATE(a.OrdDate) = b.jqhf
+                SET a.jqsl = CAST(IFNULL(b.jqsl, 0) AS DECIMAL(18,6))
+                """);
+
+            // ── Step 5: 可用 / 欠料(按天数偏移累加)──
+            await _db.Ado.ExecuteCommandAsync("""
+                UPDATE WorkOrdDetailTotal
+                SET available = IFNULL(locStock, 0),
+                    shortage  = IFNULL(locStock, 0) - IFNULL(ljsl, 0)
+                WHERE DATE(OrdDate) = DATE(NOW()) OR DATE(OrdDate) = DATE(DATE_ADD(NOW(), INTERVAL 1 DAY))
+                """);
+            await _db.Ado.ExecuteCommandAsync("""
+                UPDATE WorkOrdDetailTotal
+                SET available = IFNULL(locStock, 0) + IFNULL(zjsl, 0),
+                    shortage  = IFNULL(locStock, 0) + IFNULL(zjsl, 0) - IFNULL(ljsl, 0)
+                WHERE DATE(OrdDate) = DATE(DATE_ADD(NOW(), INTERVAL 2 DAY))
+                """);
+            await _db.Ado.ExecuteCommandAsync("""
+                UPDATE WorkOrdDetailTotal
+                SET available = IFNULL(locStock, 0) + IFNULL(zjsl, 0) + IFNULL(ztsl, 0),
+                    shortage  = IFNULL(locStock, 0) + IFNULL(zjsl, 0) + IFNULL(ztsl, 0) - IFNULL(ljsl, 0)
+                WHERE DATE(OrdDate) = DATE(DATE_ADD(NOW(), INTERVAL 3 DAY))
+                """);
+            await _db.Ado.ExecuteCommandAsync("""
+                UPDATE WorkOrdDetailTotal
+                SET available = IFNULL(locStock, 0) + IFNULL(zjsl, 0) + IFNULL(ztsl, 0) + IFNULL(jqsl, 0),
+                    shortage  = IFNULL(locStock, 0) + IFNULL(zjsl, 0) + IFNULL(ztsl, 0) + IFNULL(jqsl, 0) - IFNULL(ljsl, 0)
+                WHERE DATE(OrdDate) >= DATE(DATE_ADD(NOW(), INTERVAL 4 DAY))
+                  AND DATE(OrdDate) <= DATE(DATE_ADD(NOW(), INTERVAL 14 DAY))
+                """);
+
+            // ── Step 6: 透视写入 KB 表 ──
+            await _db.Ado.ExecuteCommandAsync("""
+                INSERT INTO WorkOrdDetailTotalKB (
+                    supplier_number, supplier_name, ItemNum, Descr, Descr1,
+                    D0, D1, D2, D3, D4, D5, D6, D7, D8, D9, D10, D11, D12, D13, D14
+                )
+                SELECT
+                    IFNULL(supplier_number, '无'), IFNULL(supplier_name, '无'),
+                    ItemNum, Descr, Descr1,
+                    IFNULL(SUM(CASE WHEN daynum = 0  THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 1  THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 2  THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 3  THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 4  THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 5  THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 6  THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 7  THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 8  THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 9  THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 10 THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 11 THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 12 THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 13 THEN ABS(shortage) END), 0),
+                    IFNULL(SUM(CASE WHEN daynum = 14 THEN ABS(shortage) END), 0)
+                FROM (
+                    SELECT DATEDIFF(DATE(a.OrdDate), DATE(NOW())) AS daynum,
+                           a.supplier_number, a.supplier_name, a.ItemNum,
+                           b.Descr, b.Descr1, a.shortage
+                    FROM WorkOrdDetailTotal a
+                    LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum
+                    WHERE a.shortage < 0
+                ) src
+                GROUP BY supplier_number, supplier_name, ItemNum, Descr, Descr1
+                """);
+
             return new { ok = true, elapsedMs = sw.ElapsedMilliseconds };
         }
         catch (Exception ex)
@@ -145,6 +360,96 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
         }
     }
 
+    /// <summary>
+    /// Step 2:从 SQL Server 的 InvMaster 读取在库库存,更新 WorkOrdDetailTotal.locStock。
+    /// </summary>
+    private async Task RefreshLocStockAsync(string domain, long tenantId)
+    {
+        // 读取 MRPLocationRange 配置(逗号分隔的库位列表)
+        var locRange = await _db.Ado.GetStringAsync(
+            "SELECT Val FROM ConfigurationItem WHERE FldName='MRPLocationRange' LIMIT 1");
+        if (string.IsNullOrWhiteSpace(locRange))
+            return;
+
+        var locations = locRange.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+        if (locations.Length == 0)
+            return;
+
+        // 通过中台连接 SQL Server
+        var remote = await _scopeFactory.GetScopeAsync(SqlServerSourceCode);
+
+        // 构造参数化 IN 查询
+        var locParams = new List<string>();
+        var pars = new List<SugarParameter> { new("@Domain", domain) };
+        for (var i = 0; i < locations.Length; i++)
+        {
+            locParams.Add($"@loc{i}");
+            pars.Add(new SugarParameter($"@loc{i}", locations[i]));
+        }
+
+        // SQL Server 查询 InvMaster(ISNULL 语法)
+        var invSql = $"""
+            SELECT ItemNum, SUM(ISNULL(AvailStatusQty, 0) + ISNULL(Assay, 0)) AS locStock
+            FROM InvMaster
+            WHERE Domain = @Domain
+              AND Location IN ({string.Join(",", locParams)})
+            GROUP BY ItemNum
+            """;
+
+        var rows = await remote.Ado.SqlQueryAsync<LocStockRow>(invSql, pars);
+        if (rows.Count == 0)
+            return;
+
+        // 批量写入临时表,再单次 JOIN UPDATE(比逐行 UPDATE 快得多)
+        await _db.Ado.ExecuteCommandAsync(
+            "CREATE TABLE IF NOT EXISTS _tmp_loc_stock (ItemNum VARCHAR(50) PRIMARY KEY, locStock DECIMAL(18,6))");
+        await _db.Ado.ExecuteCommandAsync("TRUNCATE TABLE _tmp_loc_stock");
+
+        var insertPars = new List<SugarParameter>();
+        var valuesClauses = new List<string>();
+        for (var i = 0; i < rows.Count; i++)
+        {
+            var r = rows[i];
+            if (string.IsNullOrWhiteSpace(r.ItemNum)) continue;
+            valuesClauses.Add($"(@item{i}, @stock{i})");
+            insertPars.Add(new SugarParameter($"@item{i}", r.ItemNum));
+            insertPars.Add(new SugarParameter($"@stock{i}", r.LocStock));
+        }
+
+        if (valuesClauses.Count == 0)
+            return;
+
+        // 分批 INSERT(每批 500 条,避免参数过多)
+        const int batchSize = 500;
+        for (var offset = 0; offset < insertPars.Count; offset += batchSize * 2)
+        {
+            var batchEnd = Math.Min(offset + batchSize * 2, insertPars.Count);
+            var batchPars = insertPars.GetRange(offset, batchEnd - offset);
+            var batchValues = new List<string>();
+            for (var j = offset; j < batchEnd; j += 2)
+                batchValues.Add($"({insertPars[j].ParameterName}, {insertPars[j + 1].ParameterName})");
+
+            await _db.Ado.ExecuteCommandAsync(
+                $"INSERT INTO _tmp_loc_stock (ItemNum, locStock) VALUES {string.Join(",", batchValues)}",
+                batchPars);
+        }
+
+        // 单次 JOIN UPDATE
+        await _db.Ado.ExecuteCommandAsync("""
+            UPDATE WorkOrdDetailTotal a
+            INNER JOIN _tmp_loc_stock t ON a.ItemNum = t.ItemNum
+            SET a.locStock = t.locStock
+            """);
+
+        await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_loc_stock");
+    }
+
+    private sealed class LocStockRow
+    {
+        public string? ItemNum { get; set; }
+        public decimal LocStock { get; set; }
+    }
+
     /// <summary>
     /// 创建催料待办:快照当前 WorkOrdDetailTotalKB 数据,返回新记录 Id(前端再用此 Id 发起审批流)
     /// </summary>