fix(m02): 일위대가 별도계상 자재 셈에서 뺌 · 원문 같은 책 절 모두 · 단위 까닭 시험(222-1)

- 로직 자재 별도계상(빈 칸 = 기본) = 기준에서 빼고 일위대가 상세 줄에서 지움 · 자재목록엔 남김
- 사용자 자재 별도계상 = 합에서 뺌(파이썬 · TS settle 짝 · 거울 시험)
- 사전조합 안 줄 · 프로젝트 사본 셈도 같은 길
- 별도계상 바꾸면 다시 셈(로직 자재 = 서버 · 사용자 자재 = 짝 셈)
- 원문 출처 = 로직 출처 + 호표 · 입력 출처 같은 책 절(산림품셈 12-1-2 · 12-1-3)
- 기존 시험 셋 = 직접계산 표시 · 대조용 apart 끔

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
2026-10-10 16:39:00 +09:00
co-authored by Claude Opus 5.5
parent 9085a51056
commit 9a850212d5
9 changed files with 230 additions and 14 deletions
+2 -1
View File
@@ -79,7 +79,8 @@ def _world_combo(world): # noqa: F811
{"로직": STONE, "수량": {"손": 1}},
{"로직": NET, "수량": {"손": 0}, "고름": {"4": NET_ITEM}},
]
mats = [{"코드": MAT, "수량": {"손": 2}}, {"코드": WIRE, "수량": None}]
# MAT = 직접계산(222-1 · 기본 별도계상이면 셈에서 빠짐)
mats = [{"코드": MAT, "수량": {"손": 2}, "별도계상": "직접계산"}, {"코드": WIRE, "수량": None}]
with store.at(world["system"]):
key = refs.create("precombo", {"이름": "자재 조합", "담은로직": lines, "자재": mats})[
"이름"
+148
View File
@@ -0,0 +1,148 @@
"""M02 구조물조합 일위대가 기계비빔타설(PLAN 222-1) — 단위 까닭 · 별도계상 빼기 · 원문 여럿.
① 단위 — GF000156 모래 · 자갈 = 원문 kg(QF000353) ÷ 단위중량(㎏/㎥) · 칸에 t/㎥ 값(2 · 2.1)을 넣으면
838 ÷ 2 = 419 ㎥ 처럼 1000 배 · 0 = 표 위끝(1,700 · 1,800 ㎏/㎥) → 0.49 · 0.57 ㎥ · 칸을 비우면
자동값(master_auto 「나누는 자리 = 1」)이 1 이라 838 ㎥(자료 · 기본값 몫 — 223 보고).
② 별도계상 — 로직 자재(`자재표시` · 빈 칸 = 기본 별도계상) = 기준에서 빠지고 `줄` 에서 지움 · `재료` 는
남음 · 사용자 자재 = 합에서 뺌(짝 TS `settle`) · 사전조합 안 줄 · 사본 셈 같은 길.
③ 원문 — 로직 출처 + 호표 · 입력 출처 가운데 같은 책 절 번호(「산림품셈 12-1-2 · 12-1-3」).
"""
import os
from decimal import Decimal
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M01_MasterData import M01_MasterData_Store_Make as make
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Inputs as inputs
from M02_MasterTemplete import M02_Labor_Ref as refs
from resources.tester.test_m02_labor_calc_mirror import _same, _ts
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "..", "..", "M02_MasterTemplete")
MIX = "GF000156" # 기계비빔타설 — 호표 3 시멘트 · 4 모래 · 5 자갈
GIVEN = {"재료배합": "(A)", "골재최대치수": 40}
MAT = "MT000008"
DIRECT = {"별도계상": "직접계산"}
def _whole():
return m01.loaded()[1]
def _rows(got):
return {x["이름"]: {**x, "수량": float(x["수량"])} for x in got["줄"]}
# ── ① 단위 ──────────────────────────────────────────────────────────
def test_단위중량_칸에_t_값이면_1000배_0이면_표_위끝():
wrong = labor.run_line(_whole(), MIX, {**GIVEN, "모래단위중량": 2, "자갈단위중량": 2.1})
rows = _rows(wrong)
assert rows["시멘트"]["단위"] == "kg" and rows["시멘트"]["수량"] == pytest.approx(335)
assert rows["모래(건조상태)"]["수량"] == pytest.approx(838 / 2) # 419 ㎥
assert rows["자갈 또는 부순돌"]["수량"] == pytest.approx(1032 / 2.1) # 491.4 ㎥
zero = {**GIVEN, "모래단위중량": 0, "자갈단위중량": 0}
right = _rows(labor.run_line(_whole(), MIX, zero))
assert right["모래(건조상태)"]["수량"] == pytest.approx(838 / 1700)
assert right["자갈 또는 부순돌"]["수량"] == pytest.approx(1032 / 1800)
# ── ② 별도계상 ───────────────────────────────────────────────────────
def _calc(shown=None, mats=()):
line = {"로직": MIX, "입력값": GIVEN, "수량": {"손": 1}}
if shown:
line["자재표시"] = shown
doc = labor.clean({"열": "rv", "담은로직": [line], "자재": list(mats)})
return labor.calc(doc, {})
def test_로직_자재_기본_별도계상은_기준_줄에서_빠지고_재료엔_남음():
raw = labor.run_line(_whole(), MIX, GIVEN)
cut = sum(Decimal(str(m["금액"])) for m in raw["재료"])
one = _calc()["줄"][0]
names = {x["이름"] for x in one["줄"]}
assert not names & {"시멘트", "모래(건조상태)", "자갈 또는 부순돌"}
assert {"콘크리트공", "보통인부"} <= names
assert len(one["재료"]) == 3 # 자재목록(세 칸 고침)엔 남음
want = Decimal(str(raw["기준"]["재료비"])) - cut
assert one["기준"]["재료비"] == pytest.approx(float(want))
assert one["기준"]["계"] == pytest.approx(float(Decimal(str(raw["기준"]["계"])) - cut))
assert one["기준"]["노무비"] == pytest.approx(float(raw["기준"]["노무비"]))
def test_직접계산은_그대로_섞이면_그_자재만_빠짐():
raw = labor.run_line(_whole(), MIX, GIVEN)
full = _calc({str(n): DIRECT for n in (3, 4, 5)})["줄"][0]
assert full["기준"]["계"] == pytest.approx(float(raw["기준"]["계"]))
assert len(full["줄"]) == len(raw["줄"])
part = _calc({"3": DIRECT})["줄"][0] # 시멘트만 셈
names = {x["이름"] for x in part["줄"]}
assert "시멘트" in names and "모래(건조상태)" not in names
cement = next(m for m in raw["재료"] if m["이름"] == "시멘트")
others = sum(Decimal(str(m["금액"])) for m in raw["재료"]) - Decimal(str(cement["금액"]))
assert part["기준"]["계"] == pytest.approx(float(Decimal(str(raw["기준"]["계"])) - others))
def test_사용자_자재_별도계상은_합에서_빠지고_반영은_보임():
aside = _calc(None, [{"코드": MAT, "수량": {"손": 2}}])
direct = _calc(None, [{"코드": MAT, "수량": {"손": 2}, **DIRECT}])
price = direct["자재"][0]["기준단가"]
assert aside["자재"][0]["반영"] == pytest.approx(price * 2) # 목록엔 금액 보임
assert direct["반영합"]["계"] - aside["반영합"]["계"] == pytest.approx(price * 2)
assert direct["기준합"]["재료비"] - aside["기준합"]["재료비"] == pytest.approx(price)
@pytest.mark.parametrize("mark", [None, "직접계산", "별도계상", "옛값"])
def test_사용자_자재_별도계상_파이썬과_TS_같은_답(mark):
one = {"코드": MAT, "수량": {"손": 3}, **({"별도계상": mark} if mark else {})}
doc = labor.clean({"열": "rv", "담은로직": [{"로직": MIX, "입력값": GIVEN}], "자재": [one]})
doc["자재"][0].update({"별도계상": mark} if mark else {}) # 옛 값도 그대로 넘겨 봄
answer = labor.calc(doc, {})
values = {"기계비빔타설|㎥": 2}
_same(labor.settle(answer, doc, values), _ts(answer, doc, values))
def test_사전조합_안_줄도_별도계상_뺌():
raw = labor.run_line(_whole(), MIX, GIVEN)
cut = sum(Decimal(str(m["금액"])) for m in raw["재료"])
doc = {
"이름": "비빔 조합",
"단위": "㎥",
"담은로직": [{"로직": MIX, "입력값": GIVEN, "수량": {"손": 1}}],
"자재": [
{"코드": MAT, "수량": {"손": 2}}, # 기본 별도계상 = 안 셈 · 안 줄에도 없음
],
}
ref = {"층": "system", "주인": None, "키": "SC999999"}
got = refs.Expand(_whole(), lambda r: doc)(ref)
assert [x.get("자재") for x in got["안"]] == [None]
pill = got["안"][0]
times = Decimal(1) # 기계비빔타설 알약 배 1
assert pill["기준"]["계"] * times == pytest.approx(Decimal(str(raw["기준"]["계"])) - cut)
assert got["기준"]["계"] == pytest.approx(Decimal(str(raw["기준"]["계"])) - cut)
def test_화면_별도계상_바꾸면_다시_셈():
detail = open(os.path.join(SRC, "M02_MasterTemplete_Labor_Detail.ts"), encoding="utf-8").read()
assert 'if (key === "별도계상") changed(true);' in detail # 사용자 자재 = 짝 셈
assert 'if (key === "별도계상") changed();' in detail # 로직 자재 = 서버 기준
# ── ③ 원문 여럿 ──────────────────────────────────────────────────────
def test_원문_출처는_같은_책_절_모두():
src = inputs.sources(_whole().logic(MIX))
assert src == "산림품셈 12-1-2 · 12-1-3"
assert inputs.logic_inputs([MIX])["로직"][0]["출처"] == src
assert [f["number"] for f in make.source(src)["found"]] == ["12-1-2", "12-1-3"]
def test_원문_출처_번호_없거나_더할_것_없으면_그대로():
assert inputs.sources({"출처": "자체"}) == "자체"
assert inputs.sources({"출처": None}) is None
one = {
"출처": "산림품셈 3-1",
"호표": [{"출처": "건설품셈 공통 1-2-4 3"}, {"출처": "산림품셈 3-1"}],
}
assert inputs.sources(one) == "산림품셈 3-1" # 다른 책 · 같은 절은 안 붙임
+4 -1
View File
@@ -184,7 +184,10 @@ def test_입력이_없으면_빠진_입력_목록_셈은_자동값(world):
assert _stuck(got) == [] and got["기준합"]["계"] > 0
def test_마스터_단가를_바꿔도_계산_그대로_새로_가져오면_바뀜(world):
def test_마스터_단가를_바꿔도_계산_그대로_새로_가져오면_바뀜(world, monkeypatch):
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
monkeypatch.setattr(labor_doc, "apart", lambda one: False) # M01 조합 셈과 대조(222-1 안 뺌)
root, master_dir = world["root"], world["master"]
_save(root, [_pipe()])
values = _fill_values(root)
+1 -1
View File
@@ -87,7 +87,7 @@ def world(tmp_path: Path, monkeypatch, master_copy: Path) -> dict[str, Any]:
{"로직": LOGICS[1], "입력값": {name: value}, "수량": {"손": HAND}},
{"로직": LOGICS[2]},
],
"자재": [{"코드": MATERIAL}],
"자재": [{"코드": MATERIAL, "별도계상": "직접계산"}], # 222-1 합에 셈
}
)
_write(system / "labor" / f"{COLUMN}.json", doc)