/* ============================================================================= * 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 = {}; 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 = {}; 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 = { 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 = {}; 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, }; }