"""705-3 실무자료 불러오기 빠짐 0 · 시트 통째 한 번 · [품셈으로 바꾸기](요구서 1장 · 4장 2 · 3 · 7). - 확인 안 한 줄 · 노임 · % · 「우리에 없음」 = 고정 줄(엑셀 숫자 그대로) · 체크한 줄만 품셈 · 하위 호표 = 참조 - % 줄 = 비율 고정 줄 — M02 settle 거울(파이썬 ↔ TS) · 사전조합 셈 · B09 `pct` 줄 - 모두 고정(`build_all` 막지 않음 = 붙여넣기 길) — 북부청 · 실무 xlsx 넷: 빠짐 0 · 엑셀 합계 = 우리 합계 · 줄 꼬리표 수 · 확인된 호표 그대로 · 이음 = 단가까지 같은 줄만(노임 이름 · 기계 EQ = 불러오기 길만) · 같은 이름 줄 차례 가름 · 몸에 숫자 칸 없음(422) · [모두 고정으로 들여오기] 길 = 남은 줄 있는 호표 실패(729-2 · 718-1 시험) - [품셈으로 바꾸기] 전후 — 체크 한 줄 = 품셈 · 나머지 고정(「우리에 없음」 고름) · 되돌리기 = 모두 고정 층은 임시 폴더(공용 storage 안 씀). """ import importlib.util import json from pathlib import Path import pytest from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor from B09_Estimation.B09_Estimation_Labor_Source import fixed_line from M02_MasterTemplete import M02_Hopyo_Build as hb from M02_MasterTemplete import M02_Hopyo_Fixed as hfix from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_Template_Layers as layers # 올리기 길 시험의 `client` 마당을 같이 씀 # ruff: noqa: F811 from resources.tester.test_m02_hopyo_excel import NORTH, _post, client, master_hash # noqa: F401 def _load(name): spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(f"{name}.py")) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod m02 = _load("test_m02_labor_calc_mirror") ALL = "/api/m02/layers/{}/hopyo-build-all" BUILD = "/api/m02/layers/{}/hopyo-build" DOCS = Path(__file__).resolve().parents[2] / "resources" / "knowledge" / "original" / "실무문서" FILES = { # 실무 xlsx 넷 — EST 꼴 거창 · STmate 꼴 영월 · 울진 대흥 · 울진 소광 "거창": "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx", "영월": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx", "대흥": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/" "2024년 간선임도사업(기번3).xlsx", "소광": "25년 산불진화임도(기번8)(울진소광)/" "2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx", } # (호표, 줄, 고정, 하위 참조, 이음, 엑셀 = 우리) — 영월 제22호표 = STmate 하위 호표 비목 반올림 1 원 끝수 EXPECT = { "거창": (14, 68, 56, 12, 11, 14), "영월": (23, 138, 135, 3, 0, 23), # 751-3 끝수 뒤 엑셀 금액과 같음 하나 늚(23 개 전부) "대흥": (34, 171, 143, 28, 0, 34), "소광": (25, 117, 98, 19, 0, 25), } def _fx(name, price, rate=None, unit="식"): out = {"이름": name, "규격": "", "단위": unit, "단가": price, "이음": {}} return {**out, "비율": rate} if rate else out def _all(client, key="HP-000001"): """모두 고정(막지 않음 = 붙여넣기 길 `build_all`) — 길(729-2)은 남은 줄 있는 호표 = 실패.""" folder = layers.personal_dir(7, 42) with store.at(folder): return hb.build_all(folder, folder, key) def _decided(client, body): """남은 줄(729-2)을 「우리에 없음」으로 고른 몸.""" left = client.post(BUILD.format("personal").replace("build", "unlinked"), json=body).json() return { **body, "줄": [*body["줄"], *({"차례": x["차례"], "뺌": "우리에 없음"} for x in left["남은줄"])], } def _docs(): folder = layers.personal_dir(7, 42) / "precombo" return [json.loads(p.read_bytes()) for p in sorted(folder.glob("SC-*.json"))] # ── 비율 고정 줄 셈 ────────────────────────────────────────────────────────── def _ratio_doc(): return labor.clean( { "열": "rv", "담은로직": [ {"고정": _fx("고화매트", {"재료비": 37000}, unit="㎡"), "수량": {"손": 1}}, {"고정": _fx("고정핀", {"재료비": 200}, unit="EA"), "수량": {"손": 4.95}}, {"고정": _fx("인부", {"노무비": 1000}, unit="인"), "수량": None}, {"로직": m02.PLAIN, "수량": None}, { "고정": _fx("잡재료비", {}, {"기준": "재료비", "비목": "재료비"}), "수량": {"손": 3}, }, { "고정": _fx("공구손료", {}, {"기준": "노무비", "비목": "경비"}), "수량": {"손": 2}, }, ], "자재": [{"코드": m02.MAT, "수량": None}], } ) @pytest.mark.parametrize("values", [{}, {f"자재|{m02.MAT}": "3"}, {"사석 수상고르기|㎡": 2}]) def test_비율_고정_줄_파이썬과_TS_가_같은_답(values): doc = _ratio_doc() answer = labor.calc(doc, {}) py = labor.settle(answer, doc, values) m02._same(py, m02._ts(answer, doc, values)) assert py == labor.calc(doc, values) misc, tool = [ln for ln in py["줄"] if (ln.get("고정") or {}).get("비율")] others = [ln for ln in py["줄"] if ln not in (misc, tool)] under = sum((ln["반영"] or {}).get("재료비", 0) for ln in others) # 자재 = 별도계상(합 밖) assert misc["반영"]["재료비"] == pytest.approx(under * 3 / 100, abs=0.1) and misc["배"] == 100 assert misc["반영"]["노무비"] == misc["반영"]["경비"] == 0 assert tool["반영"]["경비"] == 0 # 인부 수량 없음 = 노무비 밑 0 gap = sum((ln["반영"] or {}).get("계", 0) for ln in py["줄"]) - py["반영합"]["계"] assert -1e-6 <= gap < 3 # 751-3 반영합 = 비목마다 1 원 버림 def test_비율_정리_막힘(): bad = _fx("잡", {}, {"기준": "합계", "비목": "재료비"}) with pytest.raises(Exception, match="비율"): labor.clean({"담은로직": [{"고정": bad, "수량": {"손": 1}}]}) def test_B09_비율_고정_줄_pct(): fx = _fx("잡재료비", {}, {"기준": "재료비", "비목": "재료비"}) one = fixed_line(fx, 3) assert {k: one[k] for k in ("kind", "part", "base", "qty", "unit")} == { "kind": "pct", "part": "material", "base": "part:material", "qty": 3.0, "unit": "%" } # fmt: skip lines = [ {"kind": "fixed", "name": "고화매트", "qty": 1, "own": {"material": 37000.0}, "link": {"labor": None, "material": None, "machine": None}}, {"kind": "fixed", "name": "고정핀", "qty": 4.95, "own": {"material": 200.0}, "link": {"labor": None, "material": None, "machine": None}}, one, ] # fmt: skip for n, ln in enumerate(lines): ln["cid"], ln["n"] = f"{ln['kind']}:{ln['name']}#0", n block = {"id": "DGF", "cover": "S:f", "kind": "조합", "name": "고화매트재료비", "spec": "", "unit": "㎡", "cite": "", "origin": {}, "lines": lines} # fmt: skip out = compute_labor({"blocks": [block], "owners": {}}, {}, {}) got = out["daega_blocks"][0] pct = next(ln for ln in got["lines"] if ln["kind"] == "pct") assert pct["amounts"]["material"] == 1139.7 # (37000 + 990) × 3 % assert got["blank"] == 0 and out["gaps"] == [] # ── 줄 하나 → 고정 줄 ──────────────────────────────────────────────────────── def test_같은_이름_차례_가름_합계만_재료비_금액만(): linker, taken = hfix.Linker(), set() line = {"차례": 0, "이름": "잡석", "규격": "", "단위": "㎥", "수량": 2, "갈래": "자재", "단가": {"계": 5000, "노무비": None, "재료비": None, "경비": None}} # fmt: skip a = hfix.line_of(line, [], {"파일": "x", "호표": 1}, taken, linker) b = hfix.line_of({**line, "차례": 1}, [], {"파일": "x", "호표": 1}, taken, linker) assert (a["고정"]["이름"], b["고정"]["이름"]) == ("잡석", "잡석 (2)") assert a["고정"]["단가"] == {"노무비": 0, "재료비": 5000, "경비": 0} # 합계만 = 재료비 assert b["고정"]["출처"] == {"파일": "x", "호표": 1, "줄": 1} and a["수량"] == {"손": 2} doc = labor.clean({"담은로직": [a, b]}) # 같은 줄 가름 400 안 남 assert len(doc["담은로직"]) == 2 only = {**line, "차례": 2, "이름": "운반", "수량": None, "금액": {"계": 700, "노무비": 700, "재료비": None, "경비": None}} # fmt: skip c = hfix.line_of(only, [], {}, taken, linker) assert c["수량"] == {"손": 1} and c["고정"]["단가"]["노무비"] == 700 # 금액만 def test_퍼센트_밑수_다르면_금액_그대로(): pct = {"차례": 2, "이름": "소모자재", "규격": "주재료비의", "단위": "%", "수량": 11, "갈래": "%", "단가": {"계": 7937.2}, "금액": {"계": 873, "노무비": 0, "재료비": 873, "경비": 0}} # fmt: skip got = hfix.line_of(pct, [], {}, set(), hfix.Linker()) assert "비율" not in got["고정"] and got["고정"]["단위"] == "식" assert got["수량"] == {"손": 1} and got["고정"]["단가"]["재료비"] == 873 # ── 시트 통째 ──────────────────────────────────────────────────────────────── def test_북부청_모두_고정_빠짐_0_합계_같음_그대로(client): before = master_hash() assert _post(client).status_code == 200 got = _all(client) assert {k: got[k] for k in ("호표", "줄", "품셈", "고정", "호표참조", "빠짐", "같음")} == { "호표": 68, "줄": 415, "품셈": 0, "고정": 414, "호표참조": 1, "빠짐": 0, "같음": 68 } # fmt: skip assert got["실패"] == [] and got["다름"] == [] and got["그대로"] == 0 assert got["이음"] == 13 and got["이음후보"] == 31 docs = [d for d in _docs() if d["본뜬"]["키"].count(" ") == 1] # 호표 문서(감싼 줄 빼고) lines = [x for d in docs for x in d["담은로직"]] assert len(docs) == 68 and all(d["본뜬"]["품셈"] == [] for d in docs) assert sum("고정" in x for x in lines) == 414 and sum("참조" in x for x in lines) == 1 assert sum(bool(x.get("고정", {}).get("비율")) for x in lines) == 1 # 잡재료비 3 % again = _all(client) # 다시 = 확인된 호표 덮지 않음 assert again["그대로"] == 68 and again["고정"] == 0 and again["같음"] == 68 assert master_hash() == before @pytest.mark.parametrize("tag", list(FILES)) def test_실무_xlsx_넷_모두_고정(client, tag): path = DOCS / FILES[tag] res = _post(client, data=path.read_bytes(), name=path.name) assert res.status_code == 200, res.text got = _all(client) no, rows, fixed_n, kids, links, same = EXPECT[tag] assert (got["호표"], got["줄"], got["고정"], got["호표참조"]) == (no, rows, fixed_n, kids) assert got["빠짐"] == 0 and got["품셈"] == 0 and got["실패"] == [] assert got["고정"] + got["호표참조"] == got["줄"] # 줄 하나도 안 빠짐 assert got["이음"] == links and got["같음"] == same assert all(abs(x["차이"]) <= 1 for x in got["다름"]) def test_몸에_숫자_칸_없음(client): assert _post(client).status_code == 200 bad = {"층": "personal", "키": "HP-000001", "금액": 1} assert client.post(ALL.format("personal"), json=bad).status_code == 422 # ── [품셈으로 바꾸기] 전후 ──────────────────────────────────────────────────── def test_품셈으로_바꾸기_되돌리기(client): assert _post(client).status_code == 200 _all(client) body = {"층": "personal", "키": "HP-000001", "번": 3} one = {"차례": 1, "대상": [{"로직": "GF000101", "입력값": {"지역": "서울"}, "배": 1}]} res = client.post(BUILD.format("personal"), json=_decided(client, {**body, "줄": [one]})) assert res.status_code == 200, res.text after = res.json() assert after["다시씀"] is True and after["줄수"]["품셈"] == 1 and after["줄수"]["고정"] == 3 doc = next(d for d in _docs() if d["본뜬"]["키"] == "HP-000001 제3호표") assert doc["본뜬"]["품셈"] == [1] and doc["담은로직"][1]["로직"] == "GF000101" assert after["비교"]["같음"] is False # 품셈 줄 = 우리 값(엑셀과 다름) view = client.get("/api/m02/layers/personal/hopyo/HP-000001/3").json() assert view["호표"]["만든"]["품셈"] == [1] and view["호표"]["만든"]["고정"] == 3 back = client.post(BUILD.format("personal"), json=_decided(client, {**body, "줄": []})).json() assert back["줄수"]["품셈"] == 0 and back["비교"]["같음"] is True