From 699310e61dd5bc9974b0bfdbfbdf44c887c6a00c Mon Sep 17 00:00:00 2001
From: zgw <13623675548@163.com>
Date: Mon, 7 Sep 2026 12:43:43 +0800
Subject: [PATCH] fix(shadow): align outbound predicates with site data for
v3.3
Use the business project and captured site predicates, validate single-stock proposals, and skip unsupported scopes without changing Legacy. Add the guarded V3.3 SQL upgrade, CMD operations, regression tests and deployment runbook.
---
doc/algorithm-shadow/README.md | 2 +
doc/algorithm-shadow/SHADOW_V33_DEPLOYMENT.md | 114 ++++
.../db/sqlserver/uninstall-shadow.sql | 3 +-
.../db/sqlserver/upgrade-shadow-v3-to-v33.sql | 557 ++++++++++++++++++
.../repository/ShadowV33MigrationTest.java | 56 ++
ruoyi-admin/pom.xml | 6 +
.../shadow/ShadowPlanningEngine.java | 91 ++-
.../shadow/ShadowStockReference.java | 29 +
.../shadow/mapper/ShadowInputMapper.java | 4 +
.../mapper/algorithm/ShadowInputMapper.xml | 58 +-
.../shadow/ShadowOutboundPredicateTest.java | 283 +++++++++
.../shadow/ShadowOutboundValidationTest.java | 158 +++++
.../shadow/ShadowPlanningEngineTest.java | 7 +
.../shadow/ShadowV3OperationsPackageTest.java | 24 +-
.../resources/shadow/Sqlcmd13OutputFixture.cs | 4 +
scripts/shadow-v3-install.cmd | 14 +-
scripts/shadow-v3-preflight.cmd | 7 +-
scripts/shadow-v3-uninstall.cmd | 2 +-
18 files changed, 1394 insertions(+), 25 deletions(-)
create mode 100644 doc/algorithm-shadow/SHADOW_V33_DEPLOYMENT.md
create mode 100644 fortucky-algorithm/src/main/resources/db/sqlserver/upgrade-shadow-v3-to-v33.sql
create mode 100644 fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowV33MigrationTest.java
create mode 100644 ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowOutboundPredicateTest.java
create mode 100644 ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowOutboundValidationTest.java
diff --git a/doc/algorithm-shadow/README.md b/doc/algorithm-shadow/README.md
index 7c192a2..3f1b518 100644
--- a/doc/algorithm-shadow/README.md
+++ b/doc/algorithm-shadow/README.md
@@ -1,5 +1,7 @@
# FT546WMS 影子 v3 现场操作手册
+已部署 V3/V3.1/V3.2 的现场升级到 V3.3,请先使用 [V3.3 升级和回滚步骤](SHADOW_V33_DEPLOYMENT.md)。下面保留最初 v2 到 v3 的历史操作说明,不要把“当前现场已有 v2”套用于已升级现场。
+
## 1. 这次部署做什么
目标分支:`test/ft546wms-shadow-v3-20260903`。
diff --git a/doc/algorithm-shadow/SHADOW_V33_DEPLOYMENT.md b/doc/algorithm-shadow/SHADOW_V33_DEPLOYMENT.md
new file mode 100644
index 0000000..cd305fc
--- /dev/null
+++ b/doc/algorithm-shadow/SHADOW_V33_DEPLOYMENT.md
@@ -0,0 +1,114 @@
+# FT546WMS 影子 V3.3:升级和回滚
+
+适用现场:WMS 已运行影子 V3/V3.1/V3.2,数据库包版本为 `2026-09-03-3`。
+分支仍为 `test/ft546wms-shadow-v3-20260903`。这不是正式算法切换。
+
+## 这次改什么
+
+自动出库影子请求的项目号改取出库明细 `spare4`,不再取仓库配置中的 `FT546`。
+补齐产品、spare1/2/3、项目、fid 对电压、unit 对相别的可选筛选,以及库位锁定、状态、托盘一致性检查。
+按计划数量减已分配数量观察。新旧输入通过 `predicate_version` 区分,不把历史样本重新认定为新版本通过。
+
+共享规划器和 Legacy 存储过程、调用入口没有修改。升级只新增 `alg` 表的可空字段、替换影子刷新过程、使旧快照失效并更新影子包版本。保留决策日志和累计指标。
+
+注意:影子后台仍会增加数据库读取和计算开销,不能承诺对性能绝对零影响。升级过程需要一次计划内短暂停机。
+
+## 第一步:先准备,不停程序
+
+让服务器 AI 核实 IDEA 当前运行的 **WMS** 项目目录、分支、提交号、环境变量和日志目录,备份本地配置,并导出现有影子日志。
+不要操作 WCS 或 WCS 影子程序,也不要因进程里有 `wcs` 字样就停止它。
+
+在已经确认的 WMS 项目根目录打开 CMD,执行:
+
+```cmd
+git status --short
+git branch --show-current
+git rev-parse HEAD
+git fetch origin
+```
+
+保存当前提交号作为回退点。工作区有改动时先停止同步,让服务器 AI 保存和核对改动,不要强制覆盖。
+获得本次已推送的交付提交号后,再执行 `git pull --ff-only` 并核对 `git rev-parse HEAD`。
+本地未推送的代码不能靠服务器 `git pull` 获得,不要猜提交号。
+
+IDEA 重新加载 Maven,使用当前可用的 JDK 17 和 Maven 3.9.11 编译测试。
+首次可能需要下载 H2 测试依赖;H2 不进入运行包,依赖问题应在停机前解决。
+程序运行期间不要点 Build Project、热替换或重启;可在独立准备目录做构建,确认完成后再安排维护窗口。
+
+## 第二步:先在数据库备份副本演练
+
+本地 Java 测试和 SQL 2016 语法解析不能代替 SQL Server 实际升级验证。
+让服务器 AI/DBA 在隔离 SQL Server 上恢复备份(数据库名保持 `FT546-wms`),先验证升级成功、重复执行无破坏、升级失败可回滚。
+不要在生产库创建“测试作业”,不要触发业务回放。演练失败就不进入下一步。
+
+## 第三步:低峰时停止 WMS,升级影子表
+
+1. 在 IDEA 停止当前那个 WMS 实例,确认其 PID 已退出。不要同时启动稳定 WMS 和影子 WMS。
+2. 等待至少 130 秒,确认没有其他 WMS 影子实例仍在运行。
+3. 在项目根目录的 CMD 执行下面命令(使用现有 Windows 集成认证):
+
+```cmd
+call scripts\shadow-v3-install.cmd "10.6.22.6,1433" "FT546-wms" v33
+```
+
+必须看到 `V33_UPGRADE_OK`(重复执行为 `V33_ALREADY_INSTALLED`)和 `INSTALL_OK mode=v33`。
+现场已有 V3,**不要用 `clean`,不要卸载后重装,不要执行旧版 v2 升级来绕过错误**。
+
+遇到心跳、清单、字段或锁超时错误,保存完整输出并停止。脚本对本次变更执行事务回滚;不要修改 manifest 绕过检查。
+
+## 第四步:只启动一次,保持影子配置
+
+沿用现场已有 Run Configuration,只核对下面两个值,其他连接、日志和限流设置不要随意覆盖:
+
+```text
+FORTUCKY_SHADOW_ENABLED=true
+FORTUCKY_ALGORITHM_MODE=SHADOW
+```
+
+在 IDEA 启动一次 WMS。记录启动时间和时区。不要设置 `ACTIVE`。
+旧快照已失效,后台会重新生成;刚启动还没有新快照或样本,不代表需要再次重启。
+
+先检查登录、查询,再由现场人员按正常流程验证入库、自动出库、手动出库。
+让服务器 AI 仅做只读核对:
+
+```sql
+SELECT package_id, package_version FROM alg.shadow_package_manifest;
+SELECT warehouse_id, snapshot_generation, snapshot_status, synchronized_at, last_error
+FROM alg.shadow_snapshot_state WHERE warehouse_id = '01';
+SELECT TOP (20) stockout_detail_id, project_no, product_id, requested_quantity,
+ allocated_quantity, predicate_version, synchronized_at
+FROM alg.shadow_outbound_request_snapshot
+WHERE warehouse_id = '01' ORDER BY synchronized_at DESC;
+SELECT TOP (30) * FROM alg.decision_log ORDER BY created_at DESC;
+```
+
+包版本应为 `2026-09-07-33`,新快照应为 `PUBLISHED`,正常长度的请求输入应有 `predicate_version=1`。
+实际自动出库产生后,新日志应包含 `predicateVersion=1`;可用建议还应有真实的 `proposedStock` 和 `selectedProject`。
+快照刷新由事件触发,没业务时同步时间不连续推进,不要仅凭这个重启。
+
+## 出问题怎么回滚
+
+最短路径:停止 WMS,在原 IDEA 配置里只把 `FORTUCKY_SHADOW_ENABLED` 改为 `false`,完整重启一次,再检查正常业务。
+如仍异常,停止这个实例,使用第一步保留的原稳定源码和 Run Configuration 启动,仍保持影子关闭。
+不要两个 WMS 同时运行;不要执行 `git reset --hard` 来覆盖现场配置。
+
+紧急回滚不卸载数据库。新增的 `alg` 字段不参与 Legacy;保留它们和历史数据供排查。
+若回到旧影子代码,也先关闭影子,避免用旧逻辑继续采集混杂样本。
+
+确实需要完整卸载时,必须先备份影子数据、恢复稳定业务并停用影子,等待至少 130 秒,再由 DBA 执行:
+
+```cmd
+call scripts\shadow-v3-uninstall.cmd "10.6.22.6,1433" "FT546-wms" CONFIRM_UNINSTALL
+```
+
+卸载会删除全部本包 `alg` 对象及历史数据,只能从事先导出的证据/数据库备份恢复。它不是升级的必需步骤。
+
+## 后续怎样判断
+
+重新记录 V3.3 开始时间,只分析此次重启后的自然业务样本,建议覆盖至少 7 天及高峰。
+继续使用 `shadow-v3-export.cmd`,让服务器 AI 同时提供准确分支、提交、IDEA 配置核对结果、完整日志和本次升级输出。
+累计性能指标包含旧版本,必须用升级前后导出差值,不能当成 V3.3 单独指标。
+
+本版保守跳过:多项目/多产品候选、候选超过 200 行、建议不在合格候选集、一托多库存行、单条库存不足以覆盖剩余数量,以及托盘已有作业。
+现场动态 SQL 参数限长 80 字节;超长原始条件保留在快照,但标记版本 0,不作为有效输入。不会截断条件来凑匹配。
+这些跳过不算通过。`READY` 只表示受支持范围内的库存建议通过核验,不证明完整出库流程、FIFO 效果、性能或正式切换已验收。
diff --git a/fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql b/fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql
index 8194ebf..d5aaf6c 100644
--- a/fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql
+++ b/fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql
@@ -2,7 +2,7 @@ SET NOCOUNT ON;
SET XACT_ABORT ON;
IF SCHEMA_ID(N'alg') IS NULL THROW 51010, 'FT546 shadow uninstall refused: alg schema does not exist', 1;
IF OBJECT_ID(N'alg.shadow_package_manifest', N'U') IS NULL THROW 51011, 'FT546 shadow uninstall refused: shadow package manifest does not match', 1;
-IF (SELECT COUNT(*) FROM alg.shadow_package_manifest WHERE package_id='FT546WMS_SHADOW_ONLY' AND package_version='2026-09-03-3')<>1 OR (SELECT COUNT(*) FROM alg.shadow_package_manifest)<>1 THROW 51012, 'FT546 shadow uninstall refused: shadow package manifest does not match', 1;
+IF (SELECT COUNT(*) FROM alg.shadow_package_manifest WHERE package_id='FT546WMS_SHADOW_ONLY' AND package_version IN ('2026-09-03-3','2026-09-07-33'))<>1 OR (SELECT COUNT(*) FROM alg.shadow_package_manifest)<>1 THROW 51012, 'FT546 shadow uninstall refused: shadow package manifest does not match', 1;
DECLARE @expected_objects TABLE (object_name sysname NOT NULL PRIMARY KEY, object_type char(2) NOT NULL);
INSERT INTO @expected_objects VALUES
(N'shadow_package_manifest','U'),(N'warehouse_policy','U'),(N'project_policy','U'),(N'space_role','U'),(N'space_topology','U'),(N'inventory_item_snapshot','U'),(N'space_inventory_summary','U'),(N'lane_state','U'),(N'reservation','U'),(N'deep_workflow_lock','U'),(N'decision_log','U'),(N'shadow_snapshot_state','U'),(N'cargo_space_snapshot','U'),(N'active_job_snapshot','U'),(N'shadow_inbound_request_snapshot','U'),(N'shadow_outbound_request_snapshot','U'),(N'shadow_runtime_metric','U'),(N'shadow_instance_heartbeat','U'),
@@ -11,6 +11,7 @@ IF EXISTS (SELECT 1 FROM sys.objects object LEFT JOIN @expected_objects expected
IF EXISTS (SELECT 1 FROM @expected_objects expected LEFT JOIN sys.objects object ON object.schema_id=SCHEMA_ID(N'alg') AND object.name=expected.object_name AND object.type COLLATE DATABASE_DEFAULT=expected.object_type WHERE object.object_id IS NULL) THROW 51014, 'FT546 shadow uninstall refused: expected object is missing or has wrong type', 1;
BEGIN TRY
BEGIN TRANSACTION;
+ IF EXISTS (SELECT 1 FROM alg.shadow_instance_heartbeat WITH (UPDLOCK, HOLDLOCK) WHERE last_seen_at >= DATEADD(SECOND,-120,SYSUTCDATETIME()) OR claimed_at >= DATEADD(SECOND,-120,SYSUTCDATETIME())) THROW 51015, 'FT546 shadow uninstall refused: recent heartbeat; stop WMS and wait over 120 seconds', 1;
DROP PROCEDURE alg.usp_refresh_shadow_snapshot; DROP PROCEDURE alg.usp_claim_shadow_instance; DROP PROCEDURE alg.usp_refresh_dirty_lanes; DROP PROCEDURE alg.usp_rebuild_lane_state; DROP PROCEDURE alg.usp_compute_lane_semantic_fingerprint; DROP PROCEDURE alg.usp_sync_inventory_snapshot; DROP PROCEDURE alg.usp_rebuild_space_topology;
DROP VIEW alg.v_space_state; DROP VIEW alg.v_active_job; DROP VIEW alg.v_inventory_item;
DROP TABLE alg.shadow_instance_heartbeat; DROP TABLE alg.shadow_runtime_metric; DROP TABLE alg.shadow_outbound_request_snapshot; DROP TABLE alg.shadow_inbound_request_snapshot; DROP TABLE alg.active_job_snapshot; DROP TABLE alg.cargo_space_snapshot; DROP TABLE alg.shadow_snapshot_state; DROP TABLE alg.decision_log; DROP TABLE alg.deep_workflow_lock; DROP TABLE alg.reservation; DROP TABLE alg.lane_state; DROP TABLE alg.space_inventory_summary; DROP TABLE alg.inventory_item_snapshot; DROP TABLE alg.space_topology; DROP TABLE alg.space_role; DROP TABLE alg.project_policy; DROP TABLE alg.warehouse_policy; DROP TABLE alg.shadow_package_manifest;
diff --git a/fortucky-algorithm/src/main/resources/db/sqlserver/upgrade-shadow-v3-to-v33.sql b/fortucky-algorithm/src/main/resources/db/sqlserver/upgrade-shadow-v3-to-v33.sql
new file mode 100644
index 0000000..a8bdc32
--- /dev/null
+++ b/fortucky-algorithm/src/main/resources/db/sqlserver/upgrade-shadow-v3-to-v33.sql
@@ -0,0 +1,557 @@
+/*
+ FT546WMS shadow V3/V3.1/V3.2 -> V3.3, SQL Server 2016 RTM compatible.
+ Run only after stopping the WMS shadow-enabled JVM and waiting >120 seconds.
+ Changes only alg.*. Never executes or alters a Legacy procedure.
+ One transaction: any failure rolls back columns, procedure, state and manifest.
+*/
+SET NOCOUNT ON;
+SET XACT_ABORT ON;
+SET LOCK_TIMEOUT 5000;
+SET DEADLOCK_PRIORITY LOW;
+
+IF DB_NAME() <> N'FT546-wms'
+ THROW 51060, 'V3.3 upgrade refused: expected FT546-wms database', 1;
+IF SCHEMA_ID(N'alg') IS NULL OR OBJECT_ID(N'alg.shadow_package_manifest', N'U') IS NULL
+ THROW 51061, 'V3.3 upgrade refused: shadow package manifest is missing', 1;
+
+DECLARE @expected TABLE (object_name sysname PRIMARY KEY, object_type char(2) NOT NULL);
+INSERT INTO @expected VALUES
+(N'shadow_package_manifest','U'),(N'warehouse_policy','U'),(N'project_policy','U'),
+(N'space_role','U'),(N'space_topology','U'),(N'inventory_item_snapshot','U'),
+(N'space_inventory_summary','U'),(N'lane_state','U'),(N'reservation','U'),
+(N'deep_workflow_lock','U'),(N'decision_log','U'),(N'shadow_snapshot_state','U'),
+(N'cargo_space_snapshot','U'),(N'active_job_snapshot','U'),
+(N'shadow_inbound_request_snapshot','U'),(N'shadow_outbound_request_snapshot','U'),
+(N'shadow_runtime_metric','U'),(N'shadow_instance_heartbeat','U'),
+(N'v_inventory_item','V'),(N'v_active_job','V'),(N'v_space_state','V'),
+(N'usp_rebuild_space_topology','P'),(N'usp_sync_inventory_snapshot','P'),
+(N'usp_compute_lane_semantic_fingerprint','P'),(N'usp_rebuild_lane_state','P'),
+(N'usp_refresh_dirty_lanes','P'),(N'usp_claim_shadow_instance','P'),
+(N'usp_refresh_shadow_snapshot','P');
+
+DECLARE @columns TABLE (
+ table_name sysname, column_name sysname, type_name sysname, max_length smallint,
+ precision_value tinyint, scale_value tinyint, PRIMARY KEY (table_name, column_name));
+INSERT INTO @columns VALUES
+(N'inventory_item_snapshot',N'spare1',N'varchar',255,0,0),
+(N'inventory_item_snapshot',N'spare2',N'varchar',255,0,0),
+(N'inventory_item_snapshot',N'spare3',N'varchar',255,0,0),
+(N'inventory_item_snapshot',N'voltage',N'varchar',255,0,0),
+(N'inventory_item_snapshot',N'phase',N'varchar',255,0,0),
+(N'inventory_item_snapshot',N'tray_job_count',N'int',4,10,0),
+(N'shadow_outbound_request_snapshot',N'spare1',N'varchar',255,0,0),
+(N'shadow_outbound_request_snapshot',N'spare2',N'varchar',255,0,0),
+(N'shadow_outbound_request_snapshot',N'spare3',N'varchar',255,0,0),
+(N'shadow_outbound_request_snapshot',N'voltage',N'varchar',255,0,0),
+(N'shadow_outbound_request_snapshot',N'phase',N'varchar',255,0,0),
+(N'shadow_outbound_request_snapshot',N'allocated_quantity',N'decimal',17,38,6),
+(N'shadow_outbound_request_snapshot',N'predicate_version',N'int',4,10,0);
+
+BEGIN TRY
+ BEGIN TRANSACTION;
+ DECLARE @version varchar(40);
+ SELECT @version = package_version
+ FROM alg.shadow_package_manifest WITH (UPDLOCK, HOLDLOCK)
+ WHERE package_id = 'FT546WMS_SHADOW_ONLY';
+ IF @version IS NULL OR @version NOT IN ('2026-09-03-3', '2026-09-07-33')
+ OR (SELECT COUNT(*) FROM alg.shadow_package_manifest) <> 1
+ THROW 51062, 'V3.3 upgrade refused: exact shadow manifest does not match', 1;
+
+ IF EXISTS (
+ SELECT 1 FROM sys.objects object
+ LEFT JOIN @expected expected ON expected.object_name = object.name
+ AND expected.object_type = object.type COLLATE DATABASE_DEFAULT
+ WHERE object.schema_id = SCHEMA_ID(N'alg') AND object.is_ms_shipped = 0
+ AND object.parent_object_id = 0 AND expected.object_name IS NULL)
+ THROW 51063, 'V3.3 upgrade refused: unknown object exists in alg schema', 1;
+ IF EXISTS (
+ SELECT 1 FROM @expected expected
+ LEFT JOIN sys.objects object ON object.schema_id = SCHEMA_ID(N'alg')
+ AND object.name = expected.object_name
+ AND object.type COLLATE DATABASE_DEFAULT = expected.object_type
+ WHERE object.object_id IS NULL)
+ THROW 51064, 'V3.3 upgrade refused: expected object is missing or has wrong type', 1;
+ IF EXISTS (SELECT 1 FROM sys.triggers trigger_object
+ JOIN sys.objects parent_object ON parent_object.object_id = trigger_object.parent_id
+ WHERE parent_object.schema_id = SCHEMA_ID(N'alg'))
+ THROW 51065, 'V3.3 upgrade refused: unexpected trigger on shadow table', 1;
+
+ IF EXISTS (SELECT 1 FROM alg.shadow_instance_heartbeat WITH (UPDLOCK, HOLDLOCK)
+ WHERE last_seen_at >= DATEADD(SECOND, -120, SYSUTCDATETIME())
+ OR claimed_at >= DATEADD(SECOND, -120, SYSUTCDATETIME()))
+ THROW 51066, 'V3.3 upgrade refused: recent shadow heartbeat; stop WMS and wait over 120 seconds', 1;
+
+ IF @version = '2026-09-07-33'
+ BEGIN
+ IF EXISTS (
+ SELECT 1 FROM @columns expected
+ LEFT JOIN sys.columns column_info
+ ON column_info.object_id = OBJECT_ID(N'alg.' + expected.table_name)
+ AND column_info.name = expected.column_name
+ WHERE column_info.column_id IS NULL
+ OR TYPE_NAME(column_info.user_type_id) <> expected.type_name
+ OR column_info.max_length <> expected.max_length
+ OR column_info.precision <> expected.precision_value
+ OR column_info.scale <> expected.scale_value
+ OR column_info.is_nullable <> 1)
+ THROW 51067, 'V3.3 upgrade refused: target columns are missing or incompatible', 1;
+ COMMIT TRANSACTION;
+ PRINT 'V33_ALREADY_INSTALLED';
+ RETURN;
+ END;
+
+ IF EXISTS (
+ SELECT 1 FROM @columns expected JOIN sys.columns column_info
+ ON column_info.object_id = OBJECT_ID(N'alg.' + expected.table_name)
+ AND column_info.name = expected.column_name)
+ THROW 51068, 'V3.3 upgrade refused: partial or foreign predicate columns already exist', 1;
+
+ ALTER TABLE alg.inventory_item_snapshot ADD
+ spare1 varchar(255) NULL, spare2 varchar(255) NULL, spare3 varchar(255) NULL,
+ voltage varchar(255) NULL, phase varchar(255) NULL, tray_job_count int NULL;
+ ALTER TABLE alg.shadow_outbound_request_snapshot ADD
+ spare1 varchar(255) NULL, spare2 varchar(255) NULL, spare3 varchar(255) NULL,
+ voltage varchar(255) NULL, phase varchar(255) NULL,
+ allocated_quantity decimal(38,6) NULL, predicate_version int NULL;
+
+ DECLARE @refresh_sql nvarchar(max) = N'ALTER PROCEDURE alg.usp_refresh_shadow_snapshot
+ @warehouse_id varchar(80),
+ @snapshot_generation bigint,
+ @instance_id varchar(80)
+AS
+BEGIN
+ SET NOCOUNT ON;
+ SET XACT_ABORT ON;
+
+ IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '''') IS NULL
+ THROW 51006, ''warehouse_id is required'', 1;
+ IF NULLIF(LTRIM(RTRIM(@instance_id)), '''') IS NULL
+ THROW 51007, ''instance_id is required'', 1;
+ IF @snapshot_generation <= 0
+ THROW 51008, ''snapshot_generation must be positive'', 1;
+
+ DECLARE @pre_source_count bigint, @post_source_count bigint,
+ @pre_source_signature varbinary(32), @post_source_signature varbinary(32),
+ @source_payload nvarchar(max), @synchronized_at datetime2,
+ @numeric_warehouse_id decimal(38,0),
+ @current_generation bigint;
+
+ IF LTRIM(RTRIM(@warehouse_id)) NOT LIKE ''%[^0-9]%''
+ SET @numeric_warehouse_id = TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(@warehouse_id)));
+
+ BEGIN TRY
+ BEGIN TRANSACTION;
+
+ SELECT @current_generation = state.snapshot_generation
+ FROM alg.shadow_snapshot_state state WITH (UPDLOCK, HOLDLOCK)
+ WHERE state.warehouse_id = @warehouse_id;
+
+ IF @current_generation IS NOT NULL AND @snapshot_generation <= @current_generation
+ THROW 51009, ''snapshot_generation must advance the published generation'', 1;
+
+ SELECT @pre_source_count =
+ (SELECT COUNT_BIG(*) FROM dbo.bas_cargo_space cargo WITH (NOLOCK) WHERE cargo.warehouse_id = @warehouse_id)
+ + (SELECT COUNT_BIG(*) FROM dbo.bas_stock stock WITH (NOLOCK)
+ JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id
+ WHERE cargo.warehouse_id = @warehouse_id)
+ + (SELECT COUNT_BIG(*) FROM dbo.mydo_job job WITH (NOLOCK) WHERE job.warehouse_id = @warehouse_id)
+ + (SELECT COUNT_BIG(*) FROM dbo.mydo_job_detail detail WITH (NOLOCK)
+ JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
+ WHERE job.warehouse_id = @warehouse_id)
+ + (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
+ JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
+ JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
+ JOIN dbo.in_stockin stockin WITH (NOLOCK) ON stockin.stockin_id = inbound.stockin_id
+ WHERE (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '''')
+ AND (LTRIM(RTRIM(stockin.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
+ OR (@numeric_warehouse_id IS NOT NULL
+ AND LTRIM(RTRIM(stockin.warehouse_id)) NOT LIKE ''%[^0-9]%''
+ AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockin.warehouse_id))) = @numeric_warehouse_id)))
+ + (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
+ JOIN dbo.out_stockout stockout WITH (NOLOCK) ON stockout.stockout_id = outbound.stockout_id
+ WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
+ AND (LTRIM(RTRIM(stockout.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
+ OR (@numeric_warehouse_id IS NOT NULL
+ AND LTRIM(RTRIM(stockout.warehouse_id)) NOT LIKE ''%[^0-9]%''
+ AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockout.warehouse_id))) = @numeric_warehouse_id)));
+
+ SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), (
+ SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id],
+ cargo.group_coding_no AS [@lane_code], cargo.cargo_area_id AS [@cargo_area_id],
+ cargo.roadway_id AS [@roadway_id], cargo.cargo_space_name AS [@display_name],
+ cargo.the_platoon AS [@platoon], cargo.the_column AS [@column_no],
+ cargo.the_floor AS [@floor_no], cargo.group_coding AS [@depth_raw],
+ cargo.status AS [@space_status], cargo.locked AS [@locked], cargo.tray_code AS [@tray_code]
+ FROM dbo.bas_cargo_space cargo WITH (NOLOCK)
+ WHERE cargo.warehouse_id = @warehouse_id
+ ORDER BY cargo.cargo_space_id
+ FOR XML PATH(''cargo''), ROOT(''cargo_spaces''), TYPE
+ )), '''');
+
+ SET @source_payload = @source_payload + ''|STOCK|'' + ISNULL(CONVERT(nvarchar(max), (
+ SELECT stock.stock_id AS [@stock_id], stock.cargo_space_id AS [@space_id],
+ stock.warehouse_id AS [@warehouse_id], stock.tray_code AS [@tray_code],
+ stock.spare4 AS [@project_no], stock.product_id AS [@product_no],
+ stock.lot_no AS [@lot_no], stock.in_date AS [@in_date],
+ stock.quantity AS [@quantity], stock.out_quantity AS [@out_quantity],
+ stock.spare1 AS [@spare1], stock.spare2 AS [@spare2], stock.spare3 AS [@spare3],
+ stock.voltage AS [@voltage], stock.phase AS [@phase],
+ (SELECT COUNT(*) FROM dbo.mydo_job trayJob WITH (NOLOCK)
+ WHERE trayJob.tray_code = stock.tray_code) AS [@tray_job_count]
+ FROM dbo.bas_stock stock WITH (NOLOCK)
+ JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id
+ WHERE cargo.warehouse_id = @warehouse_id
+ ORDER BY stock.stock_id
+ FOR XML PATH(''stock''), ROOT(''stocks''), TYPE
+ )), '''');
+
+ SET @source_payload = @source_payload + ''|JOB|'' + ISNULL(CONVERT(nvarchar(max), (
+ SELECT job.job_id AS [@job_id], job.status AS [@status], job.task_no AS [@task_no],
+ job.tray_code AS [@tray_code], job.cargo_space_id AS [@source_space_id],
+ job.target_cargo_space_id AS [@target_space_id], job.in_or_out AS [@in_or_out],
+ job.create_time AS [@create_time], job.finish_time AS [@finish_time],
+ job.warehouse_id AS [@warehouse_id]
+ FROM dbo.mydo_job job WITH (NOLOCK)
+ WHERE job.warehouse_id = @warehouse_id
+ ORDER BY job.job_id
+ FOR XML PATH(''job''), ROOT(''jobs''), TYPE
+ )), '''');
+
+ SET @source_payload = @source_payload + ''|JOB_DETAIL|'' + ISNULL(CONVERT(nvarchar(max), (
+ SELECT detail.job_detail_id AS [@job_detail_id], detail.job_id AS [@job_id],
+ detail.stock_id AS [@stock_id], detail.lot_no AS [@lot_no],
+ detail.in_date AS [@in_date], detail.in_or_out_id AS [@request_id],
+ detail.in_or_out_d_id AS [@request_detail_id], detail.product_id AS [@product_id],
+ detail.stock_status AS [@stock_status], detail.in_quantity AS [@in_quantity],
+ detail.stock_quantity AS [@stock_quantity], detail.out_quantity AS [@out_quantity],
+ detail.pick_quantity AS [@pick_quantity], detail.stockin_detail_id AS [@stockin_detail_id]
+ FROM dbo.mydo_job_detail detail WITH (NOLOCK)
+ JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
+ WHERE job.warehouse_id = @warehouse_id
+ ORDER BY detail.job_detail_id
+ FOR XML PATH(''job_detail''), ROOT(''job_details''), TYPE
+ )), '''');
+
+ SET @source_payload = @source_payload + ''|INBOUND|'' + ISNULL(CONVERT(nvarchar(max), (
+ SELECT inbound.stockin_detail_id AS [@stockin_detail_id], inbound.stockin_guid AS [@stockin_guid],
+ inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no],
+ inbound.product_id AS [@product_id], inbound.quantity AS [@quantity],
+ inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no],
+ job.warehouse_id AS [@warehouse_id], stockin.warehouse_id AS [@source_warehouse_id],
+ job.task_no AS [@task_no],
+ job.tray_code AS [@tray_code], detail.job_id AS [@job_id],
+ detail.in_or_out_id AS [@request_id]
+ FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
+ JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
+ JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
+ JOIN dbo.in_stockin stockin WITH (NOLOCK) ON stockin.stockin_id = inbound.stockin_id
+ WHERE (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '''')
+ AND (LTRIM(RTRIM(stockin.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
+ OR (@numeric_warehouse_id IS NOT NULL
+ AND LTRIM(RTRIM(stockin.warehouse_id)) NOT LIKE ''%[^0-9]%''
+ AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockin.warehouse_id))) = @numeric_warehouse_id))
+ ORDER BY inbound.stockin_detail_id
+ FOR XML PATH(''inbound''), ROOT(''inbound_requests''), TYPE
+ )), '''');
+
+ SET @source_payload = @source_payload + ''|OUTBOUND|'' + ISNULL(CONVERT(nvarchar(max), (
+ SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id],
+ stockout.warehouse_id AS [@source_warehouse_id],
+ outbound.spare4 AS [@project_no],
+ outbound.spare1 AS [@spare1], outbound.spare2 AS [@spare2], outbound.spare3 AS [@spare3],
+ outbound.fid AS [@voltage], outbound.unit AS [@phase],
+ outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no],
+ outbound.quantity AS [@quantity], outbound.assign_num AS [@assign_num],
+ outbound.allot_quantity AS [@allot_quantity]
+ FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
+ JOIN dbo.out_stockout stockout WITH (NOLOCK) ON stockout.stockout_id = outbound.stockout_id
+ WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
+ AND (LTRIM(RTRIM(stockout.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
+ OR (@numeric_warehouse_id IS NOT NULL
+ AND LTRIM(RTRIM(stockout.warehouse_id)) NOT LIKE ''%[^0-9]%''
+ AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockout.warehouse_id))) = @numeric_warehouse_id))
+ ORDER BY outbound.stockout_detail_id
+ FOR XML PATH(''outbound''), ROOT(''outbound_requests''), TYPE
+ )), '''');
+
+ SET @pre_source_signature = HASHBYTES(''SHA2_256'', CONVERT(varbinary(max), @source_payload));
+
+ DELETE FROM alg.cargo_space_snapshot WHERE warehouse_id = @warehouse_id;
+ DELETE FROM alg.inventory_item_snapshot WHERE warehouse_id = @warehouse_id;
+ DELETE FROM alg.active_job_snapshot WHERE warehouse_id = @warehouse_id;
+ DELETE FROM alg.shadow_inbound_request_snapshot WHERE warehouse_id = @warehouse_id;
+ DELETE FROM alg.shadow_outbound_request_snapshot WHERE warehouse_id = @warehouse_id;
+ DELETE FROM alg.space_topology WHERE warehouse_id = @warehouse_id;
+ DELETE FROM alg.space_inventory_summary WHERE warehouse_id = @warehouse_id;
+ DELETE FROM alg.lane_state WHERE warehouse_id = @warehouse_id;
+
+ SET @synchronized_at = SYSUTCDATETIME();
+
+ INSERT INTO alg.cargo_space_snapshot (
+ warehouse_id, space_id, lane_code, cargo_area_id, roadway_id, display_name,
+ platoon, column_no, floor_no, depth_raw, space_status, locked, tray_code,
+ snapshot_generation, synchronized_at)
+ SELECT cargo.warehouse_id, cargo.cargo_space_id, cargo.group_coding_no,
+ cargo.cargo_area_id, cargo.roadway_id, cargo.cargo_space_name,
+ cargo.the_platoon, cargo.the_column, cargo.the_floor, cargo.group_coding,
+ cargo.status, cargo.locked, cargo.tray_code,
+ @snapshot_generation, @synchronized_at
+ FROM dbo.bas_cargo_space cargo WITH (NOLOCK)
+ WHERE cargo.warehouse_id = @warehouse_id;
+
+ INSERT INTO alg.inventory_item_snapshot (
+ warehouse_id, stock_id, space_id, tray_code, project_no, product_no, lot_no,
+ inbound_time_raw, inbound_at, quantity, out_quantity, snapshot_generation, synchronized_at,
+ spare1, spare2, spare3, voltage, phase, tray_job_count)
+ SELECT cargo.warehouse_id, stock.stock_id, stock.cargo_space_id, stock.tray_code,
+ stock.spare4, stock.product_id, stock.lot_no, stock.in_date,
+ TRY_CONVERT(datetime2, stock.in_date), stock.quantity, stock.out_quantity,
+ @snapshot_generation, @synchronized_at,
+ stock.spare1, stock.spare2, stock.spare3, stock.voltage, stock.phase,
+ (SELECT COUNT(*) FROM dbo.mydo_job trayJob WITH (NOLOCK)
+ WHERE trayJob.tray_code = stock.tray_code)
+ FROM dbo.bas_stock stock WITH (NOLOCK)
+ JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id
+ WHERE cargo.warehouse_id = @warehouse_id;
+
+ INSERT INTO alg.active_job_snapshot (
+ warehouse_id, job_id, task_no, tray_code, source_space_id, target_space_id,
+ status, in_or_out, create_time_raw, snapshot_generation, synchronized_at)
+ SELECT job.warehouse_id, job.job_id, job.task_no, job.tray_code,
+ NULLIF(LTRIM(RTRIM(job.cargo_space_id)), ''''),
+ NULLIF(LTRIM(RTRIM(job.target_cargo_space_id)), ''''), job.status,
+ job.in_or_out, job.create_time, @snapshot_generation, @synchronized_at
+ FROM dbo.mydo_job job WITH (NOLOCK)
+ WHERE job.warehouse_id = @warehouse_id
+ AND (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '''');
+
+ ;WITH inbound_source AS (
+ SELECT @warehouse_id AS warehouse_id, job.task_no, job.tray_code,
+ detail.in_or_out_id AS request_id, job.warehouse_id AS raw_warehouse_id,
+ inbound.spare4 AS project_no, inbound.product_id, inbound.lot_no,
+ ROW_NUMBER() OVER (
+ PARTITION BY job.task_no, job.tray_code
+ ORDER BY inbound.stockin_detail_id, detail.job_detail_id) AS source_rank
+ FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
+ JOIN dbo.mydo_job_detail detail WITH (NOLOCK)
+ ON detail.stockin_detail_id = inbound.stockin_detail_id
+ JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
+ JOIN dbo.in_stockin stockin WITH (NOLOCK) ON stockin.stockin_id = inbound.stockin_id
+ WHERE (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '''')
+ AND (LTRIM(RTRIM(stockin.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
+ OR (@numeric_warehouse_id IS NOT NULL
+ AND LTRIM(RTRIM(stockin.warehouse_id)) NOT LIKE ''%[^0-9]%''
+ AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockin.warehouse_id))) = @numeric_warehouse_id))
+ )
+ INSERT INTO alg.shadow_inbound_request_snapshot (
+ warehouse_id, task_no, tray_no, request_id, raw_warehouse_id,
+ project_no, product_id, lot_no, snapshot_generation, synchronized_at)
+ SELECT @warehouse_id, ISNULL(task_no, ''''), ISNULL(tray_code, ''''), request_id,
+ raw_warehouse_id, project_no, product_id, lot_no,
+ @snapshot_generation, @synchronized_at
+ FROM inbound_source
+ WHERE source_rank = 1;
+
+ INSERT INTO alg.shadow_outbound_request_snapshot (
+ warehouse_id, stockout_detail_id, raw_warehouse_id, project_no, product_id,
+ requested_quantity, snapshot_generation, synchronized_at,
+ allocated_quantity, spare1, spare2, spare3, voltage, phase, predicate_version)
+ SELECT @warehouse_id, outbound.stockout_detail_id, outbound.warehouse_id,
+ outbound.spare4, outbound.product_id,
+ CONVERT(decimal(38,6), outbound.quantity),
+ @snapshot_generation, @synchronized_at,
+ CONVERT(decimal(38,6), outbound.allot_quantity),
+ outbound.spare1, outbound.spare2, outbound.spare3, outbound.fid, outbound.unit,
+ CASE WHEN ISNULL(DATALENGTH(outbound.product_id), 0) <= 80
+ AND ISNULL(DATALENGTH(outbound.spare1), 0) <= 80
+ AND ISNULL(DATALENGTH(outbound.spare2), 0) <= 80
+ AND ISNULL(DATALENGTH(outbound.spare3), 0) <= 80
+ AND ISNULL(DATALENGTH(outbound.spare4), 0) <= 80
+ AND ISNULL(DATALENGTH(outbound.fid), 0) <= 80
+ AND ISNULL(DATALENGTH(outbound.unit), 0) <= 80
+ THEN 1 ELSE 0 END
+ FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
+ JOIN dbo.out_stockout stockout WITH (NOLOCK) ON stockout.stockout_id = outbound.stockout_id
+ WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
+ AND (LTRIM(RTRIM(stockout.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
+ OR (@numeric_warehouse_id IS NOT NULL
+ AND LTRIM(RTRIM(stockout.warehouse_id)) NOT LIKE ''%[^0-9]%''
+ AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockout.warehouse_id))) = @numeric_warehouse_id));
+
+ EXEC alg.usp_rebuild_space_topology @warehouse_id = @warehouse_id,
+ @snapshot_generation = @snapshot_generation;
+ EXEC alg.usp_sync_inventory_snapshot @warehouse_id = @warehouse_id,
+ @snapshot_generation = @snapshot_generation;
+ EXEC alg.usp_rebuild_lane_state @warehouse_id = @warehouse_id,
+ @snapshot_generation = @snapshot_generation;
+
+ SELECT @post_source_count =
+ (SELECT COUNT_BIG(*) FROM dbo.bas_cargo_space cargo WITH (NOLOCK) WHERE cargo.warehouse_id = @warehouse_id)
+ + (SELECT COUNT_BIG(*) FROM dbo.bas_stock stock WITH (NOLOCK)
+ JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id
+ WHERE cargo.warehouse_id = @warehouse_id)
+ + (SELECT COUNT_BIG(*) FROM dbo.mydo_job job WITH (NOLOCK) WHERE job.warehouse_id = @warehouse_id)
+ + (SELECT COUNT_BIG(*) FROM dbo.mydo_job_detail detail WITH (NOLOCK)
+ JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
+ WHERE job.warehouse_id = @warehouse_id)
+ + (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
+ JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
+ JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
+ JOIN dbo.in_stockin stockin WITH (NOLOCK) ON stockin.stockin_id = inbound.stockin_id
+ WHERE (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '''')
+ AND (LTRIM(RTRIM(stockin.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
+ OR (@numeric_warehouse_id IS NOT NULL
+ AND LTRIM(RTRIM(stockin.warehouse_id)) NOT LIKE ''%[^0-9]%''
+ AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockin.warehouse_id))) = @numeric_warehouse_id)))
+ + (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
+ JOIN dbo.out_stockout stockout WITH (NOLOCK) ON stockout.stockout_id = outbound.stockout_id
+ WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
+ AND (LTRIM(RTRIM(stockout.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
+ OR (@numeric_warehouse_id IS NOT NULL
+ AND LTRIM(RTRIM(stockout.warehouse_id)) NOT LIKE ''%[^0-9]%''
+ AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockout.warehouse_id))) = @numeric_warehouse_id)));
+
+ SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), (
+ SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id],
+ cargo.group_coding_no AS [@lane_code], cargo.cargo_area_id AS [@cargo_area_id],
+ cargo.roadway_id AS [@roadway_id], cargo.cargo_space_name AS [@display_name],
+ cargo.the_platoon AS [@platoon], cargo.the_column AS [@column_no],
+ cargo.the_floor AS [@floor_no], cargo.group_coding AS [@depth_raw],
+ cargo.status AS [@space_status], cargo.locked AS [@locked], cargo.tray_code AS [@tray_code]
+ FROM dbo.bas_cargo_space cargo WITH (NOLOCK)
+ WHERE cargo.warehouse_id = @warehouse_id
+ ORDER BY cargo.cargo_space_id
+ FOR XML PATH(''cargo''), ROOT(''cargo_spaces''), TYPE
+ )), '''');
+ SET @source_payload = @source_payload + ''|STOCK|'' + ISNULL(CONVERT(nvarchar(max), (
+ SELECT stock.stock_id AS [@stock_id], stock.cargo_space_id AS [@space_id],
+ stock.warehouse_id AS [@warehouse_id], stock.tray_code AS [@tray_code],
+ stock.spare4 AS [@project_no], stock.product_id AS [@product_no],
+ stock.lot_no AS [@lot_no], stock.in_date AS [@in_date],
+ stock.quantity AS [@quantity], stock.out_quantity AS [@out_quantity],
+ stock.spare1 AS [@spare1], stock.spare2 AS [@spare2], stock.spare3 AS [@spare3],
+ stock.voltage AS [@voltage], stock.phase AS [@phase],
+ (SELECT COUNT(*) FROM dbo.mydo_job trayJob WITH (NOLOCK)
+ WHERE trayJob.tray_code = stock.tray_code) AS [@tray_job_count]
+ FROM dbo.bas_stock stock WITH (NOLOCK)
+ JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id
+ WHERE cargo.warehouse_id = @warehouse_id
+ ORDER BY stock.stock_id
+ FOR XML PATH(''stock''), ROOT(''stocks''), TYPE
+ )), '''');
+ SET @source_payload = @source_payload + ''|JOB|'' + ISNULL(CONVERT(nvarchar(max), (
+ SELECT job.job_id AS [@job_id], job.status AS [@status], job.task_no AS [@task_no],
+ job.tray_code AS [@tray_code], job.cargo_space_id AS [@source_space_id],
+ job.target_cargo_space_id AS [@target_space_id], job.in_or_out AS [@in_or_out],
+ job.create_time AS [@create_time], job.finish_time AS [@finish_time],
+ job.warehouse_id AS [@warehouse_id]
+ FROM dbo.mydo_job job WITH (NOLOCK)
+ WHERE job.warehouse_id = @warehouse_id
+ ORDER BY job.job_id
+ FOR XML PATH(''job''), ROOT(''jobs''), TYPE
+ )), '''');
+ SET @source_payload = @source_payload + ''|JOB_DETAIL|'' + ISNULL(CONVERT(nvarchar(max), (
+ SELECT detail.job_detail_id AS [@job_detail_id], detail.job_id AS [@job_id],
+ detail.stock_id AS [@stock_id], detail.lot_no AS [@lot_no],
+ detail.in_date AS [@in_date], detail.in_or_out_id AS [@request_id],
+ detail.in_or_out_d_id AS [@request_detail_id], detail.product_id AS [@product_id],
+ detail.stock_status AS [@stock_status], detail.in_quantity AS [@in_quantity],
+ detail.stock_quantity AS [@stock_quantity], detail.out_quantity AS [@out_quantity],
+ detail.pick_quantity AS [@pick_quantity], detail.stockin_detail_id AS [@stockin_detail_id]
+ FROM dbo.mydo_job_detail detail WITH (NOLOCK)
+ JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
+ WHERE job.warehouse_id = @warehouse_id
+ ORDER BY detail.job_detail_id
+ FOR XML PATH(''job_detail''), ROOT(''job_details''), TYPE
+ )), '''');
+ SET @source_payload = @source_payload + ''|INBOUND|'' + ISNULL(CONVERT(nvarchar(max), (
+ SELECT inbound.stockin_detail_id AS [@stockin_detail_id], inbound.stockin_guid AS [@stockin_guid],
+ inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no],
+ inbound.product_id AS [@product_id], inbound.quantity AS [@quantity],
+ inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no],
+ job.warehouse_id AS [@warehouse_id], stockin.warehouse_id AS [@source_warehouse_id],
+ job.task_no AS [@task_no],
+ job.tray_code AS [@tray_code], detail.job_id AS [@job_id],
+ detail.in_or_out_id AS [@request_id]
+ FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
+ JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
+ JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
+ JOIN dbo.in_stockin stockin WITH (NOLOCK) ON stockin.stockin_id = inbound.stockin_id
+ WHERE (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '''')
+ AND (LTRIM(RTRIM(stockin.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
+ OR (@numeric_warehouse_id IS NOT NULL
+ AND LTRIM(RTRIM(stockin.warehouse_id)) NOT LIKE ''%[^0-9]%''
+ AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockin.warehouse_id))) = @numeric_warehouse_id))
+ ORDER BY inbound.stockin_detail_id
+ FOR XML PATH(''inbound''), ROOT(''inbound_requests''), TYPE
+ )), '''');
+ SET @source_payload = @source_payload + ''|OUTBOUND|'' + ISNULL(CONVERT(nvarchar(max), (
+ SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id],
+ stockout.warehouse_id AS [@source_warehouse_id],
+ outbound.spare4 AS [@project_no],
+ outbound.spare1 AS [@spare1], outbound.spare2 AS [@spare2], outbound.spare3 AS [@spare3],
+ outbound.fid AS [@voltage], outbound.unit AS [@phase],
+ outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no],
+ outbound.quantity AS [@quantity], outbound.assign_num AS [@assign_num],
+ outbound.allot_quantity AS [@allot_quantity]
+ FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
+ JOIN dbo.out_stockout stockout WITH (NOLOCK) ON stockout.stockout_id = outbound.stockout_id
+ WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
+ AND (LTRIM(RTRIM(stockout.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
+ OR (@numeric_warehouse_id IS NOT NULL
+ AND LTRIM(RTRIM(stockout.warehouse_id)) NOT LIKE ''%[^0-9]%''
+ AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockout.warehouse_id))) = @numeric_warehouse_id))
+ ORDER BY outbound.stockout_detail_id
+ FOR XML PATH(''outbound''), ROOT(''outbound_requests''), TYPE
+ )), '''');
+ SET @post_source_signature = HASHBYTES(''SHA2_256'', CONVERT(varbinary(max), @source_payload));
+
+ IF @pre_source_count <> @post_source_count
+ OR @pre_source_signature <> @post_source_signature
+ THROW 51025, ''FT546 shadow refresh refused: source changed during refresh'', 1;
+
+ MERGE alg.shadow_snapshot_state WITH (HOLDLOCK) AS target
+ USING (SELECT @warehouse_id AS warehouse_id) AS source
+ ON source.warehouse_id = target.warehouse_id
+ WHEN MATCHED THEN UPDATE SET
+ snapshot_generation = @snapshot_generation,
+ snapshot_status = ''PUBLISHED'',
+ synchronized_at = SYSUTCDATETIME(),
+ source_signature = @post_source_signature,
+ source_row_count = @post_source_count,
+ instance_id = @instance_id,
+ last_error = NULL
+ WHEN NOT MATCHED THEN INSERT (
+ warehouse_id, snapshot_generation, snapshot_status, synchronized_at,
+ source_signature, source_row_count, instance_id, last_error)
+ VALUES (
+ @warehouse_id, @snapshot_generation, ''PUBLISHED'', SYSUTCDATETIME(),
+ @post_source_signature, @post_source_count, @instance_id, NULL);
+
+ COMMIT TRANSACTION;
+ END TRY
+ BEGIN CATCH
+ IF @@TRANCOUNT > 0
+ ROLLBACK TRANSACTION;
+ THROW;
+ END CATCH;
+END;';
+ EXEC sys.sp_executesql @refresh_sql;
+
+ -- Keep the generation monotonic; old rows cannot become predicate-version-1 evidence.
+ UPDATE alg.shadow_snapshot_state WITH (UPDLOCK, HOLDLOCK)
+ SET snapshot_status = 'INITIALIZING', source_signature = NULL, synchronized_at = NULL,
+ last_error = NULL;
+ UPDATE alg.shadow_package_manifest
+ SET package_version = '2026-09-07-33'
+ WHERE package_id = 'FT546WMS_SHADOW_ONLY' AND package_version = '2026-09-03-3';
+ IF @@ROWCOUNT <> 1
+ THROW 51069, 'V3.3 upgrade refused: manifest changed during upgrade', 1;
+ COMMIT TRANSACTION;
+ PRINT 'V33_UPGRADE_OK';
+END TRY
+BEGIN CATCH
+ IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
+ THROW;
+END CATCH;
+GO
diff --git a/fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowV33MigrationTest.java b/fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowV33MigrationTest.java
new file mode 100644
index 0000000..6a0fb07
--- /dev/null
+++ b/fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowV33MigrationTest.java
@@ -0,0 +1,56 @@
+package com.ruoyi.algorithm.repository;
+
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ShadowV33MigrationTest {
+ private String migration() throws Exception {
+ try (var input = getClass().getResourceAsStream("/db/sqlserver/upgrade-shadow-v3-to-v33.sql")) {
+ assertThat(input).as("V3.3 migration resource").isNotNull();
+ return new String(input.readAllBytes(), StandardCharsets.UTF_8).toLowerCase(Locale.ROOT)
+ .replace("''", "'");
+ }
+ }
+
+ @Test
+ void guardsExactOwnershipAndStoppedInstanceBeforeTransactionalChanges() throws Exception {
+ String sql = migration();
+ assertThat(sql).contains("2026-09-03-3", "2026-09-07-33", "ft546wms_shadow_only",
+ "unknown object", "expected object", "updlock, holdlock", "last_seen_at", "claimed_at",
+ "dateadd(second, -120, sysutcdatetime())", "set lock_timeout 5000", "rollback transaction");
+ assertThat(sql.indexOf("unknown object")).isLessThan(sql.indexOf("alter table"));
+ assertThat(sql.indexOf("begin transaction")).isLessThan(sql.indexOf("alter table"));
+ assertThat(sql.indexOf("recent shadow heartbeat")).isLessThan(sql.indexOf("alter table"));
+ assertThat(sql).doesNotContain("alter table dbo.", "update dbo.", "delete from dbo.",
+ "truncate table", "drop table", "delete from alg.decision_log", "update alg.decision_log");
+ }
+
+ @Test
+ void bothSourceSignaturesCaptureAllNewPredicateInputs() throws Exception {
+ String sql = migration();
+ for (String field : new String[]{"spare1", "spare2", "spare3", "voltage", "phase"}) {
+ assertThat(sql.split("stock\\." + field + " as", -1)).hasSize(3);
+ }
+ for (String field : new String[]{"spare1", "spare2", "spare3", "spare4", "fid", "unit"}) {
+ assertThat(sql.split("outbound\\." + field + " as", -1)).hasSize(3);
+ }
+ assertThat(sql).contains("@pre_source_signature <> @post_source_signature", "source changed during refresh");
+ assertThat(sql.split(java.util.regex.Pattern.quote("as [@tray_job_count]"), -1)).hasSize(3);
+ }
+
+ @Test
+ void newSnapshotsUseBusinessProjectRemainingInputsAndVersionMarker() throws Exception {
+ String sql = migration();
+ assertThat(sql).contains("outbound.spare4, outbound.product_id", "outbound.allot_quantity",
+ "outbound.spare1, outbound.spare2, outbound.spare3, outbound.fid, outbound.unit",
+ "datalength(outbound.spare4)", "then 1 else 0 end",
+ "predicate_version int null", "allocated_quantity decimal(38,6) null", "tray_job_count int null");
+ assertThat(sql).doesNotContain("warehouse.project_no", "join dbo.bas_warehouse");
+ assertThat(sql).contains("snapshot_status = 'initializing'", "source_signature = null");
+ assertThat(sql).doesNotContain("set snapshot_generation = 0");
+ }
+}
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index 61f0a19..f443b1b 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -131,6 +131,12 @@
test
+
+ com.h2database
+ h2
+ test
+
+
com.microsoft.sqlserver
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngine.java b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngine.java
index 4761709..b5d2ab9 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngine.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngine.java
@@ -13,6 +13,7 @@ import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import java.time.Instant;
+import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@@ -29,6 +30,7 @@ public class ShadowPlanningEngine {
private static final int RESULT_REASON_MAX_LENGTH = 600;
private static final int NUMBER_MAX_LENGTH = 64;
private static final String PUBLISHED = "PUBLISHED";
+ private static final int MAX_OUTBOUND_CANDIDATES = 200;
private final SchedulingFacade schedulingFacade;
private final ShadowInputMapper inputMapper;
@@ -87,19 +89,83 @@ public class ShadowPlanningEngine {
String warehouseId = snapshot.warehouseId();
ShadowStockReference reference = inputMapper.selectProjectOutboundReference(
warehouseId, snapshot.generation(), stockoutDetailId);
- if (reference == null || isBlank(reference.getProjectNo())) {
- return OutboundShadowObservation.unavailable(snapshot, stockoutDetailId,
- "SKIPPED_MISSING_PROJECT",
- outboundDiagnostic(snapshot, stockoutDetailId, reference,
- "No project number was available for shadow planning"));
+ if (reference == null || !Integer.valueOf(1).equals(reference.getPredicateVersion())) {
+ return unavailableProject(snapshot, stockoutDetailId, reference,
+ "SKIPPED_MISSING_PREDICATE_INPUT", "Request is missing or predates predicate version 1");
}
+ if (reference.getRequestedQuantity() == null || reference.getRequestedQuantity().signum() <= 0) {
+ return unavailableProject(snapshot, stockoutDetailId, reference,
+ "SKIPPED_INVALID_REMAINING_QUANTITY", "No positive unallocated quantity was captured");
+ }
+ List candidates = inputMapper.selectProjectOutboundCandidates(
+ warehouseId, snapshot.generation(), stockoutDetailId);
+ if (candidates.size() > MAX_OUTBOUND_CANDIDATES) {
+ return unavailableProject(snapshot, stockoutDetailId, reference,
+ "SKIPPED_CANDIDATE_LIMIT", "Eligible candidate set exceeds 200 rows");
+ }
+ if (candidates.isEmpty()) {
+ return unavailableProject(snapshot, stockoutDetailId, reference,
+ "ERROR_STOCK_NOT_FOUND", "No inventory satisfies the captured outbound predicates");
+ }
+ var projects = candidates.stream().map(ShadowStockReference::getProjectNo).distinct().toList();
+ if (projects.size() != 1 || isBlank(projects.get(0))) {
+ return unavailableProject(snapshot, stockoutDetailId, reference,
+ "SKIPPED_UNSUPPORTED_PROJECT_SCOPE", "Planner requires one nonblank candidate project");
+ }
+ var products = candidates.stream().map(ShadowStockReference::getProductId).distinct().toList();
+ if (products.size() != 1 || isBlank(products.get(0))) {
+ return unavailableProject(snapshot, stockoutDetailId, reference,
+ "SKIPPED_UNSUPPORTED_PRODUCT_SCOPE", "Planner requires one nonblank candidate product");
+ }
+ // The shared planner is single-source and does not apply the site's optional predicates.
+ // Keep it unchanged; validate its proposal against the generation-qualified candidate set.
OutboundRequest request = new OutboundRequest(stockoutDetailId, null,
- reference.getProjectNo(), reference.getProductId(), null, null, null, true);
- return toOutboundObservation(snapshot, reference.getRequestId(), reference,
- schedulingFacade.planOutboundFromPublishedSnapshot(warehouseId, request));
+ projects.get(0), products.get(0), null, null, null, false);
+ OutboundPlan plan = schedulingFacade.planOutboundFromPublishedSnapshot(warehouseId, request);
+ if (plan.assignment().isEmpty()) {
+ return toOutboundObservation(snapshot, stockoutDetailId, reference, plan);
+ }
+ var assignment = plan.assignment().orElseThrow();
+ var selected = candidates.stream().filter(candidate ->
+ Objects.equals(candidate.getSpaceId(), assignment.sourceSpaceId())
+ && Objects.equals(candidate.getTrayNo(), assignment.trayNo())
+ && Objects.equals(candidate.getProjectNo(), assignment.projectNo())
+ && Objects.equals(candidate.getProductId(), assignment.productId())).toList();
+ if (selected.size() != 1) {
+ return unavailableProject(snapshot, stockoutDetailId, reference,
+ "SKIPPED_INELIGIBLE_PROPOSAL", "Proposed source is not one unambiguous eligible stock row");
+ }
+ ShadowStockReference stock = selected.get(0);
+ if (!Integer.valueOf(0).equals(stock.getTrayJobCount())) {
+ return unavailableProject(snapshot, stockoutDetailId, reference,
+ "SKIPPED_EXISTING_TRAY_JOB", "Existing tray jobs require unsupported delivery compatibility checks");
+ }
+ if (!Integer.valueOf(1).equals(stock.getTrayItemCount())) {
+ return unavailableProject(snapshot, stockoutDetailId, reference,
+ "SKIPPED_MULTI_STOCK_TRAY", "Selected tray does not contain exactly one inventory row");
+ }
+ if (stock.getAvailableQuantity() == null
+ || stock.getAvailableQuantity().compareTo(reference.getRequestedQuantity()) < 0) {
+ return unavailableProject(snapshot, stockoutDetailId, reference,
+ "SKIPPED_MULTI_STOCK_REQUEST", "Selected single stock cannot cover the remaining request quantity");
+ }
+ StringBuilder reason = new StringBuilder(outboundDiagnostic(snapshot, stockoutDetailId, reference,
+ assignment.reason()));
+ appendField(reason, "selectedProject", stock.getProjectNo(), WIDE_IDENTIFIER_MAX_LENGTH);
+ appendField(reason, "selectedTray", stock.getTrayNo(), WIDE_IDENTIFIER_MAX_LENGTH);
+ appendField(reason, "availableQuantity", objectOrEmpty(stock.getAvailableQuantity()), NUMBER_MAX_LENGTH);
+ return new OutboundShadowObservation(true, snapshot.generation(), warehouseId,
+ snapshot.synchronizedAt(), snapshot.sourceSignature(), stockoutDetailId,
+ "READY", stock.getStockId(), stock.getSpaceId(), reason.toString());
});
}
+ private OutboundShadowObservation unavailableProject(PublishedShadowSnapshot snapshot, String requestId,
+ ShadowStockReference reference, String code, String reason) {
+ return OutboundShadowObservation.unavailable(snapshot, requestId, code,
+ outboundDiagnostic(snapshot, requestId, reference, reason));
+ }
+
public OutboundShadowObservation planExactStockOutbound(PublishedShadowSnapshot snapshot,
String stockoutDetailId, String stockId) {
return planWithStableSnapshot(snapshot, () -> {
@@ -313,6 +379,15 @@ public class ShadowPlanningEngine {
reference == null ? null : reference.getSnapshotSynchronizedAt(), SNAPSHOT_SYNC_MAX_LENGTH);
appendField(diagnostic, "shadowMatches", numberOrUnknown(
reference == null ? null : reference.getShadowMatchCount()), NUMBER_MAX_LENGTH);
+ appendField(diagnostic, "predicateVersion", numberOrUnknown(
+ reference == null ? null : reference.getPredicateVersion()), NUMBER_MAX_LENGTH);
+ if (reference != null && reference.getPredicateVersion() != null) {
+ appendField(diagnostic, "spare1", reference.getSpare1(), 120);
+ appendField(diagnostic, "spare2", reference.getSpare2(), 120);
+ appendField(diagnostic, "spare3", reference.getSpare3(), 120);
+ appendField(diagnostic, "voltage", reference.getVoltage(), 120);
+ appendField(diagnostic, "phase", reference.getPhase(), 120);
+ }
appendField(diagnostic, "reason", resultReason, RESULT_REASON_MAX_LENGTH);
return diagnostic.toString();
}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowStockReference.java b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowStockReference.java
index fad13ab..ca57d26 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowStockReference.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowStockReference.java
@@ -13,6 +13,35 @@ public class ShadowStockReference {
private String snapshotSynchronizedAt;
private BigDecimal requestedQuantity;
private Integer shadowMatchCount;
+ private Integer predicateVersion;
+ private String spare1;
+ private String spare2;
+ private String spare3;
+ private String voltage;
+ private String phase;
+ private BigDecimal availableQuantity;
+ private Integer trayItemCount;
+ private Integer trayJobCount;
+
+ public Integer getTrayJobCount() { return trayJobCount; }
+ public void setTrayJobCount(Integer value) { trayJobCount = value; }
+
+ public Integer getPredicateVersion() { return predicateVersion; }
+ public void setPredicateVersion(Integer value) { predicateVersion = value; }
+ public String getSpare1() { return spare1; }
+ public void setSpare1(String value) { spare1 = value; }
+ public String getSpare2() { return spare2; }
+ public void setSpare2(String value) { spare2 = value; }
+ public String getSpare3() { return spare3; }
+ public void setSpare3(String value) { spare3 = value; }
+ public String getVoltage() { return voltage; }
+ public void setVoltage(String value) { voltage = value; }
+ public String getPhase() { return phase; }
+ public void setPhase(String value) { phase = value; }
+ public BigDecimal getAvailableQuantity() { return availableQuantity; }
+ public void setAvailableQuantity(BigDecimal value) { availableQuantity = value; }
+ public Integer getTrayItemCount() { return trayItemCount; }
+ public void setTrayItemCount(Integer value) { trayItemCount = value; }
public String getRequestId() {
return requestId;
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/mapper/ShadowInputMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/mapper/ShadowInputMapper.java
index 1b7bb2b..142ccef 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/mapper/ShadowInputMapper.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/mapper/ShadowInputMapper.java
@@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.Instant;
+import java.util.List;
@Mapper
public interface ShadowInputMapper {
@@ -21,6 +22,9 @@ public interface ShadowInputMapper {
ShadowStockReference selectStockReference(@Param("warehouseId") String warehouseId,
@Param("generation") long generation, @Param("stockId") String stockId);
+ List selectProjectOutboundCandidates(@Param("warehouseId") String warehouseId,
+ @Param("generation") long generation, @Param("stockoutDetailId") String stockoutDetailId);
+
boolean claimInstance(@Param("warehouseId") String warehouseId,
@Param("instanceId") String instanceId, @Param("staleSeconds") int staleSeconds);
diff --git a/ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml b/ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml
index 5e44a6d..465f443 100644
--- a/ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml
+++ b/ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml
@@ -25,6 +25,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and input.tray_no = #{trayNo}
+
+ item.warehouse_id = #{warehouseId}
+ and item.snapshot_generation = #{generation}
+ and cargo.warehouse_id = item.warehouse_id
+ and cargo.snapshot_generation = item.snapshot_generation
+ and cargo.space_id = item.space_id and cargo.tray_code = item.tray_code
+ and cargo.locked = 'N'
+ and ((cargo.space_status = '2' and item.out_quantity = 0)
+ or (cargo.space_status = '4' and item.quantity > item.out_quantity))
+ and (nullif(input.project_no, '') is null or item.project_no = input.project_no)
+ and (nullif(input.product_id, '') is null or item.product_no = input.product_id)
+ and (nullif(input.spare1, '') is null or item.spare1 = input.spare1)
+ and (nullif(input.spare2, '') is null or item.spare2 = input.spare2)
+ and (nullif(input.spare3, '') is null or item.spare3 = input.spare3)
+ and (nullif(input.voltage, '') is null or item.voltage = input.voltage)
+ and (nullif(input.phase, '') is null or item.phase = input.phase)
+ and not exists (
+ select 1 from alg.active_job_snapshot job
+ where job.warehouse_id = item.warehouse_id
+ and job.snapshot_generation = item.snapshot_generation
+ and job.tray_code = item.tray_code and job.in_or_out = '26')
+
+
+
+