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 CHANGED
@@ -1,3 +1,3 @@
1
1
  # wwt-ar-summary
2
2
 
3
- 1.0.0
3
+ 1.1.0
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.0.0</span>
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:190px">识别为</th><th style="width:110px">状态</th>
135
- <th style="width:80px">行数</th><th style="width:150px">金额合计</th><th style="width:70px"></th></tr></thead>
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.0.0',
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
- { key: 'xz', name: '销售已挂账未清账', slot: 3, fileKw: ['销售挂账', '已挂账未清账'],
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
- if (byName && byHead) hits.push({ t: t, how: '文件名+表头' });
354
- else if (byName) hits.push({ t: t, how: '文件名' });
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 (hits.length > 1) {
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
- var sn = wb.SheetNames[0];
842
- var aoa = XLSX.utils.sheet_to_json(wb.Sheets[sn], { header: 1, raw: true, defval: null });
843
- var hi = findHeaderRow(aoa);
844
- var header = (aoa[hi] || []).map(function (c) { return c == null ? '' : String(c).trim(); });
845
- var rows = aoa.slice(hi + 1);
846
- var det = Core.detectTable(f.name, header, STATE.cfg);
847
- var moneyCol = det.table ? Core.findCol(header, [det.table.money]) : -1;
848
- var sum = 0, cnt = 0;
849
- if (moneyCol >= 0) {
850
- rows.forEach(function (r) {
851
- var doc = det.table ? Core.findCol(header, [det.table.doc]) : -1;
852
- if (doc >= 0 && Core.norm(r[doc]).indexOf('合计') >= 0) return;
853
- if (doc >= 0 && Core.norm(r[doc]) === '') return;
854
- if (r[moneyCol] != null && r[moneyCol] !== '') { sum += Core.toNum(r[moneyCol]); cnt++; }
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
- resolve({ name: f.name, size: f.size, error: String(e && e.message || e), table: null });
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 (list) {
872
- list.forEach(function (item) { STATE.files.push(item); });
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
- log('共 ' + STATE.files.length + ' 个文件就绪', 'ok');
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><select data-i="' + i + '"><option value="">(未指定)</option>' + op + '</select></td>' +
892
- '<td>' + (f.error ? '<span class="tag bad">读取失败</span>'
893
- : (f.table ? '<span class="tag ok">' + esc(f.table.name) + '</span>' : '<span class="tag warn">待指定</span>')) + '</td>' +
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
- if (usable.length < 5) log('注意:只提供了 ' + usable.length + ' 张表(正常是 5 张)', 'warn');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wwt-ar-summary",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "应收帐款汇总表 · 离线单文件工具(自动更新)",
5
5
  "main": "ar-summary.html",
6
6
  "files": [
package/version.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wwt-ar-summary",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "file": "ar-summary.html",
5
5
  "updated": "2026-11-14"
6
6
  }