/* -*- 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;