feat(b08): 고정형 항목도 일위대가 구성을 가짐 — 공종 코드는 비워 두고 사용자가 고르개로 고름(㉮) · 고르기 전엔 내역 줄이 서되 「미완 — 공종 코드 미정」 금액 밖 · 찾기는 코드 없는 줄 이름으로 후보만 띄움
936be972 실측: 봉화 제6호표 가져오기 → 내역 「기슭막이 H=2.0m 10m」 2줄이 「막힌 줄 9: 공종 코드 미정」으로 섬(전엔 줄이 사라짐) → 코드 3줄 고르고 6줄 수동 단가(검증용) → 일위대가 482,766원/m · 내역 4,827,660원 × 2줄 「수동 단가 6건 미확정」 · 되돌려 본체·인계·내역·원단위·장 전부 같음 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
@@ -176,6 +176,21 @@ def recipe_item(
|
||||
"reason": "가산 행 — 수량 계산에 안 씀" if is_percent else NOTE_SOURCE_QTY,
|
||||
}
|
||||
)
|
||||
# ㉮ 일위대가 구성 — 일위대가로 가는 줄마다 한 줄(수량 = 그 원단위 줄). **공종 코드는 비움**:
|
||||
# 원문 코드는 파일 카운터라 못 쓰고 이름으로 확정도 안 함(명세 2장) → 사용자가 고르개로 고름.
|
||||
# 고르기 전엔 그 장 금액이 「미완 — 공종 코드 미정」(조용한 0 아님 · 2026-09-14 판정).
|
||||
unit_rows = [
|
||||
{
|
||||
"seq": index,
|
||||
"name": row["name"],
|
||||
"spec": row["spec"],
|
||||
"unit": row["unit"],
|
||||
"from_row": row["seq"],
|
||||
}
|
||||
for index, row in enumerate(
|
||||
(row for row in rows if row["destination"] == "unit_price"), start=1
|
||||
)
|
||||
]
|
||||
notes = [NOTE_SOURCE_QTY]
|
||||
if percent:
|
||||
notes.append(NOTE_PERCENT.format(count=len(percent), names=" · ".join(percent)))
|
||||
@@ -191,6 +206,11 @@ def recipe_item(
|
||||
"vars": {},
|
||||
"tables": {},
|
||||
"rows": rows,
|
||||
"unit_price": {
|
||||
"unit": hopyo["unit"],
|
||||
"note": "STmate 호표 구성 — 줄마다 공종 코드를 고르개로 골라야 금액이 섬(원문 코드는 안 씀)",
|
||||
"rows": unit_rows,
|
||||
},
|
||||
# 출처 — 원문 공사명·파일명이 들어감. 공유 만들 때 뺄지·가릴지 판정(PLAN 4장).
|
||||
"origin": {
|
||||
"kind": "stmate_xlsx",
|
||||
|
||||
@@ -136,6 +136,11 @@ export function unitPriceEditor(options: EditorOptions): HTMLElement {
|
||||
find.addEventListener("click", () => {
|
||||
picking = index;
|
||||
render();
|
||||
// 코드가 아직 없는 줄은 그 줄 이름으로 **후보만** 띄움 — 고르는 것은 사용자(명세 2장 · 이름 자동 확정 금지).
|
||||
if (!row.ref_code && !row.work_item_code && row.name) {
|
||||
query.value = row.name;
|
||||
void search();
|
||||
}
|
||||
picker.querySelector("input")?.focus();
|
||||
});
|
||||
code.append(" ", find);
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""고정형 항목 사슬 ㉮ — 인계·내역으로 가는 문(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
|
||||
|
||||
|
||||
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원
|
||||
@@ -122,6 +122,10 @@ def test_착공판_계약단가_줄은_안_뽑고_항목에_적는다(tmp_path:
|
||||
assert "가산 행 1줄" in item["note"] and "공구손료" in item["note"]
|
||||
assert "계약단가·낙찰율 줄 1줄" in item["note"] and "현행 품셈 대조 전" in item["note"]
|
||||
assert item["item_kind"] == "fixed" and item["origin"]["project"] == "시험"
|
||||
# ㉮ 일위대가 구성 — 일위대가로 가는 줄만(별산·가산 행 뺌) · 공종 코드는 비워 둠(사용자가 고름)
|
||||
unit_rows = item["unit_price"]["rows"]
|
||||
assert [(r["name"], r["from_row"]) for r in unit_rows] == [("메쌓기", 2)]
|
||||
assert not any(r.get("work_item_code") or r.get("ref_code") for r in unit_rows)
|
||||
|
||||
|
||||
def test_시트가_없으면_사유(tmp_path: Path) -> None:
|
||||
|
||||
Reference in New Issue
Block a user