Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -54,6 +54,7 @@ from M02_MasterTemplete.M02_Item_Source import (
|
||||
labor_doc_rows,
|
||||
price_bundle,
|
||||
read_json,
|
||||
version_of,
|
||||
)
|
||||
|
||||
LIBRARY = "library"
|
||||
@@ -100,6 +101,12 @@ def _precombo_files(layer: str, owner: str | None, key: str) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def source_version(files: dict[str, Any]) -> str:
|
||||
"""서고 사본 원본판(719-1) = 일위대가.json 지문 — 굳힌 SC · SK 문서 + 풀어 굳힌 SK 줄(바탕
|
||||
M01 줄 · 바탕판) · 단가는 뺌(단가 바뀜은 [업데이트] 단가 굳힘 몫) · 값 덮기 전에 셈."""
|
||||
return version_of(files.get(LABOR_FILE))
|
||||
|
||||
|
||||
def _build(kind: str, layer: str, owner: str | None, key: str) -> tuple[dict, dict]:
|
||||
"""서고 원본 → (사본 파일들, 원본 표지)."""
|
||||
if kind not in KINDS:
|
||||
@@ -247,7 +254,7 @@ def import_copy(
|
||||
if isinstance(read_json(library_dir(project_root) / name / MANIFEST_NAME), dict):
|
||||
return {"폴더": name, "만듦": False}
|
||||
manifest = {"종류": kind, "원본": origin, "가져온날": now_text(), "가져온이": who}
|
||||
_write(project_root, name, files, manifest, values)
|
||||
_write(project_root, name, files, {**manifest, "원본판": source_version(files)}, values)
|
||||
return {"폴더": name, "만듦": True}
|
||||
|
||||
|
||||
@@ -293,7 +300,8 @@ def resolve(project_root: str | Path, ref: dict[str, Any], dist: Any) -> str | N
|
||||
got = _build(kind, layer, owner, key)
|
||||
except (LookupError, ValueError):
|
||||
return None
|
||||
got = (got[0], {"종류": kind, "원본": got[1], "가져온날": now_text(), "가져온이": None})
|
||||
made = {"종류": kind, "원본": got[1], "가져온날": now_text(), "가져온이": None}
|
||||
got = (got[0], {**made, "원본판": source_version(got[0])})
|
||||
files, manifest = got
|
||||
values = haul_values(lines_of(files), ref.get("given"), ref.get("own"), dist)
|
||||
name = base + (f"~{tag(values)}" if values else "")
|
||||
|
||||
@@ -44,7 +44,7 @@ def _texts(row: dict) -> list[str]:
|
||||
out.append(row["결과"]["식"])
|
||||
for item in row.get("호표", []):
|
||||
out.append(item["수량"])
|
||||
if not isinstance(item.get("요소"), dict):
|
||||
if isinstance(item.get("요소"), str): # 단가산출 참조 · 고정 더한줄 = 요소 없음(719-1)
|
||||
out.append(item["요소"])
|
||||
out += [extra["식"] for extra in row.get("덧줄", [])]
|
||||
return out
|
||||
@@ -200,6 +200,10 @@ class _Collector:
|
||||
return
|
||||
self.logics.add(key)
|
||||
self.keys.add(key)
|
||||
self.row(row)
|
||||
|
||||
def row(self, row: dict) -> None:
|
||||
"""로직 줄 하나가 닿는 것 — 마스터 로직 · 굳힌 단가산출 줄(719-1 · 마스터 밖 줄)."""
|
||||
if isinstance(row.get("흐름"), dict): # 흐름 로직 — 옛 칸 대신 흐름이 쓰는 것을 모음
|
||||
self.flow(row)
|
||||
return
|
||||
@@ -221,12 +225,17 @@ class _Collector:
|
||||
self.pick(row, item["요소"], item.get("단위"))
|
||||
|
||||
|
||||
def collect(logic_keys: list[str], materials: list[str] | None = None) -> dict[str, Any]:
|
||||
"""담은 로직 + 자재(MT)의 닫힌 단가 묶음 — `{파일 이름: 파일(줄은 모은 것만 · 차례 그대로)}`."""
|
||||
def collect(
|
||||
logic_keys: list[str], materials: list[str] | None = None, extra: list[dict] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""담은 로직 + 자재(MT)의 닫힌 단가 묶음 — `{파일 이름: 파일(줄은 모은 것만 · 차례 그대로)}` ·
|
||||
`extra` = 마스터 밖 로직 줄(굳힌 단가산출 719-1) — 그 줄이 닿는 것만 모음(줄은 안 담음)."""
|
||||
files, whole = master()
|
||||
collector = _Collector(whole)
|
||||
for key in logic_keys:
|
||||
collector.logic(key)
|
||||
for row in extra or []:
|
||||
collector.row(row)
|
||||
for code in materials or []:
|
||||
collector.add(code)
|
||||
out: dict[str, Any] = {}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""프로젝트 사본 — M02 단가산출(SK) 풀어 굳힘(PLAN 719-1 · 요구서 내역12 원칙 3 · 4장 14).
|
||||
|
||||
사본을 만들 때(구조물 사본 `M02_Item_Source.build_source` · B09 서고 사본 `Bill_Library`) 굳힌
|
||||
참조사본 안 SK 문서마다 고친 줄(`M02_Sangeun_Calc.resolve` = 바탕 사슬 + 덮개 · 더한줄 · 뺀줄)을
|
||||
풀어 사본 `단가.json` 로직 줄 하나로 굳힘 — 사본 SK 문서는 그 줄만 바탕으로 가리킴(덮개 0 ·
|
||||
연결 끊김). 셈은 그대로 `M02_Sangeun_Calc` 한 벌(사본 색인 = 사본 `단가.json`).
|
||||
|
||||
굳힌 키 = SK 참조 이름표(`ref_id` = 층 · 주인 · 키)에서 정해지는 `GX9#####`(해시 · 겹치면
|
||||
이름표 차례로 다음 번호) — 같은 SK = 같은 굳힌 키([업데이트] 때 B09 덮개 cover · cid · 입력값 ·
|
||||
고른자재가 짝을 안 잃음). 원본 층 문서는 읽기만.
|
||||
사본 SK 문서 = {종류 · 키 · 이름 · 단위 · 구분 · 상세구분, 바탕 {로직: 굳힌 키}, 입력값(사슬 합),
|
||||
열린입력, 켜쌓기율, 고름 · 자재표시(굳힌 줄 cid), 덮개 {}, 더한줄 [], 뺀줄 [],
|
||||
굳힌줄(= 단가.json 줄), 원본(원래 SK 문서 · 「M02 에서 고침」 근거), 바탕판, 짝잃음}.
|
||||
단가산출 속 일위대가(SC) · SK 더한줄 = 참조 줄 그대로 → 사본 셈이 굳힌 참조사본으로 펼침.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
|
||||
#: 사본 `단가.json` 안 굳힌 SK 줄 파일(`master_formula.Master` 가 그룹 「로직」 으로 읽음)
|
||||
FILE = "로직_단가산출.json"
|
||||
ROW = "굳힌줄"
|
||||
_HEAD, _LOW, _SPAN = "GX", 900000, 99999 # GX900000 ~ GX999998(999999 = M01 저장 전 초안)
|
||||
|
||||
|
||||
def frozen_key(rid: str, taken: set[str]) -> str:
|
||||
"""SK 이름표 → 굳힌 로직 키 — 해시 자리 · 이미 쓴 키면 다음 번호(제자리 `taken` 에 더함)."""
|
||||
at = int(hashlib.sha256(rid.encode("utf-8")).hexdigest(), 16) % _SPAN
|
||||
while (key := f"{_HEAD}{_LOW + at:06d}") in taken:
|
||||
at = (at + 1) % _SPAN
|
||||
taken.add(key)
|
||||
return key
|
||||
|
||||
|
||||
def is_sangeun(doc: Any) -> bool:
|
||||
return isinstance(doc, dict) and doc.get("종류") == sd.TITLE
|
||||
|
||||
|
||||
def freeze(copies: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
||||
"""참조사본 `{ref_id: 문서}` → SK 를 풀어 굳힌 참조사본(새 묶음 · 받은 것 안 고침) ·
|
||||
못 푼 SK(없는 바탕 · 순환) = 그대로(사본 셈이 까닭을 냄)."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Item_Prices as prices
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
sks = sorted(rid for rid, doc in copies.items() if is_sangeun(doc))
|
||||
if not sks:
|
||||
return copies
|
||||
whole = m01.loaded()[1]
|
||||
|
||||
def read(ref: dict) -> dict | None:
|
||||
return copies.get(refs.ref_id(ref))
|
||||
|
||||
out, taken = dict(copies), set()
|
||||
for rid in sks:
|
||||
doc = copies[rid]
|
||||
try:
|
||||
got = calc.resolve(doc, whole, read, (rid,))
|
||||
except sd.DocError:
|
||||
continue
|
||||
key = frozen_key(rid, taken)
|
||||
row = prices.to_json({**got["줄"], "키": key})
|
||||
ids = sd.line_ids(row)
|
||||
frozen = {k: doc.get(k) for k in ("종류", "키", "이름", "단위", "구분", "상세구분")}
|
||||
frozen |= {
|
||||
"바탕": {"로직": key},
|
||||
"입력값": prices.to_json(got["입력값"]),
|
||||
"열린입력": list(doc.get("열린입력") or []),
|
||||
"고름": {ids[int(n)]: mt for n, mt in got["고름"].items()},
|
||||
"자재표시": {ids[int(n)]: one for n, one in got["자재표시"].items()},
|
||||
"덮개": {},
|
||||
"더한줄": [],
|
||||
"뺀줄": [],
|
||||
ROW: row,
|
||||
"원본": doc,
|
||||
"바탕판": got["바탕판"],
|
||||
"짝잃음": got["짝잃음"],
|
||||
}
|
||||
if got["켜쌓기율"] is not None:
|
||||
frozen["켜쌓기율"] = prices.to_json(got["켜쌓기율"])
|
||||
out[rid] = frozen
|
||||
return out
|
||||
|
||||
|
||||
def rows(copies: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""굳힌 SK 줄 — 사본 `단가.json` `FILE` 에 넣음(이름표 차례)."""
|
||||
return [doc[ROW] for _, doc in sorted(copies.items()) if is_sangeun(doc) and ROW in doc]
|
||||
|
||||
|
||||
def materials(copies: dict[str, Any]) -> list[str]:
|
||||
"""굳힌 SK 고른 자재 키 — 단가 묶음에 담음."""
|
||||
return [
|
||||
str(mt)
|
||||
for doc in copies.values()
|
||||
if is_sangeun(doc)
|
||||
for mt in (doc.get("고름") or {}).values()
|
||||
]
|
||||
@@ -5,7 +5,7 @@
|
||||
`일위대가.json` = 층 `labor/<열id>.json` 문서(39-1c · 없으면 옛 UA 조합 줄 ·
|
||||
규격 열 = summary_item `spec` 열 수량을 손으로 굳힘 · 704-4)
|
||||
+ 담은 로직 줄(M01 시스템) + 알약(가져온 때 그대로) +
|
||||
참조사본(단가승격 · 사전조합을 값으로 굳힘) ·
|
||||
참조사본(단가승격 · 사전조합을 값으로 굳힘 · 단가산출 SK = 풀어 굳힌 로직 줄 719-1) ·
|
||||
`단가.json` 계산에 닿는 마스터 줄 전부(담은 로직 + 일위대가 자재 MT)
|
||||
`원본판` = 모은 내용의 해시 — 지금 층 · 마스터에서 다시 모은 판과 대어 「마스터가 바뀜」 을 앎.
|
||||
프로젝트가 고치는 값(`입력값.json` · `고른자재.json`)은 원본이 아니라 여기서 안 모음.
|
||||
@@ -93,11 +93,12 @@ def labor_rows(ua_key: str) -> dict[str, Any] | None:
|
||||
def labor_doc_rows(doc: dict[str, Any]) -> dict[str, Any]:
|
||||
"""일위대가 문서(39-1a) + 담은 로직 줄 + 알약(`M02_Labor_Doc.pills` · 가져온 때 그대로 —
|
||||
복사본 계산이 마스터를 안 읽게) + 참조사본 `{ref_id: 문서}`(50-3 · 원본이 바뀌어도 그대로)."""
|
||||
from M02_MasterTemplete import M02_Item_Sangeun as sk
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
rows = {**logic_rows(doc), "알약": labor_doc.pills(doc)}
|
||||
copies = refs.frozen(doc)
|
||||
copies = sk.freeze(refs.frozen(doc)) # 단가산출(SK) = 풀어 굳힌 로직 줄(719-1)
|
||||
return {**rows, "참조사본": copies} if copies else rows
|
||||
|
||||
|
||||
@@ -162,11 +163,14 @@ def price_bundle(labor: dict[str, Any] | None, root: Path) -> dict[str, Any]:
|
||||
"""사본 단가 묶음(`단가.json`) — 담은 로직 · 굳힌 참조 로직 · 자재 · 고른 자재 · 사전조합 자재 ·
|
||||
개인 자재(`root` = 원본 층 · B09 설계내역서 서고 사본도 이 길 · 204-2)."""
|
||||
from M02_MasterTemplete import M02_Item_Prices as prices
|
||||
from M02_MasterTemplete import M02_Item_Sangeun as sk
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
copies = (labor or {}).get("참조사본") or {}
|
||||
sk_rows = sk.rows(copies) # 굳힌 단가산출 줄(719-1) — 그 줄이 닿는 마스터 줄도 모음
|
||||
logic_keys = [str(one.get("키")) for one in (labor or {}).get("로직", [])]
|
||||
logic_keys += refs.frozen_logics((labor or {}).get("참조사본") or {}) # 굳힌 단가승격 로직
|
||||
logic_keys += refs.frozen_logics(copies) # 굳힌 단가승격 로직
|
||||
doc = (labor or {}).get("줄") or {}
|
||||
materials = [str(m.get("코드")) for m in doc.get("자재") or []]
|
||||
materials += [ # 고른 로직 자재(103-1) 품목 줄도 — 복사본 계산이 그 단가를 씀
|
||||
@@ -174,8 +178,13 @@ def price_bundle(labor: dict[str, Any] | None, root: Path) -> dict[str, Any]:
|
||||
]
|
||||
materials += refs.frozen_materials((labor or {}).get("참조사본") or {}) # 175-2 사전조합 자재
|
||||
materials += fixed.link_keys(doc) # 705-1 고정 줄이 잇는 노임 · 자재 · 기계
|
||||
materials += sk.materials(copies)
|
||||
wanted = logic_keys + [code for code in materials if code]
|
||||
bundle = prices.to_json(prices.collect(logic_keys, materials)) if wanted else {}
|
||||
bundle = (
|
||||
prices.to_json(prices.collect(logic_keys, materials, sk_rows)) if wanted or sk_rows else {}
|
||||
)
|
||||
if sk_rows:
|
||||
bundle[sk.FILE] = {"그룹": "로직", "원문": "자체", "줄": sk_rows}
|
||||
if mine := own.bundle(
|
||||
[c for c in materials if own.is_own(c)], root
|
||||
): # 개인 자재(148-3 · 사전조합 것도 175-2) — 원본 목록이 바뀌어도 사본 그대로
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""719-1 프로젝트 사본 — M02 단가산출(SK) 풀어 굳힘(`M02_Item_Sangeun` · `M02_Item_Source` ·
|
||||
`M02_Item_Prices` · `B09_Estimation_Bill_Library`) — 임시 storage(견본 층 · 실제 M01 읽기만).
|
||||
|
||||
구조물 사본 · B09 서고 사본 셈 = M02 층 SK 셈 · 굳힌 줄은 사본 `단가.json` 로직 줄 · 원본 SK 를
|
||||
지워도 같은 값 · 같은 SK = 같은 굳힌 키(다시 복사 · SK 고친 뒤도) · 원본 층 지문 그대로 ·
|
||||
서고 manifest 원본판 = SK 바뀌면 다름.
|
||||
"""
|
||||
|
||||
# ruff: noqa: F811 — 임시 층 fixture `world` 를 들여 씀
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from B09_Estimation import B09_Estimation_Bill_Library as library
|
||||
from M02_MasterTemplete import M02_Item_Prices as prices
|
||||
from M02_MasterTemplete import M02_Item_Sangeun as sk
|
||||
from M02_MasterTemplete import M02_Item_Source as source
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from resources.tester.test_715_sangeun_store import PCT, _sk
|
||||
from resources.tester.test_m02_labor_ref import MINE, world # noqa: F401 — 같은 임시 층
|
||||
|
||||
COVER = {"labor:LB000036#0": {"qty": 0.5}}
|
||||
ADD = [{"종류": "인력", "요소": "LB000002", "수량": 0.1}]
|
||||
|
||||
|
||||
def _print(folder):
|
||||
"""층 폴더 지문 — 파일 이름 · 바이트."""
|
||||
out = hashlib.sha256()
|
||||
for path in sorted(p for p in folder.rglob("*") if p.is_file()):
|
||||
out.update(str(path.relative_to(folder)).encode() + path.read_bytes())
|
||||
return out.hexdigest()
|
||||
|
||||
|
||||
def _top(ref, qty=2):
|
||||
with store.at(ref["folder"]):
|
||||
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref["ref"], "수량": {"손": qty}}]})
|
||||
return doc, labor.calc(doc, {})["줄"][0]
|
||||
|
||||
|
||||
def _mine(world):
|
||||
key = _sk(world["personal"], 바탕={"로직": PCT}, 덮개=COVER, 더한줄=ADD)
|
||||
return key, {"folder": world["personal"], "ref": {**MINE, "키": key}}
|
||||
|
||||
|
||||
def test_구조물_사본_굳힌_줄_값_같음_원본_지워도(world):
|
||||
key, ref = _mine(world)
|
||||
doc, live = _top(ref)
|
||||
assert not live["까닭"] and live["기준"]["계"] > 0
|
||||
before = _print(world["personal"])
|
||||
rows = source.labor_doc_rows(doc)
|
||||
bundle = source.price_bundle(rows, world["personal"])
|
||||
assert _print(world["personal"]) == before # 원본 층 안 건드림
|
||||
rid = refs.ref_id(ref["ref"])
|
||||
frozen = rows["참조사본"][rid]
|
||||
fk = frozen["바탕"]["로직"]
|
||||
assert fk == sk.frozen_key(rid, set()) and fk.startswith("GX9")
|
||||
assert frozen["덮개"] == {} and frozen["더한줄"] == [] and frozen["뺀줄"] == []
|
||||
assert frozen["원본"]["덮개"]["labor:LB000036#0"]["qty"] == 0.5 # 고친 근거
|
||||
assert [r["키"] for r in bundle[sk.FILE]["줄"]] == [fk]
|
||||
json.dumps(rows["참조사본"], ensure_ascii=False) # 사본 파일로 쓸 수 있는 꼴(Decimal 없음)
|
||||
(world["personal"] / "sangeun" / f"{key}.json").unlink() # 원본이 사라져도 사본만 봄
|
||||
got = prices.calc(rows, json.loads(json.dumps(bundle, ensure_ascii=False)), {}, {}, None)
|
||||
line = got["줄"][0]
|
||||
assert not line["까닭"]
|
||||
for part in ("노무비", "재료비", "경비", "계"):
|
||||
assert line["기준"][part] == pytest.approx(live["기준"][part], rel=1e-9)
|
||||
assert line["반영"]["계"] == pytest.approx(live["반영"]["계"], rel=1e-9)
|
||||
|
||||
|
||||
def test_같은_SK_같은_굳힌_키_다른_SK_다른_키(world):
|
||||
key, ref = _mine(world)
|
||||
doc, _ = _top(ref)
|
||||
rid = refs.ref_id(ref["ref"])
|
||||
first = source.labor_doc_rows(doc)["참조사본"][rid]["바탕"]["로직"]
|
||||
with store.at(world["personal"]): # SK 를 고쳐도 굳힌 키 그대로(B09 덮개 짝 지킴)
|
||||
now = store.read("sangeun", key)
|
||||
store.write("sangeun", key, now["판"], {**now["문서"], "덮개": {}})
|
||||
again = source.labor_doc_rows(doc)["참조사본"][rid]
|
||||
assert again["바탕"]["로직"] == first and again["원본"]["덮개"] == {}
|
||||
other = refs.ref_id({"층": "company", "주인": "7", "키": key}) # 같은 번호 다른 층
|
||||
assert sk.frozen_key(other, set()) != first
|
||||
taken = {first}
|
||||
assert sk.frozen_key(rid, taken) != first and len(taken) == 2 # 겹치면 다음 번호
|
||||
|
||||
|
||||
def test_B09_서고_사본_값_같음_원본판(world, tmp_path):
|
||||
_, ref = _mine(world)
|
||||
with store.at(world["personal"]):
|
||||
sc = refs.create(
|
||||
"precombo",
|
||||
{"이름": "조합", "단위": "㎡", "담은로직": [{"참조": ref["ref"], "수량": {"손": 2}}]},
|
||||
)["이름"]
|
||||
_, live = _top({"folder": world["personal"], "ref": {**MINE, "키": sc}}, 1)
|
||||
before = _print(world["personal"])
|
||||
root = tmp_path / "proj"
|
||||
made = library.import_copy(root, "precombo", "personal", "7/42", sc)
|
||||
assert made["만듦"] and _print(world["personal"]) == before
|
||||
folder = library.library_dir(root) / made["폴더"]
|
||||
read = {p.name: json.loads(p.read_text(encoding="utf-8")) for p in folder.glob("*.json")}
|
||||
got = prices.calc(read[source.LABOR_FILE], read[source.PRICE_FILE], {}, {}, None)["줄"][0]
|
||||
assert not got["까닭"]
|
||||
assert got["기준"]["계"] == pytest.approx(live["기준"]["계"], rel=1e-9)
|
||||
mark = read["manifest.json"]["원본판"]
|
||||
built = library._build("precombo", "personal", "7/42", sc)[0]
|
||||
assert library.source_version(built) == mark # 다시 모아도 같은 판
|
||||
again = library.import_copy(root, "precombo", "personal", "7/42", sc)
|
||||
assert again == {"폴더": made["폴더"], "만듦": False}
|
||||
with store.at(world["personal"]): # SK 고침 → 원본판 다름
|
||||
now = store.read("sangeun", ref["ref"]["키"])
|
||||
store.write("sangeun", ref["ref"]["키"], now["판"], {**now["문서"], "덮개": {}})
|
||||
assert library.source_version(library._build("precombo", "personal", "7/42", sc)[0]) != mark
|
||||
|
||||
|
||||
def test_SK_없는_사본_그대로():
|
||||
"""SK 없는 참조사본 = 받은 묶음 그대로(기존 사본 · 공사 금액 전후 같음)."""
|
||||
copies = {"x": {"종류": "사전조합", "담은로직": []}}
|
||||
assert sk.freeze(copies) is copies and sk.rows(copies) == [] and sk.materials(copies) == []
|
||||
Reference in New Issue
Block a user