공종 마스터·자원 축 줄·B08 인계 줄마다 공종 코드가 본 판을 실음(명세 17장 규칙 3). 판이 다른 인계 줄은 내역서에서 「확인 대기」로 빠지고, 코드에 박힌 절 번호(합산 단계·표 형태 판정·가드 목록)는 마스터 판과 다르면 마스터 빌드·단가 조립을 멈춤(규칙 4). 판 기준은 common_util_pum_edition 한 곳. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
214 lines
7.7 KiB
Python
214 lines
7.7 KiB
Python
"""인계 계약 — **내보내는 키가 계약에 적힌 것과 같은지** (2026-09-07 ㉒ 앞).
|
||
|
||
⚠ 오늘 이 병이 계약 양쪽에서 하나씩 났다.
|
||
· 우리 쪽 — 레지스트리 키(`back_len_cm`)와 엔진이 읽는 키가 달라 **저장값이 안 닿음**.
|
||
· 받는 쪽 — 우리가 보낸 `composite_parts`·`quantity_gross` 등을 **안 읽고 버림**.
|
||
**「보내는 쪽은 보냈는데 받는 쪽이 안 읽는」 자리는 양쪽 다 조용하다.**
|
||
|
||
⇒ 키를 늘릴 때 **계약에 안 적고 늘리면 받는 쪽이 영영 모른다.** 이 시험이 그것을 막는다.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff # noqa: E402
|
||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table as build_unit # noqa: E402
|
||
|
||
#: **작업 공종 줄**이 내보내는 칸. 늘릴 때는 계약(8-2)에도 적고 받는 쪽에 알릴 것.
|
||
WORK_ITEM_KEYS = {
|
||
"work_item_code",
|
||
"name",
|
||
"spec",
|
||
"unit",
|
||
"quantity",
|
||
"quantity_gross",
|
||
"application_ratio_pct",
|
||
"application_ratio_breakdown",
|
||
"quantity_breakdown",
|
||
"ground_class",
|
||
"excavation_method",
|
||
"haul_distance_m",
|
||
"haul_equipment",
|
||
"station_from",
|
||
"station_to",
|
||
"spec_detail",
|
||
"composite_parts",
|
||
"structure_kind",
|
||
"variant_axis",
|
||
"variant_value",
|
||
# ⚠ 갈래 축이 둘 이상인 자리(돌쌓기 = 뒷길이 × 돌 종류) — 2026-09-08 추가.
|
||
"secondary_axes",
|
||
"spec_class",
|
||
"spec_class_basis",
|
||
"blocked_kind",
|
||
"blocked_reason",
|
||
"composite_not_ready",
|
||
"in_bill",
|
||
"in_bill_reason",
|
||
"origin",
|
||
# 공종 코드가 본 품셈 판 — B09 가 마스터 판과 다르면 값을 안 씀(명세 17장 · 2026-09-13 추가).
|
||
"pum_edition",
|
||
}
|
||
|
||
#: **자재 줄**이 내보내는 칸. ⚠ 여기에 `work_item_code` 가 들어가면 안 된다(축이 다르다).
|
||
MATERIAL_KEYS = {
|
||
"material_name",
|
||
"spec",
|
||
"unit",
|
||
"net_amount",
|
||
"total_amount",
|
||
"surcharge_pct",
|
||
"surcharge_note",
|
||
"supply_type",
|
||
"install_by",
|
||
"source_structure",
|
||
}
|
||
|
||
|
||
def _rows() -> dict:
|
||
unit = build_unit(
|
||
[
|
||
{
|
||
"structure_id": "w1",
|
||
"type_id": "retaining_wall",
|
||
"start_m": 100.0,
|
||
"end_m": 110.0,
|
||
"options": {"form": "반중력식", "height_m": 2.0, "length_m": 10.0},
|
||
}
|
||
],
|
||
{"retaining_wall": "옹벽"},
|
||
)
|
||
from B08_Quantity.B08_Quantity_Engine_MaterialSummary import build_table as build_material
|
||
|
||
haul = {
|
||
"rows": [
|
||
{
|
||
"equipment": "dozer",
|
||
"ground": "토사",
|
||
"volume_m3": 10.0,
|
||
"average_distance_m": 40.0,
|
||
"in_bill": True,
|
||
}
|
||
]
|
||
}
|
||
summary = {"rows": [{"group": "흙깎기", "item": "토사", "unit": "㎥", "amount": 5.0}]}
|
||
return build_handoff(
|
||
summary_table=summary,
|
||
haul_table=haul,
|
||
unit_quantity_table=unit,
|
||
material_table=build_material(unit),
|
||
)
|
||
|
||
|
||
def test_작업_공종_줄의_칸이_계약과_같을것() -> None:
|
||
"""⚠ 계약에 없는 칸을 늘리면 받는 쪽이 영영 모른다."""
|
||
for row in _rows()["work_items"]:
|
||
extra = set(row) - WORK_ITEM_KEYS
|
||
assert not extra, f"계약에 없는 칸: {extra}"
|
||
missing = WORK_ITEM_KEYS - set(row)
|
||
assert not missing, f"계약에 있는데 안 보내는 칸: {missing}"
|
||
|
||
|
||
def test_자재_줄의_칸이_계약과_같을것() -> None:
|
||
for row in _rows()["materials"]:
|
||
assert set(row) == MATERIAL_KEYS, set(row) ^ MATERIAL_KEYS
|
||
|
||
|
||
def test_자재_줄에_공종코드가_없을것() -> None:
|
||
"""축이 둘이라는 것은 주석이 아니라 계약으로 지킨다."""
|
||
assert "work_item_code" not in MATERIAL_KEYS
|
||
for row in _rows()["materials"]:
|
||
assert "work_item_code" not in row
|
||
|
||
|
||
def test_막힌_까닭이_세_갈래_중_하나일것() -> None:
|
||
"""받는 쪽이 「입력하면 풀리는 것」과 「우리가 만들어야 하는 것」을 갈라야 한다."""
|
||
from B08_Quantity.B08_Quantity_Engine_Handoff import (
|
||
BLOCKED_FORMULA_MISSING,
|
||
BLOCKED_INPUT_MISSING,
|
||
BLOCKED_UNIT_DATA_MISSING,
|
||
)
|
||
|
||
allowed = {None, BLOCKED_INPUT_MISSING, BLOCKED_UNIT_DATA_MISSING, BLOCKED_FORMULA_MISSING}
|
||
for row in _rows()["work_items"]:
|
||
assert row["blocked_kind"] in allowed
|
||
|
||
|
||
def test_막힌_줄에는_사유가_반드시_있을것() -> None:
|
||
"""갈래만 있고 사유가 없으면 받는 쪽 화면이 빈다."""
|
||
for row in _rows()["work_items"]:
|
||
if row["blocked_kind"]:
|
||
assert row["blocked_reason"], row["name"]
|
||
|
||
|
||
# ── 갈래 키를 우리가 조립하지 않는다 (2026-09-07 계약 변경) ─────────
|
||
#
|
||
# ⚠ 품셈 원문이 물결표를 섞어 쓴다 — 13-06-01·02 는 `∼`(U+223C), 13-06-03 은 `~`(U+FF5E).
|
||
# 두 창이 각자 키 문자열을 만들면 **글자 하나로 영영 안 맞는다.**
|
||
# ⇒ 우리는 **축과 저장 원본값**만 보내고, 원문을 읽는 쪽이 그 표기를 흡수한다.
|
||
|
||
|
||
def _structure_rows() -> list[dict]:
|
||
out = []
|
||
for type_id, options in (
|
||
("masonry_wet", {"height_m": 1.5, "length_m": 10.0, "back_len_cm": "45"}),
|
||
(
|
||
"boulder_masonry",
|
||
{"height_m": 2.0, "length_m": 10.0, "stone_cm": "60~80", "bond": "찰쌓기"},
|
||
),
|
||
):
|
||
unit = build_unit(
|
||
[
|
||
{
|
||
"structure_id": "x",
|
||
"type_id": type_id,
|
||
"start_m": 0.0,
|
||
"end_m": 10.0,
|
||
"options": options,
|
||
}
|
||
],
|
||
{type_id: type_id},
|
||
)
|
||
out.extend(build_handoff(unit_quantity_table=unit)["work_items"])
|
||
return out
|
||
|
||
|
||
def test_축과_원본값을_그대로_보낼것() -> None:
|
||
rows = {row["variant_axis"]: row for row in _structure_rows() if row["variant_axis"]}
|
||
assert rows["back_len_cm"]["variant_value"] == "45"
|
||
assert rows["stone_cm"]["variant_value"] == "60~80"
|
||
|
||
|
||
def test_갈래_키_문자열을_코드에_붙이지_말것() -> None:
|
||
"""⚠ 이 시험이 계약 변경을 지킨다 — `#55cm이하`·`#직경60㎝이상∼80㎝미만` 을 만들면 깨진다.
|
||
|
||
⚠ **메/찰은 예외다** — 그것은 키 표기가 아니라 **공종 자체가 갈리는 의미 판정**이라
|
||
코드가 `FP-13-06-01`/`-02` 로 통째로 달라진다.
|
||
"""
|
||
for row in _structure_rows():
|
||
code = str(row["work_item_code"] or "")
|
||
assert "#" not in code, f"갈래 키를 코드에 붙였다: {code}"
|
||
assert "㎝" not in code and "cm" not in code
|
||
|
||
|
||
def test_원본값을_가공하지_말것() -> None:
|
||
"""물결표·공백을 우리가 손대면 받는 쪽이 원문과 대조를 못 한다."""
|
||
rows = {row["variant_axis"]: row for row in _structure_rows() if row["variant_axis"]}
|
||
raw = rows["stone_cm"]["variant_value"]
|
||
assert raw == "60~80" # 물결표 종류를 바꾸거나 「㎝」를 붙이지 않는다
|
||
|
||
|
||
def test_돌_종류를_둘째_축으로_보낼것() -> None:
|
||
"""⚠ 품셈 13-4 품은 [주]② 로 **깬돌·깬잡석 전용**이고 돌 종류로 갈리는 표는 13-5 다.
|
||
|
||
어느 공종으로 볼지는 사용자 확정 대기라 **공종을 바꾸지 않고 원본값만** 실어 보낸다.
|
||
"""
|
||
rows = {row["variant_axis"]: row for row in _structure_rows() if row["variant_axis"]}
|
||
axes = rows["back_len_cm"]["secondary_axes"] or []
|
||
assert [item["axis"] for item in axes] == ["stone_kind"]
|