- 728-2 단위 환산 뒤 [단가산출 만들기] 500 · 400(100개 ↔ ㎡ · ㎥ ↔ m2 · 본 ↔ M · 개소 ↔ m) 걷음 — 견줌 SK 배 = ratio_of(728 규칙) · 성격 다름 = 환산 계수 = 우리 품 ÷ 엑셀 품(인력 · 장비 둘 이상 같은 비일 때만) · 못 구함 = 고름 대기(까닭 「단위 성격 다름 · 계수 필요」 · 후보[].계수 칸)
- 맞음 블록도 단위 견줌(전엔 셈 없이 기초로 써서 뒤 셈에서 막힘)
- 못옮김 = [{id, 갈래 노임|기계|자재, 이름, 규격, 단위, 수량, 글, 후보}] · 응답 만듦[].못옮김 · 환산
- 몸 바탕[번] = {로직?, 입력값?, 환산?, 잇기?} · 잇기 [{id, 노임|기계|자재: 키} | {id, 뺌}] → SK 더한줄 인력 · 기계 · 재료(엑셀 수량 × 배) · 뺌 = 본뜬 놓음 · 틀린 id · 키 400
- 못옮김 남은 SK 를 쓰는 일위대가 = 남은줄(단산 · SK · 못옮김) · hopyo-build · build-all 409
- 못옮김 남은 블록 = SK(같지 않음)
- 시험 test_729 에 단위 · 잇기 · 409 시험 하나 더함
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
219 lines
11 KiB
Python
219 lines
11 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 = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 ·
|
|
본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김} · 같은 본뜬(키 · 원본)이
|
|
이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층.
|
|
별칭 제안 = 기초 · 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_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
|
|
if key is not None and not str(got.get("키") or "").startswith(f"{key} "):
|
|
continue
|
|
if source is None or not got.get("원본") or tuple(got["원본"]) == tuple(source):
|
|
out[str(got.get("키"))] = path.stem
|
|
return out
|
|
|
|
|
|
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 _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()}}
|
|
|
|
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:
|
|
doc = _doc(self.first[no], self.blocks[no], no, self.hp, plan, self.sure)
|
|
if have:
|
|
raw = (store.root() / KIND / f"{have}.json").read_bytes()
|
|
store.write(KIND, have, store.version_of(raw), {**doc, "키": have})
|
|
return have
|
|
return refs.create(KIND, doc)["이름"]
|
|
|
|
|
|
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.update({skpatch.BASE: 0, skpatch.SK: 0})
|
|
books, alias = ledger.books(folder), {}
|
|
for no in sorted(maker.first):
|
|
old = have.get(mark(key, no))
|
|
if old and no not in chosen:
|
|
out["그대로"] += 1
|
|
continue
|
|
plan = maker.plan(no, chosen.get(no))
|
|
line = maker.first[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
|
|
name = maker.blocks[no].get("이름") or line.get("이름")
|
|
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)
|
|
return out
|