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

363 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* -*- coding: utf-8 -*- */
/**
* 원가계산서(제잡비) 부속 (kanjub_parts.js) — 항목 이름·간접비 순서 코드표·식 템플릿 평가·설정 읽개·결과 묶음
* 근거: 지식DB STmate/old/20_분석 12 §3~7 · 20 §2.1~2.2(KAN_ORD) · 25 §2(식 템플릿 26) · REA 18·20
* 부르는 곳: kanjub_calc.js(계산) · scr_kanjub.js(화면)
*/
const StmateKanjubParts = ({ num, fmt, roundDown, roundUp, roundBanker }) => {
// 항목코드(옛 화면 코드) → 원본 저장키 몸통(C_·R_·D_·S_ 뒤)
const KEY = { KITA: "T_KITA", EQIP: "EQIPSU" };
const keyOf = (code) => KEY[code] || code;
const codeOf = (key) => Object.keys(KEY).find((c) => KEY[c] === key) || key;
// 항목 이름 — 화면·출력 표기(봉화 2024 공사원가계산서 · 12번 §3~6)
const LABEL = {
DOSEOH: "노무비 가산",
KANNO: "간접노무비",
SANBO: "산재보험료",
KOBO: "고용보험료",
KUNBO: "건강보험료",
SLVBO: "노인장기요양보험료",
YUNBO: "연금보험료",
TBUGUM: "퇴직공제부금비",
ANJUN: "산업안전보건관리비",
ANJUN_B: "안전관리비(별도내역)",
PUMJIL_B: "품질관리비(별도내역)",
S_ANJUN_B: "안전관리비(수공 별도)",
S_PUMJIL: "품질관리비(수공)",
KITA: "기타경비",
S_SUDO: "수도광열비",
S_BOKRI: "복리후생비",
S_SOMO: "소모품·사무용품비",
S_TONGSIN: "여비·교통비·통신비",
S_SEGUM: "세금·공과금",
S_PEGI: "폐기물처리비",
S_DOSEO: "도서인쇄비",
S_JIGUP: "지급수수료",
KONGSU: "공사이행보증수수료",
HADOSU: "하도급대금지급보증수수료",
HWAN: "환경보전비",
EQIP: "건설기계대여금지급보증 금액",
BUDM_SM: "석면피해구제분담금",
BUDM_IC: "임금채권부담금",
ILBAN: "일반관리비",
ANJUNP: "안전점검비",
PSAMT_B: "미확정설계공종(PS)",
EXCEPT_B: "제비율 적용제외공종",
EIS_SUPPAY: "고용개선지원비",
EIS_SOCINS: "고용개선 사회보험",
EIS_INCENT: "고용개선 장려금",
S_SIGONG: "시공도작성비",
KONGSA: "공사손해보험료",
BUSAN_B: "작업설 등 감액(△)",
PURCHAS: "매입세",
T_PEGI: "폐기물처리수수료",
T_KOLJAE: "골재사용료",
NBUGAM: "비과세자재대",
SAGUP: "사급자재대",
};
// 간접비 순서 코드표(Start8x.Ini KAN_ORD_1 일반 · KAN_ORD_2 수공 — 20번 §2.1~2.2) · 코드 → 항목
const ORD_CODE = {
301: "KANNO",
302: "SANBO",
303: "KOBO",
310: "KUNBO",
304: "SLVBO",
305: "YUNBO",
306: "TBUGUM",
312: "ANJUN",
313: "ANJUN_B",
321: "KITA",
307: "KONGSU",
308: "HADOSU",
309: "HWAN",
311: "EQIP",
331: "S_SUDO",
332: "S_BOKRI",
333: "S_SOMO",
334: "S_TONGSIN",
335: "S_SEGUM",
336: "S_PEGI",
337: "S_DOSEO",
338: "S_JIGUP",
351: "S_PUMJIL",
};
const KAN_ORD_1 = "301;302;303;310;304;305;306;312;313;321;307;308;309;311";
const KAN_ORD_2 =
"351;301;302;303;310;304;305;306;312;313;331;332;333;334;335;336;337;338;307;308;309;311";
const S8 = "S_SUDO S_BOKRI S_SOMO S_TONGSIN S_SEGUM S_PEGI S_DOSEO S_JIGUP".split(" ");
// 옛 화면 칸 → 원본 키 기본값(새 공사 · 옛 설정 호환)
const VAT_IDX = { supply_10: 0, mat_10: 1, zero: 2, forest: 3, forest_exempt: 4 };
const UPDN_IDX = { up: 0, half: 1, down: 2 };
const DEF = { I_ANJUN_K: 1, I_ANJUNAUTO: 1, I_TOT_JEOL: 3, I_KWN_JEOLSA: 3, I_KWN_K_JEOLSA: 3 };
// 간접비 식 템플릿 평가(25번 §2) — [변수] 치환 · ROUNDDOWN/ROUND/ROUNDUP/IF · 사칙
function evalTpl(tpl, vars) {
let s = String(tpl).replace(/\[([A-Z0-9_+]+)\]/g, (_, v) =>
v[0] === "+" ? `+(${num(vars[v.slice(1)])})` : `(${num(vars[v])})`,
);
s = s
.replace(/ROUNDDOWN\(/g, "RD(")
.replace(/ROUNDUP\(/g, "RU(")
.replace(/ROUND\(/g, "RN(")
.replace(/\bIF\(/g, "IFF(");
if (/[^0-9.+\-*/(),<>=!&| RDUNIF]/.test(s)) return NaN;
const fn = Function("RD", "RU", "RN", "IFF", `"use strict"; return (${s});`);
return fn(
(v, d = 0) => roundDown(v, d),
(v, d = 0) => roundUp(v, d),
(v, d = 0) => roundBanker(v, d),
(c, a, b) => (c ? a : b),
);
}
// 설정 → 원본 키 읽개(옛 칸 우선) · 차수 접두 없는 키
function reader(cfg, data) {
const keys = cfg.keys || {},
rates = cfg.rates || {},
fixed = cfg.fixed || {};
const raw = (k) => (keys[k] != null && keys[k] !== "" ? num(keys[k]) : DEF[k] || 0);
const ov = {};
if (cfg.vatMode != null) ov.I_BUGA = VAT_IDX[cfg.vatMode] ?? 2;
if (cfg.vatRoundUp != null) ov.B_BUGA_UP = cfg.vatRoundUp ? 1 : 0;
if (cfg.vatExempt != null) ov.C_BUGA_M = num(cfg.vatExempt);
if (cfg.totJeolDigits != null) ov.I_TOT_JEOL = Math.max(0, -num(cfg.totJeolDigits));
if (cfg.enableProfitEndAdj === false) ov.I_TOT_JEOL = 0;
if (cfg.anjunAuto != null) ov.I_ANJUNAUTO = num(cfg.anjunAuto);
if (cfg.iyunManualAdj != null) ov.C_IYUN_M = num(cfg.iyunManualAdj);
if (cfg.indirectTruncDigits) ov.I_KAN_JEOLSA = Math.max(0, -num(cfg.indirectTruncDigits));
if (cfg.govRoundMode != null) ov.I_KWN_UPDN = UPDN_IDX[cfg.govRoundMode] ?? 0;
if (cfg.govRoundDigits != null) ov.I_KWN_JEOLSA = Math.max(0, -num(cfg.govRoundDigits));
if (keys.C_ANJUN_G == null && cfg.anjunBaseAdd != null)
ov.C_ANJUN_G = ov.C_ANJUN1_G = ov.C_ANJUN2_G = num(cfg.anjunBaseAdd);
if (keys.I_KN_SA_TYPE == null && cfg.anjunGovVatExcluded === false) ov.I_KN_SA_TYPE = 1;
if (keys.C_KWANGUP_D == null && cfg.govMat != null)
ov.C_KWANGUP_D = num(cfg.govMat) - num(keys.C_KWANGUP_K);
if (keys.C_T_JODAL == null && cfg.govFee != null) ov.C_T_JODAL = num(cfg.govFee);
// 형식(옛 scheme) — S·SA = 수자원·지역난방(3·4) · TA·SA = 실적(CAL_ACTL)
let ctx = raw("JAB_TYPE"),
actl = raw("CAL_ACTL") ? 1 : 0;
if (cfg.scheme) {
const su = cfg.scheme === "S" || cfg.scheme === "SA";
if (su && ctx !== 3 && ctx !== 4) ctx = 3;
if (!su && (ctx === 3 || ctx === 4)) ctx = 0;
actl = cfg.scheme === "TA" || cfg.scheme === "SA" ? 1 : 0;
}
const k = (name) => (name in ov ? ov[name] : raw(name));
const R = (code) =>
rates[code] != null
? num(rates[code])
: keys["R_" + keyOf(code)] != null
? num(keys["R_" + keyOf(code)])
: code === "ANJUN1" || code === "ANJUN2"
? num(rates.ANJUN ?? keys.R_ANJUN)
: 0;
const G = (name) => (keys[name] == null && name !== "C_ANJUN_G" ? k("C_ANJUN_G") : k(name));
// D_ 표시 = 저장 금액(C_) 고정 · fixed 칸이 있으면 그 표가 정본(변경 차수 시작 때 다시 짬)
const fix = (code) =>
cfg.fixed
? fixed[code] != null
? num(fixed[code])
: null
: num(keys["D_" + keyOf(code)]) === 1
? num(keys["C_" + keyOf(code)])
: null;
const pclass = data && data.projectMeta ? num(data.projectMeta.pclass, -1) : -1;
return { k, R, G, fix, ctx, actl, pclass, revModes: cfg.revModes || {}, keys };
}
// 추가금액 — 묶음 1~4(일반관리비 앞 · 이윤 뒤 · 도급 앞 · 총공사비 앞) · 차수 칸(ADD_AMT_g_ii_n) · 25번 §1.3 · REA 18
function addList(cfg, slot) {
if (Array.isArray(cfg.addRows))
return cfg.addRows.map((r) => ({
stage: num(r.group || r.stage),
name: r.name || "",
amount: num((r.amts || [])[slot - 1]),
vat: !!r.vat,
addt: num(r.addt),
}));
return (cfg.addAmounts || []).map((a) => Object.assign({ addt: 0 }, a));
}
// 결과 묶음 — 옛 출력 칸(items·lines·대문자 합계) 유지 · KAN_ORD 순서 · 저장키(keysOut)
function finish(o, cfg, r, prev) {
const sch = r.actl
? r.ctx === 3 || r.ctx === 4
? "SA"
: "TA"
: r.ctx === 3 || r.ctx === 4
? "S"
: "T";
const ordStr =
(cfg.kanOrd && cfg.kanOrd[sch[0] === "S" ? 2 : 1]) ||
(sch[0] === "S" ? KAN_ORD_2 : KAN_ORD_1);
const ord = String(ordStr)
.split(/[;,\s]+/)
.map((c) => ORD_CODE[c])
.filter(Boolean);
const pre = ["KANNO", ...ord.filter((c) => c !== "KANNO")];
for (const c of ["PUMJIL_B", "S_ANJUN_B", "BUDM_SM", "BUDM_IC"])
if (!pre.includes(c)) pre.push(c);
if (r.rows.DOSEOH) pre.unshift("DOSEOH");
const always = new Set([
"KANNO",
"SANBO",
"KOBO",
"KUNBO",
"SLVBO",
"YUNBO",
"TBUGUM",
"ANJUN",
"HWAN",
"EQIP",
]);
if (r.ctx !== 3 && r.ctx !== 4) always.add("KITA");
else S8.forEach((c) => always.add(c));
const items = pre
.filter((c) => r.rows[c] && (always.has(c) || r.rows[c].amount !== 0))
.map((c) => r.rows[c]);
const itemMap = Object.assign({}, r.rows);
const kanno = r.kanno,
indirectExpenseSum = items
.filter((x) => x.code !== "KANNO")
.reduce((s, x) => s + x.amount, 0);
const totalLabor = r.L + kanno + (r.ctx === 5 ? r.hol : 0),
totalExpense = r.S + indirectExpenseSum + r.add1;
const pCost = r.total30,
pretaxProfitAdj = r.mAdj;
const iyunDesc = r.actl ? "직접공사비+간접공사비+일반관리비" : "노무비+경비+일반관리비";
const mk = (key, label, formula, baseAmt, rate, amount) =>
Object.assign({ key, label, formula, baseAmt, base: baseAmt, rate, amount });
const post = (codes) =>
codes
.filter((c) => r.rows[c] && r.rows[c].amount !== 0)
.map((c) => {
const x = r.rows[c];
return mk(
c,
x.label,
x.formula,
x.baseAmt,
x.rate || null,
c === "BUSAN_B" ? -x.amount : x.amount,
);
});
const addLines = (g) =>
r.adds
.filter((a) => a.stage === g && num(a.amount) !== 0)
.map((a, i) =>
mk(`ADD_${g}_${i + 1}`, a.name || `추가금액 ${g}`, "추가금액", null, null, num(a.amount)),
);
const govStr = `원자재대: ${fmt(r.gD)} (조달수수료: ${fmt(r.jodal)} 포함)`;
const lines = [
mk("DIRECT_M", "직접재료비", "내역서 재료비 합계", r.M, null, r.M),
mk("DIRECT_L", "직접노무비", "내역서 직접노무비 합계", r.L, null, r.L),
mk("DIRECT_S", "산출경비", "내역서 산출경비 합계", r.S, null, r.S),
mk("DIRECT_COST", "직접공사비 계", "직재 + 직노 + 산출경비", r.T, null, r.T),
...items.map((it) => mk(it.key, it.label, it.formula, it.baseAmt, it.rate, it.amount)),
...addLines(1),
mk("TOTAL_COST", "순공사원가 계", "재료비 + 노무비 + 경비", pCost, null, pCost),
mk("ILBAN", "일반관리비", "순공사원가", r.ilbanBase, r.rows.ILBAN.rate, r.ilban),
mk(
"IYUN",
"이윤",
`${iyunDesc} (보정 -${fmt(pretaxProfitAdj)})`,
r.A - (r.actl ? 0 : r.M),
r.rIyun,
r.iyunFinal,
),
...post([
"ANJUNP",
"PSAMT_B",
"EXCEPT_B",
"EIS_SUPPAY",
"EIS_SOCINS",
"EIS_INCENT",
"S_SIGONG",
]),
...addLines(2),
mk(
"SUPPLY_VAL",
"총원가 (공급가액)",
"순공사원가 + 일반관리비 + 이윤",
r.supply308,
null,
r.supply308,
),
...post(["KONGSA", "BUSAN_B"]),
mk("VAT", "부가가치세", "공급가액 × 10%", r.supplyFinal, 10, r.vat),
...post(["PURCHAS", "T_PEGI", "T_KOLJAE", "NBUGAM"]),
...addLines(3),
mk(
"FINAL_SUPPLY",
"도급공사비",
"공급가액 + 부가가치세",
r.contractRaw,
null,
r.contractFinal,
),
mk("GOV_MAT", "관급자재대", govStr, r.gD, null, r.govFinal),
...(r.govK ? [mk("GOV_K", "관급자재대(관급자설치)", "", r.gK, null, r.govK)] : []),
...(r.sagAfter ? [mk("SAGUP", "사급자재대", "", r.sagAfter, null, r.sagAfter)] : []),
...addLines(4),
mk("GRAND_TOTAL", "총공사비", "도급 + 관급", r.grand, null, r.grand),
];
const keysOut = Object.assign({}, r.keysOut, { I_ANJUN_B: r.anjunCase.cap ? 1 : 0 });
if (r.bugaUp != null) keysOut.B_BUGA_UP = r.bugaUp;
const out = { scheme: sch, dM: r.M, dL: r.L, dS: r.S, dT: r.T, kanno, totalLabor };
Object.assign(out, { indirectExpenseSum, totalExpense, pureConstructionCost: pCost });
Object.assign(out, { ilbanBase: r.ilbanBase, ilbanDesc: "순공사원가", ilbanAmt: r.ilban });
Object.assign(out, { iyunBase: r.A - (r.actl ? 0 : r.M), iyunDesc, iyunRaw: r.P });
Object.assign(out, {
pretaxProfitAdj,
iyunFinal: r.iyunFinal,
supplyBeforeAdj: r.supply308 + r.mAdj,
});
Object.assign(out, {
vatBeforeAdj: r.vat,
contractBeforeAdj: r.contractRaw,
diffTotal: r.diffTotal || 0,
});
Object.assign(out, {
supplyFinal: r.supply308,
vatFinal: r.vat,
contractFinal: r.contractFinal,
});
Object.assign(out, { govRawTotal: r.gD - r.jodal, govFeeTotal: r.jodal, adds: r.adds });
Object.assign(out, {
anjunBaseRaw: r.anjBasis,
anjun: r.anjunCase,
knSa: r.knSa,
kongsaJ: r.kongsaJ,
});
Object.assign(out, {
govFinal: r.govFinal,
govK: r.govK,
grandTotal: r.grand,
items,
itemMap,
lines,
});
Object.assign(out, { keysOut, hollab: r.hol, target: r.target, ctx: r.ctx, actl: r.actl });
Object.assign(out, { DIRECT_COST: r.T, KANNO: kanno, IND_EXP: kanno + indirectExpenseSum });
Object.assign(out, { TOTAL_COST: pCost, ILBAN: r.ilban, IYUN_INIT: r.P });
Object.assign(out, {
PROFIT_ADJ: -pretaxProfitAdj,
IYUN: r.iyunFinal,
SUPPLY_VAL: r.supply308,
});
return Object.assign(out, {
VAT: r.vat,
FINAL_SUPPLY: r.contractFinal,
GOV_MAT: r.govFinal,
GRAND_TOTAL: r.grand,
});
}
const KAN_ORD = { 1: KAN_ORD_1, 2: KAN_ORD_2 };
const out = { KEY, keyOf, codeOf, LABEL, ORD_CODE, KAN_ORD, S8, VAT_IDX, UPDN_IDX, DEF };
return Object.assign(out, { evalTpl, reader, addList, finish });
};
if (typeof window !== "undefined") window.STMATE_KANJUB_PARTS = StmateKanjubParts;
if (typeof module !== "undefined" && module.exports) module.exports = StmateKanjubParts;