330 lines
13 KiB
JavaScript
330 lines
13 KiB
JavaScript
/* -*- coding: utf-8 -*- */
|
|
/**
|
|
* 단가 보고서 (report_unit.js) — 목록표(0101~0103 · 0202 · 0301 · 0401) · 일위대가표(0302) · 중기사용료(0203) · 환율및기초자료(0201)
|
|
* 근거: 지식DB STmate/old/20_분석 20 §2.4(코드 종류) · §3(SET_REP 서식) · 17(단가조립) · old/30_원자료/print_form_map.txt
|
|
* 정답 대조: resources/tester/test_stmate_report_sheets.py (원본 STMATE 출력 엑셀 셀 값)
|
|
*/
|
|
const StmateReportUnit = (() => {
|
|
const C =
|
|
(typeof globalThis !== "undefined" && globalThis.STMATE_REPORT_CORE) ||
|
|
require("./report_core.js");
|
|
const { num, str, code, fix } = C;
|
|
|
|
// 비목 열 — 목록표 E~H · 일위대가표 (E,F)~(K,L) 쌍
|
|
const LIST_COLS = ["E", "F", "G", "H"];
|
|
const PAIR_COLS = [
|
|
["E", "F"],
|
|
["G", "H"],
|
|
["I", "J"],
|
|
["K", "L"],
|
|
];
|
|
|
|
function iniOf(dbf) {
|
|
return (dbf && dbf.start_ini) || {};
|
|
}
|
|
const slotOf = (r) => (num(r && r.NAGCD) > 0 ? 2 : 1);
|
|
|
|
// 구성 조립 설정 — Start8x.Ini(CHBNM · OPTNM · CONLD · RXXNM · EXLNM1)
|
|
function compOptions(dbf, parent) {
|
|
const ini = iniOf(dbf);
|
|
const p = code(parent);
|
|
return {
|
|
totalFirst: p[0] === "B" && String(ini.CHBNM ?? "1") === "0",
|
|
order: ini.OPTNM || "SML",
|
|
sbmok: str(parent.SBMOK).trim(),
|
|
labor: p[0] === "X" ? new Set(String(ini.CONLD || "").match(/.{1,6}/g) || []) : null,
|
|
rx: ini.RXXNM || "",
|
|
usdRate: num(ini.EXLNM1),
|
|
};
|
|
}
|
|
|
|
function groupsOf(dbf) {
|
|
const g = {};
|
|
for (const r of (dbf && dbf.bdqty_rows) || []) (g[r.BDCODE] = g[r.BDCODE] || []).push(r);
|
|
return g;
|
|
}
|
|
|
|
// 코드 단가 조회 — 낙찰률 품목은 원단가(2칸)로 조립(지식DB 17 · smt_engine 과 같음)
|
|
function lookupOf(idx) {
|
|
return (c) => {
|
|
const r = idx.by[c];
|
|
return r ? idx.price(c, slotOf(r)) : null;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 출력 설정 — Start8x.Ini
|
|
* SH_TYP1 = 0 이면 첫 칸 코드 · 그 밖 호표 번호 / SH_TYP3 = 0 이면 비고에 코드 · 1 이면 호표 · 그 밖 가격지 위치(BQCODE)
|
|
* CH_PRN_COD = 1 이면 번호를 자리 맞춤(중기 「제 n호표」 · 자원 「 n」)
|
|
* 줄 비고 종류 이름 = COD_FMT_1~6(노무·자재·경비·대가·중기·산근 · 지식DB 20 §2.4)
|
|
*/
|
|
function printOptions(ini) {
|
|
const kind = {};
|
|
"LMSBXD".split("").forEach((p, i) => (kind[p] = String(ini[`COD_FMT_${i + 1}`] || "")));
|
|
return {
|
|
codeMode: String(ini.SH_TYP1 ?? "0") === "0",
|
|
remarkHopyo: String(ini.SH_TYP3 ?? "2") === "1",
|
|
remarkCode: String(ini.SH_TYP3 ?? "2") === "0",
|
|
padNo: String(ini.CH_PRN_COD ?? "0") === "1",
|
|
kind,
|
|
};
|
|
}
|
|
const pad = (n, w) => String(n).padStart(w);
|
|
// 목록 비고 — 코드(SH_TYP3=0) · 호표 글(SH_TYP3=1 · 호표 모드) · 가격지 위치(BQCODE)
|
|
const remarkOf = (po, c, lbl, r) =>
|
|
po.remarkCode ? c : po.remarkHopyo && !po.codeMode ? lbl : str(r.BQCODE).trim();
|
|
// 호표 비고 글 — 대가·산근 「대가 n호표」 · 그 밖 「노무 n」
|
|
const kindNo = (po, p, n) =>
|
|
p === "B" || p === "D" ? `${po.kind[p]}${pad(n, 4)}호표` : `${po.kind[p] || ""}${pad(n, 5)}`;
|
|
// 목록 첫 칸 호표 글 — 일위대가·산근 「제 n호표」 · 중기·자원 번호
|
|
function hopyoLabel(po, prefix, n) {
|
|
if (prefix === "B" || prefix === "D" || (prefix === "X" && po.padNo))
|
|
return `제${pad(n, 4)}호표`;
|
|
return po.padNo ? pad(n, 4) : String(n);
|
|
}
|
|
|
|
// 목록표 — 코드 종류(M·L·S·B·D·X)의 COSTN 전수 · 코드순
|
|
function listSheet(ctx, name, title, prefix) {
|
|
const { dbf, idx, ini, order, po } = ctx;
|
|
const sh = C.newSheet(name, title, C.projectName(dbf), "J");
|
|
const head = { A: po.codeMode ? "코드번호" : "호표", B: "명 칭", C: "규 격" };
|
|
Object.assign(head, { D: "단위", I: "비 고", _k: "head" });
|
|
order.forEach((k, i) => (head[LIST_COLS[i]] = C.bimokLabel(ini, k)));
|
|
C.push(sh, head);
|
|
idx.ofPrefix(prefix).forEach((r, i) => {
|
|
const c = code(r);
|
|
const tot = ctx.totals[c] || idx.price(c, 1);
|
|
const lbl = po.codeMode ? c : hopyoLabel(po, prefix, 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 });
|
|
const v = { M: tot.M, L: tot.L, S: tot.S, T: tot.M + tot.L + tot.S };
|
|
order.forEach((k, j) => (cells[LIST_COLS[j]] = v[k]));
|
|
const rm = remarkOf(po, c, lbl, r);
|
|
if (rm) cells.I = rm;
|
|
C.push(sh, cells);
|
|
});
|
|
return sh;
|
|
}
|
|
|
|
// 자원 목록표(0101~0103) — 단가 한 칸
|
|
function resourceSheet(ctx, name, title, prefix) {
|
|
const { dbf, idx, po } = ctx;
|
|
const sh = C.newSheet(name, title, C.projectName(dbf), "G");
|
|
const head = { A: po.codeMode ? "코드번호" : "호표", B: "명 칭", C: "규 격" };
|
|
C.push(sh, Object.assign(head, { D: "단위", E: "단 가", F: "비 고", _k: "head" }));
|
|
idx.ofPrefix(prefix).forEach((r, i) => {
|
|
const c = code(r);
|
|
const p = idx.price(c, 1);
|
|
const lbl = po.codeMode ? c : hopyoLabel(po, prefix, i + 1);
|
|
const cells = { A: lbl, B: str(r.NAME), C: str(r.SIZE), D: C.unitText(r.UNIT) };
|
|
Object.assign(cells, { E: fix(p.M + p.L + p.S), _k: "item", _code: c });
|
|
const rm = remarkOf(po, c, lbl, r);
|
|
if (rm) cells.F = rm;
|
|
C.push(sh, cells);
|
|
});
|
|
return sh;
|
|
}
|
|
|
|
// 줄 비고 — 코드(뒤 한 칸) · 호표(종류 이름 + 번호 5자리) · 가격지 위치 · 단가 0 이면 「별산」(SET_REP 0302 FLD_N3)
|
|
function lineRemark(ctx, ref, c, pct) {
|
|
const { po, hopyoNo } = ctx;
|
|
const zero = !pct && ref && ref.M + ref.L + ref.S === 0 ? "별산" : "";
|
|
if (po.codeMode || po.remarkCode) return `${zero}${c} `;
|
|
if (po.remarkHopyo) return `${zero}${kindNo(po, c[0], hopyoNo[c] || "")} `;
|
|
return ref && ref.row ? str(ref.row.BQCODE).trim() : "";
|
|
}
|
|
|
|
/**
|
|
* 구성 호표 시트 — 일위대가표(B · 0302) · 중기사용료(X · 0203)
|
|
* 호표 머리 → 구성 줄(단가·금액 쌍) → 합계(합계 줄 원 미만 절사 · 경비 지정이면 경비 한 칸)
|
|
* 총액우선이면 합계 줄에 끝수 전 합(단가 칸)도 적음 · 글 줄(코드 없음)은 수량 0 빈 줄(명칭은 BDQTY 암호 칸)
|
|
*/
|
|
function compositionSheet(ctx, name, title, prefix) {
|
|
const { dbf, idx, ini, order, po, hopyoNo } = ctx;
|
|
const isB = prefix === "B";
|
|
const sh = C.newSheet(name, title, C.projectName(dbf), "N");
|
|
const h3 = {
|
|
A: "명 칭",
|
|
B: "규 격",
|
|
C: "수 량",
|
|
D: "단위",
|
|
M: "비 고",
|
|
_k: "head",
|
|
};
|
|
const h4 = { _k: "head" };
|
|
order.forEach((k, i) => {
|
|
h3[PAIR_COLS[i][0]] = C.bimokLabel(ini, k);
|
|
h4[PAIR_COLS[i][0]] = "단 가";
|
|
h4[PAIR_COLS[i][1]] = "금 액";
|
|
});
|
|
C.push(sh, h3);
|
|
C.push(sh, h4);
|
|
let no = 0;
|
|
for (const r of idx.ofPrefix(prefix)) {
|
|
const c = code(r);
|
|
no += 1;
|
|
const wide = isB || po.padNo;
|
|
C.push(sh, { A: wide ? ` 제${pad(no, 5)} 호표` : `제 ${no} 호표`, _k: "hopyo" });
|
|
const hd = { A: str(r.NAME), B: str(r.SIZE), D: C.unitText(r.UNIT), _k: "parent", _code: c };
|
|
const rm =
|
|
po.codeMode || po.remarkCode
|
|
? c
|
|
: po.remarkHopyo
|
|
? `${po.kind[prefix]}${pad(no, 5)}`
|
|
: str(r.BQCODE).trim();
|
|
if (rm) hd.M = rm;
|
|
C.push(sh, hd);
|
|
const ev = ctx.evals[c];
|
|
if (!ev) continue;
|
|
for (const ln of ev.lines) {
|
|
if (ln.text) {
|
|
C.push(sh, {
|
|
C: 0,
|
|
_k: "text",
|
|
M: (po.remarkHopyo || po.remarkCode) && !po.codeMode ? " " : undefined,
|
|
});
|
|
continue;
|
|
}
|
|
const ref = ln.ref;
|
|
const cells = { C: ln.qty, D: ln.unit, _k: "line", _code: ln.code };
|
|
if (ref) Object.assign(cells, { A: ref.name, B: ref.spec, D: ref.unitDisp });
|
|
if (ln.pct) cells.D = "%";
|
|
const u = { M: ln.u.M, L: ln.u.L, S: ln.u.S };
|
|
u.T = fix(u.M + u.L + u.S);
|
|
const a = { M: ln.a.M, L: ln.a.L, S: ln.a.S };
|
|
a.T = fix(a.M + a.L + a.S, 1);
|
|
order.forEach((k, i) => {
|
|
cells[PAIR_COLS[i][0]] = fix(u[k]);
|
|
cells[PAIR_COLS[i][1]] = fix(a[k], 1);
|
|
});
|
|
if (!ln.pct && !ln.labor && ln.unit !== "$")
|
|
cells._f = C.pairFormulas(order, PAIR_COLS, "C", 1, true);
|
|
if (ln.code !== "PR") {
|
|
const rm2 = lineRemark(ctx, ref, ln.code, ln.pct);
|
|
if (rm2) cells.M = rm2;
|
|
}
|
|
C.push(sh, cells);
|
|
}
|
|
const tot = { A: ev.sbmokS ? "합 계(경비 로적용)" : "합 계", _k: "total", _code: c };
|
|
const raw = Object.assign({}, ev.raw, { T: fix(ev.raw.M + ev.raw.L + ev.raw.S, 4) });
|
|
const fin = Object.assign({}, ev.tot, { T: ev.T });
|
|
order.forEach((k, i) => {
|
|
if (ev.totalFirst) tot[PAIR_COLS[i][0]] = fix(raw[k], 4);
|
|
tot[PAIR_COLS[i][1]] = fin[k];
|
|
});
|
|
C.push(sh, tot);
|
|
}
|
|
return sh;
|
|
}
|
|
|
|
// 환율및기초자료(0201) — 환율 · 노임 시간 환산(CONLD · RXXNM) · 중기 구성에 쓰인 재료
|
|
function basicSheet(ctx) {
|
|
const { dbf, idx, ini, groups, po, hopyoNo } = ctx;
|
|
const rmk = (c) => (po.remarkHopyo && !po.codeMode ? kindNo(po, c[0], hopyoNo[c]) : c);
|
|
const sh = { name: "환율및기초자료", title: "1. 환 율", rows: [], cols: {} };
|
|
C.put(sh, 1, { A: "1. 환 율", _k: "title" });
|
|
C.put(sh, 2, { C: "100,000$ 미만", D: "100,000$ 이상", E: "유로화(€)", G: "엔화(100¥)" });
|
|
sh.rows[1].I = "환율및기초자료";
|
|
const e3 = { C: num(ini.EXLNM1), D: num(ini.EXHNM1), E: num(ini.EXEUR1), G: num(ini.EXYEN1) };
|
|
C.put(sh, 3, e3);
|
|
C.put(sh, 5, { A: "2. 인 건 비" });
|
|
const labor = String(ini.CONLD || "").match(/.{1,6}/g) || [];
|
|
const rx = String(ini.RXXNM || "");
|
|
let r = 6;
|
|
const lab = labor.filter((c) => idx.by[c]);
|
|
for (const c of lab) {
|
|
const p = idx.price(c, 1);
|
|
const daily = p.M + p.L + p.S;
|
|
let acc = daily;
|
|
for (const [, op, n] of rx.replace(/\s+/g, "").matchAll(/([*/])?(\d+(?:\.\d+)?)/g))
|
|
acc = op === "/" ? acc / Number(n) : acc * Number(n);
|
|
C.put(sh, r++, {
|
|
C: p.name,
|
|
D: daily,
|
|
E: `* ${rx} =`,
|
|
G: C.trunc(acc),
|
|
H: rmk(c),
|
|
_code: c,
|
|
});
|
|
}
|
|
r += 1;
|
|
C.put(sh, r++, { A: "3. 단가 및 재료비" });
|
|
C.put(sh, r++, { B: "NO", C: "단 가 명", D: "규 격", E: "단위", F: "가 격", H: "비 고" });
|
|
// 재료 = 중기(X) 구성에 쓰인 재료비(M) 코드 · 코드순
|
|
const mats = new Set();
|
|
for (const [p, rows] of Object.entries(groups))
|
|
if (p[0] === "X")
|
|
for (const g of rows) {
|
|
const c = String(g.CODE).trim();
|
|
if (/^M/.test(c) && idx.by[c] && !/%$/.test(str(idx.by[c].UNIT).trim())) mats.add(c);
|
|
}
|
|
const items = lab.concat([...mats].filter((c) => idx.by[c]).sort());
|
|
items.forEach((c, i) => {
|
|
const p = idx.price(c, 1);
|
|
const cells = {
|
|
B: String(i + 1),
|
|
C: p.name,
|
|
E: p.unitDisp,
|
|
F: p.M + p.L + p.S,
|
|
H: rmk(c),
|
|
_code: c,
|
|
};
|
|
if (p.spec.trim()) cells.D = p.spec;
|
|
C.put(sh, r++, cells);
|
|
});
|
|
return sh;
|
|
}
|
|
|
|
/**
|
|
* 단가 맥락 — 구성 호표(B·X) 줄별 계산 결과와 목록표 합계
|
|
* 구성 없는 호표 = COSTN 단가 그대로
|
|
*/
|
|
function unitContext(dbf) {
|
|
const idx = C.costnIndex(dbf);
|
|
const ini = iniOf(dbf);
|
|
const order = C.bimokOrder(ini);
|
|
const groups = groupsOf(dbf);
|
|
const lookup = lookupOf(idx);
|
|
const evals = {},
|
|
totals = {};
|
|
for (const p of ["X", "B"])
|
|
for (const r of idx.ofPrefix(p)) {
|
|
const c = code(r);
|
|
if (!groups[c]) continue;
|
|
const ev = C.compositionLines(groups[c], lookup, compOptions(dbf, r));
|
|
evals[c] = ev;
|
|
totals[c] = ev.tot;
|
|
}
|
|
// 호표 번호 = 종류별 코드순 자리(1부터)
|
|
const hopyoNo = {};
|
|
for (const p of "LMSBXD") idx.ofPrefix(p).forEach((r, i) => (hopyoNo[code(r)] = i + 1));
|
|
const po = printOptions(ini);
|
|
return { dbf, idx, ini, order, groups, evals, totals, hopyoNo, po };
|
|
}
|
|
|
|
function buildUnitSheets(dbf) {
|
|
const ctx = unitContext(dbf);
|
|
const F = (sh, indx) => C.applyForm(sh, dbf, indx);
|
|
return {
|
|
ctx,
|
|
일위대가목록표: F(listSheet(ctx, "일위대가목록표", "일위대가 목록표", "B"), "0301"),
|
|
일위대가표: F(compositionSheet(ctx, "일위대가표", "일 위 대 가 표", "B"), "0302"),
|
|
단가산출근거목록표: F(
|
|
listSheet(ctx, "단가산출근거목록표", "단가산출근거 목록표", "D"),
|
|
"0401",
|
|
),
|
|
환율및기초자료: basicSheet(ctx),
|
|
중기목록표: F(listSheet(ctx, "중기목록표", "중기 목록표", "X"), "0202"),
|
|
중기사용료: F(compositionSheet(ctx, "중기사용료", "중 기 사 용 료", "X"), "0203"),
|
|
재료비목록표: F(resourceSheet(ctx, "재료비목록표", "재료비 목록표", "M"), "0101"),
|
|
노무비목록표: F(resourceSheet(ctx, "노무비목록표", "노무비 목록표", "L"), "0102"),
|
|
경비목록표: F(resourceSheet(ctx, "경비목록표", "경비 목록표", "S"), "0103"),
|
|
};
|
|
}
|
|
|
|
return { buildUnitSheets, unitContext, compOptions, remarkOf, LIST_COLS, PAIR_COLS };
|
|
})();
|
|
|
|
if (typeof window !== "undefined") window.STMATE_REPORT_UNIT = StmateReportUnit;
|
|
if (typeof module !== "undefined" && module.exports) module.exports = StmateReportUnit;
|