feat(B09): 조합 사용 시 본체 잡재료 16% + 터파기 노티스 문구 정정
① 품셈 제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) <noreply@anthropic.com>
This commit is contained in:
@@ -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": ("⚠ 잔토처리도 같은 확정에 걸립니다 — 터파기와 되메우기의 차라 같은 뿌리입니다."),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user