Files
Aislo/resources/tester/test_b08_face_dressing.py
T

215 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""면고르기 줄 — 2026-09-14 브레인 판정 Ⓐ~Ⓓ.
종전: 면적(face_dressing 성·절토)은 있는데 토공집계가 줄을 안 세워 인계가 안 보냄(내역 0줄).
실무 대조: 오솔길 BOM 6벌 면적 셈법 ±0.11% 일치 · 그러나 내역엔 사면적 통째가 아님
(거창 줄 없음 · 영월 성토면 × 50%) — 원문 9-19-1 성토면 [주] 「식재를 위한」 · 9-19-2·3 [주]①
「식재기반 조성에만」 → Ⓐ 밑수 = 초류종자살포(파종) 면적 · 덮어쓰기 칸 · 파종 0 이면 0 + 사유
Ⓑ 절토면 토사/암 갈라 암은 시공법(리핑 9-19-2 · 발파 9-19-3) Ⓒ 토질·시공 칸 둘 · 제안값 없음
Ⓓ 성토면 기계(굴착기 0.6㎥ 형식 둘)는 사유로.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(Path(__file__).resolve().parent))
from test_b08_slope_area import 절토_설계선 # noqa: E402
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import ( # noqa: E402
FACE_DRESSING_CUT_CLASSES,
FACE_DRESSING_FILL_CLASSES,
SummaryInput,
build_rows,
build_table,
)
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff # noqa: E402
from B08_Quantity.B08_Quantity_Engine_SlopeArea import build_table as slope_table # noqa: E402
from B08_Quantity.B08_Quantity_Engine_SlopeLength import ( # noqa: E402
StationSlope,
station_slope,
)
CUT_SAND = "절토면 · 모래ㆍ사질토ㆍ점토ㆍ점질토"
FILL_CLAY = "성토면 · 인력 · 점토 또는 점질토"
# ── 사면길이 · 면적 — 절토면을 토사/암으로 ─────────────────────────────
def test_절토_사면길이가_토사_암으로_갈림() -> None:
slope = station_slope(20.0, 절토_설계선())
assert slope.cut_soil_length_m + slope.cut_rock_length_m == pytest.approx(slope.cut_length_m)
soil = sum(s.length_m for s in slope.segments if s.role == "cut" and s.material == "soil")
assert slope.cut_soil_length_m == pytest.approx(soil) and soil > 0
assert slope.cut_rock_length_m > 0
def test_2단이_아닌_측점은_설계_지반_프리셋으로_가르고_모르면_안_가름() -> None:
"""토사 프리셋은 비탈 전체가 토사 · 암 프리셋 1단은 전체가 암 · 프리셋이 없으면 못 가름."""
one = {**절토_설계선(), "two_stage_slope": False}
soil = station_slope(20.0, {**one, "geometry_preset": "soil"})
assert (
soil.cut_soil_length_m == pytest.approx(soil.cut_length_m) and soil.cut_rock_length_m == 0
)
rock = station_slope(20.0, {**one, "geometry_preset": "rock"})
assert (
rock.cut_rock_length_m == pytest.approx(rock.cut_length_m) and rock.cut_soil_length_m == 0
)
unknown = station_slope(20.0, one)
assert unknown.cut_length_m > 0 and unknown.cut_soil_length_m == unknown.cut_rock_length_m == 0
def test_면적_합계에_절토면_토사_암이_평균단면적법으로_실림() -> None:
one = StationSlope(
chainage_m=0.0, cut_length_m=4.0, cut_soil_length_m=3.0, cut_rock_length_m=1.0
)
two = StationSlope(
chainage_m=20, cut_length_m=6.0, cut_soil_length_m=2.0, cut_rock_length_m=4.0
)
slopes = [one, two]
totals = slope_table(slopes)["totals"]
assert totals["face_dressing_cut"] == pytest.approx(100.0)
assert totals["face_dressing_cut_soil"] == pytest.approx(50.0)
assert totals["face_dressing_cut_rock"] == pytest.approx(50.0)
# ── 토공집계 — 밑수 = 파종 면적 ──────────────────────────────────────
def _source(**extra) -> SummaryInput:
base = dict(
slope_totals={
"face_dressing_fill": 1000.0,
"face_dressing_cut": 500.0,
"face_dressing_cut_soil": 300.0,
"face_dressing_cut_rock": 200.0,
},
rock_classes=["토사", "연암", "경암"],
rock_ratios_pct={"연암": 50.0, "경암": 50.0},
application_ratios={"seed_spray_fill": 0.5, "seed_spray_cut": 1.0},
)
base.update(extra)
return SummaryInput(**base)
def _faces(source: SummaryInput) -> list[tuple[str, str, float, bool]]:
return [
(row.spec, row.item, round(row.amount, 6), row.in_bill)
for row in build_rows(source)
if row.group == "면고르기"
]
def test_면고르기는_파종_면적을_따라가고_절토면은_토사_암_갈래로() -> None:
"""성토 1000 × 파종 50% = 500 · 절토 500 × 100% → 토사 300 · 암 200 을 구성비로 연암·경암."""
assert _faces(_source()) == [
("성토면", "", 500.0, True),
("절토면", "토사", 300.0, True),
("절토면 · 연암", "연암", 100.0, True),
("절토면 · 경암", "경암", 100.0, True),
]
fill = next(r for r in build_table(_source())["rows"] if r["spec"] == "성토면")
assert fill["amount_gross"] == 1000.0 and fill["application_ratio_pct"] == 50.0
assert "초류종자살포" in fill["note"]
def test_덮어쓴_면적이_이기고_비고에_파종_면적과_나란히() -> None:
source = _source(face_dressing_area_m2={"fill": 123.0, "cut": 100.0})
faces = _faces(source)
assert faces[0] == ("성토면", "", 123.0, True)
assert faces[1] == ("절토면", "토사", 60.0, True) # 100 × 300/500
fill = next(r for r in build_rows(source) if r.group == "면고르기")
assert "123" in fill.note and "500" in fill.note
def test_파종_면적이_0이면_면고르기도_0이고_내역에_안_섬() -> None:
source = _source(application_ratios={"seed_spray_fill": 0.0, "seed_spray_cut": 0.0})
faces = _faces(source)
assert faces[0] == ("성토면", "", 0.0, False)
rows = [r for r in build_rows(source) if r.group == "면고르기"]
assert all("파종 면적이 0" in r.note for r in rows)
# ── 인계 — 코드 · 갈래 · 칸 비면 사유 ─────────────────────────────────
def _handoff(**extra) -> dict:
summary = build_table(_source())
return build_handoff(
summary_table=summary,
ground_classes=["토사", "연암", "경암"],
ground_methods={"연암": "ripping", "경암": "blasting"},
**extra,
)
def _items(handoff: dict) -> list[dict]:
return [row for row in handoff["work_items"] if row["name"] == "면고르기"]
def test_면고르기_인계는_성토면_절토면_토사_암을_각_절로() -> None:
items = _items(_handoff(face_dressing_cut_class=CUT_SAND, face_dressing_fill_class=FILL_CLAY))
got = [(r["spec"], r["ground_class"], r["work_item_code"], r["variant_value"]) for r in items]
assert got == [
("성토면", None, "FP-09-19-01", FILL_CLAY),
("절토면", "토사", "FP-09-19-01", CUT_SAND),
("절토면 · 연암", "연암", "FP-09-19-02", None),
("절토면 · 경암", "경암", "FP-09-19-03", None),
]
assert not any(r["blocked_kind"] for r in items)
def test_토질_시공_칸이_비면_금액_없이_입력_사유() -> None:
items = _items(_handoff())
assert [r["spec"] for r in items[:2]] == ["성토면", "절토면"] # 줄이 없으면 헛통과 막음
for row in items[:2]:
assert row["blocked_kind"] == "input_missing", row
assert "산출 조건" in row["blocked_reason"]
def test_고르는_갈래는_원문_표_두_벌과_마스터_갈래_키가_같음() -> None:
"""B08 칸의 선택지와 B09 표 읽기가 세운 갈래 — 두 벌이 갈리면 빨강."""
from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master
from B09_Estimation.B09_Estimation_UnitPrice import normalize_variant_key
node = next(
n for n in load_work_item_master()["work_items"] if n["work_item_code"] == "FP-09-19-01"
)
keys = {normalize_variant_key(k) for k in node["variant_keys"]}
choices = {
normalize_variant_key(k) for k in FACE_DRESSING_CUT_CLASSES + FACE_DRESSING_FILL_CLASSES
}
assert keys == choices
# ── 내역 — 갈래가 서면 금액 · 기계는 사유 ─────────────────────────────
def test_내역은_갈래가_서면_금액이_서고_성토면_기계는_무한궤도_제안() -> None:
"""9-19-2(연암 · 리핑) · 9-19-1(성토면 인력 점토 500㎡ × 3,269원) 금액이 섬.
ⓘ 9-19-1 은 첫 커밋(ee9368ec) 때 공기압축기 3.5 손료 미확보(공종 단위 「일부만」)에 막혀 있었고
운전경비 표 괄호 딸림값 파서 고침으로 풀림(같은 날 · `test_b09_operating_paren_spec`).
성토면·기계는 굴착기 형식 두 갈래 · 제안 무한궤도(영월 실무 「06M3 B/H」 · 브레인 ②)."""
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill
handoff = _handoff(face_dressing_cut_class=CUT_SAND, face_dressing_fill_class=FILL_CLAY)
rows = [r for r in build_bill(handoff).rows if str(r.code).startswith("FP-09-19-0")]
ripping = next(r for r in rows if r.code == "FP-09-19-02")
assert ripping.amount_krw and int(ripping.amount_krw) > 0, ripping.note
fill = next(r for r in rows if r.code == "FP-09-19-01" and r.spec == FILL_CLAY)
assert fill.amount_krw == 500 * 3269, fill.note
track = "성토면 · 기계 · 무한궤도"
machine = _handoff(face_dressing_cut_class=CUT_SAND, face_dressing_fill_class=track)
row = next(r for r in build_bill(machine).rows if r.code == "FP-09-19-01" and r.spec == track)
assert row.amount_krw and int(row.amount_krw) > 0, row.note
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import FACE_DRESSING_FILL_SUGGESTED
value, basis = FACE_DRESSING_FILL_SUGGESTED
assert value == track and "06M3 B/H" in basis and "타이어" in basis