- 원인: 옮김에서 펌프 준설선 EQ000552 · 553 잡재료율 「13∼18」 이 사라짐 → 옛 엔진 멈춤 · 새 계산기 셈(빈 잡품) → GC000268 옵션 「다름」 → 저장 밖 → 그 식을 부르는 문서 320 개가 저장본에서 지워짐(1,520 → 1,200) - 범위 글 = 잡품 율 글 그대로(from_old · TS fromOld · check 허용) · 두 대 다시 옮김 → 까닭 「범위」 옛 칸과 같음 - 빌더 = 저장본 문서가 저장 밖으로 빠지면 멈춤 · 안 씀 · 키와 까닭 알림(lost) · 시험 802_2 에 lost 0 - 시험 822_2 범위 까닭 기대 되돌림 · 842_3 거울에 범위 값 더함 - 확인: 빌더 --check 저장 1,520 · 기본 다름 0 · 옵션 다름 3(채택) · 저장본 · 목록 같음 · 655 대 시간당(옛 자료 대 새 자료) 다름 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
209 lines
11 KiB
Python
209 lines
11 KiB
Python
"""842-3 기계 인력 · 잡품 줄 지킴 시험 — 두 길 같음 · 자료 꼴 · 손셈 · 거울 · 저장 꼴 · 쓰기 없음.
|
||
|
||
짝 = resources/master_data/scripts/master_machine_crew.py ↔ common_util/common_util_machine_crew.ts ·
|
||
`master_doc_cover` row_entry · merge · cover_doc(바탕) ↔ common_util_formula_doc_cover.ts rowEntry · mergeCover · coverDoc.
|
||
① 655 대 두 길 — 로직 엔진(목록 → 조종원 수 · 직종 · 잡재료율 읽기) = 문서 엔진(식 문서 + 목록 바탕 덮음) 노 · 재 · 경 · 계.
|
||
② M01 기계 자료 — 옛 칸(조종원 · 조종원직종 · 잡재료율) 0 · 목록 꼴 틀 0 · 선원류 칸 그대로.
|
||
③ 굴착기 1.0㎥ — 인력 둘(직종 다름) = 한 줄씩 셈 합 · 잡품 재료 줄 = 단가 × 수량 × 율 · 단가산출 덮음이 위에 덮임.
|
||
④ 거울 — 읽기 · 옛 꼴 바꿈 · 바탕 덮음 · 얹은 문서 파이썬 = TS.
|
||
⑤ 내 기계 저장 꼴 — 목록 받음 · 옛 칸 → 목록 · 틀린 줄 400.
|
||
"""
|
||
|
||
import copy
|
||
import importlib
|
||
import json
|
||
import os
|
||
import subprocess
|
||
from decimal import Decimal as D
|
||
|
||
import pytest
|
||
|
||
from M01_MasterData import M01_MasterData_Doc as m01doc
|
||
from M01_MasterData import M01_MasterData_Store as m01 # scripts 길을 둠
|
||
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_Copy as skc
|
||
|
||
mcrew = importlib.import_module("master_machine_crew")
|
||
mcv = importlib.import_module("master_doc_cover")
|
||
mc = importlib.import_module("master_doc_calc")
|
||
ml = importlib.import_module("master_doc_link")
|
||
mcut = importlib.import_module("master_cut")
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
COSTS = ("노무비", "재료비", "경비", "계")
|
||
DIG = "EQ000022" # 굴착기(무한궤도) 1.0㎥ 0201-0100 · GC000267
|
||
SEA = ("선원", "준설선선장", "준설선기관사", "고급선원")
|
||
|
||
|
||
def _plain(v):
|
||
return json.loads(json.dumps(v, ensure_ascii=False, default=float))
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def world():
|
||
with store.at(store.FOLDER):
|
||
index = m01.loaded()[1]
|
||
yield {"index": index, "docs": m01doc.docs(), "rows": list(index.index["EQ"].values())}
|
||
|
||
|
||
def _logic(index, row, logic):
|
||
given = {i["이름"]: (i.get("고르기") or [""])[0] for i in logic.get("입력") or []}
|
||
if mh.takes_machine(logic):
|
||
given["기계"] = str(row.get("원문번호"))
|
||
mf = m01.cm.mf
|
||
got = mcut.engine(index, logic["키"], mf.run(index, logic["키"], mf.fill_empty(logic, given)))
|
||
return {k: round(float(got[k]), 4) for k in COSTS}
|
||
|
||
|
||
# ── ① 두 길 ────────────────────────────────────────────────────────────
|
||
def test_655대_로직_엔진_문서_엔진_같음(world):
|
||
index, same = world["index"], 0
|
||
with store.at(store.FOLDER):
|
||
for row in world["rows"]:
|
||
logic = mh.pick(row)
|
||
if logic is None or mh.why(row, logic):
|
||
continue
|
||
doc = mh.doc_sums(row, logic, index)
|
||
if doc is None or not doc["ok"]:
|
||
continue
|
||
assert {k: round(v, 4) for k, v in doc["합"].items()} == _logic(index, row, logic), row[
|
||
"키"
|
||
]
|
||
same += 1
|
||
assert len(world["rows"]) == 655 and same >= 600
|
||
|
||
|
||
# ── ② 자료 꼴 ───────────────────────────────────────────────────────────
|
||
def test_M01_기계_옛_칸_없음_목록_꼴(world):
|
||
crew = misc = 0
|
||
for row in world["rows"]:
|
||
vals = row.get("값") or {}
|
||
assert not any(k in vals for k in mcrew.OLD), row["키"]
|
||
assert mcrew.check(vals) == [], row["키"]
|
||
for part in ("단위", "출처"):
|
||
assert set(row.get(part) or {}) == set(vals), (row["키"], part)
|
||
crew += bool(vals.get("인력"))
|
||
misc += bool(vals.get("잡품"))
|
||
assert crew >= 295 and misc >= 320
|
||
assert sum(any(k in (r.get("값") or {}) for k in SEA) for r in world["rows"]) == 79
|
||
|
||
|
||
# ── ③ 손셈 ─────────────────────────────────────────────────────────────
|
||
def _hour(world, vals, cover=None):
|
||
"""굴착기 줄 값을 바꾼 사본 색인으로 시간당(문서 엔진 · 단가산출 덮음 얹음)."""
|
||
row = copy.deepcopy(world["index"].get(DIG))
|
||
row["값"] = {**mcrew.from_old(row["값"]), **vals}
|
||
index = skc.Copy(world["index"], [row], ("시험", id(vals)))
|
||
doc, given = world["docs"]["GC000267"], {"기계": "0201-0100"}
|
||
shaped = mcv.cover_doc(doc, given, cover, mcv.row_entry(doc, row["값"]))
|
||
got = mc.calc(shaped, ml.Links(index, world["docs"]), given)
|
||
assert got["셈됨"], got["표지"]
|
||
return got
|
||
|
||
|
||
def test_굴착기_인력_둘_잡품_재료_손셈(world):
|
||
with store.at(store.FOLDER):
|
||
one = _hour(world, {"인력": [{"키": "LB:1048", "인": D(1)}]})
|
||
two = _hour(world, {"인력": [{"키": "LB:1002", "인": D(2)}]})
|
||
both = _hour(
|
||
world, {"인력": [{"키": "LB:1048", "인": D(1)}, {"키": "LB:1002", "인": D(2)}]}
|
||
)
|
||
assert abs(both["노무비"] - one["노무비"] - two["노무비"]) <= 1
|
||
assert both["재료비"] == one["재료비"] and both["경비"] == one["경비"]
|
||
crew = [x for x in both["줄"] if x.get("이름") == "조종원"]
|
||
assert len(crew) == 2
|
||
none = _hour(world, {"인력": []})
|
||
assert none["노무비"] == 0
|
||
# 잡품 재료 줄 — 주연료 22 % + 재료 2 × 50 %
|
||
base = _hour(world, {})
|
||
mat = _hour(world, {"잡품": [{"줄": "주연료", "율": D(22)},
|
||
{"키": "MO:휘발유:전국평균", "수량": D(2), "율": D(50)}]}) # fmt: skip
|
||
price = world["index"].get("MO:휘발유:전국평균")["값"]
|
||
assert abs(mat["재료비"] - base["재료비"] - D(str(price)) * 2 * D("0.5")) <= 1
|
||
# 단가산출 덮음 = 목록 통째 위에(정본 2 줄 → 덮음 1 줄)
|
||
top = _hour(world, {"인력": [{"키": "LB:1048", "인": D(1)}, {"키": "LB:1002", "인": D(2)}]},
|
||
{"0201-0100": {"조종원": [{"키": "LB:1048", "인": D(1)}]}}) # fmt: skip
|
||
assert top["노무비"] == one["노무비"]
|
||
|
||
|
||
def test_빈_직종_줄은_식_입력_직종(world):
|
||
doc = world["docs"]["GC000275"]
|
||
parts = mcv.parts_of(doc)
|
||
assert mcv.row_entry(doc, {"인력": [{"키": "", "인": 1}]}) is None # 한 줄 = 식 그대로
|
||
got = mcv.cover_doc(doc, {"기계": "M"}, None, {"조종원": [{"키": "", "인": 2}]})
|
||
crew = [x for x in got["금액"] if x.get("비목") == "노무비"]
|
||
assert len(crew) == 1 and crew[0]["단가"] == doc["금액"][parts["crew"][0]]["단가"]
|
||
|
||
|
||
# ── ④ 거울 ─────────────────────────────────────────────────────────────
|
||
VALUES = [
|
||
{"가격": 1, "주연료": 19.5, "조종원": 1, "조종원직종": "1048", "잡재료율": 22},
|
||
{"가격": 1, "조종원": 1, "조종원직종": None, "잡재료율": 0, "주연료": 3},
|
||
{"가격": 1, "전력": 5, "주연료": 2, "잡재료율": 7, "조종원": 0},
|
||
{"가격": 1, "인력": [{"키": "LB:1048", "인": 1}, {"키": "LB:1002", "인": 2}], "잡품": []},
|
||
{"가격": 1, "인력": [{"키": "", "인": 1}], "잡품": [{"키": "MA:1", "수량": 2, "율": 5}]},
|
||
{"가격": 1},
|
||
{"가격": 1, "주연료": 2, "잡재료율": "13∼18", "조종원": None},
|
||
]
|
||
TARGETS = [
|
||
("GC000268", {"인력": [{"키": "LB:1048", "인": 1}], "잡품": [{"줄": "주연료", "율": 10}]}, None),
|
||
("GC000268", {"인력": [{"키": "LB:1048", "인": 1}, {"키": "LB:1002", "인": 1}]}, None),
|
||
("GC000268", {"인력": [], "잡품": [{"줄": "주연료", "율": 10}, {"키": "MA:1", "율": 5}]}, None),
|
||
("GC000272", {"인력": [{"키": "LB:1048", "인": 1}], "잡품": [{"줄": "주연료", "율": 3, "수량": 1}]},
|
||
{"값": {"가격": 9}, "잡품": [{"줄": "주연료", "율": 4}]}),
|
||
("GC000273", {"인력": [{"키": "LB:1048", "인": 1}], "잡품": [{"키": "MA:1", "율": 5}]}, None),
|
||
("GC000275", {"인력": [{"키": "", "인": 2}, {"키": "LB:1050", "인": 1}]}, None),
|
||
("GC001200", {"잡품": [{"줄": "주연료", "율": 7}, {"줄": "전력", "율": 7}]}, None),
|
||
] # fmt: skip
|
||
|
||
|
||
def test_거울_파이썬_TS_같은_답(world):
|
||
docs = {k: _plain(world["docs"][k]) for k, _, _ in TARGETS}
|
||
ask = {"값들": VALUES, "문서들": docs,
|
||
"대상": [{"식": k, "값": v, **({"덮음": c} if c else {})} for k, v, c in TARGETS]} # fmt: skip
|
||
proc = subprocess.run(
|
||
["node", os.path.join(HERE, "helper_842_3_machine_crew.cjs")],
|
||
input=json.dumps(ask, ensure_ascii=False),
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
check=True,
|
||
)
|
||
got = json.loads(proc.stdout)
|
||
assert got["읽기"] == _plain([mcrew.view(v) for v in VALUES])
|
||
assert got["옛"] == _plain([mcrew.from_old(v) for v in VALUES])
|
||
base = [mcv.row_entry(docs[k], v) for k, v, _ in TARGETS]
|
||
assert got["바탕"] == _plain(base)
|
||
assert [b is None for b in base] == [True, False, False, False, True, False, True]
|
||
want = [
|
||
mcv.cover_doc(docs[k], {"기계": "M"}, {"M": c} if c else None, b)
|
||
for (k, _, c), b in zip(TARGETS, base)
|
||
]
|
||
assert got["얹음"] == _plain(want)
|
||
|
||
|
||
# ── ⑤ 내 기계 저장 꼴 ─────────────────────────────────────────────────
|
||
def test_내기계_저장_목록_받음(world):
|
||
with store.at(store.FOLDER):
|
||
got = mym.clean({"이름": "sub3임시", "방식": "바로", "값": {
|
||
"가격": 1000, "손료계수": 2000, "주연료": 5, "주연료종류": "경유",
|
||
"인력": [{"키": "LB:1048", "인": 1}, {"키": "LB:1002", "인": 2}],
|
||
"잡품": [{"줄": "주연료", "율": 10}, {"키": "MO:휘발유:전국평균", "수량": 1, "율": 50}]}}) # fmt: skip
|
||
assert len(got["값"]["인력"]) == 2 and len(got["값"]["잡품"]) == 2
|
||
assert got["단위"]["인력"] == "인/일" and got["출처"]["잡품"] == "개인"
|
||
old = mym.clean({"이름": "sub3임시", "방식": "바로", "값": {
|
||
"가격": 1000, "손료계수": 2000, "주연료": 5, "주연료종류": "경유",
|
||
"조종원": 1, "조종원직종": "1048", "잡재료율": 10}}) # fmt: skip
|
||
assert old["값"]["인력"] == [{"키": "LB:1048", "인": 1}]
|
||
assert old["값"]["잡품"] == [{"줄": "주연료", "율": 10}]
|
||
assert not any(k in old["값"] for k in mcrew.OLD)
|
||
for bad in ({"인력": [{"키": "LB:없음", "인": 1}]}, {"인력": [{"키": "LB:1048", "인": -1}]},
|
||
{"잡품": [{"줄": "없는줄", "율": 1}]}, {"잡품": [{"키": "MA:없음", "율": 1}]}): # fmt: skip
|
||
with pytest.raises(store.StoreError) as e:
|
||
mym.clean(
|
||
{"이름": "sub3임시", "방식": "바로", "값": {"가격": 1, "손료계수": 1, **bad}}
|
||
)
|
||
assert e.value.status == 400, bad
|