- 보고서: 원본 STMATE 엑셀과 셀 대조(목록표·자재단가대비·중기·일위대가·산근 목록·설계내역서·원가계산서) · 조달청 XML 공식 XSD 검증 · PM_Prt 화면 - 제잡비: 원본 계산 순서 4형식(T/S/TA/SA) · 실무 7건 저장 결과키 전수 1원 · 요율표 조회 · 선택지 계산 · 제잡비·제비율 화면 - 식언어·수량: 함수·조건식·금액변수·문법 검사·수량변수·QCODE · 산근·구성·중기공식 편집 화면 · 순환 알림 - 화면: 원본 메뉴 체계·단축키 · 설계내역서 편집(행 넣기·지우기·이동·레벨·보관함) · 초기값 · 단가목록표 - 저장: 초기값(Start8x.Ini) 바뀐 키 저장 · 노임 환산 직종 = 초기값 CONLD · 산근·구성 결과에 적용단가 칸 채우지 않음(원본 0) - DBF 글 칸 앞 빈칸 보존 · 시험 도우미 표준입력 utf8(한글 조각 깨짐 원인) - 실무 5건 재계산 후 저장 = 바뀐 칸 0 · 시험 332건 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
460 lines
20 KiB
JavaScript
460 lines
20 KiB
JavaScript
/* -*- coding: utf-8 -*- */
|
||
/**
|
||
* 내역 보고서 (report_boq.js) — 설계내역서(0603) · 총괄내역서(0601) · 공사원가계산서(0608)
|
||
* 근거: 지식DB STmate/old/20_분석 20 §2.2(간접비 코드 순서 · 0원 항목 빠짐) · §2.6(DISP_EX 제외금액)
|
||
* 16 §4(BOQ 계층 T·E·C) · 06 §3(내역 절사) · 25 §2(간접비 식 설명)
|
||
* 정답 대조: resources/tester/test_stmate_report_boq.py (원본 STMATE 출력 엑셀)
|
||
* 금액 = 계산 엔진(calc.js calculateAll) 차수별 결과 · 구조(공종 계층·순서) = BOQ11.DBF
|
||
*/
|
||
const StmateReportBoq = (() => {
|
||
const C =
|
||
(typeof globalThis !== "undefined" && globalThis.STMATE_REPORT_CORE) ||
|
||
require("./report_core.js");
|
||
const { num, str, trunc, fix, commas } = C;
|
||
const PAIR = [
|
||
["F", "G"],
|
||
["H", "I"],
|
||
["J", "K"],
|
||
["L", "M"],
|
||
];
|
||
const STRUCT = /^(T\d|E\d|C\d|:)$/;
|
||
const COMP = { 1: "M", 2: "L", 3: "S", 4: "T" };
|
||
const rateText = (r) => String(Number(num(r).toFixed(5)));
|
||
|
||
/**
|
||
* 공종 나무 — SORTCODE 순 · T 행이 ENDCODE 의 E/C 행까지 묶음 · C 로 닫히면 제외금액 구역
|
||
* 묶음 금액 = 직속 품목 + 제외 아닌 하위 묶음 (원본 엑셀 SUMIF 와 같음)
|
||
* 비율 행(PR · 단위 %) = 같은 묶음의 표시(PRPT) 품목 금액 × 율 / 100 원 미만 절사
|
||
*/
|
||
function boqTree(dbf, stageBoq, slot) {
|
||
const raw = ((dbf && dbf.boq_rows) || [])
|
||
.slice()
|
||
.sort((a, b) => num(a.SORTCODE) - num(b.SORTCODE));
|
||
const calcBy = {};
|
||
for (const r of (stageBoq && stageBoq.rows) || []) calcBy[r._recno] = r;
|
||
const root = { kind: "T", children: [], sum: { M: 0, L: 0, S: 0 }, end: null };
|
||
const stack = [root];
|
||
const out = [];
|
||
let finalRow = null;
|
||
for (const r of raw) {
|
||
const b = str(r.BDCODE1).trim();
|
||
const top = stack[stack.length - 1];
|
||
if (/^T\d$/.test(b)) {
|
||
const node = { kind: "T", row: r, level: Number(b[1]), children: [], parent: top };
|
||
Object.assign(node, { sum: { M: 0, L: 0, S: 0 }, end: num(r.ENDCODE), excluded: false });
|
||
top.children.push(node);
|
||
stack.push(node);
|
||
out.push(node);
|
||
continue;
|
||
}
|
||
if (/^[EC]\d$/.test(b)) {
|
||
const at = stack.findIndex((n) => n.end === num(r.SORTCODE));
|
||
if (at > 0) {
|
||
const node = stack[at];
|
||
if (b[0] === "C") node.excluded = true;
|
||
stack.length = at;
|
||
if (at === 1) finalRow = { kind: "final", node: root };
|
||
}
|
||
continue;
|
||
}
|
||
if (!b || b === ":") {
|
||
out.push({ kind: b === ":" ? "page" : "blank", row: r });
|
||
continue;
|
||
}
|
||
const item = { kind: "item", row: r, parent: top, code: b };
|
||
const unit = str(r.UNIT1).trim();
|
||
if (b === "PR" || /%$/.test(unit)) {
|
||
item.pct = true;
|
||
const inK = COMP[num(r.PRIN)] || "M",
|
||
outK = COMP[num(r.PROUT)] || (b[0] === "L" ? "L" : b[0] === "S" ? "S" : "M");
|
||
const sibs = top.children.filter((c) => c.kind === "item" && !c.pct);
|
||
const marked = sibs.filter((c) => str(c.row.PRPT).trim() === "1");
|
||
const base = (marked.length ? marked : sibs).reduce(
|
||
(s, c) => s + (inK === "T" ? c.amt.M + c.amt.L + c.amt.S : c.amt[inK]),
|
||
0,
|
||
);
|
||
const q = num(r[`QTY${slot}`]) || num(r.QTY1);
|
||
item.qty = q;
|
||
item.unitP = { M: 0, L: 0, S: 0 };
|
||
item.amt = { M: 0, L: 0, S: 0 };
|
||
const k = outK === "T" ? "M" : outK;
|
||
item.unitP[k] = base;
|
||
item.amt[k] = trunc((base * q) / 100);
|
||
} else {
|
||
const c = calcBy[r._recno];
|
||
item.qty = c ? c.qty : num(r[`QTY${slot}`]);
|
||
item.unitP = c ? { M: c.mUnit, L: c.lUnit, S: c.sUnit } : { M: 0, L: 0, S: 0 };
|
||
item.amt = c ? { M: c.mAmt, L: c.lAmt, S: c.sAmt } : { M: 0, L: 0, S: 0 };
|
||
}
|
||
top.children.push(item);
|
||
out.push(item);
|
||
}
|
||
// 묶음 금액 — 아래에서 위로
|
||
const sumOf = (node) => {
|
||
const s = { M: 0, L: 0, S: 0 };
|
||
for (const ch of node.children) {
|
||
const v = ch.kind === "T" ? sumOf(ch) : ch.amt;
|
||
if (ch.kind === "T" && ch.excluded) continue;
|
||
for (const k of "MLS") s[k] += v[k];
|
||
}
|
||
node.sum = s;
|
||
return s;
|
||
};
|
||
sumOf(root);
|
||
return { out, root, finalRow };
|
||
}
|
||
|
||
// 품목 비고 — 코드 모드(SH_TYP1=0) 코드 · 호표 모드 = 대가·산근·중기는 「산근 n호표」 · 자원은 가격지 위치(BQCODE)
|
||
// (SH_TYP3=1 이면 자원도 「자재 n」) · 호표 번호 = 종류별 코드순 자리
|
||
function itemRemark(dbf, idx, b) {
|
||
const ini = (dbf && dbf.start_ini) || {};
|
||
if (b === "PR") return "";
|
||
if (String(ini.SH_TYP1 ?? "0") === "0") return b;
|
||
const r = idx.by[b];
|
||
if (!r) return "";
|
||
const kind = String(ini[`COD_FMT_${"LMSBXD".indexOf(b[0]) + 1}`] || "");
|
||
const no = idx.ofPrefix(b[0]).findIndex((x) => C.code(x) === b) + 1;
|
||
if ("BDX".includes(b[0])) return `${kind}${String(no).padStart(4)}호표`;
|
||
if (String(ini.SH_TYP3 ?? "2") === "1") return `${kind}${String(no).padStart(5)}`;
|
||
return str(r.BQCODE).trim();
|
||
}
|
||
|
||
// 설계내역서(0603) — 계(E)·총계(C) 줄은 묶음 머리 금액으로 올라가고 끝에 총합계 금액 줄
|
||
function designSheet(dbf, stage, opts = {}) {
|
||
const ini = (dbf && dbf.start_ini) || {};
|
||
const order = C.bimokOrder(ini);
|
||
const idx = C.costnIndex(dbf);
|
||
const name = opts.name || "설계내역서";
|
||
const sh = C.newSheet(name, "설 계 내 역 서", C.projectName(dbf), "O");
|
||
const h3 = { A: "공 종", B: "명 칭", C: "규 격", D: "수 량", E: "단위" };
|
||
Object.assign(h3, { N: "비 고", _k: "head" });
|
||
const h4 = { _k: "head" };
|
||
order.forEach((k, i) => {
|
||
h3[PAIR[i][0]] = C.bimokLabel(ini, k);
|
||
h4[PAIR[i][0]] = "단 가";
|
||
h4[PAIR[i][1]] = "금 액";
|
||
});
|
||
C.push(sh, h3);
|
||
C.push(sh, h4);
|
||
const tree = boqTree(dbf, stage.boq, stage.slot || 1);
|
||
const dispEx = `(${str(ini.DISP_EX || "제외금액").trim()})`;
|
||
const fill = (cells, unitP, amt, zeroUnit) => {
|
||
const u = { M: unitP.M, L: unitP.L, S: unitP.S, T: unitP.M + unitP.L + unitP.S };
|
||
const a = { M: amt.M, L: amt.L, S: amt.S, T: amt.M + amt.L + amt.S };
|
||
order.forEach((k, i) => {
|
||
cells[PAIR[i][0]] = zeroUnit ? 0 : fix(u[k]);
|
||
cells[PAIR[i][1]] = fix(a[k]);
|
||
});
|
||
};
|
||
// 쪽 넘김(:) — 빈 줄 하나에 쪽 나눔 표시(인쇄 미리보기에서 다음 쪽으로)
|
||
for (const n of tree.out) {
|
||
const r = n.row;
|
||
if (n.kind === "page") {
|
||
C.push(sh, { _k: "page" });
|
||
continue;
|
||
}
|
||
if (n.kind === "blank") {
|
||
C.push(sh, { F: 0, _k: "blank" });
|
||
continue;
|
||
}
|
||
const cells = { A: str(r.ITNUM).trim(), B: str(r.NAME1), C: str(r.SIZE1) };
|
||
if (n.kind === "T") {
|
||
Object.assign(cells, { _k: `T${n.level}` });
|
||
fill(cells, { M: 0, L: 0, S: 0 }, n.sum, true);
|
||
if (n.excluded) cells.N = dispEx;
|
||
} else {
|
||
Object.assign(cells, { D: fix(n.qty), E: C.unitText(r.UNIT1), _k: "item", _code: n.code });
|
||
fill(cells, n.unitP, n.amt, false);
|
||
const rm = itemRemark(dbf, idx, n.code);
|
||
if (rm) cells.N = rm;
|
||
}
|
||
C.push(sh, cells);
|
||
}
|
||
if (tree.finalRow) {
|
||
const cells = { _k: "final" };
|
||
const s = tree.finalRow.node.sum;
|
||
fill(cells, { M: 0, L: 0, S: 0 }, s, true);
|
||
for (const [u] of PAIR) delete cells[u];
|
||
C.push(sh, cells);
|
||
}
|
||
sh._tree = tree;
|
||
return sh;
|
||
}
|
||
|
||
// 간접비 항목 — 지식DB 20 §2.2 순서(KAN_ORD_1) · 이름 · 식 설명(25 §2)
|
||
const ITEMS = [
|
||
["KANNO", "간접노무비", "간 접 노 무 비", "직접노무비"],
|
||
["SANBO", "산재보험료", "산 재 보 험 료", "(직접노무비+간접노무비)"],
|
||
["KOBO", "고용보험료", "고 용 보 험 료", "(직접노무비+간접노무비)"],
|
||
["KUNBO", "건강보험료", "건 강 보 험 료", "직접노무비"],
|
||
["SLVBO", "노인장기요양보험료", "노인장기요양보험료", "건강보험료"],
|
||
["YUNBO", "연금보험료", "연 금 보 험 료", "직접노무비"],
|
||
["TBUGUM", "퇴직공제부금비", "퇴직공제부금비", "직접노무비"],
|
||
["ANJUN", "산업안전보건관리비", "산업안전보건관리비", ""],
|
||
["KITA", "기타경비", "기 타 경 비", "(직접노무비+간접노무비+재료비)"],
|
||
["HWAN", "환경보전비", "환 경 보 전 비", "(재료비 + 직접노무비 + 산출경비)"],
|
||
[
|
||
"EQIP",
|
||
"건설기계대여금지급보증 금액",
|
||
"건설기계대여금지급보증 금액",
|
||
"(재료비 + 직접노무비 + 산출경비)",
|
||
],
|
||
];
|
||
|
||
// 안전관리비 설명 — A) 관급재/1.1 포함 · B) 관급 빼고 1.2배 상한(지식DB 06)
|
||
function anjunNote(k) {
|
||
const it = k.itemMap.ANJUN;
|
||
if (!it) return "";
|
||
if (it.modeUsed === "fixed") return "";
|
||
const r = it.rate,
|
||
add = num(k._anjunAdd);
|
||
const noGov = k.dM + k.dL;
|
||
const a = trunc((k.anjunBaseRaw * r) / 100) + add;
|
||
const b = trunc((trunc((noGov * r) / 100) + add) * 1.2);
|
||
return (
|
||
`A)(직노+직재+간재+관급재/1.1)x${rateText(r)}% + ${commas(add)} = ${commas(a)}\n` +
|
||
`B)<(직노+직재+간재)x${rateText(r)}% + ${commas(add)}>의 1.2배 = ${commas(b)}`
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 원가 줄 — 순공사비 → 간접비(0원 빠짐) → 소계 → 일반관리비 → 이윤 → 단계별 추가금액 → 부가세 → 도급 → 관급 → 총공사비
|
||
* fixedKeys = D_ 표시(저장액 고정) 항목 — 식 설명 없음
|
||
*/
|
||
function costLines(k, cfg) {
|
||
const fixedKeys = new Set(Object.keys((cfg && cfg.fixed) || {}));
|
||
const govMat = k.govRawTotal + k.govFeeTotal;
|
||
const exp = [];
|
||
for (const [code, short, long, base] of ITEMS) {
|
||
const it = k.itemMap[code];
|
||
if (!it || !it.amount) continue;
|
||
let note = "";
|
||
if (!fixedKeys.has(code))
|
||
note = code === "ANJUN" ? anjunNote(k) : `${base} x ${rateText(it.rate)}%`;
|
||
exp.push({ code, short, long, amount: it.amount, rate: it.rate, note });
|
||
}
|
||
const adds = (st) => (k.adds || []).filter((a) => a.stage === st && num(a.amount));
|
||
return { exp, adds, govMat, fixedKeys };
|
||
}
|
||
|
||
// 총괄내역서(0601) — 공종 묶음 줄 + 원가 계산 줄
|
||
function summarySheet(dbf, stage, design, opts = {}) {
|
||
const ini = (dbf && dbf.start_ini) || {};
|
||
const order = C.bimokOrder(ini);
|
||
const cols = ["D", "E", "F", "G"];
|
||
const name = opts.name || "총괄내역서";
|
||
const sh = C.newSheet(name, "총 괄 내 역 서", C.projectName(dbf), "H");
|
||
const h3 = { A: "공 종", B: "명 칭", C: "규 격", _k: "head" };
|
||
order.forEach((k, i) => (h3[cols[i]] = C.bimokLabel(ini, k)));
|
||
C.push(sh, h3);
|
||
const minLevel = opts.minLevel ?? 2;
|
||
const put4 = (cells, s) => {
|
||
const v = { M: s.M, L: s.L, S: s.S, T: s.M + s.L + s.S };
|
||
order.forEach((k, i) => (cells[cols[i]] = v[k]));
|
||
};
|
||
for (const n of design._tree.out)
|
||
if (n.kind === "T" && n.level >= minLevel) {
|
||
const cells = { A: str(n.row.ITNUM).trim(), B: str(n.row.NAME1), C: str(n.row.SIZE1) };
|
||
put4(Object.assign(cells, { _k: `T${n.level}` }), n.sum);
|
||
C.push(sh, cells);
|
||
}
|
||
const k = stage.kanjub;
|
||
const L = costLines(k, stage.cfg);
|
||
const direct = design._tree.finalRow
|
||
? design._tree.finalRow.node.sum
|
||
: { M: k.dM, L: k.dL, S: k.dS };
|
||
const ga = { A: " 가.", B: "순공사비계", _k: "sub" };
|
||
put4(ga, direct);
|
||
C.push(sh, ga);
|
||
let no = 0;
|
||
const line = (label, amount, note, extra) => {
|
||
no += 1;
|
||
const cells = { B: `${String(no).padStart(2)}. ${label}`, D: amount, _k: "cost" };
|
||
if (note) cells.E = note;
|
||
return C.push(sh, Object.assign(cells, extra || {}));
|
||
};
|
||
const sub = (a, label, amount) =>
|
||
C.push(sh, { A: ` ${a}`, B: ` ${label}`, D: amount, _k: "sub" });
|
||
line(
|
||
"간접노무비",
|
||
k.kanno,
|
||
L.fixedKeys.has("KANNO") ? "" : `직접노무비 x ${rateText(k.itemMap.KANNO.rate)}%`,
|
||
);
|
||
for (const e of L.exp.filter((x) => x.code !== "KANNO"))
|
||
line(
|
||
e.short,
|
||
e.amount,
|
||
e.note,
|
||
e.code === "ANJUN" && L.govMat > 0 && !L.fixedKeys.has("ANJUN")
|
||
? { C: "A)관급재/1.1포함 적용" }
|
||
: null,
|
||
);
|
||
for (const a of L.adds(1)) line(a.name, num(a.amount));
|
||
const na = k.pureConstructionCost;
|
||
sub("나.", "소 계", na);
|
||
line(
|
||
"일반관리비",
|
||
k.ilbanAmt,
|
||
L.fixedKeys.has("ILBAN") ? "" : `(나.소 계) x ${rateText(num(stage.cfg?.rates?.ILBAN))}%`,
|
||
);
|
||
sub("다.", "소 계", na + k.ilbanAmt);
|
||
const iyunR = rateText(num(stage.cfg?.rates?.IYUN));
|
||
line("이 윤", k.iyunFinal, `(다.소계 - 재료비) x ${iyunR}% = ${commas(k.iyunRaw)}`);
|
||
for (const a of L.adds(2)) line(a.name, num(a.amount));
|
||
sub("라.", "공급가액", k.supplyFinal);
|
||
const vn = vatNote(stage.cfg);
|
||
line("부가가치세", k.vatFinal, vn === "총 원 가 x 10%" ? "공급가액 x 10%" : vn);
|
||
for (const a of L.adds(3)) line(a.name, num(a.amount));
|
||
sub("마.", "도급공사비", k.contractFinal);
|
||
if (k.govFinal)
|
||
line(
|
||
govLabel(dbf, stage, false),
|
||
k.govFinal,
|
||
`원자재대: ${commas(L.govMat)} (조달수수료: ${commas(k.govFeeTotal)} 포함)`,
|
||
);
|
||
for (const a of L.adds(4)) line(a.name, num(a.amount));
|
||
sub("바.", "총공사비", k.grandTotal);
|
||
return sh;
|
||
}
|
||
|
||
// 부가세 설명 — I_BUGA 갈래(공급가액 · 산림조합 · 면세품 · 재료비)
|
||
// 설정 두 꼴 — 옛 vatMode/vatExempt · 새 keys(I_BUGA · C_BUGA_M 원키)
|
||
const VAT_MODES = { 0: "supply_10", 1: "mat_10", 2: "zero", 3: "forest", 4: "forest_exempt" };
|
||
function vatNote(cfg) {
|
||
const keys = (cfg && cfg.keys) || {};
|
||
const m =
|
||
(cfg && cfg.vatMode) || ("I_BUGA" in keys ? VAT_MODES[num(keys.I_BUGA)] : "") || "supply_10";
|
||
const exempt = cfg && cfg.vatExempt != null ? num(cfg.vatExempt) : num(keys.C_BUGA_M);
|
||
if (m === "forest_exempt") return `(재료비 - 면세품목:₩${commas(exempt)} + 산출경비) x 10%`;
|
||
if (m === "forest") return "(재료비 + 산출경비) x 10%";
|
||
if (m === "mat_10") return "재료비 x 10%";
|
||
return m === "zero" ? "" : "총 원 가 x 10%";
|
||
}
|
||
|
||
// 관급 이름 — 25 판부터 도급자 설치 관급(C_KWANGUP_D)은 「관급자재대(도급자설치)」(울진 2025 출력)
|
||
function govLabel(dbf, stage, wide) {
|
||
const ini = (dbf && dbf.start_ini) || {};
|
||
const kv = (dbf && dbf.prjlst_smt) || {};
|
||
const d = num(kv[`${stage.slot}_C_KWANGUP_D`]);
|
||
if (num(ini.PRG_V) >= 25 && d > 0) return "관급자재대(도급자설치)";
|
||
return wide ? "관 급 자 재 대" : "관급자재대";
|
||
}
|
||
|
||
// 원가계산서 줄 — 차수 하나의 { key, 칸, 금액, 설명 }
|
||
function wonRows(dbf, stage) {
|
||
const k = stage.kanjub;
|
||
const L = costLines(k, stage.cfg);
|
||
const rows = [];
|
||
const add = (key, cells, amount, note) => rows.push({ key, cells, amount, note: note || "" });
|
||
add("dM", { A: "순 공 사 원 가", B: "재 료 비", C: "직 접 재 료 비" }, k.dM);
|
||
add("iM", { C: "간 접 재 료 비" }, 0);
|
||
add("by", { C: "작업설,부산물등(△)" }, 0);
|
||
add("sM", { C: "소 계" }, k.dM);
|
||
add("dL", { B: "노 무 비", C: "직 접 노 무 비" }, k.dL);
|
||
const kn = L.fixedKeys.has("KANNO") ? "" : `직접노무비 x ${rateText(k.itemMap.KANNO.rate)}%`;
|
||
add("KANNO", { C: "간 접 노 무 비" }, k.kanno, kn);
|
||
add("sL", { C: "소 계" }, k.totalLabor);
|
||
add("dS", { B: "경 비", C: "산 출 경 비" }, k.dS);
|
||
for (const e of L.exp.filter((x) => x.code !== "KANNO")) {
|
||
const note =
|
||
e.code === "ANJUN" && stage.cfg.anjunAuto === 2 && stage.slot > 1
|
||
? "당초금액 + 안전관리비 증감액"
|
||
: e.note;
|
||
add(e.code, { C: e.long }, e.amount, note);
|
||
}
|
||
for (const a of L.adds(1)) add(`A1${a.name}`, { C: a.name }, num(a.amount));
|
||
add("sS", { C: "소 계" }, k.totalExpense);
|
||
const ilR = rateText(num(stage.cfg?.rates?.ILBAN));
|
||
add(
|
||
"ILBAN",
|
||
{ A: "일 반 관 리 비" },
|
||
k.ilbanAmt,
|
||
L.fixedKeys.has("ILBAN") ? "" : `순공사원가 x ${ilR}%`,
|
||
);
|
||
const iyR = rateText(num(stage.cfg?.rates?.IYUN));
|
||
add("IYUN", { A: "이 윤" }, k.iyunFinal, `(노무비+경비+일반관리비) x ${iyR}%`);
|
||
for (const a of L.adds(2)) add(`A2${a.name}`, { A: a.name }, num(a.amount));
|
||
add("SUP", { A: "총 원 가" }, k.supplyFinal);
|
||
add("VAT", { A: "부 가 가 치 세" }, k.vatFinal, vatNote(stage.cfg));
|
||
for (const a of L.adds(3)) add(`A3${a.name}`, { A: a.name }, num(a.amount));
|
||
add("CON", { A: "도 급 공 사 비" }, k.contractFinal);
|
||
if (k.govFinal) {
|
||
const gn = `원자재대: ${commas(L.govMat)} (조달수수료: ${commas(k.govFeeTotal)} 포함)`;
|
||
add("GOV", { A: govLabel(dbf, stage, true) }, k.govFinal, gn);
|
||
}
|
||
for (const a of L.adds(4)) add(`A4${a.name}`, { A: a.name }, num(a.amount));
|
||
add("GRAND", { A: "총 공 사 비" }, k.grandTotal);
|
||
return rows;
|
||
}
|
||
|
||
/**
|
||
* 공사원가계산서(0608) — 비목 · 금액 · 구성비(총공사비 대비 %) · 비고(식 설명)
|
||
* base(당초 차수)를 주면 변경 양식 — 당초 · 변경 · 증감 · 구성비(변경) · 비고(변경)
|
||
*/
|
||
function wonSheet(dbf, stage, base) {
|
||
const sh = C.newSheet(
|
||
"공사원가계산서",
|
||
"공 사 원 가 계 산 서",
|
||
C.projectName(dbf),
|
||
base ? "I" : "G",
|
||
);
|
||
const now = wonRows(dbf, stage);
|
||
const grand = stage.kanjub.grandTotal || 1;
|
||
if (!base) {
|
||
C.put(sh, 3, { A: "구 분 ", D: "금 액", E: "구 성 비", F: "비 고", _k: "head" });
|
||
C.put(sh, 4, { A: " 비 목", _k: "head" });
|
||
now.forEach((r, i) => {
|
||
const c = Object.assign({ D: r.amount, E: (r.amount / grand) * 100, _k: "cost" }, r.cells);
|
||
if (r.note) c.F = r.note;
|
||
C.put(sh, 5 + i, c);
|
||
});
|
||
} else {
|
||
C.put(sh, 3, { A: "구 분 ", D: "금 액", G: "구 성 비", H: "비 고", _k: "head" });
|
||
C.put(sh, 4, { A: " 비 목", D: "당 초", E: "변 경", F: "증 감", _k: "head" });
|
||
const old = Object.fromEntries(wonRows(dbf, base).map((r) => [r.key, r]));
|
||
now.forEach((r, i) => {
|
||
const o = old[r.key] ? old[r.key].amount : 0;
|
||
const c = Object.assign({ D: o, E: r.amount, F: r.amount - o, _k: "cost" }, r.cells);
|
||
c.G = (r.amount / grand) * 100;
|
||
if (r.note) c.H = r.note;
|
||
C.put(sh, 5 + i, c);
|
||
});
|
||
}
|
||
const last = sh.rows[sh.rows.length - 1];
|
||
last[base ? "G" : "E"] = 100;
|
||
return sh;
|
||
}
|
||
|
||
/**
|
||
* 차수 맥락 — calc.js 결과(stages)에서 n 차수 내역·원가 · 설정(kanjubStages)
|
||
* 안전관리비 기초액(C_ANJUN_G)은 식 설명용
|
||
*/
|
||
function stageOf(state, computed, n = 1) {
|
||
const st = (computed && computed.stages && computed.stages[n]) || {
|
||
boq: computed.boqOrig,
|
||
kanjub: computed.kanjubOrig,
|
||
};
|
||
const cfg = (state.kanjubStages && state.kanjubStages[n]) || state.kanjubConfig || {};
|
||
const kanjub = Object.assign({}, st.kanjub, { _anjunAdd: num(cfg.anjunBaseAdd) });
|
||
if (cfg.fixed && cfg.fixed.ANJUN != null && kanjub.itemMap.ANJUN)
|
||
kanjub.itemMap = Object.assign({}, kanjub.itemMap, {
|
||
ANJUN: Object.assign({}, kanjub.itemMap.ANJUN, { modeUsed: "fixed" }),
|
||
});
|
||
return { boq: st.boq, kanjub, cfg, slot: n };
|
||
}
|
||
|
||
function buildBoqSheets(dbf, state, computed, n = 1, opts = {}) {
|
||
const stage = stageOf(state, computed, n);
|
||
const design = C.applyForm(designSheet(dbf, stage, opts), dbf, "0603");
|
||
const summary = C.applyForm(summarySheet(dbf, stage, design, opts), dbf, "0601");
|
||
const base = n > 1 && opts.changeForm !== false ? stageOf(state, computed, 1) : null;
|
||
const won = C.applyForm(wonSheet(dbf, stage, base), dbf, "0608");
|
||
return { stage, 공사원가계산서: won, 총괄내역서: summary, 설계내역서: design };
|
||
}
|
||
|
||
return { buildBoqSheets, boqTree, designSheet, summarySheet, wonSheet, stageOf, ITEMS };
|
||
})();
|
||
|
||
if (typeof window !== "undefined") window.STMATE_REPORT_BOQ = StmateReportBoq;
|
||
if (typeof module !== "undefined" && module.exports) module.exports = StmateReportBoq;
|