Files
Aislo/B09_Estimation/B09_Estimation_Daega_Personal.py
T
eomsangdonandClaude Opus 5.5 9633e97c3c feat(723-4): 723-1 뒤 B09 일위대가 수량 연결 마무리 — 알약 값 쓴 SC 만 덮개 나눔 · SK 속 SC · 공사 불러오기 수량 · 비고 경고
- Labor_Source = 덮개 열쇠 `C:참조|S:사본` 은 알약 값 쓴 SC 블록만(빈 수량 · 옛 손 = `C:참조` 그대로 = 703-1 지난 공사 가져옴 꼴) ·
  SK 속 SC 안 줄 수량 없음 = 엔진엔 0 값묶음(`_Loose`) · 그 줄 = 사전조합 블록(이 공사 입력) — SK 통째 빈칸 안 됨
- Bill_Hopyo(707-7 공사 통째 불러오기) = 이 공사 엑셀 수량을 굳힌 손으로 남김(723-1 `with_pills` 수량 걷기는 M02 저장용)
- Daega_Personal 원천 = 블록 origin 참조(덮개 열쇠 꼬리 무관) · 내 자료집 저장 알림 글 = 줄 수량 저장 안 함
- 비고(펼침 · 일위대가표) = 수량 없는 SC 안 줄은 「산근 n호표 · 수량 없음 — 이 공사에서 입력」(`noteOf`)
- 낡은 시험 고침(사유 = 723-1 SC 안 줄 수량 저장 안 함 · 이 공사 입력 · 알약으로 이음):
  test_204_2(서고 SC 수량 없음 · ask) · test_703(자료집 저장 손 걷힘) · test_719_1(서고 사본 = 알약 값으로 같은 값 · 없으면 수량 없음) ·
  test_719_2(SK 속 SC = 빈 줄 → 이 공사 입력 1 = M02 알약 1 금액) · test_702_3(서고 SC 빈 줄 3 → 입력 뒤 금액)
- test_723_4 = 구조물 사본 면적 알약 굳힘 · 설계 연동 · 알약 안 이음 = 빈 줄 시험 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 22:25:30 +09:00

307 lines
15 KiB
Python

"""B09 「내 자료집에 저장」(PLAN 703-2) — 펼친 일위대가(대가 블록) → 개인 층 사전조합(SC).
담는 것 = 담은로직(넣을 값 · 고른 자재 얹음) · 자재 · 줄 수량(= 수량산출 수기 `손`) —
맨 위 줄 수량 덮개 = 손(원래 손 × 덮개 ÷ 블록 줄 수량 · 대가 줄 ÷ 배를 되돌림) ·
SC 쓸 때 손 걷힘(723-1 `refs.no_qty` · 쓰는 곳 알약 · 이 공사 입력으로 이음)
안 담는 것 = 품 덮개(부른 블록 안 줄 · 품셈 블록 줄 — 로직 통째 배수로 바꾸면 다른 줄까지 바뀜) ·
단가 덮개(사전조합 꼴에 단가 칸 없음) → 수만 알림(품은 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)
rid = (block.get("origin") or {}).get("참조") or rest # 덮개 `C:참조|쓰는 곳`(723-4) = 참조만
if head == "C" and (doc := _frozen(root, rid)) 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)