"""B09 일위대가표 · 일위대가목록표 셈(PLAN 145-2) — 화면 없음 · 저장 안 함 · 파일 · M01 안 읽음. ⚠ 짝 = `B09_Estimation_Calc_Labor.ts`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 · 거울 시험 `resources/tester/test_145_2_b09_labor_calc.py`. 조작 중은 화면 짝이 바로 셈 · [저장] 때 서버가 이 파일로 정본을 다시 셈. 입력 = 대가 블록(`B09_Estimation_Labor_Source.daega_inputs` — 줄 꼴 · 수량만) + 바탕 표(144 굳힌 단가): 노무 줄 = 노무비목록표 단가 · 재료 줄 = 재료비목록표 단가 · 중기 줄 = 중기목록표(노무 · 재료 · 경비) 대가 줄 = 부른 블록 합계(노무 · 재료 · 경비) · 산근 줄 = 145-1 산근 단가(`sangeun` {키: 값}) % 줄 = 밑수(그 블록 노무 줄 금액 합) × % / 100 끝수 = 엑셀 자리 그대로 — 줄 금액 = ROUNDDOWN(단가 × 수량, 1) · % 줄 = ROUNDDOWN(밑수 × % / 100, 1) · 관급 · 별도계상 자재 = 대가 안 0원 · 블록 비목 합 = ROUNDDOWN(Σ, 0) · 합계 = 세 비목 합. 빈칸 = 단가 · 수량을 낸 원천이 없으면 None · 그 칸을 쓰는 합도 None · 「연결 없음」 목록(gaps). """ from __future__ import annotations from typing import Any from B09_Estimation.B09_Estimation_Calc_Base import SUPPLY, gap, num, rounddown, total PARTS = ("labor", "material", "expense") TAB = "dg" WHY = { "labor": "노임 단가 없음", "material": "자재 적용 단가 없음", "machine": "중기 시간 단가 없음", "daega": "부른 대가 합계 없음", "sangeun": "산근 단가 없음(145-1)", } def _prices(line: dict[str, Any], books: dict[str, Any], sums: dict) -> dict[str, float | None]: """줄 비목 단가 — 그 비목이 없는 줄은 0 · 원천이 없으면 None.""" kind, key = line["kind"], line.get("key") out: dict[str, float | None] = dict.fromkeys(PARTS, 0.0) if kind == "labor": out["labor"] = num((books["labor"].get(key) or {}).get("price")) elif kind == "material": row = books["material"].get(key) or {} # 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락에서 셈 — 두 번 안 셈) out["material"] = 0.0 if row.get("supply") in SUPPLY else num(row.get("price")) elif kind in ("machine", "daega", "sangeun"): row = {"machine": books["machine"], "daega": sums, "sangeun": books["sangeun"]}[kind].get( key ) or {} for part in PARTS: out[part] = num(row.get(part)) else: # blank — 못 펼친 줄 out = dict.fromkeys(PARTS, None) return out def _base(spec: str, lines: list[dict[str, Any]]) -> float | None: """% 줄 밑수 — `part:비목` 그 비목 줄 금액 합 · `lines:labor` 노무 줄 금액 합 · `line:이름` 그 줄.""" kind, _, name = spec.partition(":") if kind == "part": return total([ln["amounts"][name] for ln in lines]) if kind == "line": return next( (total(list(ln["amounts"].values())) for ln in lines if ln.get("name") == name), None ) return total([ln["amounts"]["labor"] for ln in lines if ln["kind"] == "labor"]) def _block( block: dict[str, Any], books: dict[str, Any], sums: dict[str, dict], gaps: list, ) -> dict[str, Any]: lines = [] for line in block["lines"]: if line["kind"] == "pct": continue qty = num(line.get("qty")) prices = _prices(line, books, sums) amounts = { part: None if p is None or qty is None else rounddown(p * qty, 1) for part, p in prices.items() } ref = {"ref": {"key": line["key"]}} if line.get("key") else {} # 집계표(145-5) 이음 lines.append({**line, **ref, "qty": qty, "prices": prices, "amounts": amounts}) if qty is None or None in prices.values(): why = line.get("why") or ( "수량 없음" if qty is None else WHY.get(line["kind"], "단가 없음") ) gaps.append(gap(TAB, block["id"], f"{block['name']} · {line.get('name') or ''}", why)) for line in block["lines"]: if line["kind"] != "pct": continue base = _base(line.get("base") or "lines:labor", lines) pct = num(line.get("qty")) prices = dict.fromkeys(PARTS, 0.0) amounts = dict.fromkeys(PARTS, 0.0) part = line.get("part") or "expense" prices[part] = base amounts[part] = None if base is None or pct is None else rounddown(base * pct / 100, 1) lines.append({**line, "qty": pct, "prices": prices, "amounts": amounts}) if amounts[part] is None: gaps.append( gap(TAB, block["id"], f"{block['name']} · {line.get('name')}", "% 밑수 없음") ) for line in lines: line["price"] = total(list(line["prices"].values())) line["amount"] = total(list(line["amounts"].values())) parts: dict[str, float | None] = {} for part in PARTS: s = total([ln["amounts"][part] for ln in lines]) 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")}, "key": block["id"], "lines": lines, **parts, "sum": total([parts[p] for p in PARTS]), } def compute_labor( daega: dict[str, Any] | None, sheets: dict[str, Any], sangeun: dict[str, Any] | None = None, ) -> dict[str, Any]: """대가 탭 둘 — `daega_blocks`(일위대가표) · `daega`(목록표) · `daega_owners` + 바탕 gaps 이어 붙임. 블록은 부른 블록을 먼저 셈(겹 대가) · 순환은 빈칸 · 번호 = 입력 차례(대가1 …).""" daega = daega or {} blocks = daega.get("blocks") or [] books = { "labor": {r["key"]: r for r in sheets.get("labor") or []}, "material": {r["key"]: r for r in sheets.get("material") or []}, "machine": {r["key"]: r for r in sheets.get("machine") or []}, "sangeun": sangeun or {}, } by_id = {b["id"]: b for b in blocks} no = {b["id"]: n for n, b in enumerate(blocks, 1)} done: dict[str, dict[str, Any]] = {} gaps: list[dict[str, str]] = [] def run(bid: str, trail: tuple[str, ...]) -> None: if bid in done or bid in trail or bid not in by_id: return for line in by_id[bid]["lines"]: if line["kind"] == "daega": run(line.get("key"), (*trail, bid)) done[bid] = _block(by_id[bid], books, done, gaps) for b in blocks: run(b["id"], ()) out_blocks = [] for b in blocks: got = {**done[b["id"]], "no": no[b["id"]]} for line in got["lines"]: if line["kind"] == "daega" and line.get("key") in no: line["ref_no"] = f"대가{no[line['key']]}" out_blocks.append(got) listing = [ { "no": b["no"], "key": b["id"], "kind": b["kind"], "name": b["name"], "spec": b["spec"], "unit": b["unit"], "sum": b["sum"], "labor": b["labor"], "material": b["material"], "expense": b["expense"], } for b in out_blocks ] return { "daega_blocks": out_blocks, "daega": listing, "daega_owners": dict(daega.get("owners") or {}), "gaps": [*(sheets.get("gaps") or []), *gaps], } def daega_price(sheets: dict[str, Any], owner: str) -> dict[str, Any] | None: """내역서(145-3)가 읽는 대가 단가 — 구조물 id(연결된 사본 주인) → 호표 · 비목 단가. 사본이 없으면 None.""" bid = (sheets.get("daega_owners") or {}).get(owner) row = next((r for r in sheets.get("daega") or [] if r["key"] == bid), None) if row is None: return None return { "no": f"대가{row['no']}", "key": bid, **{k: row[k] for k in ("labor", "material", "expense", "sum")}, }