Files
Aislo/resources/tester/test_810_4_match.py
T
eomsangdonandClaude Opus 5.5 1e4ce036c6 feat(812-2): 연결 찾기 확정 = 로직 — 단가 · 끝수 = 참고(확정을 안 막음) · 율 값 = 비율 · 노임 · 자재 종류 · 산출 단가 = 다른 항
- M02_Doc_Match_Note(새) — 율 = 율 줄 셈값 ÷ 대상 줄 금액 합(대상 비목) · 짝 줄 다른 항 대 참고 가르기
- M02_Doc_Match — 같은 줄 = 로직 같은 줄(코드 다름은 짝 대조로) · 연결후보 `참고` 칸 · 참고 적은 조합 · 후보 먼저
- 띠 글 「확정 — 로직 같음 · 참고: 단가 다름 n 줄 · 우리 단가로 셈 ○○ 원」
- test_810_4 기대 고침(단가 차이 · 끝수 = 참고로 옮김) · 전수 도우미 확정 다시 셈 = 우리 금액 · 확정 참고 셈

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

222 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.
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""810-4 지킴 — 가져온 식 ↔ 우리 M01 기초 연결 찾기(`M02_Doc_Match`) · 확정일 때만 바꾸기(`M02_Doc_MatchSk`).
(a) 판정 — 거창 원파일 블록: 확정(콘크리트 표층 인력포설 = GC000297 형식 B-Type · 금액 원 단위 같음) ·
근접(합판거푸집 4회 = GC000211 보통 · 4회 — 단가 차이 · 비목 · 율) · 요구 704 파형강관 매설 Φ800 =
GF000179 관경 800 근접 · 다른 항 = 단위 환산(× 6) · 기계 규격 · 율 줄 · 없음(후보 0)
(b) 옵션 풀이 — 줄 수량이 의존하는 변수만 돌려 좁힘 · 글 근거 값과 다르면 「옵션 글」 다른 항(확정 아님)
(c) 줄 대조 — 같음 · 단위 환산 · 율 줄 = 다른 항 · 단가 차이 · 끝수 = 참고(812-2 · 확정을 안 막음)
(d) 바꾸기 — 임시 층 개인: 확정 SK = 같은 키에 M01 사본(바탕 · 입력값) · 금액 = M01 문서 셈 · 원래 몸 보관 →
되돌리기 = 원래 금액 · 근접 SK 바꾸기 = 409 · M01 문서 파일 그대로
(e) 전수(1,518 문서 × 원파일 산근 2,220 블록 · 파일마다 프로세스) — 확정은 모두 다시 셈해도 금액 같음 ·
판정 셋 · 근접 다른 항 갈래 표(S8104_ALL=1 일 때만 · 수십 분 · S8104_ROWS=<pkl> = 줄 목록 저장)
원파일 없으면 그 시험만 skip(다른 PC).
"""
from __future__ import annotations
import os
import sys
from decimal import Decimal as D
from functools import lru_cache
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
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
from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402
from M02_MasterTemplete import M02_Doc_Match as dm # 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 — 같은 임시 층
@lru_cache(maxsize=1)
def ctx():
whole = t2.whole()
docs = dm.ml.load_all()
return whole, docs, dm.Index(docs), dm.Memo(dm.ml.Links(whole, docs))
def block(no: int, tag: str = "거창") -> dict:
"""원파일 블록 → 가져온 식 문서(형제 산근 없는 블록만 씀 — 형제는 저장 층에서 읽음)."""
out = t2.fn.emit(t2.native("EST", tag), no)
return dim.derive(t2.sk_of(out), None, t2.whole())
def judge(no: int) -> dict:
whole, _, index, links = ctx()
return dm.match(block(no), dim.Links(whole, "우리"), index, links)
# ── (a) 판정 ─────────────────────────────────────────────────────────────
def test_sure_one_block() -> None:
got = judge(33) # 콘크리트 포장포설 t=20cm이하
assert got["판정"] == dm.SURE and got["키"] == "GC000297"
assert got["옵션"] == {"형식": "B-Type"} and not got["다른"]
assert got["금액"]["가져온"] == got["금액"]["우리"]
def test_near_formwork_lists_differences() -> None:
got = judge(41) # 합판거푸집 4회
assert got["판정"] == dm.NEAR and got["키"] == "GC000211"
assert got["옵션"]["사용횟수"] == "4회" and got["옵션"]["유형"] == "보통"
kinds = {d["항"] for d in got["다른"]}
assert kinds == {"비목"} # 공구손료 1 % 재료비 ↔ 경비 = 로직(812-2)
assert [d["항"] for d in got["참고"]] == ["단가 차이"] # 대전 합판 단가 = 값(참고)
def test_req704_pipe_is_near_with_unit_rate_machine() -> None:
got = judge(19) # 파형강관 매설 Φ800 ↔ GF000179(요구 704)
assert got["판정"] == dm.NEAR and got["키"] == "GF000179" and got["옵션"] == {"관경": 800}
kinds = [d["항"] for d in got["다른"]]
assert kinds.count("단위 환산") == 2 and "기계 규격" in kinds and "율 줄" in kinds
assert any("× 6" in d["글"] for d in got["다른"])
def test_none_without_candidates() -> None:
got = judge(7) # 지장목 제거 — 같은 단위 · 자원 묶음 기초 없음
assert got["판정"] == dm.NONE and not got.get("키")
# ── (b) 옵션 풀이 ────────────────────────────────────────────────────────
def test_option_narrowing_and_text_evidence() -> None:
whole, docs, index, links = ctx()
imported = block(41)
base = docs["GC000211"]
dom, ev = dm.domains(base, imported, index.regions)
assert dom["n_사용"][0] == "4회" and "4회" in ev["n_사용"] # 규격 글 「4회」 = 근거 먼저
got = dm._calc(imported, dim.Links(whole, "우리"), {})
mine = dm.facts(imported, got)
want = {"수량": {x["수량"] for x in mine if x["수량"] is not None}, "단가": set()}
small = dm.narrow(base, links, {k: list(v) for k, v in dom.items()}, want, ev)
assert (
small["S_유형"] == ["보통"] and D(0) in small["r_높이"]
) # 5 ÷ 45 · 2 ÷ 45 = 보통 · 감률 0
res = dm.try_doc(base, links, imported, got, mine, index.regions)
assert res["옵션"]["n_사용"] == "4회"
off = dm.verdict(mine, mine, got, got, [dm._off(base, "n_사용", "1회", {"4회"})])
assert off["판정"] != dm.SURE and off["다른"][0]["항"] == "옵션 글"
# ── (c) 줄 대조 ──────────────────────────────────────────────────────────
def _line(name, kind, qty, price, cost="노무비", code=""):
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} # fmt: skip
def test_compare_kinds_and_no_tolerance() -> None:
a = [_line("인부", "노임", "0.04", "100"), _line("공", "노임", "0.5", "10")]
tot = {"노무비": 9, "재료비": 0, "경비": 0, "계": 9}
assert dm.verdict(a, list(a), tot, tot)["판정"] == dm.SURE
off = dm.verdict(a, list(a), tot, {**tot, "노무비": 10, "계": 10})
assert off["판정"] == dm.SURE and off["참고"][0]["항"] == "끝수" # 끝수 = 참고(812-2)
b = [_line("인부", "노임", "0.24", "100"), _line("공", "노임", "0.5", "11")]
res = dm.verdict(a, b, tot, tot)
assert [d["항"] for d in res["다른"]] == ["단위 환산"] and res["판정"] == dm.NEAR
assert [d["항"] for d in res["참고"]] == ["단가 차이"]
rate = {**_line("율", "율", "1", "3", "경비"), "수량": None, "단가": None}
res = dm.verdict([*a, rate], a, tot, tot)
assert res["다른"] == [{"항": "율 줄", "글": "가져온 쪽만 — 율"}] and res["판정"] == dm.NEAR
named = dm.verdict(
a, list(a), tot, tot, [dm._unnamed({"이름": "구조물되메우기"}, {"이름": "숲가꾸기"})]
)
assert named["판정"] == dm.NEAR # 값만 같은 남의 기초 = 확정 아님
assert not dm.same_name({"이름": "모르타르"}, {"이름": "기초다짐 및 뒷채움(인력)"})
assert dm.same_name({"이름": "유로폼 설치및해체"}, {"이름": "유로폼 설치 및 해체"})
c = [_line("딴", "노임", "3", "7", code="LB9")]
assert dm.verdict(a, c, tot, tot)["판정"] == dm.NONE
# ── (d) 바꾸기 · 되돌리기 ────────────────────────────────────────────────
COSTS = ("노무비", "재료비", "경비")
def _money(key: str) -> list:
from M02_MasterTemplete import M02_Doc_Sk_Path as path
got = path.read(key)["셈"]
return [got[k] for k in COSTS]
def test_swap_only_when_sure_and_back(world) -> None:
folder = world["personal"]
est = t2._file(t2.EST["거창"])
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)["키"]
sure = fh.make(folder, hp, 33)["이름"]
near = fh.make(folder, hp, 41)["이름"]
old = store.read("sangeun", sure)
money0 = _money(sure)
found = msk.find(sure, old["판"])
assert found[dm.KEY]["판정"] == dm.SURE
meta = store.read("sangeun", sure)["문서"]["가져온"]["원본"][dm.KEY]
assert meta["키"] == "GC000297" and meta["옵션"] == {"형식": "B-Type"}
assert _money(sure) == money0 # 찾기 = 기록만 · 금액 그대로
msk.swap(sure, found["판"])
now = store.read("sangeun", sure)
doc = now["문서"]
assert doc["바탕"] == {"로직": "GC000297"} and doc["입력값"] == {"형식": "B-Type"}
assert not doc.get("가져온") and doc[msk.KEEP]["가져온"]["로직"]
assert isinstance((doc.get("수식") or {}).get("문서"), dict) # 수식 사본(802-4a) 찍힘
want = dm._calc(dm.ml.load_all()["GC000297"], dm.ml.Links(t2.whole()), {"형식": "B-Type"})
assert _money(sure) == [want[k] for k in COSTS] # 바꾼 뒤 = M01 사본 셈
msk.back(sure, now["판"])
again = store.read("sangeun", sure)["문서"]
assert again["가져온"]["로직"] and not again.get("바탕")
assert _money(sure) == money0 # 되돌리기 = 원래 가져온 식 금액
with pytest.raises(store.StoreError) as e:
msk.swap(near, "")
assert e.value.status == 409
assert store.read("sangeun", near)["문서"]["가져온"]["로직"] # 근접 = 그대로
assert before == {p.name: p.read_bytes() for p in dm.ml.FOLDER.glob("수식_*.json")}
# ── (e) 전수 ─────────────────────────────────────────────────────────────
@pytest.mark.skipif(os.environ.get("S8104_ALL") != "1", reason="전수 = S8104_ALL=1")
def test_every_block_table() -> None:
import collections
import subprocess
rows = ["--rows", os.environ["S8104_ROWS"]] if os.environ.get("S8104_ROWS") else []
out = subprocess.run(
[sys.executable, str(HERE / "helper_810_4_sweep.py"), *rows], capture_output=True, text=True
)
assert out.returncode == 0, out.stderr[-2000:]
import json
got = json.loads(out.stdout.strip().splitlines()[-1])
assert got["블록"] == 2220 and got["오류"] == 0 and got["확정다시셈다름"] == 0
assert set(got["판정"]) <= {dm.SURE, dm.NEAR, dm.NONE}
assert sum(got["판정"].values()) == 2220 and got["판정"].get(dm.SURE, 0) >= 1
assert not {"단가 차이", "끝수"} & set(got["근접항"]) # 값 차이 = 참고(812-2)
assert collections.Counter(got["확정참고"]).get("단가 차이", 0) >= 1
def test_parent_reads_swapped_sibling(world) -> None:
"""형제 SK 를 우리 기초 사본으로 바꿔도 부르는 가져온 식은 그 사본 문서로 셈(연결 안 끊김)."""
folder = world["personal"]
est = t2._file(t2.EST["거창"])
with store.at(folder):
hp = fh.upload_native(folder, est.name, est.read_bytes(), t2.WHO)["키"]
parent = fh.make(folder, hp, 15)["이름"] # 전석 메쌓기 — 형제 산근 둘
sk = store.read("sangeun", parent)["문서"]
sib = next(x["참조"]["키"] for x in sk["가져온"]["로직"]["호표"] if x.get("참조"))
got = store.read("sangeun", sib)
body = msk.copy_body(got["문서"], {"키": "GC000211", "옵션": {}}) # 같은 ㎡
store.write("sangeun", sib, got["판"], body)
mid = _money(parent)
assert None not in mid and sum(mid) != 94215 # 사본 문서로 셈(끊기면 None)
msk.back(sib, store.read("sangeun", sib)["판"])
assert sum(_money(parent)) == 94215 # 되돌리면 원파일 금액