별표2 (5)(가) 열셋째. 근거는 줄마다 이미 있었고 묶는 자리만 없었음. 원가계산에 「산출기초」 탭으로 세움. 넷으로 접음 — ① 어느 판으로 계산했나(데이터 기준일·지문) ② 무엇을 골랐나(고른 값만) ③ 공종마다 무엇을 근거로 했나(줄 문구 그대로) ④ 못 채운 자리(0 으로 안 때운 자리). - 판 목록은 장부(_manifest.json)를 그대로 읽음 — 목록을 따로 적으면 한쪽만 고쳐짐. - ⚠ 여기서 값을 다시 계산하지 않음 — 금액 칸이 아예 없음(두 벌 방지, 시험으로 못 박음). ⇒ 「설계서 구성」의 산출기초가 반쪽 → 있음. 법이 정한 13 중 9 가 서고, 우리가 더 낼 것은 공사설명서 하나(서식·설계하중 표기는 사용자에게 받아야 함). 곁들여: 자재총괄의 「미분류」를 「미정(발주기관 결정)」으로 고침 — 발주기관이 정할 자리인데 우리가 못 만든 것처럼 읽히던 문구(V-14). 값·판정은 그대로. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
3.5 KiB
Python
88 lines
3.5 KiB
Python
"""산출기초 — 줄에 달린 근거를 한 장으로 (2026-09-09).
|
|
|
|
별표2 (5)(가) 열셋째. **새로 세는 장이 아니다** — 이미 줄마다 달려 있는 근거를 모은다.
|
|
|
|
⚠ 겨누는 것 다섯
|
|
① 어느 판으로 계산했나 — **장부(`_manifest.json`)를 그대로** 읽는다(목록을 따로 안 적음)
|
|
② 고른 값만 싣는다 — 안 고른 칸은 기본값이라 줄이 안 선다
|
|
③ 근거는 **줄에 달린 문구 그대로** — 여기서 새로 짓지 않는다
|
|
④ 못 채운 자리는 **사유째** 모은다(0 으로 때운 자리가 아님)
|
|
⑤ 여기서 **값을 다시 계산하지 않는다** — 일위대가가 낸 값과 두 벌이 되면 안 된다
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from B09_Estimation.B09_Estimation_BasisSheet import ( # noqa: E402
|
|
basis_sheet,
|
|
chosen_conditions,
|
|
dataset_versions,
|
|
work_item_basis,
|
|
)
|
|
from B09_Estimation.B09_Estimation_UnitPrice import cached_build # noqa: E402
|
|
|
|
|
|
def test_판_목록은_장부를_그대로_읽는다() -> None:
|
|
manifest = json.loads(
|
|
(ROOT / "resources" / "data_cost_input_value" / "_manifest.json").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
ledger = {str(row["file"]) for row in manifest["files"]}
|
|
rows = dataset_versions()
|
|
assert ledger <= {row["file"] for row in rows}
|
|
# 공종 마스터도 함께 실린다 — 품셈 원문을 정규화한 판이다.
|
|
assert any(row["dataset_id"] == "work_item_master" for row in rows)
|
|
for row in rows:
|
|
assert row["effective_date"], row
|
|
|
|
|
|
def test_고른_값만_싣는다() -> None:
|
|
assert chosen_conditions({}) == []
|
|
assert chosen_conditions({"misc_material_percent": "", "fuel_region": ""}) == []
|
|
picked = chosen_conditions({"fuel_region": "01", "transport_distance_km": "12"})
|
|
assert [row["value"] for row in picked] == ["01", "12"]
|
|
|
|
|
|
def test_품_할증은_고른_것을_풀어_적는다() -> None:
|
|
picked = chosen_conditions({"labor_surcharge": {"1-4-1": "1-4-1:0"}})
|
|
assert len(picked) == 1
|
|
assert "10%" in picked[0]["item"]
|
|
assert "작업시기" in picked[0]["value"]
|
|
|
|
|
|
def test_근거는_줄에_달린_문구_그대로다() -> None:
|
|
build = cached_build()
|
|
rows = {row["code"]: row for row in work_item_basis(build)}
|
|
assert rows, "근거가 달린 공종이 하나도 없습니다"
|
|
for code, row in rows.items():
|
|
notes = {
|
|
detail.note.strip()
|
|
for detail in build.book.details.get(code, [])
|
|
if detail.note and detail.note.strip()
|
|
}
|
|
# 줄에 없는 문구를 지어내지 않는다(공종 단위 출처 한 줄만 더 붙을 수 있다).
|
|
assert notes <= set(row["notes"])
|
|
|
|
|
|
def test_못_채운_자리는_사유째_모은다() -> None:
|
|
sheet = basis_sheet(cached_build(), {})
|
|
assert sheet["gaps"], "못 채운 자리가 하나도 없다고 나옵니다 — 셈이 이상합니다"
|
|
for row in sheet["gaps"]:
|
|
assert row["kind"] and row["code"] and row["reason"]
|
|
|
|
|
|
def test_값을_다시_계산하지_않는다() -> None:
|
|
"""⑤ 이 장에는 금액 칸이 없다 — 금액은 일위대가·내역서 몫이다."""
|
|
sheet = basis_sheet(cached_build(), {})
|
|
text = json.dumps(sheet, ensure_ascii=False)
|
|
assert "총액" not in text and "합계 금액" not in text
|
|
for row in sheet["work_items"]:
|
|
assert set(row) == {"code", "name", "unit", "notes"}
|