Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
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) => {
|
||||
|
||||
@@ -17,7 +17,7 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B09_Estimation.B09_Estimation_Calc_Total import compute_total # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Total import CURRENT_ONLY, compute_total # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Rates import rate_candidates # noqa: E402
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
@@ -60,6 +60,7 @@ EXCEL_LINES = { # 총괄 D49~D58 · D60 · D62 · D64 · D66
|
||||
"profit": 108109955,
|
||||
"vat": 102911727,
|
||||
"supply": 69850000,
|
||||
**{key: 0 for key in CURRENT_ONLY}, # 708-2 현행 비목 — 엑셀(기번3)에 없음 = 요율 빈칸 = 0
|
||||
}
|
||||
EXCEL_SUBTOTALS = { # 총괄 D59 · D61 · D63 · D65 · D67
|
||||
"net_cost": 868874829,
|
||||
@@ -103,7 +104,8 @@ def test_간접재료비_작업설은_직접재료비만_바꿈():
|
||||
|
||||
def test_빈칸_규칙():
|
||||
blank = compute_total(SUMS, {})
|
||||
assert all(v is None for k, v in blank["lines"].items() if k != "supply")
|
||||
assert all(v is None for k, v in blank["lines"].items() if k not in ("supply", *CURRENT_ONLY))
|
||||
assert all(blank["lines"][k] == 0 for k in CURRENT_ONLY) # 708-2 요율 빈칸 = 해당 없음
|
||||
assert blank["subtotals"]["net_cost"] is None and blank["subtotals"]["grand"] is None
|
||||
assert blank["lines"]["supply"] is None # 조달수수료율 빈칸
|
||||
# 요율 하나만 빈칸 — 그 줄 · 소계 빈칸 · 다른 줄은 셈
|
||||
@@ -157,6 +159,8 @@ def test_거울_TS_가_같은_값():
|
||||
[{**SUMS, "supply_net": 2_000_000_000}, ESTIMATE],
|
||||
[{**SUMS, "labor": None}, ESTIMATE],
|
||||
[SUMS, {**ESTIMATE, "rates": {**RATES, "care": None, "profit": 12.5}}],
|
||||
[SUMS, CURRENT],
|
||||
[{**SUMS, "labor": None}, CURRENT],
|
||||
]
|
||||
assert _run_ts(cases) == [compute_total(s, e) for s, e in cases]
|
||||
|
||||
@@ -208,8 +212,53 @@ console.log(JSON.stringify(out));
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
out = json.loads(result.stdout)
|
||||
assert out["editable"] == [True, False, True, False, False]
|
||||
assert out["editable"] == [True, False, True, True, False] # 708-2 퇴직공제 요율 칸
|
||||
assert out["care"] == 13.14 and out["careGone"] is False
|
||||
assert out["grandBlank"] is None # 요율 지우면 그 줄 · 소계 빈칸
|
||||
assert out["base"] == 1000 and out["scrap"] == 300
|
||||
assert out["link"] == {"tab": "total", "key": "safety"}
|
||||
|
||||
|
||||
CURRENT = { # 708-2 현행 비목 — 율 넷 · 공사이행보증 금액 · 불러온 율 표시
|
||||
**ESTIMATE,
|
||||
"rates": {
|
||||
**RATES,
|
||||
"retirement": 2.3,
|
||||
"wage_claim": 0.09,
|
||||
"asbestos": 0.006,
|
||||
"subcontract": 0.1,
|
||||
},
|
||||
"rates_imported": ["retirement", "safety"],
|
||||
"performance_fee": 1234567,
|
||||
}
|
||||
|
||||
|
||||
def test_708_2_현행_비목_다섯():
|
||||
out = compute_total(SUMS, CURRENT)
|
||||
ln, total_labor, direct = out["lines"], 346800554 + 50286080, 710359136
|
||||
assert ln["retirement"] == int(346800554 * 2.3 / 100) # 직접노무비 × 율
|
||||
assert ln["wage_claim"] == int(total_labor * 0.09 / 100) # (직노 + 간노) × 율
|
||||
assert ln["asbestos"] == int(total_labor * 0.006 / 100)
|
||||
assert ln["subcontract"] == int(direct * 0.1 / 100) # (재료 + 직노 + 산출경비) × 율
|
||||
assert ln["performance"] == 1234567 # 금액 손입력
|
||||
added = sum(ln[k] for k in CURRENT_ONLY)
|
||||
assert out["subtotals"]["net_cost"] == EXCEL_SUBTOTALS["net_cost"] + added # 나.소계에 듦
|
||||
assert out["cost"]["expense_sum"] == 277553778 + added # 경비 묶음 안
|
||||
assert out["cost"]["retirement"] == ln["retirement"]
|
||||
assert (
|
||||
out["rates"]["retirement"]["from"] == "import"
|
||||
and out["rates"]["safety"]["from"] == "import"
|
||||
)
|
||||
assert out["rates"]["wage_claim"]["from"] == "user"
|
||||
# 요율 빈칸 = 해당 없음(0 · 소계 빈칸 아님) — 옛 공사 금액 그대로
|
||||
old = compute_total(SUMS, ESTIMATE)
|
||||
assert all(old["lines"][k] == 0 for k in CURRENT_ONLY)
|
||||
assert old["subtotals"] == EXCEL_SUBTOTALS
|
||||
|
||||
|
||||
def test_708_2_저장_칸():
|
||||
from B09_Estimation import B09_Estimation_Store as store
|
||||
|
||||
raw = {"estimate": {"performance_fee": 1234567, "rates_imported": ["retirement", 3]}}
|
||||
got = store.clean_user(raw)["estimate"]
|
||||
assert got["performance_fee"] == 1234567 and got["rates_imported"] == ["retirement", "3"]
|
||||
|
||||
@@ -15,7 +15,7 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B09_Estimation.B09_Estimation_Calc_Total import compute_total # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Total import CURRENT_ONLY, compute_total # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Rates import rate_table # noqa: E402
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
@@ -142,7 +142,7 @@ def test_엑셀_검산_손값이면_그대로():
|
||||
out = total(SUMS, EXCEL, 2026)
|
||||
assert out["subtotals"]["grand"] == 1201879000
|
||||
assert out["lines"]["safety"] == 16586996 and out["lines"]["profit"] == 108109955
|
||||
assert all(r["from"] == "user" for r in out["rates"].values())
|
||||
assert all(r["from"] == "user" for k, r in out["rates"].items() if k not in CURRENT_ONLY)
|
||||
|
||||
|
||||
def _node(script: str) -> str:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
총액 대조 = 엑셀 비목 합 + 실은 요율 · 손입력으로 우리 식(`compute_total`) → 엑셀 총공사비:
|
||||
대흥 = 같음 · 영월 = 「국가지점번호판검증수수료」 762,000 원만 다름(우리 표에 줄 없음) ·
|
||||
거창 = 퇴직공제부금비(셈 없는 현행 비목)만큼 · 소광 = 산출근거 빈칸 율 넷 · 폐기물처리 줄.
|
||||
거창 = 같음(708-2 퇴직공제부금비 셈 뒤) · 소광 = 산출근거 빈칸 율 넷 · 폐기물처리 줄.
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -61,7 +61,7 @@ def test_707_2_요율_짝(rates):
|
||||
2.3,
|
||||
)
|
||||
assert gc["estimate"]["safety_base"] == 0 # 글에 + 기초액 없음
|
||||
assert any("퇴직공제부금비" in n and "셈 없음" in n for n in gc["notes"])
|
||||
assert gc["notes"] == [] # 708-2 퇴직공제부금비 = 셈(「셈 없음」 없음)
|
||||
sk = rates["소광"]
|
||||
assert sum("율 없음" in n for n in sk["notes"]) == 4 # 산출근거 빈칸 — 지금 율 그대로
|
||||
assert any("페기물처리" in n for n in sk["notes"])
|
||||
@@ -73,11 +73,11 @@ def test_707_2_총액_대조(rates):
|
||||
assert got["대흥"]["differ"] == []
|
||||
assert got["영월"]["excel_grand"] - got["영월"]["grand"] == 762000 # 국가지점번호판 줄 몫
|
||||
assert set(got["영월"]["differ"]) == {"도급공사비", "총공사비"}
|
||||
# 708-2 — 퇴직공제부금비 5,603,907(직노 × 2.3 %)가 순공사원가에 듦 → 총공사비 = 엑셀
|
||||
assert got["거창"]["grand"] == got["거창"]["excel_grand"] == 744653000
|
||||
assert got["거창"]["differ"] == []
|
||||
lines = {ln["key"]: ln for ln in got["거창"]["lines"]}
|
||||
rate_keys = ("indirect_labor", "accident", "employment", "health", "care", "pension")
|
||||
assert all(lines[k]["same"] for k in (*rate_keys, "safety", "other", "environment"))
|
||||
assert not lines["overhead"]["same"] # 퇴직공제부금비 5,603,907 이 순공사원가에 없음
|
||||
assert lines["overhead"]["excel"] - lines["overhead"]["ours"] == 448312 # = 5,603,907 × 8 %
|
||||
assert lines["retirement"]["ours"] == lines["retirement"]["excel"] == 5603907
|
||||
|
||||
|
||||
def test_707_3_단가표_읽기():
|
||||
|
||||
Reference in New Issue
Block a user