@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,671 @@
|
|
|
1
|
+
// 供应商对账报表:每个供应商导出一个 Excel 对账单文件,内容是对账界面的
|
|
2
|
+
// 11 表结构:1 张汇总表 + 最多 10 类明细表(外发工序加工、委外加工入库、
|
|
3
|
+
// 次品返厂、返修关联加工厂、物料采购、物料采购入库、物料采购退货、
|
|
4
|
+
// 收款付款、优惠金额、应收应付记账)。
|
|
5
|
+
// 数据流:GET /api/agent/v1/supplier/list 分页取供应商,
|
|
6
|
+
// 再逐供应商 GET /api/agent/v1/supplier/balance-record 取 11 表数据。
|
|
7
|
+
// 生成逻辑从原 Python 报表脚本 export_supplier_reconciliation.py 移植,
|
|
8
|
+
// 表结构、列名、合计行、文件名与原脚本一致。与原脚本的差异:
|
|
9
|
+
// 1. 对账状态由 admin 的文字字段 recordStateStr 改为数字 recordState
|
|
10
|
+
// (1-未对账 2-已对账),本模块转换为文字显示。
|
|
11
|
+
// 2. 外发明细的「完成」列在原脚本读 recover_ 前缀键;agent 接口的发出与
|
|
12
|
+
// 回收共用一行结构,因此完成列直接读同名字段。
|
|
13
|
+
// 3. agent DTO 不提供委外加工入库、次品返厂两表的创建时间列,输出为空。
|
|
14
|
+
// 4. 无供应商欠款数据权限时本期欠款、期末欠款可能为空,按 0 显示。
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { call } = require('../cli/api');
|
|
18
|
+
const { dateStr, createThrottle, fetchAll } = require('./util');
|
|
19
|
+
const { createWorkbook, saveWorkbook } = require('./excel');
|
|
20
|
+
|
|
21
|
+
/** 服务器对同一 AI Key 每分钟限 30 次请求,两次请求间隔至少 2100 毫秒 */
|
|
22
|
+
const REQUEST_INTERVAL_MS = 2100;
|
|
23
|
+
/** 供应商列表分页大小(服务端单页上限 100) */
|
|
24
|
+
const PAGE_LIMIT = 100;
|
|
25
|
+
/** 金额列的货币格式(与原脚本 numFmt 164 一致) */
|
|
26
|
+
const CURRENCY_FORMAT = '[$¥-804]#,##0.00;[Red]-[$¥-804]#,##0.00';
|
|
27
|
+
|
|
28
|
+
/** 表头底色(浅蓝,与原脚本一致) */
|
|
29
|
+
const HEADER_FILL = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD9EAF7' } };
|
|
30
|
+
const BORDER = {
|
|
31
|
+
top: { style: 'thin', color: { argb: 'FF000000' } },
|
|
32
|
+
left: { style: 'thin', color: { argb: 'FF000000' } },
|
|
33
|
+
bottom: { style: 'thin', color: { argb: 'FF000000' } },
|
|
34
|
+
right: { style: 'thin', color: { argb: 'FF000000' } },
|
|
35
|
+
};
|
|
36
|
+
const CENTER = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
|
37
|
+
|
|
38
|
+
/** 空值转空字符串,其余转字符串 */
|
|
39
|
+
function text(value) {
|
|
40
|
+
return value === null || value === undefined ? '' : String(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 取第一个非空值(null、undefined、空字符串视为空) */
|
|
44
|
+
function first(...values) {
|
|
45
|
+
const found = values.find((v) => v !== null && v !== undefined && v !== '');
|
|
46
|
+
return found === undefined ? '' : found;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 对账状态数字转文字:1-未对账 2-已对账,其余为空 */
|
|
50
|
+
function recordStateText(value) {
|
|
51
|
+
return ({ 1: '未对账', 2: '已对账' })[value] ?? '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 毫秒时间戳或日期字符串转「yyyy-MM-dd HH:mm:ss」,解析失败返回原始文字 */
|
|
55
|
+
function formatDateTime(value) {
|
|
56
|
+
if (value === null || value === undefined || value === '') {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
let date = null;
|
|
60
|
+
if (typeof value === 'number') {
|
|
61
|
+
date = new Date(value);
|
|
62
|
+
} else {
|
|
63
|
+
const s = String(value).trim();
|
|
64
|
+
// 纯数字字符串按毫秒时间戳处理(Java Timestamp 的常见序列化形式)
|
|
65
|
+
if (/^\d+(\.\d+)?$/.test(s)) {
|
|
66
|
+
date = new Date(Number(s));
|
|
67
|
+
} else if (/^\d{4}-\d{2}-\d{2}/.test(s)) {
|
|
68
|
+
date = new Date(s);
|
|
69
|
+
}
|
|
70
|
+
if (!date || Number.isNaN(date.getTime())) {
|
|
71
|
+
return s;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (Number.isNaN(date.getTime())) {
|
|
75
|
+
return text(value);
|
|
76
|
+
}
|
|
77
|
+
const pad = (v) => String(v).padStart(2, '0');
|
|
78
|
+
return (
|
|
79
|
+
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
|
80
|
+
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 列定义辅助函数。
|
|
86
|
+
* textCol:普通文本列;amount:数值列(sum 参与合计行,currency 显示货币格式)。
|
|
87
|
+
* 需要特殊取值的列通过 get 函数提供。
|
|
88
|
+
*/
|
|
89
|
+
function textCol(key, header, get) {
|
|
90
|
+
return { key, header, get };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function amount(key, header, { sum = false, currency = false } = {}) {
|
|
94
|
+
return { key, header, sum, currency, numeric: true };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 优惠金额列取绝对值(原脚本对 优惠金额明细 的 needAmount 取 abs) */
|
|
98
|
+
function absAmount(key, header, { sum = false, currency = false } = {}) {
|
|
99
|
+
return {
|
|
100
|
+
key,
|
|
101
|
+
header,
|
|
102
|
+
sum,
|
|
103
|
+
currency,
|
|
104
|
+
numeric: true,
|
|
105
|
+
get: (row) => {
|
|
106
|
+
const v = row[key];
|
|
107
|
+
if (v === null || v === undefined || v === '') {
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
return Math.abs(Number(v));
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 记账明细的生产单/商品列:多个生产批次时从 produceBatchSnList 逐个拼接 */
|
|
116
|
+
function batchJoin(field, fallbackKey) {
|
|
117
|
+
return (row) => {
|
|
118
|
+
if (Array.isArray(row.produceBatchSnList)) {
|
|
119
|
+
return row.produceBatchSnList
|
|
120
|
+
.map((item) => (item && typeof item === 'object' ? text(item[field]) : ''))
|
|
121
|
+
.join('、');
|
|
122
|
+
}
|
|
123
|
+
return fallbackKey ? text(row[fallbackKey]) : '';
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------- 各明细表的列定义(列名与列序对照原脚本 constants) ----------
|
|
128
|
+
|
|
129
|
+
/** 外发工序加工明细:发出列与完成列共用 agent 的同名字段(见文件头说明第 2 点) */
|
|
130
|
+
const OUTWARD = [
|
|
131
|
+
textCol('date', '日期'),
|
|
132
|
+
textCol('businessSn', '生产单/批次号'),
|
|
133
|
+
textCol('goodsSn', '款式'),
|
|
134
|
+
textCol('categoryName', '一级分类'),
|
|
135
|
+
textCol('colorName', '颜色'),
|
|
136
|
+
textCol('sizeName', '尺码'),
|
|
137
|
+
textCol('processNames', '工序'),
|
|
138
|
+
textCol('outWardRemark', '外发备注'),
|
|
139
|
+
textCol('recoverRemark', '回收备注'),
|
|
140
|
+
amount('outWardNumber', '发出数', { sum: true }),
|
|
141
|
+
amount('recoverNumber', '已收回数', { sum: true }),
|
|
142
|
+
amount('repairDeductNumber', '返修扣减', { sum: true }),
|
|
143
|
+
textCol('date', '完成日期'),
|
|
144
|
+
textCol('businessSn', '完成生产单/批次号'),
|
|
145
|
+
textCol('goodsSn', '完成款式'),
|
|
146
|
+
textCol('categoryName', '完成一级分类'),
|
|
147
|
+
textCol('colorName', '完成颜色'),
|
|
148
|
+
textCol('sizeName', '完成尺码'),
|
|
149
|
+
textCol('processNames', '完成工序'),
|
|
150
|
+
textCol('businessKey', '完成单号'),
|
|
151
|
+
textCol('businessItemDesc', '完成类型'),
|
|
152
|
+
amount('recoverNumber', '完成数量', { sum: true }),
|
|
153
|
+
amount('price', '单价', { currency: true }),
|
|
154
|
+
amount('needAmount', '应付金额', { sum: true, currency: true }),
|
|
155
|
+
textCol('statementStateStr', '结算情况'),
|
|
156
|
+
textCol('recordState', '对账状态', (row) => recordStateText(row.recordState)),
|
|
157
|
+
textCol('recordRemark', '备注'),
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
/** 委外加工入库明细、成品采购入库明细(创建时间列 agent 不提供,输出为空) */
|
|
161
|
+
const PROCESS = [
|
|
162
|
+
textCol('date', '日期'),
|
|
163
|
+
textCol('businessSn', '入库单'),
|
|
164
|
+
textCol('produceOrdersSn', '生产单/批次号'),
|
|
165
|
+
textCol('goodsSn', '款式'),
|
|
166
|
+
textCol('categoryName', '一级分类'),
|
|
167
|
+
textCol('colorName', '颜色'),
|
|
168
|
+
textCol('sizeName', '尺码'),
|
|
169
|
+
amount('number', '入库数', { sum: true }),
|
|
170
|
+
textCol('storehouseName', '入库仓库'),
|
|
171
|
+
textCol('businessRemark', '成品入库单备注'),
|
|
172
|
+
textCol('remark', '入库单备注'),
|
|
173
|
+
amount('price', '单价', { currency: true }),
|
|
174
|
+
amount('needAmount', '应付金额', { sum: true, currency: true }),
|
|
175
|
+
textCol('statementStateStr', '结算情况'),
|
|
176
|
+
textCol('recordState', '对账状态', (row) => recordStateText(row.recordState)),
|
|
177
|
+
textCol('recordRemark', '备注'),
|
|
178
|
+
textCol('createTime', '创建时间', () => ''),
|
|
179
|
+
textCol('handlerName', '创建人'),
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
/** 次品返厂明细(创建时间列 agent 不提供,输出为空) */
|
|
183
|
+
const REWORK = [
|
|
184
|
+
textCol('date', '日期'),
|
|
185
|
+
textCol('ordersSn', '次品返厂单'),
|
|
186
|
+
textCol('goodsStoreInOrdersSn', '入库单'),
|
|
187
|
+
textCol('produceOrdersSn', '生产单/批次号'),
|
|
188
|
+
textCol('goodsSn', '款式'),
|
|
189
|
+
textCol('categoryName', '一级分类'),
|
|
190
|
+
textCol('colorName', '颜色'),
|
|
191
|
+
textCol('sizeName', '尺码'),
|
|
192
|
+
amount('price', '单价', { currency: true }),
|
|
193
|
+
textCol('businessRemark', '成品入库单备注'),
|
|
194
|
+
textCol('remark', '返厂单备注'),
|
|
195
|
+
textCol('illustrate', '说明'),
|
|
196
|
+
amount('number', '数量', { sum: true }),
|
|
197
|
+
amount('needAmount', '应付金额', { sum: true, currency: true }),
|
|
198
|
+
textCol('statementStateStr', '结算情况'),
|
|
199
|
+
textCol('recordState', '对账状态', (row) => recordStateText(row.recordState)),
|
|
200
|
+
textCol('recordRemark', '备注'),
|
|
201
|
+
textCol('createTime', '创建时间', () => ''),
|
|
202
|
+
textCol('handlerName', '创建人'),
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
/** 返修关联加工厂明细 */
|
|
206
|
+
const REPAIR = [
|
|
207
|
+
textCol('date', '日期'),
|
|
208
|
+
textCol('ordersSn', '质检单'),
|
|
209
|
+
textCol('produceOrdersSn', '生产单/批次号'),
|
|
210
|
+
textCol('goodsSn', '款式'),
|
|
211
|
+
textCol('colorName', '颜色'),
|
|
212
|
+
textCol('sizeName', '尺码'),
|
|
213
|
+
amount('price', '单价', { currency: true }),
|
|
214
|
+
textCol('businessRemark', '质检单备注'),
|
|
215
|
+
amount('number', '数量', { sum: true }),
|
|
216
|
+
amount('needAmount', '应付金额', { sum: true, currency: true }),
|
|
217
|
+
textCol('statementStateStr', '结算情况'),
|
|
218
|
+
textCol('recordState', '对账状态', (row) => recordStateText(row.recordState)),
|
|
219
|
+
textCol('remark', '备注'),
|
|
220
|
+
textCol('createTime', '创建时间', (row) => formatDateTime(row.createTime)),
|
|
221
|
+
textCol('handlerName', '创建人'),
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
/** 物料采购明细、物料采购入库明细共用列定义 */
|
|
225
|
+
const PURCHASE = [
|
|
226
|
+
textCol('date', '日期'),
|
|
227
|
+
textCol('businessSn', '采购单/采购入库单'),
|
|
228
|
+
textCol('materialName', '物料'),
|
|
229
|
+
textCol('materialSn', '货号'),
|
|
230
|
+
textCol('colorName', '颜色'),
|
|
231
|
+
textCol('propertyValueNameStr', '规格'),
|
|
232
|
+
textCol('produceOrdersSn', '生产单/批次号'),
|
|
233
|
+
textCol('supplierSn', '供应商单号'),
|
|
234
|
+
textCol('goodsSn', '款式'),
|
|
235
|
+
amount('purchasePrice', '采购单价', { currency: true }),
|
|
236
|
+
amount('purchaseRealPrice', '抹零后采购单价', { currency: true }),
|
|
237
|
+
amount('purchaseNumber', '基础采购数', { sum: true }),
|
|
238
|
+
textCol('purchaseUnitName', '采购单位'),
|
|
239
|
+
amount('needAmount', '应付金额', { sum: true, currency: true }),
|
|
240
|
+
textCol('expectTime', '采购预计到料日期', (row) => formatDateTime(row.expectTime)),
|
|
241
|
+
textCol('convertRate', '转换系数'),
|
|
242
|
+
amount('basePrice', '基础单价', { currency: true }),
|
|
243
|
+
amount('price', '抹零后基础单价', { currency: true }),
|
|
244
|
+
amount('number', '基础入库数', { sum: true }),
|
|
245
|
+
textCol('baseUnitName', '基础单位'),
|
|
246
|
+
textCol('remark', '采购单备注'),
|
|
247
|
+
textCol('statementStateStr', '结算情况'),
|
|
248
|
+
textCol('recordState', '对账状态', (row) => recordStateText(row.recordState)),
|
|
249
|
+
textCol('recordRemark', '备注'),
|
|
250
|
+
textCol('createTime', '创建时间', (row) => formatDateTime(row.createTime)),
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
/** 物料采购退货出库明细 */
|
|
254
|
+
const REFUND = [
|
|
255
|
+
textCol('date', '日期'),
|
|
256
|
+
textCol('businessSn', '退货出库单'),
|
|
257
|
+
textCol('materialName', '物料'),
|
|
258
|
+
textCol('materialSn', '货号'),
|
|
259
|
+
textCol('colorName', '颜色'),
|
|
260
|
+
textCol('propertyValueNameStr', '规格'),
|
|
261
|
+
amount('purchaseRefundPrice', '单价', { currency: true }),
|
|
262
|
+
textCol('purchaseStoreOutNumber', '出库数'),
|
|
263
|
+
textCol('purchaseRefundUnitName', '退货单位'),
|
|
264
|
+
amount('needAmount', '应收金额', { sum: true, currency: true }),
|
|
265
|
+
textCol('convertRate', '转换系数'),
|
|
266
|
+
amount('price', '基础单价', { currency: true }),
|
|
267
|
+
amount('number', '基础出库数', { sum: true }),
|
|
268
|
+
textCol('baseUnitName', '基础单位'),
|
|
269
|
+
textCol('remark', '退货单备注'),
|
|
270
|
+
textCol('materialStoreOutRemark', '出库单备注'),
|
|
271
|
+
textCol('statementStateStr', '结算情况'),
|
|
272
|
+
textCol('recordState', '对账状态', (row) => recordStateText(row.recordState)),
|
|
273
|
+
textCol('recordRemark', '备注'),
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
/** 收款、付款明细 */
|
|
277
|
+
const PAY = [
|
|
278
|
+
textCol('date', '日期'),
|
|
279
|
+
textCol('businessItem', '业务类型'),
|
|
280
|
+
textCol('businessSn', '单号'),
|
|
281
|
+
amount('realAmount', '付款金额', { sum: true, currency: true }),
|
|
282
|
+
textCol('statementStateStr', '结算情况'),
|
|
283
|
+
textCol('recordState', '对账状态', (row) => recordStateText(row.recordState)),
|
|
284
|
+
textCol('remark', '备注'),
|
|
285
|
+
textCol('businessRemark', '单据备注'),
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
/** 优惠金额明细(needAmount 取绝对值) */
|
|
289
|
+
const DISCOUNT = [
|
|
290
|
+
textCol('date', '日期'),
|
|
291
|
+
textCol('businessItem', '业务类型'),
|
|
292
|
+
textCol('businessSn', '单号'),
|
|
293
|
+
absAmount('needAmount', '优惠金额', { sum: true, currency: true }),
|
|
294
|
+
textCol('statementStateStr', '结算情况'),
|
|
295
|
+
textCol('recordState', '对账状态', (row) => recordStateText(row.recordState)),
|
|
296
|
+
textCol('remark', '备注'),
|
|
297
|
+
textCol('businessRemark', '单据备注'),
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
/** 应收记账、应付记账明细 */
|
|
301
|
+
const ACCOUNT = [
|
|
302
|
+
textCol('date', '日期'),
|
|
303
|
+
textCol('businessItem', '业务类型'),
|
|
304
|
+
textCol('accountItemName', '账目类型'),
|
|
305
|
+
textCol('businessSn', '单号'),
|
|
306
|
+
textCol('produceOrdersSn', '生产单', batchJoin('ordersSn', 'produceOrdersKey')),
|
|
307
|
+
textCol('goodsName', '商品', batchJoin('goodsName', '')),
|
|
308
|
+
textCol('goodsSn', '货号', batchJoin('goodsSn', '')),
|
|
309
|
+
amount('needAmount', '应付金额', { sum: true, currency: true }),
|
|
310
|
+
textCol('payWayStr', '付款方式'),
|
|
311
|
+
textCol('payerName', '代付人'),
|
|
312
|
+
textCol('feeReimbursementOrdersSn', '报销单'),
|
|
313
|
+
textCol('feeReimbursementOrderState', '报销单状态'),
|
|
314
|
+
textCol('statementStateStr', '结算情况'),
|
|
315
|
+
textCol('recordState', '对账状态', (row) => recordStateText(row.recordState)),
|
|
316
|
+
textCol('handlerName', '创建人'),
|
|
317
|
+
textCol('remark', '备注'),
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
// ---------- Excel 生成 ----------
|
|
321
|
+
|
|
322
|
+
/** 取一列在某行上的显示值:数值列返回 number,空值返回空字符串 */
|
|
323
|
+
function cellValue(column, row) {
|
|
324
|
+
if (column.get) {
|
|
325
|
+
return column.get(row);
|
|
326
|
+
}
|
|
327
|
+
const v = row[column.key];
|
|
328
|
+
if (column.numeric) {
|
|
329
|
+
if (v === null || v === undefined || v === '') {
|
|
330
|
+
return '';
|
|
331
|
+
}
|
|
332
|
+
return Number(v);
|
|
333
|
+
}
|
|
334
|
+
return text(v);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** 表头行样式:加粗、浅蓝底、细边框、居中换行 */
|
|
338
|
+
function styleHeaderRow(row, height = 22) {
|
|
339
|
+
row.height = height;
|
|
340
|
+
row.eachCell({ includeEmpty: true }, (cell) => {
|
|
341
|
+
cell.font = { bold: true };
|
|
342
|
+
cell.fill = HEADER_FILL;
|
|
343
|
+
cell.border = BORDER;
|
|
344
|
+
cell.alignment = CENTER;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** 写一张明细表:表头 + 数据行 + 合计行(数量列保留 4 位、金额列保留 2 位) */
|
|
349
|
+
function addDetailSheet(workbook, name, columns, rows) {
|
|
350
|
+
const sheet = workbook.addWorksheet(name, { views: [{ state: 'frozen', ySplit: 1 }] });
|
|
351
|
+
styleHeaderRow(sheet.addRow(columns.map((c) => c.header)));
|
|
352
|
+
|
|
353
|
+
const sums = columns.map(() => 0);
|
|
354
|
+
for (const row of rows) {
|
|
355
|
+
const values = columns.map((column, i) => {
|
|
356
|
+
const value = cellValue(column, row);
|
|
357
|
+
if (column.sum && typeof value === 'number') {
|
|
358
|
+
sums[i] += value;
|
|
359
|
+
}
|
|
360
|
+
return value;
|
|
361
|
+
});
|
|
362
|
+
const added = sheet.addRow(values);
|
|
363
|
+
added.height = 20;
|
|
364
|
+
added.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
|
365
|
+
cell.border = BORDER;
|
|
366
|
+
cell.alignment = CENTER;
|
|
367
|
+
if (columns[colNumber - 1].currency) {
|
|
368
|
+
cell.numFmt = CURRENCY_FORMAT;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 合计行:第一列固定「合计」,参与合计的列输出累计值
|
|
374
|
+
const totalValues = columns.map((column, i) => {
|
|
375
|
+
if (i === 0) {
|
|
376
|
+
return '合计';
|
|
377
|
+
}
|
|
378
|
+
if (column.sum) {
|
|
379
|
+
const digits = column.currency ? 2 : 4;
|
|
380
|
+
return Math.round(sums[i] * 10 ** digits) / 10 ** digits;
|
|
381
|
+
}
|
|
382
|
+
return '';
|
|
383
|
+
});
|
|
384
|
+
const totalRow = sheet.addRow(totalValues);
|
|
385
|
+
totalRow.height = 20;
|
|
386
|
+
totalRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
|
387
|
+
cell.font = { bold: true };
|
|
388
|
+
cell.border = BORDER;
|
|
389
|
+
cell.alignment = CENTER;
|
|
390
|
+
if (columns[colNumber - 1].currency) {
|
|
391
|
+
cell.numFmt = CURRENCY_FORMAT;
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
columns.forEach((column, i) => {
|
|
396
|
+
sheet.getColumn(i + 1).width = 14;
|
|
397
|
+
});
|
|
398
|
+
return sheet;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** 汇总表:供应商与对账周期、五个金额汇总、按类型应付金额 */
|
|
402
|
+
function addSummarySheet(workbook, data, supplierName, start, end) {
|
|
403
|
+
const sheet = workbook.addWorksheet('汇总', { views: [{ state: 'frozen', ySplit: 1 }] });
|
|
404
|
+
const share = data.shareSum || {};
|
|
405
|
+
const keys = [
|
|
406
|
+
['initDebtAmount', '期初欠款'],
|
|
407
|
+
['needAmount', '本期应付金额'],
|
|
408
|
+
['realAmount', '本期实付金额'],
|
|
409
|
+
['debtAmount', '本期欠款'],
|
|
410
|
+
['endDebtAmount', '期末欠款'],
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
// 第 1 行:供应商名称与对账周期
|
|
414
|
+
const infoRow = sheet.getRow(1);
|
|
415
|
+
infoRow.getCell(1).value = '供应商:';
|
|
416
|
+
infoRow.getCell(2).value = supplierName;
|
|
417
|
+
infoRow.getCell(4).value = '对账周期:';
|
|
418
|
+
infoRow.getCell(5).value = `${start}至${end}`;
|
|
419
|
+
infoRow.height = 20;
|
|
420
|
+
for (const col of [1, 4]) {
|
|
421
|
+
const cell = infoRow.getCell(col);
|
|
422
|
+
cell.border = BORDER;
|
|
423
|
+
cell.alignment = CENTER;
|
|
424
|
+
}
|
|
425
|
+
for (const col of [2, 5]) {
|
|
426
|
+
const cell = infoRow.getCell(col);
|
|
427
|
+
cell.font = { bold: true };
|
|
428
|
+
cell.border = BORDER;
|
|
429
|
+
cell.alignment = CENTER;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 第 3~4 行:五个金额汇总(无欠款权限时按 0 显示)
|
|
433
|
+
const headerRow = sheet.getRow(3);
|
|
434
|
+
keys.forEach(([key, header], i) => {
|
|
435
|
+
headerRow.getCell(i + 1).value = header;
|
|
436
|
+
});
|
|
437
|
+
styleHeaderRow(headerRow);
|
|
438
|
+
const valueRow = sheet.getRow(4);
|
|
439
|
+
keys.forEach(([key], i) => {
|
|
440
|
+
const v = share[key];
|
|
441
|
+
const number = v === null || v === undefined || v === '' ? 0 : Number(v);
|
|
442
|
+
valueRow.getCell(i + 1).value = Number.isFinite(number) ? number : 0;
|
|
443
|
+
});
|
|
444
|
+
valueRow.height = 20;
|
|
445
|
+
valueRow.eachCell({ includeEmpty: true }, (cell) => {
|
|
446
|
+
cell.border = BORDER;
|
|
447
|
+
cell.alignment = CENTER;
|
|
448
|
+
cell.numFmt = CURRENCY_FORMAT;
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// 第 6 行起:按业务类型的应付金额
|
|
452
|
+
const typeHeader = sheet.getRow(6);
|
|
453
|
+
typeHeader.getCell(1).value = '类型';
|
|
454
|
+
typeHeader.getCell(2).value = '应付金额';
|
|
455
|
+
styleHeaderRow(typeHeader);
|
|
456
|
+
(data.lstType || []).forEach((typeRow, index) => {
|
|
457
|
+
const row = sheet.getRow(7 + index);
|
|
458
|
+
row.getCell(1).value = text(typeRow.typeName);
|
|
459
|
+
const amount = typeRow.needAmount === null || typeRow.needAmount === undefined || typeRow.needAmount === ''
|
|
460
|
+
? 0
|
|
461
|
+
: Number(typeRow.needAmount);
|
|
462
|
+
row.getCell(2).value = Number.isFinite(amount) ? amount : 0;
|
|
463
|
+
row.height = 20;
|
|
464
|
+
row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
|
465
|
+
cell.border = BORDER;
|
|
466
|
+
cell.alignment = CENTER;
|
|
467
|
+
if (colNumber === 2) {
|
|
468
|
+
cell.numFmt = CURRENCY_FORMAT;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
for (let i = 1; i <= 5; i += 1) {
|
|
474
|
+
sheet.getColumn(i).width = 28;
|
|
475
|
+
}
|
|
476
|
+
return sheet;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** 组装一个供应商的全部工作表:汇总表必有,明细表只在实际有数据时保留 */
|
|
480
|
+
function buildSheets(workbook, data, supplierName, start, end) {
|
|
481
|
+
addSummarySheet(workbook, data, supplierName, start, end);
|
|
482
|
+
const configs = [
|
|
483
|
+
['外发工序加工明细', OUTWARD, [...(data.lstOutWard || []), ...(data.lstOutWardRecover || [])]],
|
|
484
|
+
['委外加工入库明细、成品采购入库明细', PROCESS, data.lstProcess || []],
|
|
485
|
+
['次品返厂明细', REWORK, data.lstRework || []],
|
|
486
|
+
['返修关联加工厂明细', REPAIR, data.lstRepairDeduction || []],
|
|
487
|
+
['物料采购明细', PURCHASE, data.lstPurchaseOrderDetail || []],
|
|
488
|
+
['物料采购入库明细', PURCHASE, data.lstPurchaseDetail || []],
|
|
489
|
+
['物料采购退货出库明细', REFUND, data.lstRefundDetail || []],
|
|
490
|
+
['收款、付款明细', PAY, (data.lstPay || []).filter((row) => row.accountItemId !== 5)],
|
|
491
|
+
['优惠金额明细', DISCOUNT, (data.lstPay || []).filter((row) => row.accountItemId === 5)],
|
|
492
|
+
['应收记账、应付记账明细', ACCOUNT, data.lstSupplierAccount || []],
|
|
493
|
+
];
|
|
494
|
+
let sheetCount = 1;
|
|
495
|
+
for (const [sheetName, columns, rows] of configs) {
|
|
496
|
+
if (rows.length) {
|
|
497
|
+
addDetailSheet(workbook, sheetName, columns, rows);
|
|
498
|
+
sheetCount += 1;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return sheetCount;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ---------- 查询与参数 ----------
|
|
505
|
+
|
|
506
|
+
/** 分页取供应商列表(可选按名称过滤,服务端为模糊匹配) */
|
|
507
|
+
async function fetchSuppliers(throttle, supplierName) {
|
|
508
|
+
const { list } = await fetchAll(
|
|
509
|
+
(page, limit) => throttle(() => call('GET', '/api/agent/v1/supplier/list', {
|
|
510
|
+
supplierName: supplierName || undefined,
|
|
511
|
+
page,
|
|
512
|
+
limit,
|
|
513
|
+
})),
|
|
514
|
+
{ limit: PAGE_LIMIT },
|
|
515
|
+
);
|
|
516
|
+
return list;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** 供应商行取 id 与名称(agent 字段为 id/supplierName) */
|
|
520
|
+
function chooseSuppliers(suppliers, argv) {
|
|
521
|
+
const idOf = (row) => first(row.supplierId, row.id);
|
|
522
|
+
const nameOf = (row) => row.supplierName || row.name || '未命名供应商';
|
|
523
|
+
if (argv.supplierId !== undefined && argv.supplierId !== null && argv.supplierId !== '') {
|
|
524
|
+
const wanted = String(argv.supplierId);
|
|
525
|
+
const found = suppliers.find((row) => String(idOf(row)) === wanted);
|
|
526
|
+
return [{ id: argv.supplierId, name: found ? nameOf(found) : wanted }];
|
|
527
|
+
}
|
|
528
|
+
if (argv.supplier) {
|
|
529
|
+
if (suppliers.length !== 1) {
|
|
530
|
+
throw new Error(`名称“${argv.supplier}”匹配到 ${suppliers.length} 个供应商;请改用更精确的名称或供应商 id`);
|
|
531
|
+
}
|
|
532
|
+
return [{ id: idOf(suppliers[0]), name: nameOf(suppliers[0]) }];
|
|
533
|
+
}
|
|
534
|
+
return suppliers
|
|
535
|
+
.filter((row) => idOf(row) !== '')
|
|
536
|
+
.map((row) => ({ id: idOf(row), name: nameOf(row) }));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** 解析并校验日期字符串,返回当日零点的 Date */
|
|
540
|
+
function parseDay(value, label) {
|
|
541
|
+
const s = String(value || '');
|
|
542
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) {
|
|
543
|
+
throw new Error(`${label}日期格式应为 yyyy-MM-dd:${s}`);
|
|
544
|
+
}
|
|
545
|
+
const date = new Date(`${s}T00:00:00`);
|
|
546
|
+
if (Number.isNaN(date.getTime())) {
|
|
547
|
+
throw new Error(`${label}日期无效:${s}`);
|
|
548
|
+
}
|
|
549
|
+
return date;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** 取一个月前的同一天(目标月天数不足时取当月最后一天) */
|
|
553
|
+
function monthBack(date) {
|
|
554
|
+
const year = date.getMonth() > 0 ? date.getFullYear() : date.getFullYear() - 1;
|
|
555
|
+
const month = date.getMonth() > 0 ? date.getMonth() - 1 : 11;
|
|
556
|
+
const lastDay = new Date(year, month + 1, 0).getDate();
|
|
557
|
+
return new Date(year, month, Math.min(date.getDate(), lastDay));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* 确定对账周期。优先使用显式传入的 from/to(必须同时提供);
|
|
562
|
+
* 未提供时按 period 取默认区间(recent-month 近一个自然月到当天,
|
|
563
|
+
* last-month 上个自然月,current-month 本月 1 日到当天)。
|
|
564
|
+
* 区间必须有效且不超过 365 天(与原脚本一致)。
|
|
565
|
+
*/
|
|
566
|
+
function resolveRange(argv) {
|
|
567
|
+
let start;
|
|
568
|
+
let end;
|
|
569
|
+
if (argv.from !== undefined || argv.to !== undefined) {
|
|
570
|
+
if (!argv.from || !argv.to) {
|
|
571
|
+
throw new Error('自定义对账周期必须同时提供 from 与 to(格式 yyyy-MM-dd)');
|
|
572
|
+
}
|
|
573
|
+
start = parseDay(argv.from, '开始');
|
|
574
|
+
end = parseDay(argv.to, '结束');
|
|
575
|
+
} else {
|
|
576
|
+
const period = argv.period || 'recent-month';
|
|
577
|
+
const today = new Date();
|
|
578
|
+
if (period === 'recent-month') {
|
|
579
|
+
start = monthBack(today);
|
|
580
|
+
end = today;
|
|
581
|
+
} else if (period === 'last-month') {
|
|
582
|
+
end = new Date(today.getFullYear(), today.getMonth(), 0);
|
|
583
|
+
start = new Date(end.getFullYear(), end.getMonth(), 1);
|
|
584
|
+
} else if (period === 'current-month') {
|
|
585
|
+
start = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
586
|
+
end = today;
|
|
587
|
+
} else {
|
|
588
|
+
throw new Error(`不支持的对账周期:${period}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (end < start) {
|
|
592
|
+
throw new Error('对账周期结束日期不能早于开始日期');
|
|
593
|
+
}
|
|
594
|
+
if ((end - start) / 86400000 > 365) {
|
|
595
|
+
throw new Error('对账周期不能超过 366 天');
|
|
596
|
+
}
|
|
597
|
+
return { start: dateStr(start), end: dateStr(end) };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** 文件名安全处理:替换非法字符、去掉首尾空白与点号(与原脚本 safe_filename 一致) */
|
|
601
|
+
function safeFilename(value) {
|
|
602
|
+
const cleaned = String(value ?? '')
|
|
603
|
+
.replace(/[\\/:*?"<>|\x00-\x1f]+/g, '_')
|
|
604
|
+
.trim()
|
|
605
|
+
.replace(/^[.\s]+|[.\s]+$/g, '');
|
|
606
|
+
return cleaned || '未命名供应商';
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ---------- 报表入口 ----------
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* 生成供应商对账 Excel。
|
|
613
|
+
* argv:{ supplier 供应商名称(可选,不传导出全部),
|
|
614
|
+
* supplierId 供应商 id(可选,优先于名称),
|
|
615
|
+
* from/to 对账起止日期(可选,默认近一个自然月),
|
|
616
|
+
* period 默认周期(可选), out 输出目录 }。
|
|
617
|
+
* 每个供应商一个文件,文件已存在时报错,不覆盖。
|
|
618
|
+
*/
|
|
619
|
+
async function generate(argv = {}) {
|
|
620
|
+
const throttle = createThrottle(REQUEST_INTERVAL_MS);
|
|
621
|
+
const { start, end } = resolveRange(argv);
|
|
622
|
+
|
|
623
|
+
// 指定 id 时不按名称过滤,保证能从全量列表解析出名称
|
|
624
|
+
const hasId = argv.supplierId !== undefined && argv.supplierId !== null && argv.supplierId !== '';
|
|
625
|
+
const suppliers = await fetchSuppliers(throttle, hasId ? undefined : argv.supplier);
|
|
626
|
+
const chosen = chooseSuppliers(suppliers, argv);
|
|
627
|
+
if (!chosen.length) {
|
|
628
|
+
throw new Error('没有可导出的供应商');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const outDir = argv.out ? path.resolve(String(argv.out)) : process.cwd();
|
|
632
|
+
const filePaths = [];
|
|
633
|
+
let totalSheets = 0;
|
|
634
|
+
let summaryOnlyCount = 0;
|
|
635
|
+
for (let i = 0; i < chosen.length; i += 1) {
|
|
636
|
+
const { id, name } = chosen[i];
|
|
637
|
+
const data = (await throttle(() => call('GET', '/api/agent/v1/supplier/balance-record', {
|
|
638
|
+
supplierId: id,
|
|
639
|
+
from: start,
|
|
640
|
+
to: end,
|
|
641
|
+
}))) || {};
|
|
642
|
+
const workbook = createWorkbook();
|
|
643
|
+
const sheetCount = buildSheets(workbook, data, name, start, end);
|
|
644
|
+
if (sheetCount === 1) {
|
|
645
|
+
summaryOnlyCount += 1;
|
|
646
|
+
}
|
|
647
|
+
totalSheets += sheetCount;
|
|
648
|
+
|
|
649
|
+
const filePath = path.resolve(
|
|
650
|
+
outDir,
|
|
651
|
+
`供应商对账_${safeFilename(name)}_${start.replace(/-/g, '')}至${end.replace(/-/g, '')}.xlsx`,
|
|
652
|
+
);
|
|
653
|
+
if (fs.existsSync(filePath)) {
|
|
654
|
+
throw new Error(`输出文件已存在:${filePath};如需替换请先删除旧文件`);
|
|
655
|
+
}
|
|
656
|
+
await saveWorkbook(workbook, filePath);
|
|
657
|
+
filePaths.push(filePath);
|
|
658
|
+
if ((i + 1) % 10 === 0) {
|
|
659
|
+
process.stdout.write(`供应商对账进度:已导出 ${i + 1}/${chosen.length} 个供应商\n`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
let summary = `供应商对账导出完成:供应商 ${filePaths.length} 个(对账周期 ${start} 至 ${end}),共保留 ${totalSheets} 张工作表(汇总及非空明细)`;
|
|
664
|
+
if (summaryOnlyCount) {
|
|
665
|
+
summary += `;其中 ${summaryOnlyCount} 个供应商只有汇总表(明细无数据)`;
|
|
666
|
+
}
|
|
667
|
+
summary += `;输出目录:${outDir}`;
|
|
668
|
+
return { filePaths, summary };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
module.exports = { generate };
|