"""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" # 다른 책 · 같은 절은 안 붙임