- 새 M02_Hopyo_Router — 옛 hopyo 길 모두 M02_Labor_Router 에서 옮김(옛 화면 785-7 까지 돎) · 새 길 hopyo-judge 목록 · 호표 · 블록 · hopyo/{키}/check · hopyo/{키}/move · main 등록 · 시동 warm · Fit.ensure 걷음 · 올림 때 엑셀 사전(Row.lexicon) · 블록 풀이 뒤 셈 · 끝내기 때 저장본 지움
- 새 M02_Hopyo_Judge — 1차 줄 판(Row) + 단산 줄 2차 풀이(Clause → Solve) · 엑셀 칸(이름 · 규격 · 단위 · 단가) · 블록 금액 · 절 몫 · 블록 풀이 파생 저장본(HP 폴더 _derived · 입력 지문 · 지문 다를 때만) · judged(folder, key) · 검산(입력값 = 엔진이 쓴 칸 모두 · 지역 · 자재지역 · 조종원직종 비움 · 맡은 절 단위 줄 · 몫과 품 대조 · 금액 = 일위대가 로직 줄과 같은 셈)
- 새 M02_Hopyo_Make — 고른 것 → SC 줄(같은 별칭 · 줄 본뜬 {호표, 차례, 행} · 수량 = 엑셀 × 몫 × 배 · 같은 기초 둘째 = 한 줄 SC 사본 · 별도계상 자재 · 이음 · 율 · 고정 · 참조) · SK 더한줄(바탕 없음 · SkUnit.adds · Sangeun.Maker.write) · 하위 먼저 · 입력 필요 · 단위 환산 못 정함 = 그 줄(블록)만 막힘 · 장부 고른 것 꼴 짝 · 이미 옮긴 것 = 덮음 없으면 409(남의 SC 덮지 않음) · 확인한 줄 몸(Lines) = Build 에서 옮김
- Build 나눔 — 모두 고정 길 · Builder · _order · _origins · build_all 그대로(B09 빌림) · 확인한 줄 몸 → Make.Lines
- SkUnit · Sangeun — SkPatch · Fit · Basis 부름 걷음(기계경비 기초 차례 SkUnit 으로 · 근거 꼬리표 = Clause.basis · 맞춤 셈 409 없앰 · 재료 고름 MO 됨) · Kids 가 _same_mark 가짐
- 시험 새 test_642_5_make(판정 · 검산 · 옮기기 · 예시 1 · 2 · 3 · 덮음 409 · 막는 길) · test_m02_hopyo_excel 마당에 새 Router 더함 · HOPYO_MAX_BYTES 자리(길 옮긴 까닭)
- 낡은 시험 고침: test_718_1 · test_729 「맞춤 셈 전 409」 뺌(맞춤 뒤 셈 안 기다림) · test_628 맞춤셈 캐시 넣기 뺌(근거 = 조항 색인) · test_729 근거 없음 꼴 · 고정 이음 유가 칸(785-3 LINKS 늘어난 까닭)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
323 lines
17 KiB
Python
323 lines
17 KiB
Python
"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 729-1 · 778-5 D-3 · 요구 621 · 623) — 산근(단산) 블록마다
|
||
줄 판정 모음(`M02_Hopyo_SkUnit`) → 사용자가 확인한 블록만 단위 SK.
|
||
|
||
모두 줄로만(바탕 기초 안 씀 · 조항 색인 기초 = 근거 꼬리표만) · 자동 없음 — `확인 [{번, 고름?: {차례: 고름}}]`
|
||
(블록마다 [확인] · [옮기기])에 든 블록만 만듦 · 확인 안 한 블록 = 판정만(안 만듦) · 줄 다 이어지지 않은 확인 블록 =
|
||
`못만듦` · 하위 단산 줄 = 그 블록 SK 참조(확인 목록에 있으면 먼저 만듦 · 없으면 못만듦).
|
||
SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 ·
|
||
바탕 없음 · 본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래 「단위」, 근거 {로직, 이름}, 못옮김 []} ·
|
||
같은 본뜬(키 · 원본)이 이 층에 있으면 그 키에 덮음 · 만드는 층 = 부르는 층.
|
||
판정 · 확인 = 모든 블록(780-1 · 요구 627) — 일위대가가 부르는 블록 + 일위대가 밖(내역서 등이 부름) · 줄마다
|
||
`부름 [호표 번]`([] = 일위대가 밖) · 밖 블록 = 이름 · 단위 블록 것 · 근거 꼬리표 없음(맞춤셈 열쇠 = 부르는 줄).
|
||
같은 내용(733-1 · `M02_Library_Same` 지문) SK 가 이 층에 있으면 새로 안 만들고 그것을 씀 — 이름 = 먼저 만든 것 ·
|
||
뒤 출처 = 그 SK `본뜬.또 [그 블록 본뜬 + 이름]`(`made` · `origin_of` 가 읽음) · 다른 출처가 쓰는 SK 는 내용을
|
||
안 바꿈(바뀌면 떼고 다시 찾음) · 응답 `같은것씀 [{번, 키}]` · 위 층 같은 것 = `위층같음 [{번, 층, 주인, 키}]`(알림만).
|
||
일위대가 만들기(`M02_Hopyo_SkLink`)가 부르는 `Maker.plan(번)`(확인 없음) = 고름 대기 「확인 전」 — 못 이음.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
from types import SimpleNamespace
|
||
from typing import Any
|
||
|
||
from common_util.common_util_time import utc_now_text
|
||
from M01_MasterData import M01_MasterData_Store as m01
|
||
from M02_MasterTemplete import M02_Hopyo_Clause as clause
|
||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||
from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit
|
||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||
from M02_MasterTemplete import M02_Hopyo_Units as units
|
||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||
from M02_MasterTemplete import M02_Library_Same as same
|
||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||
from M02_MasterTemplete.M02_Hopyo_Kids import _same_mark
|
||
|
||
ORIGIN = "실무자료"
|
||
KIND = "sangeun"
|
||
WAIT_WHY = "확인 전 — [단가산출]에서 블록 [확인] 뒤 [옮기기]"
|
||
WAIT = "고름 대기"
|
||
skpatch = SimpleNamespace(WAIT=WAIT, SK="SK") # 옛 이름(SkLink 가 읽음 · 785-7 지움)
|
||
|
||
|
||
def mark(key: str, no: int) -> str:
|
||
return f"{key} 단산{no}"
|
||
|
||
|
||
def made(
|
||
folder: str | Path, key: str | None = None, source: refs.Here | None = None
|
||
) -> dict[str, str]:
|
||
"""이 층 실무자료 단가산출 `{본뜬 키: SK 키}`(`key` = 그 HP 것만 · `source` = 파일 층으로 거름)."""
|
||
out = {}
|
||
for path in sorted((Path(folder) / KIND).glob("SK-*.json")):
|
||
try:
|
||
got = json.loads(path.read_bytes()).get("본뜬")
|
||
except (OSError, ValueError, AttributeError):
|
||
continue
|
||
if not isinstance(got, dict) or got.get("층") != ORIGIN:
|
||
continue
|
||
for one in [got, *(got.get("또") or [])]: # 또 = 같은 내용으로 이 SK 를 쓴 뒤 출처(733-1)
|
||
mark_ = str((one or {}).get("키") or "") if isinstance(one, dict) else ""
|
||
if not mark_ or (key is not None and not mark_.startswith(f"{key} ")):
|
||
continue
|
||
if source is None or not one.get("원본") or tuple(one["원본"]) == tuple(source):
|
||
out[mark_] = path.stem
|
||
return out
|
||
|
||
|
||
def origin_of(doc: dict, mark_: str, source: refs.Here | None = None) -> dict[str, Any]:
|
||
"""그 블록 출처의 본뜬(갈래 · 못옮김 · 놓음 …) — 또 가운데 `mark_` · 없으면 제 본뜬."""
|
||
own = doc.get("본뜬") or {}
|
||
want = {"키": mark_, "원본": list(source) if source else None}
|
||
also = [x for x in own.get("또") or [] if isinstance(x, dict)]
|
||
return next((x for x in also if _same_mark(x, want)), own)
|
||
|
||
|
||
def calls(hp: dict[str, Any]) -> tuple[dict[int, dict], dict[int, dict]]:
|
||
"""(블록 {번: 블록}, 부르는 첫 줄 {번: 줄 + `_호표`})."""
|
||
blocks = {int(b["번"]): b for b in hp.get("단산") or [] if b.get("번") is not None}
|
||
first: dict[int, dict] = {}
|
||
for h in hp.get("호표") or []:
|
||
for line in h.get("줄") or []:
|
||
ref = line.get("참조") or {}
|
||
no = int(ref.get("번") or 0) if ref.get("종류") == "단산" else 0
|
||
if no in blocks and no not in first:
|
||
first[no] = {**line, "_호표": h.get("번")}
|
||
return blocks, first
|
||
|
||
|
||
def callers(hp: dict[str, Any]) -> dict[int, list]:
|
||
"""{단산 번: 부르는 호표 번 [차례대로 · 겹침 뺌]} — 없는 번 = 일위대가 밖."""
|
||
out: dict[int, list] = {}
|
||
for h in hp.get("호표") or []:
|
||
for line in h.get("줄") or []:
|
||
ref = line.get("참조") or {}
|
||
if ref.get("종류") == "단산" and ref.get("번"):
|
||
got = out.setdefault(int(ref["번"]), [])
|
||
if h.get("번") not in got:
|
||
got.append(h.get("번"))
|
||
return out
|
||
|
||
|
||
def _bad(text: str, status: int = 400) -> store.StoreError:
|
||
return store.StoreError(status, text)
|
||
|
||
|
||
def _sure(raw: Any) -> dict[int, dict[int, Any]]:
|
||
"""확인 목록 `[{번, 고름?: {차례: 고름}}]` → `{번: {차례: 고름}}`(고름 꼴은 `M02_Hopyo_SkUnit` 이 봄)."""
|
||
if not raw:
|
||
return {}
|
||
if not isinstance(raw, list) or len(raw) > 500:
|
||
raise _bad("확인은 [{번, 고름?: {차례: 고름}}]")
|
||
out: dict[int, dict[int, Any]] = {}
|
||
for one in raw:
|
||
try:
|
||
picks = one.get("고름") or {}
|
||
out[int(one["번"])] = {int(k): v for k, v in picks.items()}
|
||
except (TypeError, ValueError, KeyError, AttributeError) as e:
|
||
raise _bad("확인은 [{번, 고름?: {차례: 고름}}]") from e
|
||
return out
|
||
|
||
|
||
def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) -> dict[str, Any]:
|
||
name = str(block.get("이름") or line.get("이름") or "").strip() or f"단산{no}"
|
||
unit = str(line.get("단위") or block.get("단위") or "").strip() or "식"
|
||
keep = {k: plan[k] for k in ("갈래", "엑셀품", "못옮김", "근거") if k in plan}
|
||
keep |= {"놓음": plan["놓음"]} if plan.get("놓음") else {}
|
||
return {
|
||
"종류": "단가산출",
|
||
"이름": name,
|
||
"단위": unit,
|
||
"구분": ORIGIN,
|
||
"상세구분": str(hp.get("파일") or ""),
|
||
"본뜬": {"층": ORIGIN, "키": mark(hp["키"], no), "파일": hp.get("파일"), **sure, **keep},
|
||
"바탕": plan["바탕"],
|
||
"입력값": plan["입력값"],
|
||
**({"환산": plan["환산"]} if plan.get("환산") else {}),
|
||
"열린입력": [],
|
||
"고름": {},
|
||
**{k: plan[k] for k in ("자재표시", "덮개", "더한줄", "뺀줄")},
|
||
}
|
||
|
||
|
||
class Maker:
|
||
"""한 번 만들기 — 파일 · 이 층 SK 를 한 번 읽음(`source` = 파일 층 · 쓰기 = `store.root()`)."""
|
||
|
||
def __init__(self, source: Path, key: str, who: Any = None) -> None:
|
||
self.source, self.key, self.who = Path(source), key, who
|
||
self.hp = hopyo_store.read(source, key)
|
||
self.blocks, self.first = calls(self.hp)
|
||
self.done = True # 옛 이름(SkLink) — 맞춤셈 안 기다림(785-5)
|
||
self.here = refs.where(source)
|
||
self.whole = m01.loaded()[1]
|
||
self.sure = {"원본": list(self.here), "확인": {"누가": who, "때": utc_now_text()}}
|
||
self.same, self.reused, self.above = same.Same(), [], []
|
||
self.keys = made(source, key, self.here) # {본뜬 키: SK} — 만든 것 더해 감
|
||
self.judged: dict[int, dict] = {}
|
||
self._units: units.Units | None = None
|
||
|
||
def units(self) -> units.Units:
|
||
if self._units is None:
|
||
books = ledger.books(self.source)
|
||
self._units = units.Units(books, ledger.doc_id(self.source, self.key))
|
||
return self._units
|
||
|
||
def basis(self, no: int) -> dict[str, Any] | None:
|
||
"""근거 꼬리표(참고) — 조항 색인(`M02_Hopyo_Clause.basis`) · 표지(기초 없음 · 옛 번호 · 준용 ·
|
||
번호 없음) 있으면 표지 · 후보 하나면 그 기초 · 여럿이면 블록 풀이(`M02_Hopyo_Judge` 저장본 ›
|
||
그 블록만 셈) 첫 고름 · 일위대가 밖 블록 = 없음."""
|
||
if no not in self.first:
|
||
return None
|
||
got = clause.basis(self.blocks[no])
|
||
if got["표지"]:
|
||
return {"로직": None, "이름": got["글"], "판정": got["표지"]}
|
||
cands = got["후보"]
|
||
top = cands[0]["키"] if len(cands) == 1 else self._solved(no)
|
||
if not top:
|
||
return None
|
||
return {"로직": top, "이름": (self.whole.logic(top) or {}).get("이름"), "판정": None}
|
||
|
||
def _solved(self, no: int) -> str | None:
|
||
from M02_MasterTemplete import M02_Hopyo_Judge as judge # Judge → 여기 — 늦게 읽음
|
||
|
||
cache = judge.solved(self.source, self.key) if getattr(self, "key", None) else None
|
||
line = {k: v for k, v in self.first[no].items() if k != "_호표"}
|
||
got = (cache or {}).get(str(no)) or judge._solve(judge.master(), self.blocks[no], line, [])
|
||
return next((s["고름"] for s in got["절"] if s["고름"]), None)
|
||
|
||
def ref_of(self, sub: dict[str, Any]) -> dict[str, Any] | None:
|
||
"""하위 줄 → 참조 칸 — 단산 = 이 HP 그 블록 SK · 일위 = 확인된 그 호표 SC · 없으면 None."""
|
||
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build # Build → SkLink → 여기
|
||
|
||
if sub.get("종류") == "단산":
|
||
got = self.keys.get(mark(self.key, int(sub["번"])))
|
||
else:
|
||
done = hopyo_build.confirmed(store.root(), self.here)
|
||
got = done.get(hopyo_build.mark(self.key, int(sub["번"])))
|
||
layer, owner = refs.here()
|
||
return {"층": layer, "주인": owner, "키": got} if got else None
|
||
|
||
def judge(self, no: int, picks: dict | None = None) -> dict[str, Any]:
|
||
line, block = self.first.get(no) or {}, self.blocks[no]
|
||
unit = str(line.get("단위") or block.get("단위") or "").strip()
|
||
try:
|
||
got = skunit.judge(block, self.units(), self.ref_of, picks, unit)
|
||
except skunit.PickError as e:
|
||
raise _bad(f"단산{no} · {e}") from e
|
||
self.judged[no] = got
|
||
return got
|
||
|
||
def plan(self, no: int, picks: dict | None = None) -> dict[str, Any]:
|
||
"""확인한 블록 → 단위 SK 꼴 · 확인 없음(`picks` None) · 고를 줄 남음 = 고름 대기."""
|
||
if picks is None:
|
||
return {"갈래": WAIT, "까닭": WAIT_WHY, "후보": []}
|
||
got = self.judge(no, picks)
|
||
if got["고를줄"]:
|
||
return {"갈래": WAIT, "까닭": got["판정"], "후보": []}
|
||
return skunit.plan(got, self.basis(no))
|
||
|
||
def make(self, no: int, sure: dict[int, dict], out: dict, path: tuple = ()) -> None:
|
||
"""확인한 블록 하나 — 확인 목록의 하위 단산 먼저 · 못 만들면 `못만듦`."""
|
||
mark_ = mark(self.key, no)
|
||
if no in path or any(x["번"] == no for x in out["만듦"] + out["못만듦"]):
|
||
return
|
||
for one in self.blocks[no].get("항목") or []:
|
||
sub = one.get("하위") or {}
|
||
m = int(sub.get("번") or 0)
|
||
if sub.get("종류") == "단산" and m in sure and mark(self.key, m) not in self.keys:
|
||
self.make(m, sure, out, (*path, no))
|
||
plan = self.plan(no, sure[no])
|
||
got = self.judged[no]
|
||
lost = [r for r in got["줄"] if r["상태"] == units.LINKED and r["차례"] not in got["담음"]]
|
||
name = self.blocks[no].get("이름") or (self.first.get(no) or {}).get("이름")
|
||
if plan["갈래"] == WAIT or lost:
|
||
why = plan.get("까닭") or f"하위 「{lost[0]['이름']}」 — 그 블록 먼저 확인"
|
||
out["못만듦"].append({"번": no, "이름": name, "까닭": why})
|
||
return
|
||
key = self.write(no, plan, self.keys.get(mark_))
|
||
self.keys[mark_] = key
|
||
out["만듦"].append({"번": no, "이름": name, "키": key, "갈래": skunit.UNIT, "못옮김": []})
|
||
|
||
def write(self, no: int, plan: dict, have: str | None) -> str:
|
||
"""그 블록 SK 를 씀 → 키 — 다른 출처(또)가 쓰는 SK 의 내용은 안 바꿈(바뀌면 떼고 다시 찾음)."""
|
||
doc = _doc(self.first.get(no) or {}, self.blocks[no], no, self.hp, plan, self.sure)
|
||
root, mine = store.root(), doc["본뜬"]
|
||
self.above += [{"번": no, **r} for r in self.same.find_above(root, KIND, doc)]
|
||
if have:
|
||
raw, old, own, rest = self._read(have, mine)
|
||
owner = _same_mark(own, mine)
|
||
if self.same.finger(KIND, old) == self.same.finger(KIND, doc): # 내용 같음 = 출처만
|
||
self._put(
|
||
have, raw, old, *((mine, rest) if owner else (own, [*rest, self._entry(doc)]))
|
||
)
|
||
return have
|
||
if owner and not rest: # 혼자 쓰던 SK — 제자리 덮음
|
||
store.write(KIND, have, store.version_of(raw), {**doc, "키": have})
|
||
self.same.add(root, KIND, have, doc)
|
||
return have
|
||
head = {k: v for k, v in rest[0].items() if k != "이름"} if owner else own
|
||
self._put(have, raw, old, head, rest[1:] if owner else rest) # 떼어 냄
|
||
found = self.same.find_same(root, KIND, doc)
|
||
if found:
|
||
raw, old, own, rest = self._read(found[0], mine)
|
||
self._put(found[0], raw, old, own, [*rest, self._entry(doc)])
|
||
self.reused.append({"번": no, "키": found[0]})
|
||
return found[0]
|
||
key = refs.create(KIND, doc)["이름"]
|
||
self.same.add(root, KIND, key, doc)
|
||
return key
|
||
|
||
@staticmethod
|
||
def _entry(doc: dict) -> dict[str, Any]:
|
||
return {**doc["본뜬"], "이름": doc["이름"]}
|
||
|
||
@staticmethod
|
||
def _read(key: str, mine: dict) -> tuple[bytes, dict, dict, list]:
|
||
"""(바이트, 문서, 제 본뜬(또 뺌), 또 가운데 `mine` 뺀 것)."""
|
||
raw = (store.root() / KIND / f"{key}.json").read_bytes()
|
||
doc = json.loads(raw)
|
||
got = doc.get("본뜬") or {}
|
||
own = {k: v for k, v in got.items() if k != "또"}
|
||
rest = [x for x in got.get("또") or [] if isinstance(x, dict) and not _same_mark(x, mine)]
|
||
return raw, doc, own, rest
|
||
|
||
@staticmethod
|
||
def _put(key: str, raw: bytes, doc: dict, own: dict, also: list) -> None:
|
||
body = {**doc, "본뜬": {**own, "또": also} if also else own}
|
||
if body != doc:
|
||
store.write(KIND, key, store.version_of(raw), body)
|
||
|
||
|
||
def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]:
|
||
"""[단가산출] — 블록마다 줄 판정 · 확인한 블록만 단위 SK → `{블록, 판정 [{번, 이름, 단위, 판정, 고를줄,
|
||
우리금액, 엑셀금액, 근거, 만든, 줄, 부름}], 만듦 [{번, 이름, 키, 갈래, 못옮김}], 못만듦 [{번, 이름, 까닭}], 그대로,
|
||
같은것씀, 위층같음}` · `picks` = 확인 목록."""
|
||
sure = _sure(picks)
|
||
maker = Maker(folder, key, who)
|
||
bad = sorted(set(sure) - set(maker.blocks))
|
||
if bad:
|
||
raise _bad(f"없는 블록 「단산{bad[0]}」", 404)
|
||
out: dict[str, Any] = {"블록": len(maker.blocks), "만듦": [], "못만듦": []}
|
||
for no in sorted(sure):
|
||
maker.make(no, sure, out)
|
||
rows = []
|
||
call = callers(maker.hp)
|
||
for no in sorted(maker.blocks):
|
||
line, block = maker.first.get(no), maker.blocks[no]
|
||
got = maker.judged.get(no) or maker.judge(no)
|
||
name = block.get("이름") or (line or {}).get("이름")
|
||
head = {
|
||
"번": no,
|
||
"이름": name,
|
||
"단위": (line or block).get("단위"),
|
||
"부름": call.get(no, []),
|
||
}
|
||
keep = ("판정", "고를줄", "우리금액", "엑셀금액", "줄")
|
||
head |= {k: got[k] for k in keep} | {"근거": maker.basis(no)}
|
||
rows.append({**head, "만든": maker.keys.get(mark(key, no))})
|
||
out["판정"] = rows
|
||
out["그대로"] = sum(1 for r in rows if r["만든"] and r["번"] not in sure)
|
||
out["같은것씀"], out["위층같음"] = maker.reused, maker.above
|
||
return out
|