Files
Aislo/resources/tester/test_822_2_machine_hourly_all.py
T
eomsangdonandClaude Opus 5.5 c7cbef86a0 feat(826-2): M01 기계 = 한 대 한 파일 · M01 저장 길 줄이기
- 기계.json 한 파일 → 폴더 기계/(EQ 키마다 파일 + _머리.json · 차례 = 키 차례) · 옮긴 빌더 build_기계폴더_826_2 (655 대 머리 · 줄 · 차례 대조 뒤 옛 파일 지움)
- 읽기 도우미 master_machine_files — cm.load · Store_Cache · Store.read 가 옛 「기계.json」 한 표 꼴로 지어 줌(API · 화면 · 색인 그대로) · 쓰기 = 바뀐 대 파일만
- 기계.json 직접 열던 곳 도우미로: M02_San_Basis_Index · M02_Logic_Standard 지문 · build_용도 · master_keys.adopt · _틀 · _갱신절차
- 저장 몸 M01_MasterData_Store_Save — 전 = 기억 · 후 = 고친 파일만 얕은 사본 · 틀은 고친 줄만 · 조합 · 일위대가 문서는 로직 줄이 빠질 때만 · 재료 찾기 기억 이어받음 · 저장 뒤 쓴 파일만 기억에 갈아 끼움
- 마스터 사본 시험 22 개 = 기계 폴더도 복사 · 821_2 · 815_2 글 대조 = 대 파일 하나 대조(옛 한 파일 글 대조 낡음)
- 새 시험 test_826_2 — 폴더 = 한 표 · 한 대 한 파일 · 새 · 지움 · 409 · 옛 길과 같은 오류 스무 견본

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 19:45:00 +09:00

278 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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",
"파워트로웰",
)