From c892bdc93592b54115799379228a9d67248859c2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 08:30:30 +0900 Subject: [PATCH] =?UTF-8?q?feat(B09):=20=EC=A1=B0=ED=95=A9=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EC=8B=9C=20=EB=B3=B8=EC=B2=B4=20=EC=9E=A1=EC=9E=AC?= =?UTF-8?q?=EB=A3=8C=2016%=20+=20=ED=84=B0=ED=8C=8C=EA=B8=B0=20=EB=85=B8?= =?UTF-8?q?=ED=8B=B0=EC=8A=A4=20=EB=AC=B8=EA=B5=AC=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ① 품셈 제8장 [주]⑤ 의 마지막 조각 — 「…조합하여 사용할 때는 …**잡재료비율을 16%로 계상**하고, 리퍼·브레이커·집게의 손료 및 치즐 소모율을 추가」. 손료·치즐만 넣고 이 줄을 빠뜨리면 본체 재료비가 22% 로 남아 조금씩 비싸짐. - 기계마다 「#조합」 층을 함께 세움(손료·조종원 같음, 잡재료만 16%) - 조립 **끝에 한 번** 조합 공종의 본체 줄을 그 층으로 바꿔 닮 — 도중에는 어느 공종이 조합인지 아직 모름. 바꾼 줄 24 - 부착 장비는 **이름이 아니라 분류번호**로 잡음(0103 리퍼 · 0230 브레이커 · 0240 진동콤팩터 · 7206 집게) — 이름은 원천이 뭉개 놓는 일이 있음(0230 이 그랬음) - 결과: 구조물터파기 72,378.3 → 72,333.2 · 돌쌓기(메) 78,864.5 → 78,423.8 · 돌쌓기(찰) 61,757.3 → 61,418.0 · 본체 30,892,279 → **30,849,376원** ② 터파기 노티스 문구 정정 — 「실무는 기초만 세고 우리는 벽 전체를 셈(6.6배)」이 **틀렸음**. 우리 식은 정본(기초 0.45 + 비탈) 그대로이고, 6.6배는 소광리 시트가 비탈 줄을 안 적어서 난 것임. ⇒ 「비탈 터파기를 셀지 확정 대기 — 안 세면 약 1/7」로 고침. 값·금액은 그대로 검증: pytest 290 통과(신규 1) Co-Authored-By: Claude Opus 5 (1M context) --- .../B09_Estimation_BillOfQuantities_Rows.py | 25 +++--- B09_Estimation/B09_Estimation_UnitPrice.py | 89 +++++++++++++++++++ 2 files changed, 99 insertions(+), 15 deletions(-) diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py index bcf9bb38..ed02f559 100644 --- a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py +++ b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py @@ -332,26 +332,21 @@ def _leaf_row( #: 수량 산식이 **사용자 확정을 기다리는** 공종 — 금액은 세우되 그 사실을 함께 싣는다. #: -#: ⚠ **값을 우리가 바꾸지 않는다.** 실무 시트와 어긋나는 것이 확인됐을 뿐이고, 어느 쪽으로 -#: 갈지는 설계 판단이다(계획서 4-12 3단계, 확정 대기). -#: 돌쌓기(찰) H=2.0·ℓ3=45 · m당 기준으로 실무 대비 -#: 터파기 실무 0.35 우리 2.30 (약 6.6배) — 실무는 기초만(기초폭 × 깊이 0.3), -#: 우리는 벽 전체를 센다 -#: 되메우기 실무 0.185 우리 0.40 · 잔토정리 실무 0.165 우리 1.90 (위 둘의 차) +#: ⚠ **값을 우리가 바꾸지 않는다.** 어느 쪽으로 갈지는 설계 판단이다(계획서 4-12 3단계). +#: +#: ⚠ **2026-09-09 문구를 고쳤다.** 처음에는 「실무는 기초만 세고 우리는 벽 전체를 센다」로 +#: 적었는데 **그것이 틀렸다** — 우리 식은 정본(기초 0.45 + 비탈) 그대로이고, 6.6배 차이는 +#: **소광리 시트가 비탈 터파기 줄을 안 적어서** 난 것이다. 다투는 자리는 「우리 식이 +#: 맞는가」가 아니라 **「비탈 터파기를 셀 것인가」**다. _PENDING_FORMULA: dict[str, str] = { "FP-09-13": ( - "⚠ 터파기 수량 산식이 확정 대기입니다 — 실무 시트는 기초만(기초폭 × 깊이 0.3) 세고 " - "우리는 벽 전체를 셉니다(m당 실무 0.35 vs 우리 2.30, 약 6.6배). 실무 식으로 확정되면 " - "이 줄 금액이 약 1/6.6 로 줄어듭니다." + "⚠ 비탈 터파기를 셀지 확정 대기입니다 — 정본은 세고(기초 0.45 + 비탈) 다른 실무 " + "시트는 안 셉니다. 안 세는 쪽으로 확정되면 이 줄 금액이 약 1/7 로 줄어듭니다." ), "FP-09-14": ( - "⚠ 되메우기 수량 산식이 확정 대기입니다 — 터파기와 같은 뿌리입니다" - "(m당 실무 0.185 vs 우리 0.40)." - ), - "FP-09-15": ( - "⚠ 잔토처리 수량 산식이 확정 대기입니다 — 터파기·되메우기의 차라 같은 뿌리입니다" - "(m당 실무 0.165 vs 우리 1.90)." + "⚠ 되메우기도 같은 확정에 걸립니다 — 비탈 터파기를 안 세면 되메울 부피도 함께 줍니다." ), + "FP-09-15": ("⚠ 잔토처리도 같은 확정에 걸립니다 — 터파기와 되메우기의 차라 같은 뿌리입니다."), } diff --git a/B09_Estimation/B09_Estimation_UnitPrice.py b/B09_Estimation/B09_Estimation_UnitPrice.py index 79921d94..c4c02382 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice.py +++ b/B09_Estimation/B09_Estimation_UnitPrice.py @@ -19,6 +19,7 @@ from __future__ import annotations import re from dataclasses import dataclass, field +from dataclasses import replace as dataclass_replace from decimal import Decimal from functools import lru_cache @@ -102,6 +103,8 @@ class UnitPriceBuild: #: ⚠ 값이 남의 절에서 온 것이면 **화면이 그렇게 말해야** 한다 — 안 그러면 나중에 #: 「이 숫자 어디서 왔지」로 되짚을 길이 없다. factor_sources: dict[str, str] = field(default_factory=dict) + #: 조합 사용(품셈 [주]⑤)으로 **잡재료 16% 층으로 바꿔 단 줄** 수. + combined_swapped: int = 0 #: 배분율 표인데 일부 몫만 붙은 공종 — 「단가가 일부만 섬」. 값은 붙은 몫(%). partial_ratio: dict[str, Decimal] = field(default_factory=dict) #: 시공능력 공식으로 장비 몫을 세운 공종 — 산출근거를 화면에 그대로 보인다. @@ -137,6 +140,50 @@ def _add_labor_titles(book: PriceBook, wages: dict[str, Decimal]) -> None: _ATTACHMENT_WORDS = ("브레이커", "리퍼", "부착용집게", "집게") +#: 조합 사용 시 본체(굴착기·불도저)의 잡재료비율 — 품셈 제8장 [주]⑤. +#: 「…리퍼, 브레이커, 부착용집게를 **조합하여 사용**할 때는 …**잡재료비율을 16%로 계상**하고, +#: 리퍼, 브레이커, 부착용 집게의 손료 및 치즐 소모율을 추가하는 것이다.」 +#: ⚠ 손료·치즐만 더하고 이 줄을 빠뜨리기 쉽다 — 그러면 본체 재료비가 계속 22% 로 서서 +#: 조금씩 비싸진다(굴착기 0.7 기준 시간당 1,285원). +COMBINED_MISC_PERCENT = Decimal(16) + +#: 조합으로 쓰는 부착 장비 — 이 층이 붙은 공종의 본체는 위 비율을 쓴다. +#: 카탈로그 분류번호로 잡는다 — 0103 유압식 리퍼 · 0230 대형 브레이커 · +#: 0240 유압식 진동콤팩터(굴착기 부착용) · 7206 부착용 집게. +#: ⚠ 이름이 아니라 **번호**로 잡는다 — 이름은 원천이 뭉개 놓는 일이 있다(0230 이 그랬다). +_ATTACHMENT_PREFIXES = ("X-0103-", "X-0230-", "X-0240-", "X-7206-") + + +def _apply_combined_misc_rate(book: PriceBook, work_item_titles: list[str]) -> int: + """조합 사용 공종의 본체 기계를 **잡재료 16% 짜리 층**으로 바꿔 단다. + + 바꾼 줄 수를 돌려준다. ⚠ 원래 층은 그대로 둔다 — 조합이 아닌 공종은 22% 그대로다. + """ + swapped = 0 + for title_code in work_item_titles: + details = book.details.get(title_code) or [] + if not any(detail.ref_code.startswith(_ATTACHMENT_PREFIXES) for detail in details): + continue + for index, detail in enumerate(details): + if not detail.ref_code.startswith("X-") or detail.ref_code.startswith( + _ATTACHMENT_PREFIXES + ): + continue + combined = f"{detail.ref_code}#조합" + if combined not in book.titles: + continue + details[index] = dataclass_replace( + detail, + ref_code=combined, + note=( + (detail.note + " · " if detail.note else "") + + "조합 사용 — 잡재료 16% (품셈 제8장 [주]⑤)" + ), + ) + swapped += 1 + return swapped + + def _add_machine_layers(book: PriceBook, machine_codes: set[str]) -> list[str]: """`S`(취득가) · `L`(운전사) · `M`(연료) 을 세우고 그 위에 `X` 를 올린다. @@ -232,6 +279,32 @@ def _add_machine_layers(book: PriceBook, machine_codes: set[str]) -> list[str]: # 잡재료는 **주연료의 %** — 유가와 같이 움직인다. liters = liters * (Decimal(1) + record.misc_material_percent / Decimal(100)) book.add_detail(PriceDetail(hourly_code, fuel_code, liters, note="주연료 + 잡재료")) + + # 조합 사용(리퍼·브레이커·집게)일 때 쓸 **잡재료 16%** 짜리 층을 함께 세운다. + # 같은 기계라도 조합이면 본체 잡재료가 줄어든다(품셈 제8장 [주]⑤). + combined_code = f"{hourly_code}#조합" + if combined_code not in book.titles: + book.add_title( + PriceTitle( + code=combined_code, + kind=PriceKind.MACHINE_HOURLY, + name=machine.name, + spec=(f"{machine.specification} · 조합").strip(" ·"), + unit="hr", + ) + ) + book.add_detail( + PriceDetail(combined_code, base_code, Decimal(1), note="시간당 손료") + ) + book.add_detail( + PriceDetail( + combined_code, + fuel_code, + record.fuel_liters_per_hour + * (Decimal(1) + COMBINED_MISC_PERCENT / Decimal(100)), + note=f"주연료 + 잡재료 {COMBINED_MISC_PERCENT}% (조합 사용)", + ) + ) else: incomplete.append(f"{code} (연료소모량 없음)") @@ -251,6 +324,17 @@ def _add_machine_layers(book: PriceBook, machine_codes: set[str]) -> list[str]: slots=_slots(wages[wage_code]), ) ) + # 조합 층에도 조종원을 같이 단다 — 본체를 모는 사람은 하나뿐이다. + combined_code = f"{hourly_code}#조합" + if combined_code in book.titles: + book.add_detail( + PriceDetail( + combined_code, + wage_code, + per_hour_person, + note="조종원 (1일 8시간 × 제수당·상여·퇴직충당 16/12 × 25/20)", + ) + ) book.add_detail( PriceDetail( hourly_code, @@ -668,6 +752,11 @@ def build_unit_prices( covered += machine_share if covered < Decimal(100): build.partial_ratio[work_item_code] = covered + # ⚠ **마지막에 한 번** — 조합 사용 공종의 본체 기계를 잡재료 16% 층으로 바꿔 단다 + # (품셈 제8장 [주]⑤). 조립 도중에 바꾸면 어느 공종이 조합인지 아직 모른다. + build.combined_swapped = _apply_combined_misc_rate( + build.book, [code for code in build.book.titles if code.startswith("B-")] + ) return build