Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
124 lines
4.7 KiB
Python
124 lines
4.7 KiB
Python
"""고정형 항목 사슬 ㉮ — 인계·내역으로 가는 문(2026-09-14 브레인 판정 「공종 코드가 없으면 어디에도 안 감」).
|
||
|
||
실측(936be972): 뽑은 고정형엔 일위대가 구성(`unit_price.rows`)이 없어 `priced_sheets` 가 그 장을 안 셈 →
|
||
구조물 줄 수량 0 · 내역 줄 사라짐 · 본체 −4,062,480. 고친 길: 일위대가로 가는 줄마다 구성 줄을 두되
|
||
**공종 코드는 비움** → 그 장은 호표 줄로 인계되고 금액은 「미완 — 공종 코드 미정」(조용한 0 아님) →
|
||
사용자가 고르개로 코드를 고르면 금액이 섬. 자원 축은 후보만(이름 자동 확정 금지 · 명세 2장).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from decimal import Decimal
|
||
from pathlib import Path
|
||
from types import SimpleNamespace
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
if str(ROOT) not in sys.path:
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from B08_Quantity.B08_Quantity_Engine_StmateRecipe import recipe_item # noqa: E402
|
||
from B08_Quantity.B08_Quantity_Engine_StructurePriceLink import ( # noqa: E402
|
||
price_sheets,
|
||
priced_sheets,
|
||
)
|
||
from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import with_rows # noqa: E402
|
||
from B09_Estimation.B09_Estimation_PriceBook import Money3, PriceBookError # noqa: E402
|
||
|
||
HOPYO = {
|
||
"no": 6,
|
||
"source_code": "B00010",
|
||
"name": "기슭막이",
|
||
"spec": "H=2.0m",
|
||
"unit": "M",
|
||
"basis": [],
|
||
"contract_rows": 0,
|
||
"rows": [
|
||
{
|
||
"name": "깬잡석채집",
|
||
"spec": "L=45cm",
|
||
"amount": 2.09,
|
||
"unit": "M2",
|
||
"remark": "",
|
||
"source_code": "",
|
||
},
|
||
{
|
||
"name": "고임돌채집",
|
||
"spec": "기계",
|
||
"amount": 0.31,
|
||
"unit": "M3",
|
||
"remark": "",
|
||
"source_code": "",
|
||
},
|
||
],
|
||
}
|
||
|
||
|
||
class Book:
|
||
titles = {"B-FP-X#채집": ("깬잡석 채집", "㎡", Money3(Decimal(0), Decimal(1000), Decimal(0)))}
|
||
|
||
def title(self, code: str) -> SimpleNamespace:
|
||
if code not in self.titles:
|
||
raise PriceBookError(f"단가표에 없는 코드입니다: {code}")
|
||
name, unit, _money = self.titles[code]
|
||
return SimpleNamespace(name=name, spec="", unit=unit)
|
||
|
||
def resolve(self, code: str) -> Money3:
|
||
return self.titles[code][2] if code in self.titles else self.title(code)
|
||
|
||
|
||
def _sheet(item: dict) -> dict:
|
||
"""`apply_templates` 가 낸 장 모양 — 고정형 줄 = 박힌 단위량."""
|
||
return {
|
||
"type_id": item["type_id"],
|
||
"title": "돌쌓기(찰) H=2.5",
|
||
"formula_sheet": {"vars": {}},
|
||
"billing_unit": "m",
|
||
"members": [{"structure_id": "s1", "billing_quantity": 10}],
|
||
"rows": [
|
||
{
|
||
"no": row["seq"],
|
||
"unit_amount": float(row["amount"]),
|
||
"unit": row["unit"],
|
||
"destination": row["destination"],
|
||
}
|
||
for row in item["rows"]
|
||
],
|
||
}
|
||
|
||
|
||
def test_고정형_장이_호표_줄로_인계되고_코드_없으면_미완() -> None:
|
||
item = {
|
||
**recipe_item(HOPYO, type_id="masonry_wet", file_name="a.xlsx", project="p"),
|
||
"code": "AX-ST-0000abcd",
|
||
}
|
||
entries = priced_sheets([_sheet(item)], {"masonry_wet": item})
|
||
assert [e["structure_ids"] for e in entries] == [["s1"]] # 전엔 빈 목록 — 인계에서 끊겼음
|
||
prices = price_sheets(entries, Book(), lambda code, value: None, [item])
|
||
(entry,) = prices.values()
|
||
assert entry["blocked"] == 2 and entry["total"] == 0
|
||
assert all("공종 코드 미정" in reason for reason in entry["reasons"])
|
||
|
||
|
||
def test_찾기는_코드_없는_줄_이름으로_후보만_띄운다() -> None:
|
||
ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_StructureSheet_UnitPriceEdit.ts").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
assert "query.value = row.name" in ui and "!row.ref_code && !row.work_item_code" in ui
|
||
# 후보 0 이면 다음 길(갈래 바꾸기 · 수동 단가)을 적음 — 조용히 「없음」으로 막히지 않게(2026-09-14 브레인)
|
||
assert "수동 단가를 넣을 것" in ui
|
||
|
||
|
||
def test_사용자가_코드를_고르면_금액이_선다() -> None:
|
||
item = {
|
||
**recipe_item(HOPYO, type_id="masonry_wet", file_name="a.xlsx", project="p"),
|
||
"code": "AX-ST-0000abcd",
|
||
}
|
||
picked = [
|
||
dict(row, ref_code="B-FP-X#채집", unit="㎡") for row in item["unit_price"]["rows"][:1]
|
||
]
|
||
template = with_rows(item, picked)
|
||
entries = priced_sheets([_sheet(item)], {"masonry_wet": template})
|
||
(entry,) = price_sheets(entries, Book(), lambda code, value: None, [template]).values()
|
||
assert entry["blocked"] == 0 and entry["total"] == Decimal("2090") # 2.09㎡ × 1,000원
|