- 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
149 lines
6.9 KiB
TypeScript
149 lines
6.9 KiB
TypeScript
/* =============================================================================
|
||
* 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,
|
||
};
|
||
}
|