From f2d12afef71af8c4ba8fc9509022aa464efb35ea Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 23:26:35 +0900 Subject: [PATCH] =?UTF-8?q?feat(B09):=20=E3=89=A4=20=EC=97=B4=20=EB=B0=A9?= =?UTF-8?q?=ED=96=A5=20=EA=B2=80=EC=82=AC=20=EC=B6=94=EA=B0=80=20=E2=80=94?= =?UTF-8?q?=20=ED=96=89=20=EA=B2=80=EC=82=AC=EB=A1=9C=20=EC=95=88=20?= =?UTF-8?q?=EC=9E=A1=ED=9E=88=EB=8A=94=20=EC=9D=B4=EC=A4=91=20=ED=95=A9?= =?UTF-8?q?=EC=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 배분 창이 보고 숫자를 더해 보고 **노무 열만 5,162 어긋남**을 짚음. 확인 결과: - **값은 멀쩡했음. 내 보고가 줄 하나를 빠뜨린 것**(「제근」 본표는 보통인부 줄이 **둘**임 — 굴착기 규격 2종에 각각 붙음). 실제 열 합은 재료 20,956.3 · 노무 54,943.2 · 경비 22,393.4 · 합계 98,292.9 로 표시와 **전건 일치**. - **다만 지적한 「열 방향 검사가 비어 있다」는 실재했음.** `TC = NC + GC + JC` 는 **행 방향** 검사라, 같은 성분을 두 층에서 세는 어긋남(행마다는 다 맞고 열 합만 갈림)을 못 잡음. 예 — 기계 줄 안에 든 조종원 노무가 별도 노무 줄로도 서는 경우. - `check_column_sums()` 추가하고 `detail_of()` 에서 **실제로 호출**. 표시 합계가 상세 줄의 열별 합과 다르면 멈춤. - 테스트 둘 — ① 노무 열만 부푼 모양을 만들어 **행 검사가 통과하는데 열 검사가 잡는지** 확인 ② 실제 「제근」 본표가 열 검사를 통과하고 보통인부 줄이 둘인지 확인. ⇒ 이중계상 감시가 넷(㉠할증·㉡무대·㉢배합·㉣작업효율) + **방향 검사 ㉤** 로 늘어남. 자체검증 — 신규 2건 포함 `pytest tmp/tests/ -q` 118 passed · ruff 통과. Co-Authored-By: Claude Opus 5 (1M context) --- B09_Estimation/B09_Estimation_Guards.py | 26 ++++++++++++++++++++++ B09_Estimation/B09_Estimation_UnitPrice.py | 5 ++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/B09_Estimation/B09_Estimation_Guards.py b/B09_Estimation/B09_Estimation_Guards.py index 7f07ef7d..315aeb44 100644 --- a/B09_Estimation/B09_Estimation_Guards.py +++ b/B09_Estimation/B09_Estimation_Guards.py @@ -153,3 +153,29 @@ def check_operator_hours_basis( f"{daily_wage:,.0f} × {person_days} ÷ {hours_per_day}h = {expected:,.2f} 와 다릅니다 — " "나눗수를 줄이면 작업효율을 사용료에 넣은 것이 됩니다 (PLAN 9-6 ㉣)." ) + + +def check_column_sums( + *, + rows: list[dict], + totals: dict[str, Decimal], + columns: tuple[str, ...] = ("material", "labor", "expense", "total"), + label: str = "본표", +) -> None: + """㉤ **열 방향** 검사 — 표시된 합계가 상세 줄의 열별 합과 같은가. + + `TC = NC + GC + JC` 는 **행 방향** 검사라 「같은 성분을 두 층에서 세는」 어긋남을 + 못 잡는다(행마다는 다 맞는데 열 합만 갈리는 모양). 그래서 방향을 하나 더 둔다. + + 예 — 기계 줄 안에 든 조종원 노무가 별도 노무 줄로도 서면 노무 열만 부풀고 + 행 검사는 전부 통과한다. + """ + for column in columns: + column_sum = sum((Decimal(str(row[column])) for row in rows), Decimal(0)) + shown = Decimal(str(totals[column])) + if abs(column_sum - shown) > _TOLERANCE: + raise DoubleCountError( + f"{label}: `{column}` 열 합계가 어긋납니다 — 줄 합 {column_sum:,.2f} vs " + f"표시 {shown:,.2f}. 같은 성분을 두 층에서 셌을 수 있습니다 " + "(행 방향 `TC=NC+GC+JC` 검사로는 안 잡힘)." + ) diff --git a/B09_Estimation/B09_Estimation_UnitPrice.py b/B09_Estimation/B09_Estimation_UnitPrice.py index 34351e6d..352c4ec9 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice.py +++ b/B09_Estimation/B09_Estimation_UnitPrice.py @@ -21,7 +21,7 @@ from dataclasses import dataclass, field from decimal import Decimal from functools import lru_cache -from B09_Estimation.B09_Estimation_Guards import check_surcharge_once +from B09_Estimation.B09_Estimation_Guards import check_column_sums, check_surcharge_once from B09_Estimation.B09_Estimation_MachineCost import load_machine_catalog from B09_Estimation.B09_Estimation_MachineOperating import ( load_fuel_price, @@ -367,6 +367,9 @@ def detail_of(build: UnitPriceBuild, code: str) -> dict: key: sum((Decimal(r[key]) for r in rows), Decimal(0)) for key in ("material", "labor", "expense", "total") } + # ㉤ 열 방향 검사 — 같은 성분을 두 층에서 세면 여기서 멈춘다. + # 행 방향(`TC=NC+GC+JC`)만으로는 안 잡히는 어긋남이다. + check_column_sums(rows=rows, totals=summed, label=f"{title.name} 본표") return { "code": code, "name": title.name,