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>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
# 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"]
|
||||
Reference in New Issue
Block a user