- M02_Doc_Match_Note(새) — 율 = 율 줄 셈값 ÷ 대상 줄 금액 합(대상 비목) · 짝 줄 다른 항 대 참고 가르기 - M02_Doc_Match — 같은 줄 = 로직 같은 줄(코드 다름은 짝 대조로) · 연결후보 `참고` 칸 · 참고 적은 조합 · 후보 먼저 - 띠 글 「확정 — 로직 같음 · 참고: 단가 다름 n 줄 · 우리 단가로 셈 ○○ 원」 - test_810_4 기대 고침(단가 차이 · 끝수 = 참고로 옮김) · 전수 도우미 확정 다시 셈 = 우리 금액 · 확정 참고 셈 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
222 lines
12 KiB
Python
222 lines
12 KiB
Python
# 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 # 되돌리면 원파일 금액
|