- 기계.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>
278 lines
14 KiB
Python
278 lines
14 KiB
Python
# 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",
|
||
"파워트로웰",
|
||
)
|