diff --git a/B09_Estimation/B09_Estimation_Bill_Items.py b/B09_Estimation/B09_Estimation_Bill_Items.py index f9b4fa725..3bf6feba6 100644 --- a/B09_Estimation/B09_Estimation_Bill_Items.py +++ b/B09_Estimation/B09_Estimation_Bill_Items.py @@ -16,6 +16,7 @@ from typing import Any from B08_Quantity.B08_Quantity_Haul import bill_key, haul_groups from B08_Quantity.B08_Quantity_Material_Calc import _structure_row +from M02_MasterTemplete.M02_Item_Source import item_name WHY_EARTH = "조합 밖 토공 산근 — 연결 없음" WHY_READY = "준비공 대가 — 연결 없음" @@ -47,6 +48,20 @@ EARTH: list[tuple[str, str, str, tuple[str, str, str | None] | None]] = [ #: 구조물 집계표 열 id 머리 → 공종(그 밖 = 2.2 배수공) · 커플링 = 자재라 뺌 STRUCTURE_SECTION = {"pipe:revet|": "2.1", "pipe:chute|": "2.3", "pipe:wall|": "2.3"} SKIP_COLUMNS = ("pipe:cp|",) +#: 층 이름 — 화면 낱말(번호 · 영문 층 이름은 화면에 안 냄) +LAYER_WORD = {"system": "마스터", "company": "회사", "personal": "내 자료집", "project": "이 공사"} + + +def combo_text(combo: str, names: dict[str, str]) -> str: + """조합 id 「{층}|{주인}|{키}」 → 「내 자료집 · 조합 이름」(이름을 못 읽으면 층 낱말만).""" + layer, _, rest = combo.partition("|") + owner, _, key = rest.partition("|") + if combo not in names: + try: + names[combo] = item_name({"layer": layer, "owner": owner or None, "key": key}) + except (LookupError, KeyError, ValueError, OSError): + names[combo] = "" + return " · ".join(t for t in (LAYER_WORD.get(layer, ""), names[combo]) if t) def _same(row: dict[str, Any], want: tuple[str, str, str | None]) -> bool: @@ -80,13 +95,14 @@ def structure_items(summary: dict[str, Any] | None) -> list[dict[str, Any]]: columns = (summary or {}).get("columns") or [] total = (summary or {}).get("total") or {} out = [] + names: dict[str, str] = {} for column in columns: cid = str(column.get("id") or "") if cid.startswith(SKIP_COLUMNS): continue section = next((s for head, s in STRUCTURE_SECTION.items() if cid.startswith(head)), "2.2") name, spec, unit = _structure_row(column) - combos = [c for c in column.get("combos") or [] if c] + combos = [combo_text(c, names) for c in column.get("combos") or [] if c] out.append( { "key": f"st:{cid}", diff --git a/B09_Estimation/B09_Estimation_Labor_Source.py b/B09_Estimation/B09_Estimation_Labor_Source.py index ba3d269db..7520f426d 100644 --- a/B09_Estimation/B09_Estimation_Labor_Source.py +++ b/B09_Estimation/B09_Estimation_Labor_Source.py @@ -33,6 +33,7 @@ from typing import Any from B09_Estimation import B09_Estimation_Bill_Library as library from B09_Estimation import B09_Estimation_Sangeun as sg from B09_Estimation import B09_Estimation_Sangeun_Text as st +from B09_Estimation.B09_Estimation_Bill_Items import LAYER_WORD 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_Fixed as fixed @@ -266,7 +267,8 @@ class Builder: { "kind": "사전조합", "name": doc.get("이름") or ref.get("키"), - "spec": str(ref.get("키") or ""), + "spec": LAYER_WORD.get(str(ref.get("층")), ""), # 번호(키)는 풍선 글로만 + "spec_raw": str(ref.get("키") or ""), "unit": doc.get("단위") or "", "cite": "", "origin": {"참조": refs.ref_id(ref)}, diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts index 95b7fef78..c633edc0c 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts @@ -38,7 +38,7 @@ async function copyBlock(ctx: TabContext, raw: DataRow[], row: Row, block: Row): note: `「${block.name}」 을 이 공사 안 새 호표로 — 고친 칸까지 복사`, fields: [ { key: "name", label: "이름", value: `${block.name} 복사`, required: true }, - { key: "spec", label: "규격", value: block.spec || "" }, + { key: "spec", label: "규격", value: block.kind === "사전조합" ? "" : block.spec || "" }, ], okLabel: "만들기", closeLabel: "닫기", @@ -106,7 +106,7 @@ async function savePersonal(ctx: TabContext, block: Row): Promise { const tail = left ? ` · 안 담은 고친 칸 ${left}(품 ${skip.품} · 단가 ${skip.단가}) — 품은 고정 줄(PLAN 705) 뒤 담김` : ""; - showToast(`내 자료집에 저장 「${got.이름}」(${got.키})${tail}`, "success", 6000); + showToast(`내 자료집에 저장 「${got.이름}」${tail}`, "success", 6000); return; } if (status !== 409 || !same) { @@ -116,7 +116,7 @@ async function savePersonal(ctx: TabContext, block: Row): Promise { } const over = await askConfirm({ title: "같은 이름", - message: `내 자료집에 「${name.trim()}」(${same}) 이 있음 — 덮어쓰기 · 새 이름`, + message: `내 자료집에 「${name.trim()}」 이 있음 — 덮어쓰기 · 새 이름`, okLabel: "덮어쓰기", cancelLabel: "새 이름", }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index 280c3b2d6..c7286f964 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -455,7 +455,7 @@ export function mountLaborDetail( if (!ref) return; void openRefEdit({ ref, - title: `${ref.키} ${lineOf(lineId({ 참조: ref }) ?? "")?.이름 ?? ""}`.trim(), + title: lineOf(lineId({ 참조: ref }) ?? "")?.이름 ?? "", readOnly: !edit || ref.층 !== opt.layer, closed: () => alive && recalc(), }); @@ -466,7 +466,7 @@ export function mountLaborDetail( const row = cur.담은로직[i]; const line = lineOf(lineId(row) ?? ""); openQtyFor( - `${row.참조?.키 ?? row.로직} ${(row.참조 ? line?.이름 : logics.get(row.로직)?.이름) ?? ""}`.trim(), + `${row.참조 ? "" : row.로직} ${(row.참조 ? line?.이름 : logics.get(row.로직)?.이름) ?? ""}`.trim(), row, line, line?.분모 ?? "", @@ -491,7 +491,7 @@ export function mountLaborDetail( pick: (ref: LaborRef, name: string, 별칭: string | null = null) => { const id = lineId({ 참조: ref }); if (cur.담은로직.some((r) => lineId(r) === id)) - return showToast(tc("Labor_AlreadyRef").replace("{value}", `${ref.키} ${name}`), "error"); + return showToast(tc("Labor_AlreadyRef").replace("{value}", name), "error"); const 수량 = lib ? { 손: 1 } : null; // 사전조합 줄 기본 1(51-6) cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null, 입력값: {}, 수량 }); changed(); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts index b4f6bd348..8c05e65c5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts @@ -34,6 +34,13 @@ const money = (m: LaborCost | null | undefined, times = 1): HopyoMoney => } : {}; +/** 층 낱말 — 번호(키)는 화면 글에 안 냄 */ +const layerWord = (layer: string): string => + layer === "personal" ? tc("Grid_Mine") : tc(layer === "company" ? "Lib_Company" : "Lib_System"); + +/** 일위대가번 — 번호꼴(SU-000011 · SU01010101)은 사람 글이 아니라 뺌 */ +const humanNo = (no?: string): string => (no && !/^S[A-Z]\d?-?\d{6,8}$/.test(no) ? no : ""); + /** 문서 + calc 답 → 호표 하나 — calc 없으면(계산 중) 금액 빈 칸 · 빈 줄 0 */ export function laborBlock( doc: LaborDoc, @@ -68,7 +75,9 @@ export function laborBlock( unit: line?.분모 ?? (row.참조 ? "" : (logic?.결과단위 ?? "")), prices: money(line?.기준, line?.배 || 1), amounts: money(line?.반영), - note: line?.까닭 ?? (calc && qty === null ? tc("Grid_NoQty") : (row.참조?.키 ?? row.로직)), + note: + line?.까닭 ?? + (calc && qty === null ? tc("Grid_NoQty") : row.참조 ? layerWord(row.참조.층) : row.로직), tag: HOPYO_TAG.pum, }; }); @@ -96,7 +105,7 @@ export function laborBlock( const lines = [...logicLines, ...matLines]; const block: HopyoBlock = { key: doc.열, - name: doc.일위대가번 || doc.열, + name: humanNo(doc.일위대가번) || (doc as { 이름?: string }).이름 || tc("Grid_Head"), unit: doc.단위 ?? "", total: money(calc?.반영합), lines, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts index bc08770ce..75916b241 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts @@ -15,7 +15,7 @@ import { tc } from "./M02_MasterTemplete_Labor_Text"; export interface RefEditOptions { ref: LaborRef; - /** 「SC-000001 돌붙임」 */ + /** 「돌붙임」 */ title: string; readOnly: boolean; closed: () => void; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index 0d8541919..d64dbdfc9 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -179,6 +179,8 @@ const TEXT = { Grid_Apart: ["별도계상 · 합계 밖", "Separate · not in total"], Grid_Linked: ["이 공사 단가에 이음", "Linked to project price"], Grid_Hand: ["손 단가(붙인 값)", "Hand price (pasted)"], + Grid_Head: ["일위대가", "Unit price"], + Grid_Mine: ["내 자료집", "My library"], } as const satisfies Record; export type ComboTextKey = keyof typeof TEXT; diff --git a/resources/tester/test_701_1_b08_wall_summary.py b/resources/tester/test_701_1_b08_wall_summary.py index c8723be4b..f651ca569 100644 --- a/resources/tester/test_701_1_b08_wall_summary.py +++ b/resources/tester/test_701_1_b08_wall_summary.py @@ -125,4 +125,4 @@ def test_B09_금액_길이_곱하기_대가(out): inputs = {"bill": {"items": structure_items(out)}} sheets = compute_estimation(inputs, {}, {"s-wall": price}.get) row = next(r for r in sheets["bill"]["rows"] if r["key"].endswith("#blk:w")) - assert (row["qty"], row["sum"], row["note"]) == (12.0, 1495128, "대가1 · system||SS000201") + assert (row["qty"], row["sum"], row["note"]) == (12.0, 1495128, "대가1 · 마스터") diff --git a/resources/tester/test_704_3_hopyo_grid.py b/resources/tester/test_704_3_hopyo_grid.py index 935a63bcd..96bc0d304 100644 --- a/resources/tester/test_704_3_hopyo_grid.py +++ b/resources/tester/test_704_3_hopyo_grid.py @@ -58,7 +58,7 @@ def test_머리_구성_합계_줄_꼬리표_빈_줄(): STUBS = r""" -const tc = (k) => ({ Grid_NoQty: "수량 없음", Grid_Apart: "별도계상 · 합계 밖", Labor_Layer: "켜쌓기 {n}%" })[k] ?? k; +const tc = (k) => ({ Grid_NoQty: "수량 없음", Grid_Apart: "별도계상 · 합계 밖", Labor_Layer: "켜쌓기 {n}%", Grid_Head: "일위대가", Grid_Mine: "내 자료집", Lib_Company: "회사", Lib_System: "마스터" })[k] ?? k; const valueLine = (inputs, values) => ({ text: Object.values(values ?? {}).join(" · "), empty: 0 }); """ @@ -108,7 +108,7 @@ def test_M02_문서_calc_를_격자_줄로(): "\nconsole.log(JSON.stringify({ b, foot: hopyoGrid([b]).rows.at(-1), wait: hopyoGrid([wait]).rows.at(-1) }));" ), STUBS) # fmt: skip b = out["b"] - assert b["name"] == "SU-000006" and b["unit"] == "m" and b["total"]["sum"] == 112559.55 + assert b["name"] == "일위대가" and b["unit"] == "m" and b["total"]["sum"] == 112559.55 one, two, mt1, mt2 = b["lines"] assert ( one["name"] == "돌쌓기 찰쌓기" @@ -141,3 +141,19 @@ def test_세_곳_같은_부품(): assert '"@ui/ui_template_hopyo_grid"' in m02 and '"@ui/ui_template_hopyo_grid"' in b09 kit = (ROOT / "ui_template/ui_template_ui_kit_parts.ts").read_text("utf-8") assert 'key: "hopyo-grid"' in kit # 부품 모음 견본 + + +def test_번호는_글에_안_냄_사람_글은_그대로(): + files = [GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts", + "M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts", + "M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip + ref = {"로직": "", "별칭": None, "입력값": {}, "수량": {"손": 1}, + "참조": {"층": "personal", "주인": "1/3", "키": "SC-000002"}} # fmt: skip + named = {**DOC, "일위대가번": "제5호표", "담은로직": [ref]} + out = _node(files, ( + f"\nconst a = laborBlock({json.dumps(named, ensure_ascii=False)}, null, new Map());" + f"\nconst c = laborBlock({json.dumps({**named, '일위대가번': 'SP3-000015'}, ensure_ascii=False)}, null, new Map());" + "\nconsole.log(JSON.stringify({ a, c }));" + ), STUBS) # fmt: skip + assert out["a"]["name"] == "제5호표" and out["c"]["name"] == "일위대가" + assert out["a"]["lines"][0]["note"] == "내 자료집"