Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -176,6 +176,21 @@ def recipe_item(
|
|||||||
"reason": "가산 행 — 수량 계산에 안 씀" if is_percent else NOTE_SOURCE_QTY,
|
"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]
|
notes = [NOTE_SOURCE_QTY]
|
||||||
if percent:
|
if percent:
|
||||||
notes.append(NOTE_PERCENT.format(count=len(percent), names=" · ".join(percent)))
|
notes.append(NOTE_PERCENT.format(count=len(percent), names=" · ".join(percent)))
|
||||||
@@ -191,6 +206,11 @@ def recipe_item(
|
|||||||
"vars": {},
|
"vars": {},
|
||||||
"tables": {},
|
"tables": {},
|
||||||
"rows": rows,
|
"rows": rows,
|
||||||
|
"unit_price": {
|
||||||
|
"unit": hopyo["unit"],
|
||||||
|
"note": "STmate 호표 구성 — 줄마다 공종 코드를 고르개로 골라야 금액이 섬(원문 코드는 안 씀)",
|
||||||
|
"rows": unit_rows,
|
||||||
|
},
|
||||||
# 출처 — 원문 공사명·파일명이 들어감. 공유 만들 때 뺄지·가릴지 판정(PLAN 4장).
|
# 출처 — 원문 공사명·파일명이 들어감. 공유 만들 때 뺄지·가릴지 판정(PLAN 4장).
|
||||||
"origin": {
|
"origin": {
|
||||||
"kind": "stmate_xlsx",
|
"kind": "stmate_xlsx",
|
||||||
|
|||||||
@@ -136,6 +136,11 @@ export function unitPriceEditor(options: EditorOptions): HTMLElement {
|
|||||||
find.addEventListener("click", () => {
|
find.addEventListener("click", () => {
|
||||||
picking = index;
|
picking = index;
|
||||||
render();
|
render();
|
||||||
|
// 코드가 아직 없는 줄은 그 줄 이름으로 **후보만** 띄움 — 고르는 것은 사용자(명세 2장 · 이름 자동 확정 금지).
|
||||||
|
if (!row.ref_code && !row.work_item_code && row.name) {
|
||||||
|
query.value = row.name;
|
||||||
|
void search();
|
||||||
|
}
|
||||||
picker.querySelector("input")?.focus();
|
picker.querySelector("input")?.focus();
|
||||||
});
|
});
|
||||||
code.append(" ", find);
|
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 "계약단가·낙찰율 줄 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"] == "시험"
|
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:
|
def test_시트가_없으면_사유(tmp_path: Path) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user