247 lines
11 KiB
JavaScript
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;
|