Merge remote-tracking branch 'origin/sub_desktop_1' into sub_laptop_1
This commit is contained in:
@@ -0,0 +1,369 @@
|
|||||||
|
"""STmate 골든셋 ⑤ — 공사원가계산서를 **우리 엔진**으로 되풀어 대조(PLAN 6장·12장 · 명세 8장).
|
||||||
|
|
||||||
|
짝 파일 `test_b09_golden_stmate.py` 의 다섯째 벌임 — 그 파일이 700줄에 닿아 갈라 둠.
|
||||||
|
실무 원본 읽기는 그 파일의 `_workbooks` 를 그대로 씀(한 번만 읽어 나눠 쓰는 자리).
|
||||||
|
|
||||||
|
재는 것 — 제비율(법정경비) · 간접노무비 · 일반관리비 · 이윤 · 부가세 · 도급공사비 · 총공사비.
|
||||||
|
요율은 **원본 비고에서 뽑아** 그 원본 전용 요율 판을 지어 넣음 —
|
||||||
|
「요율이 달라서 틀림」이 안 생김.
|
||||||
|
남는 차이는 **밑수 고르기 · 원 단위 버림 · 사슬**, 곧 우리 엔진 몫임.
|
||||||
|
|
||||||
|
⚠ 실무 원본은 **git 안 지식DB**라 어느 창에서든 돎. 원본이 없으면 건너뜀(시험 코드 탓이 아님).
|
||||||
|
⚠ 값을 여기서 짓지 않음 — 원본 칸을 읽어 **엔진 입력**으로만 씀.
|
||||||
|
⚠ 이 벌은 **재기만 함** — 값을 맞추려고 엔진을 고치지 않음(브레인 지시 2026-09-14).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
from dataclasses import replace
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from test_b09_golden_stmate import _num, _workbooks
|
||||||
|
|
||||||
|
from B09_Estimation.B09_Estimation_Engine_Cost import CostInput, calculate_cost
|
||||||
|
|
||||||
|
#: 원본 줄 이름(공백 지운 것) → 엔진 줄 키. 「소계」 셋은 나온 차례로 재료비·노무비·경비.
|
||||||
|
_COST_KEYS: dict[str, str] = {
|
||||||
|
"직접재료비": "direct_material",
|
||||||
|
"간접재료비": "indirect_material",
|
||||||
|
"작업설,부산물등(△)": "byproduct",
|
||||||
|
"작업설.부산물등(△)": "byproduct",
|
||||||
|
"직접노무비": "direct_labor",
|
||||||
|
"간접노무비": "indirect_labor_cost",
|
||||||
|
"산출경비": "direct_expense",
|
||||||
|
"산재보험료": "industrial_accident_insurance",
|
||||||
|
"고용보험료": "employment_insurance",
|
||||||
|
"건강보험료": "health_insurance",
|
||||||
|
"노인장기요양보험료": "long_term_care_insurance",
|
||||||
|
"연금보험료": "national_pension",
|
||||||
|
"산업안전보건관리비": "safety_management_cost",
|
||||||
|
"기타경비": "other_expense",
|
||||||
|
"환경보전비": "environment_preservation",
|
||||||
|
"건설기계대여금지급보증금액": "equipment_payment_guarantee",
|
||||||
|
"순공사원가": "net_construction_cost",
|
||||||
|
"일반관리비": "general_overhead",
|
||||||
|
"이윤": "profit",
|
||||||
|
"페기물처리": "waste_disposal", # 원본 표기(소광) 그대로
|
||||||
|
"폐기물처리비": "waste_disposal",
|
||||||
|
"총원가": "total_cost",
|
||||||
|
"부가가치세": "vat",
|
||||||
|
"도급공사비": "contract_amount",
|
||||||
|
"관급자재대": "owner_supplied_material_total",
|
||||||
|
"관급자재대(도급자설치)": "owner_supplied_material_total",
|
||||||
|
"총공사비": "grand_total",
|
||||||
|
}
|
||||||
|
#: 원본 비고에 요율이 적히는 법정경비 줄 — 이 줄만 엔진과 견줌.
|
||||||
|
_STATUTORY_KEYS = (
|
||||||
|
"industrial_accident_insurance",
|
||||||
|
"employment_insurance",
|
||||||
|
"health_insurance",
|
||||||
|
"long_term_care_insurance",
|
||||||
|
"national_pension",
|
||||||
|
"safety_management_cost",
|
||||||
|
"other_expense",
|
||||||
|
"environment_preservation",
|
||||||
|
"equipment_payment_guarantee",
|
||||||
|
)
|
||||||
|
_RE_PERCENT = re.compile(r"[x×]\s*([\d.]+)\s*%")
|
||||||
|
_RE_FLAT_ADD = re.compile(r"%\s*\+\s*([\d,]+)")
|
||||||
|
_RE_OWNER_NOTE = re.compile(r"원자재대[::]\s*([\d,]+).*?조달수수료[::]\s*([\d,]+)", re.S)
|
||||||
|
_RE_EXEMPT = re.compile(r"면세품목[::]\D*([\d,]+)")
|
||||||
|
_RE_SAFETY_AB = re.compile(r"A[))][^=]*=\s*([\d,]+).*?B[))][^=]*=\s*([\d,]+)", re.S)
|
||||||
|
#: 합성 요율 판의 「어디에나 걸리는」 구간·기간 라벨 — 구간 갈래는 이 벌이 재는 것이 아님.
|
||||||
|
_WIDE_BRACKET = "lt_1000_billion"
|
||||||
|
_WIDE_DURATION = "lte_100000_days"
|
||||||
|
#: 연금 요율을 원본이 보인 값 그대로 쓰려고 두는 표식 해(연도 갈래는 이 벌 밖).
|
||||||
|
_SHEET_YEAR = 2000
|
||||||
|
|
||||||
|
|
||||||
|
def _won(text: object) -> Decimal:
|
||||||
|
return Decimal(str(text).replace(",", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def _cost_rows(rows: list[tuple]) -> tuple[dict[str, tuple], list[tuple[str, Decimal]]]:
|
||||||
|
"""`공사원가계산서` 시트 → `({키: (금액, 요율, 기초액, 비고)}, 이름 못 고른 줄들)`.
|
||||||
|
|
||||||
|
금액 칸은 「금 액」 머리로 찾고, 당초/변경/증감으로 갈린 원본(영덕 변경설계)은 **변경** 칸을 씀.
|
||||||
|
"""
|
||||||
|
head = next(r for r in rows if any("금" in str(c or "") and "액" in str(c or "") for c in r))
|
||||||
|
amount_col = next(
|
||||||
|
i for i, c in enumerate(head) if "금" in str(c or "") and "액" in str(c or "")
|
||||||
|
)
|
||||||
|
note_col = next(
|
||||||
|
(i for i, c in enumerate(head) if "비" in str(c or "") and "고" in str(c or "")), None
|
||||||
|
)
|
||||||
|
body = rows[rows.index(head) + 1 :]
|
||||||
|
for index, cell in enumerate(body[0] if body else ()):
|
||||||
|
if "변" in str(cell or "") and "경" in str(cell or ""):
|
||||||
|
amount_col = index
|
||||||
|
got: dict[str, tuple] = {}
|
||||||
|
unknown: list[tuple[str, Decimal]] = []
|
||||||
|
subtotals = 0
|
||||||
|
for row in body:
|
||||||
|
label = ""
|
||||||
|
for cell in row[:amount_col]:
|
||||||
|
if isinstance(cell, str) and cell.strip():
|
||||||
|
label = cell.replace(" ", "").replace(" ", "")
|
||||||
|
amount = _num(row[amount_col]) if amount_col < len(row) else None
|
||||||
|
if amount is None or not label:
|
||||||
|
continue
|
||||||
|
if label == "소계":
|
||||||
|
subtotals += 1
|
||||||
|
key = ("material_cost", "labor_cost", "expense")[subtotals - 1]
|
||||||
|
else:
|
||||||
|
key = _COST_KEYS.get(label)
|
||||||
|
if key is None:
|
||||||
|
unknown.append((label, amount))
|
||||||
|
continue
|
||||||
|
note = (
|
||||||
|
" ".join(str(c) for c in row[note_col:] if isinstance(c, str))
|
||||||
|
if note_col is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
percent = _RE_PERCENT.search(note)
|
||||||
|
flat = _RE_FLAT_ADD.search(note)
|
||||||
|
got[key] = (
|
||||||
|
amount,
|
||||||
|
Decimal(percent.group(1)) if percent else None,
|
||||||
|
_won(flat.group(1)) if flat else Decimal(0),
|
||||||
|
note,
|
||||||
|
)
|
||||||
|
return got, unknown
|
||||||
|
|
||||||
|
|
||||||
|
def _cost_sheets() -> list[tuple[str, dict[str, tuple], list[tuple[str, Decimal]]]]:
|
||||||
|
return [
|
||||||
|
(name, *_cost_rows(sheets["공사원가계산서"]))
|
||||||
|
for name, sheets in _workbooks()
|
||||||
|
if "공사원가계산서" in sheets
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _sheet_rate_dataset(got: dict[str, tuple]) -> dict:
|
||||||
|
"""원본 비고에 적힌 요율만으로 **그 원본 전용 요율 판**을 지음.
|
||||||
|
|
||||||
|
비고가 빈 줄은 판에 안 들어가고, 그 줄은 엔진이 아예 안 세움(`available_items`).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def percent(key: str) -> Decimal | None:
|
||||||
|
row = got.get(key)
|
||||||
|
return row[1] if row else None
|
||||||
|
|
||||||
|
variables: dict[str, object] = {}
|
||||||
|
if (rate := percent("indirect_labor_cost")) is not None:
|
||||||
|
variables["rate_indirect_labor"] = {
|
||||||
|
"brackets": [
|
||||||
|
{
|
||||||
|
"direct_cost_bracket": _WIDE_BRACKET,
|
||||||
|
"duration_bracket": _WIDE_DURATION,
|
||||||
|
"work_type": "civil",
|
||||||
|
"rate_percent": float(rate),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (rate := percent("industrial_accident_insurance")) is not None:
|
||||||
|
variables["rate_sanjae"] = {"rate_percent": float(rate)}
|
||||||
|
if (rate := percent("employment_insurance")) is not None:
|
||||||
|
variables["rate_goyong"] = {
|
||||||
|
"brackets": [
|
||||||
|
{
|
||||||
|
"grade": "below_7",
|
||||||
|
"estimated_amount_bracket": "below_official_threshold",
|
||||||
|
"rate_percent": float(rate),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (rate := percent("health_insurance")) is not None:
|
||||||
|
variables["rate_health"] = {"rate_percent": float(rate)}
|
||||||
|
if (rate := percent("long_term_care_insurance")) is not None:
|
||||||
|
variables["rate_care"] = {"rate_percent": float(rate)}
|
||||||
|
if (rate := percent("national_pension")) is not None:
|
||||||
|
variables["rate_pension"] = {
|
||||||
|
"annual_rates": [{"year": _SHEET_YEAR, "rate_percent": float(rate)}]
|
||||||
|
}
|
||||||
|
if (rate := percent("safety_management_cost")) is not None:
|
||||||
|
variables["rate_safety_pct"] = {
|
||||||
|
"brackets": [
|
||||||
|
{
|
||||||
|
"target_amount_bracket": _WIDE_BRACKET,
|
||||||
|
"work_type": "civil",
|
||||||
|
"rate_percent": float(rate),
|
||||||
|
"base_amount_krw": float(got["safety_management_cost"][2]),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (rate := percent("other_expense")) is not None:
|
||||||
|
variables["rate_other_expense"] = {
|
||||||
|
"brackets": [
|
||||||
|
{
|
||||||
|
"direct_cost_bracket": _WIDE_BRACKET,
|
||||||
|
"duration_bracket": _WIDE_DURATION,
|
||||||
|
"work_type": "civil",
|
||||||
|
"rate_percent": float(rate),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (rate := percent("environment_preservation")) is not None:
|
||||||
|
variables["rate_environment"] = {
|
||||||
|
"all_work_types": [{"work_type": "civil_road", "rate_percent": float(rate)}]
|
||||||
|
}
|
||||||
|
if (rate := percent("equipment_payment_guarantee")) is not None:
|
||||||
|
variables["rate_equipment_payment_guarantee"] = {
|
||||||
|
"general_construction": [{"work_type": "civil_general", "rate_percent": float(rate)}],
|
||||||
|
"specialty_construction": [],
|
||||||
|
}
|
||||||
|
variables["rate_overhead"] = {
|
||||||
|
"civil_landscape_industrial": [
|
||||||
|
{
|
||||||
|
"estimated_price_bracket": _WIDE_BRACKET,
|
||||||
|
"rate_percent": float(percent("general_overhead")),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
variables["rate_profit"] = {
|
||||||
|
"brackets": [
|
||||||
|
{"estimated_price_bracket": _WIDE_BRACKET, "rate_percent": float(percent("profit"))}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
variables["rate_vat"] = {"rate_percent": float(percent("vat") or 0)}
|
||||||
|
return {"dataset_id": "sheet", "effective_date": "sheet", "variables": variables}
|
||||||
|
|
||||||
|
|
||||||
|
def _cost_input(got: dict[str, tuple], rate_path: pathlib.Path) -> CostInput:
|
||||||
|
"""원본 칸을 엔진 입력으로 옮김 — 직접비 셋 · 관급 순자재대·수수료 · 부가세 방식 · 폐기물."""
|
||||||
|
rate_path.write_text(json.dumps(_sheet_rate_dataset(got)), encoding="utf-8")
|
||||||
|
owner = _RE_OWNER_NOTE.search(got.get("owner_supplied_material_total", (0, 0, 0, ""))[3])
|
||||||
|
gross, fee = (_won(owner.group(1)), _won(owner.group(2))) if owner else (Decimal(0), Decimal(0))
|
||||||
|
exempt = _RE_EXEMPT.search(got.get("vat", (0, 0, 0, ""))[3])
|
||||||
|
return CostInput(
|
||||||
|
direct_material_krw=got["direct_material"][0],
|
||||||
|
direct_labor_krw=got["direct_labor"][0],
|
||||||
|
direct_expense_krw=got["direct_expense"][0],
|
||||||
|
indirect_material_krw=got.get("indirect_material", (Decimal(0),))[0],
|
||||||
|
# ⚠ 관급 입력은 **순자재대** — 원본 비고의 원자재대는 조달수수료를 이미 머금음.
|
||||||
|
owner_supplied_material_krw=gross - fee,
|
||||||
|
procurement_fee_krw=fee,
|
||||||
|
pension_year=_SHEET_YEAR,
|
||||||
|
vat_mode="forest_coop_exempt" if exempt else "supply",
|
||||||
|
tax_exempt_material_krw=_won(exempt.group(1)) if exempt else Decimal(0),
|
||||||
|
waste_disposal_krw=got.get("waste_disposal", (Decimal(0),))[0],
|
||||||
|
waste_placement="after_profit", # 소광 원본 자리 — 이윤 뒤·총원가 안
|
||||||
|
rate_file_path=str(rate_path),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_원가계산서_법정경비_줄마다_실무_원본_재현(tmp_path) -> None:
|
||||||
|
"""⑤ 제비율 줄 — 밑수 고르기 + 원 단위 버림이 원본과 **줄마다** 같음(실무 6건).
|
||||||
|
|
||||||
|
안 맞을 자리를 셋으로 갈라 둠 —
|
||||||
|
· 우리 엔진이 틀림 → `misses` 에 남아 실패로 드러남
|
||||||
|
· 요율 데이터가 다름 → 요율을 원본에서 뽑아 쓰므로 **안 생김**
|
||||||
|
· 원본이 식을 안 보임 → 비고 빈 줄(건강·노인장기·연금·산업안전). 앞 차수 값을 그대로
|
||||||
|
물려 적은 자리라 되풀 수 없음 — 울진 신설·영덕·소광 각 4줄, 세어만 두고 대조 밖.
|
||||||
|
· 절사 자리가 다름 → 안전관리비 **B 줄** 1원(영월). 아래 `rounding` 에 따로 담음.
|
||||||
|
|
||||||
|
⭐ **고칠 자리 하나 — 안전관리비 B 의 절사 자리**(2026-09-14 실측, 브레인 보고 대상).
|
||||||
|
우리 엔진 `버림((밑수 × 율 + 기초액) × 1.2)` 영월 20,330,639
|
||||||
|
STmate `버림(밑수 × 율 + 기초액) × 1.2` 뒤 버림 영월 20,330,638
|
||||||
|
여섯 원본 중 영월에서만 갈림(봉화·울진은 두 셈법이 같은 값).
|
||||||
|
**미채택 줄이라 총액엔 안 닿음** —
|
||||||
|
영월도 A 를 채택함. 이 벌은 재기만 하므로 엔진은 그대로 두고 차를 세어만 둠.
|
||||||
|
"""
|
||||||
|
sheets = _cost_sheets()
|
||||||
|
if not sheets:
|
||||||
|
pytest.skip("실무 원본 XLSX 가 없음")
|
||||||
|
checked, blank, misses, rounding = 0, 0, [], []
|
||||||
|
for index, (name, got, _unknown) in enumerate(sheets):
|
||||||
|
blank += sum(1 for k in _STATUTORY_KEYS if k in got and got[k][1] is None)
|
||||||
|
result = calculate_cost(_cost_input(got, tmp_path / f"rate{index}.json"))
|
||||||
|
for key in ("indirect_labor_cost", *_STATUTORY_KEYS):
|
||||||
|
if key not in got or got[key][1] is None:
|
||||||
|
continue
|
||||||
|
checked += 1
|
||||||
|
have = result.amount(key) if result.has(key) else None
|
||||||
|
if have != got[key][0]:
|
||||||
|
misses.append((name, key, got[key][0], have))
|
||||||
|
# 안전관리비 A·B 두 값도 원본 비고가 나란히 적음 — 작은 쪽 채택까지 확인.
|
||||||
|
pair = _RE_SAFETY_AB.search(got.get("safety_management_cost", (0, 0, 0, ""))[3])
|
||||||
|
if pair:
|
||||||
|
checked += 2
|
||||||
|
want = (_won(pair.group(1)), _won(pair.group(2)))
|
||||||
|
have_pair = (
|
||||||
|
result.amount("safety_management_cost_a"),
|
||||||
|
result.amount("safety_management_cost_b"),
|
||||||
|
)
|
||||||
|
if have_pair != want:
|
||||||
|
# 1원 이내는 절사 자리 차이(B 줄) — 그보다 크면 엔진이 틀린 것.
|
||||||
|
bucket = (
|
||||||
|
rounding if max(abs(h - w) for w, h in zip(want, have_pair)) <= 1 else misses
|
||||||
|
)
|
||||||
|
bucket.append((name, "safety_a_b", want, have_pair))
|
||||||
|
assert result.amount("safety_management_cost") == min(have_pair)
|
||||||
|
assert len(sheets) >= 6, len(sheets) # 실무 6건
|
||||||
|
# 54 = 요율이 보이는 줄 48(온전한 셋 ×10 + 빈칸 있는 셋 ×6) + 안전관리비 A·B 짝 6.
|
||||||
|
assert checked >= 54, checked
|
||||||
|
assert blank == 12, blank # 원본이 식을 안 보인 줄 — 3건 × 4줄
|
||||||
|
assert not misses, misses
|
||||||
|
# 절사 자리 차 — 지금 아는 것은 영월 안전관리비 B 한 줄뿐. 늘면 여기서 드러남.
|
||||||
|
assert len(rounding) == 1 and "영월" in rounding[0][0], rounding
|
||||||
|
|
||||||
|
|
||||||
|
def test_원가계산서_총공사비까지_사슬_재현(tmp_path) -> None:
|
||||||
|
"""⑤ 사슬 — 순공사원가·일반관리비·이윤·총원가·부가세·도급공사비·관급자재대·총공사비.
|
||||||
|
|
||||||
|
비고가 다 보이는 원본 셋(봉화·영월·울진 공통)만 사슬에 듦. 이윤은 두 걸음으로 가름 —
|
||||||
|
① **자동절사**(총공사비 1,000원 미만 → 이윤에서 ÷1.1 반올림)로 맞는가 — 봉화 339 · 영월 632
|
||||||
|
② 안 맞으면 그 차는 **설계자 명시 이윤조정**(★법대로 8-10, 프로그램이 역산하지 않음).
|
||||||
|
울진 공통 905,980 — 예산 맞춤이라 절사 규칙으로는 안 나옴(자동절사는 525).
|
||||||
|
조정액은 설계자 입력 칸이라 원본에서 읽어 넣음 — 이 벌이 재는 것은 **그 뒤 사슬**임.
|
||||||
|
도급공사비 차는 **원본에만 있는 줄**로만 설명됨 — 영월 국가지점번호판검증수수료 762,000.
|
||||||
|
"""
|
||||||
|
sheets = _cost_sheets()
|
||||||
|
if not sheets:
|
||||||
|
pytest.skip("실무 원본 XLSX 가 없음")
|
||||||
|
full = [s for s in sheets if all(s[1][k][1] is not None for k in _STATUTORY_KEYS if k in s[1])]
|
||||||
|
assert len(full) >= 3, [s[0] for s in full]
|
||||||
|
chained, automatic, manual, misses = 0, [], [], []
|
||||||
|
for index, (name, got, unknown) in enumerate(full):
|
||||||
|
data = _cost_input(got, tmp_path / f"chain{index}.json")
|
||||||
|
plain = calculate_cost(data)
|
||||||
|
gap = plain.amount("profit_before_adjustment") - got["profit"][0]
|
||||||
|
cut = calculate_cost(replace(data, cut_basis="grand_total", cut_unit_krw=1000))
|
||||||
|
(automatic if cut.amount("profit") == got["profit"][0] else manual).append((name, gap))
|
||||||
|
result = calculate_cost(replace(data, profit_adjustment_krw=gap))
|
||||||
|
extra = sum((amount for _label, amount in unknown), Decimal(0))
|
||||||
|
for key in (
|
||||||
|
"material_cost",
|
||||||
|
"labor_cost",
|
||||||
|
"expense",
|
||||||
|
"net_construction_cost",
|
||||||
|
"general_overhead",
|
||||||
|
"profit",
|
||||||
|
"total_cost",
|
||||||
|
"vat",
|
||||||
|
"contract_amount",
|
||||||
|
"owner_supplied_material_total",
|
||||||
|
"grand_total",
|
||||||
|
):
|
||||||
|
if key not in got:
|
||||||
|
continue
|
||||||
|
chained += 1
|
||||||
|
have = result.totals.get(key)
|
||||||
|
if have is None:
|
||||||
|
have = result.amount(key) if result.has(key) else None
|
||||||
|
# 도급공사비 아래는 원본에만 있는 줄만큼 낮게 나옴 — 그 몫을 덜어 견줌.
|
||||||
|
want = got[key][0] - (
|
||||||
|
extra if key in ("contract_amount", "grand_total") else Decimal(0)
|
||||||
|
)
|
||||||
|
if have != want:
|
||||||
|
misses.append((name, key, want, have))
|
||||||
|
assert chained >= 30, chained
|
||||||
|
assert not misses, misses
|
||||||
|
# 자동절사가 이윤을 그대로 맞추는 원본이 둘, 설계자 조정이 필요한 원본이 하나.
|
||||||
|
assert len(automatic) == 2, automatic
|
||||||
|
assert [g for _n, g in automatic] == [Decimal(339), Decimal(632)], automatic
|
||||||
|
assert [g for _n, g in manual] == [Decimal(905_980)], manual
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
④ 중기 시간당 사용료 `중기사용료` 시트 호표 — 손료·운전원·연료·잡품 ✅
|
④ 중기 시간당 사용료 `중기사용료` 시트 호표 — 손료·운전원·연료·잡품 ✅
|
||||||
③ 단가산출 Q 식 `단가산출근거` 시트 — 중기 성분 × 1/Q(0.1원) · 머리 원 미만 ✅
|
③ 단가산출 Q 식 `단가산출근거` 시트 — 중기 성분 × 1/Q(0.1원) · 머리 원 미만 ✅
|
||||||
① 일위대가·내역 절사 `일위대가표` 성분 소계 93.5% · `설계내역서` 줄 성분별 전수 ✅
|
① 일위대가·내역 절사 `일위대가표` 성분 소계 93.5% · `설계내역서` 줄 성분별 전수 ✅
|
||||||
|
⑤ 공사원가계산서 제비율·이윤·부가세·총공사비 — 옆 `test_b09_golden_cost_sheet.py`
|
||||||
|
|
||||||
⚠ 실무 원본은 **git 안 지식DB**라 어느 창에서든 돎. 원본이 없으면 건너뜀(시험 코드 탓이 아님).
|
⚠ 실무 원본은 **git 안 지식DB**라 어느 창에서든 돎. 원본이 없으면 건너뜀(시험 코드 탓이 아님).
|
||||||
⚠ 값을 여기서 짓지 않음 — 원본 칸을 읽어 **엔진 함수의 입력**으로만 씀.
|
⚠ 값을 여기서 짓지 않음 — 원본 칸을 읽어 **엔진 함수의 입력**으로만 씀.
|
||||||
@@ -50,6 +51,7 @@ def _workbooks() -> tuple[tuple[str, dict[str, list[tuple]]], ...]:
|
|||||||
"경비수량금액집계표",
|
"경비수량금액집계표",
|
||||||
"중기시간금액집계표",
|
"중기시간금액집계표",
|
||||||
"중기목록표",
|
"중기목록표",
|
||||||
|
"공사원가계산서",
|
||||||
)
|
)
|
||||||
found = []
|
found = []
|
||||||
for path in sorted(PRACTICE.rglob("*.xlsx")):
|
for path in sorted(PRACTICE.rglob("*.xlsx")):
|
||||||
|
|||||||
Reference in New Issue
Block a user