Files
Aislo/resources/tester/test_b08_face_dressing.py
T
eomsangdonandClaude Opus 5 ee9368ec66 feat(b08,b09): 면고르기 줄 — 밑수 = 초류종자살포 면적(덮어쓰기 칸 · 파종 0 이면 0 + 사유) · 절토면은 사면 조각 토사/암으로 갈라 암은 시공법(9-19-2·3) · 절토면 토질 · 성토면 시공·토질 칸(제안값 없음 · 비면 입력 사유) · 성토면 기계는 규격 미정 사유(브레인 판정 Ⓐ~Ⓓ)
실무 대조: 오솔길 BOM 6벌 면적 셈법 ±0.11% · 거창 줄 없음 · 영월 성토면 × 50% · 원문 9-19-1 성토면·9-19-2·3 식재 조건부
배정 프로젝트: 인계 줄 +3(성토면 12,350㎡ · 절토면 토사 3,085.51 · 암 2,262.62) 외 변화 0 · 본체 합계 105,512,060 그대로 · 규격 글로 시작하는 갈래는 규격에 한 번만(도자 운반 「토사 토사」 → 「토사」 · 금액 불변)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
2026-09-14 15:25:03 +09:00

212 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 - {normalize_variant_key("성토면 · 기계")}
# ── 내역 — 갈래가 서면 금액 · 기계는 사유 ─────────────────────────────
def test_내역은_암_리핑_줄에_금액이_서고_토사면_고르기는_아래층_사유를_그대로() -> None:
"""9-19-2(연암 · 리핑) 는 금액이 섬. 9-19-1 은 지금 공종 단위 「일부만」 표시(연암·보통암 갈래의
공기압축기 3.5 손료 미확보)에 걸려 흙 갈래까지 막힘 — 그 까닭이 덮이지 않고 올라와야 함.
⚠ 다음 차례(공기압축기 손료)가 서면 이 둘째 단언이 뒤집힘 — 그때 금액 단언으로 바꿀 것.
성토면·기계는 [주]·규격 미정 사유가 후보 목록 곁에 붙음(Ⓓ)."""
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 is None and "공기압축기" in fill.note, fill.note
machine = _handoff(face_dressing_cut_class=CUT_SAND, face_dressing_fill_class="성토면 · 기계")
row = next(
r for r in build_bill(machine).rows if r.code == "FP-09-19-01" and r.spec == "성토면"
)
assert row.amount_krw is None and "규격 미정" in row.note, row.note