From 30c062bd484c98727ca983b191ea279643d0b9ea Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 15 Nov 2026 15:50:56 +0900 Subject: [PATCH] =?UTF-8?q?refactor(841-1):=20B09=20=EC=9E=AC=EB=A3=8C=20?= =?UTF-8?q?=EA=B4=80=EA=B8=89=20=C2=B7=20=EB=B3=84=EB=8F=84=20=3D=20M02=20?= =?UTF-8?q?=EC=9E=90=EC=9E=AC=ED=91=9C=EC=8B=9C=20=E2=80=94=20A=20?= =?UTF-8?q?=EA=B1=B8=EC=9D=8C=20=EC=9D=B4=EC=96=B4=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 대가 · 산근 재료 줄 = M02 자재표시(기본 별도계상 · 관급) → 대가 · 산근 안 0 원(M02 셈과 같음) · % 덧줄 밑수는 엔진 값 그대로 - 자재단가대비표 기본 표시 = M02 자재표시(사용자 표시가 앞섬 · 「사급」 = 기본 끔 · 칸 누름 차례 관급 → 별도 → 사급) - 4 자재대 · 관급자재대 수량 = 대가 · 산근 밖 재료 역전개 수량(aside_qty) · 집계표에서 겹침 0 - 새 파일 B09_Estimation_Supply.py(표시 함수 · Router 700 줄 막음) - 견본 대가 = M02 반영합(1,527,221 · 1,267,314) · 전후 표 tmp/841_1/전후표.md Co-Authored-By: Claude Opus 5.5 --- B09_Estimation/B09_Estimation_Calc_Base.py | 2 + B09_Estimation/B09_Estimation_Calc_Base.ts | 4 +- B09_Estimation/B09_Estimation_Calc_Bill.py | 1 + B09_Estimation/B09_Estimation_Calc_Bill.ts | 1 + B09_Estimation/B09_Estimation_Calc_Labor.py | 15 ++++-- B09_Estimation/B09_Estimation_Calc_Labor.ts | 17 +++++-- B09_Estimation/B09_Estimation_Labor_Source.py | 17 +++++-- B09_Estimation/B09_Estimation_Router.py | 12 ++++- B09_Estimation/B09_Estimation_Sangeun.py | 23 +++++++-- B09_Estimation/B09_Estimation_Store.py | 2 +- B09_Estimation/B09_Estimation_Summary.py | 7 ++- B09_Estimation/B09_Estimation_Supply.py | 50 +++++++++++++++++++ B09_Estimation/B09_Estimation_UI_Tabs_Base.ts | 3 +- 13 files changed, 131 insertions(+), 23 deletions(-) create mode 100644 B09_Estimation/B09_Estimation_Supply.py diff --git a/B09_Estimation/B09_Estimation_Calc_Base.py b/B09_Estimation/B09_Estimation_Calc_Base.py index 14748c60d..7f2158576 100644 --- a/B09_Estimation/B09_Estimation_Calc_Base.py +++ b/B09_Estimation/B09_Estimation_Calc_Base.py @@ -183,6 +183,8 @@ def compare_sheet( if applied is None: gaps.append(gap("compare", m["key"], m["name"], "적용 단가 없음(출처 없음)")) supply = next((v for v in (mine.get("supply"), m.get("supply")) if v in SUPPLY), None) + if mine.get("supply") == "사급": # 841-1 사용자가 기본 표시(M02 자재표시)를 끔 + supply = None rows.append( { **_head(m, no), diff --git a/B09_Estimation/B09_Estimation_Calc_Base.ts b/B09_Estimation/B09_Estimation_Calc_Base.ts index 011b21db0..ba951e0f0 100644 --- a/B09_Estimation/B09_Estimation_Calc_Base.ts +++ b/B09_Estimation/B09_Estimation_Calc_Base.ts @@ -158,7 +158,9 @@ function compareSheet(materials: Row[], user: Row, gaps: Gap[]): Row[] { else [applied, appliedFrom] = [low, lowFrom]; if (applied === null) gaps.push(gap("compare", m.key, m.name, "적용 단가 없음(출처 없음)")); const supply = - [mine.supply, m.supply].find((v) => (SUPPLY as readonly string[]).includes(v)) ?? null; + mine.supply === "사급" // 841-1 사용자가 기본 표시(M02 자재표시)를 끔 + ? null + : ([mine.supply, m.supply].find((v) => (SUPPLY as readonly string[]).includes(v)) ?? null); return { ...head(m, i + 1), unit: m.unit || "", diff --git a/B09_Estimation/B09_Estimation_Calc_Bill.py b/B09_Estimation/B09_Estimation_Calc_Bill.py index a8e72890c..ae4cca570 100644 --- a/B09_Estimation/B09_Estimation_Calc_Bill.py +++ b/B09_Estimation/B09_Estimation_Calc_Bill.py @@ -132,6 +132,7 @@ def _item_row(item: dict[str, Any], prices: dict[str, Any] | None) -> dict[str, def _material_items(inputs: dict[str, Any], compare: list[dict[str, Any]]) -> list[dict]: """대비표에서 「별도」 · 「관급」 표시한 수량 있는 자재 → 4 자재대 · 관급자재대 항목.""" qty_of = {m["key"]: m.get("qty") for m in inputs.get("materials") or [] if "qty" in m} + qty_of.update(inputs.get("aside_qty") or {}) # 841-1 대가 · 산근 밖 재료 = 그 역전개 수량 out = [] for row in compare: if row["supply"] is None or row["key"] not in qty_of: diff --git a/B09_Estimation/B09_Estimation_Calc_Bill.ts b/B09_Estimation/B09_Estimation_Calc_Bill.ts index f18dde7d8..cb27950f7 100644 --- a/B09_Estimation/B09_Estimation_Calc_Bill.ts +++ b/B09_Estimation/B09_Estimation_Calc_Bill.ts @@ -95,6 +95,7 @@ function itemRow(item: Row, prices: Row | null): Row { function materialItems(inputs: Row, compare: Row[]): Row[] { const qtyOf: Row = {}; for (const m of inputs.materials || []) if ("qty" in m) qtyOf[m.key] = m.qty; + Object.assign(qtyOf, inputs.aside_qty || {}); // 841-1 대가 · 산근 밖 재료 = 그 역전개 수량 const out: Row[] = []; for (const row of compare) { if (row.supply === null || !(row.key in qtyOf)) continue; diff --git a/B09_Estimation/B09_Estimation_Calc_Labor.py b/B09_Estimation/B09_Estimation_Calc_Labor.py index 5858c2c6d..8514ce5f2 100644 --- a/B09_Estimation/B09_Estimation_Calc_Labor.py +++ b/B09_Estimation/B09_Estimation_Calc_Labor.py @@ -103,9 +103,6 @@ def _prices(line: dict[str, Any], books: dict[str, Any], sums: dict) -> dict[str ): # 841-1 = M02 엔진 단가(`own`) · B09 단가 책 안 읽음 own = line.get("own") out = {p: num(own.get(p)) if p in own else 0.0 for p in PARTS} if own else _none() - row = (books["material"].get(key) or {}) if kind == "material" else {} - if row.get("supply") in SUPPLY: # 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락) - out["material"] = 0.0 elif kind in ("daega", "sangeun"): row = {"daega": sums, "sangeun": books["sangeun"]}[kind].get(key) or {} for part in PARTS: @@ -117,6 +114,11 @@ def _prices(line: dict[str, Any], books: dict[str, Any], sums: dict) -> dict[str return out +def _apart(line: dict[str, Any], books: dict[str, Any]) -> bool: + """대가 밖 재료(841-1) — 대비표 표시(사용자 › M02 자재표시 기본) 관급 · 별도 = 자재대 · 관급 단락.""" + return (books["material"].get(line.get("key")) or {}).get("supply") in SUPPLY + + def _none() -> dict[str, float | None]: return dict.fromkeys(PARTS, None) @@ -137,8 +139,9 @@ def _base(spec: str, lines: list[dict[str, Any]], src: list) -> float | None: `line:이름` 그 줄 · `bundle:labor|material|machine` 그 종류 줄(+ 그 이음 고정 줄) 합 · `rows:0,2@labor` 블록 그 차례 줄(`src` cid · `@비목` = 그 몫) 합(778-4 · 짝 `.ts` `pctBase`).""" kind, _, name = spec.partition(":") - if kind == "part": - return total([ln["amounts"][name] for ln in lines if not ln.get("out")]) + if kind == "part": # 대가 밖 재료(841-1 `aside`)도 밑수에 — M02 엔진 「재료비의 %」 꼴 + aside = "aside" if name == "material" else "" + return total([ln.get(aside) or ln["amounts"][name] for ln in lines if not ln.get("out")]) if kind == "rows": # `3@labor` = 그 줄 그 비목 몫 out: list[float | None] = [] for one in dict.fromkeys(name.split(",")): @@ -195,6 +198,8 @@ def _block( part: None if p is None or qty is None else cut(p * qty, line.get("place")) for part, p in prices.items() } + if line["kind"] == "material" and _apart(line, books): # 관급 · 별도계상 = 대가 안 0원 + extra["aside"], prices["material"], amounts["material"] = amounts["material"], 0.0, 0.0 ref = {"ref": {"key": line["key"]}} if line.get("key") else {} # 집계표(145-5) 이음 ask = {"why": line["ask"]} if qty is None and line.get("ask") else {} # 723-4 lines.append( diff --git a/B09_Estimation/B09_Estimation_Calc_Labor.ts b/B09_Estimation/B09_Estimation_Calc_Labor.ts index 42202cd0a..14f7b8615 100644 --- a/B09_Estimation/B09_Estimation_Calc_Labor.ts +++ b/B09_Estimation/B09_Estimation_Calc_Labor.ts @@ -109,9 +109,6 @@ function linePrices(line: Row, books: Row, sums: Record): Parts { const own: Row | undefined = line.own; out = blanks(); if (own) for (const part of PARTS) out[part] = part in own ? num(own[part]) : 0; - const row: Row = kind === "material" ? books.material[key] || {} : {}; - // 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락) - if ((SUPPLY as readonly string[]).includes(row.supply)) out.material = 0; } else if (kind === "daega" || kind === "sangeun") { const src: Row = ({ daega: sums, sangeun: books.sangeun } as Row)[kind]; const row: Row = src[key] || {}; @@ -140,7 +137,12 @@ function pctBase(spec: string, lines: Row[], src: Row[]): Num { const kind = spec.slice(0, at); const name = spec.slice(at + 1); const whole = (ln: Row): Num => total(PARTS.map((p) => ln.amounts[p])); - if (kind === "part") return total(lines.filter((ln) => !ln.out).map((ln) => ln.amounts[name])); + // 대가 밖 재료(841-1 `aside`)도 밑수에 — M02 엔진 「재료비의 %」 꼴 + const aside = name === "material" ? "aside" : ""; + if (kind === "part") + return total( + lines.filter((ln) => !ln.out).map((ln) => (aside && ln[aside]) || ln.amounts[name]), + ); if (kind === "rows") { // `3@labor` = 그 줄 그 비목 몫 const out: Num[] = []; @@ -185,6 +187,13 @@ function block(b: Row, books: Row, sums: Record, gaps: Gap[], own: const p = prices[part]; amounts[part] = p === null || qty === null ? null : cut(p * qty, line.place); } + // 관급 · 별도계상 = 대가 안 0원(841-1 대비표 표시 = 사용자 › M02 자재표시) + const supply = (books.material[line.key] || {}).supply; + if (line.kind === "material" && (SUPPLY as readonly string[]).includes(supply)) { + extra.aside = amounts.material; + prices.material = 0; + amounts.material = 0; + } const ref = line.key ? { ref: { key: line.key } } : {}; // 집계표(145-5) 이음 const ask: Row = qty === null && line.ask ? { why: line.ask } : {}; // 723-4 lines.push({ ...line, ...ref, ...ask, qty, prices, amounts, ...extra }); diff --git a/B09_Estimation/B09_Estimation_Labor_Source.py b/B09_Estimation/B09_Estimation_Labor_Source.py index b2bd13df0..6e0b7c70f 100644 --- a/B09_Estimation/B09_Estimation_Labor_Source.py +++ b/B09_Estimation/B09_Estimation_Labor_Source.py @@ -38,6 +38,7 @@ from B09_Estimation import B09_Estimation_Labor_Rule as rule 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 B09_Estimation.B09_Estimation_Supply import supply_mark 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 @@ -150,6 +151,10 @@ class Builder: for line in lines: if line["kind"] == "machine": line["spec"] = (eq.get(line["key"]) or {}).get("규격") or "" + marks = one.get("자재표시") or {} # 841-1 M02 자재표시(호표 차례) → 대비표 기본 표시 + for n, line in enumerate(lines): + if line["kind"] == "material": + line["mark"] = supply_mark(marks.get(str(n))) law = rule.mark(logic, lines, engine) # 새 의미 로직 = 줄 자리 · 합밖 · 번지 · 끝전(792-4) unit = str(logic.get("결과단위") or "").replace("원/", "") spec = " · ".join(f"{k} {v}" for k, v in sorted(given.items())) @@ -198,7 +203,8 @@ class Builder: return None except (sd.DocError, mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError): return None - one = {"로직": key, "입력값": got["입력값"], "켜쌓기율": got["켜쌓기율"]} + one = {"로직": key, "입력값": got["입력값"], "켜쌓기율": got["켜쌓기율"], + "자재표시": got.get("자재표시")} # fmt: skip where = {c: n for n, c in enumerate(got["정체"])} # 율 기준 줄 목록(778-4) → 블록 차례 block = self._poomsem(calc._Over(whole, key, row), {**one, "고름": got["고름"]}) if block is None: @@ -436,7 +442,11 @@ class Builder: if line.get("qty") is None and line["kind"] != "blank": line = {**line, "why": "수량 없음 — 면적산출 알약 칸 · 손 수량을 넣음"} lines.append(line) - lines += [_material(whole, mat, mat.get("수량")) for mat in result.get("자재") or []] + marks = {m.get("코드"): m for m in doc.get("자재") or [] if isinstance(m, dict)} + lines += [ + _material(whole, {**(marks.get(mat.get("코드")) or {}), **mat}, mat.get("수량")) + for mat in result.get("자재") or [] + ] manifest = read_json(folder / MANIFEST_NAME) or {} origin = manifest.get("원본") or {} return self.put( @@ -576,7 +586,8 @@ def _material(whole: Any, mat: dict, qty: Any) -> dict[str, Any]: if price is None and mat.get("코드"): price = labor_doc.material_price(whole, mat["코드"], mat.get("단위"))["기준단가"] own = None if _f(price) is None else {"material": _f(price)} - return {"kind": "material", "key": mat.get("코드"), **names, "qty": _f(qty), "own": own} + return {"kind": "material", "key": mat.get("코드"), **names, "qty": _f(qty), "own": own, + "mark": supply_mark(mat)} # fmt: skip def _engine_bases(engine: list[dict], items: list[dict]) -> dict[str, float]: diff --git a/B09_Estimation/B09_Estimation_Router.py b/B09_Estimation/B09_Estimation_Router.py index 1260ba3a3..4acc03ef6 100644 --- a/B09_Estimation/B09_Estimation_Router.py +++ b/B09_Estimation/B09_Estimation_Router.py @@ -93,7 +93,13 @@ from B09_Estimation.B09_Estimation_Rates import rate_candidates, rate_table from B09_Estimation.B09_Estimation_Router_Import import router as import_router from B09_Estimation.B09_Estimation_Router_Link import router as link_router from B09_Estimation.B09_Estimation_Router_Quotes import router as quotes_router -from B09_Estimation.B09_Estimation_Summary import compute_summary, hopyo_lines, used_materials +from B09_Estimation.B09_Estimation_Summary import ( + compute_summary, + hopyo_lines, + totals, + used_materials, +) +from B09_Estimation.B09_Estimation_Supply import supply_defaults from common_util.common_util_auth import verify_session from common_util.common_util_project_settings import reset_section from common_util.common_util_storage import resolve_stored_project_path @@ -159,8 +165,9 @@ def compute_sheets( 화면 짝 = `B09_Estimation_UI_Shell.computeSheets`(산근만 서버 답 그대로) · memo = 두 번 셈의 앞 산근 풀이(`Sangeun.collect`).""" + supply = supply_defaults(inputs, user) base = compute_base(inputs, user) - sg = sangeun.build(root, user, memo) # 145-1 — M02 엔진 값으로 풀이(841-1 · + 166-2 손 줄 로직) + sg = sangeun.build(root, user, memo, supply) # 145-1 M02 엔진 값으로 풀이(841-1 · 166-2 손 줄) sg = sg_cover.cover_sangeun(sg, user.get("sg_cover")) # 719-3 이 공사 산근 고친 칸 by_key = {row["key"]: row for row in sg["sg_list"]} labor = compute_labor( @@ -189,6 +196,7 @@ def compute_twice(root: Path, inputs: dict[str, Any], user: dict[str, Any]) -> t keep = used_materials(first, inputs, user) inputs["materials"] = [m for m in inputs.get("materials") or [] if m["key"] in keep] inputs["material_qty"] = {row["key"]: row["qty"] for row in summary["sum_material"]} + inputs["aside_qty"] = totals(first, hopyo_lines(first))[0].get("aside") or {} # 841-1 자재대 sheets = compute_sheets(root, inputs, user, memo) return sheets, compute_summary(sheets, hopyo_lines(sheets)) diff --git a/B09_Estimation/B09_Estimation_Sangeun.py b/B09_Estimation/B09_Estimation_Sangeun.py index c18536c4f..6296f730a 100644 --- a/B09_Estimation/B09_Estimation_Sangeun.py +++ b/B09_Estimation/B09_Estimation_Sangeun.py @@ -42,7 +42,7 @@ from typing import Any from B08_Quantity.B08_Quantity_Haul_Cycle import fill_given from B09_Estimation import B09_Estimation_Sangeun_Sum as ss -from B09_Estimation.B09_Estimation_Calc_Base import gap, num, rounddown, total +from B09_Estimation.B09_Estimation_Calc_Base import SUPPLY, gap, num, rounddown, total from B09_Estimation.B09_Estimation_Store import ref_rows from M01_MasterData import M01_MasterData_Store as m01 from M01_MasterData import M01_MasterData_Store_Combo as combo_store @@ -311,7 +311,8 @@ def block_totals(lines: list[dict[str, Any]]) -> dict[str, float | None]: class Solver: """한 번 셈 — 산근 블록을 첫 등장 차례로 모음(하위 산근 포함).""" - def __init__(self) -> None: + def __init__(self, supply: Any = None) -> None: + self.supply = supply or (lambda key: None) # 자재 키 → 관급 · 별도 · None(841-1) self.prices = _Prices() self.eng: list[dict[str, Any]] = [] # 지금 푸는 로직의 엔진 줄(호표 차례 · 841-1) self.blocks: dict[str, dict[str, Any]] = {} @@ -349,6 +350,7 @@ class Solver: lines.append(self._line(whole, item, n, env, times, rate, depth)) ss.places(row, lines) # 줄 자리(792-3) self._extras(whole, row, env, lines) + self._aside(lines) _annotate(whole, row, env, lines) except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) as e: return self._stop(code, name, str(e) or type(e).__name__) @@ -409,6 +411,16 @@ class Solver: at += n return text[:start], text[-1] + def _aside(self, lines: list[dict[str, Any]]) -> None: + """관급 · 별도계상 재료 = 산근 안 0원(841-1 · % 덧줄 밑수는 엔진 값 그대로 · 자재대 · 관급 단락).""" + for ln in lines: + key = (ln.get("ref") or {}).get("key") + if ln["kind"] != "material" or self.supply(key) not in SUPPLY: + continue + part = next(iter(ln["price"]), "material") + ln["aside"], ln[part], ln["price"] = ln[part], 0.0, {part: 0.0} + self.prices.material[key] = 0.0 + def _each(self, n: int, part: str, q: float) -> float | None: """엔진 줄 n 비목 단가 = 엔진 금액 ÷ 엔진 수량 · 안 쓰는 줄(수량 0) = 0 · 못 셈 = None.""" x = self.eng[n] if n < len(self.eng) else {} @@ -608,13 +620,15 @@ def collect( entries: list[tuple[str, Path]], hands: list[tuple[str, str, dict[str, Any]]] | None = None, memo: dict[str, Any] | None = None, + supply: Any = None, ) -> dict[str, Any]: """`entries` = [(구조물 id, 사본 폴더)] → 블록 · 목록 · 「연결 없음」 · + supply = 자재 키 → 관급 · 별도(841-1 · 대비표 표시) · `hands` = 손 줄 로직(166-2 · 사본 뒤 번호 · 마스터 로직) · memo = 같은 사본 · 손 줄의 앞 풀이(두 번 셈) — 앞 풀이가 본 단가가 같으면 그 풀이 사본.""" if memo and "sg" in memo: # 841-1 풀이 = 사본 · 엔진만(바탕 표 안 읽음) → 앞 풀이 그대로 return copy.deepcopy(memo["sg"]) - solver = Solver() + solver = Solver(supply) for owner, folder in entries: whole = item_prices.load_prices(read_json(folder / PRICE_FILE) or {}) for src in rows_of(folder, whole): @@ -647,6 +661,7 @@ def build( project_root: str | Path, user: dict[str, Any] | None = None, memo: dict[str, Any] | None = None, + supply: Any = None, ) -> dict[str, Any]: """연결된 사본 전부(구조물 id 차례 → 설계내역서 서고 사본 · 702-3)의 산근 + 설계내역서 손 줄 로직(166-2 · 뒤 번호) — 차례 = `Labor_Source.daega_inputs` · @@ -662,4 +677,4 @@ def build( entries += [ (f"서고 {(m.get('원본') or {}).get('name') or f.name}", f) for f, m in copies(project_root) ] - return collect(entries, hand_sources(user), memo) + return collect(entries, hand_sources(user), memo, supply) diff --git a/B09_Estimation/B09_Estimation_Store.py b/B09_Estimation/B09_Estimation_Store.py index bd1402293..37d76abdc 100644 --- a/B09_Estimation/B09_Estimation_Store.py +++ b/B09_Estimation/B09_Estimation_Store.py @@ -328,7 +328,7 @@ def clean_user(raw: Any) -> dict[str, Any]: out: dict[str, Any] = {} if "applied" in cell: out["applied"] = _num(cell["applied"]) - if cell.get("supply") in SUPPLY: + if cell.get("supply") in (*SUPPLY, "사급"): # 사급 = 기본 표시 끔(841-1) out["supply"] = cell["supply"] if "quote" in cell: out["quote"] = _num(cell["quote"]) diff --git a/B09_Estimation/B09_Estimation_Summary.py b/B09_Estimation/B09_Estimation_Summary.py index 965c4ea12..a03cc0307 100644 --- a/B09_Estimation/B09_Estimation_Summary.py +++ b/B09_Estimation/B09_Estimation_Summary.py @@ -41,7 +41,7 @@ def expand( kind = line.get("kind") if kind == "hopyo": expand(lines_of, str(line.get("key")), qty * q, out, depth + 1) - elif kind in (*KINDS, "fixed"): + elif kind in (*KINDS, "fixed", "aside"): bucket = out.setdefault(kind, {}) bucket[str(line.get("key"))] = bucket.get(str(line.get("key")), 0.0) + qty * q @@ -154,6 +154,8 @@ def block_lines(blocks: list[dict[str, Any]] | None) -> dict[str, Any]: lines += _fixed_lines(block, line) continue kind = LINE_KIND.get(line.get("kind")) + if kind == "material" and "aside" in line: # 841-1 대가 · 산근 밖 재료 = 자재대 몫 + kind = "aside" key = (line.get("ref") or {}).get("key") if kind and key and num(line.get("qty")) is not None: lines.append({"kind": kind, "key": str(key), "qty": line["qty"]}) @@ -191,7 +193,8 @@ def used_materials(sheets: dict[str, Any], inputs: dict[str, Any], user: dict[st } marks = user.get("materials") or {} for m in inputs.get("materials") or []: - if "qty" in m or m.get("supply") or (marks.get(m["key"]) or {}).get("supply"): + own = m.get("supply") and m.get("origin") != "M01" # 841-1 M02 기본 표시는 남길 까닭 아님 + if "qty" in m or own or (marks.get(m["key"]) or {}).get("supply"): keep.add(m["key"]) return keep diff --git a/B09_Estimation/B09_Estimation_Supply.py b/B09_Estimation/B09_Estimation_Supply.py new file mode 100644 index 000000000..c2e8461be --- /dev/null +++ b/B09_Estimation/B09_Estimation_Supply.py @@ -0,0 +1,50 @@ +"""B09 자재 관급 · 별도 표시(PLAN 841-1) — M02 자재표시를 대비표 기본 표시로 · 서버 단독 · 순수 함수. + +M02 일위대가 · 단가산출 셈은 재료 줄을 자재표시(자재구분 관급 · 별도계상 — 빈 칸 = 별도계상)로 +합에서 뺌(`M02_Labor_Doc.apart`) → B09 도 같은 표시로 대가 · 산근 안 0 원 · 4 자재대 · 관급자재대로 +(`B09_Estimation_Calc_Labor._apart` · `Sangeun.Solver._aside` · `Calc_Bill` 자재대 수량 = `aside_qty`). +대비표 표시 = 사용자 › 이 기본 · 사용자 「사급」 = 기본을 끔. +""" + +from __future__ import annotations + +from typing import Any + +from M02_MasterTemplete import M02_Labor_Doc as labor_doc + + +def supply_mark(one: Any) -> str | None: + """M02 자재표시(841-1) → 대비표 기본 표시 — 관급 · 별도(별도계상 · M02 기본) · None(직접계산).""" + if labor_doc.mark_of(one, "자재구분") == "관급": + return "관급" + return "별도" if labor_doc.apart(one) else None + + +def material_marks(blocks: list[dict[str, Any]]) -> dict[str, str | None]: + """대가 블록 재료 줄 M02 표시를 자재 키마다 하나로(841-1) — 관급 하나라도 = 관급 · 별도 하나라도 = + 별도 · 모두 직접계산 = None. ponytail: 키 하나 = 표시 하나(줄마다 다르면 별도 쪽 · 자재대에 한 번).""" + out: dict[str, str | None] = {} + for block in blocks: + for line in block.get("lines") or []: + if line.get("kind") != "material" or not line.get("key") or "mark" not in line: + continue + key, was = str(line["key"]), out.get(str(line["key"])) + out[key] = "관급" if "관급" in (was, line["mark"]) else was or line["mark"] + return out + + +def supply_defaults(inputs: dict[str, Any], user: dict[str, Any]) -> Any: + """841-1 대비표 기본 표시 = M02 자재표시(대가 재료 줄 · 없으면 M02 기본 별도계상) — 로직 자재 + (B08 줄 밖 · 유가 밖) 입력 줄에 `supply` 를 얹음 → 자재 키 → 관급 · 별도(사용자 표시 › 기본).""" + marks = material_marks((inputs.get("daega") or {}).get("blocks") or []) + for row in inputs.get("materials") or []: + if row.get("origin") == "M01" and "supply" not in row and marks.get(row["key"], "별도"): + row["supply"] = marks.get(row["key"], "별도") + mine = user.get("materials") or {} + default = {row["key"]: row.get("supply") for row in inputs.get("materials") or []} + + def supply(key: Any) -> str | None: + got = (mine.get(key) or {}).get("supply") + return None if got == "사급" else got or default.get(key, marks.get(key, "별도")) + + return supply diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Base.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Base.ts index 458fbe168..8178fc13a 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Base.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Base.ts @@ -115,7 +115,8 @@ export const buildBasis: TabBuild = ({ sheets }): TabTable => { }; }; -const SUPPLY_NEXT: Record = { "": "관급", 관급: "별도", 별도: null }; +// 841-1 기본 표시(M02 자재표시)가 있어도 끌 수 있게 — 별도 다음 = 사급(표시 끔) +const SUPPLY_NEXT: Record = { "": "관급", 관급: "별도", 별도: "사급" }; /** 적용 단가 고르기 항목(183-3) — 값 있는 출처(견적 포함 · 0 뺌) 「출처 단가 · 최저」 + 「최저(자동)」 · * `pick(s)` = 그 출처 · null = 자동 */