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

247 lines
11 KiB
JavaScript

/* -*- 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;
out.push([c, num(r.QTY1)]);
}
} 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);
return { F: unit, G: half(q * unit) };
}
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");
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;