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

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;