wwt-certificate-tool 1.2.36 → 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.36</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}
@@ -315,7 +316,7 @@ input[data-f="field"][list^="dlGe_"]::placeholder{color:#a8861f;font-style:itali
315
316
  <div class="brand">
316
317
  <span class="logo">WWT</span>
317
318
  <div>
318
- <h1>金蝶凭证工具 <span class="ver">P1.2.36</span></h1>
319
+ <h1>金蝶凭证工具 <span class="ver">P1.2.49</span></h1>
319
320
  <div class="sub">纯前端离线版 · 双击即用 · 数据只存在本机,不上传任何服务器</div>
320
321
  </div>
321
322
  </div>
@@ -348,7 +349,7 @@ input[data-f="field"][list^="dlGe_"]::placeholder{color:#a8861f;font-style:itali
348
349
  <!-- ① 上传 -->
349
350
  <section class="pane on" id="tab-upload">
350
351
  <div class="notice">
351
- <b>第一次用请按顺序来:</b>先传 <b>9 个基础数据表</b>(一次传完,以后不用再传)→ 再传 <b>本月 8 个业务数据表</b> → 去「② 映射配置」改月份 → 「④ 生成凭证」→「⑦ 导出金蝶」。
352
+ <b>第一次用请按顺序来:</b>先传 <b>9 个基础数据表</b>(一次传完,以后不用再传)→ 再传 <b>本月 9 个业务数据表</b> → 去「② 映射配置」改月份 → 「④ 生成凭证」→「⑦ 导出金蝶」。
352
353
  <br><b>业务数据表必须是 .xls 原文件,不要改名、不要另存为 .xlsx。</b>
353
354
  </div>
354
355
  <div class="rowacts">
@@ -357,7 +358,7 @@ input[data-f="field"][list^="dlGe_"]::placeholder{color:#a8861f;font-style:itali
357
358
  </div>
358
359
  <h3>基础数据(9 个 · 一次上传长期有效)<span class="tip" style="margin-left:8px">支持 <b>点击按钮</b> 或 <b>把 Excel 文件拖到这里</b></span></h3>
359
360
  <div class="grid dropzone" id="grpBase" data-kind="base"></div>
360
- <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>
361
362
  <div class="grid dropzone" id="grpBiz" data-kind="biz"></div>
362
363
  <div id="uploadLog" class="log"></div>
363
364
  <input type="file" id="pickBase" multiple accept=".xls,.xlsx" style="display:none">
@@ -522,6 +523,8 @@ input[data-f="field"][list^="dlGe_"]::placeholder{color:#a8861f;font-style:itali
522
523
  <datalist id="dlSubjects"></datalist>
523
524
  <datalist id="dlAuxFixed"></datalist>
524
525
  <datalist id="dlBizFields"></datalist>
526
+ <!-- 【P1.2.38】金额字段专用下拉:在业务字段基础上加上 __AUTO_DIFF__ 自动补差额等常量 -->
527
+ <datalist id="dlAmountFields"></datalist>
525
528
  <datalist id="dlTransform"></datalist>
526
529
  <datalist id="dlAuxValue"></datalist>
527
530
 
@@ -822,6 +825,7 @@ window.WWT_CONST = (function () {
822
825
  "FOPPACCTNAME","FITEMNUM","FITEMNAME","FSUBITEMNUM","FSUBITEMNAME","FCURRENCYNUM","FCURRENCYNAME",
823
826
  "FAMOUNTFOR","FAMOUNT"];
824
827
  const MISSING_HEAD = ["来源单据","业务编号","科目","维度","名称"];
828
+ const SUBJ_MISS_HEAD = ["科目编码","科目名称","出现次数","涉及凭证号"];
825
829
 
826
830
  /* ---------- 16 种辅助核算维度 ---------- */
827
831
  // 草稿列名:[名称列, 编码列]
@@ -870,6 +874,8 @@ window.WWT_CONST = (function () {
870
874
  "调拨未确认":{"file":"调拨未确认.xls","sheet":"调拨总","datecol":"出库日期","party":"单位名称","partytype":"客户","no":"出库单号"},
871
875
  "在修车":{"file":"在修车.xls","sheet":"在修车","datecol":"进厂日期","party":"单位名称","partytype":"客户","no":"工单号"},
872
876
  "已完工未结算":{"file":"已完工未结算.xls","sheet":"完工未结","datecol":"结算日期","party":"单位名称","partytype":"客户","no":"工单号"},
877
+ // 【P1.2.39】新增第 9 个业务源:维修已结算(按实际表头 维修已结算.xls 校准)
878
+ "维修已结算":{"file":"维修已结算.xls","sheet":"维修已结算","datecol":"结算日期","party":"单位名称","partytype":"客户","no":"工单号"},
873
879
  };
874
880
  const BIZ_SLOTS = [
875
881
  {key:"入库已确认", label:"入库已确认(采购-已确认)"},
@@ -880,9 +886,13 @@ window.WWT_CONST = (function () {
880
886
  {key:"调拨未确认", label:"调拨未确认"},
881
887
  {key:"在修车", label:"在修车(维修在修)"},
882
888
  {key:"已完工未结算", label:"已完工未结算(维修完工)"},
889
+ {key:"维修已结算", label:"维修已结算(维修已结)"},
883
890
  ];
884
891
  // 对碰检测只覆盖"已确认/已结算"的来源;已完工未结算 用户要求不参与(2026-09-09)
885
- const DETECT_ALLOWED_SOURCES = ["入库已确认","销售已确认","调拨已确认"];
892
+ // 【P1.2.39】新增 维修已结算(已结算类,按设计意图纳入对碰检测)
893
+ // 【P1.2.48】对碰检测增加「入库未结算」:用户要求付款方式=挂账的入库数据,
894
+ // 即使供应商尚未在基础供应商表建档,也要报出来,便于补档案/补映射。
895
+ const DETECT_ALLOWED_SOURCES = ["入库已确认","入库未结算","销售已确认","调拨已确认","维修已结算"];
886
896
  // v1.2.20:只核对 挂账 / 对账S / 冲转S 三类(去掉 对帐F / 冲转F)
887
897
  const DETECT_ALLOWED_PAY = ["对帐S","挂帐","冲转S"];
888
898
 
@@ -974,7 +984,7 @@ window.WWT_CONST = (function () {
974
984
  const JOIN_PREFIX = ["客户","供应商","部门","员工","其他往来"];
975
985
 
976
986
  return {
977
- KINGDEE_COLUMNS, KINGDEE_NAMES, CASHFLOW_HEAD, MISSING_HEAD,
987
+ KINGDEE_COLUMNS, KINGDEE_NAMES, CASHFLOW_HEAD, MISSING_HEAD, SUBJ_MISS_HEAD,
978
988
  DIM_DISPLAY, DIM_COLUMNS, DIM_LIST, STRICT_DIMS,
979
989
  SOURCE_META, BIZ_SLOTS, BASE_SLOTS,
980
990
  DETECT_ALLOWED_SOURCES, DETECT_ALLOWED_PAY,
@@ -2481,11 +2491,19 @@ window.WWT_ENGINE = (function () {
2481
2491
  if (m && clean(m.whcode)) return { code: clean(m.whcode), name: clean(m.whname) || clean(m.whcode) };
2482
2492
  return { code: "LJ", name: "LJ仓" };
2483
2493
  }
2484
- // 维度反查:六表依次 + 白名单业务列
2494
+ // 维度反查:优先当前维度对应的基础表,再兜底其他表
2495
+ // 【P1.2.45】修复同名单位跨表串码:供应商维度先查供应商表,客户维度先查客户表。
2485
2496
  function lookupDimCode(dim, name, idx) {
2486
2497
  if (!name) return "";
2487
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) 兜底:保持原六表顺序(兼容历史模糊匹配场景)
2488
2505
  for (const d of order) {
2506
+ if (d === dim) continue; // 已查过
2489
2507
  const t = idx.tables[d];
2490
2508
  if (!t) continue;
2491
2509
  const rec = t[name] || ciGetObj(t, name);
@@ -2525,15 +2543,27 @@ window.WWT_ENGINE = (function () {
2525
2543
  code = lookupDimCode(d, name, idx) || lookupDimByField(d, vf, name, idx);
2526
2544
  }
2527
2545
  } else {
2528
- 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);
2529
2555
  if (/\.编码$/.test(vf) && name) { code = name; }
2530
2556
  if (name) {
2531
2557
  if (d === "仓库") {
2532
2558
  const w = lookupWh(name);
2533
2559
  code = w.code; name = w.name;
2534
2560
  } else if (d === "收入类型" || d === "成本类型") {
2535
- // 硬约束:只认「出库单类 / 单类」,盘亏清空,其它来源字段一律清空
2536
- if (vf === "出库单类" || vf === "单类") {
2561
+ // 硬约束:只认「出库单类 / 单类」,盘亏清空,其它来源字段一律清空;
2562
+ // 【P1.2.41】维修已结算没有「单类」,允许用「收费类型」映射成本类型;
2563
+ // 【P1.2.42】同一例外扩展到 收入类型(收费类型 → 收入类型 也可走映射表)。
2564
+ const allowVf = (vf === "出库单类" || vf === "单类") ||
2565
+ (curSrc === "维修已结算" && vf === "收费类型");
2566
+ if (allowVf) {
2537
2567
  if (name.indexOf("盘亏") >= 0) { name = ""; code = ""; }
2538
2568
  else {
2539
2569
  const r = lookupTypeDim(d, name, vf, idx);
@@ -2546,7 +2576,17 @@ window.WWT_ENGINE = (function () {
2546
2576
  code = lookupDimCode(d, name, idx) || lookupDimByField(d, vf, name, idx);
2547
2577
  if (!code) {
2548
2578
  const t = idx.tables[d];
2549
- 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
+ }
2550
2590
  }
2551
2591
  }
2552
2592
  }
@@ -2563,6 +2603,16 @@ window.WWT_ENGINE = (function () {
2563
2603
  }
2564
2604
  out[d] = { name: name, code: code };
2565
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
+
2566
2616
  return out;
2567
2617
  }
2568
2618
 
@@ -2611,7 +2661,7 @@ window.WWT_ENGINE = (function () {
2611
2661
  function truncateBiz(s, sep) {
2612
2662
  const arr = clean(s).split(sep).filter(Boolean);
2613
2663
  if (arr.length <= 3) return arr.join(sep);
2614
- return arr.slice(0, 3).join(sep) + sep + "" + (arr.length - 3) + "张";
2664
+ return arr.slice(0, 3).join(sep) + sep + "等共" + arr.length + "张";
2615
2665
  }
2616
2666
  function buildSummaryCtx(items, rule, curSrc) {
2617
2667
  const meta = C.SOURCE_META[curSrc] || {};
@@ -2760,16 +2810,28 @@ window.WWT_ENGINE = (function () {
2760
2810
 
2761
2811
  // Step A:分桶(决定几张凭证)
2762
2812
  const genMode = opts.modeOverride || rule.genMode || "一对一";
2813
+ const gb = opts.groupByOverride || rule.genGroupBy || "";
2763
2814
  let buckets = [];
2764
2815
  if (genMode === "一对一") {
2765
2816
  buckets = rowsIn.map(it => [it]);
2766
2817
  } else if (genMode === "全部汇总") {
2767
- 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
+ }
2768
2830
  } else {
2769
- const gb = opts.groupByOverride || rule.genGroupBy || "单位名称";
2831
+ const useGb = gb || "单位名称";
2770
2832
  const m = new Map();
2771
2833
  for (const it of rowsIn) {
2772
- const v = clean(it.row[gb]) || "(空)";
2834
+ const v = clean(it.row[useGb]) || "(空)";
2773
2835
  if (!m.has(v)) m.set(v, []);
2774
2836
  m.get(v).push(it);
2775
2837
  }
@@ -2785,11 +2847,15 @@ window.WWT_ENGINE = (function () {
2785
2847
  stats.vouchers++;
2786
2848
 
2787
2849
  // Step B:sub_bucket = group_key值 × 分支lines
2788
- 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 : "单位名称";
2789
2854
  const subMap = new Map();
2790
2855
  for (const it of bucket) {
2791
2856
  const gv = groupKeyField ? (clean(it.row[groupKeyField]) || "(空)") : "";
2792
- 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)]);
2793
2859
  if (!subMap.has(k)) subMap.set(k, { gv: gv, lines: it._lines, items: [], acct: {} });
2794
2860
  subMap.get(k).items.push(it);
2795
2861
  }
@@ -2905,8 +2971,18 @@ window.WWT_ENGINE = (function () {
2905
2971
  * covered:被规则命中(candidate→产出凭证)
2906
2972
  * candidate_missed:满足某条规则全部 = 条件、但未生成凭证(一般 = 缺编码/客户未匹配);P1.2.15 起 generate() 主循环无此中间态,固定 0
2907
2973
  * excluded:既不满足任一规则的 = 条件,也未被命中(设计排除——例:本场景对账S规则不接 冲转F/挂帐)
2974
+ * missing_code_count / missing_code_amount:生成后 _缺失编码 非空的分录(严格维度有名称无编码)
2908
2975
  * 通过明细分桶,让 UI 能清楚告诉用户"漏了哪几单、为什么不原因"。 ---- */
2909
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
+ }
2910
2986
  const bySource = [];
2911
2987
  for (const curSrc of Object.keys(usedRows)) {
2912
2988
  const rows = (opts.biz && opts.biz[curSrc]) || [];
@@ -2934,6 +3010,7 @@ window.WWT_ENGINE = (function () {
2934
3010
  金额: amountCol ? num(row[amountCol]) : 0,
2935
3011
  });
2936
3012
  }
3013
+ const mc = missingCodeBySrc[curSrc] || { count: 0, amount: 0 };
2937
3014
  bySource.push({
2938
3015
  source: curSrc,
2939
3016
  total: rows.length,
@@ -2944,6 +3021,8 @@ window.WWT_ENGINE = (function () {
2944
3021
  excluded_amount: round2(excludedAmt),
2945
3022
  excluded_clipped: totalUncovered > 200,
2946
3023
  excluded_rows: exclRows,
3024
+ missing_code_count: mc.count,
3025
+ missing_code_amount: round2(mc.amount),
2947
3026
  unmatched_amount: round2(excludedAmt),
2948
3027
  amount_col: amountCol,
2949
3028
  never_matched: totalUncovered,
@@ -2988,6 +3067,31 @@ window.WWT_ENGINE = (function () {
2988
3067
  };
2989
3068
  }
2990
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
+
2991
3095
  /* ---------------- 单条规则测试预览(voucher preview)----------------
2992
3096
  * 与 generate() 共用同一套 evalConditions / calcDimValues / leafSubject 逻辑,
2993
3097
  * 只是只跑传入的 rule,并在结果上挂一份"覆盖率审计":
@@ -3074,14 +3178,26 @@ window.WWT_ENGINE = (function () {
3074
3178
  for (const it of rowsIn) it._lines = resolveEffectiveLines(r, it.row, curSrc, idx);
3075
3179
 
3076
3180
  const genMode = r.genMode;
3181
+ const gb = r.genGroupBy || "";
3077
3182
  let buckets = [];
3078
3183
  if (genMode === "一对一") buckets = rowsIn.map(it => [it]);
3079
- else if (genMode === "全部汇总") buckets = [rowsIn];
3080
- else {
3081
- 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 || "单位名称";
3082
3198
  const m = new Map();
3083
3199
  for (const it of rowsIn) {
3084
- const v = clean(it.row[gb]) || "(空)";
3200
+ const v = clean(it.row[useGb]) || "(空)";
3085
3201
  if (!m.has(v)) m.set(v, []);
3086
3202
  m.get(v).push(it);
3087
3203
  }
@@ -3093,7 +3209,8 @@ window.WWT_ENGINE = (function () {
3093
3209
  const curVno = vno++;
3094
3210
  let eidx = 0;
3095
3211
  stats.vouchers++;
3096
- const groupKeyField = (genMode === "一对一") ? null : "单位名称";
3212
+ const groupKeyField = (genMode === "一对一") ? null :
3213
+ (genMode === "全部汇总" && gb && gb !== "单位名称") ? null : "单位名称";
3097
3214
  const subMap = new Map();
3098
3215
  for (const it of bucket) {
3099
3216
  const gv = groupKeyField ? (clean(it.row[groupKeyField]) || "(空)") : "";
@@ -3289,6 +3406,8 @@ window.WWT_ENGINE = (function () {
3289
3406
  // 兼容旧字段(仍叫 never_matched,但其内容仅含真正应处理子集漏命中的行)
3290
3407
  never_matched: candidatesMissed.length,
3291
3408
  unmatched_amount: round2(unmatchedAmt),
3409
+ missing_code_count: missingCodeCount,
3410
+ missing_code_amount: round2(failedAmount),
3292
3411
  amount_col: amountCol,
3293
3412
  never_matched_rows: candidatesMissed,
3294
3413
  excluded_rows: excludedRows,
@@ -3462,7 +3581,7 @@ window.WWT_ENGINE = (function () {
3462
3581
  parseAmtExpr, sumAmtExpr, evalAmtExpr, isAutoDiff, splitFieldsOf,
3463
3582
  calcDimValues, leafSubject, resolveEffectiveLines, lookupTypeDim,
3464
3583
  isExcludedByRule, isExcludedByGlobals,
3465
- generate, balanceCheck, buildKingdee, detect, auditRule,
3584
+ generate, balanceCheck, buildKingdee, detect, auditRule, checkSubjects,
3466
3585
  lookupMap, lookupTypeMap,
3467
3586
  };
3468
3587
  })();
@@ -3486,7 +3605,7 @@ const S = window.__STATE__ = {
3486
3605
  };
3487
3606
 
3488
3607
  /* ---------------- 版本 & 框架自动更新(只更新程序本身,本地数据/规则不动) ---------------- */
3489
- const WWT_VER=(function(){const raw='P1.2.36';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;})();
3490
3609
  const UPD_URL_KEY='wwt_fe_updurl';
3491
3610
  // 默认更新源:Gitee 上本项目的 version.json(API 接口,浏览器可直接跨域读)
3492
3611
  const DEFAULT_UPD_URL='https://gitee.com/api/v5/repos/wwtmrhu/certificate-generation/contents/dist/version.json';
@@ -3998,7 +4117,7 @@ function _treeRenderBody(){
3998
4117
  const q=(_treeQ||'').trim().toLowerCase();
3999
4118
  const groups=[
4000
4119
  {kind:'base', title:'基础数据', slots:C.BASE_SLOTS, total:9},
4001
- {kind:'biz', title:'业务数据', slots:C.BIZ_SLOTS, total:8},
4120
+ {kind:'biz', title:'业务数据', slots:C.BIZ_SLOTS, total:9},
4002
4121
  ];
4003
4122
  let html='', vis=0;
4004
4123
  for(const g of groups){
@@ -4774,10 +4893,6 @@ function renderRules(){
4774
4893
  const conds=(r.conditions||[]).map(c=>'<span class="cond"><b>'+esc(c.field)+'</b> '+esc(c.op)+' '+esc(c.value)+'</span>').join('');
4775
4894
  let body='<div class="rulebody" id="rb'+i+'">';
4776
4895
  body+='<div style="margin:6px 0"><b>命中条件</b>('+(r.logic||'AND')+'):'+(conds||'<span style="color:#8b949e">无条件(全表命中)</span>')+'</div>';
4777
- if((r.excludeWhen||[]).length){
4778
- const exc=r.excludeWhen.map(c=>'<span class="cond"><b>'+esc(c.field)+'</b> '+esc(c.op)+' '+esc(c.value)+'</span>').join('');
4779
- 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>';
4780
- }
4781
4896
  if((r.branches||[]).length){
4782
4897
  body+='<div><b>分支</b>:'+r.branches.map((b,bi)=>
4783
4898
  '<div class="linechip"><span class="tag b">分支'+(bi+1)+' '+esc(b.name||'')+'</span>'+
@@ -4791,7 +4906,7 @@ function renderRules(){
4791
4906
  '<span class="tag b" style="margin-left:6px">'+esc(r.id)+'</span>'+
4792
4907
  (r.enabled===false?'<span class="tag r" style="margin-left:4px">已停用</span>':'')+'</div>'+
4793
4908
  '<div class="rulemeta">来源:'+esc(r.source)+' · 生成:'+esc(r.genMode||'一对一')+
4794
- (r.genMode==='匹配汇总'?'(按 '+esc(r.genGroupBy||'单位名称')+')':'')+
4909
+ ((r.genMode==='匹配汇总' || (r.genMode==='全部汇总' && r.genGroupBy && r.genGroupBy!=='单位名称'))?'(按 '+esc(r.genGroupBy||'单位名称')+')':'')+
4795
4910
  (r.lineSplitBy?' · 拆分:'+esc(r.lineSplitBy):'')+'</div></div>'+
4796
4911
  '<div style="display:flex;gap:5px"><button class="mini" onclick="UI.toggleRuleBody('+i+')">展开/收起</button>'+
4797
4912
  '<button class="mini" onclick="UI.editRule('+i+')">✎ 编辑</button>'+
@@ -4857,6 +4972,13 @@ function populateEditorDatalists(){
4857
4972
  '支付方式','结算方式','摘要','备注','凭证号','分录序号','收据号','订单号','单据号','商品名称','型号','规格'];
4858
4973
  for(const c of common){ if(!seen.has(c)){ seen.add(c); opts.push('<option value="'+esc(c)+'">'); } }
4859
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
+ }
4860
4982
  }
4861
4983
  // 0b) 转换:常用 strip_suffix / strip / replace 模板
4862
4984
  const dlT=$('dlTransform');
@@ -5039,7 +5161,7 @@ function ruleFormHTML(){
5039
5161
  '<span class="w60">业务源</span><select id="rf_src">'+srcOpts+'</select>'+
5040
5162
  '<label class="chk"><input type="checkbox" id="rf_en"'+(r.enabled===false?'':' checked')+'>启用</label></div>'+
5041
5163
  '<div class="row"><span class="w60">生成方式</span><select id="rf_mode">'+modeOpts+'</select>'+
5042
- '<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="匹配汇总按此字段拆证;全部汇总如需按此字段拆证也填(默认单位名称表示不拆)">'+
5043
5165
  '<span class="w60">拆分字段</span><input class="w180" id="rf_split" value="'+esc(r.lineSplitBy||'')+'" title="多个用 + 或 , 分隔">'+
5044
5166
  '<span class="w60">条件逻辑</span><select id="rf_logic">'+logicOpts+'</select></div></div>';
5045
5167
 
@@ -5047,18 +5169,6 @@ function ruleFormHTML(){
5047
5169
  (r.conditions||[]).map((c,i)=>condRowHTML(i,c)).join('')+'</div>'+
5048
5170
  '<button class="mini" onclick="UI.addCond()">+ 加条件</button></div>';
5049
5171
 
5050
- // 【P1.2.34】规则级排除:满足条件的行整条规则不处理,且不计入覆盖率审计
5051
- h+='<div class="sec" style="background:#fff7e6;border-left:3px solid #fa8c16"><h4>排除条件(满足这些条件的行<b>整条不处理</b>,不计入审计·不显示在排除/漏命中里)</h4>'+
5052
- '<div style="font-size:11.5px;color:#888;margin:3px 0">典型场景:「厂牌 = 资产工具库」时由其他流程直接处理,本规则不参与。留空 = 不启用排除。</div>'+
5053
- '<div class="row"><span class="w60">逻辑</span>'+
5054
- '<select class="w80" id="rf_exclogic">'+
5055
- '<option value="AND"'+((r.excludeLogic||'AND')==='AND'?' selected':'')+'>AND(全部满足)</option>'+
5056
- '<option value="OR"'+(r.excludeLogic==='OR'?' selected':'')+'>OR(任一满足)</option>'+
5057
- '</select></div>'+
5058
- '<div id="rf_exconds">'+
5059
- (r.excludeWhen||[]).map((c,i)=>condRowHTML(i,c)).join('')+'</div>'+
5060
- '<button class="mini" onclick="UI.addExcludeCond()">+ 加排除条件</button></div>';
5061
-
5062
5172
  h+='<div class="sec"><h4>分支(可选:按条件走不同分录;命中第一个分支就停)</h4><div id="rf_branches">'+
5063
5173
  (r.branches||[]).map((b,bi)=>branchHTML(bi,b)).join('')+'</div>'+
5064
5174
  '<button class="mini" onclick="UI.addBranch()">+ 加分支</button></div>';
@@ -5098,7 +5208,7 @@ function lineHTML(bi,li,l){
5098
5208
  return '<div class="sec linerow" data-bi="'+bi+'" data-li="'+li+'" style="background:#fff;margin:5px 0">'+
5099
5209
  '<div class="row"><select class="w60" data-lf="side">'+sideOpts+'</select>'+
5100
5210
  '<span class="w60">科目</span><input class="w140" data-lf="subject" value="'+esc(l.subject||'')+'" placeholder="如 1122 / 6002" list="dlSubjects" title="可下拉选会计科目表里的编码;也可手动输入">'+
5101
- '<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">'+
5102
5212
  '<button class="mini del" onclick="UI.delRow(this,\'linerow\')">删除分录</button></div>'+
5103
5213
  '<div class="row"><span class="w60">摘要</span><input class="grow" data-lf="summary" value="'+esc(l.summary||'')+'" placeholder="{单位名称}{收入板块}对帐S{原单号}"></div>'+
5104
5214
  '<div style="font-size:11.5px;color:#57606a;margin:3px 0">辅助核算维度:</div>'+
@@ -5128,14 +5238,6 @@ function syncFormToObj(){
5128
5238
  return {field:g('field').value.trim(),op:g('op').value,value:g('value').value.trim(),
5129
5239
  logic:g('logic').value||undefined};
5130
5240
  });
5131
- // 【P1.2.34】排除条件:满足时整条规则不处理、不进 audit
5132
- r.excludeLogic=$('rf_exclogic') ? $('rf_exclogic').value : 'AND';
5133
- const exConds=[...document.querySelectorAll('#rf_exconds > .condrow')].map(row=>{
5134
- const g=f=>row.querySelector('[data-f="'+f+'"]');
5135
- return {field:g('field').value.trim(),op:g('op').value,value:g('value').value.trim(),
5136
- logic:g('logic').value||undefined};
5137
- });
5138
- r.excludeWhen=exConds.length ? exConds : undefined;
5139
5241
  // 分支
5140
5242
  const bsecs=[...document.querySelectorAll('#rf_branches > [data-bi]')];
5141
5243
  r.branches=bsecs.map(sec=>{
@@ -5209,7 +5311,7 @@ function doGenerate(){
5209
5311
  const bs=(res.audit&&res.audit.by_source)||[];
5210
5312
  if(bs.length){
5211
5313
  h+='<div class="banner" style="margin-top:6px"><b>📊 业务源覆盖率</b> <span style="color:#57606a;font-size:12px">(按源表汇总:被覆盖行 / 真漏行(候选未命中) / 被排除行(不满足条件);点开看具体单号)</span>'+
5212
- '<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>'+
5213
5315
  bs.map(b=>{
5214
5316
  const cm=b.candidate_missed||0;
5215
5317
  const excl=b.excluded||0;
@@ -5229,6 +5331,8 @@ function doGenerate(){
5229
5331
  if(firstExcl.原单号) reasonBits.push('原单号='+firstExcl.原单号);
5230
5332
  }
5231
5333
  const exclReason = reasonBits.slice(0,2).join(',');
5334
+ const mc=b.missing_code_count||0;
5335
+ const ma=b.missing_code_amount||0;
5232
5336
  return '<tr'+cmCls+'>'+
5233
5337
  '<td><code>'+esc(b.source)+'</code></td>'+
5234
5338
  '<td class="num">'+total+'</td>'+
@@ -5256,10 +5360,13 @@ function doGenerate(){
5256
5360
  '</details>'+
5257
5361
  '</div>'
5258
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>'+
5259
5365
  '<td>'+(b.amount_col?esc(b.amount_col):'<span style="color:#8b949e">(无)</span>')+'</td>'+
5260
5366
  '<td>'+(cm>0
5261
5367
  ? '<span style="color:#a40e26">⚠ '+cm+' 行满足条件却未生成凭证(点开被排除行明细确认是规则没命中还是缺编码)</span>'
5262
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>':'')+
5263
5370
  (excl>0?'<br><span style="color:#92400e;font-weight:600;font-size:11.5px">另外 '+excl+' 行被本批规则排除(金额 ¥'+fmt(exclAmount)+'),点开「展开全部明细」看哪些单</span>':'')+
5264
5371
  '</td></tr>';
5265
5372
  }).join('')+
@@ -5301,6 +5408,31 @@ function doGenerate(){
5301
5408
  * 底部按钮:📤 导出试入金蝶(直接下载本规则的金蝶导入 Excel)/ 关闭
5302
5409
  * ============================================================ */
5303
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
+
5304
5436
  function unmatchedTableHtml(rows, sourceKey){
5305
5437
  const partyLabel = esc((C.SOURCE_META[sourceKey] && C.SOURCE_META[sourceKey].party) || '单位名称');
5306
5438
  return '<div class="tablewrap" style="max-height:280px;margin-top:6px"><table><thead><tr>'+
@@ -5364,7 +5496,7 @@ function previewRule(ruleId){
5364
5496
 
5365
5497
  let h='<div class="modalbox" style="max-width:980px"><div class="modalhd"><h3>🧪 规则预览 · '+esc(r.name||r.id)+
5366
5498
  '</h3><span style="color:#57606a;font-size:12px;margin-left:8px">'+esc(r.source)+' · '+esc(r.genMode||'一对一')+
5367
- (r.genMode==='匹配汇总'?'(按 '+esc(r.genGroupBy||'单位名称')+')':'')+' · '+linesTxt+'</span>'+
5499
+ ((r.genMode==='匹配汇总' || (r.genMode==='全部汇总' && r.genGroupBy && r.genGroupBy!=='单位名称'))?'(按 '+esc(r.genGroupBy||'单位名称')+')':'')+' · '+linesTxt+'</span>'+
5368
5500
  '<button class="mini" onclick="UI.closeModal()">✕ 关闭</button></div><div class="modalbd">';
5369
5501
 
5370
5502
  // 顶部 banner:先说"覆盖率 N% + 命中数 / 应处理子集",再分别说"应处理子集漏命中" 与 "源表被规则排除"
@@ -5426,23 +5558,35 @@ function previewRule(ruleId){
5426
5558
  if(!pvRows.length){
5427
5559
  h+='<div style="color:#8b949e;padding:8px">本规则没生成任何凭证('+esc(r0.candidate_in_source?'无候选行':'源表是空的')+')。</div>';
5428
5560
  }else{
5561
+ // 【P1.2.48】凭证预览表格改为金蝶式「核算维度」单列
5562
+ let previewMissingDims=[];
5429
5563
  h+='<div class="tablewrap" style="max-height:340px"><table><thead><tr>'+
5430
5564
  '<th>凭证号</th><th>分录序号</th><th>业务日期</th><th>摘要</th><th>科目</th><th>借方</th><th>贷方</th>'+
5431
- '<th>客户</th><th>部门</th><th>收入类型</th><th>成本类型</th></tr></thead><tbody>'+
5432
- pvRows.map(e=>'<tr'+(e['_缺失编码']?' class="bad"':'')+'>'+
5433
- '<td><b>'+esc(e['凭证号'])+'</b></td><td>'+esc(e['分录序号'])+'</td>'+
5434
- '<td>'+esc(e['业务日期']||'')+'</td><td>'+esc((e['摘要']||'').slice(0,40))+'</td>'+
5435
- '<td><b>'+esc(e['科目编码'])+'</b> '+esc(e['科目名称']||'')+'</td>'+
5436
- '<td class="num">'+(e['借方金额']?fmt(e['借方金额']):'')+'</td>'+
5437
- '<td class="num">'+(e['贷方金额']?fmt(e['贷方金额']):'')+'</td>'+
5438
- '<td>'+esc((e['客户名称']||'')+' '+(e['客户编码']||''))+'</td>'+
5439
- '<td>'+esc((e['部门名称']||'')+' '+(e['部门编码']||''))+'</td>'+
5440
- '<td>'+esc((e['收入类型名称']||'')+' '+(e['收入类型编码']||''))+'</td>'+
5441
- '<td>'+esc((e['成本类型名称']||'')+' '+(e['成本类型编码']||''))+'</td></tr>').join('')+
5442
- '</tbody></table></div>'+
5443
- '<div style="margin-top:5px;color:#57606a;font-size:12px">共 <b>'+pvStats.vouchers+'</b> 张凭证,<b>'+
5444
- 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>':' · 无编码缺失')+
5445
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
+ }
5446
5590
  }
5447
5591
 
5448
5592
  h+='</div><div class="modalft">'+
@@ -5474,12 +5618,18 @@ function exportRulePreview(){
5474
5618
  const ma=[C.MISSING_HEAD].concat(pMissing.map(m=>C.MISSING_HEAD.map(h=>m[h]||'')));
5475
5619
  XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(ma),'缺失编码清单');
5476
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
+ }
5477
5626
  const fname='金蝶导入_'+ (r.name||r.id).replace(/[\\/:*?"<>|]/g,'_').slice(0,30) + '_' + r.id + '.xlsx';
5478
5627
  XLSX.writeFile(wb, fname);
5479
5628
  const whFbk = aoa.whFallback || 0;
5480
5629
  const incFbk = aoa.incMiss || 0;
5481
5630
  let msg='已导出 '+fname+(whFbk?'(含 '+whFbk+' 条仓库 LJ 兜底)':'');
5482
5631
  if (incFbk) msg += '(含 '+incFbk+' 条 6050 收入类型兜底,建议去「③ 规则」补 dim 收入类型)';
5632
+ if (pSubj.length) msg += '(⚠ '+pSubj.length+' 个科目编码在会计科目表里查不到,金蝶导入会失败)';
5483
5633
  toast(msg);
5484
5634
  }catch(e){ toast('导出失败:'+e.message); }
5485
5635
  }
@@ -5487,8 +5637,9 @@ function exportRulePreview(){
5487
5637
  /* ============================================================
5488
5638
  * ⑤ 凭证审核
5489
5639
  * ============================================================ */
5640
+ // 【P1.2.48】审核表格把客户/供应商/部门/员工/仓库/收入类型/成本类型等合并成「核算维度」单列
5490
5641
  const AUD_COLS=['凭证号','分录序号','记账日期','业务日期','摘要','科目编码','科目名称','借方金额','贷方金额',
5491
- '客户名称','部门名称','收入类型名称','成本类型名称','供应商名称','结算方式名称','来源单据','规则名称'];
5642
+ '核算维度','结算方式名称','来源单据','规则名称'];
5492
5643
  const AUD_NUM=['借方金额','贷方金额'];
5493
5644
  let audPage=1;
5494
5645
  /** S.draft 里"分组来源":cv_upload | 业务源 key(如 销售已确认) */
@@ -5561,7 +5712,12 @@ function renderAudit(){
5561
5712
  /* ---- 3) 选组过滤 ---- */
5562
5713
  let rows=S.draft.map((r,i)=>({r:r,i:i,g:_srcGroup(r)}));
5563
5714
  if(group!=='all') rows=rows.filter(o=>o.g===group);
5564
- 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
+ );
5565
5721
 
5566
5722
  /* ---- 4) 不平标记(只算当前 visible 组)---- */
5567
5723
  const bad={};
@@ -5577,8 +5733,13 @@ function renderAudit(){
5577
5733
  let h='<table><thead><tr><th style="width:34px"></th>'+AUD_COLS.map(c=>'<th>'+esc(c)+'</th>').join('')+'</tr></thead><tbody>';
5578
5734
  h+=slice.map(o=>{
5579
5735
  const r=o.r;
5580
- 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>'+
5581
5739
  AUD_COLS.map(c=>{
5740
+ if(c==='核算维度'){
5741
+ return '<td>'+dim.html+'</td>';
5742
+ }
5582
5743
  const v=nz(r[c]);
5583
5744
  const canEdit=['摘要','科目编码','借方金额','贷方金额','记账日期','业务日期'].indexOf(c)>=0||C.DIM_LIST.some(d=>C.DIM_DISPLAY[d][0]===c||C.DIM_DISPLAY[d][1]===c);
5584
5745
  const cls=AUD_NUM.indexOf(c)>=0?'num':'';
@@ -5714,17 +5875,29 @@ function doExport(){
5714
5875
  const ma=[C.MISSING_HEAD].concat(S.missing.map(m=>C.MISSING_HEAD.map(h=>m[h]||'')));
5715
5876
  XLSX.utils.book_append_sheet(wb,XLSX.utils.aoa_to_sheet(ma),'缺失编码清单');
5716
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
+ }
5717
5883
  const fname='金蝶导入.xlsx';
5718
5884
  XLSX.writeFile(wb,fname);
5719
5885
  // 【P1.2.12】把仓库空兜底条数展示给用户,让他知道哪些行被自动补了
5720
5886
  const whFbk = aoa.whFallback || 0;
5721
5887
  // 【P1.2.21】6050 收入类型兜底条数(应该 =0;>0 提示用户该规则行漏配 dim 收入类型)
5722
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
+ : '';
5723
5895
  $('expBanner').innerHTML='<div class="banner ok">✅ 已导出 <b>'+fname+'</b>:'+S.draft.length+' 条分录,'+
5724
5896
  (aoa.length-2)+' 行数据'+(S.missing&&S.missing.length?',另含「缺失编码清单」'+S.missing.length+' 行':'')+
5897
+ (missSubj.length?',另含「缺失科目清单」'+missSubj.length+' 行':'')+
5725
5898
  (whFbk ? ',<b>🔧 '+whFbk+' 行仓库列被自动补 LJ 兜底</b>(建议到「② 映射」补厂牌→仓库映射后重跑)' : '')+
5726
5899
  (incFbk ? ',<b>🛟 '+incFbk+' 行 6050 收入类型被自动补 089</b>(你这条规则漏了「收入类型」维度,建议去「③ 规则」补上)' : '')+
5727
- '</div>';
5900
+ '</div>'+subjWarn;
5728
5901
  $('expStat').innerHTML='文件已下载到浏览器的下载目录(一般为「下载」文件夹)。导入金蝶前请确认:<b>前两行(字段键行)不要删</b>。'+
5729
5902
  (whFbk ? '<br><span style="color:#d4880f">⚠ '+whFbk+' 行仓库列被自动补 LJ:去「② 映射→① 厂牌→仓库」点「🔄 自动扫描」把所有业务厂牌拉进来,下次导出就不会再兜底了。</span>' : '')+
5730
5903
  (incFbk ? '<br><span style="color:#d4880f">⚠ '+incFbk+' 行 6050(其他业务收入)缺「收入类型」维度,引擎已自动补 089/其他业务收入-其他。建议去「③ 规则」找对应规则的 6050 行,加上 dim=收入类型 fixedCode=089 fixedValue=其他业务收入-其他。</span>' : '');
@@ -6887,13 +7060,6 @@ window.UI={
6887
7060
  else editing.conditions.push({field:'',op:'=',value:'',logic:''});
6888
7061
  redrawRuleForm();
6889
7062
  },
6890
- // 【P1.2.34】加排除条件:写到 #rf_exconds
6891
- addExcludeCond:()=>{
6892
- syncFormToObj();
6893
- if(!editing.excludeWhen) editing.excludeWhen=[];
6894
- editing.excludeWhen.push({field:'',op:'=',value:'',logic:''});
6895
- redrawRuleForm();
6896
- },
6897
7063
  addBranch:()=>{
6898
7064
  syncFormToObj();
6899
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.36",
3
- "npm": "1.2.36",
4
- "url": "https://cdn.jsdelivr.net/npm/wwt-certificate-tool@1.2.36/dist/app.html",
5
- "url_unpkg": "https://unpkg.com/wwt-certificate-tool@1.2.36/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.36.html",
7
- "note": "「全局排除」字段可点选升级:原 P1.2.35 的字段名输入框是普通文本框,用户每次要手敲且容易跨业务源选错字段。P1.2.36 把字段名从「输入」改成「点选」:① 为每个业务源分组(销售已确认/入库已确认/出库未确认 8 个)生成独立的 datalist(dlGe_<业务源 key>),只列该业务源上传后的列名 + 常用字段名兜底,确保看不到别的业务源的字段;② 字段输入框加 ▼ 下拉箭头 + 淡黄底色背景,提示「点此选字段」;③ 分组标题同步显示「N 个条件 · N 个可选字段」,让用户进弹窗第一眼就知道每个分组有哪些字段可选。回归 202/202 全过,浏览器实测 5/6 通过(1 项为测试脚本在条件行未添加前的判断漏洞,不影响实际功能)。"
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.36",
3
+ "version": "1.2.49",
4
4
  "description": "WWT 金蝶凭证工具 · 纯前端版(离线单文件 HTML,双击即用)",
5
5
  "keywords": [
6
6
  "WWT",