@qinsilk/scm-agent-tools 0.0.0-stage → 0.2.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 +48 -2
- package/cli/api.js +114 -0
- package/cli/commands/data.js +112 -0
- package/cli/commands/docs.js +121 -0
- package/cli/commands/log.js +152 -0
- package/cli/commands/query.js +285 -0
- package/cli/commands/report.js +213 -0
- package/cli/commands/staff.js +505 -0
- package/cli/config.js +46 -0
- package/cli/datasets.js +541 -0
- package/cli/scm-agent.js +110 -0
- package/cli/skills.js +89 -0
- package/cli/table.js +43 -0
- package/docs/api.md +2067 -0
- package/docs/cli.md +252 -0
- package/package.json +27 -4
- package/reports/excel.js +107 -0
- package/reports/factoryEvaluation.js +128 -0
- package/reports/goodsBom.js +648 -0
- package/reports/goodsFirstOrder.js +409 -0
- package/reports/goodsInventory.js +220 -0
- package/reports/goodsSize.js +440 -0
- package/reports/image.js +216 -0
- package/reports/leakCheck.js +694 -0
- package/reports/materialPurchaseExport.js +393 -0
- package/reports/materialTransit.js +593 -0
- package/reports/processFollowup.js +312 -0
- package/reports/produceBoard.js +353 -0
- package/reports/produceCost.js +366 -0
- package/reports/produceCostDetail.js +622 -0
- package/reports/produceDaily.js +485 -0
- package/reports/storeInGoods.js +276 -0
- package/reports/supplierBalance.js +671 -0
- package/reports/ticketStatistics.js +289 -0
- package/reports/util.js +83 -0
- package/skills/bom/345/225/206/345/223/201/344/277/241/346/201/257/SKILL.md +40 -0
- package/skills/qinsilk-scm/SKILL.md +126 -0
- package/skills//344/276/233/345/272/224/345/225/206/345/257/271/350/264/246/345/257/274/345/207/272/SKILL.md +38 -0
- package/skills//345/225/206/345/223/201/345/260/272/345/257/270/344/277/241/346/201/257/SKILL.md +40 -0
- package/skills//345/225/206/345/223/201/351/246/226/345/215/225/351/235/242/346/226/231/345/267/245/345/272/217/345/215/225/344/273/267/SKILL.md +39 -0
- package/skills//345/267/245/345/216/202/350/257/204/344/274/260/346/212/245/350/241/250/SKILL.md +39 -0
- package/skills//346/210/220/345/223/201/345/205/245/345/272/223/346/212/245/350/241/250/SKILL.md +40 -0
- package/skills//346/234/215/350/243/205/346/210/220/345/223/201/347/233/230/345/255/230/350/241/250/SKILL.md +39 -0
- package/skills//346/274/217/346/226/231/346/243/200/346/265/213/346/227/245/346/212/245/SKILL.md +40 -0
- package/skills//347/211/251/346/226/231/345/234/250/351/200/224/347/212/266/346/200/201/346/212/245/350/241/250/SKILL.md +40 -0
- package/skills//347/211/251/346/226/231/351/207/207/350/264/255/345/257/274/345/207/272/SKILL.md +39 -0
- package/skills//347/224/237/344/272/247/345/215/225/345/267/245/345/272/217/350/267/237/350/277/233/350/241/250/SKILL.md +39 -0
- package/skills//347/224/237/344/272/247/346/210/220/346/234/254/346/212/245/350/241/250/SKILL.md +41 -0
- package/skills//347/224/237/344/272/247/346/210/220/346/234/254/346/212/245/350/241/250/346/230/216/347/273/206/SKILL.md +40 -0
- package/skills//347/224/237/344/272/247/346/227/245/346/212/245/SKILL.md +42 -0
- package/skills//347/224/237/344/272/247/347/212/266/345/206/265/347/273/237/350/256/241/350/241/250/SKILL.md +39 -0
- package/skills//347/224/237/344/272/247/350/212/202/347/202/271/347/234/213/346/235/277/SKILL.md +39 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
// 物料在途状态报表:三个工作表(采购在途 / 已入库未发料 / 物料缺口提醒)。
|
|
2
|
+
// 数据来自用料中心物料明细视图(进行中的用料行 + 生产中/待生产的生产单)。
|
|
3
|
+
// 「物料缺口提醒」对辅料按主面料采购入库数折算:
|
|
4
|
+
// 单品预裁数 = min over 面料( floor( 面料已入库 × 该单品下单数 / 分配总量 ÷ 单件用量(含损耗) ) )
|
|
5
|
+
// 预计总用量(面料采购) = Σ 预裁数 × 单件用量(含损耗、含缩水)
|
|
6
|
+
// 预估差额 = 预计总用量(面料采购) - 基础已采购
|
|
7
|
+
// 逐单调生产单详情取 BOM 与下单数(接口限流,请求间隔至少 2.1 秒),
|
|
8
|
+
// 款式图来自详情或按单号查生产单列表。
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { call } = require('../cli/api');
|
|
12
|
+
const { dateStr, shiftDate, timestamp, fetchAll, createThrottle } = require('./util');
|
|
13
|
+
const { createWorkbook, saveWorkbook, embedImage } = require('./excel');
|
|
14
|
+
const { downloadImages } = require('./image');
|
|
15
|
+
|
|
16
|
+
const SHEETS = {
|
|
17
|
+
purchase: '采购在途',
|
|
18
|
+
stored: '已入库未发料',
|
|
19
|
+
gap: '物料缺口提醒',
|
|
20
|
+
};
|
|
21
|
+
// 用料状态:0 进行中(报表固定口径,与用料中心页面默认一致)
|
|
22
|
+
const USE_STATE_PROCESSING = 0;
|
|
23
|
+
// 物料类型名:面料。判定「主面料」用(类型名缺失时按类型值 1=面料)
|
|
24
|
+
const FABRIC_TYPE_NAME = '面料';
|
|
25
|
+
const FABRIC_MATERIAL_TYPE = 1;
|
|
26
|
+
// 换算结果保留的小数位(与接口数据同精度)
|
|
27
|
+
const ROUND_PLACES = 4;
|
|
28
|
+
// 逐单请求详情/列表的节流间隔(毫秒)
|
|
29
|
+
const DETAIL_THROTTLE_MS = 2100;
|
|
30
|
+
|
|
31
|
+
// 三个工作表的列定义(列名、字段、类型),与原脚本的表头顺序一致
|
|
32
|
+
const COLUMNS_PURCHASE = [
|
|
33
|
+
{ header: '图片', field: 'imgUrl', kind: 'image' },
|
|
34
|
+
{ header: '物料名称', field: 'name', kind: 'text' },
|
|
35
|
+
{ header: '物料货号', field: 'sn', kind: 'text' },
|
|
36
|
+
{ header: '颜色', field: 'colorName', kind: 'text' },
|
|
37
|
+
{ header: '规格', field: 'spec', kind: 'text' },
|
|
38
|
+
{ header: '类型', field: 'smTypeName', kind: 'text' },
|
|
39
|
+
{ header: '生产单号', field: 'ordersSnDisplay', kind: 'text' },
|
|
40
|
+
{ header: '款式图', field: 'goodsImgUrl', kind: 'image' },
|
|
41
|
+
{ header: '物料色号', field: 'supplierColor', kind: 'text' },
|
|
42
|
+
{ header: '采购数', field: 'purchaseNumber', kind: 'number' },
|
|
43
|
+
{ header: '采购单位', field: 'purchaseUnitName', kind: 'text' },
|
|
44
|
+
];
|
|
45
|
+
const COLUMNS_STORED = [
|
|
46
|
+
{ header: '生产单号', field: 'ordersSnDisplay', kind: 'text' },
|
|
47
|
+
{ header: '款式图', field: 'goodsImgUrl', kind: 'image' },
|
|
48
|
+
{ header: '预计总用量(下单数)', field: 'orderRequirementNumber', kind: 'number' },
|
|
49
|
+
{ header: '发料数', field: 'issueInNumber', kind: 'number' },
|
|
50
|
+
{ header: '物料货号', field: 'sn', kind: 'text' },
|
|
51
|
+
{ header: '规格', field: 'spec', kind: 'text' },
|
|
52
|
+
{ header: '类型', field: 'smTypeName', kind: 'text' },
|
|
53
|
+
{ header: '物料色号', field: 'supplierColor', kind: 'text' },
|
|
54
|
+
{ header: '采购数', field: 'purchaseNumber', kind: 'number' },
|
|
55
|
+
{ header: '采购已入库', field: 'purchaseInNumber', kind: 'number' },
|
|
56
|
+
{ header: '采购单位', field: 'purchaseUnitName', kind: 'text' },
|
|
57
|
+
{ header: '基础已入库', field: 'baseInNumber', kind: 'number' },
|
|
58
|
+
{ header: '基础单位', field: 'baseUnitName', kind: 'text' },
|
|
59
|
+
];
|
|
60
|
+
const COLUMNS_GAP = [
|
|
61
|
+
{ header: '生产单号', field: 'ordersSnDisplay', kind: 'text' },
|
|
62
|
+
{ header: '款式图', field: 'goodsImgUrl', kind: 'image' },
|
|
63
|
+
{ header: '物料货号', field: 'sn', kind: 'text' },
|
|
64
|
+
{ header: '颜色', field: 'colorName', kind: 'text' },
|
|
65
|
+
{ header: '物料色号', field: 'supplierColor', kind: 'text' },
|
|
66
|
+
{ header: '预计总用量(下单数)', field: 'orderRequirementNumber', kind: 'number' },
|
|
67
|
+
{ header: '预计总用量(面料采购)', field: 'requirementByFabricPurchase', kind: 'number' },
|
|
68
|
+
{ header: '预估差额', field: 'shortfallByFabricPurchase', kind: 'number' },
|
|
69
|
+
{ header: '采购数', field: 'purchaseNumber', kind: 'number' },
|
|
70
|
+
{ header: '采购已入库', field: 'purchaseInNumber', kind: 'number' },
|
|
71
|
+
{ header: '采购单位', field: 'purchaseUnitName', kind: 'text' },
|
|
72
|
+
{ header: '基础已采购', field: 'basePurchaseNumber', kind: 'number' },
|
|
73
|
+
{ header: '基础已入库', field: 'baseInNumber', kind: 'number' },
|
|
74
|
+
{ header: '基础单位', field: 'baseUnitName', kind: 'text' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const BORDER = {
|
|
78
|
+
top: { style: 'thin', color: { argb: 'FFCCCCCC' } },
|
|
79
|
+
left: { style: 'thin', color: { argb: 'FFCCCCCC' } },
|
|
80
|
+
bottom: { style: 'thin', color: { argb: 'FFCCCCCC' } },
|
|
81
|
+
right: { style: 'thin', color: { argb: 'FFCCCCCC' } },
|
|
82
|
+
};
|
|
83
|
+
// 表头底色:浅蓝(与原脚本一致)
|
|
84
|
+
const HEADER_FILL = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE8F1FB' } };
|
|
85
|
+
// 嵌图行的统一行高(约 92 像素),有图无图都用同一高度,图片底部对齐
|
|
86
|
+
const IMAGE_ROW_HEIGHT = 69;
|
|
87
|
+
// 图片显示尺寸(像素):宽 60 高 80,接近原脚本 64x88 的图片框
|
|
88
|
+
const IMAGE_SIZE = { width: 60, height: 80 };
|
|
89
|
+
|
|
90
|
+
/** 文本归一化:空值返回空字符串 */
|
|
91
|
+
function text(value) {
|
|
92
|
+
return value == null ? '' : String(value).trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 参与运算的数值:缺失或非法按 0 处理 */
|
|
96
|
+
function amountOf(value) {
|
|
97
|
+
const parsed = Number(value);
|
|
98
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 四舍五入(远离零)保留 places 位小数 */
|
|
102
|
+
function roundHalfUp(value, places) {
|
|
103
|
+
const factor = 10 ** places;
|
|
104
|
+
const magnitude = Math.abs(value) * factor;
|
|
105
|
+
const rounded = Math.round(magnitude + Number.EPSILON * magnitude);
|
|
106
|
+
const result = (value < 0 ? -rounded : rounded) / factor;
|
|
107
|
+
return Object.is(result, -0) ? 0 : result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 换算结果保留 4 位小数 */
|
|
111
|
+
function round4(value) {
|
|
112
|
+
return roundHalfUp(value, ROUND_PLACES);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 除法:与后端 BigDecimalUtil.div 一致,HALF_UP 保留 2 位;分母为 0 或缺失返回 0 */
|
|
116
|
+
function divide(numerator, denominator) {
|
|
117
|
+
const divisor = Number(denominator);
|
|
118
|
+
if (!Number.isFinite(divisor) || divisor === 0) {
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
return roundHalfUp(amountOf(numerator) / divisor, 2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 数量列取值:数值写数字,无法转换按原文返回 */
|
|
125
|
+
function numeric(value) {
|
|
126
|
+
if (value === null || value === undefined || value === '') {
|
|
127
|
+
return '';
|
|
128
|
+
}
|
|
129
|
+
const parsed = Number(value);
|
|
130
|
+
if (!Number.isFinite(parsed)) {
|
|
131
|
+
return text(value);
|
|
132
|
+
}
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 采购已入库 = 采购数 - 采购待入库数(采购单位口径) */
|
|
137
|
+
function purchaseInNumber(row) {
|
|
138
|
+
return round4(amountOf(row.purchaseNumber) - amountOf(row.purchaseWinNumber));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 基础已入库 = 采购已入库 × 转换系数 */
|
|
142
|
+
function baseInNumberOf(row) {
|
|
143
|
+
return round4(purchaseInNumber(row) * amountOf(row.convertRate || 1));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 基础已采购 = 采购数 × 转换系数 */
|
|
147
|
+
function basePurchaseNumberOf(row) {
|
|
148
|
+
return round4(amountOf(row.purchaseNumber) * amountOf(row.convertRate || 1));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 生产单号:ordersSn 优先,缺失时用 produceOrdersSn */
|
|
152
|
+
function ordersSnOf(row) {
|
|
153
|
+
return text(row.ordersSn) || text(row.produceOrdersSn);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 用料中心行是否面料:类型名优先(smTypeName == 面料),
|
|
158
|
+
* 类型名缺失时按物料类型值 1 判断。
|
|
159
|
+
*/
|
|
160
|
+
function rowIsFabric(row) {
|
|
161
|
+
const name = text(row.smTypeName);
|
|
162
|
+
if (name) {
|
|
163
|
+
return name === FABRIC_TYPE_NAME;
|
|
164
|
+
}
|
|
165
|
+
if (row.materialType != null && row.materialType !== '') {
|
|
166
|
+
return amountOf(row.materialType) === FABRIC_MATERIAL_TYPE;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* BOM 行是否面料。BOM 行本身没有类型字段,用生产单详情 materialList 里
|
|
173
|
+
* 同物料单品的 materialTypeName / materialType 判断。
|
|
174
|
+
*/
|
|
175
|
+
function bomIsFabric(bomRow, materialTypeIndex) {
|
|
176
|
+
const info = materialTypeIndex.get(String(bomRow.materialSkuId));
|
|
177
|
+
if (info) {
|
|
178
|
+
if (info.name) {
|
|
179
|
+
return info.name === FABRIC_TYPE_NAME;
|
|
180
|
+
}
|
|
181
|
+
if (info.type != null) {
|
|
182
|
+
return amountOf(info.type) === FABRIC_MATERIAL_TYPE;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 单件用量(含损耗率,不含缩水率):number × (1 + 损耗率/100) */
|
|
189
|
+
function perUseWithLoss(bomRow) {
|
|
190
|
+
return amountOf(bomRow.number) * (1 + amountOf(bomRow.materialLossRate) / 100);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 单件用量(含损耗率与缩水率):× (1+损耗率/100) ÷ (1-缩水率/100);分母为 0 返回 0 */
|
|
194
|
+
function perUseWithLossAndShrink(bomRow) {
|
|
195
|
+
const divisor = 1 - amountOf(bomRow.shrinkRate) / 100;
|
|
196
|
+
if (divisor === 0) {
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
return perUseWithLoss(bomRow) / divisor;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 把一张生产单详情整理成缺口计算用的索引:
|
|
204
|
+
* quantities:商品单品 id → 下单数(整数,小数截断);
|
|
205
|
+
* materials:(商品单品 id, 物料单品 id) → BOM 行,同键保留第一条。
|
|
206
|
+
*/
|
|
207
|
+
function bomIndexOf(detail) {
|
|
208
|
+
const quantities = new Map();
|
|
209
|
+
for (const goods of (detail && detail.orderGoods) || []) {
|
|
210
|
+
if (goods == null || goods.goodsSkuId == null) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
quantities.set(String(goods.goodsSkuId), Math.trunc(amountOf(goods.number)));
|
|
214
|
+
}
|
|
215
|
+
const materials = new Map();
|
|
216
|
+
for (const bom of (detail && detail.bomList) || []) {
|
|
217
|
+
if (bom == null || bom.goodsSkuId == null || bom.materialSkuId == null) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const key = `${bom.goodsSkuId}|${bom.materialSkuId}`;
|
|
221
|
+
if (!materials.has(key)) {
|
|
222
|
+
materials.set(key, bom);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { quantities, materials };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** (生产单号, 物料单品id) → 面料采购已入库数(采购单位口径) */
|
|
229
|
+
function fabricInNumbers(rows) {
|
|
230
|
+
const result = new Map();
|
|
231
|
+
for (const row of rows) {
|
|
232
|
+
if (!rowIsFabric(row)) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const sn = ordersSnOf(row);
|
|
236
|
+
const skuId = text(row.materialSkuId);
|
|
237
|
+
if (sn && skuId) {
|
|
238
|
+
result.set(`${sn}|${skuId}`, purchaseInNumber(row));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 每个商品单品的「预裁数」:该单品用到的每个面料单品各算一个可裁数后取最小。
|
|
246
|
+
* 可裁数 = floor( 面料已入库 × 该单品下单数 ÷ 分配总量 ÷ 单件用量(含损耗) ),
|
|
247
|
+
* 中间除法按 2 位小数四舍五入(与后端 getPreCutNumber 一致)。
|
|
248
|
+
* 面料已入库为 0 时按 0 参与;面料行不在本批用料数据里时跳过该面料。
|
|
249
|
+
*/
|
|
250
|
+
function preCutNumbers(sn, orderBom, fabricIn, materialTypeIndex) {
|
|
251
|
+
const { quantities, materials } = orderBom;
|
|
252
|
+
// 每个面料单品分到的商品单品集合与下单数合计
|
|
253
|
+
const fabricSkus = new Map();
|
|
254
|
+
for (const [key, bomRow] of materials) {
|
|
255
|
+
if (!bomIsFabric(bomRow, materialTypeIndex) || perUseWithLoss(bomRow) <= 0) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const [, materialSkuId] = key.split('|');
|
|
259
|
+
if (!fabricSkus.has(materialSkuId)) {
|
|
260
|
+
fabricSkus.set(materialSkuId, new Set());
|
|
261
|
+
}
|
|
262
|
+
fabricSkus.get(materialSkuId).add(key);
|
|
263
|
+
}
|
|
264
|
+
const fabricTotal = new Map();
|
|
265
|
+
for (const [materialSkuId, keys] of fabricSkus) {
|
|
266
|
+
let total = 0;
|
|
267
|
+
for (const key of keys) {
|
|
268
|
+
const [goodsSkuId] = key.split('|');
|
|
269
|
+
total += quantities.get(goodsSkuId) || 0;
|
|
270
|
+
}
|
|
271
|
+
fabricTotal.set(materialSkuId, total);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const result = new Map();
|
|
275
|
+
for (const [key, bomRow] of materials) {
|
|
276
|
+
if (!bomIsFabric(bomRow, materialTypeIndex)) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const [goodsSkuId, materialSkuId] = key.split('|');
|
|
280
|
+
const perUse = perUseWithLoss(bomRow);
|
|
281
|
+
const total = fabricTotal.get(materialSkuId) || 0;
|
|
282
|
+
if (perUse <= 0 || total === 0) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const inNumber = fabricIn.get(`${sn}|${materialSkuId}`);
|
|
286
|
+
if (inNumber === undefined) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const allocated = divide(inNumber * (quantities.get(goodsSkuId) || 0), total);
|
|
290
|
+
const cut = Math.floor(divide(allocated, perUse));
|
|
291
|
+
const current = result.get(goodsSkuId);
|
|
292
|
+
result.set(goodsSkuId, current === undefined ? cut : Math.min(current, cut));
|
|
293
|
+
}
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 预计总用量(面料采购):该物料在各商品单品上的 Σ(预裁数 × 单件用量含损耗缩水)。
|
|
299
|
+
*/
|
|
300
|
+
function requirementByFabricPurchase(row, preCut, orderBom) {
|
|
301
|
+
const materialSkuId = text(row.materialSkuId);
|
|
302
|
+
let total = 0;
|
|
303
|
+
for (const [key, bomRow] of orderBom.materials) {
|
|
304
|
+
const [, bomMaterialSkuId] = key.split('|');
|
|
305
|
+
if (bomMaterialSkuId !== materialSkuId) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const [goodsSkuId] = key.split('|');
|
|
309
|
+
const cut = preCut.get(goodsSkuId) || 0;
|
|
310
|
+
if (cut <= 0) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
total += cut * perUseWithLossAndShrink(bomRow);
|
|
314
|
+
}
|
|
315
|
+
return round4(total);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** 单元格值:数量列写数字,图片列留空(图片单独嵌入) */
|
|
319
|
+
function cellValue(row, column) {
|
|
320
|
+
const value = row[column.field];
|
|
321
|
+
if (column.kind === 'image') {
|
|
322
|
+
return '';
|
|
323
|
+
}
|
|
324
|
+
if (column.kind === 'number') {
|
|
325
|
+
return numeric(value);
|
|
326
|
+
}
|
|
327
|
+
return text(value);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** 列宽:图片 10、数量 15、文本按标题长度(与原脚本一致) */
|
|
331
|
+
function columnWidth(column) {
|
|
332
|
+
if (column.kind === 'image') {
|
|
333
|
+
return 10;
|
|
334
|
+
}
|
|
335
|
+
if (column.kind === 'number') {
|
|
336
|
+
return 15;
|
|
337
|
+
}
|
|
338
|
+
return Math.max(12, Math.min(24, column.header.length * 3 + 4));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** 写一个工作表:单层表头 + 数据行 + 嵌图 + 自动筛选 + 冻结首行 */
|
|
342
|
+
function buildSheet(workbook, sheetName, columns, rows, images) {
|
|
343
|
+
const sheet = workbook.addWorksheet(sheetName, {
|
|
344
|
+
views: [{ state: 'frozen', ySplit: 1 }],
|
|
345
|
+
pageSetup: { orientation: 'landscape', fitToPage: true, fitToWidth: 1, fitToHeight: 0 },
|
|
346
|
+
});
|
|
347
|
+
const headerRow = sheet.addRow(columns.map((column) => column.header));
|
|
348
|
+
headerRow.height = 24;
|
|
349
|
+
headerRow.eachCell((cell) => {
|
|
350
|
+
cell.font = { bold: true };
|
|
351
|
+
cell.fill = HEADER_FILL;
|
|
352
|
+
cell.border = BORDER;
|
|
353
|
+
cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
|
354
|
+
});
|
|
355
|
+
const imageCols = columns
|
|
356
|
+
.map((column, index) => (column.kind === 'image' ? index + 1 : 0))
|
|
357
|
+
.filter(Boolean);
|
|
358
|
+
rows.forEach((row, index) => {
|
|
359
|
+
const rowNumber = index + 2;
|
|
360
|
+
const added = sheet.addRow(columns.map((column) => cellValue(row, column)));
|
|
361
|
+
added.height = IMAGE_ROW_HEIGHT;
|
|
362
|
+
added.eachCell((cell, colNumber) => {
|
|
363
|
+
const kind = columns[colNumber - 1].kind;
|
|
364
|
+
cell.border = BORDER;
|
|
365
|
+
cell.alignment = {
|
|
366
|
+
vertical: 'middle',
|
|
367
|
+
horizontal: kind === 'text' ? 'left' : kind === 'number' ? 'right' : 'center',
|
|
368
|
+
wrapText: kind === 'text',
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
for (const col of imageCols) {
|
|
372
|
+
const url = text(row[columns[col - 1].field]);
|
|
373
|
+
if (url && images.has(url)) {
|
|
374
|
+
embedImage(workbook, sheet, images.get(url), rowNumber, col, IMAGE_SIZE);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
columns.forEach((column, index) => {
|
|
379
|
+
sheet.getColumn(index + 1).width = columnWidth(column);
|
|
380
|
+
});
|
|
381
|
+
if (rows.length) {
|
|
382
|
+
const lastColLetter = sheet.getColumn(columns.length).letter;
|
|
383
|
+
sheet.autoFilter = `A1:${lastColLetter}${rows.length + 1}`;
|
|
384
|
+
}
|
|
385
|
+
return sheet;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* 生成物料在途状态报表 Excel。
|
|
390
|
+
* argv.days:业务日期回看天数,默认 90(结束日今天,起始日往前 days-1 天);
|
|
391
|
+
* argv.from/to:直接指定日期区间,覆盖 days;argv.out:输出目录。
|
|
392
|
+
*/
|
|
393
|
+
async function generate(argv = {}) {
|
|
394
|
+
const days = Math.max(Number(argv.days) > 0 ? Number(argv.days) : 90, 1);
|
|
395
|
+
const to = argv.to || dateStr();
|
|
396
|
+
const from = argv.from || shiftDate(-(days - 1), new Date(`${to}T00:00:00`));
|
|
397
|
+
if (from > to) {
|
|
398
|
+
throw new Error('起始日期不能晚于结束日期');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const outDir = argv.out || process.cwd();
|
|
402
|
+
const filePath = path.resolve(outDir, `物料在途状态报表${timestamp()}.xlsx`);
|
|
403
|
+
if (fs.existsSync(filePath)) {
|
|
404
|
+
throw new Error(`输出文件已存在:${filePath};如需替换请先删除旧文件或更换输出目录`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 企业配置:produceBatchSnEnable 决定生产单号列显示批次号还是生产单号;失败按未开启处理
|
|
408
|
+
let useBatchSn = false;
|
|
409
|
+
try {
|
|
410
|
+
const config = await call('GET', '/api/agent/v1/company-config');
|
|
411
|
+
useBatchSn = config && config.produceBatchSnEnable === true;
|
|
412
|
+
} catch (e) {
|
|
413
|
+
// 配置接口失败不中断导出
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 用料中心物料明细:进行中的用料行 + 生产中/待生产的生产单(spoState 固定 2,4)
|
|
417
|
+
const { list: rows } = await fetchAll(
|
|
418
|
+
(page, limit) => call('GET', '/api/agent/v1/material-requirement/list', {
|
|
419
|
+
from,
|
|
420
|
+
to,
|
|
421
|
+
spoState: '2,4',
|
|
422
|
+
useState: USE_STATE_PROCESSING,
|
|
423
|
+
page,
|
|
424
|
+
limit,
|
|
425
|
+
})
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const isAccessory = (row) => !rowIsFabric(row);
|
|
429
|
+
// 三个 sheet 的行筛选(与原脚本一致)
|
|
430
|
+
const purchaseRows = rows.filter((row) => amountOf(row.purchaseNumber) !== 0);
|
|
431
|
+
const storedRows = rows.filter(
|
|
432
|
+
(row) => row.useState === USE_STATE_PROCESSING
|
|
433
|
+
&& purchaseInNumber(row) > 0
|
|
434
|
+
&& amountOf(row.issueInNumber) === 0
|
|
435
|
+
);
|
|
436
|
+
if (!purchaseRows.length && !storedRows.length) {
|
|
437
|
+
throw new Error('没有符合条件的物料在途数据');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// 物料缺口提醒:先对「进行中且有辅料行」的生产单逐单取详情(含 BOM 与下单数)
|
|
441
|
+
const throttle = createThrottle(DETAIL_THROTTLE_MS);
|
|
442
|
+
const candidateSns = [...new Set(
|
|
443
|
+
rows.filter((row) => row.useState === USE_STATE_PROCESSING && isAccessory(row))
|
|
444
|
+
.map((row) => ordersSnOf(row))
|
|
445
|
+
.filter(Boolean)
|
|
446
|
+
)].sort();
|
|
447
|
+
const details = new Map();
|
|
448
|
+
const bomFailed = [];
|
|
449
|
+
for (const sn of candidateSns) {
|
|
450
|
+
try {
|
|
451
|
+
const detail = await throttle(() => call('GET', '/api/agent/v1/produce/detail', { sn }));
|
|
452
|
+
if (detail) {
|
|
453
|
+
details.set(sn, detail);
|
|
454
|
+
}
|
|
455
|
+
} catch (e) {
|
|
456
|
+
bomFailed.push(sn);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 缺口计算:先整理每张单的 BOM 索引与物料类型索引
|
|
461
|
+
const bomIndexes = new Map();
|
|
462
|
+
const materialTypeIndexes = new Map();
|
|
463
|
+
for (const [sn, detail] of details) {
|
|
464
|
+
bomIndexes.set(sn, bomIndexOf(detail));
|
|
465
|
+
const typeIndex = new Map();
|
|
466
|
+
for (const material of detail.materialList || []) {
|
|
467
|
+
if (material && material.materialSkuId != null) {
|
|
468
|
+
typeIndex.set(String(material.materialSkuId), { name: text(material.materialTypeName), type: material.materialType });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
materialTypeIndexes.set(sn, typeIndex);
|
|
472
|
+
}
|
|
473
|
+
const fabricIn = fabricInNumbers(rows);
|
|
474
|
+
for (const row of rows) {
|
|
475
|
+
row.basePurchaseNumber = basePurchaseNumberOf(row);
|
|
476
|
+
const sn = ordersSnOf(row);
|
|
477
|
+
const orderBom = bomIndexes.get(sn);
|
|
478
|
+
if (!orderBom || !isAccessory(row)) {
|
|
479
|
+
// 没有 BOM(详情读取失败)或面料行:两列留空而不是填 0,避免被当成「无缺口」
|
|
480
|
+
row.requirementByFabricPurchase = '';
|
|
481
|
+
row.shortfallByFabricPurchase = '';
|
|
482
|
+
} else {
|
|
483
|
+
const preCut = preCutNumbers(sn, orderBom, fabricIn, materialTypeIndexes.get(sn));
|
|
484
|
+
row.requirementByFabricPurchase = requirementByFabricPurchase(row, preCut, orderBom);
|
|
485
|
+
row.shortfallByFabricPurchase = round4(
|
|
486
|
+
amountOf(row.requirementByFabricPurchase) - amountOf(basePurchaseNumberOf(row))
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const gapRows = rows.filter((row) => {
|
|
491
|
+
if (row.useState !== USE_STATE_PROCESSING || !isAccessory(row)) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
const required = Number(row.requirementByFabricPurchase);
|
|
495
|
+
const shortfall = Number(row.shortfallByFabricPurchase);
|
|
496
|
+
return Number.isFinite(required) && required > 0 && Number.isFinite(shortfall) && shortfall >= 0;
|
|
497
|
+
});
|
|
498
|
+
const gapSkipped = rows.filter(
|
|
499
|
+
(row) => row.useState === USE_STATE_PROCESSING
|
|
500
|
+
&& isAccessory(row)
|
|
501
|
+
&& !(Number(row.requirementByFabricPurchase) > 0)
|
|
502
|
+
).length;
|
|
503
|
+
|
|
504
|
+
// 款式图:详情已有的单直接用;其余单按单号查生产单列表补图
|
|
505
|
+
const goodsImgBySn = new Map();
|
|
506
|
+
for (const [sn, detail] of details) {
|
|
507
|
+
goodsImgBySn.set(sn, text(detail.goodsImgUrl));
|
|
508
|
+
}
|
|
509
|
+
const wantedSns = [...new Set(
|
|
510
|
+
[...purchaseRows, ...storedRows, ...gapRows].map((row) => ordersSnOf(row)).filter(Boolean)
|
|
511
|
+
)];
|
|
512
|
+
for (const sn of wantedSns) {
|
|
513
|
+
if (goodsImgBySn.has(sn)) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
// 只按单号关键词查询,不带业务日期:款式图所属生产单的业务日期可能在本报表区间之外
|
|
518
|
+
const data = await throttle(() => call('GET', '/api/agent/v1/produce/list', { searchKey: sn }));
|
|
519
|
+
const matched = (data && data.list ? data.list : []).find((item) => text(item.ordersSn) === sn);
|
|
520
|
+
goodsImgBySn.set(sn, matched ? text(matched.goodsImgUrl) : '');
|
|
521
|
+
} catch (e) {
|
|
522
|
+
goodsImgBySn.set(sn, '');
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
for (const row of rows) {
|
|
526
|
+
row.goodsImgUrl = goodsImgBySn.get(ordersSnOf(row)) || '';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 行排序与展示字段整理(规格、单号显示、换算列)
|
|
530
|
+
const sortRows = (list) => list.sort((a, b) => {
|
|
531
|
+
const left = [ordersSnOf(a), text(a.sn), text(a.supplierColor)];
|
|
532
|
+
const right = [ordersSnOf(b), text(b.sn), text(b.supplierColor)];
|
|
533
|
+
for (let i = 0; i < 3; i += 1) {
|
|
534
|
+
if (left[i] !== right[i]) {
|
|
535
|
+
return left[i] < right[i] ? -1 : 1;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return 0;
|
|
539
|
+
});
|
|
540
|
+
const transformRow = (row) => {
|
|
541
|
+
const properties = Array.isArray(row.skuProperties) ? row.skuProperties : [];
|
|
542
|
+
const spec = properties.map((item) => text(item.propertyValue)).join('-');
|
|
543
|
+
const batchSn = text(row.produceBatchSn);
|
|
544
|
+
return {
|
|
545
|
+
...row,
|
|
546
|
+
spec: spec || '-',
|
|
547
|
+
ordersSnDisplay: useBatchSn && batchSn ? batchSn : ordersSnOf(row),
|
|
548
|
+
purchaseInNumber: purchaseInNumber(row),
|
|
549
|
+
baseInNumber: baseInNumberOf(row),
|
|
550
|
+
};
|
|
551
|
+
};
|
|
552
|
+
const sheetPurchase = sortRows(purchaseRows).map(transformRow);
|
|
553
|
+
const sheetStored = sortRows(storedRows).map(transformRow);
|
|
554
|
+
const sheetGap = sortRows(gapRows).map(transformRow);
|
|
555
|
+
|
|
556
|
+
// 批量下载全部图片(物料图 + 款式图)
|
|
557
|
+
const urls = new Set();
|
|
558
|
+
for (const row of [...sheetPurchase, ...sheetStored, ...sheetGap]) {
|
|
559
|
+
for (const column of [...COLUMNS_PURCHASE, ...COLUMNS_STORED, ...COLUMNS_GAP]) {
|
|
560
|
+
if (column.kind === 'image') {
|
|
561
|
+
const url = text(row[column.field]);
|
|
562
|
+
if (url) {
|
|
563
|
+
urls.add(url);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const images = await downloadImages([...urls]);
|
|
569
|
+
|
|
570
|
+
const workbook = createWorkbook();
|
|
571
|
+
buildSheet(workbook, SHEETS.purchase, COLUMNS_PURCHASE, sheetPurchase, images);
|
|
572
|
+
buildSheet(workbook, SHEETS.stored, COLUMNS_STORED, sheetStored, images);
|
|
573
|
+
buildSheet(workbook, SHEETS.gap, COLUMNS_GAP, sheetGap, images);
|
|
574
|
+
|
|
575
|
+
await saveWorkbook(workbook, filePath);
|
|
576
|
+
|
|
577
|
+
const notes = [];
|
|
578
|
+
if (gapSkipped) {
|
|
579
|
+
notes.push(`${gapSkipped} 行辅料的「预计总用量(面料采购)」为 0 或无法计算(整单面料尚未入库、面料行不在本批数据或 BOM 读取失败),未纳入缺口提醒`);
|
|
580
|
+
}
|
|
581
|
+
if (bomFailed.length) {
|
|
582
|
+
notes.push(`${bomFailed.length} 张生产单的详情读取失败(${bomFailed.slice(0, 5).join('、')}${bomFailed.length > 5 ? ' 等' : ''})`);
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
filePaths: [filePath],
|
|
586
|
+
summary: `物料在途状态报表已生成:业务日期 ${from} ~ ${to},用料中心明细 ${rows.length} 行;` +
|
|
587
|
+
`「采购在途」${sheetPurchase.length} 行、「已入库未发料」${sheetStored.length} 行、「物料缺口提醒」${sheetGap.length} 行,` +
|
|
588
|
+
`嵌图 ${images.size} 张,单号显示${useBatchSn ? '生产批次号优先' : '生产单号'}` +
|
|
589
|
+
(notes.length ? `;${notes.join(';')}` : ''),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
module.exports = { generate };
|