@chaobinchen/mes-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -0
- package/bin/mes.js +263 -0
- package/package.json +40 -0
- package/src/commands/init.js +77 -0
- package/src/commands/material.js +483 -0
- package/src/commands/progress.js +172 -0
- package/src/commands/reports.js +334 -0
- package/src/commands/weighing.js +490 -0
- package/src/commands/work-order.js +508 -0
- package/src/config.js +90 -0
- package/src/db.js +92 -0
- package/src/display.js +78 -0
- package/src/encoding.js +57 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 物料相关命令(场景 18–23)
|
|
3
|
+
*
|
|
4
|
+
* 18. stock-records (sr) — 物料批次出入库记录
|
|
5
|
+
* 19. stock-inventory (si) — 物料库存查询
|
|
6
|
+
* 20. trace-forward (tf) — 半成品编码+批次追溯原料
|
|
7
|
+
* 21. trace-backward (tb) — 原料编码+批次反向跟踪半成品
|
|
8
|
+
* 22. material-usage (mu) — 近几天原料使用明细
|
|
9
|
+
* 23. stock-coverage (sc) — 原料库存与使用量对比
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const { queryWithParams, close, sql } = require('../db');
|
|
15
|
+
const { resolveConfig } = require('../config');
|
|
16
|
+
const { printTable } = require('../display');
|
|
17
|
+
const chalk = require('chalk');
|
|
18
|
+
|
|
19
|
+
// ─── 场景 18:物料批次出入库记录 ───────────────────────────────────────────────
|
|
20
|
+
const STOCK_RECORDS_SQL = `
|
|
21
|
+
SELECT
|
|
22
|
+
material_code,
|
|
23
|
+
material_name,
|
|
24
|
+
operate_batch,
|
|
25
|
+
operate_quantity,
|
|
26
|
+
CASE
|
|
27
|
+
WHEN operate_type = 1 THEN N'下架'
|
|
28
|
+
WHEN operate_type = 2 THEN N'上架'
|
|
29
|
+
ELSE N'未知类型'
|
|
30
|
+
END AS operate_type_text,
|
|
31
|
+
CASE
|
|
32
|
+
WHEN task_type = 1 THEN N'入库'
|
|
33
|
+
WHEN task_type = 2 THEN N'出库'
|
|
34
|
+
WHEN task_type = 4 THEN N'移库'
|
|
35
|
+
WHEN task_type = 8 THEN N'盘点'
|
|
36
|
+
ELSE N'未知任务'
|
|
37
|
+
END AS task_type_text,
|
|
38
|
+
CASE
|
|
39
|
+
WHEN operate_property = 1 THEN N'正常执行'
|
|
40
|
+
WHEN operate_property = 2 THEN N'虚补'
|
|
41
|
+
WHEN operate_property = 4 THEN N'虚退'
|
|
42
|
+
ELSE N'未知性质'
|
|
43
|
+
END AS operate_property_text,
|
|
44
|
+
CASE
|
|
45
|
+
WHEN quality_inspection_status = 1 THEN N'未启用'
|
|
46
|
+
WHEN quality_inspection_status = 2 THEN N'待检验'
|
|
47
|
+
WHEN quality_inspection_status = 4 THEN N'检验中'
|
|
48
|
+
WHEN quality_inspection_status = 8 THEN N'合格'
|
|
49
|
+
WHEN quality_inspection_status = 16 THEN N'不合格'
|
|
50
|
+
WHEN quality_inspection_status = 32 THEN N'过期'
|
|
51
|
+
ELSE N'未知状态'
|
|
52
|
+
END AS quality_status,
|
|
53
|
+
CASE
|
|
54
|
+
WHEN consumed_state = 1 THEN N'未过账'
|
|
55
|
+
WHEN consumed_state = 2 THEN N'已过账'
|
|
56
|
+
WHEN consumed_state = 4 THEN N'过账撤销'
|
|
57
|
+
ELSE N'未知状态'
|
|
58
|
+
END AS consumed_status,
|
|
59
|
+
CASE
|
|
60
|
+
WHEN terminal = 1 THEN N'PDA端'
|
|
61
|
+
WHEN terminal = 2 THEN N'网页端'
|
|
62
|
+
ELSE N'未知终端'
|
|
63
|
+
END AS terminal_text,
|
|
64
|
+
CASE
|
|
65
|
+
WHEN audit_status = 0 THEN N'未审核'
|
|
66
|
+
WHEN audit_status = 1 THEN N'已审核'
|
|
67
|
+
WHEN audit_status = 2 THEN N'无需审核'
|
|
68
|
+
ELSE N'未知状态'
|
|
69
|
+
END AS audit_status_text,
|
|
70
|
+
user_name,
|
|
71
|
+
store_name,
|
|
72
|
+
area_name,
|
|
73
|
+
location_name,
|
|
74
|
+
CONVERT(NVARCHAR(19), create_time, 120) AS record_time
|
|
75
|
+
FROM wms_tasks_execute
|
|
76
|
+
WHERE material_code = @material_code
|
|
77
|
+
AND ISNULL(is_delete, 0) = 0
|
|
78
|
+
AND (@batch IS NULL OR operate_batch = @batch)
|
|
79
|
+
ORDER BY create_time DESC, id DESC;
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
async function stockRecordsCommand(materialCode, options, command) {
|
|
83
|
+
let dbConfig;
|
|
84
|
+
try { dbConfig = resolveConfig(command.parent.opts()); }
|
|
85
|
+
catch (err) { console.error(chalk.red('✖ 连接配置错误:'), err.message); process.exitCode = 1; return; }
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
console.log(chalk.dim(`⏳ 正在查询物料出入库记录: ${materialCode} …`));
|
|
89
|
+
const rows = await queryWithParams(STOCK_RECORDS_SQL, [
|
|
90
|
+
{ name: 'material_code', type: sql.NVarChar(100), value: materialCode },
|
|
91
|
+
{ name: 'batch', type: sql.NVarChar(100), value: options.batch || null },
|
|
92
|
+
], dbConfig);
|
|
93
|
+
printTable(rows, `物料出入库记录 — ${materialCode}${options.batch ? ' / ' + options.batch : ''}`);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(chalk.red('✖ 查询失败:'), err.message);
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
} finally { await close(); }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── 场景 19:物料库存查询 ─────────────────────────────────────────────────────
|
|
101
|
+
const STOCK_INVENTORY_SQL = `
|
|
102
|
+
SELECT
|
|
103
|
+
material_code,
|
|
104
|
+
material_name,
|
|
105
|
+
batch,
|
|
106
|
+
CAST(quantity AS FLOAT) AS quantity,
|
|
107
|
+
CAST(frozen_quantity AS FLOAT) AS frozen_quantity,
|
|
108
|
+
CAST(quantity_to_checkin AS FLOAT) AS quantity_to_checkin,
|
|
109
|
+
CAST(quantity_to_checkout AS FLOAT) AS quantity_to_checkout,
|
|
110
|
+
unit,
|
|
111
|
+
store_name,
|
|
112
|
+
area_name,
|
|
113
|
+
location_name,
|
|
114
|
+
CASE
|
|
115
|
+
WHEN quality_inspection_status = 0 THEN N'未启用'
|
|
116
|
+
WHEN quality_inspection_status = 1 THEN N'未检验'
|
|
117
|
+
WHEN quality_inspection_status = 2 THEN N'检验中'
|
|
118
|
+
WHEN quality_inspection_status = 4 THEN N'合格'
|
|
119
|
+
WHEN quality_inspection_status = 8 THEN N'不合格'
|
|
120
|
+
WHEN quality_inspection_status = 16 THEN N'过期'
|
|
121
|
+
ELSE N'未知状态'
|
|
122
|
+
END AS quality_status,
|
|
123
|
+
CASE WHEN is_expired = 1 THEN N'已过期' ELSE N'未过期' END AS expired_status,
|
|
124
|
+
CONVERT(NVARCHAR(10), expiration_date, 120) AS expiration_date
|
|
125
|
+
FROM wms_inventory_record
|
|
126
|
+
WHERE material_code = @material_code
|
|
127
|
+
AND ISNULL(is_delete, 0) = 0
|
|
128
|
+
AND (@batch IS NULL OR batch = @batch)
|
|
129
|
+
ORDER BY expiration_date, quantity DESC;
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
async function stockInventoryCommand(materialCode, options, command) {
|
|
133
|
+
let dbConfig;
|
|
134
|
+
try { dbConfig = resolveConfig(command.parent.opts()); }
|
|
135
|
+
catch (err) { console.error(chalk.red('✖ 连接配置错误:'), err.message); process.exitCode = 1; return; }
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
console.log(chalk.dim(`⏳ 正在查询物料库存: ${materialCode} …`));
|
|
139
|
+
const rows = await queryWithParams(STOCK_INVENTORY_SQL, [
|
|
140
|
+
{ name: 'material_code', type: sql.NVarChar(100), value: materialCode },
|
|
141
|
+
{ name: 'batch', type: sql.NVarChar(100), value: options.batch || null },
|
|
142
|
+
], dbConfig);
|
|
143
|
+
printTable(rows, `物料库存 — ${materialCode}${options.batch ? ' / ' + options.batch : ''}`);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error(chalk.red('✖ 查询失败:'), err.message);
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
} finally { await close(); }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── 场景 20:半成品编码+批次追溯原料 ─────────────────────────────────────────
|
|
151
|
+
const TRACE_FORWARD_SQL = `
|
|
152
|
+
SET NOCOUNT ON;
|
|
153
|
+
|
|
154
|
+
IF OBJECT_ID('tempdb..#bulk_work_orders_trace') IS NOT NULL DROP TABLE #bulk_work_orders_trace;
|
|
155
|
+
CREATE TABLE #bulk_work_orders_trace (id BIGINT NOT NULL);
|
|
156
|
+
INSERT INTO #bulk_work_orders_trace (id)
|
|
157
|
+
SELECT id FROM psm_work_order
|
|
158
|
+
WHERE product_code = @semi_product_code
|
|
159
|
+
AND batch_num = @semi_batch_num
|
|
160
|
+
AND tr_type = 2
|
|
161
|
+
AND ISNULL(is_deleted, 0) = 0;
|
|
162
|
+
|
|
163
|
+
IF NOT EXISTS (SELECT 1 FROM #bulk_work_orders_trace)
|
|
164
|
+
BEGIN
|
|
165
|
+
SELECT CAST(0 AS BIT) AS success,
|
|
166
|
+
N'未找到半成品编码' + @semi_product_code + N'、半成品批次' + @semi_batch_num + N'对应的半成品工单' AS message,
|
|
167
|
+
CAST(NULL AS NVARCHAR(100)) AS material_code, CAST(NULL AS NVARCHAR(200)) AS material_name,
|
|
168
|
+
CAST(NULL AS NVARCHAR(100)) AS material_batch, CAST(NULL AS DECIMAL(38,10)) AS actual_quantity;
|
|
169
|
+
RETURN;
|
|
170
|
+
END;
|
|
171
|
+
|
|
172
|
+
CREATE UNIQUE CLUSTERED INDEX IX_bulk_work_orders_trace_id ON #bulk_work_orders_trace(id);
|
|
173
|
+
|
|
174
|
+
;WITH premix_material AS (
|
|
175
|
+
SELECT b.code AS material_code, b.name AS material_name,
|
|
176
|
+
ISNULL(NULLIF(LTRIM(RTRIM(e.batch_number)), N''), N'未记录批次') AS material_batch,
|
|
177
|
+
ISNULL(e.quantity, 0) AS actual_quantity
|
|
178
|
+
FROM psm_bulk_trcp_logic_premix_barcode b
|
|
179
|
+
INNER JOIN #bulk_work_orders_trace wo ON wo.id = b.work_order_id
|
|
180
|
+
INNER JOIN psm_bulk_trcp_logic_premix_barcode_execute e
|
|
181
|
+
ON e.put_id = b.id AND e.work_order_id = b.work_order_id
|
|
182
|
+
WHERE b.put_material_type = 0 AND ISNULL(b.is_delete, 0) = 0
|
|
183
|
+
),
|
|
184
|
+
manual_material AS (
|
|
185
|
+
SELECT b.code AS material_code, b.name AS material_name,
|
|
186
|
+
ISNULL(NULLIF(LTRIM(RTRIM(r.barcode_material_batch)), N''), N'未记录批次') AS material_batch,
|
|
187
|
+
ISNULL(r.barcode_material_quantity, 0) AS actual_quantity
|
|
188
|
+
FROM psm_bulk_trcp_logic_step_barcode b
|
|
189
|
+
INNER JOIN #bulk_work_orders_trace wo ON wo.id = b.work_order_id
|
|
190
|
+
INNER JOIN psm_bulk_trcplc_report_put_material_item r
|
|
191
|
+
ON r.tr_set_barcode_id = b.id AND r.work_order_id = b.work_order_id
|
|
192
|
+
WHERE b.put_material_type = 0 AND ISNULL(b.is_delete, 0) = 0
|
|
193
|
+
),
|
|
194
|
+
auto_phase_base AS (
|
|
195
|
+
SELECT p.work_order_id, p.spar00 AS material_code, p.spar02 AS material_name,
|
|
196
|
+
REPLACE(REPLACE(REPLACE(ISNULL(p.execute_text, N''), N'<br/>', N'<br>'), N':', N':'), CHAR(13)+CHAR(10), N'') AS exec_text
|
|
197
|
+
FROM psm_bulk_trcp_logic_step_phase p
|
|
198
|
+
INNER JOIN #bulk_work_orders_trace wo ON wo.id = p.work_order_id
|
|
199
|
+
WHERE p.ep_type = 2 AND ISNULL(p.is_delete, 0) = 0
|
|
200
|
+
),
|
|
201
|
+
auto_material AS (
|
|
202
|
+
SELECT ap.material_code, ap.material_name,
|
|
203
|
+
ISNULL(NULLIF(LTRIM(RTRIM(parsed.batch_txt)), N''), N'未记录批次') AS material_batch,
|
|
204
|
+
ISNULL(TRY_CONVERT(DECIMAL(38,10), REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(parsed.weight_txt)), N'Kg', N''), N'KG', N''), N'kg', N'')), 0) AS actual_quantity
|
|
205
|
+
FROM auto_phase_base ap
|
|
206
|
+
CROSS APPLY (SELECT CHARINDEX(N'投加批次:', ap.exec_text) AS batch_tag_pos, CHARINDEX(N'投加重量:', ap.exec_text) AS weight_tag_pos) p1
|
|
207
|
+
CROSS APPLY (SELECT CASE WHEN p1.batch_tag_pos > 0 THEN p1.batch_tag_pos + LEN(N'投加批次:') ELSE 0 END AS batch_start_pos,
|
|
208
|
+
CASE WHEN p1.weight_tag_pos > 0 THEN p1.weight_tag_pos + LEN(N'投加重量:') ELSE 0 END AS weight_start_pos) p2
|
|
209
|
+
CROSS APPLY (SELECT CASE WHEN p2.batch_start_pos > 0 THEN CHARINDEX(N'<br>', ap.exec_text + N'<br>', p2.batch_start_pos) ELSE 0 END AS batch_end_pos,
|
|
210
|
+
CASE WHEN p2.weight_start_pos > 0 THEN CHARINDEX(N'<br>', ap.exec_text + N'<br>', p2.weight_start_pos) ELSE 0 END AS weight_end_pos) p3
|
|
211
|
+
CROSS APPLY (SELECT CASE WHEN p2.batch_start_pos > 0 AND p3.batch_end_pos > p2.batch_start_pos THEN SUBSTRING(ap.exec_text, p2.batch_start_pos, p3.batch_end_pos - p2.batch_start_pos) ELSE NULL END AS batch_txt,
|
|
212
|
+
CASE WHEN p2.weight_start_pos > 0 AND p3.weight_end_pos > p2.weight_start_pos THEN SUBSTRING(ap.exec_text, p2.weight_start_pos, p3.weight_end_pos - p2.weight_start_pos) ELSE NULL END AS weight_txt) parsed
|
|
213
|
+
WHERE NULLIF(LTRIM(RTRIM(ap.material_code)), N'') IS NOT NULL
|
|
214
|
+
),
|
|
215
|
+
all_material AS (
|
|
216
|
+
SELECT material_code, material_name, material_batch, actual_quantity FROM premix_material
|
|
217
|
+
UNION ALL
|
|
218
|
+
SELECT material_code, material_name, material_batch, actual_quantity FROM manual_material
|
|
219
|
+
UNION ALL
|
|
220
|
+
SELECT material_code, material_name, material_batch, actual_quantity FROM auto_material
|
|
221
|
+
)
|
|
222
|
+
SELECT CAST(1 AS BIT) AS success, CAST(NULL AS NVARCHAR(200)) AS message,
|
|
223
|
+
material_code, MAX(material_name) AS material_name, material_batch, SUM(actual_quantity) AS actual_quantity
|
|
224
|
+
FROM all_material
|
|
225
|
+
GROUP BY material_code, material_batch
|
|
226
|
+
ORDER BY material_code, material_batch
|
|
227
|
+
OPTION (RECOMPILE);
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
async function traceForwardCommand(options, command) {
|
|
231
|
+
let dbConfig;
|
|
232
|
+
try { dbConfig = resolveConfig(command.parent.opts()); }
|
|
233
|
+
catch (err) { console.error(chalk.red('✖ 连接配置错误:'), err.message); process.exitCode = 1; return; }
|
|
234
|
+
|
|
235
|
+
if (!options.code || !options.batch) {
|
|
236
|
+
console.error(chalk.red('✖ 请通过 --code <半成品编码> --batch <半成品批次> 指定查询条件'));
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
console.log(chalk.dim(`⏳ 正在追溯原料: 半成品 ${options.code} / ${options.batch} …`));
|
|
243
|
+
const rows = await queryWithParams(TRACE_FORWARD_SQL, [
|
|
244
|
+
{ name: 'semi_product_code', type: sql.NVarChar(100), value: options.code },
|
|
245
|
+
{ name: 'semi_batch_num', type: sql.NVarChar(100), value: options.batch },
|
|
246
|
+
], dbConfig);
|
|
247
|
+
|
|
248
|
+
if (!rows || rows.length === 0) {
|
|
249
|
+
console.log(chalk.yellow('⚠ 查询无返回结果'));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (!rows[0].success) {
|
|
253
|
+
console.error(chalk.red(`✖ ${rows[0].message}`));
|
|
254
|
+
process.exitCode = 1;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// 去掉 success/message 元数据列
|
|
258
|
+
const display = rows.map(r => ({
|
|
259
|
+
material_code: r.material_code,
|
|
260
|
+
material_name: r.material_name,
|
|
261
|
+
material_batch: r.material_batch,
|
|
262
|
+
actual_quantity: r.actual_quantity,
|
|
263
|
+
}));
|
|
264
|
+
printTable(display, `原料追溯 — ${options.code} / ${options.batch}`);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error(chalk.red('✖ 查询失败:'), err.message);
|
|
267
|
+
process.exitCode = 1;
|
|
268
|
+
} finally { await close(); }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─── 场景 21:原料编码+批次反向跟踪半成品 ─────────────────────────────────────
|
|
272
|
+
const TRACE_BACKWARD_SQL = `
|
|
273
|
+
SET NOCOUNT ON;
|
|
274
|
+
|
|
275
|
+
IF OBJECT_ID('tempdb..#matched_bulk_order_ids_reverse') IS NOT NULL DROP TABLE #matched_bulk_order_ids_reverse;
|
|
276
|
+
CREATE TABLE #matched_bulk_order_ids_reverse (work_order_id BIGINT NOT NULL PRIMARY KEY);
|
|
277
|
+
|
|
278
|
+
;WITH premix_match AS (
|
|
279
|
+
SELECT DISTINCT b.work_order_id
|
|
280
|
+
FROM psm_bulk_trcp_logic_premix_barcode b
|
|
281
|
+
INNER JOIN psm_bulk_trcp_logic_premix_barcode_execute e ON e.put_id = b.id AND e.work_order_id = b.work_order_id
|
|
282
|
+
WHERE b.code = @material_code
|
|
283
|
+
AND ISNULL(NULLIF(LTRIM(RTRIM(e.batch_number)), N''), N'未记录批次') = @material_batch
|
|
284
|
+
AND b.put_material_type = 0 AND ISNULL(b.is_delete, 0) = 0
|
|
285
|
+
),
|
|
286
|
+
manual_match AS (
|
|
287
|
+
SELECT DISTINCT b.work_order_id
|
|
288
|
+
FROM psm_bulk_trcp_logic_step_barcode b
|
|
289
|
+
INNER JOIN psm_bulk_trcplc_report_put_material_item r ON r.tr_set_barcode_id = b.id AND r.work_order_id = b.work_order_id
|
|
290
|
+
WHERE b.code = @material_code
|
|
291
|
+
AND ISNULL(NULLIF(LTRIM(RTRIM(r.barcode_material_batch)), N''), N'未记录批次') = @material_batch
|
|
292
|
+
AND b.put_material_type = 0 AND ISNULL(b.is_delete, 0) = 0
|
|
293
|
+
),
|
|
294
|
+
auto_phase_base AS (
|
|
295
|
+
SELECT p.work_order_id,
|
|
296
|
+
REPLACE(REPLACE(REPLACE(ISNULL(p.execute_text, N''), N'<br/>', N'<br>'), N':', N':'), CHAR(13)+CHAR(10), N'') AS exec_text
|
|
297
|
+
FROM psm_bulk_trcp_logic_step_phase p
|
|
298
|
+
WHERE p.ep_type = 2 AND p.spar00 = @material_code AND ISNULL(p.is_delete, 0) = 0
|
|
299
|
+
),
|
|
300
|
+
auto_match AS (
|
|
301
|
+
SELECT DISTINCT ap.work_order_id
|
|
302
|
+
FROM auto_phase_base ap
|
|
303
|
+
CROSS APPLY (SELECT CHARINDEX(N'投加批次:', ap.exec_text) AS batch_tag_pos) p1
|
|
304
|
+
CROSS APPLY (SELECT CASE WHEN p1.batch_tag_pos > 0 THEN p1.batch_tag_pos + LEN(N'投加批次:') ELSE 0 END AS batch_start_pos) p2
|
|
305
|
+
CROSS APPLY (SELECT CASE WHEN p2.batch_start_pos > 0 THEN CHARINDEX(N'<br>', ap.exec_text + N'<br>', p2.batch_start_pos) ELSE 0 END AS batch_end_pos) p3
|
|
306
|
+
CROSS APPLY (SELECT CASE WHEN p2.batch_start_pos > 0 AND p3.batch_end_pos > p2.batch_start_pos
|
|
307
|
+
THEN SUBSTRING(ap.exec_text, p2.batch_start_pos, p3.batch_end_pos - p2.batch_start_pos)
|
|
308
|
+
ELSE NULL END AS batch_txt) parsed
|
|
309
|
+
WHERE ISNULL(NULLIF(LTRIM(RTRIM(parsed.batch_txt)), N''), N'未记录批次') = @material_batch
|
|
310
|
+
)
|
|
311
|
+
INSERT INTO #matched_bulk_order_ids_reverse (work_order_id)
|
|
312
|
+
SELECT DISTINCT x.work_order_id FROM (
|
|
313
|
+
SELECT work_order_id FROM premix_match
|
|
314
|
+
UNION ALL SELECT work_order_id FROM manual_match
|
|
315
|
+
UNION ALL SELECT work_order_id FROM auto_match
|
|
316
|
+
) x;
|
|
317
|
+
|
|
318
|
+
IF NOT EXISTS (SELECT 1 FROM #matched_bulk_order_ids_reverse)
|
|
319
|
+
BEGIN
|
|
320
|
+
SELECT CAST(0 AS BIT) AS success,
|
|
321
|
+
N'未找到原料编码' + @material_code + N'、原料批次' + @material_batch + N'对应的半成品工单' AS message,
|
|
322
|
+
CAST(NULL AS NVARCHAR(100)) AS semi_product_code, CAST(NULL AS NVARCHAR(200)) AS semi_product_name,
|
|
323
|
+
CAST(NULL AS NVARCHAR(100)) AS semi_batch_num, CAST(NULL AS NVARCHAR(100)) AS order_num,
|
|
324
|
+
CAST(NULL AS DECIMAL(38,10)) AS planned_quantity;
|
|
325
|
+
RETURN;
|
|
326
|
+
END;
|
|
327
|
+
|
|
328
|
+
SELECT CAST(1 AS BIT) AS success, CAST(NULL AS NVARCHAR(200)) AS message,
|
|
329
|
+
w.product_code AS semi_product_code, w.product_name AS semi_product_name,
|
|
330
|
+
w.batch_num AS semi_batch_num, w.order_num, w.quantity AS planned_quantity
|
|
331
|
+
FROM psm_work_order w
|
|
332
|
+
INNER JOIN #matched_bulk_order_ids_reverse m ON m.work_order_id = w.id
|
|
333
|
+
WHERE w.tr_type = 2 AND ISNULL(w.is_deleted, 0) = 0
|
|
334
|
+
ORDER BY w.product_code, w.batch_num, w.order_num
|
|
335
|
+
OPTION (RECOMPILE);
|
|
336
|
+
`;
|
|
337
|
+
|
|
338
|
+
async function traceBackwardCommand(options, command) {
|
|
339
|
+
let dbConfig;
|
|
340
|
+
try { dbConfig = resolveConfig(command.parent.opts()); }
|
|
341
|
+
catch (err) { console.error(chalk.red('✖ 连接配置错误:'), err.message); process.exitCode = 1; return; }
|
|
342
|
+
|
|
343
|
+
if (!options.code || !options.batch) {
|
|
344
|
+
console.error(chalk.red('✖ 请通过 --code <原料编码> --batch <原料批次> 指定查询条件'));
|
|
345
|
+
process.exitCode = 1;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
console.log(chalk.dim(`⏳ 正在反向追溯半成品: 原料 ${options.code} / ${options.batch} …`));
|
|
351
|
+
const rows = await queryWithParams(TRACE_BACKWARD_SQL, [
|
|
352
|
+
{ name: 'material_code', type: sql.NVarChar(100), value: options.code },
|
|
353
|
+
{ name: 'material_batch', type: sql.NVarChar(100), value: options.batch },
|
|
354
|
+
], dbConfig);
|
|
355
|
+
|
|
356
|
+
if (!rows || rows.length === 0) {
|
|
357
|
+
console.log(chalk.yellow('⚠ 查询无返回结果'));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (!rows[0].success) {
|
|
361
|
+
console.error(chalk.red(`✖ ${rows[0].message}`));
|
|
362
|
+
process.exitCode = 1;
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const display = rows.map(r => ({
|
|
366
|
+
semi_product_code: r.semi_product_code,
|
|
367
|
+
semi_product_name: r.semi_product_name,
|
|
368
|
+
semi_batch_num: r.semi_batch_num,
|
|
369
|
+
order_num: r.order_num,
|
|
370
|
+
planned_quantity: r.planned_quantity,
|
|
371
|
+
}));
|
|
372
|
+
printTable(display, `半成品反向追溯 — 原料 ${options.code} / ${options.batch}`);
|
|
373
|
+
} catch (err) {
|
|
374
|
+
console.error(chalk.red('✖ 查询失败:'), err.message);
|
|
375
|
+
process.exitCode = 1;
|
|
376
|
+
} finally { await close(); }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ─── 场景 22:近几天原料使用明细 ──────────────────────────────────────────────
|
|
380
|
+
const MATERIAL_USAGE_SQL = `
|
|
381
|
+
SELECT
|
|
382
|
+
CAST(e.operation_end_time AS DATE) AS use_date,
|
|
383
|
+
bw.order_num AS semi_order_num,
|
|
384
|
+
bw.batch_num AS semi_batch_num,
|
|
385
|
+
bw.product_name AS semi_product_name,
|
|
386
|
+
e.material_code,
|
|
387
|
+
e.material_batch,
|
|
388
|
+
e.actual_quantity,
|
|
389
|
+
e.operator,
|
|
390
|
+
e.operation_end_time
|
|
391
|
+
FROM psm_weighing_execute e
|
|
392
|
+
INNER JOIN psm_work_order ww ON e.work_order_id = ww.id AND ww.tr_type = 1 AND ISNULL(ww.is_deleted, 0) = 0
|
|
393
|
+
LEFT JOIN psm_work_order bw ON bw.order_num = ww.order_num AND bw.tr_type = 2 AND ISNULL(bw.is_deleted, 0) = 0
|
|
394
|
+
WHERE e.material_code = @material_code
|
|
395
|
+
AND (@material_batch IS NULL OR e.material_batch = @material_batch)
|
|
396
|
+
AND e.operation_end_time >= DATEADD(DAY, -@days, CAST(GETDATE() AS DATE))
|
|
397
|
+
ORDER BY e.operation_end_time DESC, bw.order_num;
|
|
398
|
+
`;
|
|
399
|
+
|
|
400
|
+
async function materialUsageCommand(materialCode, options, command) {
|
|
401
|
+
let dbConfig;
|
|
402
|
+
try { dbConfig = resolveConfig(command.parent.opts()); }
|
|
403
|
+
catch (err) { console.error(chalk.red('✖ 连接配置错误:'), err.message); process.exitCode = 1; return; }
|
|
404
|
+
|
|
405
|
+
const days = parseInt(options.days || '180', 10);
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
console.log(chalk.dim(`⏳ 正在查询原料使用明细: ${materialCode} (近 ${days} 天) …`));
|
|
409
|
+
const rows = await queryWithParams(MATERIAL_USAGE_SQL, [
|
|
410
|
+
{ name: 'material_code', type: sql.NVarChar(100), value: materialCode },
|
|
411
|
+
{ name: 'material_batch', type: sql.NVarChar(100), value: options.batch || null },
|
|
412
|
+
{ name: 'days', type: sql.Int, value: days },
|
|
413
|
+
], dbConfig);
|
|
414
|
+
printTable(rows, `原料使用明细 — ${materialCode} (近 ${days} 天)`);
|
|
415
|
+
} catch (err) {
|
|
416
|
+
console.error(chalk.red('✖ 查询失败:'), err.message);
|
|
417
|
+
process.exitCode = 1;
|
|
418
|
+
} finally { await close(); }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ─── 场景 23:原料库存与使用量对比 ────────────────────────────────────────────
|
|
422
|
+
const STOCK_COVERAGE_SQL = `
|
|
423
|
+
;WITH current_stock AS (
|
|
424
|
+
SELECT ISNULL(SUM(quantity), 0) AS current_total_qty, ISNULL(SUM(frozen_quantity), 0) AS current_frozen_qty
|
|
425
|
+
FROM wms_inventory_record
|
|
426
|
+
WHERE material_code = @material_code AND ISNULL(is_delete, 0) = 0 AND (@material_batch IS NULL OR batch = @material_batch)
|
|
427
|
+
),
|
|
428
|
+
recent_usage AS (
|
|
429
|
+
SELECT ISNULL(SUM(actual_quantity), 0) AS recent_used_qty
|
|
430
|
+
FROM psm_weighing_execute
|
|
431
|
+
WHERE material_code = @material_code
|
|
432
|
+
AND (@material_batch IS NULL OR material_batch = @material_batch)
|
|
433
|
+
AND operation_end_time >= DATEADD(DAY, -@days, CAST(GETDATE() AS DATE))
|
|
434
|
+
)
|
|
435
|
+
SELECT
|
|
436
|
+
@material_code AS material_code,
|
|
437
|
+
@material_batch AS material_batch,
|
|
438
|
+
CAST(cs.current_total_qty AS FLOAT) AS current_total_qty,
|
|
439
|
+
CAST(cs.current_frozen_qty AS FLOAT) AS current_frozen_qty,
|
|
440
|
+
CAST(cs.current_total_qty - cs.current_frozen_qty AS FLOAT) AS current_available_qty,
|
|
441
|
+
CAST(ru.recent_used_qty AS FLOAT) AS recent_used_qty,
|
|
442
|
+
CAST(ru.recent_used_qty * 1.0 / NULLIF(@days, 0) AS FLOAT) AS avg_daily_used_qty,
|
|
443
|
+
CAST(ROUND((cs.current_total_qty - cs.current_frozen_qty) * 1.0
|
|
444
|
+
/ NULLIF(ru.recent_used_qty * 1.0 / NULLIF(@days, 0), 0), 0) AS INT) AS coverage_days
|
|
445
|
+
FROM current_stock cs
|
|
446
|
+
CROSS JOIN recent_usage ru;
|
|
447
|
+
`;
|
|
448
|
+
|
|
449
|
+
async function stockCoverageCommand(materialCode, options, command) {
|
|
450
|
+
let dbConfig;
|
|
451
|
+
try { dbConfig = resolveConfig(command.parent.opts()); }
|
|
452
|
+
catch (err) { console.error(chalk.red('✖ 连接配置错误:'), err.message); process.exitCode = 1; return; }
|
|
453
|
+
|
|
454
|
+
const days = parseInt(options.days || '180', 10);
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
console.log(chalk.dim(`⏳ 正在分析原料库存覆盖天数: ${materialCode} (近 ${days} 天) …`));
|
|
458
|
+
const rows = await queryWithParams(STOCK_COVERAGE_SQL, [
|
|
459
|
+
{ name: 'material_code', type: sql.NVarChar(100), value: materialCode },
|
|
460
|
+
{ name: 'material_batch', type: sql.NVarChar(100), value: options.batch || null },
|
|
461
|
+
{ name: 'days', type: sql.Int, value: days },
|
|
462
|
+
], dbConfig);
|
|
463
|
+
if (!rows || rows.length === 0) {
|
|
464
|
+
console.log(chalk.yellow('⚠ 查询无返回结果'));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const r = rows[0];
|
|
468
|
+
const { printDetail } = require('../display');
|
|
469
|
+
printDetail(r, `库存与用量对比 — ${materialCode} (近 ${days} 天)`);
|
|
470
|
+
} catch (err) {
|
|
471
|
+
console.error(chalk.red('✖ 查询失败:'), err.message);
|
|
472
|
+
process.exitCode = 1;
|
|
473
|
+
} finally { await close(); }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
module.exports = {
|
|
477
|
+
stockRecordsCommand,
|
|
478
|
+
stockInventoryCommand,
|
|
479
|
+
traceForwardCommand,
|
|
480
|
+
traceBackwardCommand,
|
|
481
|
+
materialUsageCommand,
|
|
482
|
+
stockCoverageCommand,
|
|
483
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 命令:progress <orderNum> — 查询半成品工单的总体进度
|
|
3
|
+
*
|
|
4
|
+
* 输入:半成品工单号(如 MO6303A035)
|
|
5
|
+
* 输出:工单号、计划生产日期、生产车间、计划生产量、称量状态、生产状态
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const { queryWithParams, close, sql } = require('../db');
|
|
11
|
+
const { resolveConfig } = require('../config');
|
|
12
|
+
const { printDetail } = require('../display');
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
|
|
15
|
+
// ─── SQL ─────────────────────────────────────────────────────────────────────
|
|
16
|
+
const PROGRESS_SQL = `
|
|
17
|
+
DECLARE
|
|
18
|
+
@bulk_work_order_id BIGINT,
|
|
19
|
+
@weight_work_order_id BIGINT,
|
|
20
|
+
@plan_start_time DATETIME2,
|
|
21
|
+
@workshop_name NVARCHAR(200),
|
|
22
|
+
@quantity DECIMAL(38, 10),
|
|
23
|
+
@bulk_count INT,
|
|
24
|
+
@weight_count INT;
|
|
25
|
+
|
|
26
|
+
SELECT
|
|
27
|
+
@bulk_work_order_id = MAX(CASE WHEN tr_type = 2 THEN id END),
|
|
28
|
+
@weight_work_order_id = MAX(CASE WHEN tr_type = 1 THEN id END),
|
|
29
|
+
@plan_start_time = MAX(CASE WHEN tr_type = 2 THEN plan_start_time END),
|
|
30
|
+
@workshop_name = MAX(CASE WHEN tr_type = 2 THEN workshop_name END),
|
|
31
|
+
@quantity = MAX(CASE WHEN tr_type = 2 THEN quantity END),
|
|
32
|
+
@bulk_count = SUM(CASE WHEN tr_type = 2 THEN 1 ELSE 0 END),
|
|
33
|
+
@weight_count = SUM(CASE WHEN tr_type = 1 THEN 1 ELSE 0 END)
|
|
34
|
+
FROM psm_work_order
|
|
35
|
+
WHERE order_num = @order_num
|
|
36
|
+
AND ISNULL(is_deleted, 0) = 0
|
|
37
|
+
AND tr_type IN (1, 2);
|
|
38
|
+
|
|
39
|
+
IF ISNULL(@bulk_count, 0) = 0 AND ISNULL(@weight_count, 0) = 0
|
|
40
|
+
BEGIN
|
|
41
|
+
SELECT
|
|
42
|
+
CAST(0 AS BIT) AS success,
|
|
43
|
+
N'未找到工单号' + @order_num + N',请重新输入其他工单号' AS message,
|
|
44
|
+
CAST(NULL AS NVARCHAR(100)) AS order_num,
|
|
45
|
+
CAST(NULL AS DATETIME2) AS plan_start_time,
|
|
46
|
+
CAST(NULL AS NVARCHAR(200)) AS workshop_name,
|
|
47
|
+
CAST(NULL AS DECIMAL(38,10)) AS quantity,
|
|
48
|
+
CAST(NULL AS NVARCHAR(50)) AS weighing_status,
|
|
49
|
+
CAST(NULL AS NVARCHAR(50)) AS production_status;
|
|
50
|
+
RETURN;
|
|
51
|
+
END;
|
|
52
|
+
|
|
53
|
+
IF ISNULL(@bulk_count, 0) <> 1 OR ISNULL(@weight_count, 0) <> 1
|
|
54
|
+
BEGIN
|
|
55
|
+
SELECT
|
|
56
|
+
CAST(0 AS BIT) AS success,
|
|
57
|
+
N'工单号' + @order_num + N'对应的半成品工单或称量工单数据不完整,请联系管理员' AS message,
|
|
58
|
+
CAST(NULL AS NVARCHAR(100)) AS order_num,
|
|
59
|
+
CAST(NULL AS DATETIME2) AS plan_start_time,
|
|
60
|
+
CAST(NULL AS NVARCHAR(200)) AS workshop_name,
|
|
61
|
+
CAST(NULL AS DECIMAL(38,10)) AS quantity,
|
|
62
|
+
CAST(NULL AS NVARCHAR(50)) AS weighing_status,
|
|
63
|
+
CAST(NULL AS NVARCHAR(50)) AS production_status;
|
|
64
|
+
RETURN;
|
|
65
|
+
END;
|
|
66
|
+
|
|
67
|
+
SELECT
|
|
68
|
+
CAST(1 AS BIT) AS success,
|
|
69
|
+
CAST(NULL AS NVARCHAR(200)) AS message,
|
|
70
|
+
@order_num AS order_num,
|
|
71
|
+
@plan_start_time AS plan_start_time,
|
|
72
|
+
@workshop_name AS workshop_name,
|
|
73
|
+
@quantity AS quantity,
|
|
74
|
+
CASE
|
|
75
|
+
WHEN w.execute_state IS NULL THEN N'未生成称量执行记录'
|
|
76
|
+
WHEN w.execute_state = 1 THEN N'未称量'
|
|
77
|
+
WHEN w.execute_state = 2 THEN N'称量中'
|
|
78
|
+
WHEN w.execute_state = 8 THEN N'称量完成'
|
|
79
|
+
ELSE N'未知状态(' + CAST(w.execute_state AS NVARCHAR) + N')'
|
|
80
|
+
END AS weighing_status,
|
|
81
|
+
CASE
|
|
82
|
+
WHEN b.execute_state IS NULL THEN N'未生成生产执行记录'
|
|
83
|
+
WHEN b.execute_state = 0 THEN N'未生产'
|
|
84
|
+
WHEN b.execute_state = 1 THEN N'生产中'
|
|
85
|
+
WHEN b.execute_state = 2 THEN N'已暂停'
|
|
86
|
+
WHEN b.execute_state = 4 THEN N'投料完成'
|
|
87
|
+
WHEN b.execute_state = 8 THEN N'出锅中'
|
|
88
|
+
WHEN b.execute_state = 16 THEN N'已出锅'
|
|
89
|
+
ELSE N'未知状态(' + CAST(b.execute_state AS NVARCHAR) + N')'
|
|
90
|
+
END AS production_status
|
|
91
|
+
FROM (SELECT 1 AS dummy) s
|
|
92
|
+
OUTER APPLY (
|
|
93
|
+
SELECT TOP (1) execute_state
|
|
94
|
+
FROM psm_weighing_work_order_execute
|
|
95
|
+
WHERE parent_id = @weight_work_order_id
|
|
96
|
+
AND ISNULL(is_deleted, 0) = 0
|
|
97
|
+
ORDER BY update_time DESC, id DESC
|
|
98
|
+
) w
|
|
99
|
+
OUTER APPLY (
|
|
100
|
+
SELECT TOP (1) execute_state
|
|
101
|
+
FROM psm_bulk_execute_work_order
|
|
102
|
+
WHERE work_order_id = @bulk_work_order_id
|
|
103
|
+
AND ISNULL(is_deleted, 0) = 0
|
|
104
|
+
ORDER BY update_time DESC, id DESC
|
|
105
|
+
) b;
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
// ─── 命令入口 ─────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* progress 子命令入口
|
|
112
|
+
* @param {string} orderNum 工单号(位置参数)
|
|
113
|
+
* @param {Object} options 子命令自身选项(当前无额外选项)
|
|
114
|
+
* @param {import('commander').Command} command Commander 子命令实例
|
|
115
|
+
*/
|
|
116
|
+
async function progressCommand(orderNum, options, command) {
|
|
117
|
+
let dbConfig;
|
|
118
|
+
try {
|
|
119
|
+
dbConfig = resolveConfig(command.parent.opts());
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error(chalk.red('✖ 连接配置错误:'), err.message);
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
console.log(chalk.dim(`⏳ 正在查询工单进度: ${orderNum} …`));
|
|
128
|
+
|
|
129
|
+
const rows = await queryWithParams(
|
|
130
|
+
PROGRESS_SQL,
|
|
131
|
+
[{ name: 'order_num', type: sql.NVarChar(100), value: orderNum }],
|
|
132
|
+
dbConfig
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (!rows || rows.length === 0) {
|
|
136
|
+
console.log(chalk.yellow('⚠ 查询无返回结果,请检查工单号是否正确'));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const row = rows[0];
|
|
141
|
+
|
|
142
|
+
// success = false 时打印错误信息
|
|
143
|
+
if (!row.success) {
|
|
144
|
+
console.error(chalk.red(`✖ ${row.message}`));
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 格式化输出(隐藏 success / message 两个元数据列)
|
|
150
|
+
const display = {
|
|
151
|
+
order_num: row.order_num,
|
|
152
|
+
plan_start_time: row.plan_start_time instanceof Date
|
|
153
|
+
? row.plan_start_time.toISOString().replace('T', ' ').slice(0, 19)
|
|
154
|
+
: row.plan_start_time,
|
|
155
|
+
workshop_name: row.workshop_name,
|
|
156
|
+
quantity: row.quantity !== null && row.quantity !== undefined
|
|
157
|
+
? Number(row.quantity).toString()
|
|
158
|
+
: null,
|
|
159
|
+
weighing_status: row.weighing_status,
|
|
160
|
+
production_status: row.production_status,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
printDetail(display, `工单进度 — ${orderNum}`);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error(chalk.red('✖ 查询失败:'), err.message);
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
} finally {
|
|
168
|
+
await close();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { progressCommand };
|