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

346 lines
15 KiB
JavaScript

/* -*- coding: utf-8 -*- */
/**
* 보고서 묶음 (report_book.js) — 자재단가대비표(0105) · 목차 · 안내 · 시트 차례 · SET_REP 서식 표
* 근거: 지식DB STmate/old/20_분석 20 §2.3(가격지 슬롯 이름 JMOK · 축약 JMK) · §3(SET_REP 75종)
* 19 §3(목차·안내 시트) · old/30_원자료/print_form_map.txt(서식 번호 ↔ 출력 함수)
* 정답 대조: resources/tester/test_stmate_report_sheets.py
*/
const StmateReportBook = (() => {
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 Co = g.STMATE_REPORT_COLL || require("./report_coll.js");
const Sa = g.STMATE_REPORT_SAN || require("./report_san.js");
const In = g.STMATE_REPORT_INIT || require("./report_init.js");
const Lk = g.STMATE_REPORT_LINK || require("./report_link.js");
const { num, str, code } = C;
/**
* 자재단가대비표(0105) — 재료비(M) 전수 · 가격지 5칸(단가·페이지) · 적용 단가와 적용 가격지 축약
* 가격지 이름 = Start8x.Ini JMOK1~6 · 축약 = JMK10~50 · 적용 슬롯 = COSTN PG_1
*/
function priceCompareSheet(ctx) {
const { dbf, idx, ini, po } = ctx;
const sh = C.newSheet("자재단가대비표", "자재단가 대비표", C.projectName(dbf), "R");
const slots = ["E", "G", "I", "K", "M"];
const h3 = {
A: po.codeMode ? "코드번호" : "호표",
B: "명 칭",
C: "규 격",
D: "단위",
};
const h4 = { _k: "head" };
slots.forEach((c, i) => {
h3[c] = str(ini[`JMOK${i + 1}`] || "").trim() || `가격${i + 1}`;
h4[c] = "단 가";
h4[String.fromCharCode(c.charCodeAt(0) + 1)] = "페이지";
});
Object.assign(h3, { O: str(ini.JMOK6 || "적용 단가").trim(), Q: "비 고", _k: "head" });
Object.assign(h4, { O: "단 가", P: "페이지" });
C.push(sh, h3);
C.push(sh, h4);
idx.ofPrefix("M").forEach((r, i) => {
const c = code(r);
const lbl = po.codeMode ? c : po.padNo ? String(i + 1).padStart(4) : String(i + 1);
const cells = { A: lbl, B: str(r.NAME), C: str(r.SIZE), D: C.unitText(r.UNIT) };
Object.assign(cells, { _k: "item", _code: c });
slots.forEach((col, j) => {
cells[col] = num(r[`AMT_${j + 1}1`]);
const pg = str(r[`PG_${j + 1}1`]).trim();
if (pg) cells[String.fromCharCode(col.charCodeAt(0) + 1)] = pg;
});
const p = idx.price(c, 1);
cells.O = p.M + p.L + p.S;
const s = num(r.PG_1);
if (s >= 1 && s <= 5) cells.P = str(ini[`JMK${s}0`] || "").trim();
const rm = U.remarkOf(po, c, lbl, r);
if (rm) cells.Q = rm;
C.push(sh, cells);
});
return sh;
}
// 시트 차례 — 원본 STMATE 엑셀 내보내기 기본 순서(영월·울진 출력 목차) · 인쇄순서(F1-52)로 바꿈
const ORDER = [
"공사원가계산서",
"총괄내역서",
"설계내역서",
"일위대가목록표",
"일위대가표",
"일위대가수량금액집계표",
"단가산출근거목록표",
"단가산출근거",
"단가산출근거수량금액집계표",
"환율및기초자료",
"중기목록표",
"중기사용료",
"재료비목록표",
"노무비목록표",
"경비목록표",
"일식견적목록표",
"표준시장목록표",
"실적단가목록표",
"자재단가대비표",
"재료비수량금액집계표",
"노무비수량금액집계표",
"경비수량금액집계표",
"일식견적수량금액집계표",
"표준시장수량금액집계표",
"중기시간금액집계표",
];
const TOC = "〓 목 차 〓";
const GUIDE = "※※안내※※";
// 재등록 최소 판 — 24 판 출력은 「w24.02」 · 25 판부터 프로그램 판(원본 목차 D2 실측 · 울진 25.07 은 출력 프로그램 판)
function regVersion(ini) {
const v = str(ini.PRG_V || "").trim();
return num(v) >= 25 ? `w${v}` : "w24.02";
}
/**
* 목차 시트(〓 목 차 〓) — 원본 출력 그대로: D1 INITIAL 지움 검사 수식 · B2 제목 · D2~D3 재등록 안내 ·
* 4행부터 「n. 시트이름」 + 바로가기(→) · 안내 시트가 1번 · INITIAL 은 목록 밖
*/
function tocSheet(sheets, dbf) {
const ini = (dbf && dbf.start_ini) || {};
const sh = {
name: TOC,
title: "목 차",
rows: [],
cols: { B: 25.625, D: 23.625 },
tab: "0000FF",
};
const chk = `=IF('${Lk.INIT}'!C4,"※※ 기본정보의 일부가 삭제되어 Stmate로 변환할 수 없습니다 ※※","")`;
C.put(sh, 1, { _f: { D: chk }, _sz: { D: 12 }, _bold: { D: true } });
C.put(sh, 2, {
B: "목 차",
D: `※ 본 엑셀자료는 【STmate ${regVersion(ini)}】버전 이상에서 STmate 자료로 다시 등록할 수 있습니다.`,
_sz: { B: 20, D: 9 },
});
C.put(sh, 3, {
D: " 단, 임의로 수정한 내용(Sheet삭제, 산근내용변경, 기타계산식변경..)은 반영되지 않을 수 있습니다.",
_sz: { D: 9 },
});
sheets.forEach((s, i) => {
const cells = { B: `${i + 1}. ${s.name}`, D: `${s.name} →`, _sz: { B: 11, D: 9 } };
cells._links = { D: `${Lk.q(s.name)}!A1` };
const tag = Lk.tagOf(s);
cells._f = {
D: tag
? `=HYPERLINK("#"&${Lk.q(s.name)}!${tag}2&"!A1","${s.name} →")`
: `=HYPERLINK("#${Lk.q(s.name)}!A1","${s.name} →")`,
};
C.put(sh, 4 + i, cells);
});
return sh;
}
/**
* 안내 시트(※※안내※※) — 원본 출력 글 · 판·산근 수식 갈래 · 아래 알림(안내 및 오류) 줄
* 알림 차례 = 설계내역서 % 단위 → 총괄 이윤보정액(I) · 총공사비(D) → 일위대가표 → 단가산출근거 → 중기사용료
*/
function guideSheet(sheets, dbf, k) {
const ini = (dbf && dbf.start_ini) || {};
const sh = { name: GUIDE, title: "안내", rows: [], cols: { A: 9.125, B: 4.75 }, tab: "FF0000" };
const by = {};
for (const s of sheets) by[s.id || s.name] = s;
C.put(sh, 2, {
A: "목차 →",
B: "=== 엑셀파일에 작성에 대한 안내문(반드시 읽어보세요) ===",
_links: { A: `${Lk.q(TOC)}!B2` },
_f: { A: `=HYPERLINK("#${Lk.q(TOC)}!B2","목차 →")` },
_sz: { A: 9, B: 12 },
_bold: { B: true },
});
const line = (r, t, extra) =>
C.put(sh, r, Object.assign({ B: t, _sz: { B: 10 } }, extra || {}));
line(4, "엑셀파일 수정후 계산식 반영이 안될경우 :【 Ctrl+Alt(+Shift)+F9 】", {
_sz: { B: 14 },
_bold: { B: true },
});
[
"◈ 엑셀파일내의 산식은 기초단가, 복합단가, 내역서 및 총괄내역서까지 연결됩니다.",
" (모든산식은 단가만을 불러오며, 명칭, 규격, 단위, 비고.. 등은 TEXT로 입력됩니다)",
"◈ 각 Sheet우측에는 해당 항목을 찾아가는 위치이동(→) 링크가 있으며, 클릭하면 이동됩니다.",
" (Alt + ← 키를 이용하여 찾아갔던 항목을 되돌아 올 수 있습니다.)",
"◈ 자재단가 대비표의 적용단가는 수식이 적용되지 않았으나, 우측에 최소단가 수식을 입력하였습니다.",
"◈ 일위대가표 및 중기사용료 및 총괄내역서의 양식은 일반양식을 적용합니다.",
].forEach((t, i) => line(6 + i, t));
// 산근 수량 산식 갈래 — report_san 수치 글 다듬기와 같은 판단(FLT_JAB=1 · 원본 5건 실측 · 추정)
const sanFml = String(ini.FLT_JAB ?? "2").trim() === "1";
const san = by.단가산출근거;
const t12 = "◈ 단가산출근거의 수량부분은 산출근거 우측 연두색 부분에서 수정합니다. →";
if (sanFml && san && Lk.tagOf(san))
line(12, t12, {
_links: { B: `${Lk.q(san.name)}!Y3` },
_f: { B: `=HYPERLINK("#"&${Lk.q(san.name)}!${Lk.tagOf(san)}2&"!Y3","${t12}")` },
});
else line(12, "◈ 단가산출근거의 수량부분은 계산식이 입력되지 않았습니다.");
let r = 13;
const v25 = num(ini.PRG_V) >= 25;
if (v25 || sanFml)
line(
r++,
`◈ [목차] 및 [안내]${v25 ? " " : ""}Sheet의 이름을 변경하지 마시고, Sheet이름을 변경할때 공백문자 및 특수문자를 사용하지 마십시요.`,
);
r += 1;
line(r++, "※※※ 아래의 내용(안내 및 오류)을 확인/검토 하세요 ※※※", {
_bold: { B: true },
});
for (const w of guideNotes(by, dbf, k)) {
const ref = `${Lk.q(w.sh.name)}!${w.col}${w.row}`;
C.put(sh, r++, {
B: " → ",
C: w.text,
_links: { B: ref },
_f: { B: Lk.jump(w.sh, w.col, w.row, " → ") },
_sz: { B: 9, C: 9 },
_bold: w.bold ? { C: true } : undefined,
});
}
return sh;
}
/**
* 안내 알림 줄 — % 단위 코드 줄(산식 못 넣음 · 손료계산 형식 권고) · 총괄 이윤보정액 · 총공사비 확인
* 총공사비 확인 줄 = 이윤보정액이 천 원 끝전 맞춤 범위(1,000 미만)일 때(원본 5건 실측 · 대흥 905,980 = 없음 · 추정)
* 중기 % 줄 = 규격 「주연료의 %」(잡품·잡유)는 연료 줄 밑수 산식이 있어 알림 없음(대흥 실측)
*/
function guideNotes(by, dbf, k) {
const unit = {};
for (const r of (dbf && dbf.costn_rows) || []) unit[code(r)] = str(r.UNIT).trim();
const isP = (c) => /%$/.test(unit[c] || "");
const out = [];
const pctMsg = (s, row, c) =>
`【${s.name}】${row}번째줄 에서 사용된【${c}】코드의 %단위(${unit[c]}) 는 산식을 입력할 수 없으니 손료계산 형식으로 변경하십시요.`;
const scan = (s, kind, get, col) =>
s &&
s.rows.forEach((r, i) => {
const c = r._k === kind && get(r);
if (c && isP(c)) out.push({ sh: s, col: col(c), row: i + 1, text: pctMsg(s, i + 1, c) });
});
scan(
by.설계내역서,
"item",
(r) => r._code,
() => "A",
);
const sm = by.총괄내역서;
if (sm) {
// 변경 양식(당초·변경 두 줄)이면 둘 다 — 변경 줄 = 이름 칸 빈 다음 줄(영덕 실측)
const pair = (rn) => {
const nx = sm.rows[rn];
return nx && !nx.A && !nx.B && nx.D != null ? [rn, rn + 1] : [rn];
};
const iy = sm.rows.findIndex((r) => r._k === "cost" && /이\s*윤/.test(String(r.B || ""))) + 1;
const n = sm.rows.length;
const last = n > 1 && !sm.rows[n - 1].A && !sm.rows[n - 1].B ? [n - 1, n] : [n];
if (iy > 0)
for (const r of pair(iy))
out.push({
sh: sm,
col: "I",
row: r,
bold: true,
text: `【${sm.name}】${r}번째줄의 I열 이윤보정액은 총공사비 금액이 다를경우 임의조정(±α) 하십시요.`,
});
if (k && Math.abs(num(k.pretaxProfitAdj)) < 1000)
for (const r of last)
out.push({
sh: sm,
col: "D",
row: r,
text: `【${sm.name}】${r}번째줄의 D열에 계산된 총공사비 금액을 확인하십시요.`,
});
}
const own = { M: "I", L: "G", S: "K" };
scan(
by.일위대가표,
"line",
(r) => r._code,
(c) => own[c[0]] || "C",
);
scan(
by.단가산출근거,
"line",
(r) => r._ref,
() => "B",
);
const spec = Lk.specOf({ _dbf: dbf });
const jm = by.중기사용료;
if (jm)
jm.rows.forEach((r, i) => {
if (r._k === "line" && isP(r._code) && !/주연료/.test(spec[r._code] || ""))
out.push({
sh: jm,
col: own[r._code[0]] || "C",
row: i + 1,
text: pctMsg(jm, i + 1, r._code),
});
});
return out;
}
/**
* 전체 묶음 — dbf(불러온 자료) · state(sim_state) · computed(calculateAll) · n 차수
* 돌려줌: { sheets:[시트 모형…], byName }
*/
function buildBook(dbf, state, computed, n = 1, opts = {}) {
const unit = U.buildUnitSheets(dbf);
const boq = B.buildBoqSheets(dbf, state, computed, n, opts);
const pc = C.applyForm(priceCompareSheet(unit.ctx), dbf, "0105");
// 집계표(0501~0508) — 변경 차수면 당초·변경 두 줄 양식 · 단가산출근거(0402) 펼침
const base = n > 1 ? B.boqTree(dbf, B.stageOf(state, computed, 1).boq, 1) : null;
const coll = Co.buildCollSheets(
unit.ctx,
boq.설계내역서._tree,
Object.assign({ baseTree: base }, opts),
);
const san = C.applyForm(Sa.sanSheet(unit.ctx, opts), dbf, "0402");
const all = Object.assign({}, unit, boq, coll, { 자재단가대비표: pc, 단가산출근거: san });
const order = opts.order && opts.order.length ? opts.order : ORDER;
const sheets = order.filter((id) => all[id]).map((id) => Object.assign(all[id], { id }));
const init = In.initialSheet(dbf, state, sheets, n, opts);
Lk.linkBook(sheets, dbf); // 시트 간 참조 수식 · 바로가기(→) · 이름표 수식
const guide = guideSheet(sheets, dbf, boq.stage.kanjub);
const book = [tocSheet([guide].concat(sheets), dbf), guide].concat(sheets, [init]);
for (const s of book) delete s._tree;
return { sheets: book, byName: Object.fromEntries(book.map((s) => [s.name, s])) };
}
// SET_REP 서식 → 시트 이름(구현된 것) — 없으면 null
const FORM_SHEET = {
"0101": "재료비목록표",
"0102": "노무비목록표",
"0103": "경비목록표",
"0105": "자재단가대비표",
"0201": "환율및기초자료",
"0202": "중기목록표",
"0203": "중기사용료",
"0301": "일위대가목록표",
"0302": "일위대가표",
"0401": "단가산출근거목록표",
"0402": "단가산출근거",
"0104": "일식견적목록표",
"0106": "표준시장목록표",
"0206": "실적단가목록표",
"0501": "재료비수량금액집계표",
"0502": "노무비수량금액집계표",
"0503": "경비수량금액집계표",
"0504": "일식견적수량금액집계표",
"0505": "중기시간금액집계표",
"0506": "일위대가수량금액집계표",
"0507": "단가산출근거수량금액집계표",
"0508": "표준시장수량금액집계표",
"0601": "총괄내역서",
"0603": "설계내역서",
"0608": "공사원가계산서",
};
return { buildBook, priceCompareSheet, tocSheet, guideSheet, ORDER, FORM_SHEET };
})();
if (typeof window !== "undefined") window.STMATE_REPORT_BOOK = StmateReportBook;
if (typeof module !== "undefined" && module.exports) module.exports = StmateReportBook;