"""B09 원가 단가 한 벌 · 수요 모음(PLAN 144) — 서버 단독(M01 을 읽는 곳은 여기 하나). 원가 단가 = 프로젝트 한 벌 — 원가산출에서 M01 단가를 한 번 복사해 굳힘(`price_book.json`) · [단가 갱신] 때만 새로 · 대가 · 산근도 이 단가로 셈(한 직종 · 한 자재 = 한 단가). 줄 = 수요 따라(사용자 승인 ① · 10-9) — 엑셀 목록을 기본으로 깔지 않음: 연결된 사본이 부른 자원 키(`B09_Estimation_Linked` — 143-1 「연결된 사본」 원천 · 없으면 빈 표) + 기계가 부르는 운전사(조종원직종 → 인력 원문번호) · 연료(주연료종류 → 유가 「종류:전국평균」) + B08 총괄자재수량집계표 자재(M01 짝 없음 → 출처 빈칸 · 적용 단가는 사용자 입력) 운전사 시간 계수 = M01 CC000336(시간환산 · 상여계수 · 휴지계수 분자 · 분모 곱). 연료 지역 = 전국평균(M01 기계경비 로직 지역 첫 값) — 다른 지역 값은 자재 적용 단가를 고쳐 씀. """ from __future__ import annotations from datetime import datetime from decimal import Decimal from typing import Any from M01_MasterData import M01_MasterData_Store as m01 #: 운전사 시간 노임 계수 표(건설품셈 공통 8-1-3 · 건협기술-2230) DRIVER_TABLE = "CC000336" DRIVER_PARTS = ("시간환산", "상여계수", "휴지계수") #: 연료 지역 — M01 기계경비 로직 지역 고르기 첫 값 FUEL_REGION = "전국평균" #: 자재품목 값 열 → 대비표 출처 칸 MT_SOURCES = ("관급", "물가자료", "유통물가", "물가정보", "거래가격") def _f(value: Any) -> float | None: if isinstance(value, Decimal): return float(value) if isinstance(value, bool) or not isinstance(value, (int, float)): return None return float(value) def _master(): return m01.loaded()[1] def _row(master, key: str) -> dict[str, Any] | None: return master.index.get(key[:2], {}).get(key) def labor_input(row: dict[str, Any]) -> dict[str, Any]: return { "key": row["키"], "code": str(row.get("원문번호") or ""), "name": row.get("이름") or "", "spec": row.get("규격") or "", "unit": "인", "price": _f(row.get("값")), "source": row.get("출처") or "", } def machine_input(row: dict[str, Any]) -> dict[str, Any]: v = row.get("값") or {} return { "key": row["키"], "code": str(row.get("원문번호") or ""), "name": row.get("이름") or "", "spec": row.get("규격") or "", "price": _f(v.get("가격")), "loss": _f(v.get("손료계수")), "fuel": v.get("주연료종류") or None, "fuel_lph": _f(v.get("주연료")), "misc_pct": _f(v.get("잡재료율")), "crew": _f(v.get("조종원")), "crew_code": str(v["조종원직종"]) if v.get("조종원직종") else None, "source": (row.get("출처") or {}).get("가격") or row.get("원문") or "", } def material_input(row: dict[str, Any]) -> dict[str, Any]: """자재품목(MT) · 유가(MO) 줄 → 대비표 입력 줄.""" if row["키"].startswith("MO"): prices = {"유가": _f(row.get("값"))} pages = {"유가": row.get("규격") or None} origin = "유가" spec = f"{row.get('상세구분') or ''} {row.get('규격') or ''}".strip() unit = (row.get("단위") or "").replace("원/", "") else: prices = {s: _f(row.get(s)) for s in MT_SOURCES} pages = {s: (row.get("면수") or {}).get(s) for s in MT_SOURCES if s != "관급"} origin = "M01" spec = row.get("규격") or "" unit = row.get("단위") or "" return { "key": row["키"], "name": row.get("이름") or "", "spec": spec, "unit": unit, "origin": origin, "prices": prices, "pages": pages, "source": row.get("출처") or "", } def driver_factor(master) -> dict[str, Any] | None: table = _row(master, DRIVER_TABLE) if not table: return None parts = {r.get("구분"): r for r in table.get("줄") or []} if any(p not in parts for p in DRIVER_PARTS): return None top = bottom = 1 text = [] for p in DRIVER_PARTS: top *= int(parts[p]["분자"]) bottom *= int(parts[p]["분모"]) text.append(f"{parts[p]['분자']}/{parts[p]['분모']}") return {"num": top, "den": bottom, "text": " × ".join(text)} def fx_inputs(master) -> list[dict[str, Any]]: return [ {"key": key, "name": row.get("이름") or "", "unit": row.get("단위") or "", "value": _f(row.get("값")), "date": row.get("규격") or ""} for key, row in sorted(master.index.get("FX", {}).items()) ] # fmt: skip def _fuel_row(master, kind: str) -> dict[str, Any] | None: return master.numbers.get("MO", {}).get(f"{kind}:{FUEL_REGION}") def demand(master, keys: list[str]) -> dict[str, Any]: """수요 키 → 줄 차례(노무 · 기계 · 자재) + 연료 짝 · 찾지 못한 키.""" labor: list[str] = [] machines: list[str] = [] materials: list[str] = [] missing: list[str] = [] fuel_keys: dict[str, str] = {} def add(bucket: list[str], key: str) -> None: if key not in bucket: bucket.append(key) for key in keys: if _row(master, key) is None: missing.append(key) elif key.startswith("LB"): add(labor, key) elif key.startswith("EQ"): add(machines, key) elif key.startswith(("MT", "MO")): add(materials, key) for key in machines: v = _row(master, key).get("값") or {} code = v.get("조종원직종") driver = master.numbers.get("LB", {}).get(str(code)) if code and v.get("조종원") else None if driver: add(labor, driver["키"]) kind = v.get("주연료종류") fuel = _fuel_row(master, kind) if kind and v.get("주연료") else None if fuel: add(materials, fuel["키"]) fuel_keys[kind] = fuel["키"] return { "labor": labor, "machines": machines, "materials": materials, "fuel_keys": fuel_keys, "missing": missing, } def _convert(master, key: str) -> dict[str, Any]: row = _row(master, key) if key.startswith("LB"): return labor_input(row) if key.startswith("EQ"): return machine_input(row) return material_input(row) def refresh_book(keys: list[str]) -> dict[str, Any]: """[단가 갱신] — 수요 키 전부를 지금 M01 값으로 새로 굳힘.""" master = _master() want = demand(master, keys) rows = { key: _convert(master, key) for key in [*want["labor"], *want["machines"], *want["materials"]] } return { "made": datetime.now().isoformat(timespec="seconds"), "rows": rows, "driver_factor": driver_factor(master), "fx": fx_inputs(master), } def extend_book(book: dict[str, Any] | None, keys: list[str]) -> dict[str, Any]: """[저장] — 책에 없는 새 줄만 더함(있는 줄 · 계수 · 환율은 안 바꿈) · 책이 없으면 새로 굳힘.""" if book is None: return refresh_book(keys) master = _master() want = demand(master, keys) rows = dict(book["rows"]) for key in [*want["labor"], *want["machines"], *want["materials"]]: if key not in rows: rows[key] = _convert(master, key) return {**book, "rows": rows} def b08_material_rows(b08_sheets: dict[str, Any] | None) -> list[dict[str, Any]]: """B08 총괄자재수량집계표 줄(수량 있는 줄만) → 자재 입력 줄(M01 짝 없음 · 출처 빈칸).""" rows = ((b08_sheets or {}).get("material") or {}).get("rows") or [] out = [] for row in rows: if row.get("amount") is None: continue out.append( { "key": f"b08:{row['key']}", "name": row.get("group") or "", "spec": row.get("spec") or "", "unit": row.get("unit") or "", "origin": "B08", "prices": {}, "pages": {}, "source": "B08 총괄자재수량집계표", "qty": row.get("amount"), } ) return out def build_inputs( book: dict[str, Any] | None, keys: list[str], b08_sheets: dict[str, Any] | None = None ) -> dict[str, Any]: """표 셈 입력 — 줄 = 수요 · 값 = 굳힌 책(없는 줄은 지금 M01 값 · `fresh` 표시).""" master = _master() want = demand(master, keys) saved = (book or {}).get("rows") or {} def pick(key: str) -> dict[str, Any]: if key in saved: return saved[key] return {**_convert(master, key), "fresh": True} factor = (book or {}).get("driver_factor") if book else driver_factor(master) return { "labor": [pick(k) for k in want["labor"]], "machines": [pick(k) for k in want["machines"]], "materials": [pick(k) for k in want["materials"]] + b08_material_rows(b08_sheets), "fuel_keys": want["fuel_keys"], "driver_factor": factor, "fx": (book or {}).get("fx") if book else fx_inputs(master), "missing": want["missing"], }