/* -*- coding: utf-8 -*- */ /** * 조달청 공사원가호환규정 XML (report_pps.js) — 스키마 V1.0.5 한글 요소 이름 그대로 * 근거: 지식DB STmate/old/20_분석 30(조달청 호환규정 XSD) · old/30_원자료/조달청_공사원가호환규정.xsd * 28 §3(Conv_ST8_XML 변환) · 20 §2.2(간접비 코드 301~370) * 정답 대조: resources/tester/test_stmate_report_pps.py (XSD 검증 + 금액 합 = 원가계산서) * 짜임: 공사원가호환규정 → 작성정보 · 공사정보(기계경비산출) · 공사내역서(공사제경비 · 단일공사내역서) */ const StmateReportPps = (() => { 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 B = g.STMATE_REPORT_BOQ || require("./report_boq.js"); const { num, str, code } = C; const esc = (s) => String(s ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); // normalizedString — 줄바꿈·탭 없음 · 길이 제한(XSD maxLength) const ns = (s, max) => String(s ?? "") .replace(/[\r\n\t]+/g, " ") .trim() .slice(0, max); const codeOf = (s, max) => String(s ?? "") .toUpperCase() .replace(/[^0-9A-Z]/g, "") .slice(0, max); const dbl = (v) => { const n = Number(v); return Number.isFinite(n) ? String(C.fix(n, 6)) : "0"; }; const tag = (name, inner) => `<${name}>${inner}`; const leaf = (name, v) => `<${name}>${esc(v)}`; const RES = { M: "자재", L: "노무", S: "경비항목" }; // 자원구분 — 기계경비상세는 손료(경비 천원 단위)·기계경비 갈래 · 그 밖은 일위대가 const resKind = (c, machine, unit) => machine && c[0] === "S" && unit === "천원" ? "손료" : RES[c[0]] || (machine ? "기계경비" : "일위대가"); // 단가 3칸 + 금액 3칸 const money = (u, a) => leaf("재료비단가", dbl(u.M)) + leaf("노무비단가", dbl(u.L)) + leaf("경비단가", dbl(u.S)) + leaf("재료비", dbl(a.M)) + leaf("노무비", dbl(a.L)) + leaf("경비", dbl(a.S)); // 구성 줄 — 일위대가상세 · 기계경비상세 function detailLines(ev, machine) { const nm = machine ? "기계경비상세" : "일위대가상세"; return ev.lines .filter((ln) => !ln.text) .map((ln) => { const c = ln.code === "PR" ? "PR" : ln.code; const ref = ln.ref || {}; let body = leaf("자원구분", resKind(c, machine, String(ref.unit || "").trim())) + leaf("자원코드", codeOf(c, 17) || "PR") + leaf("계산제외", "false") + leaf("품명", ns(ref.name || "비율 항목", 50)) + leaf("규격", ns(ref.spec || "", 100)) + leaf("단위", ns(ln.pct ? "%" : ref.unitDisp || ln.unit || "", 10)) + leaf("수량", dbl(ln.qty)) + money(ln.u, ln.a); if (!machine) body += leaf("관급", "false"); return tag(nm, body + leaf("비고", "")); }) .join(""); } // 공사제경비 — 원가계산서 줄(간접비 코드 · 식 설명 · 요율 · 금액) function costItems(stage) { const k = stage.kanjub; const rates = (stage.cfg && stage.cfg.rates) || {}; const DTP = { KANNO: 201, SANBO: 301, KOBO: 302, KUNBO: 303, SLVBO: 310, YUNBO: 304 }; Object.assign(DTP, { TBUGUM: 305, ANJUN: 306, KITA: 321, HWAN: 309, EQIP: 311 }); const out = []; const item = (dtp, sum, name, formula, rate, add, amount, desc) => out.push( tag( "제경비항목", leaf("세부공종코드", String(dtp)) + leaf("집계항목여부", sum ? "true" : "false") + leaf("비목명", ns(name, 50)) + leaf("적용산식", ns(formula, 200)) + leaf("적용요율", dbl(rate)) + leaf("추가금액", dbl(add)) + leaf("산출금액", dbl(amount)) + leaf("적용산식설명", ns(desc, 200)), ), ); item(100, true, "순공사비계", "재료비+노무비+경비", 0, 0, k.dT, "직접공사비"); for (const [cd, short] of B.ITEMS) { const it = k.itemMap[cd]; if (!it || !it.amount) continue; const add = cd === "ANJUN" ? num(stage.cfg.anjunBaseAdd) : 0; item( DTP[cd], false, short, `${it.baseDesc || ""} x ${it.rate}%`, it.rate, add, it.amount, it.baseDesc || "", ); } item(400, true, "소계", "순공사원가", 0, 0, k.pureConstructionCost, "재료비+노무비+경비"); item( 401, false, "일반관리비", `순공사원가 x ${num(rates.ILBAN)}%`, num(rates.ILBAN), 0, k.ilbanAmt, k.ilbanDesc || "", ); item( 402, false, "이윤", `(노무비+경비+일반관리비) x ${num(rates.IYUN)}%`, num(rates.IYUN), 0, k.iyunFinal, k.iyunDesc || "", ); item(500, true, "공급가액", "순공사원가+일반관리비+이윤", 0, 0, k.supplyFinal, "총원가"); item(502, false, "부가가치세", "공급가액 x 10%", 10, 0, k.vatFinal, "부가가치세"); item(600, true, "도급공사비", "공급가액+부가가치세", 0, 0, k.contractFinal, "도급액"); if (k.govFinal) item(653, false, "관급자재대", "관급자재 합계", 0, 0, k.govFinal, "관급자재"); item(700, true, "총공사비", "도급공사비+관급자재대", 0, 0, k.grandTotal, "총공사비"); return tag("공사제경비", out.join("")); } // 내역서 — 공종(T 묶음)마다 하나 · 내역서번호 = 단계별 두 자리 · 세부공종 = 직속 품목 function boqXml(tree, idx) { const out = []; const walk = (node, prefix, govFlag, exFlag) => { let i = 0; const items = node.children.filter((c) => c.kind === "item"); if (node.kind === "T" && node.row) { const r = node.row; const ex = exFlag || node.excluded; const gov = govFlag || (node.excluded && /관급/.test(str(r.NAME1))); const kind = ex ? (gov ? "관급자재공종" : "제요율적용제외공종") : "일반공종"; const sub = items .map((it) => { const u = it.unitP, a = it.amt; return tag( "세부공종", leaf("세부공종코드", codeOf(it.code, 17)) + leaf("자원구분", resKind(it.code, false)) + leaf("계산제외", "false") + leaf("세부공종번호", ns(it.row.ITNUM, 20)) + leaf("세부공종명", ns(it.row.NAME1, 50)) + leaf("규격", ns(it.row.SIZE1, 100)) + leaf("단위", ns(C.unitText(it.row.UNIT1), 10)) + leaf("수량", dbl(it.qty)) + money(u, a) + leaf("관급", gov ? "true" : "false") + leaf("자재구분", "구분없음") + leaf("세부공종구분", "일반내역") + leaf("비고", ns((idx.by[it.code] && idx.by[it.code].BQCODE) || "", 50)), ); }) .join(""); out.push( tag( "내역서", leaf("내역서번호", prefix) + leaf("공종구분", kind) + leaf("계산제외", "false") + leaf("공종번호", ns(r.ITNUM, 20)) + leaf("공종명", ns(r.NAME1 || r.SIZE1 || "공종", 50)) + leaf("재료비", dbl(node.sum.M)) + leaf("노무비", dbl(node.sum.L)) + leaf("경비", dbl(node.sum.S)) + leaf("비고", "") + sub, ), ); govFlag = gov; exFlag = ex; } for (const ch of node.children) if (ch.kind === "T") walk(ch, prefix + String(++i).padStart(2, "0"), govFlag, exFlag); }; walk(tree.root, "", false, false); return out.join(""); } /** * XML 문서 — dbf(불러온 자료) · state · computed(calculateAll) · n 차수 * opts.today = 파일제작일(YYYY-MM-DD) · opts.company = 작성업체명 */ function buildPpsXml(dbf, state, computed, n = 1, opts = {}) { const ctx = U.unitContext(dbf); const { idx, ini } = ctx; const stage = B.stageOf(state, computed, n); const design = B.designSheet(dbf, stage); const tree = design._tree; const pr = (dbf && dbf.prjlst) || {}; const pname = ns(C.projectName(dbf) || "공사", 100); const today = opts.today || new Date().toISOString().slice(0, 10); const rx = String(ini.RXXNM || "1/8*16/12*25/20"); const coef = C.fix(eval1(rx), 8); const writer = tag( "작성정보", leaf("작성업체명", ns(opts.company || ini.SUBNM || "작성업체", 50)) + leaf("프로그램명", "STMATE w 8.0 재현") + leaf("프로그램버전", "8.0") + leaf("파일제작일", today) + leaf("파일검증여부", "false"), ); const mach = tag( "기계경비산출", tag( "환율", leaf("미국_1USD", dbl(num(ini.EXLNM1))) + leaf("유럽연합_1EUR", dbl(num(ini.EXEUR1))) + leaf("일본_100엔", dbl(num(ini.EXYEN1))), ) + tag( "시간당노임산출", tag( "H0-일반구간", leaf("산출식", ns(rx, 200)) + leaf("계산값", dbl(coef)) + leaf("적용구분", "산출식"), ), ), ); const info = tag( "공사정보", leaf("공사관리번호", ns(pr.PNUM || pr.PCODE || "0", 20) || "0") + leaf("주공사", "토목공사") + leaf("공사명", pname) + leaf("비고", "") + mach, ); const head = (c, r, tot, extra) => leaf(extra, codeOf(c, extra === "기계경비코드" ? 17 : 12)) + leaf("번호", String(ctx.hopyoNo[c] || 1)) + leaf("품명", ns(r.NAME, 50) || c) + leaf("규격", ns(r.SIZE, 100)) + leaf("단위", ns(C.unitText(r.UNIT), 10)); const ilwi = idx .ofPrefix("B") .map((r) => { const c = code(r), ev = ctx.evals[c], t = ctx.totals[c] || idx.price(c, 1); const bm = str(r.SBMOK).trim() === "S" ? "경비" : "합계"; return tag( "일위대가", head(c, r, t, "일위대가코드") + leaf("재료비", dbl(t.M)) + leaf("노무비", dbl(t.L)) + leaf("경비", dbl(t.S)) + leaf("비목구분", bm) + leaf("비고", ns(r.BQCODE, 50)) + (ev ? detailLines(ev, false) : ""), ); }) .join(""); const memos = (dbf && dbf.costn_memos) || {}; const san = idx .ofPrefix("D") .map((r) => { const c = code(r), p = idx.price(c, 1); return tag( "중기단가산출", head(c, r, p, "중기단가산출코드") + leaf("재료비", dbl(p.M)) + leaf("노무비", dbl(p.L)) + leaf("경비", dbl(p.S)) + leaf("비목구분", "합계") + leaf("비고", ns(r.BQCODE, 50)) + leaf("중기단가산출식", (memos[c] && memos[c].text) || ""), ); }) .join(""); const machine = idx .ofPrefix("X") .map((r) => { const c = code(r), ev = ctx.evals[c], t = ctx.totals[c] || idx.price(c, 1); return tag( "기계경비", head(c, r, t, "기계경비코드") + leaf("작업시간", "E0") + leaf("작업장소", "H0") + leaf("재료비", dbl(t.M)) + leaf("노무비", dbl(t.L)) + leaf("경비", dbl(t.S)) + leaf("비고", ns(r.BQCODE, 50)) + (ev ? detailLines(ev, true) : ""), ); }) .join(""); const base = (r) => leaf("품명", ns(r.NAME, 50) || code(r)) + leaf("규격", ns(r.SIZE, 100)) + leaf("단위", ns(C.unitText(r.UNIT), 10)); const res = (p, nm, codeTag, body) => idx .ofPrefix(p) .map((r) => tag(nm, leaf(codeTag, codeOf(code(r), 16)) + base(r) + body(r))) .join(""); const pv = (r) => idx.price(code(r), 1); const mats = res( "M", "자재", "자재코드", (r) => leaf("재료비단가", dbl(pv(r).M + pv(r).L + pv(r).S)) + leaf("비고", ns(r.BQCODE, 50)), ); const labs = res( "L", "노무", "노무코드", (r) => leaf("노무비단가", dbl(pv(r).M + pv(r).L + pv(r).S)) + leaf("비고", ns(r.BQCODE, 50)), ); const exps = idx .ofPrefix("S") .map((r) => { const p = pv(r); const machineCost = str(r.UNIT).trim() === "천원"; return tag( "경비항목", leaf("경비항목코드", codeOf(code(r), 16)) + leaf("경비구분", machineCost ? "건설기계" : "일반경비") + base(r) + leaf("통화구분", "대한민국_천원") + leaf("재료비단가", dbl(p.M)) + leaf("노무비단가", dbl(p.L)) + leaf("경비단가", dbl(p.S)) + leaf("비고", ns(r.BQCODE, 50)), ); }) .join(""); const single = tag( "단일공사내역서", tag("단일공사정보", leaf("공사구분", "토목공사") + leaf("공사설명", pname)) + boqXml(tree, idx) + ilwi + san + machine + mats + labs + exps, ); return ( '\n' + tag( "공사원가호환규정", leaf("스키마버전", "1.0.5") + writer + info + tag("공사내역서", costItems(stage) + single), ) ); } // 노임 환산 계수 — 식을 왼쪽부터 곱·나눔(Start8x.Ini RXXNM) function eval1(expr) { let acc = 1; for (const [, op, n] of String(expr || "") .replace(/\s+/g, "") .matchAll(/([*/])?(\d+(?:\.\d+)?)/g)) acc = op === "/" ? acc / Number(n) : acc * Number(n); return acc; } return { buildPpsXml, eval1 }; })(); if (typeof window !== "undefined") window.STMATE_REPORT_PPS = StmateReportPps; if (typeof module !== "undefined" && module.exports) module.exports = StmateReportPps;