feat(est-test): 단가 도구(중복 정리·단가정의 엑셀·할증/낙찰/복원·바꾸기·SUM)와 항목선택계산 색인 번호 읽기
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
"""EST 테스트 앱 단가 도구(원본 중복 항목 정리 · 단가정의 엑셀 · 할증/낙찰/복원 · 항목선택계산 SUM 줄)."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from test_est_rea_parity import run
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def run_js(code: str):
|
||||
runner = f"""
|
||||
const fs = require('fs'), vm = require('vm');
|
||||
const ctx = vm.createContext({{ structuredClone }});
|
||||
vm.runInContext(fs.readFileSync('tools/est-test/native-price-tools.js', 'utf8'), ctx);
|
||||
process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(code)}, ctx)));
|
||||
"""
|
||||
return json.loads(subprocess.check_output(["node", "-e", runner], cwd=ROOT).decode("utf-8"))
|
||||
|
||||
|
||||
def test_price_tools_rules():
|
||||
out = run_js(
|
||||
r"""(() => {
|
||||
const P = EstPriceTools;
|
||||
const same = P.samecheck([{ Name: '철 근', Size: 'D10', Unite: 'ton', TC: 5 }, { Name: '철근', Size: 'D10', Unite: 'ton', TC: 9 },
|
||||
{ Name: '철근|', Size: 'D10', Unite: 'ton', TC: 9 }, { Name: '모래', TC: 1 }]);
|
||||
const g = P.gungu([{ Code: 'C1', Index: 3, Name: '포장', Size: 't=5', Unite: 'm2', EXPLEAN: '포함: 재료\n제외: 운반' }, { Code: 'C2', Name: '빈' }]);
|
||||
const rows = [{ Name: 'a', TC: 100 }, { Name: 'b', TC: 200, OTC: 210 }, { Name: '', TC: 0 }];
|
||||
const n = P.setRate(rows, [], 'NachalRatio', 88);
|
||||
const one = [{ Name: 'x', TC: 1 }, { Name: 'y', TC: 2 }]; P.setRate(one, [1], 'Ratio', 5);
|
||||
const r = { Name: '', Size: '', LinkIndex: 0 };
|
||||
P.sumRow('HeadMainDat', r, { name: '기구손료', size: '', rate: 2, base: 2, target: 3, picked: [1, 3] });
|
||||
const errs = [];
|
||||
for (const [row, o] of [[{ Index: 1 }, {}], [{}, { base: 0, target: 2, picked: [1] }], [{}, { base: 1, target: 1, picked: [] }],
|
||||
[{}, { base: 1, target: 1, picked: [1, 2, 3, 4, 5, 6] }]])
|
||||
try { P.sumRow('HeadMainDat', row, { name: '', size: '', rate: 1, ...o }); } catch (e) { errs.push(e.message.slice(0, 8)); }
|
||||
return { same: [same.removed, same.rows.map((x) => [x.Name, x.TC])], g, n, rows: rows.map((x) => [x.NachalRatio ?? null, x.OTC ?? null]),
|
||||
one: one.map((x) => x.Ratio ?? null), r: [r.Code, r.LinkIndex, r.GN, r.Biul, r.Unite], errs };
|
||||
})()"""
|
||||
)
|
||||
assert out["same"] == [2, [["철근|", 9], ["모래", 1]]]
|
||||
assert out["g"] == [["C1", "3. 포장 (t=5) [m2]"], ["", "포함: 재료"], ["", "제외: 운반"], ["", ""]]
|
||||
assert out["n"] == 3
|
||||
assert out["rows"] == [[88, 100], [88, 210], [None, None]]
|
||||
assert out["one"] == [5, 5]
|
||||
assert out["r"] == ["A0103", 62, 2, 0.02, "%"]
|
||||
assert out["errs"] == ["현재 라인에는 ", "비목별항목은 비", "더할항목의 색인", "더할항목의 색인"]
|
||||
|
||||
|
||||
def test_sum_row_code_numbers_select_groups():
|
||||
result = run(
|
||||
"math",
|
||||
"""(()=>{
|
||||
const rows=[{Index:1,Code:'H'},{Code:'a',GN:1,NC:100,NU:100,SKind:1},{Code:'b',GN:1,NC:50,NU:50,SKind:3},
|
||||
{Code:'c',GN:1,NC:7,NU:7,SKind:2},{Code:'A0103',LinkIndex:62,GN:10,OCheck:2,SCheck:2}];
|
||||
const out=EstRows.head(rows,{HeadDigits:0});
|
||||
return out.rows?out.rows[4].NC:out[4].NC;
|
||||
})()""",
|
||||
)
|
||||
assert result == 15
|
||||
@@ -489,6 +489,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- 남음: 구성비·요율 전환(CalcBase), 약어 표시(SName), 제비율 기준 문서 보기(원본 한글 파일).
|
||||
- [x] 수량산출서 검색·전체보기(원본 TFRMMULLANG btnSerch·btnAllDisplay — 빈칸·|·괄호 무시 포함 검색) — 고르기 줄을 `native-quantity-sheets.js` 로 옮겨 700줄 제한 유지.
|
||||
- 검증완료: `test_est_quantity_sheets.py` 검색 시험 포함 수량 시험 22개 통과. ORCA 브라우저에서 「집수」 검색 → 항목에 집수정이 있는 「배수」만·선택 이동, 전체보기 → 3개 복귀, 샘플 복구.
|
||||
- [x] 단가 도구(spec_pb2) — 중복 항목 정리(자재단가: 품명·규격·단위 빈칸·| 무시 같으면 TC 가장 큰 행만, 같으면 뒤 행, 묻지 않음), 단가정의 엑셀변환(표준시장단가 설명문 있는 항목만 「코드 / 번호. 품명 (규격) [단위]」+설명 줄, .xlsx), 할증·낙찰(고른 2행 이상 또는 전 행 Ratio·NachalRatio, 원래 단가 OTC 보관, 100 이면 자리 해제)·복원(두 율 0), 바꾸기(Main Data 로 현재 표 하나만 갱신, 못 찾음 표시), SUM(현재 줄을 항목선택계산 62 줄로: Code 'A'+두 자리 색인·적용율·비목, 원본 거부 문구, 줄 자신 SKind = 묶음 최대+1). 엔진: 62 줄 Code 를 두 자리 색인 번호로 읽음(설치 main.estx 의 「A01」 꼴, 예전 글자 해석은 A0103 의 C 를 놓쳤음).
|
||||
- 검증완료: `test_est_price_tools.py` 2개(중복 2개 지움·단가정의 줄·율 쓰기 범위·SUM Code A0103·거부 4종, 엔진 A0103 → 색인 1·3 합 150×10% = 15) 및 REA 대조 39개 통과. ORCA 브라우저에서 보통인부 낙찰 90% 172,068→154,861, 복원 →172,068, 바꾸기(Main) → 2026 하반기 값, 샘플 복구.
|
||||
- [x] 워드형 수량산출서 표와 편집 도구(원본 TFRMMULWORD·TFRMMULLANG — spec_word · spec_pa) — 가져오기가 WPT <table> 을 통째로 버리던 결함 고침: 표를 한 줄 { table: [[칸]] }(글·수량 표지 r/L/G·품명/규격 표지·수식·세로병합)로 보존(설치 main.estx 표 5개 9×13~5×5). 문서 읽기는 `native-quantity-wpt.cjs` 로 분리. 표 화면: 칸 고르기(Shift 범위)·글/수식 고치기·방향 셀 합(우·좌·위·아래, 원본 범위 규칙·1,234.5 꼴)·수량으로 지정/해제·셀수직병합/분리·표 모양(테두리)·아래 표 만들기. 줄 도구: 문단(정렬·들여쓰기)·그림 넣기·한페이지 삽입/삭제(웹 한 쪽 45줄)·품명지정/규격지정(한 줄 하나·40자·제목 위 금지·원본 문구)·테두리선·산출근거 양식(화면 표시)·공종명칭 입력(머리 제목 줄 굵게 큰 글자). 수량 불러오기(Main Data 산출서 75개에서 새 산출서로), 수량목록보기(모든 산출서·수량 항목·원문 미리보기·검색·선택수량복사), 내역서 수량붙여넣기(연결)(고른 줄부터 아래로 수량·연결, 품명,규격입력 체크 시 이름도). 표·서식을 고친 산출서는 ESTX 저장 거부(JSON), 고치지 않은 표 산출서는 ESTX 저장이 표 줄을 건너 문단만 맞춤.
|
||||
- 검증완료: `test_est_quantity_table.py` 2개(표 읽기·문단 G 표지 무시·아래 합 SUM(B2:B3)=1,002.5·수량 값·병합/분리·설치 표 5개) 및 수량·ESTX 내보내기 회귀 49개 통과. ORCA 브라우저에서 보차도경계석 보수 표 35칸, 수량 1.0→2.5 항목 수량 반영, 아래 합 SUM(D2:D6)=3.923, 수량 불러오기 75개·문단 가운데·산출근거 양식·쪽 삽입 45줄·삭제, 공종명칭 「보차도경계석 보수(L=1m)」, 품명지정 ⟦품명⟧, 수량목록 복사 → 내역 붙여넣기 1·0.19·연결 id:36, 샘플 복구.
|
||||
- 남음: 글자모양(글꼴·색), 수량산출서 원본 모양 인쇄(쪽·간지), 엑셀형 시트 수량붙여넣기(연결).
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<script defer src="native-quantity.cjs"></script>
|
||||
<script defer src="native-quantity-table.js"></script>
|
||||
<script defer src="native-quantity-list.js"></script>
|
||||
<script defer src="native-price-tools.js"></script>
|
||||
<script defer src="native-actions-more.js"></script>
|
||||
<script defer src="native-wonga.js"></script>
|
||||
<script defer src="native-jungsang.js"></script>
|
||||
@@ -309,6 +310,13 @@
|
||||
><button data-action="book">품셈</button
|
||||
><button data-action="book-paste">품셈자료 붙여넣기</button
|
||||
><button data-action="mul-paste">수량붙여넣기(연결)</button
|
||||
><button data-action="price-sum">SUM</button
|
||||
><button data-action="price-change">바꾸기</button
|
||||
><button data-action="price-ratio">할증</button
|
||||
><button data-action="price-nachal">낙찰</button
|
||||
><button data-action="price-restore">복원</button
|
||||
><button data-action="price-samecheck">중복 항목 정리</button
|
||||
><button data-action="price-gungu-excel">단가정의 엑셀변환</button
|
||||
><button data-action="add-row">행 추가</button><button data-action="copy-row">복사</button
|
||||
><button data-action="paste-row">붙여넣기</button
|
||||
><button data-action="delete-row">삭제</button
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// 동작 사슬 이어 붙이기(native-business-actions.js 가 700줄 한계라 새 모듈 처리기는 여기서 차례로 부른다).
|
||||
const EstActionsMore = (() => {
|
||||
const modules = () => [typeof EstQuantityList !== "undefined" && EstQuantityList].filter(Boolean);
|
||||
const modules = () =>
|
||||
[
|
||||
typeof EstQuantityList !== "undefined" && EstQuantityList,
|
||||
typeof EstPriceTools !== "undefined" && EstPriceTools,
|
||||
].filter(Boolean);
|
||||
function handle(name, state, node) {
|
||||
for (const module of modules()) if (module.handle(name, state, node)) return true;
|
||||
return false;
|
||||
|
||||
@@ -247,5 +247,5 @@ const EstDataUpdate = (() => {
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
return { apply, isFuel, handle };
|
||||
return { apply, isFuel, table, handle };
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// 단가 목록 도구(spec_pb2): 중복 항목 정리(TfrmStuffc popupSamecheck 0xf61d94), 단가정의 엑셀변환(TfrmSiljuk popupGunguToExcel),
|
||||
// 할증·낙찰·복원(TfrmSetDiget 모드 51·52, btnRestor 0x110919c), 바꾸기(barbtnChange → TfrmChangeValue: Main Data 로 이 표만 갱신),
|
||||
// SUM(barbtnSum → 항목선택계산 LinkIndex 62 줄, TfrmSetGabBiIndex SUM 모드).
|
||||
const EstPriceTools = (() => {
|
||||
const num = (v) => Number(v) || 0;
|
||||
const key = (s) => String(s ?? "").replace(/[ |]/g, "");
|
||||
const TABLE_KIND = { StuffcDat: 5, LaborcDat: 6, SilJukDat: 21, EstiMateDat: 101 };
|
||||
|
||||
// 같은 품명·규격·단위(빈칸·| 무시) 묶음에서 TC 가장 큰 행 하나만(같으면 뒤 행) 남김. 묻지 않음, 다시 셈 없음.
|
||||
function samecheck(rows) {
|
||||
const keep = new Map();
|
||||
rows.forEach((row, i) => {
|
||||
const k = `${key(row.Name)}\u0000${key(row.Size)}\u0000${key(row.Unite)}`,
|
||||
prev = keep.get(k);
|
||||
if (prev == null || !(num(rows[prev].TC) > num(row.TC))) keep.set(k, i);
|
||||
});
|
||||
const kept = new Set(keep.values());
|
||||
const removed = rows.length - kept.size;
|
||||
return { rows: rows.filter((_, i) => kept.has(i)), removed };
|
||||
}
|
||||
|
||||
// 표준시장단가 EXPLEAN 이 있는 행만 「코드 / 번호. 품명 (규격) [단위]」 + 설명 줄
|
||||
function gungu(rows) {
|
||||
const out = [];
|
||||
for (const r of rows) {
|
||||
const text = String(r.EXPLEAN ?? "").trim();
|
||||
if (!text) continue;
|
||||
out.push([
|
||||
String(r.Code ?? ""),
|
||||
`${r.Index ?? ""}. ${r.Name ?? ""} (${r.Size ?? ""}) [${r.Unite ?? ""}]`,
|
||||
]);
|
||||
for (const line of text.split(/\r?\n/)) out.push(["", line]);
|
||||
out.push(["", ""]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 할증(Ratio)·낙찰(NachalRatio) 율 쓰기. v = 100 이면 원래값(그 율 칸 100, 자리 해제), 복원 = 0.
|
||||
function setRate(rows, indices, field, v) {
|
||||
const list = indices.length >= 2 ? indices : rows.map((_, i) => i);
|
||||
for (const i of list) {
|
||||
const row = rows[i];
|
||||
if (!num(row.TC) && !String(row.Name ?? "").trim()) continue;
|
||||
if (!num(row.OTC)) row.OTC = row.TC;
|
||||
for (const f of [field].flat()) row[f] = v;
|
||||
if (v === 100) Object.assign(row, { Diget: 0, DigetIndex: 0 });
|
||||
}
|
||||
return list.length;
|
||||
}
|
||||
|
||||
// 항목선택계산(62) 줄
|
||||
function sumRow(view, row, o) {
|
||||
const banned =
|
||||
view === "EstMainDat"
|
||||
? [
|
||||
5, 6, 21, 51, 52, 53, 54, 56, 57, 58, 59, 61, 101, 103, 104, 105, 211, 212, 213, 214,
|
||||
215, 217, 218,
|
||||
]
|
||||
: [5, 6, 21, 51, 52, 53, 61, 101, 103, 104, 105, 211, 212, 213, 214, 215, 217, 218];
|
||||
if (
|
||||
(view === "EstMainDat" && num(row.Level) > 0) ||
|
||||
(view === "HeadMainDat" && num(row.Index) > 0) ||
|
||||
(banned.includes(num(row.LinkIndex)) &&
|
||||
(String(row.Name ?? "").trim() || String(row.Size ?? "").trim()))
|
||||
)
|
||||
throw Error("현재 라인에는 합계(SUM)를 할수가 없습니다.");
|
||||
if ((o.base === 0) !== (o.target === 0)) throw Error("비목별항목은 비목별적용만 가능합니다.");
|
||||
if (!o.picked.length) throw Error("더할항목의 색인들을 선택하세요");
|
||||
if (o.picked.length > 5) throw Error("더할항목의 색인들은 6개이상 선택할수없습니다");
|
||||
Object.assign(row, {
|
||||
Name: o.name,
|
||||
Size: o.size,
|
||||
LinkIndex: 62,
|
||||
Code: `A${o.picked.map((n) => String(n).padStart(2, "0")).join("")}`,
|
||||
GN: o.rate,
|
||||
Biul: o.rate / 100,
|
||||
Unite: "%",
|
||||
OCheck: o.base,
|
||||
SCheck: o.target,
|
||||
});
|
||||
}
|
||||
function nextSKind(rows, at, view) {
|
||||
let start = at,
|
||||
end = at;
|
||||
const head = (r) => (view === "EstMainDat" ? num(r.Level) > 0 : num(r.Index) > 0);
|
||||
while (start > 0 && !head(rows[start - 1])) start--;
|
||||
while (end + 1 < rows.length && !head(rows[end + 1])) end++;
|
||||
return Math.max(0, ...rows.slice(start, end + 1).map((r) => num(r.SKind))) + 1;
|
||||
}
|
||||
|
||||
async function run(name, state) {
|
||||
const view = state.view,
|
||||
rows = state.project.tables[view],
|
||||
picked = [...state.selection].filter((i) => rows?.[i]).sort((a, b) => a - b);
|
||||
if (name === "price-samecheck") {
|
||||
if (view !== "StuffcDat") throw Error("자재단가 화면에서 쓰세요.");
|
||||
let removed = 0;
|
||||
EstApp.change((project) => {
|
||||
const out = samecheck(project.tables.StuffcDat);
|
||||
project.tables.StuffcDat = out.rows;
|
||||
removed = out.removed;
|
||||
});
|
||||
state.notice = `중복 항목 ${removed}개를 지웠습니다.`;
|
||||
} else if (name === "price-gungu-excel") {
|
||||
const list = gungu(state.project.tables.SilJukDat || []);
|
||||
if (!list.length) throw Error("단가정의(설명)가 있는 표준시장단가가 없습니다.");
|
||||
const response = await fetch("/api/export-report-xlsx", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: "단가정의",
|
||||
columns: ["코드", "단가정의"],
|
||||
rows: list,
|
||||
errors: [],
|
||||
}),
|
||||
});
|
||||
if (!response.ok) throw Error((await response.json()).error);
|
||||
EstApp.download(
|
||||
`${state.project.name || "공사"}_단가정의.xlsx`,
|
||||
await response.blob(),
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
);
|
||||
} else if (name === "price-ratio" || name === "price-nachal" || name === "price-restore") {
|
||||
if (!TABLE_KIND[view]) throw Error("자재·노임·표준시장·견적 단가 화면에서 쓰세요.");
|
||||
// 복원(원본은 할증·낙찰 창 안 단추): 두 율 모두 0
|
||||
const field =
|
||||
name === "price-nachal"
|
||||
? "NachalRatio"
|
||||
: name === "price-ratio"
|
||||
? "Ratio"
|
||||
: ["Ratio", "NachalRatio"];
|
||||
let v = 0;
|
||||
if (name !== "price-restore") {
|
||||
const input = prompt(
|
||||
name === "price-nachal" ? "낙찰율을 입력하세요(%)" : "할증율을 입력하세요(%)",
|
||||
"",
|
||||
);
|
||||
if (input == null) return;
|
||||
v = Number(input);
|
||||
if (!Number.isFinite(v)) throw Error("율은 수치로 입력하세요.");
|
||||
}
|
||||
if (
|
||||
!confirm(
|
||||
v === 100 || v === 0 ? "원래값으로 복원하시겠습니까?" : "설정값을 적용하시겠습니까?",
|
||||
)
|
||||
)
|
||||
return;
|
||||
let n = 0;
|
||||
EstApp.change((project) => (n = setRate(project.tables[view], picked, field, v)));
|
||||
state.notice = `${n}개 행에 적용했습니다.`;
|
||||
} else if (name === "price-change") {
|
||||
const kind = TABLE_KIND[view];
|
||||
if (!kind) throw Error("자재·노임·표준시장·견적 단가 화면에서 쓰세요.");
|
||||
if (
|
||||
!confirm(
|
||||
"선택한 Main데이타의 단가로 바꾸시겠습니까?\n바꾸기후 전체재계산을 해야 금액이 바뀝니다.",
|
||||
)
|
||||
)
|
||||
return;
|
||||
const source = await EstApp.api("/api/main-data");
|
||||
let missing = 0;
|
||||
EstApp.change((project) => {
|
||||
missing = EstDataUpdate.table(project.tables[view], source.tables[view] || [], kind);
|
||||
});
|
||||
state.notice = missing
|
||||
? `${missing} 개의 찾지못한 데이타가 있습니다. 확인란에 체크된 데이타를 검토하세요.`
|
||||
: "Main 데이타의 단가로 바꿨습니다.";
|
||||
} else if (name === "price-sum") {
|
||||
if (!["EstMainDat", "HeadMainDat"].includes(view) || !picked.length)
|
||||
throw Error("내역서·일위대가에서 SUM 줄로 바꿀 행을 고르세요.");
|
||||
const row = rows[picked[0]];
|
||||
const letters = prompt("더할 색인(A~Z, 최대 5개, 예 AC)", "")?.toUpperCase();
|
||||
if (letters == null) return;
|
||||
const rate = Number(prompt("적용율(%)", "100"));
|
||||
const o = {
|
||||
name: prompt("명칭", row.Name || "잡재료비") ?? "",
|
||||
size: row.Size ?? "",
|
||||
rate: Number.isFinite(rate) ? rate : 0,
|
||||
base: num(row.OCheck),
|
||||
target: num(row.SCheck),
|
||||
picked: [...new Set(letters.replace(/[^A-Z]/g, ""))].map((c) => c.charCodeAt(0) - 64),
|
||||
};
|
||||
sumRow(view, structuredClone(row), o);
|
||||
EstApp.change((project) => {
|
||||
const list = project.tables[view];
|
||||
sumRow(view, list[picked[0]], o);
|
||||
list[picked[0]].SKind = nextSKind(list, picked[0], view);
|
||||
});
|
||||
} else return false;
|
||||
EstApp.render();
|
||||
return true;
|
||||
}
|
||||
const names = new Set([
|
||||
"price-samecheck",
|
||||
"price-gungu-excel",
|
||||
"price-ratio",
|
||||
"price-nachal",
|
||||
"price-restore",
|
||||
"price-change",
|
||||
"price-sum",
|
||||
]);
|
||||
function handle(name, state) {
|
||||
if (!names.has(name)) return false;
|
||||
run(name, state).catch((error) => {
|
||||
state.notice = error.message;
|
||||
EstApp.render();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return { samecheck, gungu, setRate, sumRow, handle };
|
||||
})();
|
||||
@@ -13,13 +13,18 @@ const EstRows = (() => {
|
||||
}
|
||||
function percent(row, preceding, digits = 0) {
|
||||
if ([1, 3].includes(num(row.CalcIndex))) return row;
|
||||
// f1a8c4 matches Chr(64 + source.SKind) against the percentage row's Code.
|
||||
const selectedGroups = String(row.Code || "").toUpperCase();
|
||||
// 항목선택계산(62) Code = 'A' + 두 자리 색인 번호들(원본 SUM effc54, 예 A0103 = A·C); 그 밖 꼴은 글자 Chr(64+SKind) 로.
|
||||
const selectedGroups = String(row.Code || "").toUpperCase(),
|
||||
numbered = /^A((?:\d\d)+)$/.exec(selectedGroups),
|
||||
groups = numbered ? new Set(numbered[1].match(/\d\d/g).map(Number)) : null;
|
||||
const eligible =
|
||||
num(row.LinkIndex) === 62
|
||||
? preceding.filter(
|
||||
(r) =>
|
||||
num(r.SKind) > 0 && selectedGroups.includes(String.fromCharCode(64 + num(r.SKind))),
|
||||
num(r.SKind) > 0 &&
|
||||
(groups
|
||||
? groups.has(num(r.SKind))
|
||||
: selectedGroups.includes(String.fromCharCode(64 + num(r.SKind)))),
|
||||
)
|
||||
: preceding;
|
||||
const base = baseKind(row),
|
||||
|
||||
@@ -97,6 +97,7 @@ const allowed = new Set([
|
||||
"native-data-update.js",
|
||||
"native-quantity-table.js",
|
||||
"native-quantity-list.js",
|
||||
"native-price-tools.js",
|
||||
"native-actions-more.js",
|
||||
"native-wonga-tools.js",
|
||||
"native-estimate-print.js",
|
||||
|
||||
Reference in New Issue
Block a user