- 장부 읽는 차례: 자동 정확 열쇠(마스터 · 같은 불러오기 `불러오기` 표시) → 제안 정확 열쇠(`제안` True · 자동 아님) → 품명|단위 느슨 - 만들기 · 단가산출 만들기가 이은 짝에 불러오기 열쇠(`doc_id` 층/주인/HP 키) 남김 · 장부 화면 고침은 그 표시 그대로 둠 - 줄 맞춤: 내 제안 = 숫자 판정 그대로 + 꼬리표 「내 제안」 + `제안`(고르기 창 맨 위 · 한 번 눌러 짝 통째) · 호표 통째 제안 = 체크 안 함 - 장부 화면(회사 · 개인 층) = 「내 제안 목록」 · 승인 칸(대기 · 승인 · 반려 = M01 서명 · 반려 기억) · [삭제] → [거두기] · 마스터 층(M01) 그대로 - M01 별칭 장부 모으기 · 승인 · 반려 길 그대로 · 기존 층 장부 파일 안 지움(제안으로 읽힘) - 낡은 시험 고침: hopyo ledger(아래 층이 이김 → 마스터만 자동 · 제안 · 승인 상태) · hopyo match(별칭 자동 규칙 = 마스터 층 장부로) · hopyo screen(머리 글 · 거두기 단추) - 새 시험 test_739_1_alias_central(층 짝 자동 0 · 내 제안 · 이은 짝 → 제안 · M01 후보 · 승인 뒤 자동 · 반려 상태) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
314 lines
16 KiB
Python
314 lines
16 KiB
Python
"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 729-1 · 요구서 내역19 1장 7 · 3장 6) —
|
|
산근(단산) 블록마다 매칭 → 비교 → 기초 · SK · 고름 대기.
|
|
|
|
블록마다 그 블록을 부르는 첫 엑셀 줄(「단산N참조」)의 맞춤셈(`M02_Hopyo_Fit` 캐시 · 없으면 그 블록만
|
|
셈) → `M02_Hopyo_SkPatch.plan`:
|
|
기초 = SK 바탕 = 1위 기초 · 입력값 · 덮개 없음 · SK = 바탕 기초 + 덮개 · 더한줄 · 뺀줄(엑셀 품 그대로) ·
|
|
고름 대기 = SK 안 만듦(바탕 없는 SK 로 몰래 채우지 않음) · 후보 다섯(성격 다른 단위 = `계수` 칸 뜻) →
|
|
사용자가 고른 기초 `바탕 {번: {로직?, 입력값?, 환산?, 잇기?}}` 로 다시 부르면 그 기초에 견줘 SK(이미 있으면
|
|
그 키에 덮음) · 잇기 `[{id, 노임|기계|자재: 키} | {id, 뺌}]` = 못옮김 → SK 더한줄 · 남은 못옮김 = 응답
|
|
`만듦[].못옮김` · 본뜬 그대로(그 SK 를 쓰는 일위대가 만들기 = 남은 줄 409 · `M02_Hopyo_SkLink`) ·
|
|
그대로 SK 의 못옮김 = 응답 `남은못옮김 [{번, 이름, 키, 못옮김}]` · 잇기만 보냈는데 1위 기초가 고름 대기면
|
|
그 SK 바탕(로직 · 입력값 · 환산)으로 다시 셈(729-5).
|
|
SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 ·
|
|
본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김} · 같은 본뜬(키 · 원본)이
|
|
이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층.
|
|
같은 내용(733-1 · `M02_Library_Same` 지문) SK 가 이 층에 있으면 새로 안 만들고 그것을 씀 — 이름 = 먼저 만든 것 ·
|
|
뒤 출처 = 그 SK `본뜬.또 [그 블록 본뜬 + 이름]`(`made` · `origin_of` 가 읽음 · 갈래 · 못옮김은 출처마다) ·
|
|
다른 출처가 쓰는 SK 는 내용을 안 바꿈(고른 기초 · 잇기로 바뀌면 떼고 다시 찾음) · 응답 `같은것씀 [{번, 키}]` ·
|
|
위 층(회사 · 마스터) 같은 것 = 응답 `위층같음 [{번, 층, 주인, 키}]`(알림만 · 연결 안 함).
|
|
별칭 제안 = 기초 · SK 만든 블록의 엑셀 줄 → 이 층 별칭 장부 짝 = 바탕 기초까지만(차이 SK 는 이 층 문서 ·
|
|
요구서 3장 6) · 장부에 이미 짝 · 갈래가 있는 줄은 그대로 · 승인 길 = M01 별칭 장부(716).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
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_Fit as fit
|
|
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
|
from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch
|
|
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
|
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 import M02_Sangeun_Doc as sd
|
|
|
|
ORIGIN = "실무자료"
|
|
KIND = "sangeun"
|
|
|
|
|
|
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 _same_mark(a: dict, b: dict) -> bool:
|
|
"""같은 출처 — 본뜬 키 같음 · 원본(파일 층)이 둘 다 있으면 같음."""
|
|
if a.get("키") != b.get("키"):
|
|
return False
|
|
return not a.get("원본") or not b.get("원본") or tuple(a["원본"]) == tuple(b["원본"])
|
|
|
|
|
|
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 _bad(text: str, status: int = 400) -> store.StoreError:
|
|
return store.StoreError(status, text)
|
|
|
|
|
|
def _picks(raw: Any) -> dict[int, dict[str, Any]]:
|
|
"""사용자 고름 `{번: {로직?, 입력값?, 환산?, 잇기?}}` 재검사 — 돈 로직만 · 입력 이름 = 그 로직 입력 ·
|
|
환산 = SK 꼴(`M02_Sangeun_Doc`) · 잇기 = `[{id, …}]`(id 짝 · 키는 `M02_Hopyo_SkPatch` 가 봄)."""
|
|
if not raw:
|
|
return {}
|
|
if not isinstance(raw, dict) or len(raw) > 500:
|
|
raise _bad("바탕은 {블록 번: {로직, 입력값}}")
|
|
logics = ledger._known()[0]
|
|
out = {}
|
|
for no, one in raw.items():
|
|
code = str((one or {}).get("로직") or "") if isinstance(one, dict) else "?"
|
|
if code and code not in logics:
|
|
raise _bad(f"단산{no} · 돈 로직이 아님 「{code}」")
|
|
if not code and not (one.get("환산") or one.get("잇기")):
|
|
raise _bad(f"단산{no} · 로직 · 환산 · 잇기 중 하나")
|
|
pick: dict[str, Any] = {}
|
|
given = one.get("입력값") if code else None
|
|
if code:
|
|
pick["로직"] = code
|
|
if given is not None:
|
|
bad = set(given) - ledger.input_names(code) if isinstance(given, dict) else {1}
|
|
if bad:
|
|
raise _bad(f"단산{no} · 모르는 입력 {sorted(map(str, bad))}")
|
|
pick["입력값"] = given
|
|
if one.get("환산"):
|
|
try:
|
|
pick["환산"] = sd._swap_how(one["환산"])
|
|
except sd.DocError as e:
|
|
raise _bad(f"단산{no} · {e}") from e
|
|
links = one.get("잇기")
|
|
if links:
|
|
if not isinstance(links, list) or not all(
|
|
isinstance(x, dict) and x.get("id") for x in links
|
|
):
|
|
raise _bad(f"단산{no} · 잇기는 [{{id, 노임|기계|자재: 키}}]")
|
|
pick["잇기"] = links
|
|
out[int(no)] = pick
|
|
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 ("갈래", "엑셀품", "못옮김")}
|
|
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 ("자재표시", "덮개", "더한줄", "뺀줄")},
|
|
}
|
|
|
|
|
|
def _old_base(folder: Path, key: str, mark_: str, source: refs.Here) -> dict[str, Any]:
|
|
"""이미 있는 SK 의 바탕 고름 `{로직, 입력값, 환산?}` · `본뜬` — 읽기 실패 = 빈 것."""
|
|
try:
|
|
doc = json.loads((Path(folder) / KIND / f"{key}.json").read_bytes())
|
|
except (OSError, ValueError):
|
|
return {}
|
|
base = {"로직": (doc.get("바탕") or {}).get("로직"), "입력값": doc.get("입력값") or {}}
|
|
return {
|
|
**base,
|
|
**({"환산": doc["환산"]} if doc.get("환산") else {}),
|
|
"본뜬": origin_of(doc, mark_, source),
|
|
}
|
|
|
|
|
|
def _alias(line: dict, plan: dict) -> dict[str, Any]:
|
|
origin = {"품명": line.get("이름"), "규격": line.get("규격"), "단위": line.get("단위")}
|
|
clauses = (line.get("근거") or {}).get("출처") or []
|
|
pair = {"로직": plan["바탕"]["로직"], "입력값": plan["입력값"], "배": 1, "몫": None}
|
|
return {
|
|
"원래": {k: str(v or "") for k, v in origin.items()},
|
|
"갈래": "짝",
|
|
"짝": [pair],
|
|
"엑셀조항": clauses if isinstance(clauses, list) else [],
|
|
}
|
|
|
|
|
|
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)
|
|
got = fit.result(source, key) or {}
|
|
self.cache, self.done = got.get("블록") or {}, bool(got.get("끝"))
|
|
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(), [], []
|
|
|
|
def plan(self, no: int, pick: dict | None = None) -> dict[str, Any]:
|
|
line, block = self.first[no], self.blocks[no]
|
|
entry = self.cache.get(fit.unit_key(line))
|
|
if entry is None and not (pick or {}).get("로직"):
|
|
entry = fit.compute(block, line)
|
|
try:
|
|
return skpatch.plan(entry, block, line, self.whole, pick)
|
|
except skpatch.LinkError as e:
|
|
raise _bad(f"단산{no} · {e}") from e
|
|
|
|
def write(self, no: int, plan: dict, have: str | None) -> str:
|
|
"""그 블록 SK 를 씀 → 키 — 다른 출처(또)가 쓰는 SK 의 내용은 안 바꿈(바뀌면 떼고 다시 찾음)."""
|
|
doc = _doc(self.first[no], 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 · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래, 못옮김,
|
|
환산?}], 그대로, 남은못옮김 [{번, 이름, 키, 못옮김}], 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}],
|
|
별칭, 같은것씀 [{번, 키}], 위층같음 [{번, 층, 주인, 키}]}` · `picks` = 고름."""
|
|
chosen = _picks(picks)
|
|
maker = Maker(folder, key, who)
|
|
bad = sorted(set(chosen) - set(maker.first))
|
|
if bad:
|
|
raise _bad(f"없는 블록 · 부르는 줄 없음 「단산{bad[0]}」", 404)
|
|
if not maker.done and set(maker.first) - set(chosen):
|
|
fit.ensure(folder, key)
|
|
raise _bad("맞춤 셈 중 — 끝나면 다시 누름", 409)
|
|
have = made(folder, key, maker.here)
|
|
out: dict[str, Any] = {"블록": len(maker.first), "만듦": [], "그대로": 0, "고름대기": []}
|
|
out["남은못옮김"] = []
|
|
out.update({skpatch.BASE: 0, skpatch.SK: 0})
|
|
books, alias = ledger.books(folder), {}
|
|
for no in sorted(maker.first):
|
|
old = have.get(mark(key, no))
|
|
line = maker.first[no]
|
|
name = maker.blocks[no].get("이름") or line.get("이름")
|
|
if old and no not in chosen:
|
|
out["그대로"] += 1
|
|
lost = _old_base(folder, old, mark(key, no), maker.here)["본뜬"].get("못옮김")
|
|
if lost:
|
|
out["남은못옮김"].append({"번": no, "이름": name, "키": old, "못옮김": lost})
|
|
continue
|
|
plan = maker.plan(no, chosen.get(no))
|
|
if plan["갈래"] == skpatch.WAIT and old and not chosen[no].get("로직"):
|
|
base = _old_base(
|
|
folder, old, mark(key, no), maker.here
|
|
) # 고른 기초로 만든 SK 의 잇기 = 그 바탕 그대로
|
|
if base.get("로직"):
|
|
pick = {k: v for k, v in base.items() if k != "본뜬"}
|
|
plan = maker.plan(no, {**pick, **chosen[no]})
|
|
if plan["갈래"] == skpatch.WAIT:
|
|
head = {"번": no, "이름": maker.blocks[no].get("이름") or line.get("이름")}
|
|
out["고름대기"].append({**head, "단위": line.get("단위"), **plan})
|
|
continue
|
|
got = maker.write(no, plan, old)
|
|
out[plan["갈래"]] += 1
|
|
made_ = {"번": no, "이름": name, "키": got, "갈래": plan["갈래"], "못옮김": plan["못옮김"]}
|
|
out["만듦"].append({**made_, **({"환산": plan["환산"]} if plan.get("환산") else {})})
|
|
found = ledger.recall(line, folder, books)
|
|
if no in chosen or found is None or found.get("규격다름"):
|
|
alias[ledger.key(line)] = _alias(line, plan)
|
|
out["별칭"] = ledger.write(
|
|
folder, alias, who, maker.hp.get("파일") or None, ledger.doc_id(folder, key)
|
|
)
|
|
out["같은것씀"], out["위층같음"] = maker.reused, maker.above
|
|
return out
|