feat(B09 원가계산서 · 708-2): 현행 비목 다섯 셈 — 퇴직공제 · 임금채권 · 석면 · 하도급 요율 셈 · 공사이행보증 금액 손입력
- 셈 짝(Calc_Total py ↔ ts) — STmate 일반형식 식 · 자리(지식DB 12 · 35 분석) · 경비 묶음 안 · 나.소계에 듦 퇴직공제부금비 = 직접노무비 × 율 · 임금채권 · 석면 = (직노 + 간노) × 율 · 하도급 = 순공사비 × 율 · 공사이행보증 = 금액 직접 입력(performance_fee) · 자동 요율 없음 · 요율 빈칸 = 해당 없음(0) - 요율 출처 「불러옴」 = rates_imported(707-2 불러오기가 실음 · 요율 고치면 사용자) - 707-2 불러오기 — 「셈 없음」 알림 뺌 · 공사이행보증 줄 금액 = 손입력 칸 - 총괄 · 원가계산서 탭 · 인쇄 미리보기 = 같은 줄 짜기 - 낡은 시험 고침 — 현행 비목 기대값 빈칸 → 0 · 퇴직공제 요율 칸 고침 가능 · 거창 총공사비 = 엑셀 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,14 @@
|
||||
산업안전보건관리비 = A(관급 순자재대 ÷ 1.1 포함) · B(관급 빼고 × 1.2) 중 작은 쪽 ·
|
||||
이윤 = ROUNDDOWN((다 − 재료비) × 율, 0) − 보정 · 부가세 = ROUNDDOWN(공급가액 × 율, 0) + 조정.
|
||||
빈칸 규칙 = 요율(사용자 · 자동 둘 다 없음) · 비목 합이 빈칸이면 그 줄 빈칸(0 을 채우지 않음) · 그 줄을 쓰는 소계도 빈칸 ·
|
||||
손입력(기초액 · 보정 · 조정 · 간접재료비 · 작업설) 빈칸 = 0 · 엑셀에 없는 현행 비목 = 줄만(늘 빈칸).
|
||||
손입력(기초액 · 보정 · 조정 · 간접재료비 · 작업설) 빈칸 = 0.
|
||||
현행 비목 다섯(PLAN 708-2 · 엑셀 총괄에 없음 · 경쟁사 둘 다 셈) = STmate 일반형식 식 · 자리
|
||||
(지식DB `STmate 분석/20_분석/12_원가계산_4형식과_RATE표_복원.md` 일반 · 수공 경비 차례 ·
|
||||
`35_형식과_단계의_공통과_차이.md` 밑수 표) — 경비 묶음 안 · 나.소계(순공사원가)에 듦 ·
|
||||
퇴직공제부금비 = 직접노무비 × 율 · 임금채권 · 석면 = (직접노무비 + 간접노무비) × 율 ·
|
||||
하도급대금지급보증수수료 = 순공사비 × 율 · 공사이행보증수수료 = 금액 손입력(`performance_fee` ·
|
||||
STmate 식 = 공종 · 기간 표라 뒤로) · 자동 요율 없음 · 요율 빈칸 = 해당 없음(0 · 빈칸 아님).
|
||||
요율 출처 = 사용자 값 중 `rates_imported` 키 = 「import」(707-2 불러옴).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -29,7 +36,8 @@ from typing import Any
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import num, rounddown, roundup, total
|
||||
from common_util.common_util_time import kst_today
|
||||
|
||||
#: 제비율 줄 (키, 이름, M01 요율 후보 키, 산식 글) — 엑셀 총괄 49~58 차례 · 나.소계에 듦
|
||||
#: 제비율 줄 (키, 이름, M01 요율 후보 키, 산식 글) — 엑셀 총괄 49~58 차례 +
|
||||
#: 현행 비목(STmate 경비 차례) · 나.소계에 듦
|
||||
RATE_LINES: tuple[tuple[str, str, str, str], ...] = (
|
||||
("indirect_labor", "간접노무비", "RT000008", "직접노무비 × 율"),
|
||||
("accident", "산재보험료", "RT000001", "(직접노무비 + 간접노무비) × 율"),
|
||||
@@ -37,19 +45,18 @@ RATE_LINES: tuple[tuple[str, str, str, str], ...] = (
|
||||
("health", "건강보험료", "RT000003", "직접노무비 × 율"),
|
||||
("care", "노인장기요양보험료", "RT000005", "건강보험료 × 율"),
|
||||
("pension", "연금보험료", "RT000004", "직접노무비 × 율"),
|
||||
("retirement", "퇴직공제부금비", "RT000011", "직접노무비 × 율"),
|
||||
("safety", "산업안전보건관리비", "RT000006", "A · B 중 작은 쪽 + 기초액"),
|
||||
("other", "기타경비", "RT000009", "(직접노무비 + 간접노무비 + 재료비) × 율"),
|
||||
("performance", "공사이행보증수수료", "RT000016", "금액 직접 입력"),
|
||||
("subcontract", "하도급대금지급보증수수료", "RT000014", "순공사비 × 율"),
|
||||
("environment", "환경보전비", "RT000010", "순공사비 × 율"),
|
||||
("machine", "건설기계대여금지급보증", "RT000015", "순공사비 × 율"),
|
||||
("asbestos", "석면피해구제분담금", "RT000013", "(직접노무비 + 간접노무비) × 율"),
|
||||
("wage_claim", "임금채권보장분담금", "RT000012", "(직접노무비 + 간접노무비) × 율"),
|
||||
)
|
||||
#: 엑셀에 없는 현행 비목 — 줄만 두고 빈칸(밑수 · 고르는 값 미정 · 나.소계에 안 듦)
|
||||
CURRENT_ONLY: tuple[tuple[str, str, str], ...] = (
|
||||
("retirement", "퇴직공제부금비", "RT000011"),
|
||||
("wage_claim", "임금채권보장분담금", "RT000012"),
|
||||
("asbestos", "석면피해구제분담금", "RT000013"),
|
||||
("subcontract", "하도급대금지급보증수수료", "RT000014"),
|
||||
("performance", "공사이행보증수수료", "RT000016"),
|
||||
)
|
||||
#: 현행 비목 — 요율 빈칸 = 해당 없음(0) · 공사이행보증 = 금액 손입력(율 칸 없음)
|
||||
CURRENT_ONLY = ("retirement", "subcontract", "asbestos", "wage_claim", "performance")
|
||||
#: 소계 아래 줄 (키, 이름, 후보 키, 산식 글)
|
||||
TAIL_LINES: tuple[tuple[str, str, str, str], ...] = (
|
||||
("overhead", "일반관리비", "RT000017", "나.소계 × 율"),
|
||||
@@ -135,6 +142,7 @@ def compute_total(
|
||||
sums = sums if isinstance(sums, dict) else {}
|
||||
est = estimate if isinstance(estimate, dict) else {}
|
||||
rates_in = est.get("rates") if isinstance(est.get("rates"), dict) else {}
|
||||
imported = est.get("rates_imported") if isinstance(est.get("rates_imported"), list) else []
|
||||
|
||||
def hand(key: str) -> float:
|
||||
return num(est.get(key)) or 0.0
|
||||
@@ -164,7 +172,7 @@ def compute_total(
|
||||
mine, got = num(rates_in.get(key)), auto.get(key) or {}
|
||||
rates[key] = {
|
||||
"rate": got.get("rate") if mine is None else mine,
|
||||
"from": "user"
|
||||
"from": ("import" if key in imported else "user")
|
||||
if mine is not None
|
||||
else ("auto" if got.get("rate") is not None else None),
|
||||
"auto": got.get("rate"),
|
||||
@@ -193,6 +201,14 @@ def compute_total(
|
||||
line["other"] = _pct(total([labor, line["indirect_labor"], material]), rate["other"])
|
||||
line["environment"] = _pct(direct, rate["environment"])
|
||||
line["machine"] = _pct(direct, rate["machine"])
|
||||
line["retirement"] = _pct(labor, rate["retirement"])
|
||||
line["subcontract"] = _pct(direct, rate["subcontract"])
|
||||
line["asbestos"] = _pct(total_labor, rate["asbestos"])
|
||||
line["wage_claim"] = _pct(total_labor, rate["wage_claim"])
|
||||
for key in CURRENT_ONLY: # 요율 빈칸 = 해당 없음
|
||||
if rate[key] is None:
|
||||
line[key] = 0.0
|
||||
line["performance"] = hand("performance_fee")
|
||||
net_cost = total([direct, *(line[key] for key, *_ in RATE_LINES)]) # 나. 순공사원가
|
||||
line["overhead"] = _pct(net_cost, rate["overhead"])
|
||||
da = total([net_cost, line["overhead"]]) # 다
|
||||
@@ -220,8 +236,7 @@ def compute_total(
|
||||
"indirect_labor": line["indirect_labor"],
|
||||
"labor": total_labor,
|
||||
"expense": expense,
|
||||
**{key: line[key] for key in ("accident", "employment", "health", "care", "pension")},
|
||||
**{key: line[key] for key in ("safety", "other", "environment", "machine")},
|
||||
**{key: line[key] for key, *_ in RATE_LINES[1:]},
|
||||
"expense_sum": total(
|
||||
[expense, *(line[key] for key, *_ in RATE_LINES if key != "indirect_labor")]
|
||||
),
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
* 입력 sums = 설계내역서 비목 합(145-3 `sheets.bill.sums`) · estimate = 사용자 값(`user.estimate`) ·
|
||||
* table = M01 요율 압축 표(payload `rate_table`) · year = 연금 해 — 자동 고르기(166-1) = 파이썬 짝 머리 글
|
||||
* 식 · 끝수 = 엑셀 총괄 49~67 그대로 · 빈칸 규칙 = 파이썬 짝 머리 글 그대로.
|
||||
* 현행 비목 다섯(708-2) = STmate 일반형식 식 · 자리(경비 묶음 안 · 나.소계에 듦) — 근거 = 파이썬 짝 머리 글.
|
||||
* ========================================================================== */
|
||||
|
||||
import { num, rounddown, roundup, total, type Row } from "./B09_Estimation_Calc_Base";
|
||||
|
||||
type Num = number | null;
|
||||
|
||||
/** 제비율 줄 [키, 이름, M01 요율 후보 키, 산식 글] — 엑셀 총괄 49~58 차례 · 나.소계에 듦 */
|
||||
/** 제비율 줄 [키, 이름, M01 요율 후보 키, 산식 글] — 엑셀 총괄 49~58 차례 + 현행 비목 · 나.소계에 듦 */
|
||||
export const RATE_LINES: Array<[string, string, string, string]> = [
|
||||
["indirect_labor", "간접노무비", "RT000008", "직접노무비 × 율"],
|
||||
["accident", "산재보험료", "RT000001", "(직접노무비 + 간접노무비) × 율"],
|
||||
@@ -23,19 +24,18 @@ export const RATE_LINES: Array<[string, string, string, string]> = [
|
||||
["health", "건강보험료", "RT000003", "직접노무비 × 율"],
|
||||
["care", "노인장기요양보험료", "RT000005", "건강보험료 × 율"],
|
||||
["pension", "연금보험료", "RT000004", "직접노무비 × 율"],
|
||||
["retirement", "퇴직공제부금비", "RT000011", "직접노무비 × 율"],
|
||||
["safety", "산업안전보건관리비", "RT000006", "A · B 중 작은 쪽 + 기초액"],
|
||||
["other", "기타경비", "RT000009", "(직접노무비 + 간접노무비 + 재료비) × 율"],
|
||||
["performance", "공사이행보증수수료", "RT000016", "금액 직접 입력"],
|
||||
["subcontract", "하도급대금지급보증수수료", "RT000014", "순공사비 × 율"],
|
||||
["environment", "환경보전비", "RT000010", "순공사비 × 율"],
|
||||
["machine", "건설기계대여금지급보증", "RT000015", "순공사비 × 율"],
|
||||
["asbestos", "석면피해구제분담금", "RT000013", "(직접노무비 + 간접노무비) × 율"],
|
||||
["wage_claim", "임금채권보장분담금", "RT000012", "(직접노무비 + 간접노무비) × 율"],
|
||||
];
|
||||
/** 엑셀에 없는 현행 비목 — 줄만 두고 빈칸 */
|
||||
export const CURRENT_ONLY: Array<[string, string, string]> = [
|
||||
["retirement", "퇴직공제부금비", "RT000011"],
|
||||
["wage_claim", "임금채권보장분담금", "RT000012"],
|
||||
["asbestos", "석면피해구제분담금", "RT000013"],
|
||||
["subcontract", "하도급대금지급보증수수료", "RT000014"],
|
||||
["performance", "공사이행보증수수료", "RT000016"],
|
||||
];
|
||||
/** 현행 비목 — 요율 빈칸 = 해당 없음(0) · 공사이행보증 = 금액 손입력(율 칸 없음) */
|
||||
export const CURRENT_ONLY = ["retirement", "subcontract", "asbestos", "wage_claim", "performance"];
|
||||
/** 소계 아래 줄 */
|
||||
export const TAIL_LINES: Array<[string, string, string, string]> = [
|
||||
["overhead", "일반관리비", "RT000017", "나.소계 × 율"],
|
||||
@@ -113,6 +113,7 @@ export function computeTotal(
|
||||
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 imported: unknown[] = Array.isArray(est.rates_imported) ? est.rates_imported : [];
|
||||
const hand = (key: string): number => num(est[key]) || 0;
|
||||
|
||||
const material = num(s.material);
|
||||
@@ -138,7 +139,14 @@ export function computeTotal(
|
||||
const autoRate: Num = got.rate ?? null;
|
||||
rates[key] = {
|
||||
rate: mine === null ? autoRate : mine,
|
||||
from: mine !== null ? "user" : autoRate !== null ? "auto" : null,
|
||||
from:
|
||||
mine !== null
|
||||
? imported.includes(key)
|
||||
? "import"
|
||||
: "user"
|
||||
: autoRate !== null
|
||||
? "auto"
|
||||
: null,
|
||||
auto: autoRate,
|
||||
why: got.why || "",
|
||||
};
|
||||
@@ -168,6 +176,12 @@ export function computeTotal(
|
||||
line.other = pct(total([labor, line.indirect_labor, material]), rate.other);
|
||||
line.environment = pct(direct, rate.environment);
|
||||
line.machine = pct(direct, rate.machine);
|
||||
line.retirement = pct(labor, rate.retirement);
|
||||
line.subcontract = pct(direct, rate.subcontract);
|
||||
line.asbestos = pct(totalLabor, rate.asbestos);
|
||||
line.wage_claim = pct(totalLabor, rate.wage_claim);
|
||||
for (const key of CURRENT_ONLY) if (rate[key] === null) line[key] = 0; // 요율 빈칸 = 해당 없음
|
||||
line.performance = hand("performance_fee");
|
||||
const netCost = total([direct, ...RATE_LINES.map(([key]) => line[key])]);
|
||||
line.overhead = pct(netCost, rate.overhead);
|
||||
const da = total([netCost, line.overhead]);
|
||||
@@ -193,15 +207,7 @@ export function computeTotal(
|
||||
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,
|
||||
...Object.fromEntries(RATE_LINES.slice(1).map(([key]) => [key, line[key]])),
|
||||
expense_sum: total([
|
||||
expense,
|
||||
...RATE_LINES.filter(([key]) => key !== "indirect_labor").map(([key]) => line[key]),
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
indirect_material · scrap = 그 줄 금액 ·
|
||||
profit_adjust = ROUNDDOWN((순공사원가 − 재료비 + 일반관리비) × 율) − 엑셀 이윤 ·
|
||||
vat_adjust = 엑셀 부가세 − ROUNDDOWN(총원가 × 율) ·
|
||||
procurement_fee_pct = 관급 조달수수료 ÷ 순자재대(소수 2~4 자리 중 수수료를 그대로 내는 첫 값).
|
||||
procurement_fee_pct = 관급 조달수수료 ÷ 순자재대(소수 2~4 자리 중 수수료를 그대로 내는 첫 값) ·
|
||||
performance_fee = 공사이행보증수수료 줄 금액(708-2 · 율 셈 없음).
|
||||
대조(`check`) = 엑셀 비목 합 + 실은 값으로 우리 식(`compute_total`)을 돌려 줄마다 엑셀 금액과 견줌 —
|
||||
1 원 넘게 다른 줄 = 「식 다름」(노인장기요양 · 산업안전보건관리비 A/B 등 식 꼴이 다른 비목) ·
|
||||
우리 표에 없는 행(폐기물처리 · 국가지점번호판 …) · 셈 없는 현행 비목(퇴직공제부금비 …)
|
||||
= 「짝 없음」 · 「셈 없음」.
|
||||
우리 표에 없는 행(폐기물처리 · 국가지점번호판 …) = 「짝 없음」.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,7 +27,6 @@ from typing import Any
|
||||
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import num, rounddown
|
||||
from B09_Estimation.B09_Estimation_Calc_Total import (
|
||||
CURRENT_ONLY,
|
||||
RATE_LINES,
|
||||
TAIL_LINES,
|
||||
compute_total,
|
||||
@@ -68,7 +67,6 @@ ROWS: tuple[tuple[str, str], ...] = (
|
||||
("소계", "subtotal"),
|
||||
)
|
||||
RATE_KEYS = {key: (name, rt) for key, name, rt, _ in (*RATE_LINES, *TAIL_LINES)}
|
||||
NO_CALC = {key: (name, rt) for key, name, rt in CURRENT_ONLY}
|
||||
_PCT = re.compile(r"[x×*]\s*([\d.]+)\s*%")
|
||||
_BASE = re.compile(r"%\s*\+\s*([\d,]+)")
|
||||
_HANGUL = re.compile(r"[가-힣]")
|
||||
@@ -204,16 +202,14 @@ def read_rates(name: str, data: bytes) -> dict[str, Any]:
|
||||
excel[key] = min(excel[key], amount or excel[key])
|
||||
continue
|
||||
excel[key] = amount if amount is not None else excel.get(key, 0.0)
|
||||
if key in RATE_KEYS or key in NO_CALC:
|
||||
if key == "performance": # 공사이행보증 = 금액 손입력(율 셈 없음)
|
||||
if amount:
|
||||
estimate["performance_fee"] = amount
|
||||
elif key in RATE_KEYS:
|
||||
if row["rate"] is not None:
|
||||
rates[key] = row["rate"]
|
||||
elif amount:
|
||||
notes.append(f"율 없음 — 「{row['name']}」 산출근거 빈칸 · 지금 율 그대로")
|
||||
if key in NO_CALC and amount:
|
||||
notes.append(
|
||||
f"셈 없음 — 「{row['name']}」 {amount:,.0f} 원 · 율 {row['rate']}% 만 실음"
|
||||
"(우리 원가계산서 줄만 · 총액에 안 듦)"
|
||||
)
|
||||
if key == "safety" and row["text"]:
|
||||
got = _BASE.search(row["text"])
|
||||
estimate["safety_base"] = float(got.group(1).replace(",", "")) if got else 0.0
|
||||
|
||||
@@ -350,6 +350,7 @@ def _rate_patch(name: str, data: bytes, mine: dict[str, Any]) -> dict[str, Any]:
|
||||
raise HTTPException(400, str(error)) from error
|
||||
check = import_rates.check(read, mine, rate_table())
|
||||
estimate = {**mine, **read["estimate"], "rates": {**(mine.get("rates") or {}), **read["rates"]}}
|
||||
estimate["rates_imported"] = sorted({*(mine.get("rates_imported") or []), *read["rates"]})
|
||||
keep = ("kind", "sheet", "rows", "rates", "notes")
|
||||
return {**{k: read[k] for k in keep}, "check": check, "patch": {"estimate": estimate}}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ ESTIMATE_FIELDS = (
|
||||
"indirect_material", # 간접재료비(원)
|
||||
"scrap", # 작업설 부산물(원 · 빼는 값)
|
||||
"procurement_fee_pct", # 조달수수료율(%)
|
||||
"performance_fee", # 공사이행보증수수료(원 · 708-2)
|
||||
)
|
||||
|
||||
TEXT_FIELDS = ("work_type", "environment_kind")
|
||||
@@ -345,6 +346,9 @@ def clean_user(raw: Any) -> dict[str, Any]:
|
||||
rates = given.get("rates")
|
||||
if isinstance(rates, dict):
|
||||
estimate["rates"] = {str(k): _num(v) for k, v in rates.items()}
|
||||
imported = given.get("rates_imported") # 707-2 불러온 요율 줄 키(출처 「불러옴」)
|
||||
if isinstance(imported, list):
|
||||
estimate["rates_imported"] = [str(k) for k in imported]
|
||||
bill = clean_bill(raw.get("bill"))
|
||||
out = {"materials": materials, "estimate": estimate, **({"bill": bill} if bill else {})}
|
||||
if cover := clean_cover(raw.get("daega_cover")):
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
* 맨 위 = 요율 고르는 값(추정가격 · 공사기간 · 환경보전비 공종 — 누르면 도로 ↔ 그 밖)(166-1)
|
||||
* 요율 = 자동(M01 조달청 제비율 · 짝 셈이 고름) · 사용자가 적으면 앞섬 · 출처 칸 누름 = 자동으로
|
||||
* 고칠 칸 = 요율 · 손입력(기초액 · 이윤 보정 · 부가세 조정 · 조달수수료율) ·
|
||||
* 엑셀에 없는 현행 비목 = 줄만(빈칸)
|
||||
* 현행 비목(708-2) = 경비 묶음 안 · 요율 빈칸 = 해당 없음(0) · 공사이행보증 = 금액 손입력 ·
|
||||
* 요율 출처 = 사용자 · 불러옴(707-2 `rates_imported`) — 요율을 고치면 사용자
|
||||
* 원가계산서 = 총괄 값 거울 + 구성비 · 고칠 칸 = 간접재료비 · 작업설 · 줄 누름 = 총괄 그 줄로
|
||||
* 레벨(185-1 · 201-1) = 줄 `level`(화면이 정함 · 줄 짜임이 이 파일에 있음 · 셈 짝은 값만) →
|
||||
* 공용 표 level 옵션(명칭 칸 들여쓰기 · 보기 전용) · 1단 = 굵게(is-title)
|
||||
@@ -42,8 +43,22 @@ const HAND: Record<string, [string, string]> = {
|
||||
supply: ["procurement_fee_pct", "조달수수료율(%)"],
|
||||
indirect_material: ["indirect_material", "간접재료비(원)"],
|
||||
scrap: ["scrap", "작업설 부산물(원)"],
|
||||
performance: ["performance_fee", "공사이행보증수수료(원)"],
|
||||
};
|
||||
const RATE_KEYS = new Set([...RATE_LINES, ...TAIL_LINES].map(([key]) => key));
|
||||
/** 요율 칸을 고치는 줄 — 공사이행보증 = 금액 손입력(율 칸 없음) */
|
||||
const RATE_KEYS = new Set(
|
||||
[...RATE_LINES, ...TAIL_LINES].map(([key]) => key).filter((key) => key !== "performance"),
|
||||
);
|
||||
|
||||
/** 요율 줄 사용자 값 고침 · 지움 — 불러옴 표시도 뗌 */
|
||||
function setRate(estimate: Row, key: string, v: number | null): void {
|
||||
const rates: Row = { ...(estimate.rates || {}) };
|
||||
if (v === null) delete rates[key];
|
||||
else rates[key] = v;
|
||||
estimate.rates = rates;
|
||||
if (Array.isArray(estimate.rates_imported))
|
||||
estimate.rates_imported = estimate.rates_imported.filter((k: string) => k !== key);
|
||||
}
|
||||
|
||||
const ENV_NAME: Record<string, string> = { road: "도로", other: "그 밖의 토목" };
|
||||
|
||||
@@ -52,12 +67,8 @@ function editEstimate(ctx: TabContext, rowKey: string, column: string, value: st
|
||||
const v = parseNumber(value);
|
||||
if (v === undefined) return;
|
||||
const estimate: Row = { ...(ctx.user.estimate || {}) };
|
||||
if (column === "rate" && RATE_KEYS.has(rowKey)) {
|
||||
const rates: Row = { ...(estimate.rates || {}) };
|
||||
if (v === null) delete rates[rowKey];
|
||||
else rates[rowKey] = v;
|
||||
estimate.rates = rates;
|
||||
} else if (HAND[rowKey]) {
|
||||
if (column === "rate" && RATE_KEYS.has(rowKey)) setRate(estimate, rowKey, v);
|
||||
else if (HAND[rowKey]) {
|
||||
const field = HAND[rowKey][0];
|
||||
if (v === null) delete estimate[field];
|
||||
else estimate[field] = v;
|
||||
@@ -71,11 +82,9 @@ function clickEstimate(ctx: TabContext, rowKey: string, column: string, from: un
|
||||
if (rowKey === "in_env") {
|
||||
if (estimate.environment_kind === "road") delete estimate.environment_kind;
|
||||
else estimate.environment_kind = "road";
|
||||
} else if (column === "candidate" && from === "user") {
|
||||
const rates: Row = { ...(estimate.rates || {}) };
|
||||
delete rates[rowKey];
|
||||
estimate.rates = rates;
|
||||
} else return;
|
||||
} else if (column === "candidate" && (from === "user" || from === "import"))
|
||||
setRate(estimate, rowKey, null);
|
||||
else return;
|
||||
ctx.setUser({ ...ctx.user, estimate });
|
||||
}
|
||||
|
||||
@@ -122,8 +131,13 @@ function billRows(bill: Row | undefined): DataRow[] {
|
||||
];
|
||||
}
|
||||
|
||||
function rateText(formula: string, rate: Num): string {
|
||||
return rate === null ? `${formula}(요율 빈칸)` : formula.replace("율", `${rate}%`);
|
||||
function rateText(formula: string, rate: Num, key = ""): string {
|
||||
if (key === "performance") return formula;
|
||||
if (rate === null)
|
||||
return CURRENT_ONLY.includes(key)
|
||||
? `해당 없음(요율 빈칸) — ${formula}`
|
||||
: `${formula}(요율 빈칸)`;
|
||||
return formula.replace("율", `${rate}%`);
|
||||
}
|
||||
|
||||
export const buildTotal: TabBuild = (ctx): TabTable => {
|
||||
@@ -137,8 +151,10 @@ export const buildTotal: TabBuild = (ctx): TabTable => {
|
||||
/** 출처 칸 글 — 사용자 · 자동 까닭 · 자동 없음 */
|
||||
const source = (key: string): string => {
|
||||
const r: Row = picked[key] || {};
|
||||
if (r.from === "user")
|
||||
return `사용자 값 · 누르면 자동${r.auto === null || r.auto === undefined ? "(없음)" : ` ${r.auto}%`}으로`;
|
||||
if (key === "performance") return "금액 직접 입력(율 셈 없음)";
|
||||
if (r.from === "user" || r.from === "import")
|
||||
return `${r.from === "user" ? "사용자" : "불러옴"} 값 · 누르면 자동${r.auto === null || r.auto === undefined ? "(없음)" : ` ${r.auto}%`}으로`;
|
||||
if (CURRENT_ONLY.includes(key)) return "사용자 · 불러옴만(자동 없음)";
|
||||
return r.from === "auto" ? `자동 — ${r.why}` : `자동 없음${r.why ? ` — ${r.why}` : ""}`;
|
||||
};
|
||||
const hand = (key: string): Num => {
|
||||
@@ -165,7 +181,7 @@ export const buildTotal: TabBuild = (ctx): TabTable => {
|
||||
...extra,
|
||||
});
|
||||
const rateRow = ([key, name, rt, formula]: [string, string, string, string], no: number) =>
|
||||
row(key, "", `${no}. ${name}`, line[key] ?? null, rateText(formula, rate(key)), {
|
||||
row(key, "", `${no}. ${name}`, line[key] ?? null, rateText(formula, rate(key), key), {
|
||||
rate: rate(key),
|
||||
candidate: source(key),
|
||||
from: picked[key]?.from ?? null,
|
||||
@@ -204,11 +220,6 @@ export const buildTotal: TabBuild = (ctx): TabTable => {
|
||||
expense: sums.expense ?? null,
|
||||
}),
|
||||
...RATE_LINES.map((spec, i) => rateRow(spec, i + 1)),
|
||||
...CURRENT_ONLY.map(([key, name, rt]) =>
|
||||
row(key, "", name, null, "현행 비목 · 엑셀에 없음 — 밑수 · 고르는 값 미정(줄만)", {
|
||||
candidate: cand[rt] ?? rt,
|
||||
}),
|
||||
),
|
||||
row("net_cost", "나.", "소계(순공사원가)", sub.net_cost ?? null),
|
||||
rateRow(TAIL_LINES[0], RATE_LINES.length + 1),
|
||||
row("da", "다.", "소계", sub.da ?? null),
|
||||
@@ -302,7 +313,7 @@ export const buildCost: TabBuild = (ctx): TabTable => {
|
||||
const formula: Record<string, string> = Object.fromEntries(
|
||||
[...RATE_LINES, ...TAIL_LINES].map(([key, , , f]) => [
|
||||
key,
|
||||
rateText(f, picked[key]?.rate ?? null),
|
||||
rateText(f, picked[key]?.rate ?? null, key),
|
||||
]),
|
||||
);
|
||||
const rows: DataRow[] = COST_ROWS.map(([key, group, part, name, link]) => ({
|
||||
@@ -339,7 +350,7 @@ export const buildCost: TabBuild = (ctx): TabTable => {
|
||||
level: nameLevel(rows),
|
||||
mergeSame: ["group", "part"],
|
||||
banner: unfinished(ctx.sheets) ?? undefined,
|
||||
hint: "고칠 칸은 간접재료비 · 줄을 누르면 총괄 그 줄로 이동",
|
||||
hint: "고칠 칸은 간접재료비 · 작업설 · 공사이행보증수수료 · 줄을 누르면 총괄 그 줄로 이동",
|
||||
editable: (i, key) => key === "amount" && String(rows[i]?.key) in HAND,
|
||||
onEdit: (i, key, value) => editEstimate(ctx, String(rows[i]?.key), key, value),
|
||||
link: (i) => {
|
||||
|
||||
Reference in New Issue
Block a user