까닭: 파워트로웰이 품셈 기계(EQ000534 별칭)로 이어져 「못 이은 기계」 견본이 사라짐. 820-1 정리 시험만 진짜 마스터. 504 시험 = 별칭 없을 때 내 기계 예정 · 별칭 있을 때 EQ000534 · 금액 546 그대로. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
239 lines
13 KiB
Python
239 lines
13 KiB
Python
# 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"])
|
||
assert led["다음"]["EQ"] == 669 and led["다음"]["GC"] == 1196 # 결번 · 재사용 안 함
|
||
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
|