# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀 """822-2 기계 시간당 경비 식 완성 — 지킴 시험(정본 고침은 임시 사본 폴더 · 임시 storage 만). ① 모든 기계 = 식 있음 또는 까닭 글(「셀 자료가 없음」 0) ② 식 고름 = 목록 › 꼴(채운 칸) 한 함수 ③ M01 [새 기계] 저장 = 꼴 식 목록 등록 · 지우면 목록에서 뺌 · 로직 파일 글 그대로(임시 마스터) ④ 직접 값 식(GC001199) — 내 기계 「직접」 시간당 · 줄 쌓기 식 ⑤ 전력 줄 — 점보드릴 135㎾ × 전력 단가 + 잡재료 6 % + 손료 + 조종원 · 계약종별 바꿈 · 내 기계 전력 · 연료 + 전력(822-2b GC001200 · + 조종원 GC001201) ⑥ 옮긴 기계 견본(아스팔트 믹싱플랜트 · 타워크레인 · 직종 입력 = 아스팔트 스프레어) ⑦ 미리보기 = 서버 값 ⑧ 안 옮긴 기계 금액 그대로 ⑨ POWER TROWEL 이름. """ from __future__ import annotations import json import shutil import sys from decimal import Decimal from pathlib import Path import pytest from fastapi import Depends, FastAPI from fastapi.testclient import TestClient from common_util import common_util_auth as auth from M01_MasterData import M01_MasterData_Router as router_module from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_Machine_Hourly as mh from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_MyMachine as mym from M02_MasterTemplete import M02_Sangeun_Calc as calc from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층 ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "resources" / "master_data" / "scripts")) import master_machine_coef as mc # noqa: E402 MACHINE, LOGIC = "기계.json", "로직_건설품셈_08장_건설기계.json" POWER, POWER_CREW, ONE_FUEL, DIRECT = "GC001196", "GC001197", "GC001198", "GC001199" FUEL_POWER, FUEL_POWER_CREW = "GC001200", "GC001201" NAME = "sub1임시기계822" # 안 옮긴 기계 — 822-2 전 금액(전국평균 · 기본 옵션) KEEP = {"EQ000001": 68550, "EQ000022": 108958} def _rows() -> dict[str, dict]: return {r["키"]: r for r in m01.read(MACHINE)[0]["줄"]} def _wage(job: str) -> Decimal: return Decimal(str(m01.loaded()[1].get(f"LB:{job}")["값"])) def _codes(key: str) -> list[str]: row = next(r for r in m01.read(LOGIC)[0]["줄"] if r["키"] == key) return [str(c) for c in next(i for i in row["입력"] if i["이름"] == "기계").get("고르기", [])] # ① · ② ─────────────────────────────────────────────────────────────── def test_every_machine_has_formula_or_reason(): """모든 기계 — 식을 고르고 값이 다 있음 · 아니면 까닭 글(빈 답 0).""" silent = [] for key, row in _rows().items(): row = m01.loaded()[1].get(key) logic = mh.pick(row) if not logic and not mh.why(row, logic): silent.append(key) assert silent == [] assert mh.hourly("EQ000343")["reason"] == "가격 없음" # TBM 3.0m assert "일 단위" in mh.hourly("EQ000621")["reason"] # 산림품셈 예취기(1일 1대당) assert mh.hourly("EQ000202")["reason"] == "주연료 「73.7+」 — 수 아님" # 표층재생기 · 안 풂 assert "범위" in mh.hourly("EQ000552")["reason"] # 펌프 준설선 잡재료 13∼18 assert ( "중유" in mh.hourly("EQ000187")["reason"] ) # 아스팔트 믹싱플랜트 40t — 유가 표에 중유 없음 def test_pick_listed_then_shape(): """목록에 있으면 그 식(짝 차례) · 없으면 꼴 — 내 기계와 같은 규칙.""" assert mh.logic_of("0101-0007")["키"] in {"GC000268", "GC000269"} # 목록 shape = mym.logic_key full = {"가격": 1, "손료계수": 3, "상각비계수": 1, "정비비계수": 1, "관리비계수": 1} assert shape({"값": {**full, "주연료": 1, "조종원": 1, "조종원직종": "1048"}}) == "GC000268" assert shape({"값": {**full, "주연료": 1, "조종원": 1}}) == "GC000275" assert shape({"값": {**full, "주연료": 1}}) == "GC000272" assert shape({"값": {"가격": 1, "손료계수": 3, "주연료": 1}}) == ONE_FUEL assert shape({"값": {**full, "조종원": 1, "조종원직종": "1048"}}) == "GC000276" assert shape({"값": {**full, "전력": 5}}) == POWER assert shape({"값": {**full, "전력": 5, "조종원": 1, "조종원직종": "1048"}}) == POWER_CREW assert shape({"값": full}) == "GC000273" assert shape({"방식": "직접", "값": {}}) == DIRECT assert shape({"값": {**full, "주연료": 1, "전력": 5}}) == FUEL_POWER both = {**full, "주연료": 1, "전력": 5, "조종원": 1} assert shape({"값": {**both, "조종원직종": "1048"}}) == FUEL_POWER_CREW assert shape({"값": both}) is None # 직종 없는 조종원 — 까닭 assert shape({"값": {**full, "조종원": 1}}) is None # 직종 없는 조종원만 — 까닭 assert shape({"값": {**full, "주연료": "73.7+"}}) is None # ③ M01 [새 기계] 식 등록 ────────────────────────────────────────────── @pytest.fixture def copy(tmp_path: Path, monkeypatch) -> Path: for path in m01.FOLDER.glob("*.json"): if not path.name.startswith("_") or path.name == m01.mk.BOOK.name: shutil.copy(path, tmp_path / path.name) shutil.copytree(m01.FOLDER / "기계", tmp_path / "기계") # 기계 = 폴더(826-2) monkeypatch.setattr(m01, "FOLDER", tmp_path) return tmp_path def _app() -> TestClient: app = FastAPI() app.dependency_overrides[auth.verify_session] = lambda: { "user_id": 1, "name": "시험", "role": "SYSTEM_ADMIN", "company_id": 1, } # fmt: skip deps = [Depends(auth.verify_session), Depends(auth.require_system_admin)] app.include_router(router_module.router, dependencies=deps) return TestClient(app) def _save(client: TestClient, changes: list[dict]) -> None: version = client.get("/api/m01/machine/row", params={"key": "EQ000022"}).json()["version"] body = {"files": [{"file": MACHINE, "version": version, "changes": changes}]} res = client.post("/api/m01/save", json=body) assert res.status_code == 200, res.text def test_new_machine_registers_and_unregisters(copy: Path): client = _app() logic_text = (copy / LOGIC).read_text(encoding="utf-8") seed = json.loads(json.dumps(_rows()["EQ000022"], default=float)) row = {**seed, "키": "", "원문번호": "9999-0822", "이름": NAME, "출처": {}, "단위": {}} del row["별칭"] draft = client.post("/api/m01/machine/formula", json=row).json() assert draft["키"] == "GC000268" and draft["방법"] == "꼴" and not draft["까닭"] _save(client, [{"op": "add", "row": row}]) assert "9999-0822" in _codes("GC000268") key = next(k for k, r in _rows().items() if r["원문번호"] == "9999-0822") hour = client.get("/api/m01/machine/hourly", params={"keys": key}).json()[key] assert hour["로직"] == "GC000268" and hour["계"] == mh.hourly("EQ000022")["계"] saved = json.loads(json.dumps(_rows()[key], default=float)) got = client.post("/api/m01/machine/formula", json=saved).json() assert got["방법"] == "목록" and got["키"] == "GC000268" _save(client, [{"op": "delete", "key": key}]) assert "9999-0822" not in _codes("GC000268") assert (copy / LOGIC).read_text(encoding="utf-8") == logic_text _save( client, [{"op": "add", "row": {**row, "원문번호": ""}}] ) # 빈 원문번호 = 「자체 EQ…」 받을 키 key = max(k for k, r in _rows().items() if r["이름"] == NAME) assert _rows()[key]["원문번호"] == f"자체 {key}" and f"자체 {key}" in _codes("GC000268") _save(client, [{"op": "delete", "key": key}]) assert (copy / LOGIC).read_text(encoding="utf-8") == logic_text # ④ 직접 값 식 ───────────────────────────────────────────────────────── def _at(folder, fn, *args): with store.at(folder): return fn(*args) def test_direct_formula_my_machine(world): me = world["personal"] body = { "이름": NAME, "규격": "직접", "방식": "직접", "시간당": {"노무비": 10, "재료비": 20, "경비": 30}, } made = _at(me, mym.create, body) assert made["시간당"]["로직"] == DIRECT and made["시간당"]["계"] == 60 no = made["줄"]["원문번호"] whole = _at(me, mym.whole) run = m01.cm.mf.run(whole, DIRECT, {"기계": no}) # 직접 값 식 — 내 기계 원문번호를 받음 assert (run["노무비"], run["재료비"], run["경비"]) == (10, 20, 30) line = {"종류": "로직", "로직": DIRECT, "입력값": {"기계": no}, "수량": "2"} name = _at( me, lambda: refs.create("sangeun", {"이름": NAME, "단위": "시간", "더한줄": [line]})["이름"] ) doc = _at(me, lambda: store.read("sangeun", name)["문서"]) got = _at(me, calc.calc_doc, doc, None, False)["기준"] assert (got["노무비"], got["재료비"], got["경비"]) == (20, 40, 60) # ⑤ 전력 줄 ──────────────────────────────────────────────────────────── def _preview(key: str) -> dict: v = json.loads(json.dumps(_rows()[key]["값"], default=float)) fuel = m01.loaded()[1].numbers["MO"].get(f"{v.get('주연료종류')}:전국평균") wage = float(_wage(v["조종원직종"])) if v.get("조종원직종") else 0 return mc.preview(v, float((fuel or {}).get("값") or 0), wage, 114.4) def test_power_line_jumbo_drill(): h = mh.hourly("EQ000347") # 점보드릴 2붐 135㎾ · 잡재료 6 % · 조종원 1(건설기계운전사) assert h["로직"] == POWER_CREW loss = 605472 * 1000 * 2515 // 10**7 elec = Decimal("135") * Decimal("114.4") misc = int(elec * 6) / Decimal(100) assert h["경비"] == loss and h["재료비"] == int(elec + misc) assert h["노무비"] == int(_wage("1048") / 8) p = _preview("EQ000347") assert (p["노무비"], p["재료비"], p["경비"], p["계"]) == ( h["노무비"], h["재료비"], h["경비"], h["계"], ) home = m01.calc(POWER_CREW, {"기계": "5805-0002", "전력 계약종별": "주택용 저압"}) assert home["ok"] and float(home["sums"]["재료비"]) > h["재료비"] # 120 원/kWh > 114.4 def test_power_my_machine(world): me = world["personal"] body = { "이름": NAME, "규격": "전력", "방식": "바로", "값": {"가격": 1000, "손료계수": 2000, "전력": 10, "잡재료율": 5}, } hour = _at(me, mym.create, body)["시간당"] assert hour["로직"] == POWER want = mc.preview({"가격": 1000, "손료계수": 2000, "전력": 10, "잡재료율": 5}, 0, 0, 114.4) assert (hour["재료비"], hour["경비"]) == (want["재료비"], want["경비"]) == (1201, 200) # 822-2b 연료 + 전력 둘 다(경쟁사 파워트로웰 꼴) — 손료 + 주연료 + 잡품(둘의 %) + 전력 = 미리보기 vals = {**body["값"], "주연료": 1, "주연료종류": "휘발유"} both = _at(me, mym.create, {**body, "규격": "연료전력", "값": vals})["시간당"] oil = float(m01.loaded()[1].numbers["MO"]["휘발유:전국평균"]["값"]) want = mc.preview(vals, oil, 0, 114.4) assert both["로직"] == FUEL_POWER and want["주연료"] > 0 and want["전력"] > 0 assert (both["재료비"], both["경비"], both["계"]) == (want["재료비"], want["경비"], want["계"]) vals |= {"조종원": 1, "조종원직종": "1048"} # + 조종원 = GC001201 crew = _at(me, mym.create, {**body, "규격": "연료전력조종원", "값": vals})["시간당"] want = mc.preview(vals, oil, float(_wage("1048")), 114.4) assert crew["로직"] == FUEL_POWER_CREW and crew["노무비"] == want["노무비"] > 0 assert (crew["재료비"], crew["경비"], crew["계"]) == (want["재료비"], want["경비"], want["계"]) # ⑥ · ⑦ 옮긴 기계 견본 ───────────────────────────────────────────────── def test_moved_samples(): plant = mh.hourly("EQ000188") # 아스팔트 믹싱플랜트 60t — 연료 + 조종원 2 인 assert plant["로직"] == "GC000268" and plant["노무비"] == int(_wage("1048") * 2 / 8) assert plant["재료비"] > 0 tower = mh.hourly("EQ000165") # 타워크레인 — 건설기계운전사 조종원 줄(요구 799) assert tower["로직"] == "GC000276" and tower["노무비"] == int(_wage("1048") / 8) spray = m01.loaded()[1].get("EQ000198") # 아스팔트 스프레어 — 직종 빈 칸 = 직종 입력 식 assert mh.pick(spray)["키"] == "GC000275" and "조종원직종" in json.dumps( mh.pick(spray)["입력"], ensure_ascii=False ) assert "가격" in mh.hourly("EQ000198")["reason"] assert "3450-0642" not in _codes("GC000273") # 손료만 식에서 뺌 for key, logic in ( ("EQ000529", POWER), ("EQ000534", ONE_FUEL), ("EQ000532", POWER), ("EQ000426", POWER), ): h = mh.hourly(key) p = _preview(key) assert h["로직"] == logic and (p["재료비"], p["경비"], p["계"]) == ( h["재료비"], h["경비"], h["계"], ), key for key in ("EQ000188", "EQ000165"): h, p = mh.hourly(key), _preview(key) assert (p["노무비"], p["재료비"], p["경비"]) == (h["노무비"], h["재료비"], h["경비"]), key # ⑧ · ⑨ ──────────────────────────────────────────────────────────────── def test_unmoved_same_and_name(): for key, total in KEEP.items(): assert mh.hourly(key)["계"] == total row = _rows()["EQ000534"] assert (row["이름"], row["옛이름"], row["별칭"]) == ( "POWER TROWEL", "POWER TLOWEL", "파워트로웰", )