feat(stmate-test): 차수별 원가계산서 — 변경설계 당초·변경 1원 · 부가세 5택 · 원가계산 모듈 분리
- 차수(1~4)별 내역 수량·단가와 [n_KANJUB] 설정으로 원가계산서 계산(calculateAll.stages) - D_ 표시 항목 = 저장 금액 고정(변경 시 건강·장기요양·연금·퇴직공제 당초액 유지) - 변경 안전관리비(I_ANJUNAUTO=2) = 당초액 + 당초액 × 밑수 증감율 - 부가세 I_BUGA 0~4(공급가액·재료비·없음·산림조합·산림조합 면세품) · B_BUGA_UP +1원 · 추가금액 차수 칸 - kanjub_calc.js 로 원가계산서 분리(calc.js 700줄 한도) - 지킴 시험: 원가계산서 7건 차수별(변경 2건 당초·변경) 대조 · 장부 33.5% → 34.1% Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
// STMATE 재현 앱 calc.js 를 상태 JSON(stdin)으로 돌려 원가계산서 금액을 냄
|
||||
// STMATE 재현 앱 calc.js 를 상태 JSON(stdin)으로 돌려 차수별 원가계산서 금액을 냄
|
||||
const C = require("../../tools/stmate-test/calc.js");
|
||||
let buf = "";
|
||||
process.stdin.on("data", (d) => (buf += d));
|
||||
process.stdin.on("end", () => {
|
||||
const k = C.calculateAll(JSON.parse(buf)).kanjubOrig;
|
||||
const out = { dM: k.dM, dL: k.dL, dS: k.dS, dT: k.dT };
|
||||
for (const it of k.items) out[it.code] = it.amount;
|
||||
Object.assign(out, { ILBAN: k.ilbanAmt, IYUN_M: k.pretaxProfitAdj, GOV: k.govFinal });
|
||||
out.GRAND = k.grandTotal;
|
||||
const res = C.calculateAll(JSON.parse(buf));
|
||||
const out = {};
|
||||
for (const [n, st] of Object.entries(res.stages)) {
|
||||
const k = st.kanjub;
|
||||
const o = { dM: k.dM, dL: k.dL, dS: k.dS, dT: k.dT };
|
||||
for (const it of k.items) o[it.code] = it.amount;
|
||||
Object.assign(o, { ILBAN: k.ilbanAmt, IYUN_M: k.pretaxProfitAdj, GOV: k.govFinal });
|
||||
o.GRAND = k.grandTotal;
|
||||
out[n] = o;
|
||||
}
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""STMATE 재현 앱 원가계산서 — 실무 STC 4건의 저장값(PRJLST.DBF · PRJLST.SMT)과 1원 대조.
|
||||
"""STMATE 재현 앱 원가계산서 — 실무 STC 7건의 차수별 저장값(PRJLST.DBF n칸 · PRJLST.SMT [n_KANJUB])과 1원 대조.
|
||||
|
||||
정답은 STC 안 원본 저장값에서 읽음(코드에 박지 않음). 직접비는 내역 저장 단가 기준.
|
||||
변경설계 2건은 당초(1)·변경(2) 차수 모두 대조 — D_ 당초액 유지 · 안전관리비 비례 · 산림조합 부가세 포함.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -21,23 +22,26 @@ STCS = {
|
||||
"울진2025": "25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC",
|
||||
"영월2024": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC",
|
||||
"대흥2024": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).STC",
|
||||
"소광원설계": "25년 산불진화임도(기번8)(울진소광)/1. 설계원본/03-내역/22-2025년 산불진화임도사업(울진 금강송 소광).STC",
|
||||
"영덕변경": "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).STC",
|
||||
"소광변경1차": "25년 산불진화임도(기번8)(울진소광)/변경 1차분-2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC",
|
||||
}
|
||||
SMT_KEYS = {
|
||||
"KANNO": "1_C_KANNO",
|
||||
"SANBO": "1_C_SANBO",
|
||||
"KOBO": "1_C_KOBO",
|
||||
"KUNBO": "1_C_KUNBO",
|
||||
"SLVBO": "1_C_SLVBO",
|
||||
"YUNBO": "1_C_YUNBO",
|
||||
"ANJUN": "1_C_ANJUN",
|
||||
"KITA": "1_C_T_KITA",
|
||||
"HWAN": "1_C_HWAN",
|
||||
"EQIP": "1_C_EQIPSU",
|
||||
"ILBAN": "1_C_ILBAN",
|
||||
"IYUN_M": "1_C_IYUN_M",
|
||||
"GOV": "1_C_UPDNKWN",
|
||||
"KANNO": "C_KANNO",
|
||||
"SANBO": "C_SANBO",
|
||||
"KOBO": "C_KOBO",
|
||||
"KUNBO": "C_KUNBO",
|
||||
"SLVBO": "C_SLVBO",
|
||||
"YUNBO": "C_YUNBO",
|
||||
"ANJUN": "C_ANJUN",
|
||||
"KITA": "C_T_KITA",
|
||||
"HWAN": "C_HWAN",
|
||||
"EQIP": "C_EQIPSU",
|
||||
"ILBAN": "C_ILBAN",
|
||||
"IYUN_M": "C_IYUN_M",
|
||||
"GOV": "C_UPDNKWN",
|
||||
}
|
||||
DBF_KEYS = {"dM": "MCOST1", "dL": "LCOST1", "dS": "SCOST1", "dT": "TCOST1", "GRAND": "COSTW1"}
|
||||
DBF_KEYS = {"dM": "MCOST", "dL": "LCOST", "dS": "SCOST", "dT": "TCOST", "GRAND": "COSTW"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(STCS))
|
||||
@@ -51,7 +55,12 @@ def test_kanjub_matches_stc_to_the_won(name):
|
||||
encoding="utf-8",
|
||||
)
|
||||
)
|
||||
expected = {k: loaded["prjlst_smt"][v] for k, v in SMT_KEYS.items()}
|
||||
expected |= {k: loaded["prjlst"][v] for k, v in DBF_KEYS.items()}
|
||||
diffs = {k: (out[k], float(v)) for k, v in expected.items() if abs(out[k] - float(v)) >= 0.5}
|
||||
assert out, f"{name} 원가계산서 없음"
|
||||
diffs = {}
|
||||
for n, got in out.items():
|
||||
expected = {k: loaded["prjlst_smt"][f"{n}_{v}"] for k, v in SMT_KEYS.items()}
|
||||
expected |= {k: loaded["prjlst"][f"{v}{n}"] for k, v in DBF_KEYS.items()}
|
||||
for k, v in expected.items():
|
||||
if abs(got[k] - float(v)) >= 0.5:
|
||||
diffs[f"{n}:{k}"] = (got[k], float(v))
|
||||
assert not diffs, f"{name} 불일치 {diffs}"
|
||||
|
||||
+10
-10
@@ -17,11 +17,11 @@
|
||||
|
||||
- `test_stmate_ilwi_eval.py` — 일위대가·중기 구성 재계산(원설계 5건 + 영덕 변경) 1원 · 연쇄 재계산(기초단가 → 원가계산서) 원설계 5건 전 단가·총공사비 그대로
|
||||
- `test_stmate_smt_eval.py` — 단가산출근거(D코드) 재계산: 원설계 5건(위 4건 + 소광 원설계) 282개 1원 · 낙찰률 단가 7건 127개
|
||||
- `test_stmate_kanjub_stc.py` — 실무 STC 4건(봉화 2024 · 울진 2025 · 영월 2024 · 울진 대흥 2024) 원가계산서 18항목(직접비 4 · 간접비 11 · 이윤감액 · 관급 · 총공사비) 1원 대조 · 직접비는 내역 저장 단가 기준
|
||||
- `test_stmate_kanjub_stc.py` — 실무 STC 7건 차수별(변경 2건은 당초·변경) 원가계산서 18항목(직접비 4 · 간접비 11 · 이윤감액 · 관급 · 총공사비) 1원 대조 · 직접비는 내역 저장 단가 기준
|
||||
|
||||
## 진행률
|
||||
|
||||
- 합계: 전체 246 · 구현 42 · 부분 81 · 없음 123 → 진행률 33.5%((42 + 81×0.5) / 246) · 구현만 세면 17.1%
|
||||
- 합계: 전체 246 · 구현 44 · 부분 80 · 없음 122 → 진행률 34.1%((44 + 80×0.5) / 246) · 구현만 세면 17.9%
|
||||
|
||||
### G1 화면·메뉴 (구현 5 · 부분 21 · 없음 28) — 28.7%
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
- [ ] F1-53 인쇄영역 선택(당초/변경/분할·레벨·변경 1~8차) — wM_Sel_Prn · 현재: 없음
|
||||
- [ ] F1-54 특수문자·그림코드·메모 입력 — mMinMsg_ · wInDlgImg · wInDlgMemo · 현재: 없음
|
||||
|
||||
### G2 계산 엔진 (구현 19 · 부분 23 · 없음 19) — 50.0%
|
||||
### G2 계산 엔진 (구현 20 · 부분 22 · 없음 19) — 50.8%
|
||||
|
||||
- [x] F2-01 노임 일→시간 환산식 좌→우 순차 — 18번 §2.1 · Start8x RXXNM · 현재: 구현(calc.js 84)
|
||||
- [ ] F2-02 노임 환산 갈래 H0~H5(터널·준설선) — wM_Set_Sys cmBx_Sel_RX · 30번 §4 · 현재: 없음
|
||||
@@ -125,13 +125,13 @@
|
||||
- [ ] F2-41 이윤/수수료 택일·이윤 직접입력 — 12번 §10.1-2 pn02 · 현재: 없음
|
||||
- [x] F2-42 끝전조정(차액 세전환산 이윤 차감)·1원 보정 — 16번 §4.4 · REA 17 §5 · 현재: 구현(총공사비 기준 절사 · 절사 없음 = 저장 이윤감액 · 4건 1원)
|
||||
- [ ] F2-43 공급가액 절사 9택 — 12번 §10.2 · 현재: 없음
|
||||
- [ ] F2-44 부가세 설계 5택(산림조합 형식·면세품 포함) — 12번 §10.1 · 현재: 부분(10%·재료 10%·없음만, 산림조합 2택 없음)
|
||||
- [x] F2-44 부가세 설계 5택(산림조합 형식·면세품 포함) — 12번 §10.1 · REA 20 · 현재: 구현(I_BUGA 0~4 · 올림 +1 · 영덕 면세품 대조 · 안전품질 F 항 미포함)
|
||||
- [ ] F2-45 공사손해보험·작업설 감액·비과세자재·매입세 — 12번 §3.1 · 현재: 없음
|
||||
- [ ] F2-46 관급/사급 내용·사급자재/1.1·사급비 위치 4택 — 12번 §10.1 · 25번 §2.6 · 현재: 없음
|
||||
- [ ] F2-47 도급공사비/도급예정액·관급 2종 절사·절상·반올림 — 12번 §10.2 · 현재: 부분(관급 한 갈래만)
|
||||
- [ ] F2-48 총공사비 절사 자리 — 12번 §10.2 · 현재: 부분(I_TOT_JEOL 읽음, 기본값 버그 calc.js 468)
|
||||
- [x] F2-49 간접비 일괄 절사 7택 — wM_KanJub cBx_K_JeolSa · 현재: 구현
|
||||
- [ ] F2-50 추가금액 1~9단계 삽입 — 11번 §9 · 25번 §1.3 · 현재: 부분(저장 단계 1~4 계산 반영 · 화면 입력·5칸·부가세 여부 없음)
|
||||
- [ ] F2-50 추가금액 1~9단계 삽입 — 11번 §9 · 25번 §1.3 · 현재: 부분(저장 단계 1~4 · 차수 칸 · 부가세 여부 계산 반영 · 화면 입력 없음)
|
||||
- [ ] F2-51 간접비 순서 코드표(KAN_ORD_1/2) — 20번 §2.1 · 현재: 없음(순서가 코드에 고정)
|
||||
- [ ] F2-52 원가 일반형식 T — 12번 §3 · 현재: 부분(이행·하도급·석면·임금채권·고용개선·품질 빠짐)
|
||||
- [ ] F2-53 원가 수공형식 S — 12번 §4 · 현재: 부분(세부경비·일반관리비 밑수 다름)
|
||||
@@ -248,19 +248,19 @@
|
||||
- [ ] F5-35 금액 한글 표기(AMTTOHAN) — 24번 §2.4 · 현재: 없음
|
||||
- [ ] F5-36 SET_REP 데이터 기반 열·폭·제목 — 20번 §3 · 현재: 부분(양식표 표시만, 출력에 미적용)
|
||||
|
||||
### G6 설계변경·계약·실행·기성 (구현 2 · 부분 9 · 없음 25) — 18.1%
|
||||
### G6 설계변경·계약·실행·기성 (구현 3 · 부분 10 · 없음 23) — 22.2%
|
||||
|
||||
- [ ] F6-01 차수 4벌(원설계+3차변경) 칸 관리 — 08번 §2 · 24번 §7 · 현재: 부분(2·3 칸을 변경·계약 값으로 덮어씀 view_prj_boq 560)
|
||||
- [ ] F6-01 차수 4벌(원설계+3차변경) 칸 관리 — 08번 §2 · 24번 §7 · 현재: 부분(계산 = 차수별 수량·단가·원가계산서 · 저장은 2·3 칸 덮어씀 view_prj_boq 560)
|
||||
- [ ] F6-02 통합형 BOQ11+BOQ12에서 BOQ12 현재안 — 16번 §4.5 · 현재: 없음(늘 BOQ11 server.py 465)
|
||||
- [ ] F6-03 분리형 변경 STC 생성·차수 인계 — 16번 §4.6 · 현재: 없음
|
||||
- [ ] F6-04 변경 수량 입력 — 설계변경 · 현재: 부분(공종별 입력, 1 단위 칸 app.js 536)
|
||||
- [ ] F6-05 변경 단가·신규 공종·삭제 항목(0 유지) — 08번 §2 · 현재: 없음
|
||||
- [x] F6-06 변경 직접비 당초·변경·증감 — 19번 §6.5 · 현재: 구현(calc.js 661~670)
|
||||
- [x] F6-07 간접비 변경 모드: 재계산 — 08번 §2 · 현재: 구현(calc.js 357)
|
||||
- [ ] F6-08 간접비 변경 모드: 당초액 유지(건강·장기요양·연금·퇴직공제) — 19번 §6 · 35번 §4 · 현재: 부분(세 항목 고정, 퇴직공제·선택 없음)
|
||||
- [ ] F6-09 간접비 변경 방법 A(비율)·방법 B(증감×율) — 08번 §2 · 현재: 부분(식만, 고를 화면 없음)
|
||||
- [x] F6-08 간접비 변경 모드: 당초액 유지(건강·장기요양·연금·퇴직공제) — 19번 §6 · 35번 §4 · 현재: 구현(D_ 표시 항목 = 저장 금액 고정 · 변경 2건 대조)
|
||||
- [ ] F6-09 간접비 변경 방법 A(비율)·방법 B(증감×율) — 08번 §2 · 현재: 부분(안전관리비 I_ANJUNAUTO=2 방법 A 대조 · 다른 항목·고를 화면 없음)
|
||||
- [ ] F6-10 보험료 변경 계산 3모드 — wM_KanJub cBx_CH_BO · 현재: 없음
|
||||
- [ ] F6-11 2차·3차 변경 계산 — wM_KanJub cBx_Chasu · 현재: 없음(차수 칸 처리 없음)
|
||||
- [ ] F6-11 2차·3차 변경 계산 — wM_KanJub cBx_Chasu · 현재: 부분(차수 1~4 계산 길 · 2차수 2건 대조 · 3·4차 표본 없음)
|
||||
- [ ] F6-12 원가계산서 당초/변경 대비·변경분만 인쇄 — PM_Won_A_C · Cmbx_PORD1_H · 현재: 부분(화면 증감 표, 인쇄 양식 없음)
|
||||
- [ ] F6-13 설계변경내역서(0643·0644) — CHANGE_STR · 현재: 없음(0603과 같은 표 view_kanjub_prt 531)
|
||||
- [ ] F6-14 변경내역서(변경율)(0645) — CHANGE_AMT · 현재: 없음
|
||||
|
||||
+42
-232
@@ -61,6 +61,15 @@ const StmateCalc = (() => {
|
||||
: roundDown(val, digits);
|
||||
}
|
||||
|
||||
// 원가계산서(제잡비) 계산 — kanjub_calc.js
|
||||
const calcKanjub = (globalThis.STMATE_KANJUB_FACTORY || require("./kanjub_calc.js"))({
|
||||
num,
|
||||
fmt,
|
||||
roundDown,
|
||||
roundHalfUp,
|
||||
applyRoundMode,
|
||||
});
|
||||
|
||||
function evalFormula(exprStr, vars = {}) {
|
||||
let s = String(exprStr || "")
|
||||
.replace(/'[^']*'/g, "")
|
||||
@@ -247,6 +256,7 @@ const StmateCalc = (() => {
|
||||
}
|
||||
|
||||
function calcBoq(data, resMap, machMap, sanMap, ilwiMap, useChangedQty = false) {
|
||||
const slot = typeof useChangedQty === "number" ? useChangedQty : useChangedQty ? 2 : 1;
|
||||
const sys = data.sysConfig || {},
|
||||
chnMode = sys.chnMode || "bimok",
|
||||
optOrder = (sys.optOrder || "SML").toUpperCase();
|
||||
@@ -274,8 +284,19 @@ const StmateCalc = (() => {
|
||||
govRawTotal = 0,
|
||||
govFeeTotal = 0;
|
||||
const rows = (data.boqItems || []).map((row) => {
|
||||
const u = lookupUnit(row.refId),
|
||||
qty = Number(useChangedQty && row.changedQty !== undefined ? row.changedQty : row.qty) || 0;
|
||||
const su = slot > 1 && row.units && row.units[slot];
|
||||
const u = su
|
||||
? Object.assign({}, lookupUnit(row.refId), { M: su[0], L: su[1], S: su[2] })
|
||||
: lookupUnit(row.refId);
|
||||
u.T = u.M + u.L + u.S;
|
||||
const qty =
|
||||
Number(
|
||||
slot > 1 && row.qtys
|
||||
? row.qtys[slot - 1]
|
||||
: slot > 1 && row.changedQty !== undefined
|
||||
? row.changedQty
|
||||
: row.qty,
|
||||
) || 0;
|
||||
const rawM = u.M * qty,
|
||||
rawL = u.L * qty,
|
||||
rawS = u.S * qty,
|
||||
@@ -334,235 +355,6 @@ const StmateCalc = (() => {
|
||||
return Object.assign({ rows, directTotal: directT, totals }, totals);
|
||||
}
|
||||
|
||||
function calcKanjub(data, boqRes, origKanjubRes = null) {
|
||||
const cfg = data.kanjubConfig || {},
|
||||
scheme = cfg.scheme || "T",
|
||||
rates = cfg.rates || {},
|
||||
revModes = cfg.revModes || {};
|
||||
const indTruncDigits = Number(cfg.indirectTruncDigits) || 0;
|
||||
const dM = boqRes.directM,
|
||||
dL = boqRes.directL,
|
||||
dS = boqRes.directS,
|
||||
dT = boqRes.directTotal,
|
||||
govMat = cfg.govMat ?? boqRes.govCombinedRaw,
|
||||
govFee = cfg.govFee ?? boqRes.govFeeTotal,
|
||||
govPureMat = govMat - govFee;
|
||||
const adds = cfg.addAmounts || [];
|
||||
const addSum = (stage) =>
|
||||
adds.filter((a) => a.stage === stage).reduce((acc, a) => acc + num(a.amount), 0);
|
||||
const truncInd = (v) => roundDown(v, indTruncDigits);
|
||||
|
||||
function resolveRow(code, label, baseDesc, baseAmt, ratePct, rawCalcAmt) {
|
||||
const truncAmt = truncInd(rawCalcAmt);
|
||||
const base = Object.assign(
|
||||
{ key: code, code, label, formula: baseDesc },
|
||||
{ baseDesc, baseAmt, base: baseAmt, rate: ratePct, ratePct },
|
||||
);
|
||||
if (!origKanjubRes) return Object.assign(base, { amount: truncAmt, modeUsed: "원설계" });
|
||||
const origRow = origKanjubRes.itemMap[code],
|
||||
origAmt = origRow ? origRow.amount : 0,
|
||||
origBase = origRow ? origRow.baseAmt : 0;
|
||||
const mode = revModes[code] || "recalculated";
|
||||
const finalAmt =
|
||||
mode === "carry_original"
|
||||
? origAmt
|
||||
: mode === "proportional"
|
||||
? origAmt + truncInd(origAmt * (origBase > 0 ? (baseAmt - origBase) / origBase : 0))
|
||||
: mode === "delta_rate"
|
||||
? origAmt + truncInd(((baseAmt - origBase) * ratePct) / 100)
|
||||
: truncAmt;
|
||||
return Object.assign(base, {
|
||||
amount: finalAmt,
|
||||
origAmt,
|
||||
deltaAmt: finalAmt - origAmt,
|
||||
modeUsed: mode,
|
||||
});
|
||||
}
|
||||
|
||||
const items = [],
|
||||
itemMap = {};
|
||||
const pushItem = (row) => (items.push(row), (itemMap[row.code] = row), row.amount);
|
||||
const pushRate = (code, label, desc, base, defR) => {
|
||||
const r = num(rates[code], defR);
|
||||
return pushItem(resolveRow(code, label, desc, base, r, (base * r) / 100));
|
||||
};
|
||||
|
||||
const kanno = pushRate("KANNO", "간접노무비", "직접노무비", dL, 14.5);
|
||||
const totalLabor = dL + kanno;
|
||||
pushRate("SANBO", "산재보험료", "노무비(직노+간노)", totalLabor, 3.56);
|
||||
pushRate("KOBO", "고용보험료", "노무비(직노+간노)", totalLabor, 1.01);
|
||||
const kunbo = pushRate("KUNBO", "건강보험료", "직접노무비", dL, 3.545);
|
||||
pushRate("SLVBO", "노인장기요양보험료", "건강보험료(확정액)", kunbo, 12.95);
|
||||
pushRate("YUNBO", "연금보험료", "직접노무비", dL, 4.5);
|
||||
pushRate("TBUGUM", "퇴직공제부금비", "직접노무비", dL, 0);
|
||||
|
||||
const rAnjun = num(rates.ANJUN, 1.86),
|
||||
anjunBaseAdd = num(cfg.anjunBaseAdd, 0);
|
||||
const anjunGovDiv = cfg.anjunGovVatExcluded !== false ? govPureMat / 1.1 : govPureMat;
|
||||
const anjunBaseNoGov = scheme === "TA" || scheme === "SA" ? dT : dM + dL,
|
||||
anjunBaseWithGov = anjunBaseNoGov + anjunGovDiv;
|
||||
const anjunNormal = truncInd((anjunBaseWithGov * rAnjun) / 100) + anjunBaseAdd;
|
||||
const anjunCap12 = truncInd(((anjunBaseNoGov * rAnjun) / 100 + anjunBaseAdd) * 1.2);
|
||||
const anjunDesc =
|
||||
scheme === "TA" || scheme === "SA"
|
||||
? "직접공사비+관급원자재/1.1 (+기초액)"
|
||||
: "직재+직노+관급원자재/1.1 (+기초액)";
|
||||
pushItem(
|
||||
resolveRow(
|
||||
"ANJUN",
|
||||
"산업안전보건관리비",
|
||||
anjunDesc,
|
||||
roundHalfUp(anjunBaseWithGov, 0),
|
||||
rAnjun,
|
||||
cfg.anjunCap12 !== false ? Math.min(anjunNormal, anjunCap12) : anjunNormal,
|
||||
),
|
||||
);
|
||||
|
||||
if (scheme === "S" || scheme === "SA") {
|
||||
const suBase = dM + totalLabor;
|
||||
[
|
||||
["S_SUDO", "수도광열비", 0.45],
|
||||
["S_BOKRI", "복리후생비", 1.15],
|
||||
["S_SOMO", "소모품·사무용품비", 0.65],
|
||||
["S_TONGSIN", "여비·교통·통신비", 0.85],
|
||||
["S_SEGUM", "세금·공과금", 0.55],
|
||||
["S_PEGI", "폐기물처리비", 0.35],
|
||||
["S_DOSEO", "도서인쇄비", 0.25],
|
||||
["S_JIGUP", "지급수수료", 0.45],
|
||||
].forEach(([sc, sl, defR]) =>
|
||||
pushRate(sc, `[수공] ${sl}`, "재료비+노무비(직노+간노)", suBase, defR),
|
||||
);
|
||||
} else {
|
||||
pushRate(
|
||||
"KITA",
|
||||
"기타경비",
|
||||
scheme === "TA" ? "직접공사비(실적일반)" : "재료비+노무비(직노+간노)",
|
||||
scheme === "TA" ? dT : dM + totalLabor,
|
||||
6.0,
|
||||
);
|
||||
}
|
||||
|
||||
const hwanBase = scheme === "TA" ? dT : dM + dL + dS,
|
||||
hwanDesc = scheme === "TA" ? "직접공사비(실적일반)" : "재료비+직접노무비+산출경비";
|
||||
pushRate("HWAN", "환경보전비", hwanDesc, hwanBase, 0.8);
|
||||
pushRate("EQIP", "건설기계대여대금 지급보증", hwanDesc, hwanBase, 0.4);
|
||||
|
||||
const indirectExpenseSum = items
|
||||
.filter((x) => x.code !== "KANNO")
|
||||
.reduce((acc, x) => acc + x.amount, 0);
|
||||
const add1 = addSum(1),
|
||||
totalExpense = dS + indirectExpenseSum + add1,
|
||||
pureConstructionCost = dM + totalLabor + totalExpense;
|
||||
|
||||
const rIlban = num(rates.ILBAN, 6.0),
|
||||
ilbanBase = scheme === "SA" ? dT + kanno : pureConstructionCost;
|
||||
const ilbanDesc = scheme === "SA" ? "직접공사비+간접노무비(실적수공)" : "순공사원가(재+노+경)",
|
||||
ilbanAmt = truncInd((ilbanBase * rIlban) / 100);
|
||||
|
||||
const rIyun = num(rates.IYUN, 15.0);
|
||||
const iyunBase =
|
||||
scheme === "TA" || scheme === "SA"
|
||||
? pureConstructionCost + ilbanAmt
|
||||
: totalLabor + totalExpense + ilbanAmt;
|
||||
const iyunDesc =
|
||||
scheme === "TA" || scheme === "SA"
|
||||
? "직접비+간접비+일반관리비(실적형식)"
|
||||
: "노무비+경비+일반관리비(재료비 제외)";
|
||||
const iyunRaw = truncInd((iyunBase * rIyun) / 100);
|
||||
|
||||
const vatMode = cfg.vatMode || "supply_10",
|
||||
totJeolDigits = num(cfg.totJeolDigits, -3),
|
||||
add2 = addSum(2),
|
||||
add3 = addSum(3),
|
||||
add4 = addSum(4);
|
||||
const calcVat = (sVal) =>
|
||||
vatMode === "zero"
|
||||
? 0
|
||||
: vatMode === "mat_10"
|
||||
? roundDown(dM * 0.1, 0)
|
||||
: vatMode === "mat_exp_10"
|
||||
? roundDown((dM + dS) * 0.1, 0)
|
||||
: roundDown(sVal * 0.1, 0);
|
||||
|
||||
const govFinal =
|
||||
govMat > 0
|
||||
? applyRoundMode(govMat, cfg.govRoundMode || "up", num(cfg.govRoundDigits, -3))
|
||||
: 0;
|
||||
const iyunManual = num(cfg.iyunManualAdj, 0),
|
||||
supplyBeforeAdj = pureConstructionCost + ilbanAmt + iyunRaw - iyunManual + add2,
|
||||
vatBeforeAdj = calcVat(supplyBeforeAdj),
|
||||
contractBeforeAdj = supplyBeforeAdj + vatBeforeAdj + add3;
|
||||
let contractTarget = contractBeforeAdj,
|
||||
diffTotal = 0,
|
||||
pretaxProfitAdj = iyunManual,
|
||||
iyunFinal = iyunRaw - iyunManual,
|
||||
supplyFinal = supplyBeforeAdj,
|
||||
vatFinal = vatBeforeAdj;
|
||||
|
||||
if (totJeolDigits < 0 && cfg.enableProfitEndAdj !== false) {
|
||||
// 총액 절사 = 총공사비(도급 + 관급 + 단계6 추가) 기준 · 차액은 도급에서 뺌
|
||||
contractTarget =
|
||||
roundDown(contractBeforeAdj + govFinal + add4, totJeolDigits) - govFinal - add4;
|
||||
diffTotal = contractBeforeAdj - contractTarget;
|
||||
pretaxProfitAdj = vatMode === "supply_10" ? roundHalfUp(diffTotal / 1.1, 0) : diffTotal;
|
||||
iyunFinal = iyunRaw - pretaxProfitAdj;
|
||||
supplyFinal = pureConstructionCost + ilbanAmt + iyunFinal + add2;
|
||||
vatFinal = calcVat(supplyFinal);
|
||||
if (supplyFinal + vatFinal + add3 !== contractTarget)
|
||||
vatFinal = contractTarget - supplyFinal - add3;
|
||||
}
|
||||
const contractFinal = supplyFinal + vatFinal + add3;
|
||||
const grandTotal = contractFinal + govFinal + add4;
|
||||
const mkLine = (key, label, formula, baseAmt, rate, amount) =>
|
||||
Object.fromEntries([
|
||||
["key", key],
|
||||
["label", label],
|
||||
["formula", formula],
|
||||
["baseAmt", baseAmt],
|
||||
["base", baseAmt],
|
||||
["rate", rate],
|
||||
["amount", amount],
|
||||
]);
|
||||
const totStr = "재료비 + 노무비 + 경비",
|
||||
iyunStr = `${iyunDesc} (보정 -${fmt(pretaxProfitAdj)})`,
|
||||
supStr = "순공사원가 + 일반관리비 + 이윤",
|
||||
finStr = "공급가액 + 부가가치세 (천원절사)",
|
||||
govStr = `관급원자재(${fmt(govPureMat)}) + 수수료(${fmt(govFee)})`;
|
||||
const pCost = pureConstructionCost;
|
||||
const lines = [
|
||||
mkLine("DIRECT_M", "직접재료비", "내역서 재료비 합계", dM, null, dM),
|
||||
mkLine("DIRECT_L", "직접노무비", "내역서 직접노무비 합계", dL, null, dL),
|
||||
mkLine("DIRECT_S", "산출경비", "내역서 산출경비 합계", dS, null, dS),
|
||||
mkLine("DIRECT_COST", "직접공사비 계", "직재 + 직노 + 산출경비", dT, null, dT),
|
||||
...items.map((it) => mkLine(it.key, it.label, it.formula, it.baseAmt, it.rate, it.amount)),
|
||||
mkLine("TOTAL_COST", "순공사원가 계", totStr, pCost, null, pCost),
|
||||
mkLine("ILBAN", "일반관리비", ilbanDesc, ilbanBase, rIlban, ilbanAmt),
|
||||
mkLine("IYUN", "이윤 (끝전보정 후)", iyunStr, iyunBase, rIyun, iyunFinal),
|
||||
mkLine("SUPPLY_VAL", "총원가 (공급가액)", supStr, supplyFinal, null, supplyFinal),
|
||||
mkLine("VAT", "부가가치세", "공급가액 × 10%", supplyFinal, 10, vatFinal),
|
||||
mkLine("FINAL_SUPPLY", "도급공사비", finStr, contractBeforeAdj, null, contractFinal),
|
||||
mkLine("GOV_MAT", "관급자재대", govStr, govMat, null, govFinal),
|
||||
mkLine("GRAND_TOTAL", "총공사비 (COSTW1)", "도급 + 관급", grandTotal, null, grandTotal),
|
||||
];
|
||||
|
||||
const out = { scheme, dM, dL, dS, dT, kanno, totalLabor };
|
||||
Object.assign(out, { indirectExpenseSum, totalExpense, pureConstructionCost: pCost });
|
||||
Object.assign(out, { ilbanBase, ilbanDesc, ilbanAmt, iyunBase, iyunDesc, iyunRaw });
|
||||
Object.assign(out, { pretaxProfitAdj, iyunFinal, supplyBeforeAdj, vatBeforeAdj });
|
||||
Object.assign(out, { contractBeforeAdj, diffTotal, supplyFinal, vatFinal, contractFinal });
|
||||
Object.assign(out, { govRawTotal: govPureMat, govFeeTotal: govFee, adds });
|
||||
Object.assign(out, { govFinal, grandTotal, items, itemMap, lines });
|
||||
Object.assign(out, { DIRECT_COST: dT, KANNO: kanno, IND_EXP: kanno + indirectExpenseSum });
|
||||
Object.assign(out, { TOTAL_COST: pCost, ILBAN: ilbanAmt, IYUN_INIT: iyunRaw });
|
||||
Object.assign(out, { PROFIT_ADJ: -pretaxProfitAdj, IYUN: iyunFinal, SUPPLY_VAL: supplyFinal });
|
||||
return Object.assign(out, {
|
||||
VAT: vatFinal,
|
||||
FINAL_SUPPLY: contractFinal,
|
||||
GOV_MAT: govFinal,
|
||||
GRAND_TOTAL: grandTotal,
|
||||
});
|
||||
}
|
||||
|
||||
function evalKOrdTransition(pclass, regMode, flag, currentK) {
|
||||
const K = Number(currentK) || 1;
|
||||
const nextK =
|
||||
@@ -630,8 +422,25 @@ const StmateCalc = (() => {
|
||||
const ilwi = calcIlwi(data, resources.map, machines.map, sanCal.map);
|
||||
const boqOrig = calcBoq(data, resources.map, machines.map, sanCal.map, ilwi.map, false),
|
||||
kanjubOrig = calcKanjub(data, boqOrig, null);
|
||||
const st = data.kanjubStages || {};
|
||||
const boqRev = calcBoq(data, resources.map, machines.map, sanCal.map, ilwi.map, true),
|
||||
kanjubRev = calcKanjub(data, boqRev, kanjubOrig);
|
||||
kanjubRev = st[2]
|
||||
? calcKanjub(data, boqRev, kanjubOrig, st[2])
|
||||
: calcKanjub(data, boqRev, kanjubOrig);
|
||||
const stages = {};
|
||||
for (const n of Object.keys(st).map(Number)) {
|
||||
const boq =
|
||||
n === 1
|
||||
? boqOrig
|
||||
: n === 2
|
||||
? boqRev
|
||||
: calcBoq(data, resources.map, machines.map, sanCal.map, ilwi.map, n);
|
||||
stages[n] = {
|
||||
boq,
|
||||
kanjub:
|
||||
n === 1 ? kanjubOrig : n === 2 ? kanjubRev : calcKanjub(data, boq, kanjubOrig, st[n]),
|
||||
};
|
||||
}
|
||||
const revMap = Object.fromEntries((boqRev.rows || []).map((r) => [r.id, r]));
|
||||
(boqOrig.rows || []).forEach((r) => {
|
||||
const rv = revMap[r.id];
|
||||
@@ -644,6 +453,7 @@ const StmateCalc = (() => {
|
||||
kanjubRev,
|
||||
resList: resources.list,
|
||||
revComputed: { revBoq: boqRev, revKj: kanjubRev },
|
||||
stages,
|
||||
});
|
||||
return Object.assign(base, { boqComputed: boqOrig, kanjubComputed: kanjubOrig });
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="kanjub_calc.js"></script>
|
||||
<script src="calc.js"></script>
|
||||
<script src="smt_eval.js"></script>
|
||||
<script src="smt_engine.js"></script>
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
/* -*- coding: utf-8 -*- */
|
||||
/**
|
||||
* 원가계산서(제잡비) 계산 (kanjub_calc.js) — 직접비 → 간접비 → 일반관리비 → 이윤 → 부가세 → 끝전 → 관급 → 총공사비
|
||||
* 근거: 지식DB STmate/old/20_분석 06 · 16 · 25 · REA 17 §5 · REA 20(부가세·관급·추가금액 순서)
|
||||
* 정답 대조: resources/tester/test_stmate_kanjub_stc.py
|
||||
*/
|
||||
const StmateKanjubFactory = ({ num, fmt, roundDown, roundHalfUp, applyRoundMode }) => {
|
||||
function calcKanjub(data, boqRes, origKanjubRes = null, stageCfg = null) {
|
||||
const cfg = stageCfg || data.kanjubConfig || {},
|
||||
fixed = cfg.fixed || {},
|
||||
scheme = cfg.scheme || "T",
|
||||
rates = cfg.rates || {},
|
||||
revModes = cfg.revModes || {};
|
||||
const indTruncDigits = Number(cfg.indirectTruncDigits) || 0;
|
||||
const dM = boqRes.directM,
|
||||
dL = boqRes.directL,
|
||||
dS = boqRes.directS,
|
||||
dT = boqRes.directTotal,
|
||||
govMat = cfg.govMat ?? boqRes.govCombinedRaw,
|
||||
govFee = cfg.govFee ?? boqRes.govFeeTotal,
|
||||
govPureMat = govMat - govFee;
|
||||
const adds = cfg.addAmounts || [];
|
||||
const addSum = (stage) =>
|
||||
adds.filter((a) => a.stage === stage).reduce((acc, a) => acc + num(a.amount), 0);
|
||||
const truncInd = (v) => roundDown(v, indTruncDigits);
|
||||
|
||||
function resolveRow(code, label, baseDesc, baseAmt, ratePct, rawCalcAmt) {
|
||||
// D_ 표시 항목 = 저장 금액 고정(변경 차수의 당초액 유지)
|
||||
const truncAmt = fixed[code] != null ? fixed[code] : truncInd(rawCalcAmt);
|
||||
const base = Object.assign(
|
||||
{ key: code, code, label, formula: baseDesc },
|
||||
{ baseDesc, baseAmt, base: baseAmt, rate: ratePct, ratePct },
|
||||
);
|
||||
if (!origKanjubRes) return Object.assign(base, { amount: truncAmt, modeUsed: "원설계" });
|
||||
const origRow = origKanjubRes.itemMap[code],
|
||||
origAmt = origRow ? origRow.amount : 0,
|
||||
origBase = origRow ? origRow.baseAmt : 0;
|
||||
const mode = revModes[code] || "recalculated";
|
||||
const finalAmt =
|
||||
mode === "carry_original"
|
||||
? origAmt
|
||||
: mode === "proportional"
|
||||
? origAmt + truncInd(origAmt * (origBase > 0 ? (baseAmt - origBase) / origBase : 0))
|
||||
: mode === "delta_rate"
|
||||
? origAmt + truncInd(((baseAmt - origBase) * ratePct) / 100)
|
||||
: truncAmt;
|
||||
return Object.assign(base, {
|
||||
amount: finalAmt,
|
||||
origAmt,
|
||||
deltaAmt: finalAmt - origAmt,
|
||||
modeUsed: mode,
|
||||
});
|
||||
}
|
||||
|
||||
const items = [],
|
||||
itemMap = {};
|
||||
const pushItem = (row) => (items.push(row), (itemMap[row.code] = row), row.amount);
|
||||
const pushRate = (code, label, desc, base, defR) => {
|
||||
const r = num(rates[code], defR);
|
||||
return pushItem(resolveRow(code, label, desc, base, r, (base * r) / 100));
|
||||
};
|
||||
|
||||
const kanno = pushRate("KANNO", "간접노무비", "직접노무비", dL, 14.5);
|
||||
const totalLabor = dL + kanno;
|
||||
pushRate("SANBO", "산재보험료", "노무비(직노+간노)", totalLabor, 3.56);
|
||||
pushRate("KOBO", "고용보험료", "노무비(직노+간노)", totalLabor, 1.01);
|
||||
const kunbo = pushRate("KUNBO", "건강보험료", "직접노무비", dL, 3.545);
|
||||
pushRate("SLVBO", "노인장기요양보험료", "건강보험료(확정액)", kunbo, 12.95);
|
||||
pushRate("YUNBO", "연금보험료", "직접노무비", dL, 4.5);
|
||||
pushRate("TBUGUM", "퇴직공제부금비", "직접노무비", dL, 0);
|
||||
|
||||
const rAnjun = num(rates.ANJUN, 1.86),
|
||||
anjunBaseAdd = num(cfg.anjunBaseAdd, 0);
|
||||
const anjunGovDiv = cfg.anjunGovVatExcluded !== false ? govPureMat / 1.1 : govPureMat;
|
||||
const anjunBaseNoGov = scheme === "TA" || scheme === "SA" ? dT : dM + dL,
|
||||
anjunBaseWithGov = anjunBaseNoGov + anjunGovDiv;
|
||||
const anjunNormal = truncInd((anjunBaseWithGov * rAnjun) / 100) + anjunBaseAdd;
|
||||
const anjunCap12 = truncInd(((anjunBaseNoGov * rAnjun) / 100 + anjunBaseAdd) * 1.2);
|
||||
// 변경 차수 자동(I_ANJUNAUTO=2) = 당초액 + 당초액 × 밑수 증감율(지식DB 11 §10 방법 A)
|
||||
const o = origKanjubRes;
|
||||
const anjunChange =
|
||||
cfg.anjunAuto === 2 && o && o.anjunBaseRaw > 0
|
||||
? o.itemMap.ANJUN.amount +
|
||||
truncInd((o.itemMap.ANJUN.amount * (anjunBaseWithGov - o.anjunBaseRaw)) / o.anjunBaseRaw)
|
||||
: null;
|
||||
const anjunDesc =
|
||||
scheme === "TA" || scheme === "SA"
|
||||
? "직접공사비+관급원자재/1.1 (+기초액)"
|
||||
: "직재+직노+관급원자재/1.1 (+기초액)";
|
||||
pushItem(
|
||||
resolveRow(
|
||||
"ANJUN",
|
||||
"산업안전보건관리비",
|
||||
anjunDesc,
|
||||
roundHalfUp(anjunBaseWithGov, 0),
|
||||
rAnjun,
|
||||
fixed.ANJUN != null
|
||||
? fixed.ANJUN
|
||||
: anjunChange != null
|
||||
? anjunChange
|
||||
: cfg.anjunCap12 !== false
|
||||
? Math.min(anjunNormal, anjunCap12)
|
||||
: anjunNormal,
|
||||
),
|
||||
);
|
||||
|
||||
if (scheme === "S" || scheme === "SA") {
|
||||
const suBase = dM + totalLabor;
|
||||
[
|
||||
["S_SUDO", "수도광열비", 0.45],
|
||||
["S_BOKRI", "복리후생비", 1.15],
|
||||
["S_SOMO", "소모품·사무용품비", 0.65],
|
||||
["S_TONGSIN", "여비·교통·통신비", 0.85],
|
||||
["S_SEGUM", "세금·공과금", 0.55],
|
||||
["S_PEGI", "폐기물처리비", 0.35],
|
||||
["S_DOSEO", "도서인쇄비", 0.25],
|
||||
["S_JIGUP", "지급수수료", 0.45],
|
||||
].forEach(([sc, sl, defR]) =>
|
||||
pushRate(sc, `[수공] ${sl}`, "재료비+노무비(직노+간노)", suBase, defR),
|
||||
);
|
||||
} else {
|
||||
pushRate(
|
||||
"KITA",
|
||||
"기타경비",
|
||||
scheme === "TA" ? "직접공사비(실적일반)" : "재료비+노무비(직노+간노)",
|
||||
scheme === "TA" ? dT : dM + totalLabor,
|
||||
6.0,
|
||||
);
|
||||
}
|
||||
|
||||
const hwanBase = scheme === "TA" ? dT : dM + dL + dS,
|
||||
hwanDesc = scheme === "TA" ? "직접공사비(실적일반)" : "재료비+직접노무비+산출경비";
|
||||
pushRate("HWAN", "환경보전비", hwanDesc, hwanBase, 0.8);
|
||||
pushRate("EQIP", "건설기계대여대금 지급보증", hwanDesc, hwanBase, 0.4);
|
||||
|
||||
const indirectExpenseSum = items
|
||||
.filter((x) => x.code !== "KANNO")
|
||||
.reduce((acc, x) => acc + x.amount, 0);
|
||||
const add1 = addSum(1),
|
||||
totalExpense = dS + indirectExpenseSum + add1,
|
||||
pureConstructionCost = dM + totalLabor + totalExpense;
|
||||
|
||||
const rIlban = num(rates.ILBAN, 6.0),
|
||||
ilbanBase = scheme === "SA" ? dT + kanno : pureConstructionCost;
|
||||
const ilbanDesc = scheme === "SA" ? "직접공사비+간접노무비(실적수공)" : "순공사원가(재+노+경)",
|
||||
ilbanAmt = fixed.ILBAN != null ? fixed.ILBAN : truncInd((ilbanBase * rIlban) / 100);
|
||||
|
||||
const rIyun = num(rates.IYUN, 15.0);
|
||||
const iyunBase =
|
||||
scheme === "TA" || scheme === "SA"
|
||||
? pureConstructionCost + ilbanAmt
|
||||
: totalLabor + totalExpense + ilbanAmt;
|
||||
const iyunDesc =
|
||||
scheme === "TA" || scheme === "SA"
|
||||
? "직접비+간접비+일반관리비(실적형식)"
|
||||
: "노무비+경비+일반관리비(재료비 제외)";
|
||||
const iyunRaw = truncInd((iyunBase * rIyun) / 100);
|
||||
|
||||
const vatMode = cfg.vatMode || "supply_10",
|
||||
totJeolDigits = num(cfg.totJeolDigits, -3),
|
||||
add2 = addSum(2),
|
||||
add3 = addSum(3),
|
||||
add4 = addSum(4);
|
||||
// 부가세(I_BUGA · REA 20) — 0 공급가액 · 1 재료비 · 2 없음 · 3 산림조합(직접비−노무비+부가세 붙는 추가금액)
|
||||
// · 4 산림조합 면세품(3 에서 면세품 C_BUGA_M 뺌) · 올림 표시(B_BUGA_UP)면 +1원
|
||||
const vatUp = cfg.vatRoundUp ? 1 : 0,
|
||||
addVat = adds
|
||||
.filter((a) => a.stage === 2 && a.vat)
|
||||
.reduce((acc, a) => acc + num(a.amount), 0),
|
||||
forestBase = dT - dL + addVat - (vatMode === "forest_exempt" ? num(cfg.vatExempt) : 0);
|
||||
const calcVat = (sVal) =>
|
||||
vatMode === "zero"
|
||||
? 0
|
||||
: vatMode === "mat_10"
|
||||
? roundDown(dM * 0.1, 0) + vatUp
|
||||
: vatMode === "forest" || vatMode === "forest_exempt"
|
||||
? roundDown(forestBase * 0.1, 0) + vatUp
|
||||
: roundDown(sVal * 0.1, 0) + vatUp;
|
||||
|
||||
const govFinal =
|
||||
govMat > 0
|
||||
? applyRoundMode(govMat, cfg.govRoundMode || "up", num(cfg.govRoundDigits, -3))
|
||||
: 0;
|
||||
const iyunManual = num(cfg.iyunManualAdj, 0),
|
||||
supplyBeforeAdj = pureConstructionCost + ilbanAmt + iyunRaw - iyunManual + add2,
|
||||
vatBeforeAdj = calcVat(supplyBeforeAdj),
|
||||
contractBeforeAdj = supplyBeforeAdj + vatBeforeAdj + add3;
|
||||
let contractTarget = contractBeforeAdj,
|
||||
diffTotal = 0,
|
||||
pretaxProfitAdj = iyunManual,
|
||||
iyunFinal = iyunRaw - iyunManual,
|
||||
supplyFinal = supplyBeforeAdj,
|
||||
vatFinal = vatBeforeAdj;
|
||||
|
||||
if (totJeolDigits < 0 && cfg.enableProfitEndAdj !== false) {
|
||||
// 총액 절사 = 총공사비(도급 + 관급 + 단계6 추가) 기준 · 차액은 도급에서 뺌
|
||||
contractTarget =
|
||||
roundDown(contractBeforeAdj + govFinal + add4, totJeolDigits) - govFinal - add4;
|
||||
diffTotal = contractBeforeAdj - contractTarget;
|
||||
pretaxProfitAdj = vatMode === "supply_10" ? roundHalfUp(diffTotal / 1.1, 0) : diffTotal;
|
||||
iyunFinal = iyunRaw - pretaxProfitAdj;
|
||||
supplyFinal = pureConstructionCost + ilbanAmt + iyunFinal + add2;
|
||||
vatFinal = calcVat(supplyFinal);
|
||||
if (supplyFinal + vatFinal + add3 !== contractTarget)
|
||||
vatFinal = contractTarget - supplyFinal - add3;
|
||||
}
|
||||
const contractFinal = supplyFinal + vatFinal + add3;
|
||||
const grandTotal = contractFinal + govFinal + add4;
|
||||
const mkLine = (key, label, formula, baseAmt, rate, amount) =>
|
||||
Object.fromEntries([
|
||||
["key", key],
|
||||
["label", label],
|
||||
["formula", formula],
|
||||
["baseAmt", baseAmt],
|
||||
["base", baseAmt],
|
||||
["rate", rate],
|
||||
["amount", amount],
|
||||
]);
|
||||
const totStr = "재료비 + 노무비 + 경비",
|
||||
iyunStr = `${iyunDesc} (보정 -${fmt(pretaxProfitAdj)})`,
|
||||
supStr = "순공사원가 + 일반관리비 + 이윤",
|
||||
finStr = "공급가액 + 부가가치세 (천원절사)",
|
||||
govStr = `관급원자재(${fmt(govPureMat)}) + 수수료(${fmt(govFee)})`;
|
||||
const pCost = pureConstructionCost;
|
||||
const lines = [
|
||||
mkLine("DIRECT_M", "직접재료비", "내역서 재료비 합계", dM, null, dM),
|
||||
mkLine("DIRECT_L", "직접노무비", "내역서 직접노무비 합계", dL, null, dL),
|
||||
mkLine("DIRECT_S", "산출경비", "내역서 산출경비 합계", dS, null, dS),
|
||||
mkLine("DIRECT_COST", "직접공사비 계", "직재 + 직노 + 산출경비", dT, null, dT),
|
||||
...items.map((it) => mkLine(it.key, it.label, it.formula, it.baseAmt, it.rate, it.amount)),
|
||||
mkLine("TOTAL_COST", "순공사원가 계", totStr, pCost, null, pCost),
|
||||
mkLine("ILBAN", "일반관리비", ilbanDesc, ilbanBase, rIlban, ilbanAmt),
|
||||
mkLine("IYUN", "이윤 (끝전보정 후)", iyunStr, iyunBase, rIyun, iyunFinal),
|
||||
mkLine("SUPPLY_VAL", "총원가 (공급가액)", supStr, supplyFinal, null, supplyFinal),
|
||||
mkLine("VAT", "부가가치세", "공급가액 × 10%", supplyFinal, 10, vatFinal),
|
||||
mkLine("FINAL_SUPPLY", "도급공사비", finStr, contractBeforeAdj, null, contractFinal),
|
||||
mkLine("GOV_MAT", "관급자재대", govStr, govMat, null, govFinal),
|
||||
mkLine("GRAND_TOTAL", "총공사비 (COSTW1)", "도급 + 관급", grandTotal, null, grandTotal),
|
||||
];
|
||||
|
||||
const out = { scheme, dM, dL, dS, dT, kanno, totalLabor };
|
||||
Object.assign(out, { indirectExpenseSum, totalExpense, pureConstructionCost: pCost });
|
||||
Object.assign(out, { ilbanBase, ilbanDesc, ilbanAmt, iyunBase, iyunDesc, iyunRaw });
|
||||
Object.assign(out, { pretaxProfitAdj, iyunFinal, supplyBeforeAdj, vatBeforeAdj });
|
||||
Object.assign(out, { contractBeforeAdj, diffTotal, supplyFinal, vatFinal, contractFinal });
|
||||
Object.assign(out, { govRawTotal: govPureMat, govFeeTotal: govFee, adds });
|
||||
out.anjunBaseRaw = anjunBaseWithGov;
|
||||
Object.assign(out, { govFinal, grandTotal, items, itemMap, lines });
|
||||
Object.assign(out, { DIRECT_COST: dT, KANNO: kanno, IND_EXP: kanno + indirectExpenseSum });
|
||||
Object.assign(out, { TOTAL_COST: pCost, ILBAN: ilbanAmt, IYUN_INIT: iyunRaw });
|
||||
Object.assign(out, { PROFIT_ADJ: -pretaxProfitAdj, IYUN: iyunFinal, SUPPLY_VAL: supplyFinal });
|
||||
return Object.assign(out, {
|
||||
VAT: vatFinal,
|
||||
FINAL_SUPPLY: contractFinal,
|
||||
GOV_MAT: govFinal,
|
||||
GRAND_TOTAL: grandTotal,
|
||||
});
|
||||
}
|
||||
|
||||
return calcKanjub;
|
||||
};
|
||||
|
||||
if (typeof window !== "undefined") window.STMATE_KANJUB_FACTORY = StmateKanjubFactory;
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = StmateKanjubFactory;
|
||||
@@ -52,8 +52,25 @@ def _num(kv: dict, key: str, default: float = 0.0) -> float:
|
||||
return default
|
||||
|
||||
|
||||
def add_amounts(kv: dict) -> list[dict]:
|
||||
"""ADD_AMT_<단계>_<행>_<칸> — 저장 단계 1~4 = 화면 단계 3~6(일반관리비 앞 · 이윤 뒤 · 도급 앞 · 총공사비 앞)."""
|
||||
# D_ 표시(=1) 항목은 저장 금액(C_) 고정 — 변경 차수에서 당초액 유지(건강·장기요양·연금·퇴직공제 등)
|
||||
FIXED_KEYS = {
|
||||
"KANNO": "KANNO",
|
||||
"SANBO": "SANBO",
|
||||
"KOBO": "KOBO",
|
||||
"KUNBO": "KUNBO",
|
||||
"SLVBO": "SLVBO",
|
||||
"YUNBO": "YUNBO",
|
||||
"TBUGUM": "TBUGUM",
|
||||
"ANJUN": "ANJUN",
|
||||
"KITA": "T_KITA",
|
||||
"HWAN": "HWAN",
|
||||
"EQIP": "EQIPSU",
|
||||
"ILBAN": "ILBAN",
|
||||
}
|
||||
|
||||
|
||||
def add_amounts(kv: dict, slot: int = 1) -> list[dict]:
|
||||
"""ADD_AMT_<단계>_<행>_<차수> — 단계 1~4 = 화면 단계 3~6(일반관리비 앞 · 이윤 뒤 · 도급 앞 · 총공사비 앞)."""
|
||||
out = []
|
||||
for stage in range(1, 5):
|
||||
for i in range(1, int(_num(kv, f"ADD_CNT_{stage}")) + 1):
|
||||
@@ -62,36 +79,51 @@ def add_amounts(kv: dict) -> list[dict]:
|
||||
{
|
||||
"stage": stage,
|
||||
"name": kv.get(f"ADD_NAME_{row}", ""),
|
||||
"amount": _num(kv, f"ADD_AMT_{row}_1"),
|
||||
"amount": _num(kv, f"ADD_AMT_{row}_{slot}"),
|
||||
"vat": _num(kv, f"ADD_BUGA_{row}") == 1,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def build_kanjub_config(kv: dict) -> dict:
|
||||
VAT_MODES = {0: "supply_10", 1: "mat_10", 2: "zero", 3: "forest", 4: "forest_exempt"}
|
||||
|
||||
|
||||
def stage_slots(kv: dict) -> list[int]:
|
||||
"""원가계산서가 저장된 차수 — [n_KANJUB] 묶음이 있는 차수."""
|
||||
return [n for n in range(1, 5) if any(k.startswith(f"{n}_C_") for k in kv)] or [1]
|
||||
|
||||
|
||||
def build_kanjub_config(kv: dict, slot: int = 1) -> dict:
|
||||
jab_type = int(_num(kv, "JAB_TYPE"))
|
||||
cal_actl = int(_num(kv, "CAL_ACTL"))
|
||||
scheme = ("SA" if jab_type == 1 else "TA") if cal_actl == 1 else ("S" if jab_type == 1 else "T")
|
||||
tot_jeol = int(_num(kv, "1_I_TOT_JEOL"))
|
||||
p = f"{slot}_"
|
||||
tot_jeol = int(_num(kv, p + "I_TOT_JEOL"))
|
||||
return {
|
||||
"slot": slot,
|
||||
"scheme": scheme,
|
||||
"indirectTruncDigits": 0,
|
||||
"anjunBaseAdd": _num(kv, "1_C_ANJUN_G"),
|
||||
"anjunBaseAdd": _num(kv, p + "C_ANJUN_G"),
|
||||
"anjunGovVatExcluded": True,
|
||||
"anjunCap12": True,
|
||||
"vatMode": "supply_10",
|
||||
"vatMode": VAT_MODES.get(int(_num(kv, p + "I_BUGA")), "zero"),
|
||||
"vatExempt": _num(kv, p + "C_BUGA_M"),
|
||||
"vatRoundUp": _num(kv, p + "B_BUGA_UP") == 1,
|
||||
"anjunAuto": int(_num(kv, p + "I_ANJUNAUTO")),
|
||||
"totJeolDigits": -tot_jeol,
|
||||
"enableProfitEndAdj": tot_jeol > 0,
|
||||
"iyunManualAdj": 0 if tot_jeol > 0 else _num(kv, "1_C_IYUN_M"),
|
||||
"govMat": _num(kv, "1_C_KWANGUP") if "1_C_KWANGUP" in kv else None,
|
||||
"govFee": _num(kv, "1_C_T_JODAL") if "1_C_T_JODAL" in kv else None,
|
||||
"iyunManualAdj": 0 if tot_jeol > 0 else _num(kv, p + "C_IYUN_M"),
|
||||
"govMat": _num(kv, p + "C_KWANGUP") if p + "C_KWANGUP" in kv else None,
|
||||
"govFee": _num(kv, p + "C_T_JODAL") if p + "C_T_JODAL" in kv else None,
|
||||
"govRoundMode": "up",
|
||||
"govRoundDigits": -int(_num(kv, "1_I_KWN_JEOLSA", 3)),
|
||||
"addAmounts": add_amounts(kv),
|
||||
"rates": {k: _num(kv, v) for k, v in RATE_KEYS.items()},
|
||||
"revModes": {
|
||||
k: ("carry_original" if k in ("KUNBO", "SLVBO", "YUNBO") else "recalculated")
|
||||
for k in REV_KEYS
|
||||
"govRoundDigits": -int(_num(kv, p + "I_KWN_JEOLSA", 3)),
|
||||
"addAmounts": add_amounts(kv, slot),
|
||||
"rates": {k: _num(kv, v.replace("1_", p, 1)) for k, v in RATE_KEYS.items()},
|
||||
"fixed": {
|
||||
code: _num(kv, p + "C_" + key)
|
||||
for code, key in FIXED_KEYS.items()
|
||||
if _num(kv, p + "D_" + key) == 1
|
||||
},
|
||||
"revModes": {k: "recalculated" for k in REV_KEYS},
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ from pydantic import BaseModel
|
||||
|
||||
from bdqty import decode_bdqty
|
||||
from excel_builder import build_stmate_full_xlsx
|
||||
from kanjub_cfg import build_kanjub_config
|
||||
from kanjub_cfg import build_kanjub_config, stage_slots
|
||||
from smt_parser import (
|
||||
build_dbf_bytes,
|
||||
extract_costn_memos,
|
||||
@@ -329,6 +329,14 @@ def convert_dbf_to_sim_state(
|
||||
"rowType": row_type,
|
||||
"qty": q1,
|
||||
"changedQty": q2 if q2 > 0 else q1,
|
||||
"qtys": [1.0] * 4 if is_pct_fee else [fnum(r.get(f"QTY{n}")) for n in range(1, 5)],
|
||||
"units": {
|
||||
n: [fnum(r.get(f"{c}_AMT{n}" if is_pct_fee else f"{c}AMT{n}")) for c in "MLS"]
|
||||
for n in range(2, 5)
|
||||
if any(
|
||||
fnum(r.get(f"{c}_AMT{n}" if is_pct_fee else f"{c}AMT{n}")) for c in "MLS"
|
||||
)
|
||||
},
|
||||
"_recno": r.get("_recno", i),
|
||||
}
|
||||
)
|
||||
@@ -365,6 +373,7 @@ def convert_dbf_to_sim_state(
|
||||
"ilwiItems": ilwi_items,
|
||||
"boqItems": boq_items,
|
||||
"kanjubConfig": build_kanjub_config(smt_kv),
|
||||
"kanjubStages": {n: build_kanjub_config(smt_kv, n) for n in stage_slots(smt_kv)},
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user