Files
Aislo/resources/tester/test_719_2_sk_project.py
T
eomsangdonandClaude Opus 5.5 ccabbad6fb feat(719-2): 공사 쪽 M02 단가산출(SK) — B09 대가 블록 · 산근 · [업데이트] · 내 자료집 SK
- 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>
2026-11-01 16:09:53 +09:00

268 lines
13 KiB
Python

"""719-2 공사 쪽 M02 단가산출(SK) — B09 대가 블록 · 산근 · [업데이트] · 내 자료집 SK(임시 storage ·
실제 M01 읽기만).
- 굳힌 SK = 품셈 꼴 대가 블록(`Labor_Source.sangeun`) · 금액 = M02 SK 셈 · 단가산출 속 일위대가 줄 =
사전조합 블록을 부르는 줄 · 로직 더한줄의 하위 로직 = 산근 목록(`Sangeun.rows_of`)
- [업데이트] = SK 고친 뒤 다시 굳힘 — 굳힌 키 · cover · cid · 입력값 · 고른자재 · B09 덮개 그대로 ·
덮개 밑 값(base) = 새 단가산출 값 · 서고 사본(`sangeun`)도 원본판 다르면 다시 굳힘 ·
미리 보기 SK 줄
- 내 자료집 = SK 블록 · 품셈 블록 → 개인 층 SK(B09 덮개 = SK 덮개) → 서고로 다시 불러와 같은 금액
"""
# ruff: noqa: F811 — 임시 층 fixture `world` 를 들여 씀
import json
import pytest
from B09_Estimation import B09_Estimation_Bill_Library as library
from B09_Estimation import B09_Estimation_Daega_Personal as personal
from B09_Estimation import B09_Estimation_Prices as prices
from B09_Estimation import B09_Estimation_Refresh as refresh
from B09_Estimation import B09_Estimation_Sangeun as sangeun
from B09_Estimation.B09_Estimation_Calc_Base import compute_base
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys
from M02_MasterTemplete import M02_Item_Copy as item_copy
from M02_MasterTemplete import M02_Item_Snapshot as snapshots
from M02_MasterTemplete import M02_Item_Source as source
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from resources.tester.test_145_2_b09_labor_calc import STONE, _item, _project, _snap
from resources.tester.test_715_sangeun_store import PCT, _calc, _sk
from resources.tester.test_m02_labor_ref import MINE, world # noqa: F401 — 같은 임시 층
TWO_MATS = "GC000565" # 재료 줄 둘(고른자재 열쇠)
COVER = {"labor:LB000036#0": {"qty": 0.5}}
PRICE = {"labor:LB000002#0": {"labor": 200000}}
OWNER = "7/42"
def _json(value):
return json.loads(json.dumps(value, ensure_ascii=False, default=str))
def _sc(world, lines):
with store.at(world["personal"]):
doc = {"이름": "속 일위대가", "단위": "㎡", "담은로직": lines}
return refs.create("precombo", doc)["이름"]
def _mine(world, full=False):
"""SK = 품 덮개 · 인력 더한줄 · 로직 더한줄(하위 산근) · full = 단가 덮개 + SC 더한줄."""
added = [
{"종류": "인력", "요소": "LB000002", "수량": 0.1},
{"종류": "로직", "로직": PCT, "입력값": {}, "수량": 0.5},
]
cover = COVER
if full:
sc = _sc(world, [{"로직": PCT, "수량": {"손": 1}}])
added.append({"참조": {**MINE, "키": sc}, "수량": 2}) # 단가산출 속 일위대가
cover = {**COVER, **PRICE}
return _sk(world["personal"], 바탕={"로직": PCT}, 덮개=cover, 더한줄=added)
def _engine_ready(world):
"""엔진 입력 덧칸(715-1b `단가` · `값묶음`)이 붙었나 — 아니면 M02 도 못 셈."""
key = _sk(
world["personal"], 이름="엔진", 바탕={"로직": PCT}, 덮개={"labor:LB000002#0": {"labor": 1}}
)
got = _calc(world["personal"], key)
return not got.get("까닭") and got["줄"][1]["단가"] == 1
def _b09(root, cover=None):
keys = linked_resource_keys(root)
inputs = prices.build_inputs(None, keys)
inputs["daega"] = daega_inputs(root)
base = compute_base(inputs, {})
sg = sangeun.build(root, base)
rows = {r["key"]: r for r in sg["sg_list"]}
out = compute_labor(inputs["daega"], base, rows, cover)
return inputs["daega"], {b["id"]: b for b in out["daega_blocks"]}, rows
def _lib(world, tmp_path, key, kind="sangeun"):
root = _project(tmp_path / "proj", [])
made = library.import_copy(root, kind, "personal", OWNER, key)
return root, made["폴더"]
def _set_cover(world, key, cover):
with store.at(world["personal"]):
now = store.read("sangeun", key)
store.write("sangeun", key, now["판"], {**now["문서"], "덮개": cover})
def _same_money(block, live):
assert block["sum"] == pytest.approx(float(live["1단위"]["계"]), abs=5) # 끝수(내림)만 다름
for part, name in (("labor", "노무비"), ("material", "재료비"), ("expense", "경비")):
assert block[part] == pytest.approx(float(live["1단위"][name]), abs=3)
def test_SK_대가_블록_금액_M02_같음_하위_산근(world, tmp_path):
key = _mine(world)
live = _calc(world["personal"], key)
assert not live.get("까닭") and live["1단위"]["계"] > 0
root, folder = _lib(world, tmp_path, key)
daega, done, sg_rows = _b09(root)
block = done[daega["libs"][folder]]
assert block["cover"].startswith("P:") and block["kind"] == "품셈"
lines = block["lines"]
assert all(ln.get("sk") for ln in lines)
assert lines[0]["kind"] == "labor" and lines[0]["qty"] == 0.5
assert lines[0]["poom"] == {"qty": 0.018, "단가": 233250} # M02 에서 고친 줄 = 품셈 값 곁
assert "poom" not in lines[1]
sub = next(ln for ln in lines if ln["kind"] == "sangeun")
assert sub["key"] in sg_rows and sub["key"].startswith(f"SG:{PCT}") and sub["qty"] == 0.5
_same_money(block, live)
def test_SK_단가_덮개_속_일위대가_펼침(world, tmp_path):
if not _engine_ready(world):
pytest.skip("715-1b 엔진 입력 덧칸(단가 · 값묶음) 뒤 — 지금은 M02 SK 셈도 못 함")
key = _mine(world, full=True)
live = _calc(world["personal"], key)
root, folder = _lib(world, tmp_path, key)
daega, done, _ = _b09(root)
block = done[daega["libs"][folder]]
lines = block["lines"]
assert lines[1]["kind"] == "fixed" and lines[1]["prices"]["labor"] == pytest.approx(200000)
sc_line = next(ln for ln in lines if ln["kind"] == "daega")
assert done[sc_line["key"]]["kind"] == "사전조합" and sc_line["qty"] == 2
_same_money(block, live)
def test_구조물_사본_SK_줄_업데이트_덮개_입력값_고른자재_굳힌키_그대로(
world, tmp_path, monkeypatch
):
key = _mine(world)
ref = {**MINE, "키": key}
with store.at(world["personal"]):
from M02_MasterTemplete import M02_Labor_Doc as labor
doc = labor.clean(
{
"열": "rv",
"담은로직": [
{"참조": ref, "수량": {"손": 2}},
{"로직": "GF000219", "입력값": {}, "수량": {"손": 1}},
{"로직": TWO_MATS, "수량": {"손": 1}},
],
}
)
def build(_item_): # 원본 층 → 사본 파일(구조물 원본 대신 이 일위대가 문서)
rows = source.labor_doc_rows(doc)
return _json(
{
source.LABOR_FILE: rows,
source.PRICE_FILE: source.price_bundle(rows, world["personal"]),
snapshots.VALUES_FILE: {},
}
)
monkeypatch.setattr(item_copy, "build_snapshot", build)
root = _project(tmp_path / "proj", [(100, "SS000901")])
folder = snapshots.structures_dir(root) / snapshots.folder_name("pipe@100.0")
folder.mkdir(parents=True)
values = {"GF000219": {k: {"값": v} for k, v in STONE.items()}}
picks = {TWO_MATS: {"1": "MT000001"}}
for name, data in {
**build(None),
snapshots.VALUES_FILE: values,
snapshots.PICKS_FILE: picks,
snapshots.MANIFEST_NAME: {"원본": {**_item("SS000901"), "경로": ["x"]}},
}.items():
(folder / name).write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
rid = refs.ref_id(ref)
frozen_key = json.loads((folder / source.LABOR_FILE).read_text("utf-8"))["참조사본"][rid]
frozen_key = frozen_key["바탕"]["로직"]
daega, _, _ = _b09(root)
combo = next(b for b in daega["blocks"] if b["id"] == daega["owners"]["pipe@100.0"])
sk_block = next(b for b in daega["blocks"] if b["id"] == combo["lines"][0]["key"])
cid = sk_block["lines"][0]["cid"]
cover = {sk_block["cover"]: {cid: {"qty": 0.9}}}
# SK 고침(품 0.5 → 0.7) → [재적용](업데이트) = 다시 풀어 굳힘
_set_cover(world, key, {**COVER, "labor:LB000036#0": {"qty": 0.7}})
item_copy.reapply(root, folder.name)
after = json.loads((folder / source.LABOR_FILE).read_text("utf-8"))["참조사본"][rid]
assert after["바탕"]["로직"] == frozen_key # 굳힌 키 그대로
assert (
json.loads((folder / snapshots.VALUES_FILE).read_text("utf-8"))["GF000219"]
== (values["GF000219"])
)
assert json.loads((folder / snapshots.PICKS_FILE).read_text("utf-8")) == picks
daega2, done, _ = _b09(root, cover)
block = next(b for b in done.values() if b["cover"] == sk_block["cover"])
line = block["lines"][0]
assert line["cid"] == cid and line["covered"] == ["qty"] and line["qty"] == 0.9
assert line["base"]["qty"] == 0.7 # 덮개 밑 = 새 단가산출 값(「단가산출 값으로」 되돌림 값)
assert daega2["owners"]["pipe@100.0"] in done
def test_서고_SK_업데이트_미리보기_다시_굳힘(world, tmp_path):
key = _mine(world)
root, folder = _lib(world, tmp_path, key)
assert library.changed(root) == []
before = daega_inputs(root)
cover_key = next(b for b in before["blocks"] if b["id"] == before["libs"][folder])["cover"]
_set_cover(world, key, {**COVER, "labor:LB000036#0": {"qty": 0.7}})
got = refresh.preview(root, None, [], None)
assert [(x["키"], x["판"]) for x in got["단가산출"]] == [(key, "바뀜")]
assert [c["폴더"] for c in got["사본"]] == [folder]
assert refresh.reapply_changed(root)["다시"] == ["내 단가산출"]
assert library.changed(root) == [] and [f.name for f, _ in library.copies(root)] == [folder]
after = daega_inputs(root)
block = next(b for b in after["blocks"] if b["id"] == after["libs"][folder])
assert block["cover"] == cover_key and block["lines"][0]["qty"] == 0.7
def test_내_자료집_SK_왕복_같은_금액(world, tmp_path):
key = _mine(world)
root, folder = _lib(world, tmp_path, key)
daega, _, _ = _b09(root)
block = next(b for b in daega["blocks"] if b["id"] == daega["libs"][folder])
first, second = block["lines"][0]["cid"], block["lines"][1]["cid"]
priced = {block["cover"]: {first: {"qty": 0.9}, second: {"labor": 210000}}}
made = personal.build(root, block["cover"], priced, "내 SK 2")
doc = made["문서"]
assert doc["종류"] == "단가산출" and doc["바탕"] == {"로직": PCT}
assert doc["덮개"]["labor:LB000036#0"] == {"qty": 0.9}
assert doc["덮개"]["labor:LB000002#0"] == {"labor": 210000}
assert doc["더한줄"][0]["요소"] == "LB000002"
assert made["담음"] == {"품": 1, "단가": 1} and made["안담음"] == {"품": 0, "단가": 0}
third = block["lines"][2]["cid"] # 더한줄 인력 = 그 더한줄 수량
cover = {block["cover"]: {first: {"qty": 0.9}, third: {"qty": 0.3}}}
doc = personal.build(root, block["cover"], cover, "내 SK 2")["문서"]
assert doc["더한줄"][0]["수량"] == 0.3 and doc["덮개"] == {"labor:LB000036#0": {"qty": 0.9}}
_, done, _ = _b09(root, cover)
want = done[daega["libs"][folder]]["sum"]
with store.at(world["personal"]):
new = personal.write(doc)["이름"]
with pytest.raises(store.StoreError) as same:
personal.write(doc)
assert same.value.status == 409 and same.value.detail["같은이름"] == new
assert new.startswith("SK-") and new != key
root2, folder2 = _lib(world, tmp_path / "b", new)
daega2, done2, _ = _b09(root2)
assert done2[daega2["libs"][folder2]]["sum"] == pytest.approx(want, abs=2)
def test_내_자료집_품셈_블록_SK(world, tmp_path):
root = _project(tmp_path / "proj", [(100, "SS000901")])
_snap(root, "pipe@100.0", "SS000901", [{"로직": PCT, "승격": True, "수량": {"손": 1}}], {})
daega = daega_inputs(root)
top = next(b for b in daega["blocks"] if b["id"] == daega["owners"]["pipe@100.0"])
block = next(b for b in daega["blocks"] if b["id"] == top["lines"][0]["key"])
assert block["kind"] == "품셈" and block["origin"]["로직"] == PCT
cover = {block["cover"]: {block["lines"][0]["cid"]: {"qty": 0.5}}}
doc = personal.build(root, block["cover"], cover, "내 해체")["문서"]
assert doc["바탕"] == {"로직": PCT} and doc["단위"] == "㎡"
assert doc["덮개"] == {"labor:LB000036#0": {"qty": 0.5}}
with store.at(world["personal"]):
new = personal.write(doc)["이름"]
plain = _sk(world["personal"], 이름="견줌", 바탕={"로직": PCT}, 덮개=doc["덮개"])
a, b = _calc(world["personal"], new), _calc(world["personal"], plain)
assert a["1단위"]["계"] == b["1단위"]["계"] > 0