wwt-certificate-tool 1.2.35 → 1.2.49

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/dist/app.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <title>WWT 金蝶凭证工具 · 纯前端版 P1.2.35</title>
6
+ <title>WWT 金蝶凭证工具 · 纯前端版 P1.2.49</title>
7
7
  <style>*{box-sizing:border-box;margin:0;padding:0}
8
8
  body{font-family:"Microsoft YaHei","PingFang SC",-apple-system,Segoe UI,sans-serif;font-size:13px;color:#1f2328;background:#f4f6f9;line-height:1.6}
9
9
  h1{font-size:17px;font-weight:600}
@@ -162,6 +162,7 @@ td.num{text-align:right;font-variant-numeric:tabular-nums}
162
162
  td[contenteditable="true"]:focus{outline:2px solid #2f6feb;background:#fff}
163
163
  tr.bad td{background:#fdf1f1}
164
164
  tr.bad:hover td{background:#fbe9e9}
165
+ .dim-missing{color:#a40e26;font-weight:600;white-space:nowrap}
165
166
  .tag{display:inline-block;padding:1px 6px;border-radius:9px;font-size:11px}
166
167
  .tag.g{background:#eaf6ec;color:#116329}
167
168
  .tag.r{background:#fdf1f1;color:#a40e26}
@@ -206,6 +207,14 @@ tr.bad:hover td{background:#fbe9e9}
206
207
  .sec input,.sec select{padding:4px 7px;border:1px solid #d0d7de;border-radius:5px;font-size:12px;font-family:inherit}
207
208
  .sec input:focus,.sec select:focus{outline:none;border-color:#2f6feb}
208
209
  .w30{width:30px}.w60{width:60px}.w80{width:80px}.w110{width:110px}.w140{width:140px}.w180{width:180px}.w240{width:240px}.grow{flex:1;min-width:130px}
210
+ /* 【P1.2.36】全局排除的字段输入框:下拉箭头 + 提示色,让「点选」一目了然 */
211
+ input[data-f="field"][list^="dlGe_"]{
212
+ background:#fffbea url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 16 16'><path fill='%23866a1f' d='M3 6l5 5 5-5z'/></svg>") no-repeat right 6px center;
213
+ background-size:10px 10px;cursor:pointer;
214
+ border-color:#e0c97a;
215
+ }
216
+ input[data-f="field"][list^="dlGe_"]:focus{background-color:#fff7d4;border-color:#e0a526}
217
+ input[data-f="field"][list^="dlGe_"]::placeholder{color:#a8861f;font-style:italic}
209
218
 
210
219
  /* ============================================================
211
220
  * 📋 通用表格编辑器(grid)
@@ -307,7 +316,7 @@ tr.bad:hover td{background:#fbe9e9}
307
316
  <div class="brand">
308
317
  <span class="logo">WWT</span>
309
318
  <div>
310
- <h1>金蝶凭证工具 <span class="ver">P1.2.35</span></h1>
319
+ <h1>金蝶凭证工具 <span class="ver">P1.2.49</span></h1>
311
320
  <div class="sub">纯前端离线版 · 双击即用 · 数据只存在本机,不上传任何服务器</div>
312
321
  </div>
313
322
  </div>
@@ -340,7 +349,7 @@ tr.bad:hover td{background:#fbe9e9}
340
349
  <!-- ① 上传 -->
341
350
  <section class="pane on" id="tab-upload">
342
351
  <div class="notice">
343
- <b>第一次用请按顺序来:</b>先传 <b>9 个基础数据表</b>(一次传完,以后不用再传)→ 再传 <b>本月 8 个业务数据表</b> → 去「② 映射配置」改月份 → 「④ 生成凭证」→「⑦ 导出金蝶」。
352
+ <b>第一次用请按顺序来:</b>先传 <b>9 个基础数据表</b>(一次传完,以后不用再传)→ 再传 <b>本月 9 个业务数据表</b> → 去「② 映射配置」改月份 → 「④ 生成凭证」→「⑦ 导出金蝶」。
344
353
  <br><b>业务数据表必须是 .xls 原文件,不要改名、不要另存为 .xlsx。</b>
345
354
  </div>
346
355
  <div class="rowacts">
@@ -349,7 +358,7 @@ tr.bad:hover td{background:#fbe9e9}
349
358
  </div>
350
359
  <h3>基础数据(9 个 · 一次上传长期有效)<span class="tip" style="margin-left:8px">支持 <b>点击按钮</b> 或 <b>把 Excel 文件拖到这里</b></span></h3>
351
360
  <div class="grid dropzone" id="grpBase" data-kind="base"></div>
352
- <h3>本月业务数据(8 个 · 每月重传)<span class="tip" style="margin-left:8px">支持 <b>点击按钮</b> 或 <b>把 .xls 文件拖到这里</b></span></h3>
361
+ <h3>本月业务数据(9 个 · 每月重传)<span class="tip" style="margin-left:8px">支持 <b>点击按钮</b> 或 <b>把 .xls 文件拖到这里</b></span></h3>
353
362
  <div class="grid dropzone" id="grpBiz" data-kind="biz"></div>
354
363
  <div id="uploadLog" class="log"></div>
355
364
  <input type="file" id="pickBase" multiple accept=".xls,.xlsx" style="display:none">
@@ -514,6 +523,8 @@ tr.bad:hover td{background:#fbe9e9}
514
523
  <datalist id="dlSubjects"></datalist>
515
524
  <datalist id="dlAuxFixed"></datalist>
516
525
  <datalist id="dlBizFields"></datalist>
526
+ <!-- 【P1.2.38】金额字段专用下拉:在业务字段基础上加上 __AUTO_DIFF__ 自动补差额等常量 -->
527
+ <datalist id="dlAmountFields"></datalist>
517
528
  <datalist id="dlTransform"></datalist>
518
529
  <datalist id="dlAuxValue"></datalist>
519
530
 
@@ -814,6 +825,7 @@ window.WWT_CONST = (function () {
814
825
  "FOPPACCTNAME","FITEMNUM","FITEMNAME","FSUBITEMNUM","FSUBITEMNAME","FCURRENCYNUM","FCURRENCYNAME",
815
826
  "FAMOUNTFOR","FAMOUNT"];
816
827
  const MISSING_HEAD = ["来源单据","业务编号","科目","维度","名称"];
828
+ const SUBJ_MISS_HEAD = ["科目编码","科目名称","出现次数","涉及凭证号"];
817
829
 
818
830
  /* ---------- 16 种辅助核算维度 ---------- */
819
831
  // 草稿列名:[名称列, 编码列]
@@ -862,6 +874,8 @@ window.WWT_CONST = (function () {
862
874
  "调拨未确认":{"file":"调拨未确认.xls","sheet":"调拨总","datecol":"出库日期","party":"单位名称","partytype":"客户","no":"出库单号"},
863
875
  "在修车":{"file":"在修车.xls","sheet":"在修车","datecol":"进厂日期","party":"单位名称","partytype":"客户","no":"工单号"},
864
876
  "已完工未结算":{"file":"已完工未结算.xls","sheet":"完工未结","datecol":"结算日期","party":"单位名称","partytype":"客户","no":"工单号"},
877
+ // 【P1.2.39】新增第 9 个业务源:维修已结算(按实际表头 维修已结算.xls 校准)
878
+ "维修已结算":{"file":"维修已结算.xls","sheet":"维修已结算","datecol":"结算日期","party":"单位名称","partytype":"客户","no":"工单号"},
865
879
  };
866
880
  const BIZ_SLOTS = [
867
881
  {key:"入库已确认", label:"入库已确认(采购-已确认)"},
@@ -872,9 +886,13 @@ window.WWT_CONST = (function () {
872
886
  {key:"调拨未确认", label:"调拨未确认"},
873
887
  {key:"在修车", label:"在修车(维修在修)"},
874
888
  {key:"已完工未结算", label:"已完工未结算(维修完工)"},
889
+ {key:"维修已结算", label:"维修已结算(维修已结)"},
875
890
  ];
876
891
  // 对碰检测只覆盖"已确认/已结算"的来源;已完工未结算 用户要求不参与(2026-09-09)
877
- const DETECT_ALLOWED_SOURCES = ["入库已确认","销售已确认","调拨已确认"];
892
+ // 【P1.2.39】新增 维修已结算(已结算类,按设计意图纳入对碰检测)
893
+ // 【P1.2.48】对碰检测增加「入库未结算」:用户要求付款方式=挂账的入库数据,
894
+ // 即使供应商尚未在基础供应商表建档,也要报出来,便于补档案/补映射。
895
+ const DETECT_ALLOWED_SOURCES = ["入库已确认","入库未结算","销售已确认","调拨已确认","维修已结算"];
878
896
  // v1.2.20:只核对 挂账 / 对账S / 冲转S 三类(去掉 对帐F / 冲转F)
879
897
  const DETECT_ALLOWED_PAY = ["对帐S","挂帐","冲转S"];
880
898
 
@@ -966,7 +984,7 @@ window.WWT_CONST = (function () {
966
984
  const JOIN_PREFIX = ["客户","供应商","部门","员工","其他往来"];
967
985
 
968
986
  return {
969
- KINGDEE_COLUMNS, KINGDEE_NAMES, CASHFLOW_HEAD, MISSING_HEAD,
987
+ KINGDEE_COLUMNS, KINGDEE_NAMES, CASHFLOW_HEAD, MISSING_HEAD, SUBJ_MISS_HEAD,
970
988
  DIM_DISPLAY, DIM_COLUMNS, DIM_LIST, STRICT_DIMS,
971
989
  SOURCE_META, BIZ_SLOTS, BASE_SLOTS,
972
990
  DETECT_ALLOWED_SOURCES, DETECT_ALLOWED_PAY,
@@ -2473,11 +2491,19 @@ window.WWT_ENGINE = (function () {
2473
2491
  if (m && clean(m.whcode)) return { code: clean(m.whcode), name: clean(m.whname) || clean(m.whcode) };
2474
2492
  return { code: "LJ", name: "LJ仓" };
2475
2493
  }
2476
- // 维度反查:六表依次 + 白名单业务列
2494
+ // 维度反查:优先当前维度对应的基础表,再兜底其他表
2495
+ // 【P1.2.45】修复同名单位跨表串码:供应商维度先查供应商表,客户维度先查客户表。
2477
2496
  function lookupDimCode(dim, name, idx) {
2478
2497
  if (!name) return "";
2479
2498
  const order = ["其他往来", "客户", "供应商", "部门", "员工", "费用项目"];
2499
+ // 1) 当前维度优先
2500
+ if (dim && idx.tables[dim]) {
2501
+ const rec = idx.tables[dim][name] || ciGetObj(idx.tables[dim], name);
2502
+ if (rec && rec.code) return rec.code;
2503
+ }
2504
+ // 2) 兜底:保持原六表顺序(兼容历史模糊匹配场景)
2480
2505
  for (const d of order) {
2506
+ if (d === dim) continue; // 已查过
2481
2507
  const t = idx.tables[d];
2482
2508
  if (!t) continue;
2483
2509
  const rec = t[name] || ciGetObj(t, name);
@@ -2517,15 +2543,27 @@ window.WWT_ENGINE = (function () {
2517
2543
  code = lookupDimCode(d, name, idx) || lookupDimByField(d, vf, name, idx);
2518
2544
  }
2519
2545
  } else {
2520
- name = applyTransform(getField(row, vf, curSrc, idx), aux.transform);
2546
+ let raw = getField(row, vf, curSrc, idx);
2547
+ // 【P1.2.43】UI 的 valueField 下拉同时列出「业务源列名」和「基础表名称/编码(可当固定值用)」。
2548
+ // 当用户选了基础表里的固定名称/编码、且它不是当前业务行的列名时,直接当作固定值反查编码。
2549
+ let vfAsFixed = false;
2550
+ if (!clean(raw) && vf && vf.indexOf(".") < 0 && !Object.prototype.hasOwnProperty.call(row || {}, vf)) {
2551
+ raw = vf;
2552
+ vfAsFixed = true;
2553
+ }
2554
+ name = applyTransform(raw, aux.transform);
2521
2555
  if (/\.编码$/.test(vf) && name) { code = name; }
2522
2556
  if (name) {
2523
2557
  if (d === "仓库") {
2524
2558
  const w = lookupWh(name);
2525
2559
  code = w.code; name = w.name;
2526
2560
  } else if (d === "收入类型" || d === "成本类型") {
2527
- // 硬约束:只认「出库单类 / 单类」,盘亏清空,其它来源字段一律清空
2528
- if (vf === "出库单类" || vf === "单类") {
2561
+ // 硬约束:只认「出库单类 / 单类」,盘亏清空,其它来源字段一律清空;
2562
+ // 【P1.2.41】维修已结算没有「单类」,允许用「收费类型」映射成本类型;
2563
+ // 【P1.2.42】同一例外扩展到 收入类型(收费类型 → 收入类型 也可走映射表)。
2564
+ const allowVf = (vf === "出库单类" || vf === "单类") ||
2565
+ (curSrc === "维修已结算" && vf === "收费类型");
2566
+ if (allowVf) {
2529
2567
  if (name.indexOf("盘亏") >= 0) { name = ""; code = ""; }
2530
2568
  else {
2531
2569
  const r = lookupTypeDim(d, name, vf, idx);
@@ -2538,7 +2576,17 @@ window.WWT_ENGINE = (function () {
2538
2576
  code = lookupDimCode(d, name, idx) || lookupDimByField(d, vf, name, idx);
2539
2577
  if (!code) {
2540
2578
  const t = idx.tables[d];
2541
- if (t) { const rec = t[name] || ciGetObj(t, name); if (rec && rec.code) code = rec.code; }
2579
+ if (t) {
2580
+ const rec = t[name] || ciGetObj(t, name);
2581
+ if (rec && rec.code) code = rec.code;
2582
+ else if (vfAsFixed) {
2583
+ // 用户可能选了基础表编码当固定值,按编码反查名称
2584
+ for (const k in t) {
2585
+ if (k.indexOf("CODE::") === 0) continue;
2586
+ if (clean(t[k].code) === clean(name)) { code = t[k].code; name = t[k].name; break; }
2587
+ }
2588
+ }
2589
+ }
2542
2590
  }
2543
2591
  }
2544
2592
  }
@@ -2555,6 +2603,16 @@ window.WWT_ENGINE = (function () {
2555
2603
  }
2556
2604
  out[d] = { name: name, code: code };
2557
2605
  }
2606
+
2607
+ // 【P1.2.48】存货类科目(1403 开头)若规则未配置仓库维度,默认带出厂牌 LJ/零件(LJ仓),
2608
+ // 避免金蝶报「必要录维度未录入:仓库」。若用户已显式配置仓库维度(即使值空)则不覆盖。
2609
+ if (line && String(line.subject || "").startsWith("1403")) {
2610
+ const wh = out["仓库"];
2611
+ if (!wh || (!clean(wh.code) && !clean(wh.name))) {
2612
+ out["仓库"] = { name: "LJ仓", code: "LJ" };
2613
+ }
2614
+ }
2615
+
2558
2616
  return out;
2559
2617
  }
2560
2618
 
@@ -2603,7 +2661,7 @@ window.WWT_ENGINE = (function () {
2603
2661
  function truncateBiz(s, sep) {
2604
2662
  const arr = clean(s).split(sep).filter(Boolean);
2605
2663
  if (arr.length <= 3) return arr.join(sep);
2606
- return arr.slice(0, 3).join(sep) + sep + "" + (arr.length - 3) + "张";
2664
+ return arr.slice(0, 3).join(sep) + sep + "等共" + arr.length + "张";
2607
2665
  }
2608
2666
  function buildSummaryCtx(items, rule, curSrc) {
2609
2667
  const meta = C.SOURCE_META[curSrc] || {};
@@ -2752,16 +2810,28 @@ window.WWT_ENGINE = (function () {
2752
2810
 
2753
2811
  // Step A:分桶(决定几张凭证)
2754
2812
  const genMode = opts.modeOverride || rule.genMode || "一对一";
2813
+ const gb = opts.groupByOverride || rule.genGroupBy || "";
2755
2814
  let buckets = [];
2756
2815
  if (genMode === "一对一") {
2757
2816
  buckets = rowsIn.map(it => [it]);
2758
2817
  } else if (genMode === "全部汇总") {
2759
- buckets = [rowsIn];
2818
+ // 【P1.2.38】全部汇总也可按分组字段拆证;若分组字段为空/默认单位名称,则保持原本「单张凭证汇总全部」行为
2819
+ if (gb && gb !== "单位名称") {
2820
+ const m = new Map();
2821
+ for (const it of rowsIn) {
2822
+ const v = clean(it.row[gb]) || "(空)";
2823
+ if (!m.has(v)) m.set(v, []);
2824
+ m.get(v).push(it);
2825
+ }
2826
+ buckets = Array.from(m.values());
2827
+ } else {
2828
+ buckets = [rowsIn];
2829
+ }
2760
2830
  } else {
2761
- const gb = opts.groupByOverride || rule.genGroupBy || "单位名称";
2831
+ const useGb = gb || "单位名称";
2762
2832
  const m = new Map();
2763
2833
  for (const it of rowsIn) {
2764
- const v = clean(it.row[gb]) || "(空)";
2834
+ const v = clean(it.row[useGb]) || "(空)";
2765
2835
  if (!m.has(v)) m.set(v, []);
2766
2836
  m.get(v).push(it);
2767
2837
  }
@@ -2777,11 +2847,15 @@ window.WWT_ENGINE = (function () {
2777
2847
  stats.vouchers++;
2778
2848
 
2779
2849
  // Step B:sub_bucket = group_key值 × 分支lines
2780
- const groupKeyField = (genMode === "一对一") ? null : "单位名称";
2850
+ // 【P1.2.40】全部汇总且按非单位名称字段拆证时,取消子桶按单位名称细分,实现真正的按该字段汇总
2851
+ // 【P1.2.46】若规则配置了「拆分字段」,子桶按拆分字段拆分,不再按单位名称细分(实现 1 证内按供应商等字段分录)
2852
+ const groupKeyField = (genMode === "一对一" || splitFs.length > 0) ? null :
2853
+ (genMode === "全部汇总" && gb && gb !== "单位名称") ? null : "单位名称";
2781
2854
  const subMap = new Map();
2782
2855
  for (const it of bucket) {
2783
2856
  const gv = groupKeyField ? (clean(it.row[groupKeyField]) || "(空)") : "";
2784
- const k = JSON.stringify([gv, linesKey(it._lines)]);
2857
+ const splitT = splitFs.map(f => clean(it.row[f]));
2858
+ const k = JSON.stringify([gv, splitT, linesKey(it._lines)]);
2785
2859
  if (!subMap.has(k)) subMap.set(k, { gv: gv, lines: it._lines, items: [], acct: {} });
2786
2860
  subMap.get(k).items.push(it);
2787
2861
  }
@@ -2897,8 +2971,18 @@ window.WWT_ENGINE = (function () {
2897
2971
  * covered:被规则命中(candidate→产出凭证)
2898
2972
  * candidate_missed:满足某条规则全部 = 条件、但未生成凭证(一般 = 缺编码/客户未匹配);P1.2.15 起 generate() 主循环无此中间态,固定 0
2899
2973
  * excluded:既不满足任一规则的 = 条件,也未被命中(设计排除——例:本场景对账S规则不接 冲转F/挂帐)
2974
+ * missing_code_count / missing_code_amount:生成后 _缺失编码 非空的分录(严格维度有名称无编码)
2900
2975
  * 通过明细分桶,让 UI 能清楚告诉用户"漏了哪几单、为什么不原因"。 ---- */
2901
2976
  const amtFields = ["实收金额", "应收金额", "实付金额", "付款金额", "收款金额", "金额"];
2977
+ const missingCodeBySrc = {};
2978
+ for (const e of out) {
2979
+ if (clean(e["_缺失编码"])) {
2980
+ const src = e["来源单据"] || "";
2981
+ if (!missingCodeBySrc[src]) missingCodeBySrc[src] = { count: 0, amount: 0 };
2982
+ missingCodeBySrc[src].count++;
2983
+ missingCodeBySrc[src].amount += num(e["借方金额"]) + num(e["贷方金额"]);
2984
+ }
2985
+ }
2902
2986
  const bySource = [];
2903
2987
  for (const curSrc of Object.keys(usedRows)) {
2904
2988
  const rows = (opts.biz && opts.biz[curSrc]) || [];
@@ -2926,6 +3010,7 @@ window.WWT_ENGINE = (function () {
2926
3010
  金额: amountCol ? num(row[amountCol]) : 0,
2927
3011
  });
2928
3012
  }
3013
+ const mc = missingCodeBySrc[curSrc] || { count: 0, amount: 0 };
2929
3014
  bySource.push({
2930
3015
  source: curSrc,
2931
3016
  total: rows.length,
@@ -2936,6 +3021,8 @@ window.WWT_ENGINE = (function () {
2936
3021
  excluded_amount: round2(excludedAmt),
2937
3022
  excluded_clipped: totalUncovered > 200,
2938
3023
  excluded_rows: exclRows,
3024
+ missing_code_count: mc.count,
3025
+ missing_code_amount: round2(mc.amount),
2939
3026
  unmatched_amount: round2(excludedAmt),
2940
3027
  amount_col: amountCol,
2941
3028
  never_matched: totalUncovered,
@@ -2980,6 +3067,31 @@ window.WWT_ENGINE = (function () {
2980
3067
  };
2981
3068
  }
2982
3069
 
3070
+ /* ---------------- 导出前科目存在性校验 ----------------
3071
+ * 扫描 rows 的「科目编码」列,对照 idx.subjectExists 判断是否在「会计科目表」中存在。
3072
+ * 不存在的科目金蝶导入必失败(报「科目编码在组织 X 中不存在」)。
3073
+ * 返回 [{code, name, count, vnos:[...]}],按出现次数降序。
3074
+ */
3075
+ function checkSubjects(rows, idx) {
3076
+ const map = {};
3077
+ for (const r of (rows || [])) {
3078
+ const code = clean(r["科目编码"]);
3079
+ if (!code) continue;
3080
+ if (idx && idx.subjectExists && !idx.subjectExists(code)) {
3081
+ if (!map[code]) map[code] = { code: code, name: clean(r["科目名称"] || ""), count: 0, vnos: new Set() };
3082
+ map[code].count++;
3083
+ const vn = clean(r["凭证号"] || "");
3084
+ if (vn) map[code].vnos.add(vn);
3085
+ }
3086
+ }
3087
+ return Object.keys(map).map(k => {
3088
+ const m = map[k];
3089
+ const arr = Array.from(m.vnos);
3090
+ arr.sort((a, b) => (parseFloat(a) || 0) - (parseFloat(b) || 0));
3091
+ return { code: m.code, name: m.name, count: m.count, vnos: arr };
3092
+ }).sort((a, b) => b.count - a.count);
3093
+ }
3094
+
2983
3095
  /* ---------------- 单条规则测试预览(voucher preview)----------------
2984
3096
  * 与 generate() 共用同一套 evalConditions / calcDimValues / leafSubject 逻辑,
2985
3097
  * 只是只跑传入的 rule,并在结果上挂一份"覆盖率审计":
@@ -3066,14 +3178,26 @@ window.WWT_ENGINE = (function () {
3066
3178
  for (const it of rowsIn) it._lines = resolveEffectiveLines(r, it.row, curSrc, idx);
3067
3179
 
3068
3180
  const genMode = r.genMode;
3181
+ const gb = r.genGroupBy || "";
3069
3182
  let buckets = [];
3070
3183
  if (genMode === "一对一") buckets = rowsIn.map(it => [it]);
3071
- else if (genMode === "全部汇总") buckets = [rowsIn];
3072
- else {
3073
- const gb = r.genGroupBy || "单位名称";
3184
+ else if (genMode === "全部汇总") {
3185
+ if (gb && gb !== "单位名称") {
3186
+ const m = new Map();
3187
+ for (const it of rowsIn) {
3188
+ const v = clean(it.row[gb]) || "(空)";
3189
+ if (!m.has(v)) m.set(v, []);
3190
+ m.get(v).push(it);
3191
+ }
3192
+ buckets = Array.from(m.values());
3193
+ } else {
3194
+ buckets = [rowsIn];
3195
+ }
3196
+ } else {
3197
+ const useGb = gb || "单位名称";
3074
3198
  const m = new Map();
3075
3199
  for (const it of rowsIn) {
3076
- const v = clean(it.row[gb]) || "(空)";
3200
+ const v = clean(it.row[useGb]) || "(空)";
3077
3201
  if (!m.has(v)) m.set(v, []);
3078
3202
  m.get(v).push(it);
3079
3203
  }
@@ -3085,7 +3209,8 @@ window.WWT_ENGINE = (function () {
3085
3209
  const curVno = vno++;
3086
3210
  let eidx = 0;
3087
3211
  stats.vouchers++;
3088
- const groupKeyField = (genMode === "一对一") ? null : "单位名称";
3212
+ const groupKeyField = (genMode === "一对一") ? null :
3213
+ (genMode === "全部汇总" && gb && gb !== "单位名称") ? null : "单位名称";
3089
3214
  const subMap = new Map();
3090
3215
  for (const it of bucket) {
3091
3216
  const gv = groupKeyField ? (clean(it.row[groupKeyField]) || "(空)") : "";
@@ -3281,6 +3406,8 @@ window.WWT_ENGINE = (function () {
3281
3406
  // 兼容旧字段(仍叫 never_matched,但其内容仅含真正应处理子集漏命中的行)
3282
3407
  never_matched: candidatesMissed.length,
3283
3408
  unmatched_amount: round2(unmatchedAmt),
3409
+ missing_code_count: missingCodeCount,
3410
+ missing_code_amount: round2(failedAmount),
3284
3411
  amount_col: amountCol,
3285
3412
  never_matched_rows: candidatesMissed,
3286
3413
  excluded_rows: excludedRows,
@@ -3454,7 +3581,7 @@ window.WWT_ENGINE = (function () {
3454
3581
  parseAmtExpr, sumAmtExpr, evalAmtExpr, isAutoDiff, splitFieldsOf,
3455
3582
  calcDimValues, leafSubject, resolveEffectiveLines, lookupTypeDim,
3456
3583
  isExcludedByRule, isExcludedByGlobals,
3457
- generate, balanceCheck, buildKingdee, detect, auditRule,
3584
+ generate, balanceCheck, buildKingdee, detect, auditRule, checkSubjects,
3458
3585
  lookupMap, lookupTypeMap,
3459
3586
  };
3460
3587
  })();
@@ -3478,7 +3605,7 @@ const S = window.__STATE__ = {
3478
3605
  };
3479
3606
 
3480
3607
  /* ---------------- 版本 & 框架自动更新(只更新程序本身,本地数据/规则不动) ---------------- */
3481
- const WWT_VER=(function(){const raw='P1.2.35';return raw.indexOf('__WVER__')>=0?'P1.2.10':raw;})();
3608
+ const WWT_VER=(function(){const raw='P1.2.49';return raw.indexOf('__WVER__')>=0?'P1.2.10':raw;})();
3482
3609
  const UPD_URL_KEY='wwt_fe_updurl';
3483
3610
  // 默认更新源:Gitee 上本项目的 version.json(API 接口,浏览器可直接跨域读)
3484
3611
  const DEFAULT_UPD_URL='https://gitee.com/api/v5/repos/wwtmrhu/certificate-generation/contents/dist/version.json';
@@ -3990,7 +4117,7 @@ function _treeRenderBody(){
3990
4117
  const q=(_treeQ||'').trim().toLowerCase();
3991
4118
  const groups=[
3992
4119
  {kind:'base', title:'基础数据', slots:C.BASE_SLOTS, total:9},
3993
- {kind:'biz', title:'业务数据', slots:C.BIZ_SLOTS, total:8},
4120
+ {kind:'biz', title:'业务数据', slots:C.BIZ_SLOTS, total:9},
3994
4121
  ];
3995
4122
  let html='', vis=0;
3996
4123
  for(const g of groups){
@@ -4766,10 +4893,6 @@ function renderRules(){
4766
4893
  const conds=(r.conditions||[]).map(c=>'<span class="cond"><b>'+esc(c.field)+'</b> '+esc(c.op)+' '+esc(c.value)+'</span>').join('');
4767
4894
  let body='<div class="rulebody" id="rb'+i+'">';
4768
4895
  body+='<div style="margin:6px 0"><b>命中条件</b>('+(r.logic||'AND')+'):'+(conds||'<span style="color:#8b949e">无条件(全表命中)</span>')+'</div>';
4769
- if((r.excludeWhen||[]).length){
4770
- const exc=r.excludeWhen.map(c=>'<span class="cond"><b>'+esc(c.field)+'</b> '+esc(c.op)+' '+esc(c.value)+'</span>').join('');
4771
- body+='<div style="background:#fff7e6;padding:6px 8px;border-radius:4px;margin:4px 0"><b style="color:#d46b08">排除条件</b>('+(r.excludeLogic||'AND')+'):'+exc+' <span style="color:#888;font-size:11px">→ 满足的行整条跳过,不进 audit</span></div>';
4772
- }
4773
4896
  if((r.branches||[]).length){
4774
4897
  body+='<div><b>分支</b>:'+r.branches.map((b,bi)=>
4775
4898
  '<div class="linechip"><span class="tag b">分支'+(bi+1)+' '+esc(b.name||'')+'</span>'+
@@ -4783,7 +4906,7 @@ function renderRules(){
4783
4906
  '<span class="tag b" style="margin-left:6px">'+esc(r.id)+'</span>'+
4784
4907
  (r.enabled===false?'<span class="tag r" style="margin-left:4px">已停用</span>':'')+'</div>'+
4785
4908
  '<div class="rulemeta">来源:'+esc(r.source)+' · 生成:'+esc(r.genMode||'一对一')+
4786
- (r.genMode==='匹配汇总'?'(按 '+esc(r.genGroupBy||'单位名称')+')':'')+
4909
+ ((r.genMode==='匹配汇总' || (r.genMode==='全部汇总' && r.genGroupBy && r.genGroupBy!=='单位名称'))?'(按 '+esc(r.genGroupBy||'单位名称')+')':'')+
4787
4910
  (r.lineSplitBy?' · 拆分:'+esc(r.lineSplitBy):'')+'</div></div>'+
4788
4911
  '<div style="display:flex;gap:5px"><button class="mini" onclick="UI.toggleRuleBody('+i+')">展开/收起</button>'+
4789
4912
  '<button class="mini" onclick="UI.editRule('+i+')">✎ 编辑</button>'+
@@ -4849,6 +4972,13 @@ function populateEditorDatalists(){
4849
4972
  '支付方式','结算方式','摘要','备注','凭证号','分录序号','收据号','订单号','单据号','商品名称','型号','规格'];
4850
4973
  for(const c of common){ if(!seen.has(c)){ seen.add(c); opts.push('<option value="'+esc(c)+'">'); } }
4851
4974
  dlB.innerHTML=opts.join('')||'<option value="(业务数据还没传)">';
4975
+ // 【P1.2.38】金额字段专用下拉 = 业务字段 + 特殊常量
4976
+ const dlA=$('dlAmountFields');
4977
+ if(dlA){
4978
+ const amountOpts=opts.slice();
4979
+ amountOpts.push('<option value="__AUTO_DIFF__">__AUTO_DIFF__(自动补差额)</option>');
4980
+ dlA.innerHTML=amountOpts.join('');
4981
+ }
4852
4982
  }
4853
4983
  // 0b) 转换:常用 strip_suffix / strip / replace 模板
4854
4984
  const dlT=$('dlTransform');
@@ -5031,7 +5161,7 @@ function ruleFormHTML(){
5031
5161
  '<span class="w60">业务源</span><select id="rf_src">'+srcOpts+'</select>'+
5032
5162
  '<label class="chk"><input type="checkbox" id="rf_en"'+(r.enabled===false?'':' checked')+'>启用</label></div>'+
5033
5163
  '<div class="row"><span class="w60">生成方式</span><select id="rf_mode">'+modeOpts+'</select>'+
5034
- '<span class="w60">分组字段</span><input class="w140" id="rf_gb" value="'+esc(r.genGroupBy||'单位名称')+'" title="仅「匹配汇总」用">'+
5164
+ '<span class="w60">分组字段</span><input class="w140" id="rf_gb" value="'+esc(r.genGroupBy||'单位名称')+'" title="匹配汇总按此字段拆证;全部汇总如需按此字段拆证也填(默认单位名称表示不拆)">'+
5035
5165
  '<span class="w60">拆分字段</span><input class="w180" id="rf_split" value="'+esc(r.lineSplitBy||'')+'" title="多个用 + 或 , 分隔">'+
5036
5166
  '<span class="w60">条件逻辑</span><select id="rf_logic">'+logicOpts+'</select></div></div>';
5037
5167
 
@@ -5039,18 +5169,6 @@ function ruleFormHTML(){
5039
5169
  (r.conditions||[]).map((c,i)=>condRowHTML(i,c)).join('')+'</div>'+
5040
5170
  '<button class="mini" onclick="UI.addCond()">+ 加条件</button></div>';
5041
5171
 
5042
- // 【P1.2.34】规则级排除:满足条件的行整条规则不处理,且不计入覆盖率审计
5043
- h+='<div class="sec" style="background:#fff7e6;border-left:3px solid #fa8c16"><h4>排除条件(满足这些条件的行<b>整条不处理</b>,不计入审计·不显示在排除/漏命中里)</h4>'+
5044
- '<div style="font-size:11.5px;color:#888;margin:3px 0">典型场景:「厂牌 = 资产工具库」时由其他流程直接处理,本规则不参与。留空 = 不启用排除。</div>'+
5045
- '<div class="row"><span class="w60">逻辑</span>'+
5046
- '<select class="w80" id="rf_exclogic">'+
5047
- '<option value="AND"'+((r.excludeLogic||'AND')==='AND'?' selected':'')+'>AND(全部满足)</option>'+
5048
- '<option value="OR"'+(r.excludeLogic==='OR'?' selected':'')+'>OR(任一满足)</option>'+
5049
- '</select></div>'+
5050
- '<div id="rf_exconds">'+
5051
- (r.excludeWhen||[]).map((c,i)=>condRowHTML(i,c)).join('')+'</div>'+
5052
- '<button class="mini" onclick="UI.addExcludeCond()">+ 加排除条件</button></div>';
5053
-
5054
5172
  h+='<div class="sec"><h4>分支(可选:按条件走不同分录;命中第一个分支就停)</h4><div id="rf_branches">'+
5055
5173
  (r.branches||[]).map((b,bi)=>branchHTML(bi,b)).join('')+'</div>'+
5056
5174
  '<button class="mini" onclick="UI.addBranch()">+ 加分支</button></div>';
@@ -5060,10 +5178,12 @@ function ruleFormHTML(){
5060
5178
  '<button class="mini" onclick="UI.addLine(-1)">+ 加分录</button></div>';
5061
5179
  return h;
5062
5180
  }
5063
- function condRowHTML(i,c){
5181
+ function condRowHTML(i,c,srcKey){
5064
5182
  const opOpts=C.OPS.map(o=>'<option value="'+esc(o)+'"'+(c.op===o?' selected':'')+'>'+esc(o)+'</option>').join('');
5183
+ // 【P1.2.36】全局排除分组按业务源独立 datalist(避免看到别的业务源的字段)
5184
+ const listId = srcKey ? 'dlGe_' + srcKey : 'dlBizFields';
5065
5185
  return '<div class="row condrow" data-i="'+i+'">'+
5066
- '<input class="w140" data-f="field" value="'+esc(c.field||'')+'" placeholder="字段名,如 支付方式" list="dlBizFields">'+
5186
+ '<input class="w140" data-f="field" value="'+esc(c.field||'')+'" placeholder="点此选字段" list="'+listId+'">'+
5067
5187
  '<select class="w80" data-f="op">'+opOpts+'</select>'+
5068
5188
  '<input class="w180" data-f="value" value="'+esc(c.value===undefined?'':c.value)+'" placeholder="比较值">'+
5069
5189
  '<select class="w80" data-f="logic"><option value=""'+(!c.logic?' selected':'')+'>并且</option>'+
@@ -5088,7 +5208,7 @@ function lineHTML(bi,li,l){
5088
5208
  return '<div class="sec linerow" data-bi="'+bi+'" data-li="'+li+'" style="background:#fff;margin:5px 0">'+
5089
5209
  '<div class="row"><select class="w60" data-lf="side">'+sideOpts+'</select>'+
5090
5210
  '<span class="w60">科目</span><input class="w140" data-lf="subject" value="'+esc(l.subject||'')+'" placeholder="如 1122 / 6002" list="dlSubjects" title="可下拉选会计科目表里的编码;也可手动输入">'+
5091
- '<span class="w60">金额</span><input class="w180" data-lf="amountField" value="'+esc(l.amountField||'')+'" placeholder="实收金额+折扣费 / -折扣费 / 金额*0.13 / 金额/数量" list="dlBizFields" title="支持四则运算与括号:字段名/数字常量;例:实收金额+折扣费、-折扣费、金额*0.13、金额/数量、(金额+税额)*0.13。特殊值:__AUTO_DIFF__ 自动补差额">'+
5211
+ '<span class="w60">金额</span><input class="w180" data-lf="amountField" value="'+esc(l.amountField||'')+'" placeholder="实收金额+折扣费 / -折扣费 / 金额*0.13 / 金额/数量" list="dlAmountFields" title="支持四则运算与括号:字段名/数字常量;可下拉选「__AUTO_DIFF__」自动补差额;例:实收金额+折扣费、-折扣费、金额*0.13">'+
5092
5212
  '<button class="mini del" onclick="UI.delRow(this,\'linerow\')">删除分录</button></div>'+
5093
5213
  '<div class="row"><span class="w60">摘要</span><input class="grow" data-lf="summary" value="'+esc(l.summary||'')+'" placeholder="{单位名称}{收入板块}对帐S{原单号}"></div>'+
5094
5214
  '<div style="font-size:11.5px;color:#57606a;margin:3px 0">辅助核算维度:</div>'+
@@ -5118,14 +5238,6 @@ function syncFormToObj(){
5118
5238
  return {field:g('field').value.trim(),op:g('op').value,value:g('value').value.trim(),
5119
5239
  logic:g('logic').value||undefined};
5120
5240
  });
5121
- // 【P1.2.34】排除条件:满足时整条规则不处理、不进 audit
5122
- r.excludeLogic=$('rf_exclogic') ? $('rf_exclogic').value : 'AND';
5123
- const exConds=[...document.querySelectorAll('#rf_exconds > .condrow')].map(row=>{
5124
- const g=f=>row.querySelector('[data-f="'+f+'"]');
5125
- return {field:g('field').value.trim(),op:g('op').value,value:g('value').value.trim(),
5126
- logic:g('logic').value||undefined};
5127
- });
5128
- r.excludeWhen=exConds.length ? exConds : undefined;
5129
5241
  // 分支
5130
5242
  const bsecs=[...document.querySelectorAll('#rf_branches > [data-bi]')];
5131
5243
  r.branches=bsecs.map(sec=>{
@@ -5199,7 +5311,7 @@ function doGenerate(){
5199
5311
  const bs=(res.audit&&res.audit.by_source)||[];
5200
5312
  if(bs.length){
5201
5313
  h+='<div class="banner" style="margin-top:6px"><b>📊 业务源覆盖率</b> <span style="color:#57606a;font-size:12px">(按源表汇总:被覆盖行 / 真漏行(候选未命中) / 被排除行(不满足条件);点开看具体单号)</span>'+
5202
- '<table style="margin-top:6px"><thead><tr><th>源表</th><th class="num">总行</th><th class="num">已覆盖</th><th class="num">占比</th><th class="num">真漏(候选未命中)</th><th class="num">漏算金额</th><th class="num">被排除(不满足条件)</th><th>金额字段</th><th>说明</th></tr></thead><tbody>'+
5314
+ '<table style="margin-top:6px"><thead><tr><th>源表</th><th class="num">总行</th><th class="num">已覆盖</th><th class="num">占比</th><th class="num">真漏(候选未命中)</th><th class="num">漏算金额</th><th class="num">被排除(不满足条件)</th><th class="num">缺编码</th><th>金额字段</th><th>说明</th></tr></thead><tbody>'+
5203
5315
  bs.map(b=>{
5204
5316
  const cm=b.candidate_missed||0;
5205
5317
  const excl=b.excluded||0;
@@ -5219,6 +5331,8 @@ function doGenerate(){
5219
5331
  if(firstExcl.原单号) reasonBits.push('原单号='+firstExcl.原单号);
5220
5332
  }
5221
5333
  const exclReason = reasonBits.slice(0,2).join(',');
5334
+ const mc=b.missing_code_count||0;
5335
+ const ma=b.missing_code_amount||0;
5222
5336
  return '<tr'+cmCls+'>'+
5223
5337
  '<td><code>'+esc(b.source)+'</code></td>'+
5224
5338
  '<td class="num">'+total+'</td>'+
@@ -5246,10 +5360,13 @@ function doGenerate(){
5246
5360
  '</details>'+
5247
5361
  '</div>'
5248
5362
  : '<span style="color:#1a7f37">✅ 0</span>')+'</td>'+
5363
+ '<td class="num"'+(mc?' style="color:#a40e26;font-weight:600"':'')+'>'+
5364
+ (mc>0?'⚠ '+mc+' 行 / ¥'+fmt(ma):'<span style="color:#1a7f37">✅ 0</span>')+'</td>'+
5249
5365
  '<td>'+(b.amount_col?esc(b.amount_col):'<span style="color:#8b949e">(无)</span>')+'</td>'+
5250
5366
  '<td>'+(cm>0
5251
5367
  ? '<span style="color:#a40e26">⚠ '+cm+' 行满足条件却未生成凭证(点开被排除行明细确认是规则没命中还是缺编码)</span>'
5252
5368
  : '<span style="color:#1a7f37">✅ 真漏 0 行</span>')+
5369
+ (mc>0?'<br><span style="color:#a40e26;font-weight:600;font-size:11.5px">⚠ 该源有 '+mc+' 条分录缺维度编码(金额 ¥'+fmt(ma)+'),去「凭证审核」或「对碰检测」看明细并补基础档案</span>':'')+
5253
5370
  (excl>0?'<br><span style="color:#92400e;font-weight:600;font-size:11.5px">另外 '+excl+' 行被本批规则排除(金额 ¥'+fmt(exclAmount)+'),点开「展开全部明细」看哪些单</span>':'')+
5254
5371
  '</td></tr>';
5255
5372
  }).join('')+
@@ -5291,6 +5408,31 @@ function doGenerate(){
5291
5408
  * 底部按钮:📤 导出试入金蝶(直接下载本规则的金蝶导入 Excel)/ 关闭
5292
5409
  * ============================================================ */
5293
5410
  let _previewCache=null; // 缓存最近一次预览结果,给 exportRulePreview 复用
5411
+
5412
+ /* 【P1.2.48】金蝶式核算维度单列展示:把客户/供应商/部门/员工/仓库/收入类型/成本类型等
5413
+ * 编码+名称合并成「编码/名称」格式,多个维度用 ; 分隔,缺失编码的红色高亮提示。
5414
+ */
5415
+ function formatDimensions(r){
5416
+ const parts=[]; const missingDims=[];
5417
+ for(const d of C.DIM_LIST){
5418
+ const [nameCol,codeCol]=C.DIM_DISPLAY[d];
5419
+ const name=E.clean(r[nameCol]||''), code=E.clean(r[codeCol]||'');
5420
+ if(!name && !code) continue;
5421
+ let txt='';
5422
+ if(code && name) txt=code+'/'+name;
5423
+ else if(code) txt=code;
5424
+ else txt=name;
5425
+ if(name && !code && C.STRICT_DIMS.indexOf(d)>=0){
5426
+ txt='<span class="dim-missing" title="'+esc(d)+' 缺编码">'+esc(txt)+'⚠</span>';
5427
+ missingDims.push(d);
5428
+ }else{
5429
+ txt=esc(txt);
5430
+ }
5431
+ parts.push(txt);
5432
+ }
5433
+ return {html: parts.join('; '), missingDims: missingDims};
5434
+ }
5435
+
5294
5436
  function unmatchedTableHtml(rows, sourceKey){
5295
5437
  const partyLabel = esc((C.SOURCE_META[sourceKey] && C.SOURCE_META[sourceKey].party) || '单位名称');
5296
5438
  return '<div class="tablewrap" style="max-height:280px;margin-top:6px"><table><thead><tr>'+
@@ -5354,7 +5496,7 @@ function previewRule(ruleId){
5354
5496
 
5355
5497
  let h='<div class="modalbox" style="max-width:980px"><div class="modalhd"><h3>🧪 规则预览 · '+esc(r.name||r.id)+
5356
5498
  '</h3><span style="color:#57606a;font-size:12px;margin-left:8px">'+esc(r.source)+' · '+esc(r.genMode||'一对一')+
5357
- (r.genMode==='匹配汇总'?'(按 '+esc(r.genGroupBy||'单位名称')+')':'')+' · '+linesTxt+'</span>'+
5499
+ ((r.genMode==='匹配汇总' || (r.genMode==='全部汇总' && r.genGroupBy && r.genGroupBy!=='单位名称'))?'(按 '+esc(r.genGroupBy||'单位名称')+')':'')+' · '+linesTxt+'</span>'+
5358
5500
  '<button class="mini" onclick="UI.closeModal()">✕ 关闭</button></div><div class="modalbd">';
5359
5501
 
5360
5502
  // 顶部 banner:先说"覆盖率 N% + 命中数 / 应处理子集",再分别说"应处理子集漏命中" 与 "源表被规则排除"
@@ -5416,23 +5558,35 @@ function previewRule(ruleId){
5416
5558
  if(!pvRows.length){
5417
5559
  h+='<div style="color:#8b949e;padding:8px">本规则没生成任何凭证('+esc(r0.candidate_in_source?'无候选行':'源表是空的')+')。</div>';
5418
5560
  }else{
5561
+ // 【P1.2.48】凭证预览表格改为金蝶式「核算维度」单列
5562
+ let previewMissingDims=[];
5419
5563
  h+='<div class="tablewrap" style="max-height:340px"><table><thead><tr>'+
5420
5564
  '<th>凭证号</th><th>分录序号</th><th>业务日期</th><th>摘要</th><th>科目</th><th>借方</th><th>贷方</th>'+
5421
- '<th>客户</th><th>部门</th><th>收入类型</th><th>成本类型</th></tr></thead><tbody>'+
5422
- pvRows.map(e=>'<tr'+(e['_缺失编码']?' class="bad"':'')+'>'+
5423
- '<td><b>'+esc(e['凭证号'])+'</b></td><td>'+esc(e['分录序号'])+'</td>'+
5424
- '<td>'+esc(e['业务日期']||'')+'</td><td>'+esc((e['摘要']||'').slice(0,40))+'</td>'+
5425
- '<td><b>'+esc(e['科目编码'])+'</b> '+esc(e['科目名称']||'')+'</td>'+
5426
- '<td class="num">'+(e['借方金额']?fmt(e['借方金额']):'')+'</td>'+
5427
- '<td class="num">'+(e['贷方金额']?fmt(e['贷方金额']):'')+'</td>'+
5428
- '<td>'+esc((e['客户名称']||'')+' '+(e['客户编码']||''))+'</td>'+
5429
- '<td>'+esc((e['部门名称']||'')+' '+(e['部门编码']||''))+'</td>'+
5430
- '<td>'+esc((e['收入类型名称']||'')+' '+(e['收入类型编码']||''))+'</td>'+
5431
- '<td>'+esc((e['成本类型名称']||'')+' '+(e['成本类型编码']||''))+'</td></tr>').join('')+
5432
- '</tbody></table></div>'+
5433
- '<div style="margin-top:5px;color:#57606a;font-size:12px">共 <b>'+pvStats.vouchers+'</b> 张凭证,<b>'+
5434
- pvStats.entries+'</b> 条分录('+(r0.missing_code_count||0)+' 条编码缺失,已用红色标注)'+
5565
+ '<th>核算维度</th></tr></thead><tbody>'+
5566
+ pvRows.map(e=>{
5567
+ const dim=formatDimensions(e);
5568
+ if(dim.missingDims.length) previewMissingDims=previewMissingDims.concat(dim.missingDims);
5569
+ const hasMissing=e['_缺失编码'] || dim.missingDims.length>0;
5570
+ return '<tr'+(hasMissing?' class="bad"':'')+'>'+
5571
+ '<td><b>'+esc(e['凭证号'])+'</b></td><td>'+esc(e['分录序号'])+'</td>'+
5572
+ '<td>'+esc(e['业务日期']||'')+'</td><td>'+esc((e['摘要']||'').slice(0,40))+'</td>'+
5573
+ '<td><b>'+esc(e['科目编码'])+'</b> '+esc(e['科目名称']||'')+'</td>'+
5574
+ '<td class="num">'+(e['借方金额']?fmt(e['借方金额']):'')+'</td>'+
5575
+ '<td class="num">'+(e['贷方金额']?fmt(e['贷方金额']):'')+'</td>'+
5576
+ '<td>'+dim.html+'</td></tr>';
5577
+ }).join('')+
5578
+ '</tbody></table></div>';
5579
+ const previewMissingSet=previewMissingDims.filter((v,i,a)=>a.indexOf(v)===i);
5580
+ h+='<div style="margin-top:5px;color:#57606a;font-size:12px">共 <b>'+pvStats.vouchers+'</b> 张凭证,<b>'+
5581
+ pvStats.entries+'</b> 条分录'+
5582
+ (previewMissingSet.length?' · <b style="color:#a40e26">'+previewMissingSet.length+' 个维度缺编码('+
5583
+ previewMissingSet.join('、')+')</b>':' · 无编码缺失')+
5435
5584
  ')。预览不修改任何数据,关闭即丢。</div>';
5585
+ if(previewMissingSet.length){
5586
+ h+='<div class="banner warn" style="margin-top:6px">⚠ 核算维度缺编码:'+
5587
+ previewMissingSet.map(d=>esc(d)).join('、')+
5588
+ '。请到「⑥ 对碰检测」或「② 映射配置」补全基础档案/映射。</div>';
5589
+ }
5436
5590
  }
5437
5591
 
5438
5592
  h+='</div><div class="modalft">'+
@@ -5464,12 +5618,18 @@ function exportRulePreview(){
5464
5618
  const ma=[C.MISSING_HEAD].concat(pMissing.map(m=>C.MISSING_HEAD.map(h=>m[h]||'')));
5465
5619
  XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(ma),'缺失编码清单');
5466
5620
  }
5621
+ const pSubj = E.checkSubjects(pRows, idx);
5622
+ if(pSubj.length){
5623
+ const ma=[C.SUBJ_MISS_HEAD].concat(pSubj.map(m=>[m.code,m.name,m.count,m.vnos.join(', ')]));
5624
+ XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(ma),'缺失科目清单');
5625
+ }
5467
5626
  const fname='金蝶导入_'+ (r.name||r.id).replace(/[\\/:*?"<>|]/g,'_').slice(0,30) + '_' + r.id + '.xlsx';
5468
5627
  XLSX.writeFile(wb, fname);
5469
5628
  const whFbk = aoa.whFallback || 0;
5470
5629
  const incFbk = aoa.incMiss || 0;
5471
5630
  let msg='已导出 '+fname+(whFbk?'(含 '+whFbk+' 条仓库 LJ 兜底)':'');
5472
5631
  if (incFbk) msg += '(含 '+incFbk+' 条 6050 收入类型兜底,建议去「③ 规则」补 dim 收入类型)';
5632
+ if (pSubj.length) msg += '(⚠ '+pSubj.length+' 个科目编码在会计科目表里查不到,金蝶导入会失败)';
5473
5633
  toast(msg);
5474
5634
  }catch(e){ toast('导出失败:'+e.message); }
5475
5635
  }
@@ -5477,8 +5637,9 @@ function exportRulePreview(){
5477
5637
  /* ============================================================
5478
5638
  * ⑤ 凭证审核
5479
5639
  * ============================================================ */
5640
+ // 【P1.2.48】审核表格把客户/供应商/部门/员工/仓库/收入类型/成本类型等合并成「核算维度」单列
5480
5641
  const AUD_COLS=['凭证号','分录序号','记账日期','业务日期','摘要','科目编码','科目名称','借方金额','贷方金额',
5481
- '客户名称','部门名称','收入类型名称','成本类型名称','供应商名称','结算方式名称','来源单据','规则名称'];
5642
+ '核算维度','结算方式名称','来源单据','规则名称'];
5482
5643
  const AUD_NUM=['借方金额','贷方金额'];
5483
5644
  let audPage=1;
5484
5645
  /** S.draft 里"分组来源":cv_upload | 业务源 key(如 销售已确认) */
@@ -5551,7 +5712,12 @@ function renderAudit(){
5551
5712
  /* ---- 3) 选组过滤 ---- */
5552
5713
  let rows=S.draft.map((r,i)=>({r:r,i:i,g:_srcGroup(r)}));
5553
5714
  if(group!=='all') rows=rows.filter(o=>o.g===group);
5554
- if(kw) rows=rows.filter(o=>AUD_COLS.some(c=>String(nz(o.r[c])).toLowerCase().indexOf(kw)>=0));
5715
+ if(kw) rows=rows.filter(o=>
5716
+ AUD_COLS.some(c=>{
5717
+ if(c==='核算维度') return formatDimensions(o.r).html.replace(/<[^>]+>/g,'').toLowerCase().indexOf(kw)>=0;
5718
+ return String(nz(o.r[c])).toLowerCase().indexOf(kw)>=0;
5719
+ })
5720
+ );
5555
5721
 
5556
5722
  /* ---- 4) 不平标记(只算当前 visible 组)---- */
5557
5723
  const bad={};
@@ -5567,8 +5733,13 @@ function renderAudit(){
5567
5733
  let h='<table><thead><tr><th style="width:34px"></th>'+AUD_COLS.map(c=>'<th>'+esc(c)+'</th>').join('')+'</tr></thead><tbody>';
5568
5734
  h+=slice.map(o=>{
5569
5735
  const r=o.r;
5570
- return '<tr'+(bad[o.i]?' class="bad"':'')+'><td><button class="mini del" onclick="UI.delDraftRow('+o.i+')">删</button></td>'+
5736
+ const dim=formatDimensions(r);
5737
+ const hasMissing=dim.missingDims.length>0;
5738
+ return '<tr'+(bad[o.i]||hasMissing?' class="bad"':'')+'><td><button class="mini del" onclick="UI.delDraftRow('+o.i+')">删</button></td>'+
5571
5739
  AUD_COLS.map(c=>{
5740
+ if(c==='核算维度'){
5741
+ return '<td>'+dim.html+'</td>';
5742
+ }
5572
5743
  const v=nz(r[c]);
5573
5744
  const canEdit=['摘要','科目编码','借方金额','贷方金额','记账日期','业务日期'].indexOf(c)>=0||C.DIM_LIST.some(d=>C.DIM_DISPLAY[d][0]===c||C.DIM_DISPLAY[d][1]===c);
5574
5745
  const cls=AUD_NUM.indexOf(c)>=0?'num':'';
@@ -5704,17 +5875,29 @@ function doExport(){
5704
5875
  const ma=[C.MISSING_HEAD].concat(S.missing.map(m=>C.MISSING_HEAD.map(h=>m[h]||'')));
5705
5876
  XLSX.utils.book_append_sheet(wb,XLSX.utils.aoa_to_sheet(ma),'缺失编码清单');
5706
5877
  }
5878
+ const missSubj = E.checkSubjects(S.draft, idx);
5879
+ if (missSubj.length) {
5880
+ const ma = [C.SUBJ_MISS_HEAD].concat(missSubj.map(m => [m.code, m.name, m.count, m.vnos.join(", ")]));
5881
+ XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(ma), '缺失科目清单');
5882
+ }
5707
5883
  const fname='金蝶导入.xlsx';
5708
5884
  XLSX.writeFile(wb,fname);
5709
5885
  // 【P1.2.12】把仓库空兜底条数展示给用户,让他知道哪些行被自动补了
5710
5886
  const whFbk = aoa.whFallback || 0;
5711
5887
  // 【P1.2.21】6050 收入类型兜底条数(应该 =0;>0 提示用户该规则行漏配 dim 收入类型)
5712
5888
  const incFbk = aoa.incMiss || 0;
5889
+ const subjWarn = missSubj.length ?
5890
+ '<div class="banner warn" style="margin-top:8px"><b>⚠ 有 '+missSubj.length+' 个科目编码在「会计科目表」里查不到,金蝶导入必失败!</b>'+
5891
+ '<table style="margin-top:7px"><thead><tr><th>科目编码</th><th>科目名称</th><th>出现次数</th><th>涉及凭证号</th></tr></thead><tbody>'+
5892
+ missSubj.map(m=>'<tr><td><b>'+esc(m.code)+'</b></td><td>'+esc(m.name||'(空)')+'</td><td class="num">'+m.count+'</td><td>'+esc(m.vnos.join(', ')||'-')+'</td></tr>').join('')+
5893
+ '</tbody></table><div style="margin-top:6px">请到「③ 规则」把借方/贷方科目改成「会计科目表」里存在的末级编码;或在金蝶 WWT 组织里新增这些科目后,重新上传「会计科目表」再导出。</div></div>'
5894
+ : '';
5713
5895
  $('expBanner').innerHTML='<div class="banner ok">✅ 已导出 <b>'+fname+'</b>:'+S.draft.length+' 条分录,'+
5714
5896
  (aoa.length-2)+' 行数据'+(S.missing&&S.missing.length?',另含「缺失编码清单」'+S.missing.length+' 行':'')+
5897
+ (missSubj.length?',另含「缺失科目清单」'+missSubj.length+' 行':'')+
5715
5898
  (whFbk ? ',<b>🔧 '+whFbk+' 行仓库列被自动补 LJ 兜底</b>(建议到「② 映射」补厂牌→仓库映射后重跑)' : '')+
5716
5899
  (incFbk ? ',<b>🛟 '+incFbk+' 行 6050 收入类型被自动补 089</b>(你这条规则漏了「收入类型」维度,建议去「③ 规则」补上)' : '')+
5717
- '</div>';
5900
+ '</div>'+subjWarn;
5718
5901
  $('expStat').innerHTML='文件已下载到浏览器的下载目录(一般为「下载」文件夹)。导入金蝶前请确认:<b>前两行(字段键行)不要删</b>。'+
5719
5902
  (whFbk ? '<br><span style="color:#d4880f">⚠ '+whFbk+' 行仓库列被自动补 LJ:去「② 映射→① 厂牌→仓库」点「🔄 自动扫描」把所有业务厂牌拉进来,下次导出就不会再兜底了。</span>' : '')+
5720
5903
  (incFbk ? '<br><span style="color:#d4880f">⚠ '+incFbk+' 行 6050(其他业务收入)缺「收入类型」维度,引擎已自动补 089/其他业务收入-其他。建议去「③ 规则」找对应规则的 6050 行,加上 dim=收入类型 fixedCode=089 fixedValue=其他业务收入-其他。</span>' : '');
@@ -6771,14 +6954,35 @@ window.UI={
6771
6954
  '<br><span style="color:#a40e26">⚠ 全局排除是「粗过滤」,会作用于该业务源下<b>所有规则</b>;想只排除某一条规则请在「编辑规则 → 排除条件」单独配。</span>'+
6772
6955
  '</div>';
6773
6956
  // 每个业务源一个折叠 details
6957
+ // 【P1.2.36】先构建「每业务源独立 datalist」:只列该业务源上传后的列名(避免看到别的业务源字段)
6958
+ const dlBy = {}; // srcKey -> ['列1','列2',...]
6959
+ for (const s of slots) {
6960
+ const rows = (S.biz && S.biz[s.key]) || [];
6961
+ const seen = new Set();
6962
+ const fields = [];
6963
+ if (rows.length) {
6964
+ const r0 = rows.find(r => r && typeof r === 'object') || {};
6965
+ for (const col of Object.keys(r0)) {
6966
+ const c = String(col || '').trim();
6967
+ if (!c || seen.has(c)) continue;
6968
+ seen.add(c); fields.push(c);
6969
+ }
6970
+ }
6971
+ // 兜底常用字段(S.biz 还没上传时也能给点选项)
6972
+ const common = ['单位名称','客户名称','供应商名称','付款单位','收款单位','部门','仓库','日期','业务日期','记账日期',
6973
+ '金额','实收金额','应收金额','实付金额','付款金额','收款金额','数量','单价','折扣','折扣率',
6974
+ '支付方式','结算方式','摘要','备注','凭证号','分录序号','收据号','订单号','单据号','商品名称','型号','规格'];
6975
+ for (const c of common) { if (!seen.has(c)) { seen.add(c); fields.push(c); } }
6976
+ dlBy[s.key] = fields;
6977
+ }
6774
6978
  for (const s of slots) {
6775
6979
  const cfg = ge[s.key] || (ge[s.key] = { logic: 'AND', conditions: [] });
6776
- const condHTML = (cfg.conditions || []).map((c, i) => condRowHTML(i, c)).join('');
6980
+ const condHTML = (cfg.conditions || []).map((c, i) => condRowHTML(i, c, s.key)).join('');
6777
6981
  const condsId = 'ge_conds_' + s.key;
6778
6982
  html += '<details open style="border:1px solid #dde2e8;border-radius:6px;margin:6px 0;background:#fff">'+
6779
6983
  '<summary style="padding:8px 12px;cursor:pointer;font-weight:600">'+
6780
6984
  '<span style="display:inline-block;width:160px">'+esc(s.label)+'</span>'+
6781
- '<span style="color:#a40e26;font-size:12px">['+((cfg.conditions||[]).length)+' 个条件]</span>'+
6985
+ '<span style="color:#a40e26;font-size:12px">['+((cfg.conditions||[]).length)+' 个条件 · '+dlBy[s.key].length+' 个可选字段]</span>'+
6782
6986
  '</summary>'+
6783
6987
  '<div style="padding:4px 12px 12px">'+
6784
6988
  '<div class="row" style="margin-bottom:6px">'+
@@ -6793,6 +6997,10 @@ window.UI={
6793
6997
  '<button class="mini" onclick="UI.addGlobalExcludeCond(\''+s.key+'\')">+ 加条件</button>'+
6794
6998
  '</div></details>';
6795
6999
  }
7000
+ // 把每个业务源独立的 datalist 挂到弹窗里(用完即丢,不污染 dlBizFields)
7001
+ html += '<div style="display:none">'+
7002
+ slots.map(s => '<datalist id="dlGe_'+esc(s.key)+'">'+dlBy[s.key].map(f=>'<option value="'+esc(f)+'">').join('')+'</datalist>').join('')+
7003
+ '</div>';
6796
7004
  html += '</div>'+
6797
7005
  '<div class="modalft">'+
6798
7006
  '<button class="btn ghost" onclick="UI.closeModal()">取消</button>'+
@@ -6852,13 +7060,6 @@ window.UI={
6852
7060
  else editing.conditions.push({field:'',op:'=',value:'',logic:''});
6853
7061
  redrawRuleForm();
6854
7062
  },
6855
- // 【P1.2.34】加排除条件:写到 #rf_exconds
6856
- addExcludeCond:()=>{
6857
- syncFormToObj();
6858
- if(!editing.excludeWhen) editing.excludeWhen=[];
6859
- editing.excludeWhen.push({field:'',op:'=',value:'',logic:''});
6860
- redrawRuleForm();
6861
- },
6862
7063
  addBranch:()=>{
6863
7064
  syncFormToObj();
6864
7065
  editing.branches.push({name:'分支'+((editing.branches||[]).length+1),logic:'AND',conditions:[{field:'',op:'=',value:''}],lines:[]});
package/dist/version.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
- "ver": "P1.2.35",
3
- "npm": "1.2.35",
4
- "url": "https://cdn.jsdelivr.net/npm/wwt-certificate-tool@1.2.35/dist/app.html",
5
- "url_unpkg": "https://unpkg.com/wwt-certificate-tool@1.2.35/dist/app.html",
6
- "url_gh": "https://api.github.com/repos/huweipei745839/certificate-generation/contents/dist/WWT%E5%87%AD%E8%AF%81%E5%B7%A5%E5%85%B7_P1.2.35.html",
7
- "note": "①「全局排除」机制升级:把排除条件从每条规则里挪出来,做成外面工具栏的「🚫 全局排除(总开关)」按钮,按业务源分组配置(折叠面板)。命中行不进任何针对该业务源的规则,且不计入审计统计。典型场景:「销售已确认」里厂牌=资产工具库 的行整张表一刀切掉。② cfg 新增 globalExcludes 字段(saveCfg/loadCfg 自动读写);引擎新增 isExcludedByGlobals(),generate/auditRule 三处接入。③ by_source 返回值新增 global_excluded/global_excluded_amount 字段(与 P1.2.34 的 rule_excluded 并列)。④ 单规则的 rule.excludeWhen 保留作为兜底细粒度配置。⑤ 工具栏新增橙底按钮 + 新增 .btn.warn 样式;弹窗复用 condRowHTML 条件行模板。回归 202/202 全通过。"
2
+ "ver": "P1.2.49",
3
+ "npm": "1.2.49",
4
+ "url": "https://cdn.jsdelivr.net/npm/wwt-certificate-tool@1.2.49/dist/app.html",
5
+ "url_unpkg": "https://unpkg.com/wwt-certificate-tool@1.2.49/dist/app.html",
6
+ "url_gh": "https://api.github.com/repos/huweipei745839/certificate-generation/contents/dist/WWT%E5%87%AD%E8%AF%81%E5%B7%A5%E5%85%B7_P1.2.49.html",
7
+ "note": " 规则测试预览与凭证审核界面改为金蝶式「核算维度」单列展示(客户/供应商/部门/员工/仓库/收入类型/成本类型合并为一列,编码/名称格式)。② 核算维度缺失编码时单元格内标红并带 提示。③ 对碰检测覆盖「入库未结算」中付款方式为挂账的数据,即使供应商尚未建档也报缺失。④ 1403 开头存货科目若规则未配仓库维度,默认带出厂牌 LJ/零件(LJ仓)。⑤ 业务源覆盖率表格新增「缺编码」列,按源表显示缺编码分录行数与金额。"
8
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wwt-certificate-tool",
3
- "version": "1.2.35",
3
+ "version": "1.2.49",
4
4
  "description": "WWT 金蝶凭证工具 · 纯前端版(离线单文件 HTML,双击即用)",
5
5
  "keywords": [
6
6
  "WWT",