wwt-certificate-tool 1.2.32 → 1.2.34

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.32</title>
6
+ <title>WWT 金蝶凭证工具 · 纯前端版 P1.2.34</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}
@@ -305,7 +305,7 @@ tr.bad:hover td{background:#fbe9e9}
305
305
  <div class="brand">
306
306
  <span class="logo">WWT</span>
307
307
  <div>
308
- <h1>金蝶凭证工具 <span class="ver">P1.2.32</span></h1>
308
+ <h1>金蝶凭证工具 <span class="ver">P1.2.34</span></h1>
309
309
  <div class="sub">纯前端离线版 · 双击即用 · 数据只存在本机,不上传任何服务器</div>
310
310
  </div>
311
311
  </div>
@@ -2263,10 +2263,15 @@ window.WWT_ENGINE = (function () {
2263
2263
 
2264
2264
  /* ---------------- 金额表达式 ---------------- */
2265
2265
  function isAutoDiff(line) { return clean(line && line.amountField) === "__AUTO_DIFF__"; }
2266
+
2267
+ // 兼容旧接口:返回 { parts: [{fld, sign}], auto? }。新逻辑统一走 evalAmtExpr,
2268
+ // 这里只为了不让外部旧调用点(如果有)报错。
2266
2269
  function parseAmtExpr(expr) {
2267
2270
  const e = clean(expr);
2268
2271
  if (!e || e === "-") return null;
2269
2272
  if (e === "__AUTO_DIFF__") return { auto: true };
2273
+ // 含 * / ( ) 时视为新语法(按 evalAmtExpr 处理),parts 留空
2274
+ if (/[*\/()]/.test(e)) return { parts: [], v2: true };
2270
2275
  const parts = [];
2271
2276
  for (let p of e.split("+")) {
2272
2277
  p = p.trim();
@@ -2277,12 +2282,103 @@ window.WWT_ENGINE = (function () {
2277
2282
  }
2278
2283
  return parts.length ? { parts } : null;
2279
2284
  }
2285
+
2286
+ // ============== 表达式解析器(支持 + - * / 括号 与 字段名/数字常量) ==============
2287
+ // 语法:
2288
+ // expr := term ( ('+'|'-') term )*
2289
+ // term := atom ( ('*'|'/') atom )*
2290
+ // atom := '-' atom | number | identifier | '(' expr ')'
2291
+ // identifier := 除空白与运算符之外的连续字符(即字段名)
2292
+ //
2293
+ // 设计目标:会计场景常见公式——`实收金额+折扣费` / `-折扣费` / `实收金额*0.13` / `金额/数量` / `(金额+税额)*0.13`
2294
+ // tokenize 时跳过空白;遇到运算符边界时,前一段连续字符要么是字段名,要么是数字。
2295
+ function tokenizeExpr(s) {
2296
+ const toks = [];
2297
+ let i = 0;
2298
+ while (i < s.length) {
2299
+ const c = s.charAt(i);
2300
+ if (c === ' ' || c === '\t') { i++; continue; }
2301
+ if (c === '+' || c === '*' || c === '/' || c === '(' || c === ')') { toks.push({ t: c }); i++; continue; }
2302
+ if (c === '-') {
2303
+ // 单目 vs 二元判断:若前一个 token 是操作符/左括号/开头,则是单目
2304
+ const prev = toks[toks.length - 1];
2305
+ const isUnary = !prev || (prev.t !== ')' && !/^\d/.test(prev.v || '') && prev.t !== 'num' && prev.t !== 'id');
2306
+ toks.push({ t: isUnary ? 'unary-' : '-' });
2307
+ i++; continue;
2308
+ }
2309
+ // 读一段连续字符
2310
+ let j = i;
2311
+ while (j < s.length) {
2312
+ const cj = s.charAt(j);
2313
+ if (cj === ' ' || cj === '\t') break;
2314
+ if ('+*-/()'.indexOf(cj) >= 0) break;
2315
+ j++;
2316
+ }
2317
+ const seg = s.slice(i, j);
2318
+ i = j;
2319
+ if (seg) {
2320
+ // 判断是数字(允许可选前导 0/小数点)
2321
+ if (/^-?\d+(\.\d+)?$/.test(seg) || /^\.\d+$/.test(seg)) toks.push({ t: 'num', v: parseFloat(seg) });
2322
+ else toks.push({ t: 'id', v: seg });
2323
+ }
2324
+ }
2325
+ return toks;
2326
+ }
2327
+
2328
+ function evalAmtExpr(row, expr) {
2329
+ const e = clean(expr);
2330
+ if (!e || e === "-") return 0;
2331
+ const toks = tokenizeExpr(e);
2332
+ let pos = 0;
2333
+ function peek() { return toks[pos]; }
2334
+ function eat(t) { if (toks[pos] && toks[pos].t === t) { pos++; return true; } return false; }
2335
+ function err(m) { throw new Error('金额表达式错误: ' + m + ' (pos=' + pos + ', expr=' + e + ')'); }
2336
+
2337
+ function parseExpr() {
2338
+ let v = parseTerm();
2339
+ while (true) {
2340
+ const p = peek();
2341
+ if (!p) break;
2342
+ if (p.t === '+') { pos++; v = v + parseTerm(); }
2343
+ else if (p.t === '-') { pos++; v = v - parseTerm(); }
2344
+ else break;
2345
+ }
2346
+ return v;
2347
+ }
2348
+ function parseTerm() {
2349
+ let v = parseAtom();
2350
+ while (true) {
2351
+ const p = peek();
2352
+ if (!p) break;
2353
+ if (p.t === '*') { pos++; v = v * parseAtom(); }
2354
+ else if (p.t === '/') {
2355
+ pos++;
2356
+ const d = parseAtom();
2357
+ if (d === 0) throw new Error('金额表达式除零 (expr=' + e + ')');
2358
+ v = v / d;
2359
+ }
2360
+ else break;
2361
+ }
2362
+ return v;
2363
+ }
2364
+ function parseAtom() {
2365
+ const p = peek();
2366
+ if (!p) err('缺少操作数');
2367
+ if (p.t === 'unary-') { pos++; return -parseAtom(); }
2368
+ if (p.t === '(') { pos++; const v = parseExpr(); if (!eat(')')) err('缺少右括号'); return v; }
2369
+ if (p.t === 'num') { pos++; return p.v; }
2370
+ if (p.t === 'id') { pos++; return num(row[p.v]); }
2371
+ err('非法 token: ' + JSON.stringify(p));
2372
+ }
2373
+ const v = parseExpr();
2374
+ if (pos !== toks.length) err('未消费完: ' + toks.slice(pos).map(t => t.v || t.t).join(' '));
2375
+ return v;
2376
+ }
2377
+
2280
2378
  function sumAmtExpr(row, expr) {
2281
- const pe = parseAmtExpr(expr);
2282
- if (!pe || pe.auto) return 0;
2283
- let s = 0;
2284
- for (const it of pe.parts) s += num(row[it.fld]) * it.sign;
2285
- return s;
2379
+ if (isAutoDiff({ amountField: expr })) return 0;
2380
+ try { return round2(evalAmtExpr(row, expr)); }
2381
+ catch (e) { console.warn(e.message); return 0; }
2286
2382
  }
2287
2383
 
2288
2384
  /* ---------------- 映射查表 ---------------- */
@@ -2547,6 +2643,16 @@ window.WWT_ENGINE = (function () {
2547
2643
  }
2548
2644
  return (rule.lines || []).slice();
2549
2645
  }
2646
+
2647
+ // 【P1.2.34】规则级排除:当一行满足 rule.excludeWhen 全部条件时,
2648
+ // 本规则不处理,且不计入「被本规则排除」/「应处理子集漏命中」任何审计统计——
2649
+ // 业务上等同于这条行从未出现在本规则的视野里(用于「厂牌=资产工具库」这类
2650
+ // 由其他专门流程处理的合法跳过场景,避免覆盖率审计误报警报)。
2651
+ function isExcludedByRule(row, rule, curSrc, idx) {
2652
+ const ex = rule && rule.excludeWhen;
2653
+ if (!ex || !ex.length) return false;
2654
+ return evalConditions(row, ex, rule.excludeLogic || "AND", curSrc, idx);
2655
+ }
2550
2656
  function linesKey(lines) {
2551
2657
  return JSON.stringify(lines, (k, v) => (k === "_uid" ? undefined : v));
2552
2658
  }
@@ -2609,6 +2715,8 @@ window.WWT_ENGINE = (function () {
2609
2715
  for (let i = 0; i < rowsAll.length; i++) {
2610
2716
  if (usedRows[curSrc].has(i)) continue;
2611
2717
  const row = rowsAll[i];
2718
+ // 【P1.2.34】规则级排除:满足 excludeWhen 的行整条跳过,不进 rowsIn、不进 audit
2719
+ if (isExcludedByRule(row, rule, curSrc, idx)) continue;
2612
2720
  if (evalConditions(row, rule.conditions || [], rule.logic || "AND", curSrc, idx)) {
2613
2721
  rowsIn.push({ row: row, _i: i });
2614
2722
  }
@@ -2919,6 +3027,8 @@ window.WWT_ENGINE = (function () {
2919
3027
  for (let i = 0; i < rowsAll.length; i++) {
2920
3028
  if (usedRows.has(i)) continue;
2921
3029
  const row = rowsAll[i];
3030
+ // 【P1.2.34】规则级排除:跳过 excludeWhen 命中的行
3031
+ if (isExcludedByRule(row, r, curSrc, idx)) continue;
2922
3032
  if (evalConditions(row, r.conditions || [], r.logic || "AND", curSrc, idx)) {
2923
3033
  rowsIn.push({ row: row, _i: i });
2924
3034
  }
@@ -3071,9 +3181,16 @@ window.WWT_ENGINE = (function () {
3071
3181
  * excludedRows = 源表里不满足本规则的全部 = 条件 → 按规则就不该处理,不算遗漏
3072
3182
  * neverMatchedRows = candidatesMissed(兼容旧字段名) */
3073
3183
  const candidatesMissed = [], excludedRows = [];
3184
+ let ruleExcludedCount = 0, ruleExcludedAmount = 0;
3074
3185
  for (let i = 0; i < rowsAll.length; i++) {
3075
3186
  if (matchedIdxs.has(i)) continue;
3076
3187
  const row = rowsAll[i];
3188
+ // 【P1.2.34】规则级排除:满足 excludeWhen 的行不进任何 audit 统计
3189
+ if (isExcludedByRule(row, rule, curSrc, idx)) {
3190
+ ruleExcludedCount++;
3191
+ if (amountCol) ruleExcludedAmount += num(row[amountCol]);
3192
+ continue;
3193
+ }
3077
3194
  const rec = {
3078
3195
  原单号: clean(row[meta.no]) || clean(row["原单号"]),
3079
3196
  付款方式: clean(row["付款方式"]) || clean(row["支付方式"]),
@@ -3123,6 +3240,9 @@ window.WWT_ENGINE = (function () {
3123
3240
  candidate_missed_clipped: candidatesMissed.length >= 200, // 是否被 200 上限截断
3124
3241
  excluded: excludedRows.length, // 被本规则排除(不算遗漏)
3125
3242
  excluded_clipped: excludedRows.length >= 200,
3243
+ // 【P1.2.34】规则级排除:满足 excludeWhen 的行彻底从 audit 视野消失(业务上合法跳过)
3244
+ rule_excluded: ruleExcludedCount,
3245
+ rule_excluded_amount: round2(ruleExcludedAmount),
3126
3246
  // 兼容旧字段(仍叫 never_matched,但其内容仅含真正应处理子集漏命中的行)
3127
3247
  never_matched: candidatesMissed.length,
3128
3248
  unmatched_amount: round2(unmatchedAmt),
@@ -3296,8 +3416,9 @@ window.WWT_ENGINE = (function () {
3296
3416
  return {
3297
3417
  clean, num, round2, normPay, pad3,
3298
3418
  buildIndex, getField, compare, evalConditions, applyTransform,
3299
- parseAmtExpr, sumAmtExpr, isAutoDiff, splitFieldsOf,
3419
+ parseAmtExpr, sumAmtExpr, evalAmtExpr, isAutoDiff, splitFieldsOf,
3300
3420
  calcDimValues, leafSubject, resolveEffectiveLines, lookupTypeDim,
3421
+ isExcludedByRule,
3301
3422
  generate, balanceCheck, buildKingdee, detect, auditRule,
3302
3423
  lookupMap, lookupTypeMap,
3303
3424
  };
@@ -3322,7 +3443,7 @@ const S = window.__STATE__ = {
3322
3443
  };
3323
3444
 
3324
3445
  /* ---------------- 版本 & 框架自动更新(只更新程序本身,本地数据/规则不动) ---------------- */
3325
- const WWT_VER=(function(){const raw='P1.2.32';return raw.indexOf('__WVER__')>=0?'P1.2.10':raw;})();
3446
+ const WWT_VER=(function(){const raw='P1.2.34';return raw.indexOf('__WVER__')>=0?'P1.2.10':raw;})();
3326
3447
  const UPD_URL_KEY='wwt_fe_updurl';
3327
3448
  // 默认更新源:Gitee 上本项目的 version.json(API 接口,浏览器可直接跨域读)
3328
3449
  const DEFAULT_UPD_URL='https://gitee.com/api/v5/repos/wwtmrhu/certificate-generation/contents/dist/version.json';
@@ -4607,6 +4728,10 @@ function renderRules(){
4607
4728
  const conds=(r.conditions||[]).map(c=>'<span class="cond"><b>'+esc(c.field)+'</b> '+esc(c.op)+' '+esc(c.value)+'</span>').join('');
4608
4729
  let body='<div class="rulebody" id="rb'+i+'">';
4609
4730
  body+='<div style="margin:6px 0"><b>命中条件</b>('+(r.logic||'AND')+'):'+(conds||'<span style="color:#8b949e">无条件(全表命中)</span>')+'</div>';
4731
+ if((r.excludeWhen||[]).length){
4732
+ const exc=r.excludeWhen.map(c=>'<span class="cond"><b>'+esc(c.field)+'</b> '+esc(c.op)+' '+esc(c.value)+'</span>').join('');
4733
+ 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>';
4734
+ }
4610
4735
  if((r.branches||[]).length){
4611
4736
  body+='<div><b>分支</b>:'+r.branches.map((b,bi)=>
4612
4737
  '<div class="linechip"><span class="tag b">分支'+(bi+1)+' '+esc(b.name||'')+'</span>'+
@@ -4876,6 +5001,18 @@ function ruleFormHTML(){
4876
5001
  (r.conditions||[]).map((c,i)=>condRowHTML(i,c)).join('')+'</div>'+
4877
5002
  '<button class="mini" onclick="UI.addCond()">+ 加条件</button></div>';
4878
5003
 
5004
+ // 【P1.2.34】规则级排除:满足条件的行整条规则不处理,且不计入覆盖率审计
5005
+ h+='<div class="sec" style="background:#fff7e6;border-left:3px solid #fa8c16"><h4>排除条件(满足这些条件的行<b>整条不处理</b>,不计入审计·不显示在排除/漏命中里)</h4>'+
5006
+ '<div style="font-size:11.5px;color:#888;margin:3px 0">典型场景:「厂牌 = 资产工具库」时由其他流程直接处理,本规则不参与。留空 = 不启用排除。</div>'+
5007
+ '<div class="row"><span class="w60">逻辑</span>'+
5008
+ '<select class="w80" id="rf_exclogic">'+
5009
+ '<option value="AND"'+((r.excludeLogic||'AND')==='AND'?' selected':'')+'>AND(全部满足)</option>'+
5010
+ '<option value="OR"'+(r.excludeLogic==='OR'?' selected':'')+'>OR(任一满足)</option>'+
5011
+ '</select></div>'+
5012
+ '<div id="rf_exconds">'+
5013
+ (r.excludeWhen||[]).map((c,i)=>condRowHTML(i,c)).join('')+'</div>'+
5014
+ '<button class="mini" onclick="UI.addExcludeCond()">+ 加排除条件</button></div>';
5015
+
4879
5016
  h+='<div class="sec"><h4>分支(可选:按条件走不同分录;命中第一个分支就停)</h4><div id="rf_branches">'+
4880
5017
  (r.branches||[]).map((b,bi)=>branchHTML(bi,b)).join('')+'</div>'+
4881
5018
  '<button class="mini" onclick="UI.addBranch()">+ 加分支</button></div>';
@@ -4913,7 +5050,7 @@ function lineHTML(bi,li,l){
4913
5050
  return '<div class="sec linerow" data-bi="'+bi+'" data-li="'+li+'" style="background:#fff;margin:5px 0">'+
4914
5051
  '<div class="row"><select class="w60" data-lf="side">'+sideOpts+'</select>'+
4915
5052
  '<span class="w60">科目</span><input class="w140" data-lf="subject" value="'+esc(l.subject||'')+'" placeholder="如 1122 / 6002" list="dlSubjects" title="可下拉选会计科目表里的编码;也可手动输入">'+
4916
- '<span class="w60">金额</span><input class="w180" data-lf="amountField" value="'+esc(l.amountField||'')+'" placeholder="实收金额+折扣费 / -折扣费 / __AUTO_DIFF__" list="dlBizFields" title="多列用 + 连接;减用 - 前缀;__AUTO_DIFF__ 自动补差额">'+
5053
+ '<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__ 自动补差额">'+
4917
5054
  '<button class="mini del" onclick="UI.delRow(this,\'linerow\')">删除分录</button></div>'+
4918
5055
  '<div class="row"><span class="w60">摘要</span><input class="grow" data-lf="summary" value="'+esc(l.summary||'')+'" placeholder="{单位名称}{收入板块}对帐S{原单号}"></div>'+
4919
5056
  '<div style="font-size:11.5px;color:#57606a;margin:3px 0">辅助核算维度:</div>'+
@@ -4943,6 +5080,14 @@ function syncFormToObj(){
4943
5080
  return {field:g('field').value.trim(),op:g('op').value,value:g('value').value.trim(),
4944
5081
  logic:g('logic').value||undefined};
4945
5082
  });
5083
+ // 【P1.2.34】排除条件:满足时整条规则不处理、不进 audit
5084
+ r.excludeLogic=$('rf_exclogic') ? $('rf_exclogic').value : 'AND';
5085
+ const exConds=[...document.querySelectorAll('#rf_exconds > .condrow')].map(row=>{
5086
+ const g=f=>row.querySelector('[data-f="'+f+'"]');
5087
+ return {field:g('field').value.trim(),op:g('op').value,value:g('value').value.trim(),
5088
+ logic:g('logic').value||undefined};
5089
+ });
5090
+ r.excludeWhen=exConds.length ? exConds : undefined;
4946
5091
  // 分支
4947
5092
  const bsecs=[...document.querySelectorAll('#rf_branches > [data-bi]')];
4948
5093
  r.branches=bsecs.map(sec=>{
@@ -6584,6 +6729,13 @@ window.UI={
6584
6729
  else editing.conditions.push({field:'',op:'=',value:'',logic:''});
6585
6730
  redrawRuleForm();
6586
6731
  },
6732
+ // 【P1.2.34】加排除条件:写到 #rf_exconds
6733
+ addExcludeCond:()=>{
6734
+ syncFormToObj();
6735
+ if(!editing.excludeWhen) editing.excludeWhen=[];
6736
+ editing.excludeWhen.push({field:'',op:'=',value:'',logic:''});
6737
+ redrawRuleForm();
6738
+ },
6587
6739
  addBranch:()=>{
6588
6740
  syncFormToObj();
6589
6741
  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.32",
3
- "npm": "1.2.32",
4
- "url": "https://cdn.jsdelivr.net/npm/wwt-certificate-tool@1.2.32/dist/app.html",
5
- "url_unpkg": "https://unpkg.com/wwt-certificate-tool@1.2.32/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.32.html",
7
- "note": "界面变大适配列宽:① 上传页移除「🌲」树形入口和卡片旁按钮,只保留一个「📤 上传 Excel 数据」按钮 + 拖拽 + 点卡片(已上传→抽屉,未上传→上传弹窗),避免入口太多混淆;② 数据浏览抽屉填满窗口(dv-panel left:0/right:0 锁死,去掉之前 max-width:1100px 上限,1920px 屏幕不再留大块灰色空白);③ 左列 240→260px 防止「收入类型确认(采购·已确认)」被截断;④ 抽屉表格列宽自适应(table-layout:auto + max-content,按付款方式/付款确认等长字段完整显示)。"
2
+ "ver": "P1.2.34",
3
+ "npm": "1.2.34",
4
+ "url": "https://cdn.jsdelivr.net/npm/wwt-certificate-tool@1.2.34/dist/app.html",
5
+ "url_unpkg": "https://unpkg.com/wwt-certificate-tool@1.2.34/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.34.html",
7
+ "note": " 新增「规则级排除」机制:规则上加 excludeWhen 字段(结构同 conditions,含 excludeLogic),满足条件的行整条规则不处理,**且不进覆盖率审计**(不进 excluded_rows、不进 candidate_missed、不进任何 audit 统计);用于「厂牌=资产工具库」等由其他流程直接处理的合法跳过场景。② UI:规则编辑面板增加「排除条件」独立配置区(橙底强调),规则列表同步显示摘要;「+ 加排除条件」按钮一键加。③ auditRule 返回值新增 rule_excluded/rule_excluded_amount 字段(供后续 UI 展示)。例:「厂牌 = 资产工具库」时整条不处理、不计入审计。"
8
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wwt-certificate-tool",
3
- "version": "1.2.32",
3
+ "version": "1.2.34",
4
4
  "description": "WWT 金蝶凭证工具 · 纯前端版(离线单文件 HTML,双击即用)",
5
5
  "keywords": [
6
6
  "WWT",