Files
Aislo/resources/tester/test_821_4_import_my_machine.py
T
eomsangdonandClaude Opus 5.5 3eeb7c3cc3 fix(822-2b): 내 기계 연료 + 전력 둘 다 허용 · 새 식 둘(GC001200 연료 + 전력 · GC001201 + 조종원)
- 내 기계 저장의 「주연료 · 전력은 하나만」 막음 뺌 — 불러오기 자동 생성(경쟁사 파워트로웰 = 손료 + 휘발유 + 잡품 + 전력)이 막히던 것
- 꼴 고름(logic_key) 연료 + 전력 → GC001200 · + 조종원(직종 있음) → GC001201 · 직종 없는 조종원 = 까닭
- 조종원 줄을 한 식에 넣지 않음 — 수식 계산기가 빈 직종 「LB:」 를 못 받음(전력 식 GC001196 · 1197 짝과 같은 꼴)
- 수식 문서 두 줄 더함 · 802-2 대조 목록 다시 씀(두 줄 같음) · M01 정본 기계 · 다른 식 금액 그대로
- 낡은 시험: test_821_4 키대장 다음 번호 수 박은 기대 → 쓴 · 버린 번호보다 뒤(결번 재사용 없음) ·
  test_822_2 연료 + 전력 None · 저장 400 기대 → 새 식 · 미리보기 = 서버 값

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

241 lines
13 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 에서 받아 씀
"""821-4 지킴 — 불러오기에서 끝내 못 이은 기계 = 내 기계 자동 생성(요구서_796 4 · 5 장 · 임시 storage).
① 구성 → 우리 틀 값(EST 원파일 발전기 5.5kW — 손료계수 · 가격 · 경유 0.625ℓ · 잡품 11 % · 일반기계운전사 1 인 ·
경쟁사 노임 환산 안 가져옴) · 엑셀 「기계경비」 · STmate 「중기사용료」 · 구성 없음 = 직접 꼴 · 달러 = 환율
② 층 저장 길(`FromHopyo.make`) = 만듦 · 다시 불러와도 중복 없음 · 같은 이름 있으면 그것 · 미리 보기는 안 만듦 ·
금액(엑셀 단가 그대로) 그대로 · [우리 단가로] = 내 기계 값 · 「못 이음」 에서 빠짐 · 사본에 박힘
③ 820-1 정리 — 13 줄 · GC001195 없음(대장 폐기 · 결번) · 덤프 1 톤 · 체인톱 식 · 별칭 그대로 ·
빈칸 든 입력 이름 이음 글 왕복(820-2 지킴 옮김) · EST main 504 파워트로웰 블록 금액 546 그대로
"""
from __future__ import annotations
import json
from functools import lru_cache
from pathlib import Path
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Doc_FromNative as fn
from M02_MasterTemplete import M02_Doc_Import as dim
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_San_Book as book
from M02_MasterTemplete import M02_San_Estx as estx
from M02_MasterTemplete import M02_San_MyMachine as mm
from M02_MasterTemplete import M02_Sangeun_Copy as skc
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh
from M02_MasterTemplete import M02_Sangeun_Imported as imp
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
from resources.tester.test_815_1_import_link import aliases, no_aliases # M01 별칭 칸 임시
ROOT = Path(__file__).resolve().parents[2]
MAIN = ROOT / "resources/knowledge/original/경쟁사 프로그램/EST Plus 2/다산소프트/2026-07-main.estx"
GEN = "E0000001016" # EST main 발전기 5.5kW
GONE = [f"EQ{n:06d}" for n in range(656, 669)]
@pytest.fixture(autouse=True)
def _blank_aliases(request):
"""진짜 마스터 기계 별칭(파워트로웰 = EQ000534 등) 유무와 무관 — 별칭 없는 사본에서 잼(820-1 정리 시험만 진짜 마스터)."""
if request.node.name == "test_cleanup_820_1":
yield
return
with no_aliases("EQ"):
yield
@lru_cache(maxsize=1)
def _main_raw() -> bytes:
return MAIN.read_bytes()
@lru_cache(maxsize=1)
def _main() -> dict:
return fn.read(MAIN.name, _main_raw())
def _excel(tag: str) -> dict:
import test_797_1a_import_flags as practice
p = practice.FILES.get(tag)
if not p or not p.exists():
pytest.skip(f"실무 엑셀 없음 {tag}")
return book.sheets(p.name, p.read_bytes())
def _line(name: str, spec: str, **res) -> dict:
found = {"판정": "못 찾음", "이름": name, "규격": spec}
return {"종류": "기계", "이름": name, "자원": {"갈래": "기계", "이름": name, "규격": spec, "찾기": found, **res}} # fmt: skip
def _labor(name: str) -> str:
return next(k for k, r in m01.loaded()[1].numbers["LB"].items() if r["이름"] == name)
# ── ① 구성 → 우리 틀 값 ───────────────────────────────────────────────
def test_est_native_generator_values() -> None:
make = estx.makes # 816-2 대조 꼴은 그대로(손료 · 주연료 · 조종원 있음만)
assert make is not None
got = _main()["기계구성"][GEN]
assert got["손료"] == [[2362.0, 1063000.0, False]] and got["주연료"] == 0.625
assert (got["주연료종류"], got["잡재료율"], got["조종원"], got["조종원직종"]) == ("경유", 11.0, 1.0, "일반기계운전사") # fmt: skip
body = mm.body(got, 0, _line("발전기", "5.5kW", 코드=GEN), "main.estx")
want = {"가격": 1063.0, "손료계수": 2362.0, "주연료": 0.625, "주연료종류": "경유", "잡재료율": 11.0,
"조종원": 1.0, "조종원직종": _labor("일반기계운전사")} # fmt: skip
assert body["방식"] == "바로" and body["값"] == want
assert body["비고"].startswith("불러오기 자동 생성 — main.estx · 값 확인 필요")
def test_dollar_and_combo_pieces() -> None:
made = _main()["기계구성"]
pipe = mm.body(made["E0000000623"], 0, _line("콘크리트펌프용 파이프", "150㎜×1m"), "f")
rate = float(m01.loaded()[1].index["FX"]["FX000001"]["값"])
assert abs(pipe["값"]["가격"] - 25 * rate / 1000) < 1e-3 and "$25" in pipe["비고"]
combo = made["E0000001170"] # 대형브레이카 +굴착기(1.0㎥) — 조각 둘
first = mm.body(combo, 0, _line("굴착기", "1.0㎥"), "f")["값"]
second = mm.body(combo, 1, _line("대형브레이카", "1.0㎥"), "f")["값"]
assert first["손료계수"] == 2085 and first["주연료"] == 19.5 and first["조종원"] == 1
assert second == {"가격": 28692.0, "손료계수": 6601.0} # 다음 조각 = 손료만
assert mm.body(None, 1, _line("x", "1"), "f") is None # 구성 없는 조합 조각 = 안 만듦
def test_excel_sheets() -> None:
est = mm.xlsx_makes(_excel("2016사방"))[mm.key_of("발전기", "5.5kW")]
assert est["손료"] == [[2294.0, 1167000.0, False]] and est["주연료"] == 0.625
assert (est["잡재료율"], est["조종원"], est["조종원직종"]) == (11.0, 1.0, "일반기계운전사")
stm = mm.xlsx_makes(_excel("봉화"))["X00001"] # STmate 중기사용료 — 계수(천원당) × 10⁴
assert stm["손료"] == [[2085.0, 62550000.0, False]]
assert (stm["주연료종류"], stm["주연료"], stm["잡재료율"], stm["조종원직종"]) == ("경유", 5.0, 21.0, "건설기계운전사") # fmt: skip
def test_direct_without_make() -> None:
line = _line("소형기계", "1대", 단가={"L": 10, "M": 20, "S": 30})
got = mm.body(None, 0, line, "a.stc")
assert got["방식"] == "직접" and got["시간당"] == {"노무비": 10, "재료비": 20, "경비": 30}
# ── ② 층 저장 길 ──────────────────────────────────────────────────────
TROWEL = 504 # EST main 「콘크리트 바닥 마감(Power Trowel)」 — 파워트로웰 3.73kW = 별칭 없는 사본에서 못 이음(별칭 있으면 EQ000534)
WANT = ("파워트로웰", "3.73kW")
def test_generator_ours_formula(world) -> None:
"""발전기 5.5kW 내 기계 시간당 노무비 = 우리 노임 ÷ 8 × 1 인(EST 의 ×16/12 ×25/20 안 씀)."""
me = world["personal"]
me.mkdir(parents=True, exist_ok=True)
body = mm.body(_main()["기계구성"][GEN], 0, _line("발전기", "5.5kW"), "main.estx")
with store.at(me):
key = mym.create(body)["줄"]["키"]
hour = mh.hourly(key)
lb = m01.loaded()[1].numbers["LB"][_labor("일반기계운전사")]
assert hour["ok"] and hour["로직"] == "GC000268"
assert abs(hour["노무비"] - float(lb["값"]) / 8) < 1
def test_make_flow(world) -> None:
me = world["personal"]
me.mkdir(parents=True, exist_ok=True)
with store.at(me):
hp = fh.upload_native(me, MAIN.name, _main_raw(), {"이름": "sub3임시"})["키"]
out = fh.emit(me, hp, TROWEL) # 미리 보기 = 안 만듦 · 예정만
assert {"이름": WANT[0], "규격": WANT[1]} in out["원본"]["내기계예정"]
assert mym.rows() == []
got = fh.make(me, hp, TROWEL)
made = {(m["이름"], m["규격"]): m for m in got["내기계"]}
one = made[WANT]
assert one["새로"] and one["키"].startswith("EQP42-") and one["방식"] == "바로"
assert got["검산"]["같음"] # 금액(엑셀 단가 그대로) 그대로
n = len(mym.rows())
again = fh.make(me, hp, TROWEL) # 다시 불러옴 = 중복 없음 · 같은 SK
assert len(mym.rows()) == n and again["다시씀"]
assert not any(m["새로"] for m in again["내기계"])
doc = store.read("sangeun", got["이름"])["문서"]
assert doc[imp.KEY]["원본"]["내기계"][0]["키"] == one["키"]
items = doc[imp.KEY]["로직"]["호표"]
mine = [x for x in items if (x.get("자원") or {}).get("내기계") == one["키"]]
assert mine and all(f"개인 {one['키']}" in str(x.get("이음")) for x in mine)
assert not [
u for u in imp.unlinked(doc) if "파워트로웰" in u["이름"]
] # 「못 이음」 에서 빠짐
row = next(r for r in mym.rows() if r["키"] == one["키"])
assert row["값"]["가격"] == 2680 and row["값"]["주연료종류"] == "휘발유"
assert row["값"]["전력"] == 3.73 and "값 확인 필요" in row["비고"]
ours = imp.to_price(doc, "우리") # [우리 단가로] = 내 기계 값
assert ours["후"]["계"] is not None and ours["후"] != ours["전"]
assert not [u for u in ours["이음없음"] if "파워트로웰" in u["이름"]]
assert one["키"] in {r["키"] for r in (doc.get(skc.KEY) or {}).get("단가") or []} # 사본
conv = dim.derive(doc, None, imp._whole())
assert f"내 기계 {one['키']}" in json.dumps(
conv["변수"], ensure_ascii=False, default=str
) # 꼬리표
def test_reuse_same_name(world) -> None:
me = world["personal"]
me.mkdir(parents=True, exist_ok=True)
with store.at(me):
have = mym.create({"이름": WANT[0], "규격": WANT[1], "방식": "바로", "값": {"가격": 9, "손료계수": 9}}) # fmt: skip
out = fn.emit(_main(), TROWEL)
made = mm.attach(out, _main()["기계구성"])
assert [(m["키"], m["새로"]) for m in made] == [(have["줄"]["키"], False)]
assert len(mym.rows()) == 1
def test_not_personal_makes_nothing(world) -> None:
out = fn.emit(_main(), TROWEL)
with store.at(world["company"]):
assert mm.attach(out, _main()["기계구성"]) == []
assert out["원본"]["내기계예정"] == [{"이름": WANT[0], "규격": WANT[1]}]
# ── ③ 820-1 정리 ─────────────────────────────────────────────────────
def test_cleanup_820_1() -> None:
files, whole = m01.loaded()
eq = whole.index["EQ"]
assert not set(GONE) & set(eq) and "EQ000655" in eq and eq["EQ000655"]["규격"] == "1ton"
assert not [r for r in eq.values() if str(r.get("원문번호") or "").startswith("자체 ")]
logics = {r["키"] for d in files.values() if d.get("그룹") == "로직" for r in d.get("줄") or []}
assert "GC001195" not in logics and "GF000337" in logics and "GF000145" in logics
text = json.dumps(
[d for d in files.values() if d.get("그룹") == "로직"], ensure_ascii=False, default=str
)
assert "자체 발전기" not in text and "자체 콘크리트펌프용" not in text
led = json.loads((ROOT / "resources/master_data/_키대장.json").read_text("utf-8"))
assert all(k in led["폐기"] and k not in led["키"] for k in [*GONE, "GC001195"])
for pre in ("EQ", "GC"): # 결번 · 재사용 안 함 — 다음 번호가 쓴 · 버린 번호 모두보다 뒤
used = [int(k[2:]) for k in [*led["키"], *led["폐기"]] if k.startswith(pre)]
assert led["다음"][pre] > max(used)
alias = [r for r in eq.values() if r.get("별칭")]
assert len(alias) >= 10
def test_blank_input_name_roundtrip() -> None:
"""820-2 지킴 옮김 — 빈칸 든 입력 이름은 따옴표로 이음 글 → 읽기 왕복."""
text = f"로직(GC000268, {dim._q('전력 계약종별')}='일반용(갑)Ⅰ 저압', 기계='0101-0007')"
got = dim._logic_link(text)
assert got and got["입력"]["전력 계약종별"] == "'일반용(갑)Ⅰ 저압'"
def test_main_504_amount_same(world) -> None:
"""EST main 504 = 원파일 저장 금액 546 그대로 — 별칭 없는 사본 = 내 기계 예정 · 별칭 있으면 품셈 EQ000534."""
import test_810_2_native_doc as t
def plan() -> tuple[dict, list]:
out = fn.emit(_main(), 504)
with store.at(world["company"]): # 개인 층 아님 = 안 만들고 예정만 적음
mm.attach(out, _main()["기계구성"])
return out, out["원본"].get("내기계예정") or []
out, todo = plan()
assert {"이름": WANT[0], "규격": WANT[1]} in todo
assert float(t.doc_and_money(out)[2]["계"]) == 546
with aliases("EQ", {"EQ000534": "파워트로웰"}): # 승인 별칭 = 품셈 기계로 이어짐
out, todo = plan()
assert not [m for m in todo if m["이름"] == WANT[0]]
assert "EQ000534" in json.dumps(out, ensure_ascii=False, default=str)
assert float(t.doc_and_money(out)[2]["계"]) == 546