Files
Aislo/resources/tester/test_719_2_sk_project.py
T
eomsangdonandClaude Opus 5.5 5d8d9a4fd3 test(841-1): B09 = M02 값 — 산근 · 대가 · 고정 줄 셈 다듬기 · 낡은 시험 정리 — C 걸음
코드
- 고정 줄 단가 = M02_Labor_Fixed.line(사본 묶음 · 이은 노임 · 자재 = 묶음 값) — B09 단가 책으로 다시 안 덮음
- 대가 · 산근 고친 칸 = 품만 실제로(단가 칸 덮기 줄 남던 흠 · 품 고친 뒤 줄 금액 다시 셈 흠 py · ts 짝)
- 산근 글 = 엔진 단가 셈 그대로 · 별도계상 재료 줄 끝 「· 별도계상 = 0.0」 · 계 줄 = 블록 합
- 노무비 · 중기목록 이름에서 규격 겹침 뗌 · TS 바탕 셈 이름 PARTS → MACHINE_PARTS(짝 시험이 TS 를 이어 붙일 때 겹침)

시험(고친 까닭 = 단가 원천이 B09 책 → M02 엔진 · 노임 · 기계 덮개 · 단가 고친 칸 · 중기사용료 걷음)
- 144 바탕 · 뼈대: 운전사 · 중기사용료 · 경비 · 덮개 시험 걷음 → 노무 · 중기 나열 · 사급 표시 · 자재만 책
- 145-1 · 145-2 · 155 · 157-1 · 166-2 · 166-6 · 719-3 · 719-4 · 723-4 · 778-4 · 792-3: 줄 단가 = own(엔진) · 산근 = Solver() · collect 새 부름 꼴
- 702 · 705-1 · 719-2: 단가 칸 덮개 = 안 읽음 · 고정 줄 = M02 셈(노임 새 판 따라가기 시험 걷음)
- 203-1 · 296 · 703-3 · 795-4: 단가 책 = 자재 줄로
- 707 · 707-6 · 707-7: 단가표 불러오기 = 자재만 · 거창 이은 안내판 줄 = M02 사본 셈(엑셀 −50,000)
- 201-1: 환율 · 중기사용료 표 걷음 · b09 저장 속도: 산근 앞 풀이 그대로(단가 표 같음 견줌 걷음)
- 대상 113 파일 764 통과 · 25 실패 = 원래 실패(formatMoney · formatPrice 없음 · 단위 글 ㎜ · sec · 719-1 · 796-2e · 717-4 · 721 · 311 · 243 · 246)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-15 16:57:01 +09:00

276 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)
# 740-1 — 속 일위대가 = 넣을 때 펼쳐 담음(SC 로직 줄 = 로직 더한줄 · 본뜬) → 빠진 줄 없음
live = _calc(world["personal"], key)
added = _added(world, key)
assert live["빠진줄"] == [] and not any("참조" in x for x in added)
assert added[-1]["로직"] == PCT and added[-1]["본뜬"]["키"].startswith("SC-")
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)
assert block["blank"] == 0
_same_money(block, live)
def _added(world, key):
with store.at(world["personal"]):
return store.read("sangeun", key)["문서"]["더한줄"]
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 "labor:LB000002#0" not in doc["덮개"] # 841-1 단가 칸 = 안 읽음 · 안 담음
assert doc["더한줄"][0]["요소"] == "LB000002"
assert made["담음"]["품"] == 1 and made["담음"]["단가"] == 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