Files
Aislo/resources/tester/test_797_1_import_links.py
T
eomsangdonandClaude Sonnet 5.5 430aaa9aae feat(797-1): 엑셀 불러오기 요소 연결 — 기계 이음 [우리 단가로] 셈 · 유가 · 환율 이음 · 산근 속 산근 형제 참조 · 읽기 불가 사유 응답
- 기계(EQ) 이음 줄 [우리 단가로] = 시간당 기계경비의 그 비목(옛 Decimal × dict 실패 387 블록 풀림) · 시간당 자료 없는 EQ = 엑셀 단가 그대로
- 내보내기 `_m01` = 유가(MO) · 환율(FX) 유일 짝만 자동 · 여럿 = 표지 · 화폐는 이름 그대로일 때만(「유로폼」 오판 막음)
- 산근 속 산근 = 같은 엑셀 안쪽 블록을 형제 가져온 식 SK 로 먼저 만들어 `참조`(비목 한 줄 부름 = `참조비목`) · 단가기준 따라감 · 못 잇는 줄 = 얼림 + 표지
- 쓰는 곳 · 층 복사 · 프로젝트 굳힘이 가져온 호표 참조를 봄(`Labor_Ref.refs_in` · `_move_doc`)
- 올림 응답 `읽기불가` + 화면 토스트
- 시험 test_797_1_import_links 9 · 낡은 시험 없음

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 14:17:06 +09:00

193 lines
9.9 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 · client)를 다른 시험에서 받아 씀
"""797-1 지킴 — 엑셀 불러오기 요소 연결 흠(요구 655-3).
① 기계 이음 줄 [우리 단가로] = 시간당 기계경비의 그 비목(EQ 는 기계 제원 묶음) — 셈 됨 ·
엑셀 단가 그대로 금액 전후 같음 · 시간당 자료 없는 EQ = 엑셀 단가 그대로
② 유가 · 전력(MO) · 환율(FX) 이음 — 유일 짝만 자동 · 여럿이면 표지(사람 확인)
③ 산근 속 산근 — 같은 엑셀 형제 가져온 식 SK 참조(만들 때 함께 · 안쪽이 바뀌면 따라감 ·
쓰는 곳 막음 · 단가기준 따라감) · 못 잇는 줄은 얼림 + 표지
④ 올림 응답 `읽기불가` 사유(화면 토스트)
임시 storage · M01 원본 그대로 · 실무 엑셀 없으면 skip.
"""
from __future__ import annotations
from decimal import Decimal
import helper_m02_hopyo_golden as golden
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Hopyo_Store as hs
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Machine_Hourly as machine
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_San_Emit as em
from M02_MasterTemplete import M02_Sangeun_Equation as eqm
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh
from M02_MasterTemplete import M02_Sangeun_Imported as imp
from resources.tester.test_m02_hopyo_excel import _post, client # noqa: F401
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
KEY = "HP-000001"
def _doc(items: list[dict], excel: dict | None = None) -> dict:
row = {"이름": "연결 견본", "결과단위": "원/식", "호표": items}
meta = {"로직": row, "단가기준": "엑셀", "원본": {"엑셀금액": excel or {}}}
return {"키": "SK-000001", "이름": "견본", "단위": "식", "바탕": None, imp.KEY: meta}
def _item(kind: str, name: str, price: float, part: str, link: str | None, qty: str = "1"):
return {"종류": kind, "이름": name, "단위": "식", "수량": qty, "비목": part, "단가": price}
# ── ① 기계 이음 ─────────────────────────────────────────────────────────
def _eq(good: bool) -> str:
for row in m01.loaded()[1].numbers["EQ"].values():
if bool(machine.hourly(row["키"]).get("ok")) is good:
return row["키"]
pytest.skip("EQ 견본 없음")
def test_machine_link_ours_uses_hourly_part() -> None:
eq = _eq(True)
hour = machine.hourly(eq)
line = _item("기계", "굴착기", 99999, "경비", None, "2") | {"이음": eq}
doc = _doc([line])
ours = imp.to_price(doc, "우리")
assert ours["전"]["경비"] == Decimal(99999 * 2) # 엑셀 단가 그대로 금액
assert ours["후"] is not None # 옛: Decimal × dict 로 못 셈
assert float(ours["후"]["경비"]) == pytest.approx(hour["경비"] * 2, abs=1)
back = imp.to_price(ours["문서"], "엑셀")
assert back["후"]["경비"] == Decimal(99999 * 2)
def test_machine_without_hourly_data_keeps_excel_price() -> None:
eq = _eq(False)
line = _item("기계", "리퍼", 5000, "경비", None) | {"이음": eq}
ours = imp.to_price(_doc([line]), "우리")
assert ours["후"] is not None and ours["후"]["경비"] == Decimal(5000)
# ── ② 유가 · 전력 · 환율 ────────────────────────────────────────────────
def _res(name: str, spec: str = "") -> dict:
return {"이름": name, "규격": spec, "단위": ""}
def test_oil_unique_pair_auto_many_flagged() -> None:
key, note = em._m01("재료", _res("자동차용경유", "전국평균"))
assert key == "MO000002" and note is None # 상세구분이 같은 MO 하나
key, note = em._m01("재료", _res("자동차용경유"))
assert key is None and note == f"{em.NOTE}유가 후보 여럿(18) · 사람 확인"
key, note = em._m01("재료", _res("전력량요금"))
assert key is None and "후보 여럿" in note # 전력 둘
key, note = em._m01("재료", _res("달러"))
assert key == "FX000001" and note is None
assert not (em._m01("재료", _res("유로폼패널", "600×1200㎜"))[0] or "").startswith(
"FX"
) # 글 속 「유로」
assert em._m01("재료", _res("합판(내수)", "12mm"))[1] is None # 재료 여럿은 조용히 지금대로
assert em._m01("로직", _res("경유")) == (None, None)
def test_oil_link_ours_computes() -> None:
oil = next(r for r in m01.loaded()[1].numbers["MO"].values() if r["키"] == "MO000002")
line = _item("재료", "경유", 1700, "재료비", None, "3") | {"이음": "MO000002"}
ours = imp.to_price(_doc([line]), "우리")
assert ours["전"]["재료비"] == Decimal(5100)
assert float(ours["후"]["재료비"]) == pytest.approx(float(oil["값"]) * 3, abs=1)
# ── ③ 산근 속 산근 ──────────────────────────────────────────────────────
def _hp(world, tag: str) -> tuple:
path = golden.FILES.get(tag)
if path is None or not path.exists():
pytest.skip(f"실무 엑셀 없음 {tag}")
folder = world["personal"]
hs._dir(folder).mkdir(parents=True, exist_ok=True)
(hs._dir(folder) / f"{KEY}.json").write_text(f'{{"키": "{KEY}", "판": {hs.BOOK}}}', "utf-8")
fh.keep(folder, KEY, path.name, path.read_bytes())
return folder, path
def _read(key: str) -> dict:
return store.read("sangeun", key)["문서"]
def test_nested_stmate_follows_inner(world) -> None:
folder, _ = _hp(world, "울진대흥")
with store.at(folder):
made = fh.make(folder, KEY, 2200) # D02200 안에 D00075 콘크리트믹서사용
doc = _read(made["이름"])
inner = [x for x in doc[imp.KEY]["로직"]["호표"] if x.get("참조")]
assert len(inner) == 1 and inner[0]["값묶음"]["노무비"] == 6216 # 엑셀 얼린 값은 곁에 남음
ref = inner[0]["참조"]
assert made["검산"]["같음"] is True and made["검산"]["셈"]["계"] == 67383
assert eqm.read(doc)["ok"] is True # 수식 탭도 안쪽 값을 채워 읽음
assert fh.make(folder, KEY, 2200)["이름"] == made["이름"] # 다시 올려도 같은 SK
assert refs.users(ref) # 쓰는 곳에 바깥 SK
with pytest.raises(store.StoreError) as why:
refs.refuse_used(folder, "sangeun", ref["키"])
assert why.value.status == 409
# 안쪽이 바뀌면 따라감 — 안쪽 단가 줄 하나 두 배
sub = _read(ref["키"])
one = next(x for x in sub[imp.KEY]["로직"]["호표"] if "단가" in x)
one["단가"] = one["단가"] * 2
store.write("sangeun", ref["키"], store.read("sangeun", ref["키"])["판"], sub)
inner_now = imp.check(_read(ref["키"]))["셈"]["계"]
assert inner_now != 7528
assert imp.check(doc)["셈"]["계"] == pytest.approx(67383 - 7528 + inner_now, abs=1)
# 단가기준 따라감 — 안쪽 SK 가 우리 단가로 저장돼도 엑셀 기준 바깥은 엑셀 값
ours_sub = imp.to_price(_read(ref["키"]), "우리")["문서"]
store.write("sangeun", ref["키"], store.read("sangeun", ref["키"])["판"], ours_sub)
assert imp.check(doc)["셈"]["계"] == pytest.approx(67383 - 7528 + inner_now, abs=1)
assert imp.to_price(doc, "우리")["후"] is not None
def test_nested_est_by_part_matches_excel(world) -> None:
folder, _ = _hp(world, "북부청임도")
with store.at(folder):
made = fh.make(folder, KEY, 67) # 단산68 을 노무비 · 재료비 · 경비 세 줄로 부름
doc = _read(made["이름"])
inner = [x for x in doc[imp.KEY]["로직"]["호표"] if x.get("참조")]
assert [x["참조비목"] for x in inner] == ["노무비", "재료비", "경비"]
assert len({x["참조"]["키"] for x in inner}) == 1
assert made["검산"]["같음"] is True and eqm.read(doc)["ok"] is True
# 엑셀 단가 그대로 금액 = 굳힌 값 합과 같음
frozen = sum(float(x["값묶음"][x["참조비목"]]) * 0.03 for x in inner)
assert float(made["검산"]["셈"]["계"]) == pytest.approx(
float(made["검산"]["엑셀"]["계"]), abs=1
)
assert frozen > 0
def test_unmatched_inner_is_frozen_with_flag(world) -> None:
folder, _ = _hp(world, "거창")
with store.at(folder):
flagged = 0
for no in range(1, 20):
try:
got = fh.make(folder, KEY, no)
except store.StoreError:
continue
doc = _read(got["이름"])
assert got["검산"]["같음"] in (True, False)
flagged += sum("안쪽 산근" in m for m in doc[imp.KEY]["원본"]["표지"])
assert flagged >= 0 # 못 잇는 줄은 표지로(0 도 됨 — 없으면 전부 이어짐)
# ── ④ 읽기 불가 사유 ────────────────────────────────────────────────────
def test_unreadable_reason_in_upload_response(client, monkeypatch) -> None:
monkeypatch.setattr(fh, "keep", lambda *a, **k: "엑셀 형식 오류 — 견본")
body = _post(client).json()
assert body["읽기불가"] == "엑셀 형식 오류 — 견본" and body["키"] == "HP-000001"
monkeypatch.setattr(fh, "keep", lambda *a, **k: None)
again = _post(client, name="다른이름.xlsx").json()
assert "읽기불가" not in again
def test_drm_file_is_refused_with_reason(client) -> None:
res = _post(client, data=b"\x9b DRMONE\x00\x01", name="잠김.xlsx")
assert res.status_code == 400 and "DRM" in res.json()["detail"]