- 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>
268 lines
13 KiB
Python
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
|