From 44b20ed21fa2632441134a4cae2592348772ead2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 13 Sep 2026 22:12:17 +0900 Subject: [PATCH] =?UTF-8?q?fix(b09):=20=EB=82=B4=EC=97=AD=20=EB=B0=9B?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=20=EC=9D=B4=EB=A6=84=20=EB=A7=9E=EC=B6=A4?= =?UTF-8?q?=20=EA=B1=B7=EA=B8=B0=20=E2=80=94=20=EC=9A=B4=EB=B0=98=20?= =?UTF-8?q?=EC=A4=84=20=EB=8B=A8=EA=B0=80=EB=A5=BC=20=EA=B7=B8=20=EC=A4=84?= =?UTF-8?q?=EC=97=90=EC=84=9C,=20=EC=B4=9D=20=EC=A0=88=EC=B7=A8=EB=9F=89?= =?UTF-8?q?=EC=9D=84=20=EC=BD=94=EB=93=9C=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운반 줄 단가를 이름으로 되찾던 자리를 인계 줄 → 내역 줄 짝으로(도자운반 토사·리핑암 같은 이름 실결함) - 총 절취량 검산을 줄 이름 「깎기」·「절취」가 아니라 코드 9-3·9-4·9-5 로 셈 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- .../B09_Estimation_BillOfQuantities.py | 25 +++++-- .../B09_Estimation_BillOfQuantities_Rows.py | 12 ---- resources/tester/test_b09_bill_code_link.py | 67 +++++++++++++++++++ 3 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 resources/tester/test_b09_bill_code_link.py diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities.py b/B09_Estimation/B09_Estimation_BillOfQuantities.py index 0c9e6578..ee755457 100644 --- a/B09_Estimation/B09_Estimation_BillOfQuantities.py +++ b/B09_Estimation/B09_Estimation_BillOfQuantities.py @@ -42,6 +42,10 @@ from B09_Estimation.B09_Estimation_UnitPrice import ( _ZERO = Decimal(0) +#: 총 절취량으로 셀 공종 — 9-3 토사깎기 · 9-4 암절취 · 9-5 발파암(인계 대응표 「흙깎기」 셋). +#: ⚠ 판에 묶인 절 번호임(명세 17장) — 판이 바뀌면 대응표와 함께 고칠 것. +CUT_CODE_PREFIXES = ("FP-09-03", "FP-09-04", "FP-09-05") + #: 자재 공급 구분이 안 갈린 값. B08 이 실제로 이 값을 보낸다(2026-09-08 실물 확인). #: **관급자재대에도, 도급 재료비에도 넣지 않는다** — 어느 쪽에 넣어도 총액이 틀린다. SUPPLY_UNKNOWN = "unknown" @@ -351,7 +355,6 @@ def _number_of(path: tuple[int, ...]) -> str: from B09_Estimation.B09_Estimation_BillOfQuantities_Rows import ( # noqa: E402 _composite_row, _excluded_row, - _haul_price_of, _leaf_row, _material_row, ) @@ -411,6 +414,9 @@ def build_bill( emitted: dict[str, str] = {} # 코드 → ITEM NO. counters: dict[str, int] = {} # 부모 ITEM NO. → 마지막 번호 + # 인계 줄 → 그 줄로 선 내역 줄. ⚠ 이름으로 되찾지 않음 — 도자운반 토사·리핑암처럼 + # 이름이 같은 줄이 둘이면 **첫 줄 단가**가 둘째에도 붙음(2026-09-13 축 C 조사 실결함). + row_of: dict[int, BillRow] = {} def next_number(parent_no: str) -> str: counters[parent_no] = counters.get(parent_no, 0) + 1 @@ -440,12 +446,14 @@ def build_bill( # 물려주면 ITEM NO. 가 겹쳐 어느 줄인지 못 가린다. 머리글만 물려준다. item_no = next_number(parent_no) emitted.setdefault(leaf.code, item_no) - result.rows.append(_leaf_row(item_no, leaf, item, unit_prices, result)) + row_of[id(item)] = _leaf_row(item_no, leaf, item, unit_prices, result) + result.rows.append(row_of[id(item)]) # ── 1-2) 묶음 줄 ────────────────────────────────────────────────────────── for item in composites: counters[""] = counters.get("", 0) + 1 - result.rows.append(_composite_row(str(counters[""]), item, unit_prices, result)) + row_of[id(item)] = _composite_row(str(counters[""]), item, unit_prices, result) + result.rows.append(row_of[id(item)]) # ── 2) 공종을 못 고른 줄 — 이름째 남긴다 ──────────────────────────────────── for item in orphans: @@ -496,7 +504,8 @@ def build_bill( haul_rows = [ { "equipment": item.haul_equipment, - "unit_price_krw": _haul_price_of(item, result), + # 그 줄에 실제로 붙은 단가. 안 선 줄은 0 — ㉡ 검사에 넘길 값이다. + "unit_price_krw": getattr(row_of.get(id(item)), "unit_price_krw", None) or _ZERO, } for item in work_items if item.haul_equipment @@ -504,8 +513,14 @@ def build_bill( check_free_haul_not_priced(haul_rows=haul_rows) # ㉡ 보조 — 운반토량 합이 총 절취량을 넘지 않는가(같은 흙을 두 번 세지 않았는가). + # 절취 = **코드로** 가름(9-3 토사깎기 · 9-4 암절취 · 9-5 발파암 — 인계 대응표의 「흙깎기」 셋). + # ⚠ 종전엔 줄 이름에 「깎기」·「절취」가 드는지로 셌음 — 이름이 바뀌면 검산이 조용히 멈춤. cut_total = sum( - (item.quantity for item in work_items if "깎기" in item.name or "절취" in item.name), + ( + item.quantity + for item in work_items + if str(item.work_item_code or "").startswith(CUT_CODE_PREFIXES) + ), _ZERO, ) haul_total = sum((item.quantity for item in work_items if item.haul_equipment), _ZERO) diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py index a5c37a82..350b0df9 100644 --- a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py +++ b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py @@ -9,8 +9,6 @@ from __future__ import annotations -from decimal import Decimal - from B09_Estimation.B09_Estimation_BillOfQuantities import ( SUPPLY_OWNER, SUPPLY_UNKNOWN, @@ -25,16 +23,6 @@ from B09_Estimation.B09_Estimation_BillOfQuantities import ( from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild, find_variant_code -_ZERO = Decimal(0) - - -def _haul_price_of(item: HandoffWorkItem, result: BillResult) -> Decimal: - """그 운반 줄에 실제로 붙은 단가. 안 붙었으면 0 — ㉡ 검사에 넘길 값이다.""" - for row in result.rows: - if row.name == item.name and row.unit_price_krw is not None: - return row.unit_price_krw - return _ZERO - def _composite_row( item_no: str, diff --git a/resources/tester/test_b09_bill_code_link.py b/resources/tester/test_b09_bill_code_link.py new file mode 100644 index 00000000..e93736c4 --- /dev/null +++ b/resources/tester/test_b09_bill_code_link.py @@ -0,0 +1,67 @@ +"""B08 → B09 내역 받는 문 — 이름이 아니라 코드로 잇기 (2026-09-13, PLAN 6장 첫 일감 ①). + +겨누는 것 + ① 운반 줄 단가는 **그 줄로 선 내역 줄**에서 — 이름이 같은 줄(도자운반 토사·리핑암)이 둘이어도 + 첫 줄 단가가 둘째에 안 붙음(종전 `row.name == item.name` 실결함) + ② 총 절취량은 **코드**(9-3·9-4·9-5)로 셈 — 줄 이름에 「깎기」가 없어도 검산이 돎 +""" + +from __future__ import annotations + +from decimal import Decimal +from functools import lru_cache + +import pytest + +import B09_Estimation.B09_Estimation_BillOfQuantities as bill_module +from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill +from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices + + +@lru_cache(maxsize=1) +def _build(): + return build_unit_prices() + + +def _row(code: str, name: str, quantity: float, **extra) -> dict: + return {"work_item_code": code, "name": name, "unit": "㎥", "quantity": quantity, **extra} + + +def test_운반_줄_단가는_이름이_아니라_그_줄에서_읽는다(monkeypatch: pytest.MonkeyPatch) -> None: + seen: list[list[dict]] = [] + monkeypatch.setattr( + bill_module, "check_free_haul_not_priced", lambda haul_rows: seen.append(haul_rows) + ) + payload = { + "work_items": [ + _row("FP-10-11", "dozer 운반", 10, haul_equipment="dozer", variant_value="토사"), + # 같은 이름 · 표에 없는 갈래라 단가가 안 섬. + _row("FP-10-11", "dozer 운반", 4, haul_equipment="dozer", variant_value="없는갈래"), + ], + "materials": [], + } + bill = build_bill(payload, build=_build()) + priced = [r for r in bill.rows if not r.is_group] + assert priced[0].unit_price_krw and priced[0].unit_price_krw > 0 + assert priced[1].unit_price_krw is None + prices = [row["unit_price_krw"] for row in seen[0]] + assert prices == [priced[0].unit_price_krw, Decimal(0)] # 종전엔 둘 다 첫 줄 단가 + + +def test_총_절취량은_코드로_센다(monkeypatch: pytest.MonkeyPatch) -> None: + calls: list[dict] = [] + monkeypatch.setattr( + bill_module, "check_haul_volume_within_cut", lambda **kwargs: calls.append(kwargs) + ) + payload = { + "work_items": [ + # 이름에 「깎기」가 없음(마스터 표기 「토사깍기」) — 코드로만 절취임을 앎. + _row("FP-09-03-02", "토사깍기", 10), + _row("FP-09-12-01", "측구터파기", 3), # 절취 셈에 안 듦(종전과 같음) + _row("FP-10-11", "dozer 운반", 5, haul_equipment="dozer", variant_value="토사"), + ], + "materials": [], + } + build_bill(payload, build=_build()) + assert calls and calls[0]["total_cut_volume_m3"] == Decimal(10) + assert calls[0]["haul_volume_total_m3"] == Decimal(5)