363 lines
14 KiB
JavaScript
363 lines
14 KiB
JavaScript
/* -*- 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;
|