Files
Aislo/resources/tester/test_815_1_import_link.py
T
eomsangdonandClaude Opus 5.5 29c2a5d91b feat(830-9): 재료 DB 항목 목록만 되돌림 — 지역 · 관급 · 사급 줄 다시 따로 · 상하차 글은 이름에서 뺌
- 요구 834 · 835 · 836 · 837 — 보기 · 관리 방식(열 구성 · 조달청 0 · 일반가격 · 상하차 고르기 · 가장 낮은 값 규칙) 그대로 · 항목 목록만 EST 그대로
- 빌더 통합 단계 걷음 — material_merge = 열 한 벌 · 상하차 판독(split_load) · place 만 남김(지역 · 관급/사급 묶기 걷음)
- 재료_자재품목 17,692 → 24,027 줄 = 830-1 24,092 − 재료 아닌 줄 65 · 키 = 830-1 키 · 새 키 0 · 키대장 = 830-1 대장 − 65 · 「다음」 그대로
- 830-1 대조: 키 · 구분 · 상세구분 · 단위 · 물가지 넷 · 출처 · 면수 같음 · 다른 것 = 열 구성 · 조달청 값 0 · 비고(조달 4,552 · 가격정보 2) · 상하차 글 뺌 384(이름 297 · 규격 87 — 요구 836)
- 상하차 = 이름 · 규격 끝 인도 조건 글(도착도 259 = 하차 · 상차도 류 125 = 상차)을 칸으로 옮기고 글은 뺌 · 나머지 -
- 빌더가 손 값(상하차 · 일반가격)을 원문번호로 이어 받음 · 빌더 두 번 = 같은 파일
- 사급/관급 열은 두지 않음(요구 837 — 단가산출 쪽 몫)
- 품셈재료 옮긴 줄 2(모래 · 자갈 건조상태 전국)의 830-2 통합 이름 · 비고를 830-1 글로 되살림
- 시험: test_830_2 를 이번 상태로(통합 규칙 시험 지움 · 상하차 글 뺌 · 줄 = EST 그대로 · 24,027) · 830-2 가 통합에 맞춰 바꾼 기대를 830-1 판으로 되돌림(814_1 · 815_1 · 827_2 · 827_3 · m02_hopyo_match · m01_pick · m01_api 「시멘트 전국」 xfail 걷음 · masterdata_check 자재지역) · 울진 레미콘 = 관급 줄뿐 → 「지역 값 없음」 · check_links 0

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

229 lines
12 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.
"""815-1 지킴 — 불러오기 이음(방향 장부 `_방향_불러오기.md` K 장 · 요구 753 ~ 765).
K1 EST 원파일 자재 = 우리 자재 줄 `출처` 칸 EST 코드로 바로(글 대조 없음) · 코드 없음 = 이름 길
K2 M01 요소 줄 「별칭」 칸 풀이(`master_alias` — 콤마 · 전각 콤마 · 큰따옴표 조각 · 규격) → 기계 · 재료 ·
인력 · 유가 이음(시험 안 임시 별칭 — 마스터 자료 안 고침)
K3 표기 정규화(M/M = mm · 곱셈표 · 지름 · 괄호 · 빈칸 · 전각 · 지역 꼬리) · 정확 일치만
K5 견적 줄 = 우리 자재에 이어지면 우리 값(재료 줄) · 아니면 그대로 + 「견적」 표시 · 못 이음 수에서 뺌
금액 = 엑셀 단가 그대로 셈이 원파일 저장 금액과 같음(견적 · 코드 이음 블록).
원파일 없으면 그 시험만 skip(다른 PC).
"""
from __future__ import annotations
import sys
from contextlib import contextmanager
from pathlib import Path
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_810_2_native_doc as t # noqa: E402
from M02_MasterTemplete import M02_Doc_FromNative as fn # noqa: E402
from M02_MasterTemplete import M02_Elem_Alias as ea # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Row as hrow # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Units as units # noqa: E402
from M02_MasterTemplete import M02_Machine_Find as mf # noqa: E402
from M02_MasterTemplete import M02_San_Link as link # noqa: E402
from M02_MasterTemplete import M02_Sangeun_Imported as imp # noqa: E402
am = ea.am
GONE = object()
@contextmanager
def aliases(table: str, pairs: dict[str, str]):
"""M01 줄 `별칭` 칸을 시험 동안만 바꿈(읽은 판 안 · 파일 안 씀) — 끝나면 되돌림."""
rows = ea.book()["줄"][table]
old = {k: rows[k].get("별칭", GONE) for k in pairs}
def clear() -> None:
ea._book.cache_clear()
hrow._m01_picks.cache_clear()
for k, v in pairs.items():
rows[k]["별칭"] = v
clear()
try:
yield
finally:
for k, v in old.items():
if v is GONE:
rows[k].pop("별칭", None)
else:
rows[k]["별칭"] = v
clear()
def no_aliases(table: str):
"""그 표 별칭 칸을 시험 동안 모두 비움 — 진짜 마스터에 별칭이 더 들어가도 시험이 안 흔들림."""
return aliases(table, {k: "" for k in ea.book()["줄"][table]})
# ── 별칭 풀이 ─────────────────────────────────────────────────────────────
def test_alias_names_rule() -> None:
assert am.names("") == [] and am.names(None) == []
assert am.names(" 철선 , 결속선,보통철선 ,, 철선") == ["철선", "결속선", "보통철선"]
assert am.names('"결속선(철선) #20, 0.9M/M", 철선') == ["결속선(철선) #20, 0.9M/M", "철선"]
assert am.names("“크레인(트럭) 15Ton, 할증”") == ["크레인(트럭) 15Ton, 할증"]
idx = am.index([{"키": "A", "별칭": "철선, 결속선"}, {"키": "B", "별칭": "결속선"}])
assert idx == {"철선": ["A"], "결속선": ["A", "B"]}
def test_norm_samples() -> None:
same = [
("0.9M/M", "0.9mm", "0.9m/m", "0.9㎜", "0.9 MM"),
("600×1200", "600x1200", "600X1200", "600*1200", "600 × 1200"),
("Φ100", "φ100", "ø100", "D=100", "d = 100"),
("유로폼(패널)", "유로폼 패널", "유로폼[패널]", "유로폼패널"),
("#20", "#20", "# 20"),
]
for group in same:
assert len({am.norm(x) for x in group}) == 1, group
assert am.norm("결속선(철선)") != am.norm("철선(결속선)") # 낱말 순서 = 별칭 몫
# ── K1 EST 코드 ────────────────────────────────────────────────────────────
def test_est_code_first() -> None:
assert ea.est_code("M0005250069") == ["MT000011"] # 치 즐(0.7㎥)
src = {"꼴": "EST", "종류": "자재", "갈래": "재료", "코드": "M0005250069", "이름": "엉뚱한 글", "규격": "", "단위": "EA"} # fmt: skip
assert link.m01("재료", src) == ("MT000011", None) # 글 대조 없이 코드
gone = {**src, "코드": "M0000000123", "이름": "치즐(0.7m3)"} # 공사가 넣은 코드 = 이름 길
assert link.m01("재료", gone) == ("MT000011", None)
assert ea.est_code("자재품목 M0005250069") == [] and ea.est_code(None) == []
# 공사 직접 번호 = 파일마다 다른 품목(거창 치즐 ↔ 우리 표 Air Hammer) → 코드로 안 이음 · 이름 길
assert ea.est_code("M0000000001") == []
local = {**src, "코드": "M0000000001", "이름": "치즐(0.7㎥)"}
assert link.m01("재료", local) == ("MT000011", None)
# ── K2 별칭 → 요소 ───────────────────────────────────────────────────────────
def test_alias_machine() -> None:
with no_aliases("EQ"):
assert mf.find("진공흡입기", "5kW")["판정"] == mf.NONE
assert mf.find("크레인(트럭)", "15Ton")["판정"] != mf.ALIAS # 별칭 전
with (
no_aliases("EQ"),
aliases("EQ", {"EQ000129": '"크레인(트럭) 15Ton"', "EQ000193": "아스팔트페이버(피니셔)"}),
):
got = mf.find("크레인(트럭)", "15Ton") # 이름 + 규격 통째 별칭 = 그 EQ
assert (got["판정"], got["키"]) == (mf.ALIAS, "EQ000129")
assert mf.find("크레인(트럭)", "15Ton(할증125%)")["키"] == "EQ000129" # 할증 글 안 봄
got = mf.find("아스팔트페이버(피니셔)", "3.0m") # 이름 별칭 = 그 기종 → 규격으로 하나
assert (got["판정"], got["후보"][0]["이름"], got["후보"][0]["규격"]) == (mf.ALIAS, "아스팔트 피니셔", "3m") # fmt: skip
with no_aliases("EQ"), aliases("EQ", {"EQ000129": "크레인 갑", "EQ000130": "크레인 갑"}):
assert mf.find("크레인 갑")["판정"] == mf.MANY # 같은 별칭 둘 = 후보 여럿
def test_alias_material() -> None:
line = {"이름": "결속선(철선)", "규격": "#20, 0.9M/M", "단위": "kg"}
assert link.m01("재료", dict(line))[0] is None # 별칭 전 = 못 이음(낱말 순서 다름)
with aliases("MT", {"MT000174": "결속선(철선)", "MT000175": "결속선(철선)"}):
assert link.m01("재료", dict(line)) == ("MT000174", None) # 같은 품목 지역 줄 = 서울
assert hrow.m01_picks("재료", line)[0]["키"] == "MT000174"
assert units.Units()._M({"묶음": "M", **line})["단위"] == {"자재": "MT000174"}
assert link.m01("재료", {**line, "규격": "#8"})[0] is None # 규격 다름 = 안 이음
with aliases("MT", {"MT000194": '"결속선(철선) #20, 0.9M/M(200m/kg)"'}):
got = link.m01("재료", {**line, "규격": "#20, 0.9M/M(200m/kg)", "단위": "M"})
assert got == ("MT000194", None) # 이름 + 규격 통째 별칭
def test_alias_labor_oil() -> None:
with aliases("LB", {"LB000003": "산림특인, 특인"}):
assert [x["키"] for x in hrow.m01_picks("인력", {"이름": "특인"})] == ["LB000003"]
assert link.m01("인력", {"이름": "산림특인", "규격": "", "단위": "인"}) == (
"LB000003",
None,
)
assert units.Units()._L({"이름": "특인"})["단위"] == {"노임": "LB000003"}
with aliases("MO", {"MO000002": "디젤유"}):
assert link.m01("재료", {"이름": "디젤유", "규격": "", "단위": "L"}) == ("MO000002", None)
# ── K3 표기 맞추기 ──────────────────────────────────────────────────────────
def test_exact_norm() -> None:
assert link.m01("재료", {"이름": "치즐(0.7m3)", "규격": "", "단위": "EA"}) == ("MT000011", None)
got = link.m01("재료", {"이름": "보통철선(결속선)", "규격": "#20, 0.9M/M", "단위": "kg"})
assert got == ("MT000174", None)
got = link.m01("재료", {"이름": "유로폼 패널", "규격": "600X1200mm", "단위": "매"})
assert (
got[0] is None or got[0] == "MT000467"
) # 규격 단위 꼬리(㎜) 다름 = 정확 일치 아님 · 지어 맞추기 없음
assert (
link.m01("재료", {"이름": "보통철선(결속선)", "규격": "#20, 1.0mm", "단위": "kg"})[0]
is None
)
# ── K5 견적 ───────────────────────────────────────────────────────────────
def test_quote_line() -> None:
ours = {"꼴": "STmate", "갈래": "고정", "코드": "Q1", "이름": "치즐(0.6m3)", "규격": "", "단위": "EA", "비목": "M", "단가": {"M": 9}} # fmt: skip
assert link.m01("고정", ours) == (
"MT000010",
None,
) # 우리 값 있는 자재 = 재료 줄(우리 값 · 엑셀 = 검산)
assert ours["갈래"] == "재료" and ours["견적"] is True
empty = {
**ours,
"갈래": "고정",
"이름": "치즐(0.7m3)",
} # 우리 줄 값 칸 빔(MT000011) = 견적 값 그대로
assert link.m01("고정", empty)[0] is None and empty["갈래"] == "고정"
keep = {"꼴": "EST", "갈래": "고정", "종류": "견적", "코드": "W0000001", "이름": "도선료", "규격": "8.0ton", "비목": "S"} # fmt: skip
key, mark = link.m01("고정", keep)
assert key is None and mark == f"{link.NOTE}{link.QUOTE}" and keep["갈래"] == "고정"
assert link.why({"종류": "로직", "자원": keep}) == link.QUOTE
def test_quote_money_and_tag() -> None:
saved = t.native("EST", "거창")
no = next(n for n, b in fn.numbers(saved).items() if b.get("이름") == "국가지점번호판")
out = fn.emit(saved, no)
quotes = [h for h in out["로직"]["호표"] if (h.get("자원") or {}).get("견적")]
assert len(quotes) == 2 and all(not h.get("이음") for h in quotes)
assert sum(str(m).endswith(link.QUOTE) for m in out["원본"]["표지"]) == 2
_sk, doc, got = t.doc_and_money(out)
want = fn.numbers(saved)[no]["저장"]
assert all(abs(float(got[k]) - float(want.get(c) or 0)) < 0.5 for c, k in t.COST)
free = [x for x in imp.unlinked(_sk) if x.get("견적")]
assert len(free) == 2 and all(x["까닭"] == link.QUOTE for x in free)
notes = [v.get("설명") for v in doc["변수"] if v.get("설명") == link.QUOTE]
assert len(notes) == 2
def test_quote_kept_money_same() -> None:
saved = t.native("STC", "소광22")
out = fn.emit(
saved, 96
) # 구조물헐기 — 치즐(0.7m3) 견적 · 우리 치즐 0.7 값 칸 빔 = 견적 값 그대로 + 표지
hit = [h for h in out["로직"]["호표"] if (h.get("자원") or {}).get("견적")]
assert [(h["종류"], h["이음"]) for h in hit] == [("로직", None)]
assert any(str(m).endswith(link.QUOTE) for m in out["원본"]["표지"])
_sk, _doc, got = t.doc_and_money(out)
want = fn.numbers(saved)[96]["저장"]
assert all(abs(float(got[k]) - float(want.get(c) or 0)) < 0.5 for c, k in t.COST)
def test_est_local_code_block_money() -> None:
"""거창 = 자재가 모두 공사 직접 번호(M0000000…) → 이름 길 · 치즐(0.7㎥) = 우리 치즐 0.7(Air Hammer 아님) ·
금액 = 원파일 저장 그대로(전 블록)."""
saved = t.native("EST", "거창")
chisel = set()
for no in fn.numbers(saved):
out = fn.emit(saved, no)
for h in out["로직"]["호표"]:
r = h.get("자원") or {}
if r.get("종류") == "자재":
assert str(r.get("코드")).startswith(ea.LOCAL) or r.get("코드") == "M0004113991"
if r.get("코드") == "M0000000001":
chisel.add(h["이음"])
_sk, _doc, got = t.doc_and_money(out)
want = fn.numbers(saved)[no]["저장"]
assert all(abs(float(got[k]) - float(want.get(c) or 0)) < 0.5 for c, k in t.COST), no
assert chisel == {"MT000011"}