Files
Aislo/tools/stmate-test/report_coll.js
T

260 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* -*- coding: utf-8 -*- */
/**
* 수량금액 집계 (report_coll.js) — 투입 수량 전개 · 수량금액집계표(0501~0508) · 일식·표준시장 목록표(0104 · 0106)
* 근거: 지식DB STmate/old/20_분석 06 §3 · 18 §2(집계표 금액 = 사사오입) · 20 §3(SET_REP) · old/30_원자료/print_form_map.txt
* 전개 = 내역 품목 수량 → 일위대가(B) 구성 수량 곱 → 산근(D) 식 줄 계수(참조 단가 1 로 셈) · 중기(X)는 시간에서 멈춤
* 산근 안 같은 코드의 비목 줄(노·재·경)은 한 벌 — 비목별 계수 합의 최대 · (-=)·(-==)·「->」 제외 구간은 뺌
* 정답 대조: resources/tester/test_stmate_report_coll.py (원본 STMATE 출력 엑셀)
*/
const StmateReportColl = (() => {
const g = typeof globalThis !== "undefined" ? globalThis : {};
const C = g.STMATE_REPORT_CORE || require("./report_core.js");
const U = g.STMATE_REPORT_UNIT || require("./report_unit.js");
const { num, str, code, half } = C;
const CODE_RE = /^([A-Z])(\d{1,5})$/;
const padCode = (t) => {
const m = String(t).match(CODE_RE);
return m ? m[1] + m[2].padStart(5, "0") : t;
};
/**
* 산근 줄 계수 — 참조 단가를 1 로 두고 같은 식을 셈(줄 끝수 자르기 없음)
* 돌려줌: { 코드: {M,L,S} } 비목별 계수 합 · 제외 구간 줄은 빠짐
*/
function sanFactors(dbf, d, memoText) {
const SE = g.STMATE_SMT_EVAL;
const EN = g.STMATE_ENGINE;
const ctx = { dbfData: dbf, state: {}, uiState: {} };
const { lookup } = EN.costnLookup(ctx);
const one = (k) => {
const r = lookup(k);
if (!r) return null;
// 자원 = 제 비목 칸에만 1(「&L00006L&」 같은 비목 지정 참조도 1) · 그 밖 = 세 비목 모두 1
const own = k[0] === "L" ? "L" : k[0] === "S" ? "S" : "M";
const unit = /^[MLSW]/.test(k) ? { M: 0, L: 0, S: 0, [own]: 1 } : { M: 1, L: 1, S: 1 };
return Object.assign({}, r, unit);
};
const opts = Object.assign(EN.smtOptions(ctx, d), { lineDigits: 14, adj: null, sbmok: "" });
opts.totalFirst = false;
const ev = SE.evaluate(memoText, one, opts);
const amountLines = [];
let pending = [];
for (const ln of ev.lines) {
if (ln.kind === "amount") {
const ok = !ln.excluded;
const item = { ln, ok };
amountLines.push(item);
pending.push(item);
} else if (ln.kind === "section") pending = [];
else if (ln.kind === "exclude") {
// (-=) 지금 구간 · (-==) 이전 전부
const hit = ln.level === 1 ? pending : amountLines;
for (const it of hit) it.ok = false;
pending = [];
}
}
const out = {};
for (const { ln, ok } of amountLines) {
if (!ok) continue;
const m = String(ln.ref).match(/^([A-Z]\d{1,5})([LMS])?$/);
if (!m) continue;
const c = padCode(m[1]);
const r = lookup(c);
if (!r || /%$/.test(String(r.unit || "").trim())) continue;
const f = (out[c] = out[c] || { M: 0, L: 0, S: 0 });
for (const k of "MLS") f[k] += half(ln.amounts[k], 11); // 계수 = 소수 11자리(BDQTY 저장 자리)
}
return out;
}
/**
* 투입 수량 전개 — 차수 내역 품목(비율 줄 빼고) 전부
* 돌려줌: { qty:{코드: 수량}, by:{코드: 품목 묶음 → 수량} } (by = 공종별 집계표 0675~0677 용)
*/
function explode(dbf, items, opts = {}) {
const memos = (dbf && dbf.costn_memos) || {};
const edited = opts.editedSmt || {};
const idx = C.costnIndex(dbf);
const groups = {};
for (const r of (dbf && dbf.bdqty_rows) || [])
if (/^[BX]/.test(String(r.BDCODE || ""))) (groups[r.BDCODE] = groups[r.BDCODE] || []).push(r);
const fcache = {};
const children = (k) => {
if (fcache[k]) return fcache[k];
const out = [];
if (k[0] === "B" && groups[k]) {
for (const r of groups[k]) {
const c = String(r.CODE || "").trim();
const ref = idx.by[c];
if (!ref || /%$/.test(str(ref.UNIT).trim())) continue;
// 나누기수량(DEVD1) = 저장값의 역수(smt_eval 구성 조립과 같음)
const q = num(r.QTY1);
const devd = ["T", "t", "1"].includes(String(r.DEVD1 || "").trim());
out.push([c, devd ? (q ? 1 / q : 0) : q]);
}
} else if (k[0] === "D") {
const text = edited[k] || (memos[k] && memos[k].text) || "";
if (text.trim()) {
const f = sanFactors(dbf, k, text);
for (const [c, v] of Object.entries(f))
out.push([c, /^[MLSW]/.test(c) ? v.M + v.L + v.S : Math.max(v.M, v.L, v.S)]);
}
}
return (fcache[k] = out);
};
const qty = {};
const by = {};
const walk = (k, q, bag, depth) => {
qty[k] = (qty[k] || 0) + q;
if (bag) bag[k] = (bag[k] || 0) + q;
if (depth > 30 || !q) return;
for (const [c, f] of children(k)) walk(c, q * f, bag, depth + 1);
};
for (const it of items) {
if (!it.code || it.pct || !idx.by[it.code]) continue;
const bag = it.group != null ? (by[it.group] = by[it.group] || {}) : null;
walk(it.code, it.qty, bag, 0);
}
return { qty, by };
}
// 차수 내역 품목 — 설계내역서 나무(report_boq boqTree) 품목 줄 · 제외금액 구역(C 로 닫힌 묶음 · 관급 등) 안은 뺌
const inExcluded = (n) => {
for (let p = n.parent; p; p = p.parent) if (p.excluded) return true;
return false;
};
function stageItems(tree) {
return tree.out
.filter((n) => n.kind === "item" && !inExcluded(n))
.map((n) => ({ code: n.code, qty: n.qty, pct: n.pct, node: n }));
}
// 수량 끝수 — 소수 둘째 자리 사사오입(원본 집계표 수량 칸)
const q2 = (v) => half(v, 2);
// 호표 글 — 대가·산근(B·D)은 네 자리 맞춤 · 그 밖은 자리 맞춤(CH_PRN_COD=1)일 때만
const hopyoText = (po, prefix, n) =>
po.padNo || "BD".includes(prefix) ? String(n).padStart(4) : String(n);
/**
* 수량금액집계표 한 장 — 쓰인 코드만(코드순) · 금액 = 수량(끝수 맞춘 값) × 단가 사사오입
* 자원(0501~0504 · 0508) = 단가·금액 한 칸 / 비목(0505 중기 · 0506 대가 · 0507 산근) = 비목 4칸(BMKNM 차례)
* 호표 번호 = 자리 맞춤이면 코드 호표 번호 · 그 밖 = 쓰인 코드 차례
* exs 둘(당초·변경)이면 변경 양식 — 코드마다 당초 줄 + 변경 줄 · changeOnly 면 수량이 바뀐 코드만
*/
function collSheet(u, exs, name, title, prefix, opts = {}) {
const { dbf, idx, ini, order, po, hopyoNo } = u;
const bimok = "XBD".includes(prefix);
const sh = C.newSheet(name, title, C.projectName(dbf), bimok ? "K" : "I");
const head = { A: po.codeMode ? "코드번호" : "호표", B: "명 칭", C: "규 격" };
Object.assign(head, { D: "수 량", E: "단위", _k: "head" });
const cols = ["F", "G", "H", "I"];
if (bimok) {
head.J = "비 고";
order.forEach((k, i) => (head[cols[i]] = C.bimokLabel(ini, k)));
} else Object.assign(head, { F: "단 가", G: "금 액", H: "비 고" });
C.push(sh, head);
const money = (q, p) => {
if (!bimok) {
const unit = C.fix(p.M + p.L + p.S);
// 엑셀 수식 — 집계 금액 = ROUND(수량 × 단가, 0)(지식DB 06 §3)
return { F: unit, G: half(q * unit), _f: { G: "=ROUND(D{r}*F{r},0)" } };
}
const v = { M: half(q * p.M), L: half(q * p.L), S: half(q * p.S) };
v.T = v.M + v.L + v.S;
const out = {};
order.forEach((k, i) => (out[cols[i]] = v[k]));
return out;
};
let seq = 0;
for (const r of idx.ofPrefix(prefix)) {
const c = code(r);
if (!exs.some((e) => e.qty[c])) continue;
seq += 1;
const qs = exs.map((e) => q2(e.qty[c] || 0));
if (opts.changeOnly && qs.length > 1 && qs[0] === qs[1]) continue;
const p = idx.price(c, 1); // 1칸 = 낙찰률 적용 단가(내역 단가와 같음)
const lbl = po.codeMode ? c : hopyoText(po, prefix, po.padNo ? hopyoNo[c] : seq);
const cells = { A: lbl, B: str(r.NAME), C: str(r.SIZE), D: qs[0], E: C.unitText(r.UNIT) };
Object.assign(cells, money(qs[0], p), { _k: "item", _code: c });
const rm = U.remarkOf(po, c, lbl, r);
if (rm) cells[bimok ? "J" : "H"] = rm;
C.push(sh, cells);
if (qs.length > 1)
C.push(sh, Object.assign({ D: qs[1], _k: "item2", _code: c }, money(qs[1], p)));
}
return sh;
}
/**
* 일식·견적(W · 0104) · 표준시장(P · 0106) 목록표 — 비목 4칸 단가
*/
function wListSheet(u, name, title, prefix) {
const { dbf, idx, ini, order, po, hopyoNo } = u;
const sh = C.newSheet(name, title, C.projectName(dbf), "J");
const cols = ["E", "F", "G", "H"];
const head = { A: po.codeMode ? "코드번호" : "호표", B: "명 칭", C: "규 격" };
Object.assign(head, { D: "단위", I: "비 고", _k: "head" });
order.forEach((k, i) => (head[cols[i]] = C.bimokLabel(ini, k)));
C.push(sh, head);
let seq = 0;
for (const r of idx.ofPrefix(prefix)) {
const c = code(r);
seq += 1;
const p = idx.price(c, 1);
const v = { M: p.M, L: p.L, S: p.S, T: p.M + p.L + p.S };
const lbl = po.codeMode ? c : hopyoText(po, prefix, po.padNo ? hopyoNo[c] : seq);
const cells = { A: lbl, B: str(r.NAME), C: str(r.SIZE), D: C.unitText(r.UNIT) };
Object.assign(cells, { _k: "item", _code: c });
order.forEach((k, i) => (cells[cols[i]] = v[k]));
const rm = U.remarkOf(po, c, lbl, r);
if (rm) cells.I = rm;
C.push(sh, cells);
}
return sh;
}
// 집계표 차례 — [시트 id, 제목, 코드 종류, SET_REP 서식] · W·P 는 코드가 있을 때만
const COLL = [
["재료비수량금액집계표", "재료비 수량,금액 집계표", "M", "0501"],
["노무비수량금액집계표", "노무비 수량,금액 집계표", "L", "0502"],
["경비수량금액집계표", "경 비 수량,금액 집계표", "S", "0503"],
["일식견적수량금액집계표", "일식/견적 수량,금액 집계표", "W", "0504"],
["표준시장수량금액집계표", "표준시장/시장시공 수량,금액 집계표", "P", "0508"],
["중기시간금액집계표", "중기 시간,금액 집계표", "X", "0505"],
["일위대가수량금액집계표", "일위대가 수량,금액 집계표", "B", "0506"],
["단가산출근거수량금액집계표", "단가산출근거 수량,금액 집계표", "D", "0507"],
];
/**
* 집계 묶음 — u = report_unit unitContext · tree = 차수 설계내역 나무
* opts.baseTree(당초 차수 나무)를 주면 변경 양식(당초·변경 두 줄) · opts.changeOnly = 바뀐 코드만
*/
function buildCollSheets(u, tree, opts = {}) {
const ex = explode(u.dbf, stageItems(tree), opts);
const exs = opts.baseTree ? [explode(u.dbf, stageItems(opts.baseTree), opts), ex] : [ex];
const F = (sh, indx) => C.applyForm(sh, u.dbf, indx);
const has = (p) => u.idx.ofPrefix(p).length > 0;
const out = { _explode: ex };
if (has("W"))
out.일식견적목록표 = F(wListSheet(u, "일식견적목록표", "일식/견적 목록표", "W"), "0104");
if (has("P"))
out.표준시장목록표 = F(
wListSheet(u, "표준시장목록표", "표준시장/시장시공 목록표", "P"),
"0106",
);
// 실적단가(A — 실적 → 일식 변환 wM_Conv_A2W 의 A) 목록표(0206)
if (has("A"))
out.실적단가목록표 = F(wListSheet(u, "실적단가목록표", "실적공사비 단가목록표", "A"), "0206");
for (const [id, title, p, indx] of COLL)
if ("WP".includes(p) ? has(p) : true)
out[id] = F(collSheet(u, exs, id, title, p, opts), indx);
return out;
}
return { buildCollSheets, explode, sanFactors, stageItems, padCode };
})();
if (typeof window !== "undefined") window.STMATE_REPORT_COLL = StmateReportColl;
if (typeof module !== "undefined" && module.exports) module.exports = StmateReportColl;