Files
Aislo/resources/tester/test_642_3_row.py
T
eomsangdonandClaude Opus 5.5 553c9169cd feat(785-3): 1차 줄 판정 · 엑셀 사전 · 후보 · 수량 환산 + 유가전력 MO 이음
- 새 M02_Hopyo_Row — 단가표 시트(자재단가 · 노임단가 · 기계경비총괄표 · 기계경비적용기준 · 단가산출 · 일위대가 총괄표 · STmate 목록표 코드) → 엑셀 사전(갈래 + M01 키 후보 한 번) · 세부행 4 단계(비고 번호 → 별도계상 → 단위 → 사전) · 후보 차례 장부 → M01 표마다(LB · MT · MO · EQ · FX) → M02 층 SK · SC · 판정 넷(제안 있음 · 2차 필요 · 후보 여럿 · 후보 없음) · 고른 것 꼴 · 수량 = 엑셀 × 몫 × 배
- factor 를 Row 로 옮김(Match.factor 는 부름만)
- Elem 유가 후보(oil_rows · oils · lost 유가) · Ledger 짝 고른 것 꼴 넓힘(갈래 · 키 머리 · M01 있음 · 율 비목 · 기준 재검사 · 옛 꼴 그대로)
- 고정 줄 이음 유가 MO(LINKS · GROUPS 재료 · 재료비 = MO 값) · 일위대가 자재 목록 MO 허용(Labor_Doc 두 줄 · 줄 수 그대로)
- 화면 찾아 넣기 「M01 전체」 탭 유가 줄(oil) · 일위대가 = 고정 줄 이음 유가 · 단가산출 = 더한줄 재료 요소 MO · TS GROUPS 짝
- 시험 새 test_642_3_row(북부청 415 줄 = 단산 268 · 일위 1 · 별도계상 64 · 재료 78 · 기계 3 · 율 1 · 사방 201 · 거창 68 모두 갈래 하나 · 사전 · 장부 짝 · 환산 · MO 고정 셈 · 율 TS 거울)
- 낡은 시험 고침: test_721 의 FindKind 글 대조에 oil 더함(종류 하나 늘어난 까닭)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:27:48 +09:00

269 lines
11 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.
"""642-3 1차 줄 판정 · 엑셀 사전 · 유가전력 MO 지킴 시험(PLAN 785-3 · 설계 642-A 5-3 셋째 줄).
- 북부청 임도 415 줄 1차 갈래 수 = 단산 268 · 일위 1 · 별도계상 64 · 재료(사전) 78 · 기계 3 · 율 1
(분석 642-C 3-1) · 판정 2차 필요 268 · 같은 EST 꼴 사방 201 · 거창 68 = 모든 줄 갈래 하나(100 %)
- 엑셀 사전 = 단가표 시트 갈래 수 · 노임 19 M01 LB 하나씩 · 경유 → MO
- 장부 짝 고른 것 꼴(새) · 옛 꼴 그대로 · 틀린 키 400 · 환산 옮김(Match.factor = Row.factor)
- 유가전력 MO — 고정 줄 이음 셈(재료비 = MO 값 · 묶음 재료 → 율 밑) · 자재 목록 MO · TS 짝 거울
엑셀은 읽기만 · 층 · storage 안 씀.
"""
import collections
import json
import os
import subprocess
from pathlib import Path
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Hopyo_Elem as elem
from M02_MasterTemplete import M02_Hopyo_Excel as ex
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_Match as hm
from M02_MasterTemplete import M02_Hopyo_Row as row
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Fixed as fixed
HERE = Path(__file__).resolve().parent
DOCS = HERE.parent / "knowledge" / "original" / "실무문서"
BUKBU = DOCS / "2026_실무일위대가 모음" / "2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls"
SABANG = DOCS / "2026_실무일위대가 모음" / "2026년_북부지방산림청_사방사업(계류+댐)_설계내역서.xls"
GEOCHANG = (
DOCS
/ "01.산불거창(거창북상병곡)"
/ "01.내역서"
/ "00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx"
)
OIL = "MO000002" # 자동차용경유 전국평균
def _read(path: Path, keys: bool) -> dict:
data = path.read_bytes()
book = ex.read_book(path.name, data)
sheets, _ = ex._open(path.name, data, row.SHEETS)
return {**book, "사전": row.lexicon(sheets, keys)}
def _kinds(book: dict) -> collections.Counter:
"""줄마다 (갈래, 근거 첫 낱말) — 1차 ①~③ · ④ 사전."""
index = row._index(book["사전"])
out = collections.Counter()
for h in book["호표"]:
for line in h["줄"]:
kind, why, _ = row.first(line)
if kind is None:
hits = row.second(line, index)
kinds = sorted({x["갈래"] for x in hits}) or ["없음"]
kind, why = "/".join(kinds), "사전"
out[(kind, why.split(" ")[0])] += 1
return out
@pytest.fixture(scope="module")
def bukbu() -> dict:
return _read(BUKBU, True)
def test_북부청_415_줄_1차_갈래(bukbu):
got = _kinds(bukbu)
assert sum(got.values()) == 415
assert got == {
("단가산출", "비고"): 268,
("일위대가", "비고"): 1,
("재료", "별도계상"): 64,
("재료", "사전"): 78,
("기계", "단위"): 3,
("율", "단위"): 1,
}
def test_북부청_줄_판_판정과_후보(bukbu):
out = row.rows(bukbu)
lines = [x for v in out.values() for x in v]
assert len(out) == 68 and len(lines) == 415
verdict = collections.Counter(x["판정"] for x in lines)
assert verdict["2차 필요"] == 268 and set(verdict) <= set(row.VERDICTS)
assert all(set(x["갈래"]) <= set(row.KINDS) and x["후보"] is not None for x in lines)
first = {x["행"]: x for x in out[1]} # 제1호표(설계 2-10 기대표)
assert first[6]["갈래"] == ["재료"] and first[6]["근거"] == "별도계상"
assert first[6]["판정"] == "제안 있음" and all(p["별도계상"] for p in first[6]["후보"])
assert first[7]["번"] == 46 and first[7]["판정"] == "2차 필요"
assert first[10]["갈래"] == ["재료"] and first[10]["근거"].startswith("사전 자재단가")
rate = next(x for x in lines if x["갈래"] == ["율"])
assert rate["판정"] == "제안 있음"
assert (
rate["후보"][0]["비목"] == "재료비" and rate["후보"][0]["기준"] == "재료비"
) # 재료비의 3 %
sub = next(x for x in lines if x["갈래"] == ["일위대가"])
assert sub["번"] == 46 and sub["후보"][0]["키"] == "제46호표" and sub["판정"] == "제안 있음"
for x in lines: # 고른 것 꼴(설계 2-1)
for p in x["후보"]:
assert {"갈래", "키", "이름", "단위", "입력값", "출처", "몫"} <= set(p)
def test_엑셀_사전_단가표_시트(bukbu):
lex = bukbu["사전"]
assert lex["꼴"] == "EST"
count = collections.Counter(x["갈래"] for x in lex["줄"])
assert count == {
"단가산출": 95,
"재료": 91,
"일위대가": 68,
"기계": 32,
"인력": 19,
"유가": 4, # 자재단가 경유 · 휘발유 + 기본재료비 둘
"환율": 3,
}
people = [x for x in lex["줄"] if x["갈래"] == "인력"]
assert all(len(x["후보"]) == 1 and x["후보"][0]["키"].startswith("LB") for x in people)
oil = next(x for x in lex["줄"] if x["갈래"] == "유가" and x["시트"] == "자재단가")
assert oil["후보"][0]["키"] == OIL
json.dumps(lex, ensure_ascii=False) # HP 에 둘 수 있는 꼴
@pytest.mark.parametrize("path, total", [(SABANG, 201), (GEOCHANG, 68)])
def test_같은_EST_꼴_모든_줄_갈래_하나(path, total):
got = _kinds(_read(path, False))
assert sum(got.values()) == total
assert not [k for k in got if "/" in k[0] or k[0] == "없음"]
# ── 장부 짝 고른 것 꼴 ─────────────────────────────────────────────────────
def test_장부_짝_고른_것_꼴과_옛_꼴():
known = ledger._known()
mat = sorted(known[1])[0]
new = ledger.check_pair(
{"갈래": "유가", "키": OIL, "배": 1, "이름": "경유", "단위": "L"}, "시험", known
)
assert new == {"갈래": "유가", "키": OIL, "입력값": {}, "배": 1, "몫": None, "이름": "경유",
"단위": "L"} # fmt: skip
rate = {"갈래": "율", "비목": "재료비", "기준": "재료", "배": 1}
assert ledger.check_pair(rate, "시험", known)["기준"] == "재료"
assert ledger.check_pair({"자재": mat, "배": 2}, "시험", known) == {
"자재": mat, "입력값": {}, "배": 2, "몫": None} # fmt: skip
for bad in (
{"갈래": "유가", "키": "MT000008"},
{"갈래": "고정", "키": "MO000002"},
{"갈래": "인력", "키": "LB999999"},
{"갈래": "재료", "키": mat, "입력값": {"x": 1}},
{"갈래": "율", "비목": "재료비"},
{"갈래": "없는갈래"},
):
with pytest.raises(ledger.store.StoreError) as got:
ledger.check_pair(bad, "시험", known)
assert got.value.status == 400
line = {"이름": "경유", "규격": "", "단위": "ℓ", "수량": 3, "단가": {"재료비": 1376}}
book = [("personal", "1/2", {ledger.key(line): {"갈래": "짝", "짝": [new]}})]
out = row.judge(line, {}, {}, book, folder="x")
assert out["판정"] == "제안 있음" and out["후보"][0]["키"] == OIL
assert out["후보"][0]["출처"] == {"장부": "personal"}
def test_환산_옮김():
for have, want, spec in (
("㎡", "㎡", ""),
("cm", "m", ""),
("㎡", "㎥", "T=20cm"),
("개", "㎥", ""),
):
assert hm.factor(have, want, spec) == row.factor(have, want, spec)
assert row.factor("cm", "m")["배"] == 0.01
q = row.quantity({"수량": 2.5, "단위": "㎡", "규격": "T=20cm"}, "㎥", 0.5)
assert q["배"] == 0.2 and q["우리"] == 0.25
# ── 유가전력 MO ─────────────────────────────────────────────────────────
def _oil_price() -> float:
return float(m01.loaded()[1].get(OIL)["값"])
def test_유가_후보():
keys = [x["키"] for x in elem.oils("경유")]
assert keys[0] == OIL and all(k.startswith("MO") for k in keys)
assert elem.lost({"갈래": "유가", "이름": "경유"})[0]["키"] == OIL
OIL_DOC = {
"열": "rv",
"담은로직": [
{"로직": "GC000421", "수량": {"손": 2}},
{
"로직": "",
"고정": {
"이름": "경유",
"단위": "L",
"단가": {"재료비": 1376},
"이음": {"유가": OIL},
}, # fmt: skip
"수량": {"손": 3},
},
{
"로직": "",
"고정": {
"이름": "잡재료",
"단위": "%",
"단가": {},
"비율": {"기준": "재료", "비목": "재료비"},
}, # fmt: skip
"수량": {"손": 10},
},
],
"자재": [{"코드": OIL, "수량": {"손": 2}, "별도계상": "직접계산"}], # 기본 = 별도계상(합 밖)
}
def test_고정_줄_유가_이음_셈():
doc = labor.clean(OIL_DOC)
fx = doc["담은로직"][1]["고정"]
assert fx["이음"] == {"노임": None, "자재": None, "기계": None, "유가": OIL}
assert fixed.group_of(fx) == "재료"
assert doc["자재"][0]["코드"] == OIL # 자재 목록 MO
out = labor.calc(doc, {})
line = out["줄"][1]
assert line["기준"]["재료비"] == pytest.approx(_oil_price()) and line["이은"] == ["재료비"]
assert out["자재"][0]["기준단가"] == pytest.approx(_oil_price())
with pytest.raises(labor.store.StoreError): # 머리 틀림 = 400
labor.clean({**OIL_DOC, "담은로직": [{**OIL_DOC["담은로직"][1], "고정": {
**OIL_DOC["담은로직"][1]["고정"], "이음": {"유가": "MT000008"}}}]}) # fmt: skip
old = labor.clean({"담은로직": [{"로직": "", "고정": {"이름": "a", "단가": {}}}]})
assert old["담은로직"][0]["고정"]["이음"] == {"노임": None, "자재": None, "기계": None}
def _ts(answer: dict, doc: dict, values: dict) -> dict:
proc = subprocess.run(
["node", os.path.join(HERE, "helper_m02_labor_calc.cjs")],
input=json.dumps({"답": answer, "문서": doc, "값": values}, ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
check=True,
)
return json.loads(proc.stdout)
def _same(a, b) -> None:
if isinstance(a, dict):
assert set(a) == set(b)
for k in a:
_same(a[k], b[k])
elif isinstance(a, list):
assert len(a) == len(b)
for x, y in zip(a, b):
_same(x, y)
elif isinstance(a, (int, float)) and not isinstance(a, bool):
assert b == pytest.approx(a, rel=1e-9)
else:
assert a == b
def test_유가_율_고정_줄_TS_거울():
doc = labor.clean(OIL_DOC)
answer = labor.calc(doc, {})
py = labor.settle(answer, doc, {})
_same(py, _ts(answer, doc, {}))
rate = py["줄"][2]
oil = py["줄"][1]["반영"]["재료비"]
mats = py["자재"][0]["반영"] or 0
assert rate["기준"]["재료비"] == pytest.approx(oil + mats) # 묶음 재료 = 유가 줄 + 자재