"""M02 불러오기 엑셀 보기 하이라이트 재료(PLAN 779-2 · 요구서 621 3장 5) — 읽기만(파일 · storage 변화 0). 블록 `[{번, 판정, 고를수, SK, 갈래, 못옮김, 부름}]` = 단가산출 만들기 결과(`Sangeun.made`) + 블록 판정을 읽기만. - `판정` = 블록 판정(785-6 · `M02_Hopyo_Judge.judged` 파생 저장본 = 기초로 풂 · 기초+덧 · 줄 조합 · 고를 것 · `고를수`) · 안 불리거나 셈 전이면 None - `부름` = 부르는 호표 번 [] = 일위대가 밖(780-1) · `어긋남` · `줄` = 모든 블록(밖 포함) - `SK` = 만든 SK 키 · 없으면 None / `갈래` · `못옮김` = 그 SK 본뜬(출처별) · 없으면 None · [] 줄 상태는 목록(`M02_Hopyo_Judge.view`) 호표마다 `줄상태 [[차례, 1차 판정, 제안 있음]]` · `만든`(SC 키)로 실려 감. 칠 꼴(화면 779-4 가 정함 · 상태 이름 맞춤표 — 「이어짐」 = 확인 창과 한 규칙 `체크`): | 칠 | 줄 상태 (`줄상태`) | 블록 (`판정` · `SK` · `못옮김`) | | 초록 이어짐 | 맞음 · 별칭 · 호표(`체크`) · 체크한 다른 점 | SK 있음 + 못옮김 [] (갈래 「기초」 포함) | | 주황 엑셀과 다름 | 다른 점 + `체크` | SK 있음 + 갈래 「SK」(차이) | | 파랑 고를 것 | 후보 · `체크` 아닌 다른 점 · 호표(`체크` 아님) | SK 없음 + 판정 후보 · 고름 대기 | | 빨강 못 이음 | 없음 · 노임 · (안 이은 줄 = 마법사 몫) | SK 있음 + 못옮김 남음 | | 흐림 밖 | % · 뺌 | 판정 None (안 불리는 블록) | | 띠 내가 고름 | 화면 캐시(서버 아님) | — | 최종 판정은 화면(`SheetMap`)이 이 재료 + 캐시 고름으로 내림 — 여기는 상태 이름을 그대로 실음. `어긋남`(778-5 D-6 · 요구 623 · 624) = 불리는 블록의 엑셀 셈 어긋남(엑셀 = 과거 값 · 사용자가 보고 판단 · 칠 · 판정에 안 씀) — `{줄합, 금액, 차, 덮음, 줄 [{행, 이름, 묶음, 셈, 금액, 차, 까닭}], 까닭}` · 없으면 None. 줄 = 재현 못 한 단위 줄(`UnitLine.redo` — 엑셀 단가 × 매개값 ≠ 엑셀 줄 금액 · 셈 밖 뺌) · 덮음 실패 = 줄 합 ≠ 블록 금액. `줄`(779-5) = 불리는 블록 단위 줄 `[[출처 행, 상태, 차]]` — 상태 = 줄 잇기(`M02_Hopyo_Units.line` · D-2 이어짐 · 고를 것 · 못 이음) · 셈 밖 = 「밖」 · 차 = 이어졌는데 엑셀 금액과 참고 차(노무 = 엑셀 노임 ≠ M01 지금 노임). """ from __future__ import annotations import json from pathlib import Path from typing import Any from M02_MasterTemplete import M02_Hopyo_Judge as hjudge from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line from M02_MasterTemplete import M02_Hopyo_Units as hunits from M02_MasterTemplete import M02_Labor_Ref as refs def _origin(to: Path, sk: str, mark: str, here: refs.Here) -> dict[str, Any]: try: doc = json.loads((to / sangeun.KIND / f"{sk}.json").read_bytes()) except (OSError, ValueError): return {} return sangeun.origin_of(doc, mark, here) def _won(v: float) -> str: return f"{v:,.1f}".removesuffix(".0") def _calc(one: dict[str, Any]) -> float | None: """엑셀 단가 × 매개값(× 계수) — `UnitLine.redo` 와 같은 셈 · 매개값 · 단가 없으면 None.""" q, price = one.get("품"), one.get("단가") or {} if q is None: return None if one["묶음"] == "R": return float(one.get("기준값") or 0) * q if one["묶음"] == "X": ratio = one.get("계수") or {} return sum(float(price[c]) * q * ratio.get(c, 1) for c in unit_line.COSTS if price.get(c)) unit = unit_line.money(price) return None if unit is None else unit * q def misses(block: dict[str, Any]) -> dict[str, Any] | None: """블록 엑셀 셈 어긋남 — 재현 못 한 줄 · 덮음 실패 · 없으면 None.""" lines = [] for one in block.get("항목") or []: if one.get("재현") or one.get("밖"): continue calc, got = _calc(one), one.get("금액") if calc is None or got is None: why = "매개값 · 단가를 못 읽음(글 꼴 밖)" if got is not None else "줄 금액 없음" else: why = f"엑셀 단가 × 매개값 = {_won(calc)} ≠ 엑셀 줄 금액 {_won(float(got))}" diff = round(float(got) - calc, 1) if calc is not None and got is not None else None name = one.get("이름") or "" row = {"행": one.get("행"), "이름": name, "묶음": one["묶음"], "셈": calc, "금액": got} lines.append({**row, "차": diff, "까닭": why}) total, have = unit_line.money(block.get("금액")), block.get("줄합") cover = block.get("덮음") if not lines and cover is not False: return None diff = round(float(have) - total, 1) if total is not None and have is not None else None why = ( f"줄 합 {_won(float(have))} ≠ 엑셀 블록 금액 {_won(total)} — 엑셀 합이 줄을 뺐거나 더함" if cover is False and diff is not None else f"재현 못 한 줄 {len(lines)}" ) return {"줄합": have, "금액": total, "차": diff, "덮음": cover, "줄": lines, "까닭": why} def _wage_diff(units: hunits.Units, one: dict[str, Any], got: dict[str, Any]) -> bool: """노무 줄 엑셀 노임 ≠ M01 지금 노임(참고 차).""" # ponytail: 노무만 · 기계 · 자재 참고 차는 D-3 우리 금액 셈이 생기면 그 값으로 key = (got.get("단위") or {}).get("노임") if one["묶음"] == "L" else None row = ( next((r for rs in units.lb.values() for r in rs if str(r.get("키")) == key), None) if key else None ) ours, xl = (row or {}).get("값"), (one.get("단가") or {}).get("노무비") return bool(ours and xl and not unit_line.near(float(ours), float(xl))) def lines(block: dict[str, Any], units: hunits.Units) -> list[list[Any]]: """블록 단위 줄 `[[출처 행, 상태, 차]]`(779-5) — 행 없는 줄 뺌.""" out = [] for one in block.get("항목") or []: if one.get("행") is None: continue if one.get("밖"): out.append([one["행"], "밖", False]) continue got = units.line(one) diff = got["상태"] == hunits.LINKED and _wage_diff(units, one, got) out.append([one["행"], got["상태"], diff]) return out def blocks(folder: Path, key: str, to: Path | None = None) -> list[dict[str, Any]]: """블록마다 `{번, 판정, SK, 갈래, 못옮김, 어긋남, 줄}` — `to` = 만든 SK 를 볼 층(없으면 파일 층).""" folder, to = Path(folder), Path(to or folder) hp = hopyo_store.read(folder, key) here = refs.where(folder) have = sangeun.made(to, key, here) all_ = sangeun.calls(hp)[0] units = hunits.Units() if all_ else None call = sangeun.callers(hp) judge = hjudge.judged(folder, key) or {} # 셈 중 = None out = [] for no in sorted(all_): mark = sangeun.mark(key, no) sk = have.get(mark) origin = _origin(to, sk, mark, here) if sk else {} new = judge.get(no) or {} out.append( { "번": no, "판정": new.get("판정"), "고를수": new.get("고를수"), "SK": sk, "갈래": origin.get("갈래"), "못옮김": origin.get("못옮김") or [], "부름": call.get(no, []), "어긋남": misses(all_[no]), "줄": lines(all_[no], units) if units else [], } ) return out