wwt-ar-summary 1.0.1 → 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.1
3
+ 1.1.0
package/ar-summary.html CHANGED
@@ -112,7 +112,7 @@ aside#cfgPanel textarea {
112
112
 
113
113
  <header>
114
114
  <h1>应收帐款汇总表</h1>
115
- <span class="sub" id="verLine">v1.0.1</span>
115
+ <span class="sub" id="verLine">v1.1.0</span>
116
116
  <span class="grow"></span>
117
117
  <span id="updateMsg"></span>
118
118
  <button class="btn" id="btnCheck">检查更新</button>
@@ -132,8 +132,9 @@ aside#cfgPanel textarea {
132
132
  <div class="card" id="filePanel" style="display:none">
133
133
  <h2>2 · 确认识别结果</h2>
134
134
  <table class="files">
135
- <thead><tr><th>文件</th><th style="width:190px">识别为</th><th style="width:110px">状态</th>
136
- <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>
137
138
  <tbody id="fileBody"></tbody>
138
139
  </table>
139
140
  <div style="margin-top:14px; display:flex; gap:10px; align-items:center">
@@ -192,7 +193,7 @@ aside#cfgPanel textarea {
192
193
 
193
194
  <script>
194
195
  window.APP_META = {
195
- appVersion: '1.0.1',
196
+ appVersion: '1.1.0',
196
197
  appPkg: 'wwt-ar-summary',
197
198
  appUpdateFile: 'ar-summary.html',
198
199
  updateSources: []
@@ -285,15 +286,18 @@ https://github.com/nodeca/pako/blob/main/LICENSE
285
286
  '广州维修基地', '加工部', '阀体车间', '研发中心'],
286
287
  // 类型列(出库单类 / 收费类型)含这些词 → 内部
287
288
  typeKeywords: ['复检', 'WWT再制造', '调库销售单'],
289
+ // 表名/sheet 名含这些词 → 不是明细源表(汇总、透视、库存、入库、调拨都不参与应收)
290
+ skipKeywords: ['汇总', '统计', '透视', '库存', '入库', '调拨', '三项费用', '说明'],
288
291
  // 五张源表:文件名关键词 + 表头指纹 + 金额列 + 单号列 + 归属列
289
292
  tables: [
290
- { key: 'xs', name: '销售未结算', slot: 2, fileKw: ['出库未确认', '出库未确认单', '销售未结算'],
293
+ { key: 'xs', name: '销售未结算', slot: 2, fileKw: ['出库未确认', '销售未确认', '销售未结算'],
291
294
  must: ['出库单类'], money: '实收金额', doc: '出库单号', typeCol: '出库单类' },
292
- { key: 'xz', name: '销售已挂账未清账', slot: 3, fileKw: ['销售挂账', '已挂账未清账'],
295
+ // 关键词必须带"销售"前缀:'已挂账未清账' 这种通用词会把"维修已挂账未清账"也抢过来
296
+ { key: 'xz', name: '销售已挂账未清账', slot: 3, fileKw: ['销售挂账', '销售已挂账', '销售未清账'],
293
297
  must: ['出库单类'], money: '实收金额', doc: '出库单号', typeCol: '出库单类' },
294
- { key: 'wx', name: '维修已挂账未清账', slot: 3, fileKw: ['维修挂账', '维修已挂账'],
298
+ { key: 'wx', name: '维修已挂账未清账', slot: 3, fileKw: ['维修挂账', '维修已挂账', '维修未清账'],
295
299
  must: ['客户实收额'], money: '客户实收额', doc: '工单号', typeCol: '收费类型' },
296
- { key: 'wg', name: '完工未结算', slot: 0, fileKw: ['完工未结算', '完工'],
300
+ { key: 'wg', name: '完工未结算', slot: 0, fileKw: ['完工未结算', '已完工'],
297
301
  must: ['A结算金额'], money: 'A结算金额', doc: '工单号', typeCol: '收费类型' },
298
302
  { key: 'zx', name: '在修未结算', slot: 1, fileKw: ['在修'],
299
303
  must: ['A结算金额'], money: 'A结算金额', doc: '工单号', typeCol: '收费类型' }
@@ -344,28 +348,53 @@ https://github.com/nodeca/pako/blob/main/LICENSE
344
348
  * 返回 { table: 命中的表定义, how: '文件名'|'表头指纹', candidates: [...] }
345
349
  * 文件名优先(销售两张 / 完工与在修 的表头相同,只能靠文件名区分)
346
350
  */
347
- function detectTable(fileName, header, cfg) {
351
+ function detectTable(fileName, header, cfg, sheetName) {
348
352
  cfg = cfg || DEFAULT_CFG;
349
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
+
350
372
  var hits = [];
351
373
  cfg.tables.forEach(function (t) {
352
- var byName = t.fileKw.some(function (k) { return fn.indexOf(norm(k).toLowerCase()) >= 0; });
353
374
  var byHead = t.must.every(function (m) { return findCol(header, [m]) >= 0; });
354
- if (byName && byHead) hits.push({ t: t, how: '文件名+表头' });
355
- 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 });
356
380
  });
357
- if (hits.length === 1) return { table: hits[0].t, how: hits[0].how, candidates: hits.map(function (h) { return h.t.name; }) };
358
381
 
359
- // 文件名没命中(或命中了多张):用表头指纹互斥判断
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
+ // 文件名没命中:仅当表头指纹唯一时才自动认定,否则交给用户指定
360
392
  var headOnly = cfg.tables.filter(function (t) {
361
393
  return t.must.every(function (m) { return findCol(header, [m]) >= 0; });
362
394
  });
363
- if (hits.length > 1) {
364
- // 例如 "出库未确认" 同时含 "销售未结算" 关键词,取匹配关键词最长的
365
- hits.sort(function (a, b) { return b.how.length - a.how.length; });
366
- 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] };
367
397
  }
368
- if (headOnly.length === 1) return { table: headOnly[0], how: '表头指纹', candidates: [headOnly[0].name] };
369
398
  return { table: null, how: '未识别', candidates: headOnly.map(function (t) { return t.name; }) };
370
399
  }
371
400
 
@@ -507,6 +536,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
507
536
  r2: r2
508
537
  };
509
538
  });
539
+
510
540
  </script>
511
541
  <script>/* ============================================================================
512
542
  * 自更新:启动查最新版 → 下载校验 → 内存原地替换(不写磁盘)
@@ -837,43 +867,80 @@ https://github.com/nodeca/pako/blob/main/LICENSE
837
867
  return new Promise(function (resolve) {
838
868
  var fr = new FileReader();
839
869
  fr.onload = function () {
870
+ var items = [];
840
871
  try {
841
872
  var wb = XLSX.read(new Uint8Array(fr.result), { type: 'array', cellDates: true });
842
- var sn = wb.SheetNames[0];
843
- var aoa = XLSX.utils.sheet_to_json(wb.Sheets[sn], { header: 1, raw: true, defval: null });
844
- var hi = findHeaderRow(aoa);
845
- var header = (aoa[hi] || []).map(function (c) { return c == null ? '' : String(c).trim(); });
846
- var rows = aoa.slice(hi + 1);
847
- var det = Core.detectTable(f.name, header, STATE.cfg);
848
- var moneyCol = det.table ? Core.findCol(header, [det.table.money]) : -1;
849
- var sum = 0, cnt = 0;
850
- if (moneyCol >= 0) {
851
- rows.forEach(function (r) {
852
- var doc = det.table ? Core.findCol(header, [det.table.doc]) : -1;
853
- if (doc >= 0 && Core.norm(r[doc]).indexOf('合计') >= 0) return;
854
- if (doc >= 0 && Core.norm(r[doc]) === '') return;
855
- 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
856
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: '未找到明细表' });
857
916
  }
858
- resolve({ name: f.name, size: f.size, sheet: sn, header: header, rows: rows,
859
- table: det.table, how: det.how, moneySum: sum, count: cnt });
860
- log(' → ' + (det.table ? det.table.name + '(' + det.how + ',' + cnt + ' 行)'
861
- : '未能识别,请在列表里手动指定'), det.table ? 'ok' : 'warn');
862
917
  } catch (e) {
863
918
  log(' 解析失败:' + (e && e.message || e), 'bad');
864
- 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 });
865
920
  }
921
+ resolve(items);
866
922
  };
867
- fr.onerror = function () { resolve({ name: f.name, error: '读取失败', table: null }); };
923
+ fr.onerror = function () { resolve([{ name: f.name, sheet: '(整份)', error: '读取失败', table: null }]); };
868
924
  fr.readAsArrayBuffer(f);
869
925
  });
870
926
  });
871
927
 
872
- Promise.all(jobs).then(function (list) {
873
- 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); }); });
874
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
+
875
941
  renderFiles();
876
- 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');
877
944
  });
878
945
  }
879
946
 
@@ -889,9 +956,10 @@ https://github.com/nodeca/pako/blob/main/LICENSE
889
956
  }).join('');
890
957
  tr.innerHTML =
891
958
  '<td>' + esc(f.name) + '</td>' +
892
- '<td><select data-i="' + i + '"><option value="">(未指定)</option>' + op + '</select></td>' +
893
- '<td>' + (f.error ? '<span class="tag bad">读取失败</span>'
894
- : (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>' +
895
963
  '<td class="n">' + (f.count != null ? f.count : '') + '</td>' +
896
964
  '<td class="n">' + (f.moneySum != null ? fmt(f.moneySum) : '') + '</td>' +
897
965
  '<td><button class="btn sm" data-del="' + i + '">移除</button></td>';
@@ -925,7 +993,11 @@ https://github.com/nodeca/pako/blob/main/LICENSE
925
993
  function build() {
926
994
  var usable = STATE.files.filter(function (f) { return f.table && !f.error; });
927
995
  if (!usable.length) { log('没有可汇总的文件', 'bad'); return; }
928
- 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
+ }
929
1001
 
930
1002
  STATE.busy = true;
931
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.1",
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.1",
3
+ "version": "1.1.0",
4
4
  "file": "ar-summary.html",
5
5
  "updated": "2026-11-14"
6
6
  }