- 새 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>
240 lines
14 KiB
Python
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["근접항"]) # 값 차이 = 참고
|