Merge remote-tracking branch 'origin/dev' into sub_laptop_1

This commit is contained in:
2026-09-14 21:53:28 +09:00
13 changed files with 357 additions and 62 deletions
@@ -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",
+124
View File
@@ -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))
+1 -1
View File
@@ -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)