diff --git a/B09_Estimation/B09_Estimation_Calc_Labor.py b/B09_Estimation/B09_Estimation_Calc_Labor.py index 3b0d2c951..69c36ae85 100644 --- a/B09_Estimation/B09_Estimation_Calc_Labor.py +++ b/B09_Estimation/B09_Estimation_Calc_Labor.py @@ -115,6 +115,7 @@ def _block( parts[part] = None if s is None else rounddown(s, 0) return { **{k: block.get(k) for k in ("id", "kind", "name", "spec", "unit", "cite")}, + **({"spec_raw": block["spec_raw"]} if block.get("spec_raw") else {}), # 157-4 풍선 "key": block["id"], "lines": lines, **parts, @@ -171,6 +172,7 @@ def compute_labor( "kind": b["kind"], "name": b["name"], "spec": b["spec"], + **({"spec_raw": b["spec_raw"]} if b.get("spec_raw") else {}), "unit": b["unit"], "sum": b["sum"], "labor": b["labor"], diff --git a/B09_Estimation/B09_Estimation_Calc_Labor.ts b/B09_Estimation/B09_Estimation_Calc_Labor.ts index 48cf7835f..2baf3f019 100644 --- a/B09_Estimation/B09_Estimation_Calc_Labor.ts +++ b/B09_Estimation/B09_Estimation_Calc_Labor.ts @@ -115,6 +115,7 @@ function block(b: Row, books: Row, sums: Record, gaps: Gap[]): Row kind: b.kind, name: b.name, spec: b.spec, + ...(b.spec_raw ? { spec_raw: b.spec_raw } : {}), // 157-4 풍선(파이썬 짝) unit: b.unit, cite: b.cite, lines, @@ -160,6 +161,7 @@ export function computeLabor(daega: Row | null | undefined, sheets: Row, sangeun kind: b.kind, name: b.name, spec: b.spec, + ...(b.spec_raw ? { spec_raw: b.spec_raw } : {}), unit: b.unit, sum: b.sum, labor: b.labor, diff --git a/B09_Estimation/B09_Estimation_Labor_Source.py b/B09_Estimation/B09_Estimation_Labor_Source.py index 72a9ad8ef..b6d73a401 100644 --- a/B09_Estimation/B09_Estimation_Labor_Source.py +++ b/B09_Estimation/B09_Estimation_Labor_Source.py @@ -26,6 +26,7 @@ from pathlib import Path from typing import Any from B09_Estimation import B09_Estimation_Sangeun as sg +from B09_Estimation import B09_Estimation_Sangeun_Text as st from M02_MasterTemplete import M02_Item_Prices as item_prices from M02_MasterTemplete import M02_Labor_Doc as labor_doc from M02_MasterTemplete import M02_Labor_Ref as refs @@ -106,11 +107,16 @@ class Builder: line["spec"] = (eq.get(line["key"]) or {}).get("규격") or "" unit = str(logic.get("결과단위") or "").replace("원/", "") spec = " · ".join(f"{k} {v}" for k, v in sorted(given.items())) + shown = " · ".join( # 157-4 기계 · 직종 원문번호 = 「이름 규격」(번호 = 화면 풍선 spec_raw) + f"{k} {st._word(whole, k, v) if isinstance(v, str) else v}" + for k, v in sorted(given.items()) + ) return self.put( { "kind": "품셈", "name": title or logic.get("이름") or code, - "spec": spec, + "spec": shown, + "spec_raw": spec, "unit": unit, "cite": logic.get("출처") or "", "origin": {"로직": code, "입력값": given, "켜쌓기율": rate, "고름": picks}, @@ -197,7 +203,7 @@ class Builder: } block = self.blocks[bid] return {"kind": "daega", "key": bid, "name": block["name"], "spec": block["spec"], - "unit": block["unit"], "qty": each} # fmt: skip + "spec_raw": block.get("spec_raw"), "unit": block["unit"], "qty": each} # fmt: skip def precombo(self, whole: Any, ref: dict, doc: dict, copies: dict, depth: int = 0) -> str: """사전조합(SC) = 겹 대가 — 줄 수량 = 손(기본 1) · 로직 줄 = 품셈 호표.""" diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts index 94d8e491f..cb15fedec 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts @@ -19,6 +19,7 @@ import { type TabId, type TabTable, } from "./B09_Estimation_UI_Tabs"; +import { raw } from "./B09_Estimation_UI_Tabs_Sangeun"; const EMPTY = "연결 없음 — 연결된 구조물 조합 사본(일위대가)이 없음 · B05 · B06 에서 조합을 고르고 [저장]"; @@ -54,6 +55,7 @@ export const buildDaegaList: TabBuild = ({ sheets }): TabTable | string => { frozen: 3, hint: "호표 = 일위대가표 블록 합계 · 줄을 누르면 그 블록으로", link: (i: number): Link | null => (rows[i] ? { tab: "dg", key: String(rows[i].key) } : null), + tip: (i, key) => (key === "spec" ? raw(rows[i]?.spec, rows[i]?.spec_raw) : null), }; }; @@ -69,6 +71,7 @@ export const buildDaegaBlocks: TabBuild = ({ sheets }): TabTable | string => { key: b.id, name: `제 ${b.no} 호표 ${b.name}`, spec: b.spec, + spec_raw: b.spec_raw, unit: b.unit, note: b.kind, }); @@ -82,6 +85,7 @@ export const buildDaegaBlocks: TabBuild = ({ sheets }): TabTable | string => { key: `${b.id}|${n}`, name: `${CHILD}${ln.name || ""}`, spec: ln.spec || "", + spec_raw: ln.spec_raw, qty: ln.qty, unit: ln.unit || "", sum_price: ln.price, @@ -129,5 +133,7 @@ export const buildDaegaBlocks: TabBuild = ({ sheets }): TabTable | string => { hint: `단가 = B09 프로젝트 한 벌 굳힌 단가 · ${ROUNDS} · 줄 누름 = 단가를 낸 탭(대가 줄 = 그 블록)`, link: (i: number) => links[i] ?? null, rowClass: (i: number) => (titles.has(i) ? "is-title" : null), + tip: (i: number, key: string) => + key === "spec" ? raw(rows[i]?.spec, rows[i]?.spec_raw) : null, }; }; diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts index 8a285a8ad..1cf80dc9f 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts @@ -29,8 +29,8 @@ const PART_NAME: Record = { }; const PARTS = ["labor", "material", "expense"] as const; -/** 157-3 규격 풍선 = 원문번호 글(「이름 규격」 으로 바꾼 칸만) */ -const raw = (shown: unknown, codes: unknown): string | null => +/** 157-3 규격 풍선 = 원문번호 글(「이름 규격」 으로 바꾼 칸만) · 일위대가표도 씀(157-4) */ +export const raw = (shown: unknown, codes: unknown): string | null => codes && codes !== shown ? `원문번호 ${String(codes)}` : null; const notes = (sheets: Row): string => diff --git a/resources/tester/test_157_4_b09_labor_spec.py b/resources/tester/test_157_4_b09_labor_spec.py new file mode 100644 index 000000000..24f7a1f40 --- /dev/null +++ b/resources/tester/test_157_4_b09_labor_spec.py @@ -0,0 +1,80 @@ +"""157-4 B09 일위대가표 품셈 규격 — 기계 · 직종 원문번호 → 「이름 규격」 · 번호 = 풍선(157-3 `raw`). + +- 품셈 블록 `spec` = 「기계 굴착기(무한궤도) 0.6㎥ · …」 · `spec_raw` = 옛 글(번호) · 줄 그대로 +- 일위대가목록표 · 일위대가표 규격 칸 풍선 = 「원문번호 …」(바꾼 칸만) +""" + +import json +import re +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from B09_Estimation import B09_Estimation_Labor_Source as source # noqa: E402 +from M02_MasterTemplete import M02_Item_Prices as item_prices # noqa: E402 + +B09 = ROOT / "B09_Estimation" +IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) + + +def _block(code, given): + whole = item_prices.load_prices(item_prices.to_json(item_prices.collect([code]))) + builder = source.Builder() + bid = builder.poomsem(whole, {"로직": code, "입력값": given}) + return builder.blocks[bid] + + +def test_기계_번호는_이름_규격_번호는_spec_raw(): + given = {"기계": "0201-0060", "지역": "전국평균", "보정작업": "해당 없음"} + block = _block("GC000267", given) + assert block["spec"] == "기계 굴착기(무한궤도) 0.6㎥ · 보정작업 해당 없음 · 지역 전국평균" + assert block["spec_raw"] == "기계 0201-0060 · 보정작업 해당 없음 · 지역 전국평균" + assert len(block["lines"]) == 4 + + +def test_직종_번호도_이름(): + given = {"기계": "2106-0002", "지역": "전국평균", "조종원직종": "1048"} + block = _block("GC000275", given) + assert "1048" in block["spec_raw"] and "1048" not in block["spec"] + assert not re.search(r"\d{4}-\d{4}", block["spec"]) + + +def test_규격_칸_풍선(): + spec, raw = "기계 굴착기(무한궤도) 0.6㎥", "기계 0201-0060" + sheets = { + "daega": [{"no": 1, "key": "DG:a", "kind": "품셈", "name": "n", "spec": spec, + "spec_raw": raw, "unit": "시간"}], + "daega_blocks": [{"id": "DG:a", "no": 1, "kind": "품셈", "name": "n", "spec": spec, + "spec_raw": raw, "unit": "시간", "cite": "", "lines": [ + {"kind": "daega", "key": "DG:a", "name": "n", "spec": spec, + "spec_raw": raw, "qty": 1, "unit": "시간", "price": 1, + "amount": 1, "prices": {"labor": 1, "material": 0, "expense": 0}, + "amounts": {"labor": 1, "material": 0, "expense": 0}}], + "labor": 1, "material": 0, "expense": 0, "sum": 1}], + } # fmt: skip + sangeun = (B09 / "B09_Estimation_UI_Tabs_Sangeun.ts").read_text("utf-8") + src = re.search(r"^export const raw =.*?;$", sangeun, re.MULTILINE | re.DOTALL)[0] + "".join( + IMPORTS.sub("", (B09 / f"B09_Estimation_{n}.ts").read_text("utf-8")) + for n in ("UI_Tabs", "UI_Tabs_Labor") + ) # 산근 탭의 풍선 함수 `raw` 만(같은 이름 상수가 겹쳐 파일째는 못 잇음) + body = f""" +const sheets = {json.dumps(sheets, ensure_ascii=False)}; +const list = buildDaegaList({{ sheets }}); +const blocks = buildDaegaBlocks({{ sheets }}); +console.log(JSON.stringify({{ + list: list.tip(0, "spec"), listName: list.tip(0, "name"), + head: blocks.tip(0, "spec"), line: blocks.tip(1, "spec"), total: blocks.tip(2, "spec"), +}}));""" + result = subprocess.run( + ["node", "--experimental-strip-types", "--no-warnings", + "--input-type=module-typescript", "-e", src + body], + capture_output=True, text=True, encoding="utf-8", timeout=60, + ) # fmt: skip + assert result.returncode == 0, result.stderr + got = json.loads(result.stdout) + tip = "원문번호 기계 0201-0060" + assert got == {"list": tip, "listName": None, "head": tip, "line": tip, "total": None}