|
|
@@ -0,0 +1,993 @@
|
|
|
+# WP-C|齐套口径统一与结果不再互相覆盖(D-A02 + O-A01)
|
|
|
+
|
|
|
+> **状态:定案版(可直接执行)**。所有决策已锁,无需再确认。
|
|
|
+> 执行者:熟悉 C# / SqlSugar / Vue3 的模型或工程师。预计 1 天。
|
|
|
+> 前置:无。与 WP-B 都会改 `MaterialRequirementCalculator.cs`,但改的是**不同区段**(见 §10)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 0. 本包定案
|
|
|
+
|
|
|
+| 编号 | 决策项 | 定案 | 依据 / 推翻条件 |
|
|
|
+|---|---|---|---|
|
|
|
+| C-D1 | 口径统一方向 | 全部路径统一为**净口径**(扣除同一排程版本内其他工单已占用的库存) | 毛口径会让每个工单都以为自己能独占全部库存,多工单同时下达必然超发。净口径是唯一能自洽的口径。推翻条件:业务明确要求"单工单检查只看仓库总量、不看别人的预留" |
|
|
|
+| C-D2 | 自占用处理 | 扣占用时**排除本工单自己的占用行**(`morder_mo <> 本工单`) | 不排除的话,一个工单第二次点齐套检查会把自己第一次的占用当成别人的,缺料量凭空翻倍 |
|
|
|
+| C-D3 | 单工单检查是否写占用 | **只读不写**。只有排产(`bangId` 由排产运行产生)才写 `ic_item_stockoccupy` / `srm_po_occupy` | 手动点齐套检查是"查询"语义。若写占用,用户多点几次就会把库存全占掉,且没有任何清理时机 |
|
|
|
+| C-D4 | 结果失效范围 | `InvalidatePreviousAsync` 以 `(tenant_id, morder_no)` 为主键条件;`sentry_id` 只在 `> 0` 时**追加**;SELECT 与 UPDATE 条件必须一致 | 现状 SELECT 用 `sentry_id OR morder_no`、UPDATE 只用 `sentry_id`,两者不一致;且 `sentry_id = 0`(工单无关联订单行)会命中全部同类行,造成跨工单批量失效 |
|
|
|
+| C-D5 | 结果可追溯 | `b_examine_result` 写入 `bangid`(列已存在但从未写过)+ 新增 `check_scope` 列记录触发来源 | 不记来源就永远说不清"这条结论是排产算的还是手点的",D-A02 这类问题会反复出现 |
|
|
|
+| C-D6 | 前端缺料提示 | 缺料是**正常业务结论**,用 `ElMessage.warning` + 刷新列表;不再 `ElMessage.error` | 现状后端对缺料抛异常、前端弹红色报错且**不刷新列表**,用户看不到刚算出来的缺料状态 |
|
|
|
+| C-D7 | 下达是否因缺料阻断 | **不阻断**。下达继续沿用现有行为(记录 `materialSituation` 后继续建领料) | 改变下达阻断逻辑属业务规则变更,超出缺陷整治范围 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 目标
|
|
|
+
|
|
|
+同一个工单,无论从哪个入口触发齐套检查,都得到**同一个结论**;后一次检查不再破坏无关工单的历史结果;缺料时用户能立刻在列表上看到。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 缺陷事实(已核实,附行号)
|
|
|
+
|
|
|
+### 2.1 两条路径口径不同
|
|
|
+
|
|
|
+`WorkOrderKittingCheckService.CheckSingleAsync` 的 `bangId` 参数决定口径,而三个调用方传的值不同:
|
|
|
+
|
|
|
+| 调用方 | 位置 | 传入 `bangId` | 实际口径 |
|
|
|
+|---|---|---|---|
|
|
|
+| 排产 | `ProductionScheduleGenerationService.cs:250` `CheckSingleAsync(tenantId, wo.WorkOrd, account, scheduleBangId)` | **> 0** | **净**:扣同批次占用 + **写**占用 |
|
|
|
+| 工单下达 | `WorkOrderDispatchService.cs:277` `CheckSingleAsync(tid, wo, account)` | 0(默认值) | **毛**:不扣占用、不写占用 |
|
|
|
+| 手动单工单检查 | `WorkOrderDispatchService.cs:396` `CheckSingleAsync(tid, wo, account)` | 0 | **毛** |
|
|
|
+| 批量检查 | `WorkOrderKittingCheckService.cs:43` `CheckSingleAsync(tenantId, wo, account)` | 0 | **毛** |
|
|
|
+
|
|
|
+口径分叉点在 `MaterialRequirementCalculator.ApplySupplyAsync`:
|
|
|
+
|
|
|
+```264:289:server/Plugins/Admin.NET.Plugin.AiDOP/Order/MaterialRequirementCalculator.cs
|
|
|
+ if (bangId > 0)
|
|
|
+ {
|
|
|
+ var occupiedRows = await _db.Ado.SqlQueryAsync<OccupyRow>(
|
|
|
+ """
|
|
|
+ SELECT icitem_number AS ItemNumber, IFNULL(SUM(quantity), 0) AS TotalOccupied
|
|
|
+ FROM ic_item_stockoccupy
|
|
|
+ WHERE tenant_id = @TenantId AND bang_id = @BangId AND IsDeleted = 0
|
|
|
+ GROUP BY icitem_number
|
|
|
+ """,
|
|
|
+```
|
|
|
+
|
|
|
+`bangId = 0` 时 `occupiedStockMap` 为空 → 不扣任何占用 → 缺料量偏小 → 结论偏乐观。
|
|
|
+
|
|
|
+### 2.2 两条路径写同一张表,后写覆盖前写
|
|
|
+
|
|
|
+两条路径最终都调用 `ResourceCheckResultWriter.WriteAsync`(`WorkOrderKittingCheckService.cs:96`),而 `WriteAsync` 第一件事就是把历史结果失效:
|
|
|
+
|
|
|
+```162:198:server/Plugins/Admin.NET.Plugin.AiDOP/Order/ResourceCheckResultWriter.cs
|
|
|
+ private async Task InvalidatePreviousAsync(long entryId, long tenantId, string morderNo, DateTime now)
|
|
|
+ {
|
|
|
+ var oldIds = await _db.Ado.SqlQueryAsync<long>(
|
|
|
+ """
|
|
|
+ SELECT Id FROM b_examine_result
|
|
|
+ WHERE tenant_id = @TenantId AND IsDeleted = 0
|
|
|
+ AND (sentry_id = @EntryId OR morder_no = @MorderNo)
|
|
|
+ """,
|
|
|
+```
|
|
|
+
|
|
|
+后面的 UPDATE 却只按 `sentry_id`:
|
|
|
+
|
|
|
+```189:197:server/Plugins/Admin.NET.Plugin.AiDOP/Order/ResourceCheckResultWriter.cs
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ UPDATE b_examine_result
|
|
|
+ SET IsDeleted = 1, update_time = @Now
|
|
|
+ WHERE sentry_id = @EntryId AND tenant_id = @TenantId AND IsDeleted = 0
|
|
|
+ """,
|
|
|
+```
|
|
|
+
|
|
|
+后果:
|
|
|
+
|
|
|
+1. **口径覆盖**:排产算出的净口径结果,被用户手点一次齐套检查产生的毛口径结果覆盖(或反之)。用户看到的 `mes_morder.MaterialSituation` 取决于"最后点了哪个按钮"。实测 `M500000004` 在 `b_examine_result` 里有约 20 条同 `sentry_id` 的历史记录,仅最新一条 `IsDeleted=0`。
|
|
|
+2. **潜在跨工单误失效**:当工单没有关联订单明细行时(代码里的 `2b. 无关联订单明细行(BusinessID=0)` 分支),`entry.Id = 0`,于是 `sentry_id = 0` 会命中**所有**同类历史结果,把别的工单的结论一起标删。
|
|
|
+ 目前尚未触发——实测 22 个工单的 `BusinessID` 全部 > 0、`b_examine_result` 里 `sentry_id = 0` 的行数为 0——但这是**待引爆的雷**:只要出现一个不挂订单的工单(补单、返工单、手工单)就会炸。
|
|
|
+
|
|
|
+### 2.3 缺料时前端体验错误
|
|
|
+
|
|
|
+后端把"缺料"当异常抛:
|
|
|
+
|
|
|
+```396:401:server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/WorkOrderDispatchService.cs
|
|
|
+ var result = await _kittingCheck.CheckSingleAsync(tid, wo, account);
|
|
|
+ if (!result.IsKitted)
|
|
|
+ {
|
|
|
+ await _runLog.FailedAsync(logId, $"缺料行 {result.ShortageLineCount}", result);
|
|
|
+ throw Oops.Oh($"工单 {workord} 齐套检查未通过(缺料行 {result.ShortageLineCount})");
|
|
|
+```
|
|
|
+
|
|
|
+前端于是走进 `catch`,弹红色错误且**不刷新列表**:
|
|
|
+
|
|
|
+```268:285:Web/src/views/aidop/business/workOrderDispatchList.vue
|
|
|
+async function onSingleKitCheck(row: WorkOrderDispatchRow) {
|
|
|
+ const domain = domainParamFromCurrentTenant();
|
|
|
+ if (!domain || !row.workOrd) {
|
|
|
+ ElMessage.error('当前用户无租户信息或工单号为空');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const account = userInfo.userInfos?.account ?? '';
|
|
|
+ singleKittingWo.value = row.workOrd;
|
|
|
+ try {
|
|
|
+ await kittingCheckSingle(row.workOrd, domain, account);
|
|
|
+ ElMessage.success(`工单 ${row.workOrd} 齐套检查已执行`);
|
|
|
+ await loadList();
|
|
|
+ } catch (e: any) {
|
|
|
+ ElMessage.error(e?.message || '单工单齐套检查失败');
|
|
|
+ } finally {
|
|
|
+ singleKittingWo.value = null;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+用户观感:检查"失败了",而且列表上的物料状况还是旧值。实际上检查成功了,只是结论是缺料。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 第 1 步:事实复核(必须先做)
|
|
|
+
|
|
|
+```sql
|
|
|
+-- ① 同一工单的历史齐套结果条数与失效情况(期望:多条,仅 1 条 IsDeleted=0)
|
|
|
+SELECT Id, sentry_id, morder_no, IsDeleted, bangid, create_time
|
|
|
+FROM b_examine_result WHERE morder_no = 'M500000004' ORDER BY create_time DESC LIMIT 25;
|
|
|
+
|
|
|
+-- ② bangid 是否从未写过(期望:全 NULL 或全 0)
|
|
|
+SELECT COUNT(*) AS total, SUM(CASE WHEN IFNULL(bangid,0) > 0 THEN 1 ELSE 0 END) AS with_bang
|
|
|
+FROM b_examine_result;
|
|
|
+
|
|
|
+-- ③ sentry_id=0 的雷是否已引爆(期望:0)
|
|
|
+SELECT COUNT(*) AS zero_sentry FROM b_examine_result WHERE IFNULL(sentry_id, 0) = 0;
|
|
|
+
|
|
|
+-- ④ 当前是否存在不挂订单的工单(期望:0;若 > 0,2.2 的雷已具备引爆条件,优先级提高)
|
|
|
+SELECT COUNT(*) AS wo_without_order FROM WorkOrdMaster WHERE IFNULL(BusinessID, 0) = 0;
|
|
|
+
|
|
|
+-- ⑤ 当前有效排程版本(后续 bangId 复用它;期望:有一条 SUCCESS 记录)
|
|
|
+SELECT id, action_code, status, start_time FROM aidop_action_run_log
|
|
|
+WHERE action_code = 'S2_SCHEDULE_GENERATE' AND status = 'SUCCESS'
|
|
|
+ORDER BY start_time DESC, id DESC LIMIT 3;
|
|
|
+```
|
|
|
+
|
|
|
+若 ⑤ 无记录(从未成功排产过),本包仍可执行:`ResolveCurrentScheduleBangIdAsync` 返回 null 时口径退化为毛口径,与改动前一致,不会更差。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 第 2 步:数据库脚本
|
|
|
+
|
|
|
+新建 `server/Admin.NET.Web.Entry/UpdateScripts/1.0.325.sql`。
|
|
|
+
|
|
|
+> **迁移脚本的四条硬约束**(来自 `server/Admin.NET.Core/Update/AutoVersionUpdate.cs`,违反会导致脚本不执行或后端启动失败):
|
|
|
+> 1. **行尾必须 LF**。CRLF 会改变 SHA256,与 `sys_db_migration_log` 已记录的 hash 不一致时直接抛异常阻止启动。
|
|
|
+> 2. **脚本号必须 ≤ `.csproj` 的 `<Version>`**(`pending = scripts.Where(s => s.ParsedVersion <= currentVersion)`)。当前后端版本 **1.0.327**,已占用脚本号最大为 **1.0.323**,WP-D 取 `1.0.324`,本包取 `1.0.325`。若已被占用,取下一个未占用且 ≤ 当前版本的号并同步更新 README。
|
|
|
+> 3. 脚本内**不得出现 `DELIMITER` 字样**(含注释里),执行器直接拒绝。
|
|
|
+> 4. 已成功执行过的脚本**不可再改内容**(hash 校验),需要修正就新建更高版本号的脚本。
|
|
|
+>
|
|
|
+> 本脚本使用 `SET @` 与 `PREPARE`,执行器会识别并**整段同连接执行**(不按 `;` 拆分),与 `1.0.321.sql` 等既有脚本同一写法,可放心沿用。
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 1.0.325.sql WP-C 齐套口径统一:为齐套结果头补来源标记
|
|
|
+-- D-A02:排产(净口径)与手动检查(毛口径)写同一张 b_examine_result 且互相覆盖,
|
|
|
+-- 事后无法判断某条结论出自哪条路径。bangid 列早已存在但从未写入。
|
|
|
+-- 幂等:经 information_schema 判列是否存在,可重复执行。
|
|
|
+-- 行尾必须 LF(CRLF 会导致 SHA256 与 sys_db_migration_log 不一致并阻止启动)。
|
|
|
+SET NAMES utf8mb4;
|
|
|
+
|
|
|
+SET @ddl := (SELECT IF(
|
|
|
+ EXISTS(SELECT 1 FROM information_schema.COLUMNS
|
|
|
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'b_examine_result' AND COLUMN_NAME = 'check_scope'),
|
|
|
+ 'SELECT 1',
|
|
|
+ 'ALTER TABLE b_examine_result ADD COLUMN check_scope VARCHAR(20) NULL COMMENT ''触发来源:SCHEDULE=排产内嵌/RELEASE=工单下达/SINGLE=手动单工单/BATCH=批量/REVIEW=订单评审'' AFTER bangid'));
|
|
|
+PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
|
|
+
|
|
|
+SET @ddl := (SELECT IF(
|
|
|
+ EXISTS(SELECT 1 FROM information_schema.COLUMNS
|
|
|
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'b_examine_result' AND COLUMN_NAME = 'net_scope'),
|
|
|
+ 'SELECT 1',
|
|
|
+ 'ALTER TABLE b_examine_result ADD COLUMN net_scope TINYINT(1) NULL DEFAULT 0 COMMENT ''1=净口径(已扣同版本占用);0=毛口径'' AFTER check_scope'));
|
|
|
+PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
|
|
+```
|
|
|
+
|
|
|
+同时新建**校验脚本** `server/Admin.NET.Web.Entry/UpdateScripts/1.0.325.verify.sql`(同样 LF)。执行器在主脚本成功后逐条跑它,任一条返回 0/NULL 即写入 `verify_status=Failed`:
|
|
|
+
|
|
|
+```sql
|
|
|
+SELECT COUNT(*) FROM information_schema.COLUMNS
|
|
|
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'b_examine_result' AND COLUMN_NAME = 'check_scope';
|
|
|
+SELECT COUNT(*) FROM information_schema.COLUMNS
|
|
|
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'b_examine_result' AND COLUMN_NAME = 'net_scope';
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 第 3 步:`MaterialRequirementCalculator` 支持"只读净口径"
|
|
|
+
|
|
|
+**文件**:`server/Plugins/Admin.NET.Plugin.AiDOP/Order/MaterialRequirementCalculator.cs`
|
|
|
+
|
|
|
+### 5.1 `BuildLinesAsync` 增加两个参数
|
|
|
+
|
|
|
+**定位锚点**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ public async Task<List<ResourceCheckBomLine>> BuildLinesAsync(
|
|
|
+ OrderWorkOrderGenerationService.OrderHeader order,
|
|
|
+ OrderWorkOrderGenerationService.OrderEntryLine entry,
|
|
|
+ List<string> warnings,
|
|
|
+ long bangId = 0,
|
|
|
+ string? workOrd = null,
|
|
|
+ long morderId = 0)
|
|
|
+ {
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ /// <param name="writeOccupy">
|
|
|
+ /// 是否登记库存/在途占用。只有排产才写(占用有明确的版本与清理时机);
|
|
|
+ /// 手动齐套检查是查询语义,多点几次不应把库存占光。
|
|
|
+ /// </param>
|
|
|
+ /// <param name="excludeOccupyWorkOrd">
|
|
|
+ /// 扣减占用时要排除的工单号(通常是本工单)。不排除会把自己上一次的占用当成别人的,
|
|
|
+ /// 导致同一工单反复检查时缺料量不断翻倍。
|
|
|
+ /// </param>
|
|
|
+ public async Task<List<ResourceCheckBomLine>> BuildLinesAsync(
|
|
|
+ OrderWorkOrderGenerationService.OrderHeader order,
|
|
|
+ OrderWorkOrderGenerationService.OrderEntryLine entry,
|
|
|
+ List<string> warnings,
|
|
|
+ long bangId = 0,
|
|
|
+ string? workOrd = null,
|
|
|
+ long morderId = 0,
|
|
|
+ bool writeOccupy = true,
|
|
|
+ string? excludeOccupyWorkOrd = null)
|
|
|
+ {
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 三处内部调用透传
|
|
|
+
|
|
|
+文件内有 **3 处**完全相同的调用(约 55 / 63 / 74 行):
|
|
|
+
|
|
|
+```csharp
|
|
|
+ await ApplySupplyAsync(lines, entry.TenantId, bangId, entry.Id, workOrd, morderId);
|
|
|
+```
|
|
|
+
|
|
|
+**全部替换为**(3 处都改,可用编辑器全量替换):
|
|
|
+
|
|
|
+```csharp
|
|
|
+ await ApplySupplyAsync(lines, entry.TenantId, bangId, entry.Id, workOrd, morderId, writeOccupy, excludeOccupyWorkOrd);
|
|
|
+```
|
|
|
+
|
|
|
+> 第 74 行那处缩进是 8 空格,替换时保留各自原有缩进。
|
|
|
+
|
|
|
+### 5.3 `ApplySupplyAsync` 签名与占用查询
|
|
|
+
|
|
|
+**定位锚点**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ private async Task ApplySupplyAsync(
|
|
|
+ List<ResourceCheckBomLine> lines,
|
|
|
+ long tenantId,
|
|
|
+ long bangId = 0,
|
|
|
+ long entryId = 0,
|
|
|
+ string? workOrd = null,
|
|
|
+ long morderId = 0)
|
|
|
+ {
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ private async Task ApplySupplyAsync(
|
|
|
+ List<ResourceCheckBomLine> lines,
|
|
|
+ long tenantId,
|
|
|
+ long bangId = 0,
|
|
|
+ long entryId = 0,
|
|
|
+ string? workOrd = null,
|
|
|
+ long morderId = 0,
|
|
|
+ bool writeOccupy = true,
|
|
|
+ string? excludeOccupyWorkOrd = null)
|
|
|
+ {
|
|
|
+```
|
|
|
+
|
|
|
+**定位锚点**(占用查询块):
|
|
|
+
|
|
|
+```csharp
|
|
|
+ if (bangId > 0)
|
|
|
+ {
|
|
|
+ var occupiedRows = await _db.Ado.SqlQueryAsync<OccupyRow>(
|
|
|
+ """
|
|
|
+ SELECT icitem_number AS ItemNumber, IFNULL(SUM(quantity), 0) AS TotalOccupied
|
|
|
+ FROM ic_item_stockoccupy
|
|
|
+ WHERE tenant_id = @TenantId AND bang_id = @BangId AND IsDeleted = 0
|
|
|
+ GROUP BY icitem_number
|
|
|
+ """,
|
|
|
+ new SugarParameter("@TenantId", tenantId),
|
|
|
+ new SugarParameter("@BangId", bangId));
|
|
|
+ foreach (var r in occupiedRows)
|
|
|
+ occupiedStockMap[r.ItemNumber] = r.TotalOccupied;
|
|
|
+
|
|
|
+ var occupiedTransitRows = await _db.Ado.SqlQueryAsync<OccupyRow>(
|
|
|
+ """
|
|
|
+ SELECT ItemNumber, IFNULL(SUM(OccupyQty), 0) AS TotalOccupied
|
|
|
+ FROM srm_po_occupy
|
|
|
+ WHERE tenant_id = @TenantId AND bang_id = @BangId AND IsDeleted = 0
|
|
|
+ GROUP BY ItemNumber
|
|
|
+ """,
|
|
|
+ new SugarParameter("@TenantId", tenantId),
|
|
|
+ new SugarParameter("@BangId", bangId));
|
|
|
+ foreach (var r in occupiedTransitRows)
|
|
|
+ occupiedTransitMap[r.ItemNumber] = r.TotalOccupied;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ if (bangId > 0)
|
|
|
+ {
|
|
|
+ // 排除指定工单(通常是本工单)自己的占用,否则同一工单重复检查会把自己的占用当别人的
|
|
|
+ var excludeWo = (excludeOccupyWorkOrd ?? "").Trim();
|
|
|
+ var excludeClause = excludeWo.Length == 0 ? "" : " AND IFNULL(morder_mo, '') <> @ExcludeWo";
|
|
|
+
|
|
|
+ var occupyPars = new List<SugarParameter>
|
|
|
+ {
|
|
|
+ new("@TenantId", tenantId),
|
|
|
+ new("@BangId", bangId)
|
|
|
+ };
|
|
|
+ if (excludeWo.Length > 0)
|
|
|
+ occupyPars.Add(new SugarParameter("@ExcludeWo", excludeWo));
|
|
|
+
|
|
|
+ var occupiedRows = await _db.Ado.SqlQueryAsync<OccupyRow>(
|
|
|
+ $"""
|
|
|
+ SELECT icitem_number AS ItemNumber, IFNULL(SUM(quantity), 0) AS TotalOccupied
|
|
|
+ FROM ic_item_stockoccupy
|
|
|
+ WHERE tenant_id = @TenantId AND bang_id = @BangId AND IsDeleted = 0{excludeClause}
|
|
|
+ GROUP BY icitem_number
|
|
|
+ """,
|
|
|
+ occupyPars);
|
|
|
+ foreach (var r in occupiedRows)
|
|
|
+ occupiedStockMap[r.ItemNumber] = r.TotalOccupied;
|
|
|
+
|
|
|
+ var transitPars = new List<SugarParameter>
|
|
|
+ {
|
|
|
+ new("@TenantId", tenantId),
|
|
|
+ new("@BangId", bangId)
|
|
|
+ };
|
|
|
+ if (excludeWo.Length > 0)
|
|
|
+ transitPars.Add(new SugarParameter("@ExcludeWo", excludeWo));
|
|
|
+
|
|
|
+ var occupiedTransitRows = await _db.Ado.SqlQueryAsync<OccupyRow>(
|
|
|
+ $"""
|
|
|
+ SELECT ItemNumber, IFNULL(SUM(OccupyQty), 0) AS TotalOccupied
|
|
|
+ FROM srm_po_occupy
|
|
|
+ WHERE tenant_id = @TenantId AND bang_id = @BangId AND IsDeleted = 0{excludeClause}
|
|
|
+ GROUP BY ItemNumber
|
|
|
+ """,
|
|
|
+ transitPars);
|
|
|
+ foreach (var r in occupiedTransitRows)
|
|
|
+ occupiedTransitMap[r.ItemNumber] = r.TotalOccupied;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+> `srm_po_occupy` 的工单列名也是 `morder_mo`(见同文件末尾的 INSERT)。若实际列名不同,按表结构调整该表的 `excludeClause`。
|
|
|
+
|
|
|
+### 5.4 占用写入加开关
|
|
|
+
|
|
|
+**定位锚点**(占用写入块的入口):
|
|
|
+
|
|
|
+```csharp
|
|
|
+ // 写入占用记录(库存占用和在途占用)
|
|
|
+ if (bangId > 0)
|
|
|
+ {
|
|
|
+ foreach (var line in lines)
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ // 写入占用记录(库存占用和在途占用)。只有排产写;手动齐套检查是查询语义,只读不写
|
|
|
+ if (bangId > 0 && writeOccupy)
|
|
|
+ {
|
|
|
+ foreach (var line in lines)
|
|
|
+```
|
|
|
+
|
|
|
+块内其余代码**不动**。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 第 4 步:`WorkOrderKittingCheckService` 统一口径
|
|
|
+
|
|
|
+**文件**:`server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/WorkOrderKittingCheckService.cs`
|
|
|
+
|
|
|
+### 6.1 `CheckSingleAsync` 增加来源参数并解析 `bangId`
|
|
|
+
|
|
|
+**定位锚点**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ public async Task<SingleKittingCheckResult> CheckSingleAsync(long tenantId, string workOrd, string account, long bangId = 0)
|
|
|
+ {
|
|
|
+ var wo = workOrd.Trim();
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ /// <param name="bangId">
|
|
|
+ /// 排程版本。> 0 表示由排产驱动(净口径 + 写占用);
|
|
|
+ /// = 0 表示手动/下达触发,此时自动复用当前有效排程版本,保持口径一致但只读不写。
|
|
|
+ /// </param>
|
|
|
+ /// <param name="checkScope">触发来源,写入 b_examine_result.check_scope 供事后追溯。</param>
|
|
|
+ public async Task<SingleKittingCheckResult> CheckSingleAsync(
|
|
|
+ long tenantId, string workOrd, string account, long bangId = 0, string checkScope = "SINGLE")
|
|
|
+ {
|
|
|
+ var wo = workOrd.Trim();
|
|
|
+
|
|
|
+ // 口径统一(C-D1):手动/下达触发时也按净口径算——复用当前有效排程版本的占用账,
|
|
|
+ // 否则每个工单都以为自己能独占全部库存,多工单同时下达必然超发。
|
|
|
+ var isScheduleDriven = bangId > 0;
|
|
|
+ if (!isScheduleDriven)
|
|
|
+ bangId = await ResolveCurrentScheduleBangIdAsync(tenantId) ?? 0;
|
|
|
+```
|
|
|
+
|
|
|
+### 6.2 传递新参数给计算器
|
|
|
+
|
|
|
+**定位锚点**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ // 3. 重新执行资源检查(BOM 展开 + 库存/在途缺口计算)
|
|
|
+ var warnings = new List<string>();
|
|
|
+ var lines = await _calculator.BuildLinesAsync(order, entry, warnings, bangId, wo, wm.MorderId ?? 0);
|
|
|
+
|
|
|
+ // 4. 将新结果写入 b_examine_result / b_bom_child_examine 并更新 mes_morder
|
|
|
+ var checkResult = await _writer.WriteAsync(order, entry, wo, wm.MorderId, lines, account, DateTime.Now);
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ // 3. 重新执行资源检查(BOM 展开 + 库存/在途缺口计算)
|
|
|
+ // 非排产触发:只读不写占用(C-D3),并排除本工单自身占用(C-D2)
|
|
|
+ var warnings = new List<string>();
|
|
|
+ var lines = await _calculator.BuildLinesAsync(
|
|
|
+ order, entry, warnings, bangId, wo, wm.MorderId ?? 0,
|
|
|
+ writeOccupy: isScheduleDriven,
|
|
|
+ excludeOccupyWorkOrd: wo);
|
|
|
+
|
|
|
+ // 4. 将新结果写入 b_examine_result / b_bom_child_examine 并更新 mes_morder
|
|
|
+ var checkResult = await _writer.WriteAsync(
|
|
|
+ order, entry, wo, wm.MorderId, lines, account, DateTime.Now,
|
|
|
+ bangId: bangId, checkScope: checkScope);
|
|
|
+```
|
|
|
+
|
|
|
+> 注意:排产路径也传了 `excludeOccupyWorkOrd: wo`。这是正确的——排产在算某工单前会先删掉该工单在本版本下的旧占用(`ProductionScheduleGenerationService` 单工单重排分支就是这么做的),排除自身不改变排产语义,却能防住"同版本内重复计算同一工单"的叠加。
|
|
|
+
|
|
|
+### 6.3 新增排程版本解析方法
|
|
|
+
|
|
|
+**在 `LoadWorkOrdMasterAsync` 方法之前插入**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ /// <summary>
|
|
|
+ /// 取当前有效排程版本 id(= 最近一次成功排产的 aidop_action_run_log.id,与 bang_id 同源)。
|
|
|
+ /// 与 ProductionScheduleGenerationService.ResolveCurrentScheduleBangIdAsync 同逻辑;
|
|
|
+ /// 无成功排产记录时返回 null,口径退化为毛口径(与改动前行为一致,不会更差)。
|
|
|
+ /// </summary>
|
|
|
+ private async Task<long?> ResolveCurrentScheduleBangIdAsync(long tenantId)
|
|
|
+ {
|
|
|
+ var id = await _db.Ado.GetLongAsync(
|
|
|
+ """
|
|
|
+ SELECT id FROM aidop_action_run_log
|
|
|
+ WHERE tenant_id = @TenantId AND action_code = @ActionCode AND status = 'SUCCESS'
|
|
|
+ ORDER BY start_time DESC, id DESC
|
|
|
+ LIMIT 1
|
|
|
+ """,
|
|
|
+ new List<SugarParameter>
|
|
|
+ {
|
|
|
+ new("@TenantId", tenantId),
|
|
|
+ new("@ActionCode", "S2_SCHEDULE_GENERATE")
|
|
|
+ });
|
|
|
+ return id > 0 ? id : null;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+### 6.4 批量检查传来源
|
|
|
+
|
|
|
+**定位锚点**(`CheckTenantWorkOrdersAsync` 内):
|
|
|
+
|
|
|
+```csharp
|
|
|
+ var one = await CheckSingleAsync(tenantId, wo, account);
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ var one = await CheckSingleAsync(tenantId, wo, account, bangId: 0, checkScope: "BATCH");
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 第 5 步:`ResourceCheckResultWriter` 修失效范围并记来源
|
|
|
+
|
|
|
+**文件**:`server/Plugins/Admin.NET.Plugin.AiDOP/Order/ResourceCheckResultWriter.cs`
|
|
|
+
|
|
|
+### 7.1 `WriteAsync` 增加两个参数
|
|
|
+
|
|
|
+**定位锚点**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ public async Task<OrderResourceCheckResult> WriteAsync(
|
|
|
+ OrderWorkOrderGenerationService.OrderHeader order,
|
|
|
+ OrderWorkOrderGenerationService.OrderEntryLine entry,
|
|
|
+ string workOrd,
|
|
|
+ long? morderId,
|
|
|
+ IReadOnlyList<ResourceCheckBomLine> lines,
|
|
|
+ string account,
|
|
|
+ DateTime now)
|
|
|
+ {
|
|
|
+ await InvalidatePreviousAsync(entry.Id, entry.TenantId, workOrd, now);
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ public async Task<OrderResourceCheckResult> WriteAsync(
|
|
|
+ OrderWorkOrderGenerationService.OrderHeader order,
|
|
|
+ OrderWorkOrderGenerationService.OrderEntryLine entry,
|
|
|
+ string workOrd,
|
|
|
+ long? morderId,
|
|
|
+ IReadOnlyList<ResourceCheckBomLine> lines,
|
|
|
+ string account,
|
|
|
+ DateTime now,
|
|
|
+ long bangId = 0,
|
|
|
+ string checkScope = "REVIEW")
|
|
|
+ {
|
|
|
+ await InvalidatePreviousAsync(entry.Id, entry.TenantId, workOrd, now);
|
|
|
+```
|
|
|
+
|
|
|
+### 7.2 INSERT 写入 `bangid` 与 `check_scope`
|
|
|
+
|
|
|
+**定位锚点**(INSERT 的列清单与 VALUES):
|
|
|
+
|
|
|
+```csharp
|
|
|
+ INSERT INTO b_examine_result (
|
|
|
+ Id, sorderid, bill_no, sentry_id, entry_seq, morder_id, morder_no, bom_number,
|
|
|
+ need_qty, need_time, earliest_times, latest_times, order_statr_time, kitting_times,
|
|
|
+ sys_capacity_date, sys_material_date,
|
|
|
+ create_by_name, create_time, update_by_name, update_time,
|
|
|
+ tenant_id, factory_id, company_id, IsDeleted
|
|
|
+ ) VALUES (
|
|
|
+ @Id, @SorderId, @BillNo, @SentryId, @EntrySeq, @MorderId, @MorderNo, @BomNumber,
|
|
|
+ @NeedQty, @NeedTime, @NeedTime, @NeedTime, @NeedTime, @KittingTime,
|
|
|
+ @NeedTime, @KittingTime,
|
|
|
+ @User, @Now, @User, @Now,
|
|
|
+ @TenantId, @FactoryId, @CompanyId, 0
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ INSERT INTO b_examine_result (
|
|
|
+ Id, sorderid, bill_no, sentry_id, entry_seq, morder_id, morder_no, bom_number,
|
|
|
+ need_qty, need_time, earliest_times, latest_times, order_statr_time, kitting_times,
|
|
|
+ sys_capacity_date, sys_material_date,
|
|
|
+ bangid, check_scope, net_scope,
|
|
|
+ create_by_name, create_time, update_by_name, update_time,
|
|
|
+ tenant_id, factory_id, company_id, IsDeleted
|
|
|
+ ) VALUES (
|
|
|
+ @Id, @SorderId, @BillNo, @SentryId, @EntrySeq, @MorderId, @MorderNo, @BomNumber,
|
|
|
+ @NeedQty, @NeedTime, @NeedTime, @NeedTime, @NeedTime, @KittingTime,
|
|
|
+ @NeedTime, @KittingTime,
|
|
|
+ @BangId, @CheckScope, @NetScope,
|
|
|
+ @User, @Now, @User, @Now,
|
|
|
+ @TenantId, @FactoryId, @CompanyId, 0
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+并在参数列表里(`new SugarParameter("@KittingTime", kittingTime ?? needTime),` 之后)**插入**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ new SugarParameter("@BangId", bangId > 0 ? bangId : (object)DBNull.Value),
|
|
|
+ new SugarParameter("@CheckScope", string.IsNullOrWhiteSpace(checkScope) ? "REVIEW" : checkScope.Trim()),
|
|
|
+ new SugarParameter("@NetScope", bangId > 0 ? 1 : 0),
|
|
|
+```
|
|
|
+
|
|
|
+### 7.3 修 `InvalidatePreviousAsync`(本包最关键的一处)
|
|
|
+
|
|
|
+**定位锚点**(整个方法):
|
|
|
+
|
|
|
+```csharp
|
|
|
+ private async Task InvalidatePreviousAsync(long entryId, long tenantId, string morderNo, DateTime now)
|
|
|
+ {
|
|
|
+ var oldIds = await _db.Ado.SqlQueryAsync<long>(
|
|
|
+ """
|
|
|
+ SELECT Id FROM b_examine_result
|
|
|
+ WHERE tenant_id = @TenantId AND IsDeleted = 0
|
|
|
+ AND (sentry_id = @EntryId OR morder_no = @MorderNo)
|
|
|
+ """,
|
|
|
+ new SugarParameter("@EntryId", entryId),
|
|
|
+ new SugarParameter("@TenantId", tenantId),
|
|
|
+ new SugarParameter("@MorderNo", morderNo));
|
|
|
+
|
|
|
+ if (oldIds.Count == 0)
|
|
|
+ return;
|
|
|
+
|
|
|
+ foreach (var examineId in oldIds)
|
|
|
+ {
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ UPDATE b_bom_child_examine
|
|
|
+ SET is_use = 0, update_time = @Now
|
|
|
+ WHERE examine_id = @ExamineId
|
|
|
+ """,
|
|
|
+ new SugarParameter("@ExamineId", examineId),
|
|
|
+ new SugarParameter("@Now", now));
|
|
|
+ }
|
|
|
+
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ UPDATE b_examine_result
|
|
|
+ SET IsDeleted = 1, update_time = @Now
|
|
|
+ WHERE sentry_id = @EntryId AND tenant_id = @TenantId AND IsDeleted = 0
|
|
|
+ """,
|
|
|
+ new SugarParameter("@EntryId", entryId),
|
|
|
+ new SugarParameter("@TenantId", tenantId),
|
|
|
+ new SugarParameter("@Now", now));
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ /// <summary>
|
|
|
+ /// 让同一工单(或同一订单明细行)的历史齐套结果失效。
|
|
|
+ ///
|
|
|
+ /// 两个要点:
|
|
|
+ /// 1) 以 morder_no 为主条件。sentry_id 只在 > 0 时追加——工单不挂订单明细行时 entry.Id = 0,
|
|
|
+ /// 若按 sentry_id = 0 失效会命中所有同类历史结果,把别的工单的结论一起标删。
|
|
|
+ /// 2) SELECT 与 UPDATE 必须用同一套条件,否则会出现"子表明细已失效、头还是有效"的半失效状态。
|
|
|
+ /// </summary>
|
|
|
+ private async Task InvalidatePreviousAsync(long entryId, long tenantId, string morderNo, DateTime now)
|
|
|
+ {
|
|
|
+ var mo = (morderNo ?? "").Trim();
|
|
|
+ if (mo.Length == 0 && entryId <= 0)
|
|
|
+ return;
|
|
|
+
|
|
|
+ var conds = new List<string>();
|
|
|
+ var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
|
|
|
+ if (mo.Length > 0)
|
|
|
+ {
|
|
|
+ conds.Add("morder_no = @MorderNo");
|
|
|
+ pars.Add(new SugarParameter("@MorderNo", mo));
|
|
|
+ }
|
|
|
+ if (entryId > 0)
|
|
|
+ {
|
|
|
+ conds.Add("sentry_id = @EntryId");
|
|
|
+ pars.Add(new SugarParameter("@EntryId", entryId));
|
|
|
+ }
|
|
|
+
|
|
|
+ var scope = $"tenant_id = @TenantId AND IsDeleted = 0 AND ({string.Join(" OR ", conds)})";
|
|
|
+
|
|
|
+ var oldIds = await _db.Ado.SqlQueryAsync<long>(
|
|
|
+ $"SELECT Id FROM b_examine_result WHERE {scope}", pars);
|
|
|
+
|
|
|
+ if (oldIds.Count == 0)
|
|
|
+ return;
|
|
|
+
|
|
|
+ foreach (var examineId in oldIds)
|
|
|
+ {
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ UPDATE b_bom_child_examine
|
|
|
+ SET is_use = 0, update_time = @Now
|
|
|
+ WHERE examine_id = @ExamineId
|
|
|
+ """,
|
|
|
+ new SugarParameter("@ExamineId", examineId),
|
|
|
+ new SugarParameter("@Now", now));
|
|
|
+ }
|
|
|
+
|
|
|
+ var updPars = new List<SugarParameter>(pars) { new("@Now", now) };
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ $"UPDATE b_examine_result SET IsDeleted = 1, update_time = @Now WHERE {scope}",
|
|
|
+ updPars);
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. 第 6 步:下达路径与手动检查传来源
|
|
|
+
|
|
|
+**文件**:`server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/WorkOrderDispatchService.cs`
|
|
|
+
|
|
|
+### 8.1 下达
|
|
|
+
|
|
|
+**定位锚点**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ var kitting = await _kittingCheck.CheckSingleAsync(tid, wo, account);
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ var kitting = await _kittingCheck.CheckSingleAsync(tid, wo, account, bangId: 0, checkScope: "RELEASE");
|
|
|
+```
|
|
|
+
|
|
|
+(下达仍不因缺料阻断 —— C-D7。)
|
|
|
+
|
|
|
+### 8.2 手动单工单检查:缺料不再抛异常
|
|
|
+
|
|
|
+**定位锚点**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ var result = await _kittingCheck.CheckSingleAsync(tid, wo, account);
|
|
|
+ if (!result.IsKitted)
|
|
|
+ {
|
|
|
+ await _runLog.FailedAsync(logId, $"缺料行 {result.ShortageLineCount}", result);
|
|
|
+ throw Oops.Oh($"工单 {workord} 齐套检查未通过(缺料行 {result.ShortageLineCount})");
|
|
|
+ }
|
|
|
+
|
|
|
+ await _runLog.SuccessAsync(logId, "单工单齐套检查通过", result);
|
|
|
+ return new { message = "ok", materialSituation = result.MaterialSituation };
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ if (ex.Message.Contains("齐套检查未通过", StringComparison.Ordinal))
|
|
|
+ throw;
|
|
|
+ await _runLog.FailedAsync(logId, ex.Message, new { workOrd = wo, tenantId = tid });
|
|
|
+ throw;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ var result = await _kittingCheck.CheckSingleAsync(tid, wo, account, bangId: 0, checkScope: "SINGLE");
|
|
|
+
|
|
|
+ // 缺料是正常业务结论,不是接口失败:照常返回,由前端以告警形式呈现并刷新列表
|
|
|
+ await _runLog.SuccessAsync(
|
|
|
+ logId,
|
|
|
+ result.IsKitted ? "单工单齐套检查通过" : $"单工单齐套检查完成(缺料行 {result.ShortageLineCount})",
|
|
|
+ result);
|
|
|
+ return new
|
|
|
+ {
|
|
|
+ message = "ok",
|
|
|
+ materialSituation = result.MaterialSituation,
|
|
|
+ isKitted = result.IsKitted,
|
|
|
+ shortageLineCount = result.ShortageLineCount
|
|
|
+ };
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ await _runLog.FailedAsync(logId, ex.Message, new { workOrd = wo, tenantId = tid });
|
|
|
+ throw;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. 第 7 步:前端
|
|
|
+
|
|
|
+### 9.1 API 返回结果
|
|
|
+
|
|
|
+**文件**:`Web/src/views/aidop/api/workOrderDispatch.ts`
|
|
|
+
|
|
|
+**定位锚点**:
|
|
|
+
|
|
|
+```typescript
|
|
|
+/** 单工单齐套检查(替代旧 WorkOrdKittingCheckByNo) */
|
|
|
+export async function kittingCheckSingle(workOrd: string, domain: string, userAccount: string): Promise<void> {
|
|
|
+ await service.post('/api/WorkOrder/dispatch/work-order-kitting-check', null, {
|
|
|
+ params: { workord: workOrd, domain, userAccount },
|
|
|
+ timeout: 300000,
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```typescript
|
|
|
+export interface KittingCheckSingleResult {
|
|
|
+ materialSituation?: string | null;
|
|
|
+ isKitted?: boolean;
|
|
|
+ shortageLineCount?: number;
|
|
|
+}
|
|
|
+
|
|
|
+/** 单工单齐套检查(替代旧 WorkOrdKittingCheckByNo);缺料属正常结论,由调用方按 isKitted 呈现 */
|
|
|
+export async function kittingCheckSingle(
|
|
|
+ workOrd: string,
|
|
|
+ domain: string,
|
|
|
+ userAccount: string
|
|
|
+): Promise<KittingCheckSingleResult> {
|
|
|
+ const res = await service.post('/api/WorkOrder/dispatch/work-order-kitting-check', null, {
|
|
|
+ params: { workord: workOrd, domain, userAccount },
|
|
|
+ timeout: 300000,
|
|
|
+ });
|
|
|
+ const body = res.data as (KittingCheckSingleResult & { result?: KittingCheckSingleResult }) | undefined;
|
|
|
+ return body?.result ?? body ?? {};
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 9.2 缺料改为告警并刷新列表
|
|
|
+
|
|
|
+**文件**:`Web/src/views/aidop/business/workOrderDispatchList.vue`
|
|
|
+
|
|
|
+**定位锚点**:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ const account = userInfo.userInfos?.account ?? '';
|
|
|
+ singleKittingWo.value = row.workOrd;
|
|
|
+ try {
|
|
|
+ await kittingCheckSingle(row.workOrd, domain, account);
|
|
|
+ ElMessage.success(`工单 ${row.workOrd} 齐套检查已执行`);
|
|
|
+ await loadList();
|
|
|
+ } catch (e: any) {
|
|
|
+ ElMessage.error(e?.message || '单工单齐套检查失败');
|
|
|
+ } finally {
|
|
|
+ singleKittingWo.value = null;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+**替换为**:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ const account = userInfo.userInfos?.account ?? '';
|
|
|
+ singleKittingWo.value = row.workOrd;
|
|
|
+ try {
|
|
|
+ const r = await kittingCheckSingle(row.workOrd, domain, account);
|
|
|
+ if (r.isKitted === false) {
|
|
|
+ ElMessage.warning(`工单 ${row.workOrd} 缺料(缺料行 ${r.shortageLineCount ?? 0})`);
|
|
|
+ } else {
|
|
|
+ ElMessage.success(`工单 ${row.workOrd} 齐套检查已执行`);
|
|
|
+ }
|
|
|
+ await loadList();
|
|
|
+ } catch (e: any) {
|
|
|
+ ElMessage.error(e?.message || '单工单齐套检查失败');
|
|
|
+ await loadList();
|
|
|
+ } finally {
|
|
|
+ singleKittingWo.value = null;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+> `catch` 分支也加 `loadList()`:即使接口报错,物料状况可能已被后端更新,列表必须与库里一致。
|
|
|
+
|
|
|
+### 9.3 不要顺手做的事
|
|
|
+
|
|
|
+- **不要**改「批量齐套检查」按钮的提示逻辑(后端本来就不抛缺料异常)。
|
|
|
+- **不要**改表格列定义、`materialSituation` 的着色规则。
|
|
|
+- **不要**给缺料行加弹窗/抽屉展示明细——那是新功能。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 10. 与 WP-B 的并行冲突
|
|
|
+
|
|
|
+两包都改 `MaterialRequirementCalculator.cs`:
|
|
|
+
|
|
|
+| 包 | 改动区段 |
|
|
|
+|---|---|
|
|
|
+| WP-B | 构造函数、`ApplySupplyAsync` **循环入口前**的库存预取、新增私有取数方法 |
|
|
|
+| WP-C(本包) | `BuildLinesAsync` 签名与 3 处透传、`ApplySupplyAsync` **签名**、**开头的占用查询块**、**末尾的占用写入块入口** |
|
|
|
+
|
|
|
+`ApplySupplyAsync` 的签名行两包都会碰(WP-B 不改签名,只改方法体,所以实际只有本包改签名)。约定:**先完成的先提,后完成的 rebase**,rebase 后各自重跑核心验收。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 11. 验收
|
|
|
+
|
|
|
+### 11.1 编译
|
|
|
+
|
|
|
+```powershell
|
|
|
+dotnet build server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj -c Debug
|
|
|
+cd Web; pnpm build
|
|
|
+```
|
|
|
+
|
|
|
+### 11.2 口径一致(核心验收)
|
|
|
+
|
|
|
+1. 先「全量重排」跑一次,记下产生的排程版本:
|
|
|
+ ```sql
|
|
|
+ SELECT id FROM aidop_action_run_log
|
|
|
+ WHERE action_code = 'S2_SCHEDULE_GENERATE' AND status = 'SUCCESS'
|
|
|
+ ORDER BY id DESC LIMIT 1;
|
|
|
+ ```
|
|
|
+2. 记录排产写出的缺料明细:
|
|
|
+ ```sql
|
|
|
+ SELECT c.item_number, c.needCount, c.sqty, c.lack_qty
|
|
|
+ FROM b_bom_child_examine c JOIN b_examine_result e ON e.Id = c.examine_id
|
|
|
+ WHERE e.morder_no = 'M500000004' AND e.IsDeleted = 0 AND c.is_use = 1
|
|
|
+ ORDER BY c.item_number;
|
|
|
+ ```
|
|
|
+3. 对同一工单点「齐套检查」,再跑一次上面的 SQL。
|
|
|
+
|
|
|
+**期望:两次的 `lack_qty` 逐行完全一致。** 这是 D-A02 是否修好的唯一判据。
|
|
|
+
|
|
|
+### 11.3 来源可追溯
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 期望:排产那条 check_scope='SCHEDULE'、net_scope=1、bangid=排程版本;
|
|
|
+-- 手点那条 check_scope='SINGLE'、net_scope=1、bangid=同一个排程版本
|
|
|
+SELECT Id, morder_no, bangid, check_scope, net_scope, IsDeleted, create_time
|
|
|
+FROM b_examine_result WHERE morder_no = 'M500000004'
|
|
|
+ORDER BY create_time DESC LIMIT 5;
|
|
|
+```
|
|
|
+
|
|
|
+### 11.4 重复检查不叠加缺料(C-D2)
|
|
|
+
|
|
|
+对同一工单**连点三次**齐套检查,每次都记录 `lack_qty` 汇总:
|
|
|
+
|
|
|
+```sql
|
|
|
+SELECT SUM(c.lack_qty) AS lack_total
|
|
|
+FROM b_bom_child_examine c JOIN b_examine_result e ON e.Id = c.examine_id
|
|
|
+WHERE e.morder_no = 'M500000004' AND e.IsDeleted = 0 AND c.is_use = 1;
|
|
|
+```
|
|
|
+
|
|
|
+**期望:三次结果完全相同**(不递增)。
|
|
|
+
|
|
|
+### 11.5 手动检查不写占用(C-D3)
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 点齐套检查前后各跑一次,期望两次结果一致
|
|
|
+SELECT COUNT(*) AS occ_rows, IFNULL(SUM(quantity), 0) AS occ_qty
|
|
|
+FROM ic_item_stockoccupy WHERE morder_mo = 'M500000004' AND IsDeleted = 0;
|
|
|
+
|
|
|
+SELECT COUNT(*) AS po_rows, IFNULL(SUM(OccupyQty), 0) AS po_qty
|
|
|
+FROM srm_po_occupy WHERE morder_mo = 'M500000004' AND IsDeleted = 0;
|
|
|
+```
|
|
|
+
|
|
|
+### 11.6 失效范围不越界(C-D4)
|
|
|
+
|
|
|
+1. 记录另一个工单(例如 `M500000003`)当前有效结果的 Id:
|
|
|
+ ```sql
|
|
|
+ SELECT Id, morder_no, IsDeleted FROM b_examine_result
|
|
|
+ WHERE morder_no = 'M500000003' AND IsDeleted = 0;
|
|
|
+ ```
|
|
|
+2. 对 `M500000004` 点齐套检查。
|
|
|
+3. 重跑第 1 步 SQL。**期望:`M500000003` 的行仍然 `IsDeleted = 0`,Id 不变。**
|
|
|
+
|
|
|
+补充验证"雷已排除":造一个 `BusinessID = 0` 的工单(UAT 库可用),对它点齐套检查,确认其他工单结果不受影响。
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 造数前先备份 BusinessID;验证完还原
|
|
|
+UPDATE WorkOrdMaster SET BusinessID = 0 WHERE WorkOrd = '<某测试工单>';
|
|
|
+-- 点齐套检查后:
|
|
|
+SELECT COUNT(*) AS still_active FROM b_examine_result WHERE IsDeleted = 0;
|
|
|
+-- 期望:只有该测试工单的旧结果被失效,总数不会塌成 1
|
|
|
+```
|
|
|
+
|
|
|
+### 11.7 前端
|
|
|
+
|
|
|
+- 缺料工单点「齐套检查」:出现**黄色告警**(含缺料行数),列表**自动刷新**,物料状况列变为"缺料"。
|
|
|
+- 齐套工单点「齐套检查」:绿色成功提示,列表刷新,状态"齐套"。
|
|
|
+- 工单下达流程不变(缺料不阻断下达)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 12. 风险与回滚
|
|
|
+
|
|
|
+| 风险 | 影响 | 处置 |
|
|
|
+|---|---|---|
|
|
|
+| 统一净口径后缺料量普遍变大 | 用户觉得"以前齐套现在缺料了" | **这是修正而非退化**。验收 11.2 保证两条路径一致;对外说明口径变更并保留 11.3 的 `net_scope` 标记作为凭据 |
|
|
|
+| 无成功排产记录时 `bangId = 0` | 口径退化为毛口径 | 与改动前一致,不会更差。`net_scope = 0` 会如实记录 |
|
|
|
+| `InvalidatePreviousAsync` 改为按 `morder_no` 后失效面变大 | 同一工单跨订单行的历史结果一起失效 | 这是期望行为:同一工单只应有一条有效结论 |
|
|
|
+| 前端不再报错,用户忽略缺料 | 缺料被忽视 | 告警文案带缺料行数,且列表状态同步更新 |
|
|
|
+
|
|
|
+**回滚**:还原 5 个代码文件。`check_scope` / `net_scope` 列可留(可空,不影响旧代码)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 13. 明确不做
|
|
|
+
|
|
|
+- 不改 165 的表、存储过程、数据。
|
|
|
+- 不改库存来源(本库 vs 165)—— 那是 WP-B。
|
|
|
+- 不改下达的阻断规则(缺料仍可下达)。
|
|
|
+- 不改 BOM 展开算法、损耗/报废系数、虚拟件处理。
|
|
|
+- 不改在途 `PurOrdDetail` 的口径。
|
|
|
+- 不清理 `b_examine_result` / `b_bom_child_examine` 的历史数据。
|
|
|
+- 不做缺料明细的新页面/弹窗。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 14. 版本号(强制)
|
|
|
+
|
|
|
+本包**同时**包含后端代码 + 后端迁移脚本 + 前端代码,因此同一提交内**两端都要递增**:
|
|
|
+
|
|
|
+- `server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj` 的 `<Version>` / `<AssemblyVersion>` / `<FileVersion>` 三处同号 patch +1
|
|
|
+- `Web/package.json` 的 `version` patch +1(当前 `2.4.285`,提交时按仓库实际值末位 +1)
|
|
|
+
|
|
|
+若前端与后端分两次提交,则各自只递增本次纳入提交的那一端。
|