- Labor_Source: 굳힌 SK = 품셈 꼴 대가 블록(사본 단가.json 굳힌 키 줄 · M01 안 읽음) · 단가산출 속 일위대가 · SK 줄 = 그 대가 블록 부르는 줄 · 고정 줄 = fixed · 단가 고친 줄 = fixed(M02 셈 단가) · cover = P:+SK 이름표(SK 고쳐도 B09 덮개 그대로) · 줄 sk · poom(품셈 값) · 부르는 줄 수량 ÷ 굳힌 줄 배 · 사전조합 알약 = 굳힌 알약 · 서고 sangeun 사본 - Sangeun: SK 하위 로직 = 산근 · 단가산출 속 참조 걸음 · 결과단위 배 = 사본 줄에서(_unit(row) · unit_of(whole, code)) - Linked: 굳힌 SK 자원 키(굳힌 키 로직 줄 · 고정 이음) - Refresh: 미리 보기 단가산출(바뀌는 SK 줄) · 서고 사본 원본판 다르면 다시 굳힘 - Bill_Library: 종류 sangeun(내 자료집 SK 불러오기) · changed · refresh_copy(같은 폴더 · 값 그대로 · 원본판 없는 옛 사본 그대로) - Daega_Personal: 품셈 블록(굳힌 SK 포함) → 개인 층 SK(B09 품 · 단가 덮개 = SK 덮개 · 더한줄 수량 · 단가) - M02_Item_Sangeun: 굳힌 SK 정체(굳힌 줄 차례 → 원본 cid) - 화면: 일위대가 펼침 SK 줄 되돌림 [단가산출 값으로] · 곁 글 품셈 값 · 내 자료집 단가산출 글 · 업데이트 미리 보기 SK 줄 · 단가연결 내 것 단가산출(Labor_Api fetchLibrary 종류 넓힘) - 시험 test_719_2_sk_project 5 통과 + 1 건너뜀(715-1b 엔진 단가 · 값묶음 뒤 자동 실행) - test_155_5: _unit 인자 = 로직 줄로 바뀜(사본 줄에서 배 읽음) — 부르는 줄 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
305 lines
15 KiB
Python
305 lines
15 KiB
Python
"""B09 「내 자료집에 저장」(PLAN 703-2) — 펼친 일위대가(대가 블록) → 개인 층 사전조합(SC).
|
|
|
|
담는 것 = 담은로직(넣을 값 · 고른 자재 얹음) · 자재 · 줄 수량(= 수량산출 수기 `손`) —
|
|
맨 위 줄 수량 덮개 = 손(원래 손 × 덮개 ÷ 블록 줄 수량 · 대가 줄 ÷ 배를 되돌림)
|
|
안 담는 것 = 품 덮개(부른 블록 안 줄 · 품셈 블록 줄 — 로직 통째 배수로 바꾸면 다른 줄까지 바뀜) ·
|
|
단가 덮개(사전조합 꼴에 단가 칸 없음) → 수만 알림(품은 PLAN 705 고정 줄이 생기면 담김)
|
|
원천 = 블록 cover — 조합 `S:`/`L:` = 사본 폴더(일위대가 · 넣을 값 · 고른 자재 · 셈 수량) ·
|
|
사전조합 `C:`/`L:` = 굳힌 참조사본 문서 · 품셈 `P:` = 로직 한 줄 ·
|
|
복사본 `K:`(703-2) = 원본까지 따라감(이름 · 덮개 = 복사본 것)
|
|
같은 이름 = 409 `{같은이름: 키, 이름}` → 부르는 쪽이 덮어쓰기(`overwrite` = 그 키) · 새 이름 ·
|
|
쓰기 = 지금 층(`store.at` · 층 권한 · 폴더는 부르는 쪽).
|
|
품셈 블록(`P:` · 굳힌 SK 포함 · 719-2) = 개인 층 단가산출(SK · `sangeun`) — 바탕 = 그 M01 로직 ·
|
|
원본 SK 문서(바탕 · 덮개 · 더한줄 · 뺀줄) · 넣을 값 · 고른 자재(cid) · B09 품 · 단가 덮개 =
|
|
SK 덮개(같은 cid · 굳힌 SK = `정체` 로 원본 cid · 더한줄 줄 = 그 더한줄 수량 · 단가) ·
|
|
못 옮긴 칸 = 안담음 수 ·
|
|
내 자료집 SK 불러오기 = B09 서고 사본 `sangeun`(`Bill_Library`).
|
|
불러온 공사 일위대가(707-7 · 참조 층 `project`) = 하위 호표 굳힌 문서를 문서 `_하위` 에 실어 보내고
|
|
쓸 때 지금 층에 같이 씀(새 키 · `본뜬.복사원본` = 그 참조 · 같은 하위는 한 번만 ·
|
|
이미 있으면 그 키).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from B09_Estimation import B09_Estimation_Bill_Library as library
|
|
from B09_Estimation import B09_Estimation_Sangeun as sg
|
|
from B09_Estimation.B09_Estimation_Calc_Labor import COPY, COVER_KEYS, with_copies
|
|
from B09_Estimation.B09_Estimation_Labor_Source import AREA_FILE, daega_inputs
|
|
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build
|
|
from M02_MasterTemplete import M02_Item_Prices as item_prices
|
|
from M02_MasterTemplete import M02_Labor_Ref as refs
|
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
|
from M02_MasterTemplete.M02_Item_Snapshot import (
|
|
MANIFEST_NAME,
|
|
PICKS_FILE,
|
|
VALUES_FILE,
|
|
structures_dir,
|
|
)
|
|
from M02_MasterTemplete.M02_Item_Source import LABOR_FILE, PRICE_FILE, read_json
|
|
|
|
KIND = "precombo"
|
|
SK_KIND = "sangeun"
|
|
SK_KEEP = ("바탕", "입력값", "열린입력", "켜쌓기율", "고름", "자재표시", "덮개", "더한줄", "뺀줄")
|
|
SUBS = "_하위"
|
|
PRICE_KEYS = ("labor", "material", "expense")
|
|
|
|
|
|
def _combo(folder: Path) -> tuple[list[dict], list[dict]]:
|
|
"""구조물조합 사본 → (담은로직 · 자재) — 넣을 값 · 고른 자재 얹고 수량 = 셈 수량(손)."""
|
|
labor = read_json(folder / LABOR_FILE) or {}
|
|
values = read_json(folder / VALUES_FILE) or {}
|
|
picks = read_json(folder / PICKS_FILE) or {}
|
|
got = item_prices.calc(
|
|
labor, read_json(folder / PRICE_FILE) or {}, values, picks, read_json(folder / AREA_FILE)
|
|
)
|
|
doc = item_prices.with_values(labor.get("줄") or {}, values, picks)
|
|
lines = [
|
|
{**one, "수량": {"손": done.get("수량")}}
|
|
for one, done in zip(doc.get("담은로직") or [], got.get("줄") or [])
|
|
]
|
|
mats = [
|
|
{**one, "수량": {"손": done.get("수량")}}
|
|
for one, done in zip(doc.get("자재") or [], got.get("자재") or [])
|
|
]
|
|
return lines, mats
|
|
|
|
|
|
def _frozen(root: Path, rid: str) -> dict[str, Any] | None:
|
|
"""굳힌 사전조합 문서 — 구조물 사본 · 서고 사본의 `참조사본` 에서 찾음."""
|
|
folders = [p for p in sorted(structures_dir(root).glob("*")) if p.is_dir()]
|
|
folders += [p for p, _m in library.copies(root)]
|
|
for folder in folders:
|
|
doc = ((read_json(folder / LABOR_FILE) or {}).get("참조사본") or {}).get(rid)
|
|
if isinstance(doc, dict):
|
|
return doc
|
|
return None
|
|
|
|
|
|
def _precombo(doc: dict[str, Any]) -> tuple[list[dict], list[dict]]:
|
|
lines = [
|
|
{**one, "수량": {"손": (one.get("수량") or {}).get("손", 1)}}
|
|
for one in doc.get("담은로직") or []
|
|
if isinstance(one, dict)
|
|
]
|
|
return lines, [m for m in doc.get("자재") or [] if isinstance(m, dict)]
|
|
|
|
|
|
def source(root: Path, block: dict[str, Any], base: str) -> tuple[list[dict], list[dict]]:
|
|
"""블록 원천(`base` = 복사본이면 원본 cover) → (담은로직 · 자재) · 못 찾으면 LookupError."""
|
|
head, _, rest = base.partition(":")
|
|
if block["kind"] == "품셈":
|
|
origin = block.get("origin") or {}
|
|
one = {"로직": origin.get("로직"), "입력값": origin.get("입력값") or {}}
|
|
one.update({k: origin[k] for k in ("켜쌓기율", "고름") if origin.get(k) is not None})
|
|
return [{**one, "수량": {"손": 1}}], []
|
|
if head == "S":
|
|
return _combo(structures_dir(root) / rest)
|
|
if head == "L":
|
|
folder = library.library_dir(root) / rest
|
|
manifest = read_json(folder / MANIFEST_NAME) or {}
|
|
if manifest.get("종류") != "precombo":
|
|
return _combo(folder)
|
|
ref = (manifest.get("원본") or {}).get("참조")
|
|
doc = ((read_json(folder / LABOR_FILE) or {}).get("참조사본") or {}).get(refs.ref_id(ref))
|
|
if isinstance(doc, dict):
|
|
return _precombo(doc)
|
|
if head == "C" and (doc := _frozen(root, rest)) is not None:
|
|
return _precombo(doc)
|
|
raise LookupError(f"일위대가 원천을 못 찾음 「{base}」")
|
|
|
|
|
|
def build(root: Path, cover_key: str, cover: dict[str, Any], name: str | None = None) -> dict:
|
|
"""블록 cover → `{문서, 담음, 안담음}` — 문서 = 사전조합 꼴(키 · 층 쓰기 전)."""
|
|
raw = daega_inputs(root).get("blocks") or []
|
|
blocks = {b["cover"]: b for b in with_copies(raw, cover)}
|
|
block = blocks.get(cover_key)
|
|
if block is None:
|
|
raise LookupError(f"없는 일위대가 「{cover_key}」")
|
|
base = cover_key
|
|
while base.startswith("K:"): # 복사본 → 원본 cover
|
|
base = str(((cover.get(base) or {}).get(COPY) or {}).get("from") or "")
|
|
mine = cover.get(cover_key) or {}
|
|
if block["kind"] == "품셈":
|
|
return _sangeun(root, blocks.get(base, block), mine, name)
|
|
lines, mats = source(root, blocks.get(base, block), base)
|
|
kept = {"수량": 0, "넣을값": sum(bool(ln.get("입력값")) for ln in lines)}
|
|
kept["고른자재"] = sum(bool(ln.get("고름")) for ln in lines)
|
|
skip = {"품": 0, "단가": 0}
|
|
top = block["lines"] if block["kind"] != "품셈" else []
|
|
for line, one in zip(top, [*lines, *mats]):
|
|
cell = mine.get(line.get("cid")) or {}
|
|
skip["단가"] += sum(k in cell for k in PRICE_KEYS)
|
|
if "qty" not in cell:
|
|
continue
|
|
hand, each = (one.get("수량") or {}).get("손"), line.get("qty")
|
|
if not isinstance(hand, (int, float)) or not each:
|
|
skip["품"] += 1
|
|
continue
|
|
one["수량"] = {"손": round(hand * cell["qty"] / each, 9)}
|
|
kept["수량"] += 1
|
|
# 맨 위 밖 덮개 = 품셈 블록 줄 · 부른 블록 안 줄(품) · 단가
|
|
by_id = {b["id"]: b for b in blocks.values()}
|
|
todo = [] if top else [block["id"]]
|
|
todo += [ln.get("key") for ln in top if ln.get("kind") == "daega"]
|
|
seen: set[str] = set()
|
|
while todo:
|
|
sub = by_id.get(todo.pop())
|
|
if sub is None or sub["id"] in seen:
|
|
continue
|
|
seen.add(sub["id"])
|
|
for cell in (cover.get(sub["cover"]) or {}).values():
|
|
skip["품"] += "qty" in cell
|
|
skip["단가"] += sum(k in cell for k in PRICE_KEYS)
|
|
todo += [ln.get("key") for ln in sub["lines"] if ln.get("kind") == "daega"]
|
|
doc = {
|
|
"이름": (name or block.get("name") or "").strip(),
|
|
"단위": block.get("unit") or "",
|
|
"담은로직": lines,
|
|
"자재": mats,
|
|
}
|
|
if subs := _subs(root, lines):
|
|
doc[SUBS] = subs
|
|
return {"문서": doc, "담음": kept, "안담음": skip}
|
|
|
|
|
|
def _sangeun(root: Path, block: dict, mine: dict, name: str | None) -> dict[str, Any]:
|
|
"""품셈 블록 → 개인 층 SK 문서 `{문서, 담음, 안담음}` — 블록 줄 차례 = 엔진 줄 차례 =
|
|
cid 차례."""
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
|
|
|
origin = block.get("origin") or {}
|
|
rid = origin.get("단가산출")
|
|
if rid: # 굳힌 SK = 원본 문서 위에 · cid = 정체(굳힌 줄 차례 → 원본 cid)
|
|
frozen = _frozen(root, rid)
|
|
if frozen is None or not isinstance(frozen.get("원본"), dict):
|
|
raise LookupError(f"단가산출 원천을 못 찾음 「{rid}」")
|
|
doc = {k: copy.deepcopy(frozen["원본"][k]) for k in SK_KEEP if k in frozen["원본"]}
|
|
ids = list(frozen.get("정체") or [])
|
|
unit = str(frozen["원본"].get("단위") or "")
|
|
else: # M01 로직 한 줄 = 바탕 · 고름 차례 → cid
|
|
try:
|
|
row = m01.loaded()[1].logic(str(origin.get("로직")))
|
|
except Exception as e: # noqa: BLE001 — 엔진 FormulaError
|
|
raise LookupError(f"로직을 못 찾음 「{origin.get('로직')}」") from e
|
|
ids, unit = sd.line_ids(row), sg._unit(row)[0]
|
|
chosen = (origin.get("고름") or {}).items()
|
|
picks = {ids[int(n)]: mt for n, mt in chosen if int(n) < len(ids)}
|
|
doc = {"바탕": {"로직": origin.get("로직")}, "입력값": dict(origin.get("입력값") or {})}
|
|
doc |= {"고름": picks} if picks else {}
|
|
if origin.get("켜쌓기율") is not None:
|
|
doc["켜쌓기율"] = origin["켜쌓기율"]
|
|
covers, added = doc.setdefault("덮개", {}), doc.setdefault("더한줄", [])
|
|
kept, skip = {"품": 0, "단가": 0}, {"품": 0, "단가": 0}
|
|
for n, line in enumerate(block["lines"]):
|
|
cell = {k: v for k, v in (mine.get(line.get("cid")) or {}).items() if k in COVER_KEYS}
|
|
cid = ids[n] if n < len(ids) else ""
|
|
if not cell:
|
|
continue
|
|
if cid.startswith("add:#"): # 더한줄 = 그 줄 수량 · 단가(한 비목 줄만)
|
|
one = added[int(cid[5:])]
|
|
if "qty" in cell:
|
|
one["수량"], kept["품"] = cell["qty"], kept["품"] + 1
|
|
prices = [cell[k] for k in PRICE_KEYS if k in cell]
|
|
ok = len(prices) == 1 and "참조" not in one and "고정" not in one
|
|
if ok:
|
|
one["단가"], kept["단가"] = prices[0], kept["단가"] + 1
|
|
skip["단가"] += len(prices) - (1 if ok else 0)
|
|
continue
|
|
if not cid or ("qty" in cell and doc.get("열린입력")): # 열어 둔 SK = 품 배율만
|
|
skip["품"] += "qty" in cell
|
|
cell.pop("qty", None)
|
|
if not cell:
|
|
continue
|
|
if not cid:
|
|
skip["단가"] += len(cell)
|
|
continue
|
|
was = {k: v for k, v in (covers.get(cid) or {}).items() if k != "원값"}
|
|
if "qty" in cell:
|
|
was.pop("배율", None)
|
|
covers[cid] = {**was, **cell}
|
|
kept["품"] += "qty" in cell
|
|
kept["단가"] += sum(k in cell for k in PRICE_KEYS)
|
|
if not covers:
|
|
doc.pop("덮개")
|
|
head = {"종류": sd.TITLE, "이름": (name or block.get("name") or "").strip(), "단위": unit}
|
|
return {"문서": {**head, **doc}, "담음": kept, "안담음": skip}
|
|
|
|
|
|
def _own(lines: Any) -> list[dict[str, Any]]:
|
|
"""불러온 공사 하위 호표 참조(층 `project` — 층에 없음 · 굳힌 문서뿐)."""
|
|
return [
|
|
one["참조"]
|
|
for one in lines or []
|
|
if isinstance(one, dict) and (one.get("참조") or {}).get("층") == "project"
|
|
]
|
|
|
|
|
|
def _subs(root: Path, lines: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
|
"""닿는 불러온 하위 호표 `{ref_id: 굳힌 문서}` — 못 찾으면 LookupError."""
|
|
out: dict[str, dict[str, Any]] = {}
|
|
todo = _own(lines)
|
|
while todo:
|
|
ref = todo.pop()
|
|
rid = refs.ref_id(ref)
|
|
if rid in out:
|
|
continue
|
|
doc = _frozen(root, rid)
|
|
if doc is None:
|
|
raise LookupError(f"하위 호표를 못 찾음 「{ref.get('키')}」")
|
|
out[rid] = doc
|
|
todo += _own(doc.get("담은로직"))
|
|
return out
|
|
|
|
|
|
def _carry(lines: list[Any], subs: dict[str, dict[str, Any]], done: dict[str, Any]) -> list[Any]:
|
|
"""불러온 하위 참조 → 지금 층 새 키(하위 먼저 · 같은 것 한 번 · 앞서 옮긴 것 = 그 키)."""
|
|
here = refs.here()
|
|
have = {
|
|
refs.ref_id(got["복사원본"]): key
|
|
for key, got, _raw in hopyo_build._origins(store.root())
|
|
if isinstance(got.get("복사원본"), dict)
|
|
}
|
|
out = []
|
|
for one in lines:
|
|
ref = one.get("참조") if isinstance(one, dict) else None
|
|
if not isinstance(ref, dict) or ref.get("층") != "project":
|
|
out.append(one)
|
|
continue
|
|
rid = refs.ref_id(ref)
|
|
if rid not in done:
|
|
key = have.get(rid)
|
|
if key is None:
|
|
sub = {k: v for k, v in subs[rid].items() if k not in ("키", "알약")}
|
|
sub["담은로직"] = _carry(list(sub.get("담은로직") or []), subs, done)
|
|
key = refs.create(KIND, refs.origin_mark(sub, ref))["이름"]
|
|
done[rid] = {"층": here[0], "주인": here[1], "키": key}
|
|
out.append({**one, "참조": done[rid]})
|
|
return out
|
|
|
|
|
|
def write(doc: dict[str, Any], overwrite: str | None = None) -> dict[str, Any]:
|
|
"""지금 층(`store.at` 안)에 씀 — 같은 이름이면 409 · `overwrite` = 같은 이름 그 키를 덮음."""
|
|
subs = doc.get(SUBS) or {}
|
|
doc = {k: v for k, v in doc.items() if k != SUBS}
|
|
kind = SK_KIND if doc.get("종류") == refs.TITLE[SK_KIND] else KIND
|
|
same = [
|
|
path.stem
|
|
for path in sorted((store.root() / kind).glob(f"{refs.PREFIX[kind]}-*.json"))
|
|
if (read_json(path) or {}).get("이름") == doc["이름"]
|
|
]
|
|
if overwrite and overwrite not in same:
|
|
raise store.StoreError(409, f"덮어쓸 「{doc['이름']}」 이 없음 — 다시 누름")
|
|
if same and not overwrite:
|
|
raise store.StoreError(409, {"같은이름": same[0], "이름": doc["이름"]})
|
|
if subs:
|
|
doc = {**doc, "담은로직": _carry(doc["담은로직"], subs, {})}
|
|
if overwrite:
|
|
old = store.read(kind, overwrite)
|
|
return store.write(kind, overwrite, old["판"], {**doc, "키": overwrite})
|
|
return refs.create(kind, doc)
|