Files
Aislo/resources/tester/test_814_1_import_decisions.py
T
eomsangdonandClaude Opus 5.5 bed35e3bc3 feat(814-1 3·4·6·7·8·9·10): 불러오기 결정 반영 — 세 줄 묶기 · 꼬리 뗀 맞대기 · 유가 · 재료 · 까닭 · 가벼움
- 3 연결 찾기 = 엑셀 이름 규격 · 단위 꼬리 뗀 이름으로 맞댐(`M02_Doc_Match_Note.bare` · 보이는 이름 그대로) · STmate 블록 규격도 가져온 칸에
- 4 기계 세 줄 = 기계 한 줄 — STmate X 코드 노 · 재 · 경 세 참조 줄(같은 수량 · 번지) 묶음(묶어서 셈 못 하는 세 줄만 그대로) ·
  문서 단계 묶기 넓힘(차례 아무렇게 · 시간당 경비 이음 없으면 엑셀 값 묶음 · 번지 같을 때) · 조합 다음 줄 = 세 줄 뒤 · 같은 기계 한 항목
- 6 공구손료 율 비목 차이(재료비 ↔ 경비) = 참고(`M02_Doc_Match_Value.tool_cost`) — 확정 안 막음
- 7 못 이은 줄 까닭(`M02_San_Link.why` — 자재 없음 · 운임표 없음 · 기계 표에 없음 · 별칭 대기 · 후보 여럿 · 시간당 경비 없음 · 조합) =
  문서 설명 · 이음 없는 자원 띠 · 연결 「없음」 띠 = 「우리 기초 없음 — 우리 정보로 셈 n/m 줄」 · 형제만 = 「조합」
- 8 유가 = 전국평균 줄 + 지역 옵션(`MO:경유:{S_지역}`) · 전력 = 일반용(갑)Ⅰ 저압 + 계약종별 옵션 · 엑셀 값 = 검산 칸
- 9 재료 = 지역 꼬리 뗀 이름 · 규격 정확 일치면 이음(지역 여럿 = 줄 지역 › 서울)
- 10 조합 첫 줄 규격 = 그 기계 · 얼린 줄도 소계 식에(북부청임도 33) · 기계 줄 찾기 판정 늘 붙음 · 올린 직후 409(HP 보이기 전 산근 보관) ·
  엑셀 블록 「엑셀 읽기 확정」 글 · 전수 도우미 str() 뗌 · 줄 쌓기 이름에 규격이 들면 한 번만
- M02_San_Link 새 파일(San_Emit 700 줄 막음 — 요소 잇기 · 시간당 · 조합 · 까닭)
- 시험: 새 test_814_1 · 812-2 · 810-4 거푸집 41 = 근접 → 확정(공구손료 비목 = 값 · 요구 744 C1) · 바꾸기 거절 견본 = 19 ·
  812-9 조합 첫 줄 규격 「0.7+브레이카」 → 「0.7」(칩 글 · 요구 738 흠 12)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:47:08 +09:00

261 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 에서 받아 씀
"""814-1 지킴 — 불러오기 결정 열 가지(방향 장부 `_방향_불러오기.md` E 표 · 요구 742 ~ 750).
① 파일 하나(같은 공사 붙이기 걷음) ② 확장자 파일 = 단가산출표 판(산출근거 머리) · 일위대가 없음 ·
③ 엑셀 이름 규격 · 단위 꼬리 뗀 맞대기 ④ 기계 세 줄 = 한 줄(STC · EST · 엑셀) · 조합 다음 줄 기계마다 한 항목 ·
⑤ 계산식 기계 목록(빠진 기계 더함 · 기본값 그대로) · 조합 = 우리 조합 식 ⑥ 공구손료 비목 = 참고 ·
⑦ 못 이은 줄 까닭 · 「없음」 띠 ⑧ 유가 = 전국평균 + 지역 옵션 · 전력 = 일반용(갑)Ⅰ 저압 + 계약종별 옵션 ·
⑨ 재료 지역 꼬리 뗀 정확 일치 ⑩ 가벼움(조합 첫 줄 규격 · 소계 검산 · 기계 찾기 판정 · 409 · 확정 글 · 전수 도우미).
원파일 · 실무 엑셀 없으면 그 시험만 skip(다른 PC).
"""
from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
import pytest
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
for p in (ROOT, HERE):
if str(p) not in sys.path:
sys.path.insert(0, str(p))
import test_797_1a_import_flags as practice # noqa: E402
import test_810_2_native_doc as t # noqa: E402
from M02_MasterTemplete import M02_Doc_FromNative as fn # noqa: E402
from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402
from M02_MasterTemplete import M02_Doc_MatchSk as msk # noqa: E402
from M02_MasterTemplete import M02_Doc_Match_Note as note # noqa: E402
from M02_MasterTemplete import M02_Doc_NativeLayout as lay # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Store as hs # noqa: E402
from M02_MasterTemplete import M02_Machine_Hourly as mh # noqa: E402
from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402
from M02_MasterTemplete import M02_San_Emit as em # noqa: E402
from M02_MasterTemplete import M02_San_Link as link # noqa: E402
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh # noqa: E402
from M02_MasterTemplete import M02_Sangeun_Imported as imp # noqa: E402
from resources.tester.test_m02_labor_ref import world # noqa: E402, F401 — 같은 임시 층
SCRIPT = ROOT / "resources/master_data/scripts/build_기계목록_빠진기계_814_1.py"
FAMILY = [f"GC0002{n}" for n in range(67, 77)]
def excel(tag: str) -> dict:
path = practice.FILES.get(tag)
if not path or not path.exists():
pytest.skip(f"실무 엑셀 없음 {tag}")
from M02_MasterTemplete import M02_San_Xlsx as sx
return fn.pack(sx.read(path.name, path.read_bytes()), path.name)
def doc_of(saved: dict, no: int) -> tuple[dict, dict, dict]:
out = fn.emit(saved, no)
sk, doc, got = t.doc_and_money(out)
return out, doc, got
def same_money(saved: dict, no: int, got: dict) -> bool:
want = fn.numbers(saved)[no].get("저장") or {}
return all(abs(float(got[k]) - float(want.get(c) or 0)) < 0.5 for c, k in t.COST[:3])
# ── ① ② 파일 하나 · 단가산출표 판 ──────────────────────────────────────────
def test_one_file_and_list_sheet(world) -> None:
assert not hasattr(fn, "same") and not hasattr(fh, "_natives") # 붙이기 길 걷음
stc = t._file(t.STC["봉화"])
folder = world["personal"]
with store.at(folder):
got = fh.upload_native(folder, stc.name, stc.read_bytes(), t.WHO)
assert got["원파일"] and got["호표수"] == 0 and "붙음" not in got
grid = hs.grid(folder, got["키"])
blocks = fn.numbers(t.native("STC", "봉화"))
assert grid["보기"] == {"단가산출표": "s1"} and len(grid["단산"]) == len(blocks)
head = grid["책"]["시트"][0]["칸"]
assert [head[f"{c}1"]["값"] for c in "ABCDEFGH"] == list(fn._LIST_HEAD)
first = next(iter(blocks.values()))
assert head["A2"]["값"] == first["코드"] and head["H2"]["값"] == first["저장"]["T"]
est = fn.hopyo(t.native("EST", "거창"))
assert est["호표"] == [] and len(est["단산"]) == len(fn.numbers(t.native("EST", "거창")))
# ── ③ 꼬리 뗀 맞대기 ─────────────────────────────────────────────────────
def test_bare_name_tail() -> None:
doc = {
"이름": "구조물터파기(인력) 경질토사 / M3",
"결과단위": "원/M3",
"가져온": {"규격": "경질토사"},
}
assert note.bare(doc)["이름"] == "구조물터파기(인력)" and doc["이름"].endswith("/ M3")
plain = {"이름": "측구터파기", "결과단위": "원/㎥", "가져온": {"규격": "토사"}}
assert note.bare(plain) is plain
# ── ④ 세 줄 묶기 · 조합 그룹 ─────────────────────────────────────────────
def _machine_split(doc: dict, out: dict) -> int:
"""문서에 남은 기계 비목 낱 줄(같은 기계 · 같은 수량 · 비목 셋이 따로) 묶음 수."""
names = {h["이름"]: h for h in out["로직"]["호표"]}
seen: dict[tuple, set] = {}
for m in doc["금액"]:
res = (names.get(m.get("이름")) or {}).get("자원") or {}
if res.get("갈래") == "기계" and m.get("비목") in ("노무비", "재료비", "경비"):
seen.setdefault((res.get("코드"), str(m.get("수량"))), set()).add(m["비목"])
return sum(1 for v in seen.values() if len(v) == 3)
def test_three_lines_one_machine_stc() -> None:
saved = t.native("STC", "봉화")
out, doc, got = doc_of(saved, 55)
assert same_money(saved, 55, got) and _machine_split(doc, out) == 0
crawl = next(m for m in doc["금액"] if str(m["이름"]).startswith("크롤러드릴"))
assert crawl["비목"] == "묶음" # 「노 무 비 · 재 료 비 · 경 비」 → 기계 한 줄
assert not any(str(h["이름"]).replace(" ", "") == "노무비" for h in out["로직"]["호표"])
def test_three_lines_excel_stmate_and_est_combo_group() -> None:
saved = excel("봉화")
out, doc, got = doc_of(saved, 55)
assert same_money(saved, 55, got) and _machine_split(doc, out) == 0
est = t.native("EST", "main")
out, doc, got = doc_of(est, 360)
assert same_money(est, 360, got)
titles = [it["제목"] for it in lay.stack_of(doc)]
assert titles.count("덤프트럭 자동덮개시설 15톤용") == 1 # 다음 줄 셋 = 한 항목(엇갈림 없음)
# ── ⑤ 계산식 기계 목록 · 조합 식 ─────────────────────────────────────────
def _build():
spec = importlib.util.spec_from_file_location("build_814_1", SCRIPT)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def test_machine_lists_complete_and_defaults_kept() -> None:
build = _build()
add, skipped = build.plan()
assert add == {} # 빠진 기계 모두 더함(다시 돌려도 같음)
assert all(miss for *_, miss in skipped) # 못 넣음 = 기계표 빈 값(지어 넣지 않음)
docs = json.loads(build.DOC.read_text("utf-8"))["줄"]
for d in docs:
if d["키"] in FAMILY:
var = next(v for v in d["변수"] if v["열쇠"] == "기계")
assert var["연결"]["값"] == var["연결"]["목록"][0] # 기본값 = 첫 기계 그대로
whole = imp._whole()
eq = {r.get("원문번호"): k for k, r in whole.index["EQ"].items()}
for code in ("0230-0007", "0211-0060", "6630-0005", "0213-0060"): # 브레이커 · 굴착기(타이어) …
if code in eq:
assert mh.hourly(eq[code])["ok"], code
def test_combo_uses_our_combo_formula() -> None:
saved = excel("거창")
out, doc, got = doc_of(saved, 2)
assert same_money(saved, 2, got)
main = next(h for h in out["로직"]["호표"] if (h.get("자원") or {}).get("조합첫"))
part = next(h for h in out["로직"]["호표"] if (h.get("자원") or {}).get("조합"))
assert str(main["이음"]).startswith("로직(GC000271") and part["이음"] is None
assert "조합" in link.why(part)
assert main["규격"] != (main["자원"] or {}).get("규격") # 칩 글 = 그 기계 규격(조합 글 아님)
# ── ⑥ 공구손료 비목 = 참고 ───────────────────────────────────────────────
def test_tool_rate_cost_is_value() -> None:
a = {
"갈래": "율",
"이름": "공구손료",
"율": "3",
"대상": {("인력", "노무비")},
"비목": ["재료비"],
}
b = {**a, "비목": ["경비"]}
left, right = [a], [b]
def take(test):
hits = [(x, y) for x in list(left) for y in list(right) if test(x, y)][:1]
for x, y in hits:
left.remove(x), right.remove(y)
return hits
diffs, notes = note.rate_pairs(left, right, take)
assert not diffs and notes[0]["항"] == "공구손료 비목"
# ── ⑧ ⑨ 유가 · 전력 · 재료 ───────────────────────────────────────────────
def test_oil_power_material_links() -> None:
oil, _ = link.m01("재료", {"이름": "경 유", "규격": "", "단위": "ℓ"})
power, _ = link.m01("재료", {"이름": "전 력", "규격": "", "단위": "KWH"})
whole = imp._whole()
assert whole.get(oil)["상세구분"] == "전국평균"
assert whole.get(power)["상세구분"] == "일반용(갑)Ⅰ 저압"
got = dim._oil(oil)
assert got["키"] == "MO:경유:{S_지역}" and got[dim.OPTION]["연결"]["값"] == "전국평균"
got = dim._oil(power)
assert got["계약종별"] == "S_전력계약종별"
assert got[dim.OPTION]["연결"]["값"] == "일반용(갑)Ⅰ 저압"
ply, _ = link.m01("재료", {"이름": "합판(내수)", "규격": "12t×1220×2440mm", "단위": "㎡"})
nail, _ = link.m01("재료", {"이름": "철못", "규격": "N 25", "단위": "kg"})
assert whole.get(ply)["이름"] == "합판(내수), 서울" and whole.get(nail)["규격"] == "N 25"
assert link.m01("재료", {"이름": "접착제", "규격": "", "단위": "kg"}) == (None, None)
def test_oil_doc_region_option() -> None:
saved = excel("2016사방")
out, doc, got = doc_of(saved, 326)
assert same_money(saved, 326, got)
region = next(v for v in doc["변수"] if v["열쇠"] == "지역")
assert region["연결"]["값"] == "전국평균"
oil = next(v for v in doc["변수"] if str((v.get("연결") or {}).get("키", "")).startswith("MO:"))
assert "엑셀" in oil["연결"] # 엑셀 값 = 검산용
# ── ⑦ 까닭 · 띠 ──────────────────────────────────────────────────────────
def test_unlinked_reasons_and_tally(monkeypatch) -> None:
from M02_MasterTemplete import M02_Hopyo_Row as hrow
monkeypatch.setattr(hrow, "m01_picks", lambda kind, line: []) # 우리 자재표에 후보 없음
assert link.why({"자원": {"갈래": "재료", "이름": "없는자재글자", "규격": ""}}).startswith(
"자재 없음"
)
assert (
link.why({"자원": {"갈래": "재료", "이름": "구역화물 운임", "규격": ""}}) == "운임표 없음"
)
gone = {"자원": {"갈래": "기계", "찾기": {"판정": "못 찾음", "까닭": "기종 모름"}}}
assert link.why(gone) == "별칭 대기"
sib = {"호표": [{"이름": "a", "참조": {"키": "SK-1"}, "자원": {"갈래": "단가산출"}}]}
assert link.tally(sib) == {"됨": 1, "줄": 1, "조합": True}
src = (ROOT / "M02_MasterTemplete/logic/M02_Sangeun_Match.ts").read_text("utf-8")
assert "우리 기초 없음" in src and "우리 정보로 셈" in src
assert "우리정보" in Path(msk.__file__).read_text("utf-8")
# ── ⑩ 가벼움 ─────────────────────────────────────────────────────────────
def test_light_fixes(world) -> None:
saved = excel("북부청임도")
out = fn.emit(saved, 33)
chk = imp.check(t.sk_of(out), t.whole())
assert chk["같음"] and all(x["같음"] for x in chk["줄"]) # 얼린 줄도 소계 식에
src = {"이름": "진동 로울러(핸드가이드식)", "규격": "0.7Ton", "단위": "hr"}
link.m01("기계", src)
assert src["찾기"]["판정"] and src["찾기"]["이름"] # 기계 찾기 판정 붙음
seen = []
folder = world["personal"]
with store.at(folder):
def first(key: str) -> None: # HP 를 목록에 보이기 전에 산근 보관(409 막음)
seen.append(any(r["키"] == key for r in hs.rows(folder)))
hs.save(folder, "가.xlsx", {"호표": [], "단산": []}, t.WHO, None, "지문814", first)
assert seen == [False]
origin = (ROOT / "M02_MasterTemplete/logic/M02_Sangeun_Origin.ts").read_text("utf-8")
assert "엑셀 읽기 확정" in origin
helper = (HERE / "helper_810_4_sweep.py").read_text("utf-8")
assert "str(got[k])" not in helper
one = em.first_spec({"규격": "0.7+브레이카"}, [{"규격": "0.7㎥"}, {"규격": "0.7"}])
assert one["규격"] == "0.7㎥"