wwt-ar-summary 1.0.0 → 1.1.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 +1 -1
- package/ar-summary.html +120 -47
- package/package.json +1 -1
- package/version.json +1 -1
package/README.md
CHANGED
package/ar-summary.html
CHANGED
|
@@ -90,7 +90,7 @@ aside#cfgPanel {
|
|
|
90
90
|
}
|
|
91
91
|
aside#cfgPanel.open { right: 0; }
|
|
92
92
|
aside#cfgPanel header { position: static; box-shadow: none; border-bottom: 1px solid var(--line); }
|
|
93
|
-
aside#cfgPanel .body { padding: 16px 18px; overflow: auto; flex: 1; }
|
|
93
|
+
aside#cfgPanel .body { padding: 16px 18px; overflow: auto; flex: 1; min-height: 0; }
|
|
94
94
|
aside#cfgPanel label { display: block; font-size: 12px; color: var(--dim); margin: 14px 0 5px; }
|
|
95
95
|
aside#cfgPanel textarea {
|
|
96
96
|
width: 100%; min-height: 84px; padding: 8px; font: 12px/1.5 Consolas, monospace;
|
|
@@ -100,6 +100,7 @@ aside#cfgPanel textarea {
|
|
|
100
100
|
#updateMsg a { color: var(--brand2); }
|
|
101
101
|
.muted { color: var(--dim); font-size: 12px; }
|
|
102
102
|
.err { color: var(--bad); }
|
|
103
|
+
|
|
103
104
|
</style>
|
|
104
105
|
</head>
|
|
105
106
|
<body>
|
|
@@ -111,7 +112,7 @@ aside#cfgPanel textarea {
|
|
|
111
112
|
|
|
112
113
|
<header>
|
|
113
114
|
<h1>应收帐款汇总表</h1>
|
|
114
|
-
<span class="sub" id="verLine">v1.
|
|
115
|
+
<span class="sub" id="verLine">v1.1.0</span>
|
|
115
116
|
<span class="grow"></span>
|
|
116
117
|
<span id="updateMsg"></span>
|
|
117
118
|
<button class="btn" id="btnCheck">检查更新</button>
|
|
@@ -131,8 +132,9 @@ aside#cfgPanel textarea {
|
|
|
131
132
|
<div class="card" id="filePanel" style="display:none">
|
|
132
133
|
<h2>2 · 确认识别结果</h2>
|
|
133
134
|
<table class="files">
|
|
134
|
-
<thead><tr><th>文件</th><th style="width:
|
|
135
|
-
<th style="width:
|
|
135
|
+
<thead><tr><th>文件</th><th style="width:130px">工作表</th><th style="width:180px">识别为</th>
|
|
136
|
+
<th style="width:120px">依据</th>
|
|
137
|
+
<th style="width:70px">行数</th><th style="width:140px">金额合计</th><th style="width:60px"></th></tr></thead>
|
|
136
138
|
<tbody id="fileBody"></tbody>
|
|
137
139
|
</table>
|
|
138
140
|
<div style="margin-top:14px; display:flex; gap:10px; align-items:center">
|
|
@@ -191,7 +193,7 @@ aside#cfgPanel textarea {
|
|
|
191
193
|
|
|
192
194
|
<script>
|
|
193
195
|
window.APP_META = {
|
|
194
|
-
appVersion: '1.
|
|
196
|
+
appVersion: '1.1.0',
|
|
195
197
|
appPkg: 'wwt-ar-summary',
|
|
196
198
|
appUpdateFile: 'ar-summary.html',
|
|
197
199
|
updateSources: []
|
|
@@ -284,15 +286,18 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
284
286
|
'广州维修基地', '加工部', '阀体车间', '研发中心'],
|
|
285
287
|
// 类型列(出库单类 / 收费类型)含这些词 → 内部
|
|
286
288
|
typeKeywords: ['复检', 'WWT再制造', '调库销售单'],
|
|
289
|
+
// 表名/sheet 名含这些词 → 不是明细源表(汇总、透视、库存、入库、调拨都不参与应收)
|
|
290
|
+
skipKeywords: ['汇总', '统计', '透视', '库存', '入库', '调拨', '三项费用', '说明'],
|
|
287
291
|
// 五张源表:文件名关键词 + 表头指纹 + 金额列 + 单号列 + 归属列
|
|
288
292
|
tables: [
|
|
289
|
-
{ key: 'xs', name: '销售未结算', slot: 2, fileKw: ['出库未确认', '
|
|
293
|
+
{ key: 'xs', name: '销售未结算', slot: 2, fileKw: ['出库未确认', '销售未确认', '销售未结算'],
|
|
290
294
|
must: ['出库单类'], money: '实收金额', doc: '出库单号', typeCol: '出库单类' },
|
|
291
|
-
|
|
295
|
+
// 关键词必须带"销售"前缀:'已挂账未清账' 这种通用词会把"维修已挂账未清账"也抢过来
|
|
296
|
+
{ key: 'xz', name: '销售已挂账未清账', slot: 3, fileKw: ['销售挂账', '销售已挂账', '销售未清账'],
|
|
292
297
|
must: ['出库单类'], money: '实收金额', doc: '出库单号', typeCol: '出库单类' },
|
|
293
|
-
{ key: 'wx', name: '维修已挂账未清账', slot: 3, fileKw: ['维修挂账', '维修已挂账'],
|
|
298
|
+
{ key: 'wx', name: '维修已挂账未清账', slot: 3, fileKw: ['维修挂账', '维修已挂账', '维修未清账'],
|
|
294
299
|
must: ['客户实收额'], money: '客户实收额', doc: '工单号', typeCol: '收费类型' },
|
|
295
|
-
{ key: 'wg', name: '完工未结算', slot: 0, fileKw: ['完工未结算', '
|
|
300
|
+
{ key: 'wg', name: '完工未结算', slot: 0, fileKw: ['完工未结算', '已完工'],
|
|
296
301
|
must: ['A结算金额'], money: 'A结算金额', doc: '工单号', typeCol: '收费类型' },
|
|
297
302
|
{ key: 'zx', name: '在修未结算', slot: 1, fileKw: ['在修'],
|
|
298
303
|
must: ['A结算金额'], money: 'A结算金额', doc: '工单号', typeCol: '收费类型' }
|
|
@@ -343,28 +348,53 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
343
348
|
* 返回 { table: 命中的表定义, how: '文件名'|'表头指纹', candidates: [...] }
|
|
344
349
|
* 文件名优先(销售两张 / 完工与在修 的表头相同,只能靠文件名区分)
|
|
345
350
|
*/
|
|
346
|
-
function detectTable(fileName, header, cfg) {
|
|
351
|
+
function detectTable(fileName, header, cfg, sheetName) {
|
|
347
352
|
cfg = cfg || DEFAULT_CFG;
|
|
348
353
|
var fn = norm(fileName).toLowerCase();
|
|
354
|
+
var sh = norm(sheetName || '').toLowerCase(); // 跳过规则只看 sheet 名,不看文件名
|
|
355
|
+
|
|
356
|
+
// 汇总/透视/库存/入库/调拨 这类表直接排除——它们的表头也含"单位名称",不排除会误判
|
|
357
|
+
var skipped = sh ? (cfg.skipKeywords || []).filter(function (k) { return sh.indexOf(norm(k).toLowerCase()) >= 0; }) : [];
|
|
358
|
+
if (skipped.length) {
|
|
359
|
+
return { table: null, how: '跳过(' + skipped.join('/') + ')', skipped: true, candidates: [] };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** 在一段文本里找该表命中的最长关键词 */
|
|
363
|
+
function kwLen(t, text) {
|
|
364
|
+
var best = '';
|
|
365
|
+
t.fileKw.forEach(function (k) {
|
|
366
|
+
var kn = norm(k).toLowerCase();
|
|
367
|
+
if (kn && text.indexOf(kn) >= 0 && kn.length > best.length) best = kn;
|
|
368
|
+
});
|
|
369
|
+
return best.length;
|
|
370
|
+
}
|
|
371
|
+
|
|
349
372
|
var hits = [];
|
|
350
373
|
cfg.tables.forEach(function (t) {
|
|
351
|
-
var byName = t.fileKw.some(function (k) { return fn.indexOf(norm(k).toLowerCase()) >= 0; });
|
|
352
374
|
var byHead = t.must.every(function (m) { return findCol(header, [m]) >= 0; });
|
|
353
|
-
|
|
354
|
-
|
|
375
|
+
// 工作表名优先("在修""销售未确认"这种表名最可靠),文件名兜底
|
|
376
|
+
var lenSh = sh ? kwLen(t, sh) : 0;
|
|
377
|
+
var lenFn = kwLen(t, fn);
|
|
378
|
+
if (lenSh) hits.push({ t: t, how: byHead ? '工作表名+表头' : '工作表名', len: lenSh, pri: 2 });
|
|
379
|
+
else if (lenFn) hits.push({ t: t, how: byHead ? '文件名+表头' : '文件名', len: lenFn, pri: 1 });
|
|
355
380
|
});
|
|
356
|
-
if (hits.length === 1) return { table: hits[0].t, how: hits[0].how, candidates: hits.map(function (h) { return h.t.name; }) };
|
|
357
381
|
|
|
358
|
-
//
|
|
382
|
+
// 命中多张时:工作表名命中优先,其次匹配关键词更长者(更具体者优先)
|
|
383
|
+
hits.sort(function (a, b) { return (b.pri - a.pri) || (b.len - a.len); });
|
|
384
|
+
if (hits.length) {
|
|
385
|
+
return {
|
|
386
|
+
table: hits[0].t, how: hits[0].how,
|
|
387
|
+
candidates: hits.map(function (h) { return h.t.name; })
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 文件名没命中:仅当表头指纹唯一时才自动认定,否则交给用户指定
|
|
359
392
|
var headOnly = cfg.tables.filter(function (t) {
|
|
360
393
|
return t.must.every(function (m) { return findCol(header, [m]) >= 0; });
|
|
361
394
|
});
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
hits.sort(function (a, b) { return b.how.length - a.how.length; });
|
|
365
|
-
return { table: hits[0].t, how: hits[0].how, candidates: hits.map(function (h) { return h.t.name; }) };
|
|
395
|
+
if (headOnly.length === 1) {
|
|
396
|
+
return { table: headOnly[0], how: '表头指纹', candidates: [headOnly[0].name] };
|
|
366
397
|
}
|
|
367
|
-
if (headOnly.length === 1) return { table: headOnly[0], how: '表头指纹', candidates: [headOnly[0].name] };
|
|
368
398
|
return { table: null, how: '未识别', candidates: headOnly.map(function (t) { return t.name; }) };
|
|
369
399
|
}
|
|
370
400
|
|
|
@@ -506,6 +536,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
506
536
|
r2: r2
|
|
507
537
|
};
|
|
508
538
|
});
|
|
539
|
+
|
|
509
540
|
</script>
|
|
510
541
|
<script>/* ============================================================================
|
|
511
542
|
* 自更新:启动查最新版 → 下载校验 → 内存原地替换(不写磁盘)
|
|
@@ -836,43 +867,80 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
836
867
|
return new Promise(function (resolve) {
|
|
837
868
|
var fr = new FileReader();
|
|
838
869
|
fr.onload = function () {
|
|
870
|
+
var items = [];
|
|
839
871
|
try {
|
|
840
872
|
var wb = XLSX.read(new Uint8Array(fr.result), { type: 'array', cellDates: true });
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
873
|
+
log(' ' + f.name + ':共 ' + wb.SheetNames.length + ' 个工作表,逐个识别…');
|
|
874
|
+
|
|
875
|
+
wb.SheetNames.forEach(function (sn) {
|
|
876
|
+
var aoa;
|
|
877
|
+
try {
|
|
878
|
+
aoa = XLSX.utils.sheet_to_json(wb.Sheets[sn], { header: 1, raw: true, defval: null });
|
|
879
|
+
} catch (e) { return; }
|
|
880
|
+
if (!aoa || !aoa.length) return;
|
|
881
|
+
|
|
882
|
+
var hi = findHeaderRow(aoa);
|
|
883
|
+
var header = (aoa[hi] || []).map(function (c) { return c == null ? '' : String(c).trim(); });
|
|
884
|
+
var rows = aoa.slice(hi + 1);
|
|
885
|
+
// 只有含"单位名称"的才是明细表;汇总/透视那种没有这列
|
|
886
|
+
if (Core.findCol(header, ['单位名称']) < 0) {
|
|
887
|
+
if (rows.length > 1) log(' ' + sn + ':非明细表,跳过', 'dim');
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
var det = Core.detectTable(f.name, header, STATE.cfg, sn);
|
|
892
|
+
if (det.skipped) { log(' ' + sn + ':' + det.how, 'dim'); return; }
|
|
893
|
+
|
|
894
|
+
var moneyCol = det.table ? Core.findCol(header, [det.table.money]) : -1;
|
|
895
|
+
var docCol = det.table ? Core.findCol(header, [det.table.doc]) : -1;
|
|
896
|
+
var sum = 0, cnt = 0;
|
|
897
|
+
if (moneyCol >= 0) {
|
|
898
|
+
rows.forEach(function (r) {
|
|
899
|
+
var d = docCol >= 0 ? Core.norm(r[docCol]) : '';
|
|
900
|
+
if (docCol >= 0 && (!d || d.indexOf('合计') >= 0 || d.indexOf('总计') >= 0)) return;
|
|
901
|
+
if (r[moneyCol] != null && r[moneyCol] !== '') { sum += Core.toNum(r[moneyCol]); cnt++; }
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
items.push({
|
|
905
|
+
name: f.name, size: f.size, sheet: sn, header: header, rows: rows,
|
|
906
|
+
table: det.table, how: det.how, moneySum: sum, count: cnt
|
|
855
907
|
});
|
|
908
|
+
log(' ' + sn + ' → ' + (det.table ? det.table.name + '(' + det.how + ',' + cnt + ' 行)'
|
|
909
|
+
: '未识别,可在列表里指定或忽略'), det.table ? 'ok' : 'warn');
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
if (!items.length) {
|
|
913
|
+
log(' ' + f.name + ':没有找到可汇总的明细表', 'warn');
|
|
914
|
+
items.push({ name: f.name, size: f.size, sheet: '(无)', table: null, count: 0,
|
|
915
|
+
moneySum: 0, error: '未找到明细表' });
|
|
856
916
|
}
|
|
857
|
-
resolve({ name: f.name, size: f.size, sheet: sn, header: header, rows: rows,
|
|
858
|
-
table: det.table, how: det.how, moneySum: sum, count: cnt });
|
|
859
|
-
log(' → ' + (det.table ? det.table.name + '(' + det.how + ',' + cnt + ' 行)'
|
|
860
|
-
: '未能识别,请在列表里手动指定'), det.table ? 'ok' : 'warn');
|
|
861
917
|
} catch (e) {
|
|
862
918
|
log(' 解析失败:' + (e && e.message || e), 'bad');
|
|
863
|
-
|
|
919
|
+
items.push({ name: f.name, size: f.size, sheet: '(整份)', error: String(e && e.message || e), table: null });
|
|
864
920
|
}
|
|
921
|
+
resolve(items);
|
|
865
922
|
};
|
|
866
|
-
fr.onerror = function () { resolve({ name: f.name, error: '读取失败', table: null }); };
|
|
923
|
+
fr.onerror = function () { resolve([{ name: f.name, sheet: '(整份)', error: '读取失败', table: null }]); };
|
|
867
924
|
fr.readAsArrayBuffer(f);
|
|
868
925
|
});
|
|
869
926
|
});
|
|
870
927
|
|
|
871
|
-
Promise.all(jobs).then(function (
|
|
872
|
-
list.forEach(function (
|
|
928
|
+
Promise.all(jobs).then(function (lists) {
|
|
929
|
+
lists.forEach(function (list) { list.forEach(function (it) { STATE.files.push(it); }); });
|
|
873
930
|
STATE.busy = false;
|
|
931
|
+
|
|
932
|
+
// 从文件名推断账期(如 202610统计-DPS6.xlsx → 202610)
|
|
933
|
+
var tagEl = $('periodTag');
|
|
934
|
+
if (!tagEl.value) {
|
|
935
|
+
for (var i = 0; i < arr.length; i++) {
|
|
936
|
+
var m = /(20\d{4})/.exec(arr[i].name);
|
|
937
|
+
if (m) { tagEl.value = m[1]; break; }
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
874
941
|
renderFiles();
|
|
875
|
-
|
|
942
|
+
var okCount = STATE.files.filter(function (f) { return f.table && !f.error; }).length;
|
|
943
|
+
log('共 ' + STATE.files.length + ' 个明细表就绪,其中 ' + okCount + ' 个已识别', 'ok');
|
|
876
944
|
});
|
|
877
945
|
}
|
|
878
946
|
|
|
@@ -888,9 +956,10 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
888
956
|
}).join('');
|
|
889
957
|
tr.innerHTML =
|
|
890
958
|
'<td>' + esc(f.name) + '</td>' +
|
|
891
|
-
'<td
|
|
892
|
-
'<td
|
|
893
|
-
|
|
959
|
+
'<td>' + esc(f.sheet || '') + '</td>' +
|
|
960
|
+
'<td><select data-i="' + i + '"><option value="">(忽略,不计入)</option>' + op + '</select></td>' +
|
|
961
|
+
'<td>' + (f.error ? '<span class="tag bad">' + esc(f.error) + '</span>'
|
|
962
|
+
: (f.table ? '<span class="tag ok">' + esc(f.how) + '</span>' : '<span class="tag warn">待指定</span>')) + '</td>' +
|
|
894
963
|
'<td class="n">' + (f.count != null ? f.count : '') + '</td>' +
|
|
895
964
|
'<td class="n">' + (f.moneySum != null ? fmt(f.moneySum) : '') + '</td>' +
|
|
896
965
|
'<td><button class="btn sm" data-del="' + i + '">移除</button></td>';
|
|
@@ -924,7 +993,11 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
924
993
|
function build() {
|
|
925
994
|
var usable = STATE.files.filter(function (f) { return f.table && !f.error; });
|
|
926
995
|
if (!usable.length) { log('没有可汇总的文件', 'bad'); return; }
|
|
927
|
-
|
|
996
|
+
var kinds = {};
|
|
997
|
+
usable.forEach(function (f) { kinds[f.table.key] = (kinds[f.table.key] || 0) + 1; });
|
|
998
|
+
if (Object.keys(kinds).length < 5) {
|
|
999
|
+
log('提示:只识别到 ' + Object.keys(kinds).length + ' 类明细表(完整口径是 5 类:销售未结算 / 销售挂账 / 维修挂账 / 完工未结算 / 在修)', 'warn');
|
|
1000
|
+
}
|
|
928
1001
|
|
|
929
1002
|
STATE.busy = true;
|
|
930
1003
|
var res = Core.summarize(usable.map(function (f) {
|
package/package.json
CHANGED