diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities_Input.py b/B09_Estimation/B09_Estimation_BillOfQuantities_Input.py index 8731689e..1b2ae559 100644 --- a/B09_Estimation/B09_Estimation_BillOfQuantities_Input.py +++ b/B09_Estimation/B09_Estimation_BillOfQuantities_Input.py @@ -55,6 +55,11 @@ class HandoffWorkItem: application_ratio_breakdown: dict | None = None quantity_breakdown: dict | None = None #: 묶음 줄(옹벽처럼 품셈에 그 공종이 없는 것) — 무엇으로 이루어지는지. + #: ⭐ **조각 수량은 「합계」다 — 1단위당이 아니다.** B08 과의 계약이고 그쪽 시험이 지킨다 + #: (BOX 동바리 3.92 × 연장 10m · 포장 97.66㎥). 그러므로 `Σ(조각 단가 × 조각 수량)` 이 + #: **그 줄 전체 금액**이며 **줄 수량을 다시 곱하면 안 된다**. + #: ⚠ 이 줄이 안 적혀 있어서 금액이 배로 부풀었다 — 평떼 718.14㎡ 가 720배(42억). + #: 2026-09-15 랩탑 서브 실측 · 고친 자리는 `_Rows._composite_row`. composite_parts: tuple = () #: 묶음인데 아직 못 채운 조각 — 「단가 없음」과 「물량 없음」을 갈라 적는다. composite_not_ready: tuple = () diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py index 318a731a..dee8ee3e 100644 --- a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py +++ b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py @@ -171,15 +171,23 @@ def _composite_row( ) return row + # ⭐ **인계 조각 수량은 「합계」다** — B08 과의 계약이고 그쪽 시험이 지킨다 + # (BOX 동바리 3.92 × 연장 10m · 포장 97.66㎥). 그래서 위에서 더한 `money` 는 이미 + # **그 줄 전체 금액**이지 1단위 단가가 아니다. 종전엔 이것을 1단위로 보고 아래에서 + # 줄 수량을 **한 번 더** 곱해 금액이 배로 부풀었다 — 평떼 718.14㎡ 가 720배(42억)로 섰다 + # (2026-09-15 랩탑 서브 실측 · B09 시험이 줄 수량 1 로만 재서 못 잡았음). + # 계약은 그대로 두고 **읽는 쪽**을 고친다 — 합계를 그대로 쓰고 단가는 나눠서 보인다. money = money.floored(Decimal(1)) - line = bill_line(money, settle_quantity(row)) + quantity = settle_quantity(row) _set_unit(row, money) - row.unit_price_krw = round_at(money.total, OutputPlace.UNIT_PRICE_ROW) - row.amount_krw = line.total - row.material_krw = line.material - row.labor_krw = line.labor - row.expense_krw = line.expense - row.add_note("quantity", f"묶음 {len(item.composite_parts)}조각 합계") + row.unit_price_krw = ( + round_at(money.total / quantity, OutputPlace.UNIT_PRICE_ROW) if quantity else None + ) + row.amount_krw = money.total + row.material_krw = money.material + row.labor_krw = money.labor + row.expense_krw = money.expense + row.add_note("quantity", f"묶음 {len(item.composite_parts)}조각 합계 — 조각 수량이 이미 합계") _mark_manual_materials(row, [code for code, _ in row.parts], unit_prices, result) return row diff --git a/resources/tester/test_b09_composite_partial_parts.py b/resources/tester/test_b09_composite_partial_parts.py index 559363cb..b2a8bf2e 100644 --- a/resources/tester/test_b09_composite_partial_parts.py +++ b/resources/tester/test_b09_composite_partial_parts.py @@ -7,11 +7,13 @@ from __future__ import annotations +import pytest + from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill from B09_Estimation.B09_Estimation_UnitPrice import cached_build -def _composite(parts: list[dict]) -> dict: +def _composite(parts: list[dict], quantity: str = "1") -> dict: return { "work_items": [ { @@ -19,7 +21,7 @@ def _composite(parts: list[dict]) -> dict: "name": "시험 묶음", "spec": "", "unit": "개소", - "quantity": "1", + "quantity": quantity, "in_bill": True, "composite_parts": parts, "composite_not_ready": [], @@ -29,8 +31,8 @@ def _composite(parts: list[dict]) -> dict: } -def _line(parts: list[dict]): - rows = build_bill(_composite(parts)).rows +def _line(parts: list[dict], quantity: str = "1"): + rows = build_bill(_composite(parts, quantity)).rows return next(r for r in rows if r.code is None and r.name == "시험 묶음") @@ -52,3 +54,32 @@ def test_밑수_없는_조각도_같이_막음() -> None: def test_온전한_조각만이면_종전대로_금액() -> None: line = _line([{"code": "FP-09-15-02", "quantity": 2}]) assert line.amount_krw and line.amount_krw > 0, line.note + + +# ── 줄 수량이 1 이 아닐 때 — 2026-09-15 랩탑 서브가 찾은 부풀음 ── +#: ⭐ **인계 조각 수량은 「합계」다** — B08 계약이고 그쪽 시험이 지킴 +#: (BOX 동바리 3.92×10 · 포장 97.66㎥). +#: 그래서 `Σ(조각 단가 × 조각 수량)` 이 이미 **그 줄 전체 금액**이다. +#: 줄 수량을 또 곱하면 배로 부푼다. 종전 시험이 **줄 수량 1 로만 재서** 못 잡았다 — +#: 평떼 718.14㎡ 가 720배(42억)로 섰다. +_PART = {"code": "FP-09-15-02", "quantity": 2} + + +@pytest.mark.parametrize("quantity", ["2", "10", "718.14"]) +def test_묶음_금액은_줄_수량을_다시_안_곱한다(quantity) -> None: + """조각이 합계이므로 **줄 수량이 몇이든 금액은 같다**. 단가는 그 금액 ÷ 수량으로 보인다.""" + one = _line([_PART]) + many = _line([_PART], quantity) + assert one.amount_krw and many.amount_krw, (one.amount_krw, many.amount_krw) + assert many.amount_krw == one.amount_krw, (quantity, many.amount_krw, one.amount_krw) + assert many.unit_price_krw < one.unit_price_krw, (quantity, many.unit_price_krw) + + +def test_묶음_성분도_줄_수량만큼_안_부푼다() -> None: + """재·노·경 셋도 같이 봐야 함 — 합계만 보면 성분이 부푼 것을 놓친다.""" + one, many = _line([_PART]), _line([_PART], "10") + assert (many.material_krw, many.labor_krw, many.expense_krw) == ( + one.material_krw, + one.labor_krw, + one.expense_krw, + )