/* -*- coding: utf-8 -*- */ /** * 원가계산서(제잡비) 계산 (kanjub_calc.js) — STMATE Calc_Kanjub 네이티브 순서 * 직접비 → 간접노무비 → 보험 → 안전 → 경비 → (추가1) → 일반관리비 → 이윤 → 이윤 뒤 가산(추가2) * → 공사보험 → 작업설 감액 → 부가세 → 세후 가산(추가3) → 도급 절사 → 관급 → 사급 → (추가4) → 총공사비 * 근거: 지식DB STmate/old/20_분석 06 · 11 · 12 · 20(KAN_ORD) · 25 · 35 * REA 18(보험·안전·수공 경비·변경 차액) · REA 20(일반관리비·이윤 절사·공사보험·부가세·관급·추가금액 순서) * 설정: cfg.keys = PRJLST.SMT [n_KANJUB] 원본 키(차수 접두 뗌) · cfg.rates/fixed/scheme 등 옛 칸이 있으면 그 값이 앞섬 * 정답 대조: resources/tester/test_stmate_kanjub_stc.py · test_stmate_kanjub_opts.py */ const StmateKanjubFactory = (h) => { const { num, roundDown, roundUp, roundHalfUp, roundBanker } = h; // 설정 읽개·항목 표·결과 묶음 — kanjub_parts.js const P = (globalThis.STMATE_KANJUB_PARTS || require("./kanjub_parts.js"))(h); const { keyOf, LABEL, S8, reader, addList, finish } = P; function calcKanjub(data, boqRes, origKanjubRes = null, stageCfg = null) { const cfg = stageCfg || (data && data.kanjubConfig) || {}; const o = reader(cfg, data), slot = num(cfg.slot, 1), adds = addList(cfg, slot); const addSum = (g, vatOnly = false) => adds .filter((a) => a.stage === g && !(g === 1 && a.addt) && (!vatOnly || a.vat)) .reduce((s, a) => s + num(a.amount), 0); const B = { M: num(boqRes.directM), L: num(boqRes.directL), S: num(boqRes.directS), govAll: num(boqRes.govCombinedRaw), govFee: num(boqRes.govFeeTotal), }; const run1 = (m, up) => runOnce(o, cfg, B, origKanjubRes, adds, addSum, m, up); let r = run1(o.k("C_IYUN_M"), o.k("B_BUGA_UP") ? 1 : 0); // 총액 절사(I_TOT_JEOL) — 총공사비를 자리 절사한 목표에 맞도록 이윤보정액(C_IYUN_M)을 1원씩 조정 · 못 맞추면 부가세 +1(B_BUGA_UP) const tj = o.k("I_TOT_JEOL"); if (tj > 0 && !o.k("I_JEOLSA")) { let up = o.k("B_BUGA_UP") ? 1 : 0; const first = run1(0, up), target = roundDown(first.grand, -tj), diff = first.grand - target; const m0 = o.k("I_BUGA") === 0 ? roundHalfUp(diff / 1.1, 0) : diff; let m = m0, seen = new Set(); r = run1(m, up); for (let i = 0; i < 60 && r.grand !== target; i++) { seen.add(m); m += r.grand > target ? 1 : -1; if (seen.has(m)) { if (up) break; ((up = 1), (m = m0), (seen = new Set())); } r = run1(m, up); } Object.assign(r, { target, diffTotal: diff, bugaUp: up }); } return finish(o, cfg, r, origKanjubRes); } // 한 번 계산(이윤보정액 m · 부가세 +1 up 고정) function runOnce(o, cfg, B, prev, adds, addSum, iyunM, bugaUp) { const J = o.k("I_KAN_JEOLSA"), tr = (v) => roundDown(v, -J), ctx = o.ctx, actl = o.actl; const std = ctx === 0 || ctx === 1 || ctx === 2 || ctx === 5, su = ctx === 3 || ctx === 4; const rows = {}, keysOut = {}; // 항목 한 줄 — 후보(cand) · D_ 고정 · 옛 변경 계산방식(revModes) const put = (code, base, rate, cand, desc, tpl, group) => { const fx = o.fix(code); let amount = fx != null ? fx : cand; const mode = o.revModes[code]; const pr = prev && prev.itemMap && prev.itemMap[code]; if (fx == null && pr && mode && mode !== "recalculated") { const ob = num(pr.baseAmt); amount = mode === "carry_original" ? pr.amount : mode === "proportional" ? pr.amount + tr(pr.amount * (ob > 0 ? (base - ob) / ob : 0)) : mode === "delta_rate" ? pr.amount + tr(((base - ob) * rate) / 100) : amount; } const row = { key: code, code, label: LABEL[code] || code, group: group || "exp" }; Object.assign(row, { formula: desc, baseDesc: desc, baseAmt: base, base, rate, ratePct: rate, }); Object.assign(row, { cand, amount, fixed: fx != null, tpl: tpl || "" }); if (pr) Object.assign(row, { origAmt: pr.amount, deltaAmt: amount - pr.amount }); row.modeUsed = prev ? mode || "recalculated" : "원설계"; rows[code] = row; keysOut["C_" + keyOf(code)] = amount; keysOut["S_" + keyOf(code)] = cand; return amount; }; // 율 항목 — 후보 = ROUNDDOWN(밑수×율/100, −간접비 절사자리) · 식 템플릿과 그 변수(25번 §2.1)를 함께 남김 const pctItem = (code, base, desc, tplBase, vars) => { const r = o.R(code), amt = put( code, base, r, tr((base * r) / 100), desc, `ROUNDDOWN(${tplBase}*[RAT_1]/100,-[J])`, ); rows[code].tplVars = Object.assign({ RAT_1: r, J }, vars); return amt; }; // 0) 직접비 틀 — 노무비 가산(DOSEOH) · 사급 위치(I_POS_SA=3 재료비 포함) · 실적 대체(CAL_ACTL) let M = B.M, L = B.L, S = B.S; if (o.R("DOSEOH")) L += pctItem("DOSEOH", L, "직접노무비", "[LAMT]", { LAMT: L }); const posSa = o.k("I_POS_SA"), sagup = o.k("C_SAGUP"); let Kres = sagup; if (posSa === 3) ((M += sagup), (Kres = 0)); let T = M + L + S, c98 = null; if (actl) { const DM = o.R("R_DM"), DL = o.R("R_DL"); if (o.k("I_MTYP") === 1) S = T - L - M; else if (o.k("I_LTYP")) { M = roundDown(T * DM, 0); if (M + L > T) M = T - L; S = T - M - L; } else { M = roundDown(T * DM, 0); L = roundDown(T * DL, 0); S = (1 - DM - DL) * T; L = T * o.R("R_L"); c98 = T * o.R("R_DML"); } } const c90 = L + M, hol = o.k("C_EIS_HOLLAB"), U = std ? o.k("C_NBUGAM") : 0; let expAcc = actl ? 0 : S, Lp = L; // 1) 간접노무비 — 일반은 노무비, 실적은 간접공사비(I_LTYP≠0 이면 노무비 밑수에도) const kanno = pctItem("KANNO", L, "직접노무비", "[LAMT]", { LAMT: L }); if (!actl) Lp += kanno; else ((expAcc += kanno), o.k("I_LTYP") && (Lp += kanno)); if (su) expAcc += put("S_PUMJIL", 0, 0, o.k("C_S_PUMJIL"), "별도내역"); // 2) 산재·고용 = (노무비 + 고용개선 주휴노무비) × 율 expAcc += pctItem("SANBO", Lp + hol, "직접노무비+간접노무비", "([L_AMT][+LAB])", { L_AMT: Lp, LAB: hol, }); expAcc += pctItem("KOBO", Lp + hol, "직접노무비+간접노무비", "([L_AMT][+LAB])", { L_AMT: Lp, LAB: hol, }); // 3) 건강·연금·장기요양·퇴직공제 — 변경 차수 당초액 + 증감(I_BF_CH_BO) · 직접입력 유지(I_COST_BO) · REA 18 const lab = L + hol, bfch = o.k("I_BF_CH_BO"), costBo = o.k("I_COST_BO"); const chg = prev && (o.pclass < 0 || o.pclass === 7) && (bfch === 1 || bfch === 2); const prevBase = prev ? num(prev.dL) + num(prev.hollab) : 0; const prevAmt = (c) => (prev && prev.itemMap[c] ? num(prev.itemMap[c].amount) : 0); const hp = (code, sel) => { const base = o.k(sel) ? o.k("C_SANGSI") : lab, r = o.R(code); if (costBo === 1 || costBo === 2) return put(code, base, r, o.k("C_" + code), "직접입력"); if (chg) { const old = prevAmt(code), d = bfch === 2 && prevBase >= 1 ? ((lab - prevBase) * old) / prevBase : ((lab - prevBase) * r) / 100; return put(code, base, r, old + tr(d), "당초액 + 직노증감", "[C_AMT_B]+ROUNDDOWN(...)"); } return pctItem(code, base, o.k(sel) ? "상시근로자노무비" : "직접노무비", "[LAMT]", { LAMT: base, }); }; const kunbo = hp("KUNBO", "I_KUNBO"); expAcc += kunbo; const oldKun = prevAmt("KUNBO"); expAcc += chg && bfch === 2 && oldKun >= 1 ? put( "SLVBO", kunbo, o.R("SLVBO"), prevAmt("SLVBO") + tr(((kunbo - oldKun) * prevAmt("SLVBO")) / oldKun), "당초액 + 증감(비율)", ) : pctItem("SLVBO", kunbo, "건강보험료", "[C_KUNBO]", { C_KUNBO: kunbo }); expAcc += hp("YUNBO", "I_YUNBO"); expAcc += costBo === 2 ? put("TBUGUM", lab, o.R("TBUGUM"), o.k("C_TBUGUM"), "직접입력") : prev && bfch === 2 && prevBase >= 1 ? put( "TBUGUM", lab, o.R("TBUGUM"), prevAmt("TBUGUM") + tr(((lab - prevBase) * prevAmt("TBUGUM")) / prevBase), "당초액 + 증감(비율)", ) : pctItem("TBUGUM", lab, "직접노무비", "[LAMT]", { LAMT: lab }); // 4) 산업안전보건관리비 — 관급 대상액(C_KN_SA 8갈래) · 후보 3개 · 1.2배 비교 · 변경 비율(I_ANJUNAUTO=2) · REA 18 const gD = o.k("B_KWANGUP_D") ? B.govAll : o.k("C_KWANGUP_D"), gK = o.k("C_KWANGUP_K"); let jodal = o.k("B_T_JODAL") ? B.govFee : o.k("C_T_JODAL"); if (jodal > gD) jodal = gD; const gAll = gD + gK, gA = gAll - gK - jodal, knT = o.k("I_KN_SA_TYPE"); const kDiv = posSa === 1 ? Kres : Kres / 1.1; const knSa = [gA / 1.1, gA, kDiv, Kres, gA / 1.1 + Kres, gA + kDiv, gA / 1.1 + kDiv, gA + Kres][knT] ?? 0; const busan = actl ? 0 : o.k("C_BUSAN"), a0 = roundDown(T * o.R("R_MxA"), 0); const b0 = o.k("I_MTYP") === 1 ? actl ? T : c90 + busan : actl && o.k("I_LTYP") === 1 && ((c90 > a0 && o.k("I_MXA") === 0) || o.k("I_MXA") === 1) ? a0 : c90 + busan; const Dn = std ? o.k("C_T_ANJUN_N") : 0, selK = [2, 5, 6].includes(knT) ? kDiv : Kres; let c190 = ((b0 + U - Dn) * o.R("ANJUN1")) / 100 + o.G("C_ANJUN1_G"), c198 = ((b0 + U + knSa - Dn) * o.R("ANJUN2")) / 100 + o.G("C_ANJUN2_G"), c1a0 = ((b0 + U + selK - Dn) * o.R("ANJUN")) / 100 + o.k("C_ANJUN_G"); if (ctx !== 4 && o.k("I_AN_DN_TYPE") !== 1) [c190, c198, c1a0] = [c190, c198, c1a0].map((v) => roundDown(v, 0)); const ak = o.k("I_ANJUN_K"), auto = o.k("I_ANJUNAUTO"); const l18 = ak === 1 ? c198 : ak === 2 ? c1a0 : c190; let l10 = c190, cap = o.k("I_ANJUN_B") === 1, capAuto = false; if (auto === 0 && (ak === 1 || ak === 2)) l10 = ak === 1 ? c198 : c1a0; else if (cfg.anjunCap12 !== false && l10 !== 0 && 1.2 * l10 < l18) cap = capAuto = true; const anjBasis = b0 + U + knSa - Dn; let anjCand = cap ? tr(1.2 * l10) : tr(l18), anjDesc = cap ? "(직노+직재+간재)×율 + 기초액의 1.2배" : "(직노+직재+간재+관급재/1.1)×율 + 기초액"; if (prev && auto === 2 && (o.pclass < 0 || o.pclass === 7) && num(prev.anjunBaseRaw) >= 1) { const pa = prevAmt("ANJUN"), pb = num(prev.anjunBaseRaw); anjCand = pa + tr(((anjBasis - pb) * pa) / pb); anjDesc = "당초금액 + 안전관리비 증감액"; } expAcc += put("ANJUN", roundHalfUp(anjBasis, 0), o.R("ANJUN"), anjCand, anjDesc); if (su) expAcc += put("S_ANJUN_B", 0, 0, o.k("C_S_ANJUN_B"), "별도내역"); const anjB = o.k("C_ANJUN_B"), pumB = o.k("C_PUMJIL_B"); expAcc += put("ANJUN_B", 0, 0, anjB, "별도내역") + put("PUMJIL_B", 0, 0, pumB, "별도내역"); // 5) 기타경비(일반) · 수공 세부경비 8 — 공통 밑수(재료비+노무비 · 실적은 직접공사비 또는 조정계수) const b98 = actl && !o.k("I_LTYP") && c98 != null ? c98 : c90 + kanno; const a8 = roundDown(o.R("R_MxK") * T, 0), gL = actl && o.k("I_LTYP") === 1; const bKita = o.k("I_MTYP") === 1 ? actl ? T : b98 : gL && ((b98 > a8 && o.k("I_MXK") === 0) || o.k("I_MXK") === 1) ? a8 : b98; const kitaDesc = actl && o.k("I_MTYP") === 1 ? "직접공사비" : "직접노무비+간접노무비+재료비"; if (std) expAcc += pctItem("KITA", bKita + U + hol, kitaDesc, "([M_AMT]+[L_AMT][+LAB])", { M_AMT: bKita + U, L_AMT: 0, LAB: hol, }); if (su) for (const c of S8) expAcc += pctItem(c, bKita, kitaDesc, "([L_AMT]+[M_AMT])", { L_AMT: bKita, M_AMT: 0 }); // 6) 공사이행보증 · 하도급 · 환경 · 건설기계 — 밑수 직접공사비(+안전품질 별도·비과세) const F = o.k("I_ANPUM") === 1 ? anjB + pumB : 0, tBase = T + F + U; const ks = tr( (o.k("C_KONGSU_A") + ((T + F + U - o.k("C_KONGSU_MA")) * o.R("KONGSU")) / 100) * o.k("Y_KONGSU"), ); expAcc += put("KONGSU", T, o.R("KONGSU"), ks, "[기초금액+(직접공사비-기준금액)×율]×공기"); const t3 = "재료비 + 직접노무비 + 산출경비"; expAcc += pctItem("HADOSU", tBase, t3, "[T_AMT]", { T_AMT: tBase }); expAcc += pctItem("HWAN", tBase, t3, "[T_AMT]", { T_AMT: tBase }); expAcc += pctItem("EQIP", tBase, t3, "[T_AMT]", { T_AMT: tBase }); expAcc += pctItem("BUDM_SM", Lp + hol, "노무비", "[LAMT]", { LAMT: Lp + hol }); expAcc += pctItem("BUDM_IC", Lp + hol, "노무비", "[LAMT]", { LAMT: Lp + hol }); // 고용개선지원비(서울 5형식) — 주휴수당·사회보험·장려금 각 절사 후 합 · 이윤 뒤 가산 · REA 20 let eisAdd = 0; if (ctx === 5) { const i1 = o.R("EIS_HOLIR1"), i2 = o.R("EIS_HOLIR2"); const hpay = roundDown(((((L * i1) / 100) * i2) / 100) * 1.4, 0); const keep = (c) => num(o.keys["D_" + c]) === 1; const soc = keep("EIS_SOCINS") ? o.k("C_EIS_SOCINS") : roundDown(((L * (o.R("KUNBO") + o.R("YUNBO"))) / 100) * (o.R("EIS_SOCIR2") / 100), 0); const inc = keep("EIS_INCENT") ? o.k("C_EIS_INCENT") : roundDown(((((L * i1) / 100) * i2) / 100) * (o.R("EIS_INCENT") / 100), 0); keysOut.C_EIS_HOLPAY = hpay; const sup = put( "EIS_SUPPAY", L, 0, tr(hpay + soc + inc), "주휴수당+사회보험+장려금", "", "post", ); eisAdd = sup + (keep("EIS_SOCINS") ? put("EIS_SOCINS", L, 0, soc, "", "", "post") : 0); eisAdd += keep("EIS_INCENT") ? put("EIS_INCENT", L, 0, inc, "", "", "post") : 0; Lp += hol; } // 순공사원가(헬퍼 합) — 일반 = 재료비 + 노무비(직+간) + 경비 · 실적 = 직접공사비 + 간접공사비 const add1 = addSum(1); expAcc += add1; const total30 = actl ? T + expAcc : M + Lp + expAcc; const anjunp = put( "ANJUNP", total30 + knSa, o.R("ANJUNP"), tr(((total30 + knSa) * o.R("ANJUNP")) / 100), "(순공사원가+관급)×율", "", "post", ); // 7) 일반관리비 — 일반 계열은 비과세자재·관리품목자재 포함 const ilbanBase = std ? total30 + o.k("C_NBUGAM") + o.k("C_T_K_MAT") : total30; const ilban = put( "ILBAN", ilbanBase, o.R("ILBAN"), tr((ilbanBase * o.R("ILBAN")) / 100), "순공사원가", "", "ilban", ); const A = total30 + ilban; // 8) 이윤 — 일반 (계−재료비)×율 · 실적 계×율 · 원 단위 절사 · 이윤보정액 차감 · 공급가액 절사(I_JEOLSA) const rIyun = o.R("IYUN"), P = actl ? roundDown((A * rIyun) / 100, 0) : roundDown(((A - M) * rIyun) / 100, 0); const fixedAdds = anjunp + put("PSAMT_B", 0, 0, o.k("C_PSAMT_B"), "별도", "", "post") + put("EXCEPT_B", 0, 0, o.k("C_EXCEPT_B"), "별도", "", "post") + (std && posSa === 1 ? Kres : 0) + eisAdd + addSum(2); const k4 = (o.R("S_SIGONG") / 100) * o.R("S_SIGONG_1"), jeol = o.k("I_JEOLSA"); const sig = (x) => ctx === 4 ? (o.fix("S_SIGONG") ?? roundDown((A + P - x + Kres) * k4, 0)) : 0; let mAdj = iyunM, sigong = 0, RT; if (!jeol) { sigong = ctx === 4 ? (o.fix("S_SIGONG") ?? roundDown((A + (P - mAdj) + Kres) * k4, 0)) : 0; RT = A + (P - mAdj) + fixedAdds + sigong; } else { const s0 = sig(0), gross = A + P + fixedAdds + s0, target = roundDown(gross, -jeol), delta = gross - target; if (ctx !== 4) ((mAdj = delta), (sigong = s0)); else { let x = roundBanker(delta / (1 + k4) + 0.5, 0); if (x + s0 - sig(x) !== delta) x = roundDown(delta / (1 + k4), 0); sigong = x + s0 - sig(x) === delta ? sig(x) : s0 - (delta - x); mAdj = x; } RT = A + (P - mAdj) + fixedAdds + sigong; } if (ctx === 4) put( "S_SIGONG", A + P + Kres, o.R("S_SIGONG"), sigong, "(순공사원가+일반관리비+이윤+사급비)×율×계수", "", "post", ); const iyunFinal = P - mAdj, supply308 = RT; keysOut.C_IYUN_M = mAdj; // 9) 공사손해보험 — 대상액 C_KONGSA_J 8갈래(B_KONGSA=1 이면 저장값) · 공급가액 절사 자리 const G23 = posSa === 2 || posSa === 3 ? 1 : 0; const kjCase = [ gA / 1.1 + supply308, gA + supply308, supply308 + (G23 * Kres) / 1.1, supply308 + G23 * Kres, gA / 1.1 + supply308 + G23 * Kres, gA + supply308 + (G23 * Kres) / 1.1, gA / 1.1 + supply308 + (G23 * Kres) / 1.1, gA + supply308 + G23 * Kres, ]; const kongsaJ = o.k("B_KONGSA") ? o.k("C_KONGSA_J") : knT > 7 ? supply308 : roundDown(kjCase[knT], 0); keysOut.C_KONGSA_J = kongsaJ; keysOut.C_KN_SA = knSa; RT += put( "KONGSA", kongsaJ, o.R("KONGSA"), roundDown((kongsaJ * o.R("KONGSA")) / 100, -jeol), "공사손해보험 대상액", "", "post", ); RT -= put("BUSAN_B", 0, 0, o.k("C_BUSAN_B"), "작업설 등", "", "post"); // 10) 부가세(I_BUGA · REA 20) — 0 공급가액 · 1 재료비 · 2 없음 · 3 산림조합(직접비−노무비+안전품질+과세 추가) · 4 3에서 면세품 뺌 const buga = o.k("I_BUGA"), up = bugaUp ? 1 : 0, E = addSum(2, true); const forest = T - L + F + E - (buga === 4 ? o.k("C_BUGA_M") : 0); const vat = buga === 0 ? roundDown(0.1 * RT, 0) + up : buga === 1 ? roundDown(0.1 * M, 0) + up : buga === 3 || buga === 4 ? roundDown(forest * 0.1, 0) + up : 0; const supplyFinal = RT; RT += vat; // 11) 세후 가산 · 도급 절사 · 관급(도급자설치·관급자설치 따로 자리·절상/반올림/절사) · 사급 · 추가4 RT += put("PURCHAS", 0, 0, o.k("C_PURCHAS"), "", "", "post"); if (std) for (const c of ["T_PEGI", "T_KOLJAE", "NBUGAM"]) RT += put(c, 0, 0, o.k("C_" + c), "", "", "post"); if (std && posSa === 2) RT += Kres; const add3 = addSum(3); RT += add3; const contractRaw = RT; if (o.k("I_DOG_JEOL") > 0) RT = roundDown(RT, -o.k("I_DOG_JEOL")); const contractFinal = RT; const rnd = (v, digits, updn) => updn === 1 ? roundBanker(v, -digits) : updn === 2 ? roundDown(v, -digits) : roundUp(v, -digits); const comb = o.k("I_KWN_COMB"); const govFinal = rnd(comb === 0 ? gD : gAll, o.k("I_KWN_JEOLSA"), o.k("I_KWN_UPDN")); const govK = comb === 0 ? rnd(gK, o.k("I_KWN_K_JEOLSA"), o.k("I_KWN_K_UPDN")) : 0; RT += govFinal + govK; Object.assign(keysOut, { C_UPDNKWN: govFinal, C_KWANGUP: gAll, C_KWANGUP_D: gD }); Object.assign(keysOut, { C_KWANGUP_K: gK, C_T_JODAL: jodal }); for (const c of [ "DOSEOH", "S_SIGONG", "EIS_SUPPAY", "EIS_SOCINS", "EIS_INCENT", "EIS_HOLPAY", ...S8, ]) if (keysOut["C_" + c] == null) keysOut["C_" + c] = o.fix(c) ?? 0; const sagAfter = std && (posSa === 1 || posSa === 2) ? 0 : Kres; RT += sagAfter; RT += addSum(4); const anjunCase = { c190, c198, c1a0, l10, l18, cap, capAuto, b0, U, Dn, knSa }; return Object.assign( { rows, keysOut, M, L, S, T, Lp, hol, kanno, expAcc, add1, total30, ilbanBase, ilban, A, P }, { mAdj, iyunFinal, fixedAdds, sigong, supply308, kongsaJ, supplyFinal, vat, contractRaw }, { contractFinal, govFinal, govK, sagAfter, gD, gK, jodal, knSa, anjBasis, anjunCase }, { grand: RT, add3, rIyun, ctx, actl, adds }, ); } Object.assign(calcKanjub, { evalTpl: P.evalTpl, LABEL: P.LABEL, ORD_CODE: P.ORD_CODE }); Object.assign(calcKanjub, { KAN_ORD: P.KAN_ORD, keyOf, codeOf: P.codeOf }); return calcKanjub; }; if (typeof window !== "undefined") window.STMATE_KANJUB_FACTORY = StmateKanjubFactory; if (typeof module !== "undefined" && module.exports) module.exports = StmateKanjubFactory;