Files
Aislo/resources/tester/test_812_11_match_values.py
T
eomsangdonandClaude Opus 5.5 28acddb4b9 feat(812-11): 연결 찾기 값 기준 넓힘 — 같은 기종 규격 · 수량 = 참고 · 바꾸기 때 기계 줄 갈아 끼움 · 기계 작업 갈래
- 새 M02_Doc_Match_Value — 같은 기종(이름 머리 + 괄호 낱말 · 조합 구성) 규격만 다름 = 참고 「규격」 + 바꿈 · 같은 자원 수량 다름 = 참고 「수량 끝자리」 · 「품 숫자」 · 「수량」 · 같은 배 두 줄 넘음 = 단위 환산(로직) · 기계 한 묶음 + 작업량 식 = 「기계 작업」(수량 · 규격까지 같은 기초만 확정) · 작업량 틀 꼴 참고 · 옵션 값으로 못 가름 = 「옵션 확인」
- M02_Doc_Match — 사실에 단가 기호 · 규격 · 수량 글 · 조합 표지 · 값 참고 적은 조합 먼저 · 선택 글 근거 쉼표 무시(D1,800 = D1800) · 같은 자원 아닌 수량 짝 = 그 로직 까닭
- M02_Doc_MatchSk — 바꾸기 뒤 사본 문서 기계 입력(원문번호) · 줄 이름 · 규격 · 묶음 제목 = 가져온 규격(수식 길 저장) · 되돌리기 = 보관 몸 수식 사본 그대로
- 띠 — 기계 작업 「기계 한 줄 단가산출 — 그대로 씀 · 값은 [단가 갱신]으로 우리 값」 · 참고 요약에 규격 · 수량 · 옵션 확인
- helper_810_4_sweep — 과녁 = 부품 뺀 문서(화면과 같음)
- 시험 고침: 810_4 단위 환산 = 같은 배 두 줄(한 줄만 배 = 수량 값) · 812_2 기계 규격 = 무한궤도 ↔ 타이어(같은 기종 규격 = 값)
- 전수 2,220: 확정 184 → 337 · 근접 576 → 848 · 없음 1,460 → 837 · 기계 작업 198 · 확정 다시 셈 다름 0 · 목록 _목록_812-11_연결판정.xlsx

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

240 lines
14 KiB
Python

# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""812-11 지킴 — 연결 찾기 값 기준 넓힘(요구 733 · 734 · 735 · `M02_Doc_Match_Value`).
(a) 줄 대조 — 같은 기종 규격만 다름 = 확정 + 참고 「규격」(바꿈 = 가져온 원문번호) · 무한궤도 ↔ 타이어 = 다른 항 ·
수량 다름 = 참고(끝자리 · 품 숫자 · 수량) · 같은 배가 두 줄 = 단위 환산(다른 항) · 줄 구성 다름 = 다른 항
(b) 견본 — main 230 되메우기(백호 0.2㎥ 70 % + 인력 30 %) ↔ GF000113 확정 + 규격 · 수량 · main 850 PC 슬래브(2ton 미만)
↔ GC000257 확정 + 수량 끝자리 · main 828 조립식 PC 맨홀(D1800) ↔ GC000252 확정 + 품 숫자(옵션 ㉠ 글 「D1800」 =
D1,800) · 거창 46 토사절취(백호 0.7㎥) = 기계 작업 + 작업량 틀 · 거창 19 파형강관 Φ800 ↔ GF000179 근접 그대로
(c) 옵션 확인(㉢) — 값으로 못 가른 선택 = 고른 값 + 참고 「옵션 확인」
(d) 바꾸기(임시 층) — main 230: 사본 기계 줄 = 가져온 규격(0201-0020) · 금액 = M01 문서에 그 기계만 바꾼 셈 ·
되돌리기 = 원래 금액 · 기계 작업 SK 바꾸기 = 409 · M01 문서 파일 그대로
(e) 전수(S81211_ALL=1) — 2,220 블록 · 오류 0 · 확정 다시 셈 다름 0 · 기계 작업 있음 · 근접 까닭에 값 항 없음
원파일 없으면 (b) · (d) · (e) skip(다른 PC).
"""
from __future__ import annotations
import copy
import json
import os
import subprocess
import sys
from decimal import Decimal as D
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_810_2_native_doc as t2 # noqa: E402
import test_810_4_match as t4 # noqa: E402
from M02_MasterTemplete import M02_Doc_Match as dm # noqa: E402
from M02_MasterTemplete import M02_Doc_Match_Value as val # noqa: E402
from M02_MasterTemplete import M02_Doc_MatchSk as msk # noqa: E402
from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh # noqa: E402
from resources.tester.test_m02_labor_ref import world # noqa: E402, F401 — 같은 임시 층
TOT = {"노무비": 9, "재료비": 0, "경비": 0, "계": 9}
def _line(name, kind, qty, price, cost="노무비", code="", sym="", text=""):
q, p = D(qty), D(price)
return {"번": 0, "이름": name, "갈래": kind, "코드": code, "수량": dm._key(q), "단가": dm._key(p),
"비목": {cost: dm._key(q * p)}, "금액": dm._key(q * p), "합밖": False, "기호": sym,
"규격": "", "수량글": text or qty} # fmt: skip
MAN = _line("보통인부", "노임", "0.01", "100", code="LB000002")
# ── (a) 줄 대조 ──────────────────────────────────────────────────────────
def test_same_kind_spec_is_value_other_kind_is_logic() -> None:
a = [MAN, _line("굴착기", "기계", "0.03", "500", "경비", "0201-0020", "P_a", "1 ÷ Q")]
b = [MAN, _line("굴착기", "기계", "0.02", "600", "경비", "0201-0070", "P_b", "r ÷ Q")]
res = dm.verdict(a, b, TOT, TOT)
assert res["판정"] == dm.SURE and not res["다른"]
assert {x["항"] for x in res["참고"]} == {"규격", "수량", "단가 차이"}
spec = next(x for x in res["참고"] if x["항"] == "규격")
assert "0.2㎥ ↔ 0.7㎥" in spec["글"] and "바꾸면 가져온 규격" in spec["글"]
assert res["바꿈"] == [
{"기호": "P_b", "기계": "0201-0020", "이름": "굴착기(무한궤도)", "규격": "0.2㎥"}
]
same_qty = dm.verdict(a, [MAN, {**b[1], "수량": a[1]["수량"]}], TOT, TOT)
assert same_qty["판정"] == dm.SURE and len(same_qty["바꿈"]) == 1 # 수량 같아도 규격 = 값
tire = [MAN, _line("굴착기", "기계", "0.02", "600", "경비", "0211-0060", "P_b")]
crawler = [MAN, _line("굴착기", "기계", "0.02", "600", "경비", "0201-0060", "P_a")]
res = dm.verdict(crawler, tire, TOT, TOT) # 무한궤도 ↔ 타이어 = 다른 기종
assert res["판정"] != dm.SURE and [x["항"] for x in res["다른"]] == ["기계 규격"]
assert not res["바꿈"]
def test_combo_machine_is_its_own_kind() -> None:
one = _line("굴삭기(무한궤도)", "기계", "0.02", "5", "경비", "0201-0060", "P_a")
grab = _line("굴착기+부착용 집게 1.0㎥", "기계", "0.07", "5", "경비", "0201-0100", "P_b")
val.mark_combo({}, [one, grab]) # facts 가 붙이는 조합 표지
res = dm.verdict([MAN, one], [MAN, grab], TOT, TOT) # 조합 기계 ↔ 낱 기계 = 다른 기종
assert res["판정"] != dm.SURE and "기계 규격" in {x["항"] for x in res["다른"]}
pair = "로더(타이어)+소형노면파쇄기"
a = _line(pair, "기계", "0.1", "5", "경비", "0302-0095", "P_a")
val.mark_combo({}, [a])
res = dm.verdict([MAN, a], [MAN, {**a, "기호": "P_b"}], TOT, TOT) # 같은 조합 = 같음
assert res["판정"] == dm.SURE and not res["참고"]
def test_quantity_kinds_and_unit_conversion() -> None:
tail = dm.verdict([_line("크레인", "노임", "0.2959", "9", code="LB1")],
[_line("크레인", "노임", "0.2963", "9", code="LB1")], TOT, TOT) # fmt: skip
assert tail["판정"] == dm.SURE and [x["항"] for x in tail["참고"]] == ["수량 끝자리"]
poom = dm.verdict([_line("특별인부", "노임", "0.46", "9", code="LB3")],
[_line("특별인부", "노임", "0.48", "9", code="LB3", text="Q")], TOT, TOT) # fmt: skip
assert [x["항"] for x in poom["참고"]] == ["품 숫자"] and "0.46 ↔ 0.48" in poom["참고"][0]["글"]
assert "바꾸면 우리 값" in poom["참고"][0]["글"]
two = [
_line("배관공", "노임", "0.04", "9", code="LB1"),
_line("인부", "노임", "0.02", "9", code="LB2"),
]
six = [
_line("배관공", "노임", "0.24", "9", code="LB1"),
_line("인부", "노임", "0.12", "9", code="LB2"),
]
res = dm.verdict(two, six, TOT, TOT) # 같은 배(× 6)가 두 줄 = 결과 단위 기준 다름 = 로직
assert [x["항"] for x in res["다른"]] == ["단위 환산", "단위 환산"] and res["판정"] == dm.NEAR
one = dm.verdict(two[:1] + six[1:], six, TOT, TOT) # 한 줄만 배 = 수량 값
assert one["판정"] == dm.SURE and [x["항"] for x in one["참고"]] == ["품 숫자"]
kind = dm.verdict([MAN], [_line("특별인부", "노임", "0.02", "100", code="LB000003")], TOT, TOT)
assert kind["판정"] != dm.SURE and kind["다른"] # 직종 다름 = 로직
more = dm.verdict([MAN], [MAN, _line("모래", "재료", "1", "9", "재료비")], TOT, TOT)
assert more["판정"] != dm.SURE and more["다른"][0]["항"] == "줄 없음" # 줄 구성(㉡) = 로직
# ── (b) 견본 ─────────────────────────────────────────────────────────────
def _judge(tag: str, no: int) -> dict:
whole, _, index, links = t4.ctx()
try:
doc = t4.dim.derive(t2.sk_of(t2.fn.emit(t2.native("EST", tag), no)), None, t2.whole())
except FileNotFoundError:
pytest.skip("원파일 없음")
return dm.match(doc, t4.dim.Links(whole, "우리"), index, links)
def _kinds(got: dict) -> list[str]:
return [x["항"] for x in got.get("참고") or []]
def test_backfill_spec_and_quantity() -> None:
got = _judge("main", 230)
assert got["판정"] == dm.SURE and got["키"] == "GF000113" and not got["다른"]
assert {"규격", "수량"} <= set(_kinds(got))
assert got["바꿈"] == [
{"기호": "P_굴착기무한궤", "기계": "0201-0020", "이름": "굴착기(무한궤도)", "규격": "0.2㎥"}
]
def test_pc_slab_tail_and_manhole_poom() -> None:
slab = _judge("main", 850)
assert slab["판정"] == dm.SURE and slab["키"] == "GC000257" and _kinds(slab) == ["수량 끝자리"]
hole = _judge("main", 828)
assert hole["판정"] == dm.SURE and hole["키"] == "GC000252" and "품 숫자" in _kinds(hole)
assert hole["옵션"] == {"규격": "D1,800"} # ㉠ 이름 글 「D1800」 = 선택 「D1,800」
def test_cut_soil_is_machine_work_and_pipe_stays_near() -> None:
work = _judge("거창", 46)
assert work["판정"] == val.WORK and not work.get("키")
assert _kinds(work) == ["작업량 틀"] and "GC001147" in work["참고"][0]["글"]
pipe = _judge("거창", 19)
kinds = [d["항"] for d in pipe["다른"]]
assert pipe["판정"] == dm.NEAR and pipe["키"] == "GF000179"
assert kinds.count("단위 환산") == 2 and "율 줄" in kinds
def test_machine_work_needs_one_machine_group() -> None:
names = dm.note._names
head = {"가져온": {"기계": [{"기호": "P_a"}, {"기호": "P_b"}]}, "식": []}
one = [_line("굴착기", "기계", "0.03", "5", "경비", "0201-0070", "P_a", "1 ÷ Q")]
assert val.work(head, one, names)["판정"] == val.WORK
assert val.work(head, [{**one[0], "수량글": "0.03"}], names) is None # 수량 = 수뿐
assert val.work(head, [*one, {**MAN, "기호": "w"}], names) is None # 인력 섞임 = 보통 길
both = [*one, {**one[0], "기호": "P_b", "코드": "0240-0007"}]
assert val.work(head, both, names) is None # 기계 둘(조합 아님)
combo = {"가져온": {"기계": [{"기호": "P_a", "조합첫": True}, {"기호": "P_b", "조합": True}]}}
assert val.work(combo, both, names)["판정"] == val.WORK # 조합 기계 = 한 묶음
# ── (c) 옵션 확인 ────────────────────────────────────────────────────────
def test_option_check_when_values_tie() -> None:
doc = {"변수": [{"기호": "S", "아래": "규격", "이름": "규격", "열쇠": "규격",
"연결": {"갈래": "선택", "목록": ["A", "B"], "값": "A"}}]} # fmt: skip
vid = dm.md.var_id(doc["변수"][0])
got = val.option_check(doc, {vid: "A"}, [{vid: "A"}, {vid: "B"}], {vid})
assert len(got) == 1 and "규격 = A(기본값" in got[0]["글"]
assert not val.option_check(doc, {vid: "A"}, [{vid: "A"}], {vid}) # 하나로 갈림 = 없음
assert not val.option_check(doc, {vid: "A"}, [{vid: "A"}, {vid: "B"}], set()) # 수량 무관
# ── (d) 바꾸기 · 되돌리기 ────────────────────────────────────────────────
def test_swap_puts_imported_spec_machine_and_back(world) -> None:
folder = world["personal"]
try:
est = t2._file(t2.EST["main"])
except FileNotFoundError:
pytest.skip("원파일 없음")
before = {p.name: p.read_bytes() for p in dm.ml.FOLDER.glob("수식_*.json")}
with store.at(folder):
hp = fh.upload_native(folder, est.name, est.read_bytes(), t2.WHO)["키"]
name = fh.make(folder, hp, 230)["이름"]
money0 = t4._money(name)
found = msk.find(name, store.read("sangeun", name)["판"])
meta = found[dm.KEY]
assert meta["판정"] == dm.SURE and meta["바꿈"][0]["기계"] == "0201-0020"
out = msk.swap(name, found["판"])
assert [x["기계"] for x in out["바꾼기계"]] == ["0201-0020"]
from M02_MasterTemplete import M02_Doc_Sk_Path as path
doc = path.read(name)["문서"]
var = next(v for v in doc["변수"] if dm.md.var_id(v) == "P_굴착기무한궤")
assert var["연결"]["입력"]["기계"] == "'0201-0020'"
line = next(x for x in doc["금액"] if x["단가"] == "P_굴착기무한궤")
assert (line["이름"], line["규격"]) == ("굴착기(무한궤도)", "0.2㎥")
assert line["묶음"] == "굴착기(무한궤도) 0.2㎥" # 묶음 제목도 가져온 규격
base = copy.deepcopy(dm.ml.load_all()["GF000113"]) # M01 값 그대로 · 기계만 가져온 규격
for v in base["변수"]:
if dm.md.var_id(v) == "P_굴착기무한궤":
v["연결"]["입력"]["기계"] = "'0201-0020'"
want = dm._calc(base, dm.ml.Links(t2.whole()), dict(meta["옵션"]))
assert t4._money(name) == [want[k] for k in t4.COSTS] and t4._money(name) != money0
msk.back(name, store.read("sangeun", name)["판"])
again = store.read("sangeun", name)["문서"]
assert again["가져온"]["로직"] and not again.get("바탕")
assert t4._money(name) == money0 # 되돌리기 = 원래 가져온 식 금액
with store.at(folder):
gc = t2._file(t2.EST["거창"])
hp2 = fh.upload_native(folder, gc.name, gc.read_bytes(), t2.WHO)["키"]
cut = fh.make(folder, hp2, 46)["이름"]
found = msk.find(cut, store.read("sangeun", cut)["판"])
assert found[dm.KEY]["판정"] == val.WORK
with pytest.raises(store.StoreError) as e:
msk.swap(cut, found["판"])
assert e.value.status == 409
assert before == {p.name: p.read_bytes() for p in dm.ml.FOLDER.glob("수식_*.json")}
# ── (e) 전수 ─────────────────────────────────────────────────────────────
@pytest.mark.skipif(os.environ.get("S81211_ALL") != "1", reason="전수 = S81211_ALL=1")
def test_every_block_values() -> None:
out = subprocess.run(
[sys.executable, str(HERE / "helper_810_4_sweep.py")], capture_output=True, text=True
)
assert out.returncode == 0, out.stderr[-2000:]
got = json.loads(out.stdout.strip().splitlines()[-1])
assert got["블록"] == 2220 and got["오류"] == 0 and got["확정다시셈다름"] == 0
assert got["판정"].get(val.WORK, 0) >= 1 and got["판정"][dm.SURE] >= 184
assert not set(val.VALUE) & set(got["근접항"]) # 값 차이 = 참고