diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities.py b/B09_Estimation/B09_Estimation_BillOfQuantities.py index 3c42e1ac..a86d7f48 100644 --- a/B09_Estimation/B09_Estimation_BillOfQuantities.py +++ b/B09_Estimation/B09_Estimation_BillOfQuantities.py @@ -39,6 +39,13 @@ _ZERO = Decimal(0) #: **관급자재대에도, 도급 재료비에도 넣지 않는다** — 어느 쪽에 넣어도 총액이 틀린다. SUPPLY_UNKNOWN = "unknown" +#: 막힘 갈래를 사람 말로. **할 일이 다르므로 화면에서 갈라 보인다.** +_BLOCKED_LABELS = { + "input_missing": "입력이 필요합니다", + "unit_data_missing": "원단위가 없습니다(우리가 만들 것)", + "formula_missing": "전개식이 없습니다(우리가 만들 것)", +} + class BillError(ValueError): """내역서를 세울 수 없는 경우. 빈 표를 돌려주지 않고 멈춘다.""" @@ -72,6 +79,11 @@ class HandoffWorkItem: #: 묶음인데 아직 못 채운 조각 — 「단가 없음」과 「물량 없음」을 갈라 적는다. composite_not_ready: tuple = () structure_kind: str = "" + #: B08 이 적어 보낸 막힘 사유 — **문구는 B08 것을 그대로 쓴다**(두 벌로 짜지 않는다). + blocked_reason: str = "" + #: 막힘 갈래 — `input_missing`(사용자가 입력하면 풀림) / + #: `unit_data_missing`·`formula_missing`(우리가 만들어야 함). 할 일이 다르므로 가른다. + blocked_kind: str = "" @property def display_name(self) -> str: @@ -209,6 +221,8 @@ def parse_handoff(payload: dict[str, Any]) -> tuple[list[HandoffWorkItem], list[ composite_parts=tuple(row.get("composite_parts") or ()), composite_not_ready=tuple(row.get("composite_not_ready") or ()), structure_kind=row.get("structure_kind") or "", + blocked_reason=row.get("blocked_reason") or "", + blocked_kind=row.get("blocked_kind") or "", ) for row in payload["work_items"] ] @@ -539,6 +553,23 @@ def _leaf_row( result.excluded.append(row) return row + if item.blocked_reason: + # B08 이 「왜 못 골랐는지」를 적어 보냈다 — **그 문구를 그대로** 보인다. + # 사용자가 입력하면 풀리는 것(`input_missing`)과 우리가 만들어야 하는 것을 + # 가르지 않으면, 사용자가 「후보를 고르면 되나」로 잘못 읽는다. + row.note = f"{_BLOCKED_LABELS.get(item.blocked_kind, '막힘')} — {item.blocked_reason}" + result.missing.append( + { + "name": row.name, + "code": node.code, + "unit": row.unit, + "quantity": str(item.quantity), + "reason": row.note, + "blocked_kind": item.blocked_kind, + } + ) + return row + price_code = f"B-{node.code}" if price_code not in unit_prices.book.titles: # 한 층 아래에 일위대가가 있으면 **후보로 보여준다** — 임의로 고르지 않는다 diff --git a/B09_Estimation/B09_Estimation_UI_Page.ts b/B09_Estimation/B09_Estimation_UI_Page.ts index 5dd1e8fe..a40987a1 100644 --- a/B09_Estimation/B09_Estimation_UI_Page.ts +++ b/B09_Estimation/B09_Estimation_UI_Page.ts @@ -668,7 +668,13 @@ interface BillDto { rows: number; detail_rows: number; body_total_krw: string; - missing: Array<{ name: string; reason: string; unit?: string; quantity?: string }>; + missing: Array<{ + name: string; + reason: string; + unit?: string; + quantity?: string; + blocked_kind?: string; + }>; notes: string[]; }; } @@ -872,13 +878,30 @@ export async function renderB09Estimation(root: HTMLElement): Promise { note.className = "b09-hint"; note.textContent = `${L("B09_Estimation_Boq_Missing")} (${bill.summary.missing.length})`; body.append(note); - const list = document.createElement("ul"); - for (const item of bill.summary.missing) { - const li = document.createElement("li"); - li.textContent = `${item.name} — ${item.reason}`; - list.append(li); + + // ⚠ **할 일이 다르므로 갈라 보인다** — 「사용자가 입력하면 풀리는 것」과 + // 「우리가 만들어야 하는 것」. 한 목록에 섞으면 사용자가 무엇을 해야 할지 못 읽는다. + const needsInput = bill.summary.missing.filter( + (item) => item.blocked_kind === "input_missing", + ); + const rest = bill.summary.missing.filter((item) => item.blocked_kind !== "input_missing"); + for (const [labelKey, group] of [ + ["B09_Estimation_Boq_NeedsInput", needsInput], + ["B09_Estimation_Boq_NeedsWork", rest], + ] as Array<[keyof typeof ui_locales, typeof bill.summary.missing]>) { + if (group.length === 0) continue; + const head = document.createElement("div"); + head.className = "b09-hint"; + head.textContent = `${L(labelKey)} (${group.length})`; + body.append(head); + const list = document.createElement("ul"); + for (const item of group) { + const li = document.createElement("li"); + li.textContent = `${item.name} — ${item.reason}`; + list.append(li); + } + body.append(list); } - body.append(list); } if (bill.materials.length > 0) { diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 038fb415..40e7a78a 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -679,6 +679,14 @@ export const ui_locales_b2 = { "검산용 줄 — 수량만 보이고 금액을 매기지 않습니다", "Check rows — quantity only, never priced", ], + B09_Estimation_Boq_NeedsInput: [ + "입력하면 풀리는 것 — 설계 화면에서 값을 고르면 금액이 섭니다", + "Waiting on input — pick the value on the design screen and the amount appears", + ], + B09_Estimation_Boq_NeedsWork: [ + "우리가 만들어야 하는 것 — 원단위·전개식이 아직 없습니다", + "Needs build — unit data or formula is missing", + ], B09_Estimation_Boq_Missing: [ "금액을 못 세운 줄 — 0 으로 채우지 않고 그대로 보입니다", "Rows without an amount — shown as-is, not zero-filled",