Files
Aislo/B09_Estimation/B09_Estimation_Linked.py
eomsangdonandClaude Opus 5.5 3e1dc294b2 feat(795-4): B09 사본 규칙 — 만든 때 그대로 · [단가 갱신] 값만 · [재생성] 따로 · 시점 섞임 고침
- 구조물 · 서고 사본 단가 묶음 = M02 일위대가 · SC · SK 로직 사본 줄 먼저(`collect(over)`) · M01 에서 지워진 키 = `사본_남은줄.json`
- 단가 책 새 줄([저장] · 첫 굳힘) = 연결 사본 `단가.json` 자원 줄(구조물 지정 때 값 · `Linked.frozen_rows`) · 없는 자원만 지금 M01 + 책 `now_keys` · 입력 `now` · 알림 한 줄
- POST …/estimation/prices 기본 = 단가 갱신(값만 · 사본 재적용 없음) · job 재생성 = 옛 [업데이트] · 미리 보기 단가 갱신 = 바뀐 자원 · 총괄 · 대가 합 전 → 후
- B09 화면 단추 [업데이트] → [단가 갱신] · 미리 보기 창 [재생성] → 재생성 미리 보기 → [적용] · 안내 글 둘 이름 맞춤
- 옮기기 도구(보기만) — 프로젝트 층 못셈 14 = 「못셈(옛 SP 참조)」 갈래 · B09 구조물 · 서고 사본 대조(그대로 · 재생성 금액)
- 새 시험 test_795_4_b09_copy(8) · 낡은 시험 고침: test_203_1 · test_296 = 단추 이름 「업데이트」 · 호버 글 · 몸 `{ user }` 를 박아 둔 것 → 795-4 이름 · job 덧칸으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:54:10 +09:00

132 lines
6.3 KiB
Python

"""B09 연결된 사본 → 원가 수요 자원 키(PLAN 144 · 143-1 원천) — 서버 단독 · 읽기만.
연결된 사본 = `M02_Item_Snapshot.linked_snapshots`(지정 ∩ 같은 항목 사본 · B08 · B09 공용).
사본마다 `일위대가.json` 담은 로직을 그 사본 단가 묶음(`단가.json`) · 입력값 · 고른자재로 돌리며
실제로 부른 자원 줄(인력 LB · 기계 EQ · 자재 MT · 유가 MO)만 모음 — 단가 묶음 통째
(고르기 후보 전부)는 안 씀 · 값은 쓰지 않음(단가는 원가 한 벌 `price_book.json` 이 맡음) ·
사본 일위대가 자재 · 굳힌 사전조합 자재 코드 · 고정 줄 이음 키도(183-1 · 705-1 · 로직을 안 돌아 `get` 에 안 걸림).
굳힌 SK(719-2) = 굳힌 키 로직 줄로 돌림(사본 `단가.json` · M01 안 읽음).
고르기가 훑은 후보 줄도 걸림 → 원가 길이 두 번 셈해 쓰는 줄만 남김(`Summary.used_materials`).
구조물 조합 사본 없으면 그 구조물은 없음 · 일위대가 없는 사본은 자원 없음.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
from M02_MasterTemplete import M02_Item_Prices as item_prices
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
from M02_MasterTemplete import M02_Labor_Fixed as fixed
from M02_MasterTemplete.M02_Item_Snapshot import (
PICKS_FILE,
VALUES_FILE,
linked_snapshots,
structures_dir,
)
from M02_MasterTemplete.M02_Item_Source import LABOR_FILE, PRICE_FILE, read_json
#: 원가 단가 목록에 오르는 자원 표
RESOURCE_TABLES = ("LB", "EQ", "MT", "MO")
mf = item_prices.mf
def used_keys(folder: Path) -> list[str]:
"""사본 하나가 담은 로직을 돌릴 때 부른 자원 키 — 부른 차례 · 겹침 없음."""
labor = read_json(folder / LABOR_FILE)
if not isinstance(labor, dict):
return []
snapshot = item_prices.load_prices(read_json(folder / PRICE_FILE) or {})
values = read_json(folder / VALUES_FILE) or {}
picks = read_json(folder / PICKS_FILE) or {}
seen: list[str] = []
base_get = snapshot.get
def get(ref: str) -> dict[str, Any]:
row = base_get(ref)
key = str(row.get("키") or "")
if key[:2] in RESOURCE_TABLES and key not in seen:
seen.append(key)
return row
snapshot.get = get # 이 사본 묶음 한 벌에만 — 마스터 캐시는 안 건드림
# 사본 셈 한 벌(142-1)과 같은 길 — 빈 입력 = 자동값 · 켜쌓기율 · 고름 ·
# 굳힌 참조사본 안 로직까지
doc = item_prices.with_values(labor.get("줄") or {}, values, picks)
inner = [d for d in (labor.get("참조사본") or {}).values() if isinstance(d, dict)]
sks = [_sk_line(d) for d in inner if d.get("종류") == "단가산출" and "굳힌줄" in d]
lines = [*(doc.get("담은로직") or []), *(x for d in inner for x in d.get("담은로직") or [])]
for one in [*lines, *sks]:
# 못 돈 로직 — 부른 데까지만(까닭은 안 봄)
if isinstance(one, dict) and one.get("로직"):
labor_doc.run_line(
snapshot, one["로직"], one.get("입력값"), one.get("켜쌓기율"), one.get("고름")
)
mats = [*(doc.get("자재") or []), *(m for d in inner for m in d.get("자재") or [])]
codes = [str(mat.get("코드") or "") if isinstance(mat, dict) else "" for mat in mats]
codes += [k for d in [doc, *inner] for k in fixed.link_keys(d)] # 705-1 고정 줄 이음
codes += [ # 굳힌 SK 고정 더한줄 이음(719-2)
k
for d in inner
for x in (d.get("굳힌줄") or {}).get("호표") or []
if isinstance(x.get("고정"), dict)
for k in fixed.link_keys({"담은로직": [{"고정": x["고정"]}]})
]
for code in codes:
if code[:2] in RESOURCE_TABLES and code not in seen:
seen.append(code)
return seen
def _sk_line(doc: dict[str, Any]) -> dict[str, Any]:
"""굳힌 SK(719-1) → 로직 줄 꼴 — 굳힌 키(사본 `단가.json` 로직) · 고름 cid → 차례."""
from M02_MasterTemplete.M02_Sangeun_Doc import line_ids
chosen = doc.get("고름") or {}
ids = line_ids(doc["굳힌줄"])
picks = {str(n): chosen[cid] for n, cid in enumerate(ids) if cid in chosen}
one = {"로직": (doc.get("바탕") or {}).get("로직"), "입력값": doc.get("입력값")}
return {**one, "켜쌓기율": doc.get("켜쌓기율"), "고름": picks or None}
def _folders(project_root: str | Path, keep: set[str] | None = None) -> list[Path]:
"""연결된 구조물 사본(구조물 id 차례) → 설계내역서 서고 사본(204-2 · `keep` 만)."""
from B09_Estimation.B09_Estimation_Bill_Library import copies
base = structures_dir(project_root)
folders = [
base / linked["폴더"]
for _owner, linked in sorted(linked_snapshots(project_root).items())
if "일위대가" in linked.get("부분", [])
]
return [*folders, *(f for f, _ in copies(project_root) if keep is None or f.name in keep)]
def frozen_rows(project_root: str | Path) -> dict[str, dict[str, Any]]:
"""사본 단가 기준 시점 자원 줄(795-4) — 연결 사본 `단가.json` 의 노임 · 자재 · 기계 · 유가전력 ·
환율 줄 `{키: 줄}`(만든 때 값 · 같은 키 = 앞 사본 것)."""
from M02_MasterTemplete.M02_Sangeun_Copy import RES
out: dict[str, dict[str, Any]] = {}
for folder in _folders(project_root):
bundle = read_json(folder / PRICE_FILE) or {}
for name, data in bundle.items():
if name.startswith("_") or not isinstance(data, dict):
continue
for row in data.get("줄") or data.get("표") or []:
if str(row.get("키"))[:2] in RES:
out.setdefault(str(row["키"]), row)
return out
def linked_resource_keys(project_root: str | Path, keep: set[str] | None = None) -> list[str]:
"""연결된 사본 전부가 부른 자원 키 — 구조물 id 차례 → 설계내역서 서고 사본(204-2) ·
겹침 없음 · `keep` = 화면 캐시 줄이 잇는 서고 사본만(296 · 지운 줄의 사본 뺌)."""
out: list[str] = []
for folder in _folders(project_root, keep):
for key in used_keys(folder):
if key not in out:
out.append(key)
return out