443 lines
17 KiB
JavaScript
443 lines
17 KiB
JavaScript
/* -*- coding: utf-8 -*- */
|
|
/**
|
|
* 보고서 양식 변형 (report_forms.js) — 원본 출력 견본이 없는 SET_REP 서식(머리 칸·제목은 SET_REP · 값은 계산 엔진)
|
|
* 0303 일위대가표(수공양식) · 0605 분할내역서 · 0609 총괄집계표 · 0613·0614 견적내역서 · 0615 견적단가대비표
|
|
* 0606·0654·0656 내역서(도공·굴착·시도) · 0651·0655 구.시도(시기금·구기금) · 0652·0653·0672 총괄 변형
|
|
* 0675~0677 재료비·노무비·중장비 집계표 · 0671·0678·0679 투입계획 · 0695 간이명세서 · 0206 실적단가목록표
|
|
* 근거: 지식DB STmate/old/20_분석 20 §3(SET_REP FLD_N·FLD_W) · old/30_원자료/print_form_map.txt · 24 §2.4(AMTTOHAN)
|
|
* 판정: 부분 — 원본 STMATE 출력 견본이 없어 열 배치는 SET_REP 칸 이름 차례를 따름(셀 대조 없음)
|
|
*/
|
|
const StmateReportForms = (() => {
|
|
const g = typeof globalThis !== "undefined" ? globalThis : {};
|
|
const C = g.STMATE_REPORT_CORE || require("./report_core.js");
|
|
const B = g.STMATE_REPORT_BOQ || require("./report_boq.js");
|
|
const Co = g.STMATE_REPORT_COLL || require("./report_coll.js");
|
|
const In = g.STMATE_REPORT_INIT || require("./report_init.js");
|
|
const { num, str, code, half, trunc } = C;
|
|
|
|
// SET_REP 칸 이름(FLD_N1~) — 빈 이름 빼고
|
|
function fields(dbf, indx) {
|
|
const r = ((dbf && dbf.set_rep_rows) || []).find((x) => String(x.INDX).trim() === indx);
|
|
const out = [];
|
|
if (r) for (let i = 1; i <= 11; i++) out.push(str(r[`FLD_N${i}`]).trim());
|
|
return out;
|
|
}
|
|
const form = (sh, dbf, indx) => C.applyForm(sh, dbf, indx);
|
|
const T4 = (s) => s.M + s.L + s.S;
|
|
const excluded = (n) => {
|
|
for (let p = n.parent; p; p = p.parent) if (p.excluded) return true;
|
|
return false;
|
|
};
|
|
|
|
/** 0303 일위대가표(수공양식) — 호표마다 「항목번호 · 제 n 호표 · 공종 · 단위」 머리 + 구성 줄(재·노·경 단가·금액) */
|
|
function iLwiHandSheet(u) {
|
|
const { dbf, idx, evals, hopyoNo } = u;
|
|
const f = fields(dbf, "0303");
|
|
const sh = C.newSheet("일위대가표수공", "일 위 대 가 표", C.projectName(dbf), "O");
|
|
const head = { A: "명 칭", B: "규 격", C: "단위", D: "수 량", _k: "head" };
|
|
Object.assign(head, { E: "재료비 단가", F: "재료비 금액", G: "노무비 단가", H: "노무비 금액" });
|
|
Object.assign(head, { I: "경비 단가", J: "경비 금액", K: "합계 금액", L: "비 고" });
|
|
for (const r of idx.ofPrefix("B")) {
|
|
const c = code(r);
|
|
const ev = evals[c];
|
|
C.push(sh, {
|
|
A: `${f[0] || "항 목 번 호 :"} ${c}`,
|
|
E: `${f[1] || "제"} ${hopyoNo[c]} ${f[2] || "호표"}`,
|
|
_k: "hopyo",
|
|
});
|
|
C.push(sh, {
|
|
A: `${f[3] || "공 종 :"} ${str(r.NAME)} ${str(r.SIZE)}`.trim(),
|
|
_k: "parent",
|
|
});
|
|
C.push(sh, { A: `${f[4] || "단 위 :"} ${C.unitText(r.UNIT)}`, _k: "parent" });
|
|
C.push(sh, Object.assign({}, head));
|
|
if (!ev) continue;
|
|
for (const ln of ev.lines) {
|
|
if (ln.text) continue;
|
|
const ref = ln.ref || {};
|
|
const cells = {
|
|
A: ref.name || ln.code,
|
|
B: ref.spec || "",
|
|
C: ln.pct ? "%" : ref.unitDisp || ln.unit,
|
|
};
|
|
Object.assign(cells, {
|
|
D: ln.qty,
|
|
E: C.fix(ln.u.M),
|
|
F: C.fix(ln.a.M, 1),
|
|
G: C.fix(ln.u.L),
|
|
H: C.fix(ln.a.L, 1),
|
|
});
|
|
Object.assign(cells, {
|
|
I: C.fix(ln.u.S),
|
|
J: C.fix(ln.a.S, 1),
|
|
K: C.fix(T4(ln.a), 1),
|
|
L: ln.code,
|
|
_k: "line",
|
|
});
|
|
C.push(sh, cells);
|
|
}
|
|
C.push(sh, { A: "합 계", F: ev.tot.M, H: ev.tot.L, J: ev.tot.S, K: ev.T, _k: "total" });
|
|
}
|
|
return form(sh, dbf, "0303");
|
|
}
|
|
|
|
/**
|
|
* 0605 분할내역서(횡양식) — 「전체」(설계 수량·금액) · 「금회」(분할 비율 opts.splitRate %) 두 묶음
|
|
* 금회 수량 = 전체 × 비율(소수 셋째 자리 사사오입) · 금액 = 단가 × 금회 수량 원 미만 절사
|
|
*/
|
|
function splitSheet(dbf, tree, opts = {}) {
|
|
const f = fields(dbf, "0605");
|
|
const rate = opts.splitRate == null ? 100 : num(opts.splitRate);
|
|
const sh = C.newSheet("분할내역서", "분 할 내 역 서", C.projectName(dbf), "L");
|
|
C.push(sh, {
|
|
A: "공 종",
|
|
B: "명 칭",
|
|
C: "규 격",
|
|
D: "단위",
|
|
E: f[0] || "전 체",
|
|
H: f[1] || "금 회",
|
|
K: "비 고",
|
|
_k: "head",
|
|
});
|
|
C.push(sh, {
|
|
E: "수 량",
|
|
F: "단 가",
|
|
G: "금 액",
|
|
H: "수 량",
|
|
I: "금 액",
|
|
J: "비율(%)",
|
|
_k: "head",
|
|
});
|
|
let tot = 0,
|
|
cur = 0;
|
|
for (const n of tree.out) {
|
|
if (n.kind === "T") {
|
|
C.push(sh, {
|
|
A: str(n.row.ITNUM).trim(),
|
|
B: str(n.row.NAME1),
|
|
G: T4(n.sum),
|
|
_k: `T${n.level}`,
|
|
});
|
|
continue;
|
|
}
|
|
if (n.kind !== "item") continue;
|
|
const unit = T4(n.unitP);
|
|
const q2 = half((n.qty * rate) / 100, 3);
|
|
const a2 = n.pct ? trunc((T4(n.amt) * rate) / 100) : trunc(unit * q2);
|
|
if (!excluded(n)) ((tot += T4(n.amt)), (cur += a2));
|
|
const cells = {
|
|
B: str(n.row.NAME1),
|
|
C: str(n.row.SIZE1),
|
|
D: C.unitText(n.row.UNIT1),
|
|
E: n.qty,
|
|
F: unit,
|
|
};
|
|
Object.assign(cells, { G: T4(n.amt), H: q2, I: a2, J: rate, _k: "item", _code: n.code });
|
|
C.push(sh, cells);
|
|
}
|
|
C.push(sh, {
|
|
B: "합 계",
|
|
G: tot,
|
|
I: cur,
|
|
J: tot ? C.fix((cur / tot) * 100, 2) : 0,
|
|
_k: "final",
|
|
});
|
|
return form(sh, dbf, "0605");
|
|
}
|
|
|
|
/**
|
|
* 0609 총괄집계표 — 구분(공종 묶음 · 원가 항목) · 금액 · 구성비(총공사비 대비 %) · 비고
|
|
* 공종 = opts.level 단(기본 2) 묶음의 직접공사비 · 원가 항목 = 공사원가계산서 줄
|
|
*/
|
|
function totalSheet(dbf, stage, tree, opts = {}) {
|
|
const f = fields(dbf, "0609");
|
|
const sh = C.newSheet("총괄집계표", "총 괄 집 계 표", C.projectName(dbf), "F");
|
|
C.push(sh, {
|
|
A: f[0] || "구 분",
|
|
C: f[1] || "금 액",
|
|
D: f[2] || "구 성 비",
|
|
E: f[3] || "비 고",
|
|
_k: "head",
|
|
});
|
|
const grand = stage.kanjub.grandTotal || 1;
|
|
const level = opts.level || 2;
|
|
for (const n of tree.out)
|
|
if (n.kind === "T" && n.level === level && !n.excluded) {
|
|
const v = T4(n.sum);
|
|
C.push(sh, {
|
|
A: str(n.row.NAME1).trim(),
|
|
C: v,
|
|
D: C.fix((v / grand) * 100, 4),
|
|
E: "직접공사비",
|
|
_k: "item",
|
|
});
|
|
}
|
|
const won = B.wonSheet(dbf, stage, null);
|
|
for (const r of won.rows.slice(4)) {
|
|
if (r.D == null) continue;
|
|
const lbl = [r.A, r.B, r.C].filter((x) => x && String(x).trim()).pop();
|
|
C.push(sh, {
|
|
A: String(lbl || "").trim(),
|
|
C: r.D,
|
|
D: C.fix((r.D / grand) * 100, 4),
|
|
E: r.F || "",
|
|
_k: "cost",
|
|
});
|
|
}
|
|
C.push(sh, { A: In.amtToHan(stage.kanjub.grandTotal), _k: "final" });
|
|
return form(sh, dbf, "0609");
|
|
}
|
|
|
|
/**
|
|
* 0615 견적단가대비표 — 재료(M) 중 견적 가격지(5칸 AMT_51 · JMOK5) 값이 있는 것 · 견적단가 ↔ 기준(적용) 단가 · 차이 · 비율
|
|
*/
|
|
function estimateCompareSheet(u) {
|
|
const { dbf, idx, ini } = u;
|
|
const f = fields(dbf, "0615");
|
|
const sh = C.newSheet("견적단가대비표", "견 적 단 가 대 비 표", C.projectName(dbf), "J");
|
|
const est = str(ini.JMOK5 || "견적단가").trim();
|
|
C.push(sh, {
|
|
A: "코드번호",
|
|
B: "명 칭",
|
|
C: "규 격",
|
|
D: "단위",
|
|
E: f[0] || est,
|
|
F: f[1] || "기준단가",
|
|
G: "차 이",
|
|
H: "비율(%)",
|
|
I: "비 고",
|
|
_k: "head",
|
|
});
|
|
for (const r of idx.ofPrefix("M")) {
|
|
const e = num(r.AMT_51);
|
|
if (!e) continue;
|
|
const c = code(r);
|
|
const p = idx.price(c, 1);
|
|
const base = T4(p);
|
|
const cells = {
|
|
A: c,
|
|
B: str(r.NAME),
|
|
C: str(r.SIZE),
|
|
D: C.unitText(r.UNIT),
|
|
E: e,
|
|
F: base,
|
|
G: C.fix(e - base, 2),
|
|
};
|
|
Object.assign(cells, {
|
|
H: base ? C.fix((e / base) * 100, 2) : 0,
|
|
I: str(r.PG_51).trim(),
|
|
_k: "item",
|
|
_code: c,
|
|
});
|
|
C.push(sh, cells);
|
|
}
|
|
return form(sh, dbf, "0615");
|
|
}
|
|
|
|
/**
|
|
* 0651·0655 구.시도 양식 — 금액을 시기금·구기금으로 나눔(opts.fundRate = 시기금 %, 기본 100) · 합계
|
|
* 0655 = 품목 줄 · 0651 = 공종 묶음 줄
|
|
*/
|
|
function fundSheet(dbf, tree, indx, opts = {}) {
|
|
const f = fields(dbf, indx);
|
|
const rate = opts.fundRate == null ? 100 : num(opts.fundRate);
|
|
const items = indx === "0655";
|
|
const id = items ? "설계내역서구시도" : "총괄내역서구시도";
|
|
const sh = C.newSheet(
|
|
id,
|
|
items ? "설 계 내 역 서 (구.시도)" : "총 괄 내 역 서 (구.시도)",
|
|
C.projectName(dbf),
|
|
"J",
|
|
);
|
|
C.push(sh, {
|
|
A: "공 종",
|
|
B: "명 칭",
|
|
C: "규 격",
|
|
D: items ? "수 량" : "",
|
|
E: f[0] || "시 기 금",
|
|
F: f[1] || "구 기 금",
|
|
G: f[2] || "합 계",
|
|
_k: "head",
|
|
});
|
|
for (const n of tree.out) {
|
|
if (!(n.kind === "T" || (items && n.kind === "item"))) continue;
|
|
const v = T4(n.kind === "T" ? n.sum : n.amt);
|
|
const si = trunc((v * rate) / 100);
|
|
const cells = {
|
|
A: n.kind === "T" ? str(n.row.ITNUM).trim() : "",
|
|
B: str(n.row.NAME1),
|
|
C: str(n.row.SIZE1),
|
|
};
|
|
if (n.kind === "item") cells.D = n.qty;
|
|
Object.assign(cells, { E: si, F: v - si, G: v, _k: n.kind === "T" ? `T${n.level}` : "item" });
|
|
C.push(sh, cells);
|
|
}
|
|
return form(sh, dbf, indx);
|
|
}
|
|
|
|
/**
|
|
* 공종별 집계(0675 재료비 · 0676 노무비 · 0677 중장비) · 투입계획(0678 자재 · 0679 인력 · 0671 장비)
|
|
* 공종 = 설계내역 opts.level 단(기본 2) 묶음 · 작업명 = 품목 명칭 · 구분 = 투입 자원 · 수량 = 투입 수량 전개(report_coll)
|
|
*/
|
|
function groupCollSheet(u, tree, indx, prefix, opts = {}) {
|
|
const { dbf, idx } = u;
|
|
const f = fields(dbf, indx);
|
|
const rep = ((dbf && dbf.set_rep_rows) || []).find((x) => String(x.INDX).trim() === indx) || {};
|
|
const title = str(rep.HEADTITLE).trim() || indx;
|
|
const sh = C.newSheet(title.replace(/[\s,/]+/g, ""), title, C.projectName(dbf), "J");
|
|
const plan = ["0671", "0678", "0679"].includes(indx);
|
|
C.push(sh, {
|
|
A: f[0] || "공 종",
|
|
B: f[1] || "작 업 명",
|
|
C: f[2] || "구 분",
|
|
D: "규 격",
|
|
E: "단위",
|
|
F: "수 량",
|
|
G: "단 가",
|
|
H: plan ? "투입 시기" : "금 액",
|
|
_k: "head",
|
|
});
|
|
const level = opts.level || 2;
|
|
const groupOf = (n) => {
|
|
let p = n.parent;
|
|
while (p && p.row && p.level > level) p = p.parent;
|
|
return p && p.row ? p : null;
|
|
};
|
|
const blocks = new Map();
|
|
for (const n of tree.out) {
|
|
if (n.kind !== "item" || n.pct || excluded(n)) continue;
|
|
const gnode = groupOf(n);
|
|
const key = gnode ? gnode.row._recno : "-";
|
|
if (!blocks.has(key)) blocks.set(key, { node: gnode, items: [] });
|
|
blocks.get(key).items.push(n);
|
|
}
|
|
for (const { node, items } of blocks.values()) {
|
|
C.push(sh, { A: node ? str(node.row.NAME1).trim() : "", _k: "T2" });
|
|
for (const it of items) {
|
|
const ex = Co.explode(dbf, [{ code: it.code, qty: it.qty }]);
|
|
const used = Object.keys(ex.qty)
|
|
.filter((c) => c[0] === prefix && c !== it.code)
|
|
.sort();
|
|
if (it.code[0] === prefix) used.unshift(it.code);
|
|
for (const c of used) {
|
|
const r = idx.by[c];
|
|
if (!r) continue;
|
|
const q = half(ex.qty[c], 2);
|
|
const p = idx.price(c, 1);
|
|
const cells = {
|
|
B: str(it.row.NAME1),
|
|
C: str(r.NAME),
|
|
D: str(r.SIZE),
|
|
E: C.unitText(r.UNIT),
|
|
F: q,
|
|
G: T4(p),
|
|
};
|
|
if (!plan) cells.H = half(q * T4(p));
|
|
C.push(sh, Object.assign(cells, { _k: "item", _code: c }));
|
|
}
|
|
}
|
|
}
|
|
return form(sh, dbf, indx);
|
|
}
|
|
|
|
/** 0695 간이공사비 산출명세서 — 원가 항목 · 금액 · 한글 금액 */
|
|
function simpleSheet(dbf, stage) {
|
|
const sh = C.newSheet("간이공사비산출명세서", "간이공사비 산출명세서", C.projectName(dbf), "E");
|
|
C.push(sh, { A: "항 목", B: "금 액", C: "산 출 근 거", _k: "head" });
|
|
const won = B.wonSheet(dbf, stage, null);
|
|
for (const r of won.rows.slice(4)) {
|
|
if (r.D == null) continue;
|
|
const lbl = [r.A, r.B, r.C].filter((x) => x && String(x).trim()).pop();
|
|
C.push(sh, { A: String(lbl || "").trim(), B: r.D, C: r.F || "", _k: "cost" });
|
|
}
|
|
C.push(sh, { A: "총공사비(한글)", B: In.amtToHan(stage.kanjub.grandTotal), _k: "final" });
|
|
return form(sh, dbf, "0695");
|
|
}
|
|
|
|
// 제목만 바뀌는 변형 — 같은 시트 모형을 복사해 SET_REP 제목·이름(0606 도공 · 0654 굴착 · 0656 시도 · 0613 견적 · 0652·0653·0672 총괄)
|
|
function retitle(sh, dbf, indx, id) {
|
|
const copy = {
|
|
name: sh.name,
|
|
title: sh.title,
|
|
rows: sh.rows.map((r) => Object.assign({}, r)),
|
|
cols: {},
|
|
};
|
|
const rep = ((dbf && dbf.set_rep_rows) || []).find((x) => String(x.INDX).trim() === indx);
|
|
const title = rep ? str(rep.HEADTITLE).trim() : sh.title;
|
|
copy.title = title;
|
|
if (copy.rows[0] && copy.rows[0]._k === "title") copy.rows[0].A = title;
|
|
copy.name = `${title.replace(/[\s,/().]+/g, "")}${indx}`;
|
|
copy.id = id;
|
|
copy.indx = indx;
|
|
return copy;
|
|
}
|
|
|
|
/**
|
|
* 변형 묶음 — 보고서 화면 서식 목록에서 고를 때 만듦(묶음 기본 차례 밖)
|
|
* 돌려줌: { id: 시트 모형 }
|
|
*/
|
|
function buildFormSheets(u, dbf, state, computed, n = 1, opts = {}) {
|
|
const stage = B.stageOf(state, computed, n);
|
|
const design = B.designSheet(dbf, stage, opts);
|
|
const tree = design._tree;
|
|
const summary = B.summarySheet(dbf, stage, design, opts);
|
|
const out = {
|
|
일위대가표수공양식: iLwiHandSheet(u),
|
|
분할내역서: splitSheet(dbf, tree, opts),
|
|
총괄집계표: totalSheet(dbf, stage, tree, opts),
|
|
견적내역서: retitle(design, dbf, "0613", "견적내역서"),
|
|
견적내역서종양식: retitle(design, dbf, "0614", "견적내역서종양식"),
|
|
견적단가대비표: estimateCompareSheet(u),
|
|
내역서도공양식: retitle(design, dbf, "0606", "내역서도공양식"),
|
|
설계내역서굴착: retitle(design, dbf, "0654", "설계내역서굴착"),
|
|
설계내역서시도: retitle(design, dbf, "0656", "설계내역서시도"),
|
|
총괄내역서시도: retitle(summary, dbf, "0652", "총괄내역서시도"),
|
|
총괄내역서구도: retitle(summary, dbf, "0653", "총괄내역서구도"),
|
|
총괄내역서비교: retitle(summary, dbf, "0672", "총괄내역서비교"),
|
|
총괄내역서구시도: fundSheet(dbf, tree, "0651", opts),
|
|
설계내역서구시도: fundSheet(dbf, tree, "0655", opts),
|
|
재료비집계표: groupCollSheet(u, tree, "0675", "M", opts),
|
|
노무비집계표: groupCollSheet(u, tree, "0676", "L", opts),
|
|
중장비집계표: groupCollSheet(u, tree, "0677", "X", opts),
|
|
장비투입계획: groupCollSheet(u, tree, "0671", "X", opts),
|
|
자재사용계획: groupCollSheet(u, tree, "0678", "M", opts),
|
|
인력투입계획: groupCollSheet(u, tree, "0679", "L", opts),
|
|
간이명세서: simpleSheet(dbf, stage),
|
|
제잡비요율표: In.rateSheet(dbf, opts),
|
|
};
|
|
for (const [id, sh] of Object.entries(out)) Object.assign(sh, { id });
|
|
delete design._tree;
|
|
return out;
|
|
}
|
|
|
|
// 서식 번호 → 변형 시트 id
|
|
const FORM_SHEET = {
|
|
"0303": "일위대가표수공양식",
|
|
"0605": "분할내역서",
|
|
"0609": "총괄집계표",
|
|
"0613": "견적내역서",
|
|
"0614": "견적내역서종양식",
|
|
"0615": "견적단가대비표",
|
|
"0606": "내역서도공양식",
|
|
"0654": "설계내역서굴착",
|
|
"0656": "설계내역서시도",
|
|
"0652": "총괄내역서시도",
|
|
"0653": "총괄내역서구도",
|
|
"0672": "총괄내역서비교",
|
|
"0651": "총괄내역서구시도",
|
|
"0655": "설계내역서구시도",
|
|
"0675": "재료비집계표",
|
|
"0676": "노무비집계표",
|
|
"0677": "중장비집계표",
|
|
"0671": "장비투입계획",
|
|
"0678": "자재사용계획",
|
|
"0679": "인력투입계획",
|
|
"0695": "간이명세서",
|
|
};
|
|
|
|
return { buildFormSheets, FORM_SHEET, fields, splitSheet, totalSheet, groupCollSheet };
|
|
})();
|
|
|
|
if (typeof window !== "undefined") window.STMATE_REPORT_FORMS = StmateReportForms;
|
|
if (typeof module !== "undefined" && module.exports) module.exports = StmateReportForms;
|