- 복사 = 덮개 K:… 의 @copy {from · name · spec} + 원본 덮개째(B09 사용자 값 · 브레인 정함) — 셈 짝 Calc_Labor py·ts 가 원본 블록을 본뜬 새 호표(번호 끝 · 기본값 = 원본) · 이 줄 단가연결을 새 호표로 바꿀지 물음 · 복사본 지우기(잇는 줄 있으면 막음)
- 원본 없음 = 짝 잃음 알림 · 저장 다듬기 @copy 글만 · 서고 사본 원본 = prune 에서 지킴
- 내 자료집 = POST …/estimation/daega/personal → 개인 층 사전조합(B09_Estimation_Daega_Personal) — 넣을 값 · 고른 자재 · 줄 수량 덮개(손) 담음 · 품 · 단가 덮개 = 안 담은 수 알림 · 같은 이름 409 → 덮어쓰기 · 새 이름
- 어디 쓰였나 = 누른 줄 노임 · 자재 · 중기(그 밖 = 호표)를 쓰는 내역 줄 목록 → 누르면 그 줄로 · 펼침
- 새 파일 B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts · 시험 test_703_b09_daega_copy(거울 · 임시 storage 길)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
166 lines
7.8 KiB
Python
166 lines
7.8 KiB
Python
"""B09 「내 자료집에 저장」(PLAN 703-2) — 펼친 일위대가(대가 블록) → 개인 층 사전조합(SC).
|
|
|
|
담는 것 = 담은로직(넣을 값 · 고른 자재 얹음) · 자재 · 줄 수량(= 수량산출 수기 `손`) —
|
|
맨 위 줄 수량 덮개 = 손(원래 손 × 덮개 ÷ 블록 줄 수량 · 대가 줄 ÷ 배를 되돌림)
|
|
안 담는 것 = 품 덮개(부른 블록 안 줄 · 품셈 블록 줄 — 로직 통째 배수로 바꾸면 다른 줄까지 바뀜) ·
|
|
단가 덮개(사전조합 꼴에 단가 칸 없음) → 수만 알림(품은 PLAN 705 고정 줄이 생기면 담김)
|
|
원천 = 블록 cover — 조합 `S:`/`L:` = 사본 폴더(일위대가 · 넣을 값 · 고른 자재 · 셈 수량) ·
|
|
사전조합 `C:`/`L:` = 굳힌 참조사본 문서 · 품셈 `P:` = 로직 한 줄 ·
|
|
복사본 `K:`(703-2) = 원본까지 따라감(이름 · 덮개 = 복사본 것)
|
|
같은 이름 = 409 `{같은이름: 키, 이름}` → 부르는 쪽이 덮어쓰기(`overwrite` = 그 키) · 새 이름 ·
|
|
쓰기 = 지금 층(`store.at` · 층 권한 · 폴더는 부르는 쪽).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from B09_Estimation import B09_Estimation_Bill_Library as library
|
|
from B09_Estimation.B09_Estimation_Calc_Labor import COPY, with_copies
|
|
from B09_Estimation.B09_Estimation_Labor_Source import AREA_FILE, daega_inputs
|
|
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"
|
|
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 "")
|
|
lines, mats = source(root, blocks.get(base, block), base)
|
|
mine = cover.get(cover_key) or {}
|
|
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,
|
|
}
|
|
return {"문서": doc, "담음": kept, "안담음": skip}
|
|
|
|
|
|
def write(doc: dict[str, Any], overwrite: str | None = None) -> dict[str, Any]:
|
|
"""지금 층(`store.at` 안)에 씀 — 같은 이름이면 409 · `overwrite` = 같은 이름 그 키를 덮음."""
|
|
same = [
|
|
path.stem
|
|
for path in sorted((store.root() / KIND).glob("SC-*.json"))
|
|
if (read_json(path) or {}).get("이름") == doc["이름"]
|
|
]
|
|
if overwrite:
|
|
if overwrite not in same:
|
|
raise store.StoreError(409, f"덮어쓸 「{doc['이름']}」 이 없음 — 다시 누름")
|
|
old = store.read(KIND, overwrite)
|
|
return store.write(KIND, overwrite, old["판"], {**doc, "키": overwrite})
|
|
if same:
|
|
raise store.StoreError(409, {"같은이름": same[0], "이름": doc["이름"]})
|
|
return refs.create(KIND, doc)
|