@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.
Files changed (52) hide show
  1. package/README.md +48 -2
  2. package/cli/api.js +114 -0
  3. package/cli/commands/data.js +112 -0
  4. package/cli/commands/docs.js +121 -0
  5. package/cli/commands/log.js +152 -0
  6. package/cli/commands/query.js +285 -0
  7. package/cli/commands/report.js +213 -0
  8. package/cli/commands/staff.js +505 -0
  9. package/cli/config.js +46 -0
  10. package/cli/datasets.js +541 -0
  11. package/cli/scm-agent.js +110 -0
  12. package/cli/skills.js +89 -0
  13. package/cli/table.js +43 -0
  14. package/docs/api.md +2067 -0
  15. package/docs/cli.md +252 -0
  16. package/package.json +27 -4
  17. package/reports/excel.js +107 -0
  18. package/reports/factoryEvaluation.js +128 -0
  19. package/reports/goodsBom.js +648 -0
  20. package/reports/goodsFirstOrder.js +409 -0
  21. package/reports/goodsInventory.js +220 -0
  22. package/reports/goodsSize.js +440 -0
  23. package/reports/image.js +216 -0
  24. package/reports/leakCheck.js +694 -0
  25. package/reports/materialPurchaseExport.js +393 -0
  26. package/reports/materialTransit.js +593 -0
  27. package/reports/processFollowup.js +312 -0
  28. package/reports/produceBoard.js +353 -0
  29. package/reports/produceCost.js +366 -0
  30. package/reports/produceCostDetail.js +622 -0
  31. package/reports/produceDaily.js +485 -0
  32. package/reports/storeInGoods.js +276 -0
  33. package/reports/supplierBalance.js +671 -0
  34. package/reports/ticketStatistics.js +289 -0
  35. package/reports/util.js +83 -0
  36. package/skills/bom/345/225/206/345/223/201/344/277/241/346/201/257/SKILL.md +40 -0
  37. package/skills/qinsilk-scm/SKILL.md +126 -0
  38. 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
  39. package/skills//345/225/206/345/223/201/345/260/272/345/257/270/344/277/241/346/201/257/SKILL.md +40 -0
  40. 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
  41. package/skills//345/267/245/345/216/202/350/257/204/344/274/260/346/212/245/350/241/250/SKILL.md +39 -0
  42. package/skills//346/210/220/345/223/201/345/205/245/345/272/223/346/212/245/350/241/250/SKILL.md +40 -0
  43. 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
  44. package/skills//346/274/217/346/226/231/346/243/200/346/265/213/346/227/245/346/212/245/SKILL.md +40 -0
  45. 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
  46. package/skills//347/211/251/346/226/231/351/207/207/350/264/255/345/257/274/345/207/272/SKILL.md +39 -0
  47. 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
  48. package/skills//347/224/237/344/272/247/346/210/220/346/234/254/346/212/245/350/241/250/SKILL.md +41 -0
  49. 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
  50. package/skills//347/224/237/344/272/247/346/227/245/346/212/245/SKILL.md +42 -0
  51. 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
  52. 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,366 @@
1
+ // 生产成本报表:一个工作表全部行,两级分组表头(款式信息/生产单信息/实际成本/预估成本)。
2
+ // 实际成本组内在每类成本后面插入差异列:差异 =(实际 - 预估)/ 预估,
3
+ // 用「正数红色带 +、负数绿色带 -」的数字格式显示;预估缺失或为 0 时无法计算,显示「-」。
4
+ // 第三列嵌款式图,前两行冻结并加自动筛选,金额小数位读企业配置 materialDecimal。
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { call } = require('../cli/api');
8
+ const { timestamp, fetchAll } = require('./util');
9
+ const { createWorkbook, saveWorkbook, embedImage } = require('./excel');
10
+ const { downloadImages } = require('./image');
11
+
12
+ const SHEET_NAME = '生产成本报表';
13
+ // 生产类型文案(与 is-scm-front-pc 的 prodTypeList 一致)
14
+ const TYPE_NAMES = { 1: '自主生产', 2: '单工序外发', 3: '委外加工(全工序外发)', 4: '成品采购' };
15
+ // 计算规则文案与别名:CLI 或技能可能传中文名或数字
16
+ const CAL_WAY_NAMES = { 1: '按默认逻辑计算', 2: '按成衣入库数计算', 3: '按裁床数计算' };
17
+ const CAL_WAY_ALIASES = {
18
+ 1: 1, 2: 2, 3: 3,
19
+ '默认': 1, '默认逻辑': 1, '按默认逻辑计算': 1,
20
+ '成衣入库数': 2, '按成衣入库数计算': 2,
21
+ '裁床数': 3, '按裁床数计算': 3,
22
+ };
23
+ // 金额小数位上下限:超出范围按默认 2 位处理(与原脚本一致)
24
+ const DEFAULT_DECIMAL = 2;
25
+ const MAX_DECIMAL = 6;
26
+ // 差异百分比保留的小数位(与原脚本一致)
27
+ const PERCENT_DECIMAL = 2;
28
+
29
+ // 两级表头分组。列顺序与「生产成本报表」页面导出对齐,并在实际成本组内插入差异列。
30
+ // percent 列的 actual/estimated 指向该差异的两个来源字段。
31
+ const GROUPS = [
32
+ {
33
+ name: '款式信息',
34
+ columns: [
35
+ { header: '商品', field: 'goodsName', kind: 'text' },
36
+ { header: '款号', field: 'designSn', kind: 'text' },
37
+ { header: '图片', field: 'imgUrl', kind: 'image' },
38
+ { header: '大货号', field: 'goodsSn', kind: 'text' },
39
+ { header: '波段', field: 'rangesName', kind: 'text' },
40
+ { header: '设计师', field: 'designerName', kind: 'text' },
41
+ { header: '打版师', field: 'patternMakerName', kind: 'text' },
42
+ ],
43
+ },
44
+ {
45
+ name: '生产单信息',
46
+ columns: [
47
+ { header: '单号', field: 'ordersSnDisplay', kind: 'text' },
48
+ { header: '订单类型', field: 'orderTypeName', kind: 'text' },
49
+ { header: '标签', field: 'tags', kind: 'tags' },
50
+ { header: '生产类型', field: 'type', kind: 'type' },
51
+ { header: '预计交货日期', field: 'deliveryTime', kind: 'date' },
52
+ { header: '跟单员', field: 'followerName', kind: 'text' },
53
+ { header: '下单数', field: 'sumNumber', kind: 'number' },
54
+ { header: '裁床数', field: 'sumCutBedNumber', kind: 'number' },
55
+ { header: '已入库数', field: 'sumStoreInNumber', kind: 'number' },
56
+ { header: '未完成', field: 'sumNoFinishNumber', kind: 'number' },
57
+ { header: '未出数', field: 'sumNoOutNumber', kind: 'number' },
58
+ { header: '未入库数', field: 'sumNoStoreInNumber', kind: 'number' },
59
+ { header: '业务日期', field: 'businessTime', kind: 'date' },
60
+ ],
61
+ },
62
+ {
63
+ name: '实际成本',
64
+ columns: [
65
+ { header: '物料', field: 'actualMaterialCost', kind: 'money' },
66
+ { header: '物料差异', kind: 'percent', actual: 'actualMaterialCost', estimated: 'estimatedMaterialCost' },
67
+ { header: '委外加工', field: 'actualProduceOrderProcessCost', kind: 'money' },
68
+ { header: '委外加工差异', kind: 'percent', actual: 'actualProduceOrderProcessCost', estimated: 'estimatedProduceOrderProcessCost' },
69
+ { header: '工序外发', field: 'actualOutWardOrderCost', kind: 'money' },
70
+ { header: '工序外发差异', kind: 'percent', actual: 'actualOutWardOrderCost', estimated: 'estimatedOutWardOrderCost' },
71
+ { header: '工人计件', field: 'actualWorkManPieceworkCost', kind: 'money' },
72
+ { header: '工人计件差异', kind: 'percent', actual: 'actualWorkManPieceworkCost', estimated: 'estimatedWorkManPieceworkCost' },
73
+ { header: '其它费用', field: 'actualOtherCost', kind: 'money' },
74
+ { header: '其它费用差异', kind: 'percent', actual: 'actualOtherCost', estimated: 'estimatedOtherCost' },
75
+ { header: '单件成本', field: 'actualSumSingleGoodsCost', kind: 'money' },
76
+ { header: '单件成本差异', kind: 'percent', actual: 'actualSumSingleGoodsCost', estimated: 'estimatedSumSingleGoodsCost' },
77
+ { header: '件数', field: 'actualNumber', kind: 'number' },
78
+ { header: '总成本', field: 'actualSumCost', kind: 'money' },
79
+ ],
80
+ },
81
+ {
82
+ name: '预估成本',
83
+ columns: [
84
+ { header: '物料', field: 'estimatedMaterialCost', kind: 'money' },
85
+ { header: '委外加工', field: 'estimatedProduceOrderProcessCost', kind: 'money' },
86
+ { header: '工序外发', field: 'estimatedOutWardOrderCost', kind: 'money' },
87
+ { header: '工人计件', field: 'estimatedWorkManPieceworkCost', kind: 'money' },
88
+ { header: '其它费用', field: 'estimatedOtherCost', kind: 'money' },
89
+ { header: '单件成本', field: 'estimatedSumSingleGoodsCost', kind: 'money' },
90
+ { header: '件数', field: 'estimatedNumber', kind: 'number' },
91
+ { header: '总成本', field: 'estimatedSumCost', kind: 'money' },
92
+ ],
93
+ },
94
+ ];
95
+ const COLUMNS = GROUPS.flatMap((group) => group.columns);
96
+
97
+ const BORDER = {
98
+ top: { style: 'thin', color: { argb: 'FFCCCCCC' } },
99
+ left: { style: 'thin', color: { argb: 'FFCCCCCC' } },
100
+ bottom: { style: 'thin', color: { argb: 'FFCCCCCC' } },
101
+ right: { style: 'thin', color: { argb: 'FFCCCCCC' } },
102
+ };
103
+ // 两级表头底色:灰色(与原脚本一致)
104
+ const HEADER_FILL = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFC0C0C0' } };
105
+ // 图片单元格底色:浅蓝(与原脚本一致)
106
+ const IMAGE_FILL = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE8F1FB' } };
107
+
108
+ /** 文本归一化:空值返回空字符串 */
109
+ function text(value) {
110
+ return value == null ? '' : String(value).trim();
111
+ }
112
+
113
+ /**
114
+ * 数值列取值:无法转数字时按原文返回;整数去掉小数尾巴。
115
+ * 与原脚本 number() 的行为一致(金额、件数都以数字写入单元格)。
116
+ */
117
+ function numeric(value) {
118
+ if (value === null || value === undefined || value === '') {
119
+ return '';
120
+ }
121
+ const parsed = Number(value);
122
+ if (!Number.isFinite(parsed)) {
123
+ return text(value);
124
+ }
125
+ return parsed;
126
+ }
127
+
128
+ /**
129
+ * 四舍五入(远离零)保留 places 位小数。原脚本用 Decimal 的 ROUND_HALF_UP,
130
+ * 这里按正负号分别处理,避免 JavaScript 的 Math.round 对负数向正无穷方向舍入。
131
+ */
132
+ function roundHalfUp(value, places) {
133
+ const factor = 10 ** places;
134
+ const magnitude = Math.abs(value) * factor;
135
+ const rounded = Math.round(magnitude + Number.EPSILON * magnitude);
136
+ const result = (value < 0 ? -rounded : rounded) / factor;
137
+ return Object.is(result, -0) ? 0 : result;
138
+ }
139
+
140
+ /**
141
+ * 差异比例 =(实际 - 预估)/ 预估,保留两位小数。
142
+ * 预估缺失或为 0 时无法计算,返回「-」;舍入到 0 时按 0 处理,避免显示成 -0%。
143
+ */
144
+ function diffRatio(actual, estimated) {
145
+ const left = Number(actual);
146
+ const right = Number(estimated);
147
+ if (!Number.isFinite(left) || !Number.isFinite(right) || right === 0) {
148
+ return '-';
149
+ }
150
+ return roundHalfUp((left - right) / right, PERCENT_DECIMAL);
151
+ }
152
+
153
+ /** 日期列容错:接口返回 yyyy-MM-dd 字符串,取前 10 位;异常原文返回 */
154
+ function dateText(value) {
155
+ const raw = text(value);
156
+ if (!raw) {
157
+ return '';
158
+ }
159
+ const match = /^(\d{4}-\d{2}-\d{2})/.exec(raw);
160
+ return match ? match[1] : raw;
161
+ }
162
+
163
+ /** 金额数字格式:按企业配置的小数位生成,例如 ¥#,##0.00;-¥#,##0.00 */
164
+ function moneyFormat(decimal) {
165
+ const pattern = decimal > 0 ? `#,##0.${'0'.repeat(decimal)}` : '#,##0';
166
+ return `¥${pattern};-¥${pattern}`;
167
+ }
168
+
169
+ /**
170
+ * 计算一个单元格的显示值(图片列留空,图片单独嵌入)。
171
+ * percent 列的差异按该列的 actual/estimated 字段现算。
172
+ */
173
+ function cellValue(row, column) {
174
+ const value = row[column.field];
175
+ switch (column.kind) {
176
+ case 'type':
177
+ return TYPE_NAMES[value] || text(value);
178
+ case 'tags':
179
+ return Array.isArray(value) ? value.map(text).filter(Boolean).join('、') : '';
180
+ case 'date':
181
+ return dateText(value);
182
+ case 'number':
183
+ case 'money':
184
+ return numeric(value);
185
+ case 'percent':
186
+ return diffRatio(row[column.actual], row[column.estimated]);
187
+ case 'image':
188
+ return '';
189
+ default:
190
+ return text(value);
191
+ }
192
+ }
193
+
194
+ /** 单号显示:开启生产批次号且批次号非空时优先显示批次号(与页面 getProduceOrdersSn 一致) */
195
+ function ordersSnDisplay(row, useBatchSn) {
196
+ const batch = text(row.produceBatchSn);
197
+ return useBatchSn && batch ? batch : text(row.ordersSn);
198
+ }
199
+
200
+ /** 列宽:图片 10、金额 13、数量/差异 12、日期 13、文本按标题长度(与原脚本一致) */
201
+ function columnWidth(column) {
202
+ if (column.kind === 'image') {
203
+ return 10;
204
+ }
205
+ if (column.kind === 'money') {
206
+ return 13;
207
+ }
208
+ if (column.kind === 'number' || column.kind === 'percent') {
209
+ return 12;
210
+ }
211
+ if (column.kind === 'date') {
212
+ return 13;
213
+ }
214
+ return Math.max(12, Math.min(24, column.header.length * 3 + 4));
215
+ }
216
+
217
+ /**
218
+ * 生成生产成本报表 Excel。
219
+ * argv.from/to:业务日期区间 yyyy-MM-dd(不传则不限日期);
220
+ * argv.state:逗号分隔的状态值,默认 4,2,3;argv.calWay:1/2/3 或中文别名,默认按默认逻辑;
221
+ * argv.searchKey/ordersSn:搜索条件;argv.out:输出目录。
222
+ */
223
+ async function generate(argv = {}) {
224
+ const states = String(argv.state || '4,2,3')
225
+ .split(',')
226
+ .map((item) => item.trim())
227
+ .filter(Boolean);
228
+ const calWay = CAL_WAY_ALIASES[argv.calWay == null ? 1 : argv.calWay];
229
+ if (!calWay) {
230
+ throw new Error('计算规则仅支持:默认逻辑、成衣入库数、裁床数(或 1/2/3)');
231
+ }
232
+
233
+ const outDir = argv.out || process.cwd();
234
+ const filePath = path.resolve(outDir, `生产成本报表${timestamp()}.xlsx`);
235
+ if (fs.existsSync(filePath)) {
236
+ throw new Error(`输出文件已存在:${filePath};如需替换请先删除旧文件或更换输出目录`);
237
+ }
238
+
239
+ // 企业配置:materialDecimal 决定金额小数位,produceBatchSnEnable 决定单号显示;
240
+ // 读取失败时按默认处理,不中断导出(与原脚本一致)
241
+ let decimal = DEFAULT_DECIMAL;
242
+ let useBatchSn = false;
243
+ try {
244
+ const config = await call('GET', '/api/agent/v1/company-config');
245
+ const configured = Number(config && config.materialDecimal);
246
+ if (Number.isInteger(configured) && configured >= 0 && configured <= MAX_DECIMAL) {
247
+ decimal = configured;
248
+ }
249
+ useBatchSn = config && config.produceBatchSnEnable === true;
250
+ } catch (e) {
251
+ // 配置接口失败不影响导出
252
+ }
253
+
254
+ const searchSn = argv.sn || argv.ordersSn;
255
+ const { list: rawRows } = await fetchAll(
256
+ (page, limit) => call('GET', '/api/agent/v1/report/produce-cost', {
257
+ from: argv.from,
258
+ to: argv.to,
259
+ state: states.join(','),
260
+ calWay,
261
+ searchKey: argv.searchKey,
262
+ ordersSn: searchSn,
263
+ page,
264
+ limit,
265
+ })
266
+ );
267
+ // 接口的单号查询是模糊匹配,这里再按单号/批次号精确过滤一次(与原脚本一致)
268
+ const rows = searchSn
269
+ ? rawRows.filter((row) => text(row.ordersSn) === searchSn || text(row.produceBatchSn) === searchSn)
270
+ : rawRows;
271
+ if (!rows.length) {
272
+ throw new Error('没有符合条件的生产成本报表数据');
273
+ }
274
+
275
+ // 批量下载款式图;下载失败的地址不在 Map 里,对应单元格留空
276
+ const imageUrls = rows.map((row) => text(row.imgUrl));
277
+ const images = await downloadImages(imageUrls);
278
+
279
+ const workbook = createWorkbook();
280
+ const sheet = workbook.addWorksheet(SHEET_NAME, {
281
+ views: [{ state: 'frozen', ySplit: 2 }],
282
+ pageSetup: { orientation: 'landscape', fitToPage: true, fitToWidth: 1, fitToHeight: 0 },
283
+ });
284
+
285
+ // 第 1 行:分组名(写在每组第一列,整组合并);第 2 行:列标题
286
+ const topRow = [];
287
+ for (const group of GROUPS) {
288
+ group.columns.forEach((column, index) => {
289
+ topRow.push(index === 0 ? group.name : '');
290
+ });
291
+ }
292
+ const groupRow = sheet.addRow(topRow);
293
+ groupRow.height = 26;
294
+ const titleRow = sheet.addRow(COLUMNS.map((column) => column.header));
295
+ titleRow.height = 30;
296
+ for (const row of [groupRow, titleRow]) {
297
+ row.eachCell((cell) => {
298
+ cell.font = { bold: true };
299
+ cell.fill = HEADER_FILL;
300
+ cell.border = BORDER;
301
+ cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
302
+ });
303
+ }
304
+ // 每组列数大于 1 时合并第 1 行的组名单元格
305
+ let columnIndex = 1;
306
+ for (const group of GROUPS) {
307
+ if (group.columns.length > 1) {
308
+ sheet.mergeCells(1, columnIndex, 1, columnIndex + group.columns.length - 1);
309
+ }
310
+ columnIndex += group.columns.length;
311
+ }
312
+
313
+ // 差异列的数字格式:正数红色带 +,负数绿色带 -,0 不着色也不加符号。
314
+ // 颜色名必须用英文 [Red]/[Green],写中文 Excel 会拒绝打开整个文件。
315
+ const percentFormat = '[Red]+0.0#%;[Green]-0.0#%;0.0#%';
316
+ const moneyFmt = moneyFormat(decimal);
317
+ const imageCol = COLUMNS.findIndex((column) => column.kind === 'image') + 1;
318
+
319
+ rows.forEach((source, index) => {
320
+ const row = { ...source, ordersSnDisplay: ordersSnDisplay(source, useBatchSn) };
321
+ const values = COLUMNS.map((column) => cellValue(row, column));
322
+ const added = sheet.addRow(values);
323
+ added.height = 46;
324
+ added.eachCell((cell, colNumber) => {
325
+ const kind = COLUMNS[colNumber - 1].kind;
326
+ cell.border = BORDER;
327
+ cell.alignment = {
328
+ vertical: 'middle',
329
+ horizontal: kind === 'text' || kind === 'tags' || kind === 'type' || kind === 'date' ? 'left' : kind === 'image' ? 'center' : 'right',
330
+ wrapText: kind !== 'number' && kind !== 'money' && kind !== 'percent',
331
+ };
332
+ if (kind === 'money') {
333
+ cell.numFmt = moneyFmt;
334
+ }
335
+ if (kind === 'percent' && cell.value !== '-') {
336
+ cell.numFmt = percentFormat;
337
+ }
338
+ if (kind === 'image') {
339
+ cell.fill = IMAGE_FILL;
340
+ }
341
+ });
342
+ const url = imageUrls[index];
343
+ if (url && images.has(url)) {
344
+ embedImage(workbook, sheet, images.get(url), index + 3, imageCol, { width: 60, height: 60 });
345
+ }
346
+ });
347
+
348
+ COLUMNS.forEach((column, i) => {
349
+ sheet.getColumn(i + 1).width = columnWidth(column);
350
+ });
351
+ const lastColLetter = sheet.getColumn(COLUMNS.length).letter;
352
+ const lastRow = rows.length + 2;
353
+ sheet.autoFilter = `A2:${lastColLetter}${lastRow}`;
354
+
355
+ await saveWorkbook(workbook, filePath);
356
+
357
+ const skipped = imageUrls.filter((url) => url && !images.has(url)).length;
358
+ return {
359
+ filePaths: [filePath],
360
+ summary: `生产成本报表已生成:${rows.length} 行 × ${COLUMNS.length} 列,计算规则「${CAL_WAY_NAMES[calWay]}」,` +
361
+ `金额 ${decimal} 位小数,单号显示${useBatchSn ? '生产批次号优先' : '生产单号'},嵌款式图 ${images.size} 张` +
362
+ (skipped ? `(${skipped} 张下载失败已跳过)` : ''),
363
+ };
364
+ }
365
+
366
+ module.exports = { generate };