feat(b09): 301 유로폼 사용수량 12-38-2 — 임대료 또는 손료를 「자재 단가」 탭에서 설계자가 넣은 쪽으로 세움
· 손료 = 자재가 × 표 수량 곧장(패널 0.089 · 내부 패널 0.003매/㎡) + 부자재 24/52/79% · 소모자재 5%(주자재비 = 패널 + 내부 패널) — 봉화 2024 호표 재료비 보통 4,500 · 간단 3,697 과 일치 · 임대료 = 「유로폼 임대료 ㎡당」 한 줄 · 부자재 % 는 원문이 안 적어 안 걺 · 둘 다 · 한쪽만 반쯤 · 아무것도 — 안 세우고 사유(부모 12-38 까지 그대로) · 12-38-1 잔존율은 안 곱함 — 표 수량이 이미 12회·25% 몫으로 보임(우리 역산 · 차 2.5% 는 추정이라 비고에 밝힘) · 이중계상 ③ 가드에서 12-38-02 만 뺌(패널이 자재총괄에 줄 없음) · 유로폼이 자재총괄로 가면 빨강 시험 · 합산형 부모 — 갈래 벌이 같으면 여러 단계 갈래끼리 합산(12-38 = 사용수량 + 설치·해체) · 작업조 표 7 은 밑수 없음 목록에서 뺌(1단위당 = 인원 ÷ 시공량) — 12-38-3 설치·해체가 막혀 있던 자리 · 마스터 재생성 — 12-38 · 12-38-02 갈래 키만 바뀜 기본 조립 제목 금액 변화 0 · 프로젝트 내역 변화 0 · 잴 시험 일곱 빨강→초록 · 끄기 넷 빨강 · 전체 시험 1677 + 318 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
@@ -648,8 +648,10 @@ def build() -> dict[str, Any]:
|
||||
basis_found += 1
|
||||
if basis_quantity_is_grouped(basis_qty):
|
||||
basis_grouped += 1
|
||||
elif form in ("requirement", "productivity"):
|
||||
elif form in ("requirement", "productivity") and not crew_table(table):
|
||||
# 참조·계수표는 곱할 값이 아니므로 목록에 넣지 않는다 — 잡음이 되면 안 본다.
|
||||
# 작업조 표도 뺌 — 밑수가 시공량 열(1단위당 = 인원 ÷ 시공량 · B09 CrewOutput)이라
|
||||
# 「N㎡당」 문구가 없어도 곱셈이 안 틀림(12-38-3 설치·해체가 여기 걸려 막혀 있었음 · 2026-09-14 301).
|
||||
basis_missing.append(
|
||||
{
|
||||
"pum_table_id": table["table_id"],
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
⚠ 못 가른 표는 **빈칸** — 자원을 못 맞춘 표 · 계수 · 참조 · 미판정 표에 갈래를 지어내지 않는다.
|
||||
|
||||
공종마다 `variant_keys` — 표들의 갈래 + 원문이 정했으나 자원 줄로 안 서는 갈래
|
||||
(불도저 운반 공식 갈래 · 합산형 단계 잎의 암질 행 · 9-4-1 [주]① 평균) · 합산형 부모는 단계 갈래.
|
||||
(불도저 운반 공식 갈래 · 합산형 단계 잎의 암질 행 · 9-4-1 [주]① 평균 · 유로폼 12-38-2 부자재 요율 머리) · 합산형 부모는 단계 갈래.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -28,6 +28,9 @@ def _add(labels: list[str], seen: set[str], label: str, normalize: Any) -> None:
|
||||
|
||||
def attach_variant_keys(nodes: list[dict[str, Any]], edition: str) -> None:
|
||||
"""표마다 `variant_key`, 공종마다 `variant_keys` 를 채운다(자리에서)."""
|
||||
from B09_Estimation.B09_Estimation_Euroform import CODE as EUROFORM_CODE
|
||||
from B09_Estimation.B09_Estimation_Euroform import RATE_TABLE as EUROFORM_RATE_TABLE
|
||||
from B09_Estimation.B09_Estimation_Euroform import rates as euroform_rates
|
||||
from B09_Estimation.B09_Estimation_MachineProductivity_Dozer import extract_dozer_factors
|
||||
from B09_Estimation.B09_Estimation_ParentSteps import AVERAGE_BASIS, AVERAGE_VARIANT, rock_rows
|
||||
from B09_Estimation.B09_Estimation_ResourceAxis import build_resource_axis
|
||||
@@ -62,6 +65,9 @@ def attach_variant_keys(nodes: list[dict[str, Any]], edition: str) -> None:
|
||||
if code in steps:
|
||||
for rock, _, _ in rock_rows({"tables": [table]}):
|
||||
_add(labels, table_seen, rock, normalize)
|
||||
if code == EUROFORM_CODE and table.get("pum_table_id") == EUROFORM_RATE_TABLE:
|
||||
for head in euroform_rates({"tables": [table]}): # 부자재 요율 머리 갈래
|
||||
_add(labels, table_seen, head, normalize)
|
||||
table["variant_key"] = labels
|
||||
for label in labels:
|
||||
_add(keys, seen, label, normalize)
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
"""B09 원가계산 — **유로폼 사용수량** 12-38-2 (2026-09-14 브레인 301 판정 ①~⑥ · ③′).
|
||||
|
||||
정본 = 산림 12-38-2(10㎡당 패널 0.89매 · 내부 패널 0.03매 · 부자재 주자재비 간단 24 · 보통 52 · 복잡 79% ·
|
||||
소모자재 5%) · 건설 공통 6-3-3 은 참고(고시 총칙 「타 부문과 유사한 공종은 본 품셈 우선」).
|
||||
원문 「자재비는 거래형태 등을 고려하여 **임대료 또는 손료**로 산정」 — 둘을 나란히 주므로 설계자가 고름(제안값 없음).
|
||||
|
||||
고르는 자리 「자재 단가」 탭(화약류와 같은 통로) — 넣은 쪽으로 섬
|
||||
손료 패널·내부 패널 단가 × 표 수량 곧장(실무 넷 — 봉화 「31,500 × 0.89 / 10」) ·
|
||||
부자재·소모자재 = (패널 + 내부 패널) × %(봉화 「2,866.5 × 52%」)
|
||||
임대료 「유로폼 임대료 ㎡당」 한 줄(설계자가 임대기간 반영해 셈) · % 는 원문이 안 적어 안 걺
|
||||
둘 다 · 반쯤 · 아무것도 — 안 섬 + 사유(부모 12-38 까지 그대로 올라감)
|
||||
|
||||
⚠ 12-38-1 잔존율(12회 25%)은 곱하지 않음 — 표 수량이 이미 그 몫으로 보임. 곱하면 두 번 나눔(패널 몫 약 1/16).
|
||||
⚠ 가드(이중계상 ③)에서 12-38-02 를 뺀 까닭은 `B09_Estimation_Guards.SURCHARGE_INCLUDED_ITEMS` 곁에.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
CODE = "FP-12-38-02"
|
||||
PANEL = "AR-M-10aba455"
|
||||
INNER = "AR-M-5b89b294"
|
||||
RENT = "AR-M-d00a6b1a"
|
||||
QUANTITY_TABLE = "F0393"
|
||||
RATE_TABLE = "F0394"
|
||||
|
||||
CHOICE_MISSING = (
|
||||
"자재비 — 임대료 또는 손료 설계자 선택(12-38-2 「거래형태 등을 고려하여 임대료 또는 손료로 산정」)"
|
||||
" · 「자재 단가」 탭에 패널·내부 패널 단가(손료) 또는 유로폼 임대료 ㎡당(임대료) 중 하나를 넣으면 섬"
|
||||
)
|
||||
CHOICE_BOTH = "자재비 — 손료(패널 단가)와 임대료가 둘 다 들어옴 · 하나만 넣을 것"
|
||||
LOSS_HALF = "자재비 손료 — {name} 단가 없음 · 패널·내부 패널 둘 다 넣어야 섬"
|
||||
REUSE_NOTE = (
|
||||
"12-38-2 표 수량 곧장(실무 넷 실증) · 12-38-1 잔존율은 안 곱함 — 표 수량이 이미 12회·잔존율 25% 몫으로"
|
||||
" 보임(우리 역산: 10㎡ ÷ 0.72㎡ = 13.9매 × 0.75 ÷ 12 = 0.87 ≈ 0.89 · 원문이 적지는 않음) · 차 2.5% 는"
|
||||
" [주]① 「할증 및 손율 포함」으로 봄(추정) · 「25회 10%」 는 표에 수량이 없어 안 세움"
|
||||
)
|
||||
RENT_NOTE = "설계자 임대료(12-38-2 「임대료는 시중 물가지 등을 참고하여 결정」) · 부자재·소모자재 % 는 원문이 임대료에 안 적어 안 걺"
|
||||
|
||||
_RE_PERCENT = re.compile(r"(\d+(?:\.\d+)?)\s*%")
|
||||
|
||||
|
||||
def _tight(text: Any) -> str:
|
||||
return re.sub(r"\s", "", str(text or ""))
|
||||
|
||||
|
||||
def _table(node: dict[str, Any], table_id: str) -> dict[str, Any]:
|
||||
return next((t for t in node.get("tables") or [] if t.get("pum_table_id") == table_id), {})
|
||||
|
||||
|
||||
def rates(node: dict[str, Any]) -> dict[str, Decimal]:
|
||||
"""부자재 요율 표(F0394) — 머리 갈래(원문 표기 「간 단」…) → %. 마스터 갈래 키도 이것을 씀."""
|
||||
table = _table(node, RATE_TABLE)
|
||||
heads = table.get("condition_note") or []
|
||||
for row in table.get("raw_row") or []:
|
||||
found = {str(h): _RE_PERCENT.search(str(c)) for h, c in zip(heads[1:], row[1:])}
|
||||
return {h: Decimal(m.group(1)) for h, m in found.items() if m}
|
||||
return {}
|
||||
|
||||
|
||||
def _quantities(node: dict[str, Any]) -> tuple[Decimal | None, Decimal | None, Decimal | None]:
|
||||
"""(패널 매/㎡, 내부 패널 매/㎡, 소모자재 %) — 표 F0393 10㎡당을 1㎡당으로."""
|
||||
from B09_Estimation.B09_Estimation_ResourceAxis import parse_amount
|
||||
|
||||
table = _table(node, QUANTITY_TABLE)
|
||||
per = Decimal(str(table.get("basis_quantity") or 0))
|
||||
panel = inner = consumable = None
|
||||
for row in table.get("raw_row") or []:
|
||||
name, value = _tight(row[0]), str(row[-1])
|
||||
if name == "패널" and parse_amount(value) and per:
|
||||
panel = parse_amount(value) / per
|
||||
elif name == "내부패널" and parse_amount(value) and per:
|
||||
inner = parse_amount(value) / per
|
||||
elif name.startswith("소모자재") and _RE_PERCENT.search(value):
|
||||
consumable = Decimal(_RE_PERCENT.search(value).group(1))
|
||||
return panel, inner, consumable
|
||||
|
||||
|
||||
def attach_euroform(build: Any, nodes_by_code: dict[str, dict[str, Any]]) -> None:
|
||||
"""「자재 단가」 칸 셋을 세우고, 넣은 쪽으로 12-38-02 를 세움 — 부모 합산(`attach_parent_steps`) 앞."""
|
||||
from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind, PriceTitle
|
||||
|
||||
node = nodes_by_code.get(CODE) or {}
|
||||
for material in (PANEL, INNER, RENT):
|
||||
uses = build.material_uses.setdefault(material, [])
|
||||
if CODE not in uses:
|
||||
uses.append(CODE)
|
||||
book = build.book
|
||||
loss = [m for m in (PANEL, INNER) if m in book.titles]
|
||||
panel, inner, consumable = _quantities(node)
|
||||
table_rates = rates(node)
|
||||
reason = ""
|
||||
if loss and RENT in book.titles:
|
||||
reason = CHOICE_BOTH
|
||||
elif RENT in book.titles:
|
||||
title_code = f"B-{CODE}"
|
||||
book.add_title(
|
||||
PriceTitle(
|
||||
code=title_code,
|
||||
kind=PriceKind.UNIT_PRICE,
|
||||
name="유로폼 사용수량",
|
||||
spec="임대료",
|
||||
unit="㎡",
|
||||
)
|
||||
)
|
||||
book.add_detail(PriceDetail(title_code, RENT, Decimal(1), note=RENT_NOTE))
|
||||
elif len(loss) == 2 and panel and inner and consumable is not None and table_rates:
|
||||
for head, rate in table_rates.items():
|
||||
variant = _tight(head)
|
||||
title_code = f"B-{CODE}#{variant}"
|
||||
book.add_title(
|
||||
PriceTitle(
|
||||
code=title_code,
|
||||
kind=PriceKind.UNIT_PRICE,
|
||||
name=f"유로폼 사용수량 ({variant})",
|
||||
spec=variant,
|
||||
unit="㎡",
|
||||
)
|
||||
)
|
||||
book.add_detail(PriceDetail(title_code, PANEL, panel, note=REUSE_NOTE))
|
||||
book.add_detail(
|
||||
PriceDetail(title_code, INNER, inner, note="12-38-2 내부 패널 표 수량 곧장")
|
||||
)
|
||||
for label, percent in (
|
||||
("부자재(웨지핀·플랫타이·강관파이프·훅)", rate),
|
||||
("소모자재(박리제 등)", consumable),
|
||||
):
|
||||
book.add_detail(
|
||||
PriceDetail(
|
||||
title_code,
|
||||
title_code,
|
||||
Decimal(0),
|
||||
note=f"{label} — 주자재비(패널 + 내부 패널)의 {percent}% (12-38-2)",
|
||||
percent_of_material=percent,
|
||||
)
|
||||
)
|
||||
build.variants.setdefault(CODE, []).append(variant)
|
||||
elif loss:
|
||||
missing = "내부 패널" if PANEL in loss else "패널"
|
||||
reason = LOSS_HALF.format(name=missing)
|
||||
else:
|
||||
reason = CHOICE_MISSING
|
||||
if reason:
|
||||
build.component_gaps[CODE] = reason
|
||||
build.unattached[CODE] = [reason]
|
||||
@@ -231,10 +231,13 @@ def check_handoff_boundaries(violations: list[str] | None) -> None:
|
||||
|
||||
|
||||
#: 품셈 [주]가 「재료량에 할증 포함」이라 적은 공종 — 그 재료가 일위대가 재료비로 붙으면
|
||||
#: **할증 뒤 값**이 들어가 자재총괄에서 한 번 더 붙는다(㉠). 원문 넷, 코드는 마스터가 붙인 자리.
|
||||
#: **할증 뒤 값**이 들어가 자재총괄에서 한 번 더 붙는다(㉠). 원문 셋, 코드는 마스터가 붙인 자리.
|
||||
#: ⚠ 유로폼 사용수량 12-38-02([주]① 「할증 및 손율이 포함」)는 **뺐음**(2026-09-14 브레인 301 ④) —
|
||||
#: 이 가드의 전제 「자재총괄에서 할증이 한 번 더」가 안 섬: 패널은 B08 자재총괄에 줄이 없음(유로폼 ㎡ 갈 곳
|
||||
#: `unit_price`) · 손료 수량이라 할증 전 값이 원문에 없음 · 실무 넷도 일위대가에 곧장 붙임.
|
||||
#: 유로폼·패널이 자재총괄로 가는 날 되돌릴 것 — `test_b09_euroform` 이 그 자리에서 빨강.
|
||||
SURCHARGE_INCLUDED_ITEMS: dict[str, str] = {
|
||||
"FP-12-02": "용적 배합 콘크리트 참고표 「재료량에는 할증률이 포함」(마스터가 12-2 에 붙임)",
|
||||
"FP-12-38-02": "유로폼 사용수량 [주]① 「재료량에는 재료의 할증 및 손율이 포함」",
|
||||
"FP-13-11-04": "돌망태 사각형 [주]① 「자재비에는 재료의 할증을 포함」",
|
||||
"AX-WK-c0842a0d": "모르타르 배합 참고자료 ※ 「위 재료량은 할증이 포함된 것이다」",
|
||||
}
|
||||
|
||||
@@ -186,7 +186,9 @@ def _assemble(build: Any, node: dict[str, Any], names: dict[str, str]) -> None:
|
||||
return
|
||||
per_step.append((step, weight, titles))
|
||||
varying = [item for item in per_step if set(item[2]) - {""}]
|
||||
if len(varying) > 1:
|
||||
# 갈래가 같은 이름으로 여러 단계에 걸치면 갈래끼리 합산(유로폼 12-38 = 사용수량 + 설치·해체
|
||||
# 둘 다 간단·보통·복잡 · 2026-09-14 301). 갈래 벌이 다르면 짝을 못 지어 종전대로 막음.
|
||||
if len({frozenset(set(ts) - {""}) for _, _, ts in varying}) > 1:
|
||||
build.component_gaps[parent] = "갈래가 두 단계 이상에 걸쳐 조립하지 않았습니다"
|
||||
return
|
||||
units = {build.book.titles[t].unit for _, _, ts in per_step for t in ts.values()} - {""}
|
||||
|
||||
@@ -1056,6 +1056,10 @@ def build_unit_prices(
|
||||
covered += machine_share
|
||||
if covered < Decimal(100):
|
||||
build.partial_ratio[work_item_code] = covered
|
||||
# 유로폼 사용수량(12-38-2) — 임대료 또는 손료 설계자 단가로 섬(2026-09-14 301). 부모 합산 앞.
|
||||
from B09_Estimation.B09_Estimation_Euroform import attach_euroform
|
||||
|
||||
attach_euroform(build, nodes_by_code)
|
||||
# 단계 합산형 부모(9-4 암절취 = 암파쇄 + 집토) — 잎이 다 선 뒤, 조합 16% 바꿔 달기 전(명세 2장).
|
||||
from B09_Estimation.B09_Estimation_ParentSteps import attach_parent_steps
|
||||
|
||||
|
||||
@@ -342,6 +342,45 @@
|
||||
"M00118"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "AR-M-10aba455",
|
||||
"kind": "material",
|
||||
"name": "유로폼 패널",
|
||||
"spec": "600 x 1,200mm",
|
||||
"unit": "매",
|
||||
"source": {
|
||||
"pum_edition": "2026-01-01",
|
||||
"pum_table_ids": [
|
||||
"F0393"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "AR-M-5b89b294",
|
||||
"kind": "material",
|
||||
"name": "유로폼 내부 패널",
|
||||
"spec": "(200+200) x 1,200mm",
|
||||
"unit": "매",
|
||||
"source": {
|
||||
"pum_edition": "2026-01-01",
|
||||
"pum_table_ids": [
|
||||
"F0393"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "AR-M-d00a6b1a",
|
||||
"kind": "material",
|
||||
"name": "유로폼 임대료",
|
||||
"spec": "㎡당(임대기간 반영 · 설계자 셈)",
|
||||
"unit": "㎡",
|
||||
"source": {
|
||||
"pum_edition": "2026-01-01",
|
||||
"pum_table_ids": [
|
||||
"F0393"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "data_work_item_master_manifest",
|
||||
"generated_at": "2026-09-14T19:15:46+09:00",
|
||||
"generated_at": "2026-09-14T21:01:09+09:00",
|
||||
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
|
||||
"source": {
|
||||
"dataset_id": "pum_forest",
|
||||
@@ -12,8 +12,8 @@
|
||||
"files": [
|
||||
{
|
||||
"file": "work_item_master_2026-01-01.json",
|
||||
"sha256": "183a2933a2bf6f06fb99c0b86dc4073ec42c12a95784b3dd279f5eee50ee088f",
|
||||
"size_bytes": 838487
|
||||
"sha256": "915a1403611a9c713e5c59bff4422a2fe9f53d4168a44a9fada43d14b6c9d9d6",
|
||||
"size_bytes": 838599
|
||||
},
|
||||
{
|
||||
"file": "form_undetermined_2026-01-01.json",
|
||||
@@ -22,8 +22,8 @@
|
||||
},
|
||||
{
|
||||
"file": "basis_missing_2026-01-01.json",
|
||||
"sha256": "f850742268dcfb1bcc878e8b062ef44bc6a11d773074163ebb8ba1dec21a6d0d",
|
||||
"size_bytes": 16790
|
||||
"sha256": "d57f26c736829ef40c50dc616d96358db785295d1aa0d5959aa7bc9eaa9be842",
|
||||
"size_bytes": 15845
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -172,12 +172,6 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 2689
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0132",
|
||||
"section": "5-22-4. 평떼 시비",
|
||||
"pum_form": "requirement",
|
||||
"line": 2731
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0143",
|
||||
"section": "5-27. 식재면 관리",
|
||||
@@ -190,12 +184,6 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 2919
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0145",
|
||||
"section": "5-28-2. 방초매트 및 야자섬유매트 포장",
|
||||
"pum_form": "requirement",
|
||||
"line": 2929
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0153",
|
||||
"section": "6-1. 비료주기",
|
||||
@@ -628,24 +616,6 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 6217
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0339",
|
||||
"section": "12-7-1. 포장절단",
|
||||
"pum_form": "requirement",
|
||||
"line": 6254
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0340",
|
||||
"section": "12-7-2. 줄눈설치",
|
||||
"pum_form": "requirement",
|
||||
"line": 6269
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0341",
|
||||
"section": "12-8. 콘크리트 포장 거푸집",
|
||||
"pum_form": "requirement",
|
||||
"line": 6280
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0350",
|
||||
"section": "12-12. 날개벽",
|
||||
@@ -676,12 +646,6 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 6775
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0395",
|
||||
"section": "12-38-3. 설치 및 해체",
|
||||
"pum_form": "requirement",
|
||||
"line": 6943
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0401",
|
||||
"section": "13-2-4. 야면석 채집(인력)",
|
||||
@@ -729,12 +693,6 @@
|
||||
"section": "13-15-2. 목책 설치",
|
||||
"pum_form": "requirement",
|
||||
"line": 7745
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0449",
|
||||
"section": "13-16-2. 통기성매트",
|
||||
"pum_form": "requirement",
|
||||
"line": 7788
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"dataset_id": "work_item_master_forest",
|
||||
"effective_date": "2026-01-01",
|
||||
"pum_edition": "2026-01-01",
|
||||
"generated_at": "2026-09-14T19:15:46+09:00",
|
||||
"generated_at": "2026-09-14T21:01:09+09:00",
|
||||
"dataset_version": {
|
||||
"dataset_id": "pum_forest",
|
||||
"effective_date": "2026-01-01",
|
||||
@@ -23,7 +23,7 @@
|
||||
"tables_orphan": 17,
|
||||
"form_undetermined": 14,
|
||||
"basis_found": 204,
|
||||
"basis_missing": 122,
|
||||
"basis_missing": 115,
|
||||
"basis_grouped": 48
|
||||
},
|
||||
"orphan_tables": [
|
||||
@@ -35677,9 +35677,9 @@
|
||||
],
|
||||
"steps_basis": "품셈 12-38 유로폼 = 12-38-2 사용수량(자재) + 12-38-3 설치 및 해체(품) — 12-38-1 사용횟수는 금액 단계가 아니라 사용수량의 잔존율 조건",
|
||||
"variant_keys": [
|
||||
"복 잡",
|
||||
"간 단",
|
||||
"보 통",
|
||||
"간 단"
|
||||
"복 잡"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -35803,7 +35803,11 @@
|
||||
"formula_rows": [],
|
||||
"special_glyphs": [],
|
||||
"capacity_formula_here": false,
|
||||
"variant_key": [],
|
||||
"variant_key": [
|
||||
"간 단",
|
||||
"보 통",
|
||||
"복 잡"
|
||||
],
|
||||
"condition_note": [
|
||||
"구 분",
|
||||
"간 단",
|
||||
@@ -35820,7 +35824,11 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"variant_keys": []
|
||||
"variant_keys": [
|
||||
"간 단",
|
||||
"보 통",
|
||||
"복 잡"
|
||||
]
|
||||
},
|
||||
{
|
||||
"work_item_code": "FP-12-38-03",
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"""유로폼 사용수량 12-38-2 — 2026-09-14 브레인 301 판정 ①~⑥ · ③′.
|
||||
|
||||
① 산림 12-38-2 정본 ② 「임대료 또는 손료」 설계자 선택 · 제안값 없음
|
||||
③′ 손료 = 자재가 × 표 수량 곧장(실무 넷 — 봉화 「31,500 × 0.89 / 10」) · 12-38-1 잔존율은 안 곱함
|
||||
④ 이중계상 ③ 가드에서 12-38-02 만 뺌 — 패널이 B08 자재총괄에 줄이 없어서. 되살아날 자리를 시험으로 걺
|
||||
⑤ 고르는 자리 = 「자재 단가」 탭(넣은 쪽으로 섬 · 둘 다면 안 섬) ⑥ 임대료 갈래엔 % 안 걺
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import cached_build, detail_of
|
||||
|
||||
PANEL = "AR-M-10aba455"
|
||||
INNER = "AR-M-5b89b294"
|
||||
RENT = "AR-M-d00a6b1a"
|
||||
LOSS = ((PANEL, "31500", "봉화 2024"), (INNER, "21000", "봉화 2024"))
|
||||
|
||||
|
||||
def _titles(build, code: str) -> list[str]:
|
||||
return [t for t in build.book.titles if t == f"B-{code}" or t.startswith(f"B-{code}#")]
|
||||
|
||||
|
||||
def test_아무것도_안_넣으면_안_서고_설계자_선택_사유() -> None:
|
||||
build = cached_build()
|
||||
assert not _titles(build, "FP-12-38-02")
|
||||
reason = build.component_gaps.get("FP-12-38-02", "")
|
||||
assert "임대료 또는 손료" in reason, reason
|
||||
assert "임대료 또는 손료" in build.component_gaps.get("FP-12-38", "") # 부모까지 까닭이 올라감
|
||||
for material in (PANEL, INNER, RENT):
|
||||
assert "FP-12-38-02" in build.material_uses.get(material, []), material
|
||||
|
||||
|
||||
def test_손료는_표_수량_곧장_부자재_소모자재는_주자재비의_퍼센트() -> None:
|
||||
"""봉화 2024 호표 「유로폼 설치 및 해체」 재료비 보통 4,500 · 간단 3,697 과 같은 값 — 실무 실증."""
|
||||
build = cached_build(material_prices=LOSS)
|
||||
assert sorted(_titles(build, "FP-12-38-02")) == [
|
||||
"B-FP-12-38-02#간단",
|
||||
"B-FP-12-38-02#보통",
|
||||
"B-FP-12-38-02#복잡",
|
||||
]
|
||||
detail = detail_of(build, "B-FP-12-38-02#보통")["rows"]
|
||||
rows = {r["ref_code"]: Decimal(r["quantity"]) for r in detail if r.get("ref_code")}
|
||||
assert rows[PANEL] == Decimal("0.089") and rows[INNER] == Decimal("0.003"), rows
|
||||
assert build.book.resolve("B-FP-12-38-02#보통").material == Decimal(4500)
|
||||
assert build.book.resolve("B-FP-12-38-02#간단").material == Decimal(3697) # 봉화 간단 3,697
|
||||
# 부모 12-38 = 사용수량 + 설치 및 해체 — 두 단계 갈래가 같으면 갈래끼리 합산
|
||||
parent = {r["ref_code"] for r in detail_of(build, "B-FP-12-38#보통")["rows"]}
|
||||
assert parent == {"B-FP-12-38-02#보통", "B-FP-12-38-03#보통"}, parent
|
||||
|
||||
|
||||
def test_작업조_표는_밑수가_시공량이라_밑수_없음이_아님() -> None:
|
||||
"""12-38-3 「(단위: 일 당) 형틀목공 4인 · 시공량 35㎡」 — 1㎡당 = 인원 ÷ 시공량(CrewOutput).
|
||||
|
||||
밑수 「N㎡당」 문구가 없다고 `basis_missing` 에 들어 설치·해체가 통째로 막혀 있었음(작업조 표 넷).
|
||||
"""
|
||||
build = cached_build()
|
||||
crew = ("FP-05-28-02", "FP-12-07-02", "FP-12-38-03", "FP-13-16-02")
|
||||
assert not [code for code in crew if code in build.basis_missing]
|
||||
rows = {
|
||||
r["name"]: Decimal(r["quantity"]) for r in detail_of(build, "B-FP-12-38-03#보통")["rows"]
|
||||
}
|
||||
assert rows["형틀목공"] == Decimal(4) / Decimal(35), rows
|
||||
|
||||
|
||||
def test_잔존율은_안_곱하고_역산은_추정이라_밝힘() -> None:
|
||||
build = cached_build(material_prices=LOSS)
|
||||
note = next(
|
||||
r["note"]
|
||||
for r in detail_of(build, "B-FP-12-38-02#보통")["rows"]
|
||||
if r.get("ref_code") == PANEL
|
||||
)
|
||||
assert "우리 역산" in note and "추정" in note and "25회" in note, note
|
||||
|
||||
|
||||
def test_임대료는_한_줄만_퍼센트_안_걺() -> None:
|
||||
build = cached_build(material_prices=((RENT, "5000", "견적"),))
|
||||
assert _titles(build, "FP-12-38-02") == ["B-FP-12-38-02"]
|
||||
rows = detail_of(build, "B-FP-12-38-02")["rows"]
|
||||
assert [r["ref_code"] for r in rows] == [RENT], rows
|
||||
assert build.book.resolve("B-FP-12-38-02").material == Decimal(5000)
|
||||
assert "B-FP-12-38#보통" in build.book.titles
|
||||
|
||||
|
||||
def test_둘_다_넣거나_한쪽만_반쯤_넣으면_안_섬() -> None:
|
||||
both = cached_build(material_prices=(*LOSS, (RENT, "5000", "견적")))
|
||||
assert not _titles(both, "FP-12-38-02")
|
||||
assert "둘 다" in both.component_gaps["FP-12-38-02"]
|
||||
half = cached_build(material_prices=(LOSS[0],))
|
||||
assert not _titles(half, "FP-12-38-02")
|
||||
assert "내부 패널" in half.component_gaps["FP-12-38-02"]
|
||||
|
||||
|
||||
def test_유로폼이_자재총괄에_줄로_서면_가드를_되살릴_것() -> None:
|
||||
"""④ 12-38-02 를 가드에서 뺀 전제 — 패널·유로폼이 B08 자재총괄(`material`)에 줄이 없음.
|
||||
|
||||
누가 유로폼을 자재총괄로 보내는 날 할증이 두 번 붙음 → 여기서 빨강 · 가드에 12-38-02 를 되돌릴 것.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import DESTINATION
|
||||
from B09_Estimation.B09_Estimation_Guards import SURCHARGE_INCLUDED_ITEMS
|
||||
|
||||
assert "FP-12-38-02" not in SURCHARGE_INCLUDED_ITEMS
|
||||
words = ("유로폼", "패널")
|
||||
assert all(DESTINATION[n] != "material" for n in DESTINATION if any(w in n for w in words))
|
||||
root = Path(__file__).resolve().parents[2] / "resources" / "data_structure_unit"
|
||||
for path in root.glob("*.json"):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
|
||||
def walk(node):
|
||||
if isinstance(node, dict):
|
||||
name = str(node.get("name") or "")
|
||||
if any(w in name for w in words):
|
||||
assert node.get("destination") != "material", (path.name, node)
|
||||
for value in node.values():
|
||||
walk(value)
|
||||
elif isinstance(node, list):
|
||||
for value in node:
|
||||
walk(value)
|
||||
|
||||
walk(json.loads(text))
|
||||
@@ -57,7 +57,7 @@ def test_단계가_못_서면_부모를_안_세우고_사유를_남긴다():
|
||||
build = _build()
|
||||
assert not any(code.startswith("B-FP-09-05#") for code in build.book.titles)
|
||||
assert "FP-09-05-01" in build.component_gaps["FP-09-05"] # 발파 착암기 층 없음
|
||||
assert "FP-12-38-02" in build.component_gaps["FP-12-38"] # 사용수량 미구현
|
||||
assert "FP-12-38-02" in build.component_gaps["FP-12-38"] # 사용수량 — 임대료·손료 단가 안 넣음
|
||||
# 깎기 표엔 기계 이름이 없음 — [주] 원문의 기종으로 섬(부모에서 물려받지 않음)
|
||||
assert "[주]" in build.book.details["D-FP-09-05-02#경암"][0].note # Q 줄은 D 에
|
||||
|
||||
|
||||
@@ -112,9 +112,10 @@ def test_자재_합계는_할증_한_번() -> None:
|
||||
|
||||
def test_할증_포함_재료량_공종에_자재가_재료비로_붙으면_오류() -> None:
|
||||
book = PriceBook()
|
||||
book.add_title(PriceTitle(code="M-시험", kind=PriceKind.MATERIAL, name="패널", unit="매"))
|
||||
book.add_title(PriceTitle(code="B-FP-12-38-02", kind=PriceKind.UNIT_PRICE, name="사용수량"))
|
||||
book.add_detail(PriceDetail("B-FP-12-38-02", "M-시험", Decimal("0.089")))
|
||||
# 유로폼 12-38-02 는 가드에서 뺐음(2026-09-14 301 ④ · 자재총괄에 줄 없음) — 돌망태 사각형으로 잼
|
||||
book.add_title(PriceTitle(code="M-시험", kind=PriceKind.MATERIAL, name="철망태", unit="㎥"))
|
||||
book.add_title(PriceTitle(code="B-FP-13-11-04", kind=PriceKind.UNIT_PRICE, name="사각형"))
|
||||
book.add_detail(PriceDetail("B-FP-13-11-04", "M-시험", Decimal("1.0")))
|
||||
# 금액을 세우지 않고도 걸림 — 줄이 붙는 순간이 어긴 자리
|
||||
with pytest.raises(DoubleCountError):
|
||||
check_materials_before_surcharge(book)
|
||||
|
||||
Reference in New Issue
Block a user