- 양식 일위대가로 셀 구조물도 장은 인계 줄 하나(코드 AX-ST · 갈래 = 제원 키 · 수량 연장 m) - 양식이 품은 줄(기초잡석)은 따로 인계 안 함 — 이중계상 막음 - B09 내역이 자기 단가표로 B08 일위대가 엔진을 돌려 금액 · 못 받거나 막히면 사유 - 수동 단가도 씀 — 내역 요약에 미확정 N건 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
129 lines
5.8 KiB
Python
129 lines
5.8 KiB
Python
"""구조물도 일위대가 → B09 내역 받는 문 (2026-09-13, PLAN 6장 첫 일감 ② · 브레인 판정).
|
|
|
|
겨누는 것
|
|
① 양식 일위대가로 셀 구조물은 인계 줄 하나 = 코드 AX-ST · 갈래 = 제원 키 · 수량 = 연장 m
|
|
② 양식이 품은 기초잡석은 따로 인계 안 함(이중계상) — 양식 장이 없으면 종전대로 섬
|
|
③ 제원 키는 제목 글이 아니라 제원 값 — 연장이 달라도 같은 키 · 뒷길이가 다르면 다른 키
|
|
④ B09 내역 — 호표 금액 × 연장 · 금액 못 받으면 사유 · 수동 단가는 미확정
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
from functools import lru_cache
|
|
|
|
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
|
|
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff
|
|
from B08_Quantity.B08_Quantity_Engine_StructurePriceLink import (
|
|
price_sheets,
|
|
priced_sheets,
|
|
structure_price_code,
|
|
)
|
|
from B08_Quantity.B08_Quantity_Engine_StructureSheet import build_standard_sheets
|
|
from B08_Quantity.B08_Quantity_Engine_StructureTemplate import apply_templates, load_template
|
|
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table
|
|
from B09_Estimation.B09_Estimation_BillOfQuantities import bill_summary, build_bill
|
|
from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at
|
|
from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices, find_variant_code
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _build():
|
|
return build_unit_prices()
|
|
|
|
|
|
def _wall(sid: str, start: float, end: float, back: int = 45) -> dict:
|
|
return StructureInstance.model_validate(
|
|
{
|
|
"structure_id": sid,
|
|
"type_id": "masonry_wet",
|
|
"placement": "interval",
|
|
"start_m": start,
|
|
"end_m": end,
|
|
"options": {"height_m": 2.5, "back_len_cm": back},
|
|
}
|
|
).model_dump()
|
|
|
|
|
|
def _chain(walls: list[dict]) -> tuple[dict, list[dict]]:
|
|
table = build_table(walls, {"masonry_wet": "돌쌓기(찰)"}, {}, {}, None)
|
|
payload = build_standard_sheets(table, {})
|
|
apply_templates(payload, {}, {})
|
|
entries = priced_sheets(payload["sheets"], {"masonry_wet": load_template("masonry_wet")})
|
|
return table, entries
|
|
|
|
|
|
def _rows(handoff: dict, prefix: str) -> list[dict]:
|
|
return [
|
|
r for r in handoff["work_items"] if str(r.get("work_item_code") or "").startswith(prefix)
|
|
]
|
|
|
|
|
|
def test_양식_장_구조물은_호표_줄_하나로_가고_기초잡석은_따로_안_간다() -> None:
|
|
table, entries = _chain([_wall("a", 0, 10), _wall("b", 50, 56)])
|
|
assert len(entries) == 1 and entries[0]["structure_ids"] == ["a", "b"]
|
|
assert "기초잡석" in entries[0]["covered"] and "모르터" in entries[0]["covered"]
|
|
|
|
before = build_handoff(unit_quantity_table=table)
|
|
assert _rows(before, "FP-13-04-05") and _rows(before, "FP-12-25") # 종전 — 돌쌓기 ㎡ + 잡석
|
|
|
|
after = build_handoff(unit_quantity_table=table, priced_sheets=entries)
|
|
ax = _rows(after, "AX-ST-")
|
|
assert [(r["unit"], r["quantity"]) for r in ax] == [("m", 10.0), ("m", 6.0)]
|
|
assert {r["variant_axis"] for r in ax} == {"structure_sheet"}
|
|
assert not _rows(after, "FP-13-04-05") and not _rows(after, "FP-12-25")
|
|
assert set(ax[0]) == set(before["work_items"][0]) # 인계 칸 계약 그대로
|
|
|
|
|
|
def test_제원_키는_제원_값이고_연장과_제목은_안_든다() -> None:
|
|
_table, entries = _chain([_wall("a", 0, 10), _wall("c", 20, 30, back=55)])
|
|
keys = {e["structure_ids"][0]: e["key"] for e in entries}
|
|
assert "L3=45" in keys["a"] and "L3=55" in keys["c"] and "|L=" not in keys["a"]
|
|
assert keys["a"] != keys["c"]
|
|
assert structure_price_code("AX-ST-56e81a2c", "A = 1 | B~2").endswith("#A=1|B~2")
|
|
|
|
|
|
def _bill(manual: dict | None = None):
|
|
build = _build()
|
|
table, entries = _chain([_wall("a", 0, 10)])
|
|
handoff = build_handoff(unit_quantity_table=table, priced_sheets=entries)
|
|
prices = price_sheets(
|
|
entries,
|
|
build.book,
|
|
lambda code, value: find_variant_code(code, value, build),
|
|
[load_template("masonry_wet")],
|
|
manual,
|
|
)
|
|
return entries, prices, build_bill(handoff, build=build, structure_prices=prices)
|
|
|
|
|
|
def test_내역은_호표_금액에_연장을_곱한다() -> None:
|
|
entries, prices, bill = _bill()
|
|
ref = structure_price_code(entries[0]["code"], entries[0]["key"])
|
|
entry = prices[ref]
|
|
row = next(r for r in bill.rows if r.code == entries[0]["code"])
|
|
# 모르터 공종 코드가 선 뒤(랩탑 메인 AX-WK) 찰쌓기 호표 세 줄이 다 섬.
|
|
assert not entry["blocked"], entry["reasons"]
|
|
assert row.unit_price_krw == round_at(entry["money"].total, OutputPlace.UNIT_PRICE_ROW)
|
|
assert row.amount_krw == round_at(entry["money"].scaled(Decimal(10)).total, OutputPlace.BOQ_ROW)
|
|
assert row.quantity == Decimal(10) and f"호표 {ref}" in row.note
|
|
|
|
|
|
def test_금액을_못_받으면_사유로_서고_수동_단가는_미확정으로_센다() -> None:
|
|
build = _build()
|
|
table, entries = _chain([_wall("a", 0, 10)])
|
|
handoff = build_handoff(unit_quantity_table=table, priced_sheets=entries)
|
|
missing = build_bill(handoff, build=build, structure_prices={})
|
|
row = next(r for r in missing.rows if r.code == entries[0]["code"])
|
|
assert row.amount_krw is None and "못 받음" in row.note
|
|
|
|
# 모든 일위대가 줄에 수동 단가 — 막힘 없이 서고 미확정 3건.
|
|
manual = {
|
|
"masonry_wet": {str(s): {"material": 1000, "labor": 0, "expense": 0} for s in (1, 2, 3)}
|
|
}
|
|
_entries, _prices, bill = _bill(manual)
|
|
row = next(r for r in bill.rows if r.code == entries[0]["code"])
|
|
assert row.amount_krw is not None and "미확정" in row.note
|
|
summary = bill_summary(bill)
|
|
assert summary["unconfirmed_count"] == 3 and summary["unconfirmed"][0]["count"] == 3
|