Files
Aislo/B09_Estimation/B09_Estimation_Calc_Total.ts
T
eomsangdonandClaude Opus 5.5 8013fd901b feat(B09): 총괄설계내역서 · 공사원가계산서 탭 · 제비율 셈 파이썬 · TS 짝(145-4)
- compute_total(sums, estimate) · computeTotal — 설계내역서 비목 합(bill.sums) + 사용자 요율 · 손입력
- 끝수 엑셀 그대로(줄마다 ROUNDDOWN 0 · 관급 ROUNDUP −3 · 이윤 − 보정 · 부가세 + 조정) · 안전관리비 A/B 작은 쪽
- 요율 빈칸 시작 · 빈칸이면 그 줄 · 소계 빈칸 · 후보 = M01 요율_조달청제비율 글(확정 아님)
- 엑셀에 없는 현행 비목 다섯 = 줄만 · 원가계산서 = 총괄 거울 + 구성비 · 줄 누름 = 총괄 그 줄
- 엑셀 검산(순공사원가 868,874,829 · 총공사비 1,201,879,000) · 거울 시험

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
2026-10-09 15:12:40 +09:00

149 lines
6.9 KiB
TypeScript
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.
/* =============================================================================
* B09_Estimation_Calc_Total.ts
* B09 총괄설계내역서 · 공사원가계산서 셈(PLAN 145-4) — 화면 짝.
*
* ⚠ 짝 = `B09_Estimation_Calc_Total.py`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 ·
* 거울 시험 `resources/tester/test_145_4_b09_total_calc.py`.
* 조작 중은 이 파일이 바로 셈 · [저장] 때 서버가 파이썬 짝으로 정본을 다시 셈.
*
* 입력 sums = 설계내역서 비목 합(145-3 `sheets.bill.sums`) · estimate = 사용자 값(`user.estimate`)
* 식 · 끝수 = 엑셀 총괄 49~67 그대로 · 빈칸 규칙 = 파이썬 짝 머리 글 그대로.
* ========================================================================== */
import { num, rounddown, roundup, total, type Row } from "./B09_Estimation_Calc_Base";
type Num = number | null;
/** 제비율 줄 [키, 이름, M01 요율 후보 키, 산식 글] — 엑셀 총괄 49~58 차례 · 나.소계에 듦 */
export const RATE_LINES: Array<[string, string, string, string]> = [
["indirect_labor", "간접노무비", "RT000008", "직접노무비 × 율"],
["accident", "산재보험료", "RT000001", "(직접노무비 + 간접노무비) × 율"],
["employment", "고용보험료", "RT000002", "(직접노무비 + 간접노무비) × 율"],
["health", "건강보험료", "RT000003", "직접노무비 × 율"],
["care", "노인장기요양보험료", "RT000005", "건강보험료 × 율"],
["pension", "연금보험료", "RT000004", "직접노무비 × 율"],
["safety", "산업안전보건관리비", "RT000006", "A · B 중 작은 쪽 + 기초액"],
["other", "기타경비", "RT000009", "(직접노무비 + 간접노무비 + 재료비) × 율"],
["environment", "환경보전비", "RT000010", "순공사비 × 율"],
["machine", "건설기계대여금지급보증", "RT000015", "순공사비 × 율"],
];
/** 엑셀에 없는 현행 비목 — 줄만 두고 빈칸 */
export const CURRENT_ONLY: Array<[string, string, string]> = [
["retirement", "퇴직공제부금비", "RT000011"],
["wage_claim", "임금채권보장분담금", "RT000012"],
["asbestos", "석면피해구제분담금", "RT000013"],
["subcontract", "하도급대금지급보증수수료", "RT000014"],
["performance", "공사이행보증수수료", "RT000016"],
];
/** 소계 아래 줄 */
export const TAIL_LINES: Array<[string, string, string, string]> = [
["overhead", "일반관리비", "RT000017", "나.소계 × 율"],
["profit", "이윤", "RT000018", "(다.소계 − 재료비) × 율 − 보정"],
["vat", "부가가치세", "RT000019", "공급가액 × 율 + 조정"],
];
const SAFETY_VAT_DIVISOR = 1.1;
const SAFETY_B = 1.2;
/** ROUNDDOWN(밑수 × 율 ÷ 100, 0) — 둘 중 빈칸이면 빈칸 */
const pct = (base: Num, rate: Num): Num =>
base === null || rate === null ? null : rounddown((base * rate) / 100);
/** 조달수수료 = ROUNDDOWN(관급 순자재대 × 율 ÷ 100, 0) */
export const procurementFee = (net: Num, rate: Num): Num => pct(net, rate);
export function computeTotal(sums: Row | null | undefined, estimate: Row | null | undefined): Row {
const s: Row = sums && typeof sums === "object" ? sums : {};
const est: Row = estimate && typeof estimate === "object" ? estimate : {};
const ratesIn: Row = est.rates && typeof est.rates === "object" ? est.rates : {};
const rate: Record<string, Num> = {};
for (const [key] of [...RATE_LINES, ...TAIL_LINES]) rate[key] = num(ratesIn[key]);
const hand = (key: string): number => num(est[key]) || 0;
const material = num(s.material);
const labor = num(s.labor);
const expense = num(s.expense);
const supplyNet = num(s.supply_net);
const direct = total([material, labor, expense]);
const line: Record<string, Num> = {};
line.indirect_labor = pct(labor, rate.indirect_labor);
const totalLabor = total([labor, line.indirect_labor]);
line.accident = pct(totalLabor, rate.accident);
line.employment = pct(totalLabor, rate.employment);
line.health = pct(labor, rate.health);
line.care = pct(line.health, rate.care);
line.pension = pct(labor, rate.pension);
let safetyA: Num = null;
let safetyB: Num = null;
const laborMaterial = total([labor, material]);
if (laborMaterial !== null && rate.safety !== null) {
const base = hand("safety_base");
const target = laborMaterial + (supplyNet ?? 0) / SAFETY_VAT_DIVISOR;
safetyA = rounddown((target * rate.safety) / 100 + base);
safetyB = rounddown(((laborMaterial * rate.safety) / 100 + base) * SAFETY_B);
}
line.safety = safetyA === null || safetyB === null ? null : Math.min(safetyA, safetyB);
line.other = pct(total([labor, line.indirect_labor, material]), rate.other);
line.environment = pct(direct, rate.environment);
line.machine = pct(direct, rate.machine);
const netCost = total([direct, ...RATE_LINES.map(([key]) => line[key])]);
line.overhead = pct(netCost, rate.overhead);
const da = total([netCost, line.overhead]);
const profitRaw = pct(da === null || material === null ? null : da - material, rate.profit);
line.profit = profitRaw === null ? null : profitRaw - hand("profit_adjust");
const supplyPrice = total([da, line.profit]);
const vat = pct(supplyPrice, rate.vat);
line.vat = vat === null ? null : vat + hand("vat_adjust");
const contract = total([supplyPrice, line.vat]);
const fee = supplyNet === 0 ? 0 : procurementFee(supplyNet, num(est.procurement_fee_pct));
const supplyRaw = total([supplyNet, fee]);
line.supply = supplyRaw === null ? null : roundup(supplyRaw, -3);
const grand = total([contract, line.supply]);
const indirectMaterial = hand("indirect_material");
const scrap = hand("scrap");
const cost: Record<string, Num> = {
direct_material: material === null ? null : material - indirectMaterial + scrap,
indirect_material: indirectMaterial,
scrap,
material,
direct_labor: labor,
indirect_labor: line.indirect_labor,
labor: totalLabor,
expense,
accident: line.accident,
employment: line.employment,
health: line.health,
care: line.care,
pension: line.pension,
safety: line.safety,
other: line.other,
environment: line.environment,
machine: line.machine,
expense_sum: total([
expense,
...RATE_LINES.filter(([key]) => key !== "indirect_labor").map(([key]) => line[key]),
]),
overhead: line.overhead,
profit: line.profit,
supply_price: supplyPrice,
vat: line.vat,
contract,
supply: line.supply,
grand,
};
const share: Record<string, Num> = {};
for (const [key, value] of Object.entries(cost))
share[key] = value === null || !grand ? null : (value / grand) * 100;
return {
sums: { material, labor, expense, direct },
lines: line,
subtotals: { net_cost: netCost, da, supply_price: supplyPrice, contract, grand },
safety: { a: safetyA, b: safetyB },
profit_raw: profitRaw,
supply: { net: supplyNet, fee, raw: supplyRaw },
cost,
share,
};
}