Files
Aislo/resources/tester/test_778_5_hopyo_units.py
T
eomsangdonandClaude Opus 5.5 4cdb41f80b feat(778-5): 블록 = 줄 판정 모음 · 확인한 블록만 단위 SK(바탕 없음 · 근거 꼬리표) · 확인 목록 받는 길(D-3 · 요구 623 · 624)
- 새 M02_Hopyo_SkUnit: 줄 판정(Units 잇기 + 매개값 · 사용자 고름 {차례: 노임|기계|자재|기준|뺌} · 꼬리표 「내가 고름」) · 단가 = M01 지금 값으로 다시 셈(우리금액) · 엑셀 금액 차 = 참고 · 블록 판정 「옮길 수 있음」 | 「고를 줄 n」 · 단위 SK 더한줄 = 인력 · 기계경비 로직 줄 · 재료(손료율 든 수량 · 별도계상) · 율 줄(778-4 꼴 · 줄 목록 = 그 행 add:#k) · 하위 참조 · 값 고정 줄 · 저장 꼴로 다듬어 같은 내용 SK 지문 같음
- Sangeun: 기초 · SK · 고름 대기 갈림(SkPatch.plan) 걷고 단위 SK 하나 · 확인 [{번, 고름?}] 에 든 블록만 만듦 · 하위 단산 먼저 · 고를 줄 남으면 못만듦 · 일위대가 만들기가 부르는 Maker.plan(번) = 「확인 전」 고름 대기(자동 없음) · 별칭 제안 걷음(바탕 기초 없음)
- Router: sangeun 몸 바탕 → 확인(줄 수 그대로)
- 시험 새 뜻: test_729(기초 · SK · 고름 대기 · 계수 · 못옮김 잇기 · 729-5 차이 SK 바탕 길 = 바탕 없는 단위 SK 로 사라짐 → 확인 블록 단위 SK · 줄 고름 · 뺌으로 갈음) · test_718_1 · test_572 · test_779_2 · test_m02_hopyo_flow(확인 목록을 보내야 SK 생김 · 같은 파일 안 같은 내용 단위 SK 없을 수 있음 · 바탕 없는 SK = SC 에 참조 그대로)
- 새 test_778_5: 작업반장 = 건설업 · 규격 차례 무관 · 글자 규격 = 후보 · 손료율 · 설치비 · 제경비 율 줄 · 여덟 파일 줄 연결 표 · 확인 안 한 블록 안 만듦 · 고름 · 400 · 404 · 다시 올림 같은 SK 재사용 · 일위대가 확인 전 못 이음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 14:16:12 +09:00

173 lines
9.7 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.
"""778-5 줄 잇기 한 벌 · 블록 = 줄 판정 모음 · 단위 SK(D-2 · D-3 · 요구 622 · 623 · 624).
- 줄 잇기(`M02_Hopyo_Units`) — 작업반장 = 건설업 · 숫자 규격 치수 차례 · 구분자 무관 · 글자 규격 = 고를 것 ·
손료율 = 자재 줄 방법 · 우리에 없는 율 항목(설치비 · 제경비) = 율 줄 · 여덟 파일 묶음별 수 굳힘
- 단위 SK(`M02_Hopyo_SkUnit` · `M02_Hopyo_Sangeun`) — 확인 안 한 블록 안 만듦 · 확인 블록 = 바탕 없는 SK +
근거 꼬리표 · 고를 줄 남으면 못만듦 · 고르면 만듦 · 다시 올려도 같은 SK 재사용 · 일위대가 = 확인 전 못 이음
층 = 임시 폴더 · M01 · 마스터 읽기만.
"""
from __future__ import annotations
import json
import pytest
from M02_MasterTemplete import M02_Hopyo_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Material as hmat
from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun
from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit
from M02_MasterTemplete import M02_Hopyo_Store as hs
from M02_MasterTemplete import M02_Hopyo_Units as units
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete import M02_Template_Layers as layers
from resources.tester import helper_m02_hopyo_golden as golden
from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401
BUILD = "/api/m02/layers/personal/hopyo-build"
# 여덟 파일 불리는 블록 줄 연결 표(778-5 D-2 굳힘 · 바뀌면 까닭 커밋 글)
TABLE = {
"L": {"이어짐": 366, "고를 것": 0, "못 이음": 0},
"X": {"이어짐": 180, "고를 것": 10, "못 이음": 0},
"M": {"이어짐": 71, "고를 것": 55, "못 이음": 4},
"R": {"이어짐": 150, "고를 것": 2, "못 이음": 0},
"S": {"이어짐": 3, "고를 것": 0, "못 이음": 0},
"E": {"이어짐": 1, "고를 것": 0, "못 이음": 0},
}
ALL_LINKED = 156
@pytest.fixture(scope="module")
def us():
return units.Units()
def test_작업반장_건설업_기본(us):
got = us.line({"묶음": "L", "이름": "작업반장", "직종": "작업반장", "품": 0.1})
assert got["상태"] == units.LINKED and got["단위"] == {"노임": "LB000001"} # 건설업
assert got["꼬리표"] == ["기본 — 건설업 작업반장"] and got["후보"][0]["키"] == "LB000261"
one = us.line({"묶음": "L", "이름": "보통인부", "직종": "보통인부", "품": 0.5})
assert one["단위"] == {"노임": "LB000002"} and not one["꼬리표"]
def test_숫자_규격_차례_구분자_무관_글자_규격_후보(us):
assert hmat.dims("600×1200") == hmat.dims("1200x600 mm") == hmat.dims("1200*600㎜")
assert hmat.dims("1종2류(철재)") is None and hmat.lettered("1종2류(철재)")
assert not hmat.lettered("600*1200mm") and not hmat.lettered("#20, 0.9mm")
for spec in ("600×1200", "1200x600", "1200 X 600mm"):
got = us.line({"묶음": "M", "이름": "유로폼패널", "규격": spec, "단위": "매", "품": 1})
assert got["상태"] == units.LINKED and got["단위"] == {"자재": "MT000467"}, spec
named = us.line({"묶음": "M", "이름": "유로폼(패널)(600×1200)", "단위": None, "품": 1})
assert named["단위"] == {"자재": "MT000467"} # 「유로폼(패널)」 = 유로폼패널 · 단위 없는 EST 줄
paint = {"묶음": "M", "이름": "광명단조합페인트", "규격": "1종2류(철재)", "단위": "L", "품": 1}
got = us.line(paint)
assert got["상태"] == units.PICK and got["글"] == units.LETTERS and got["후보"]
def test_손료율_자재_줄_율_항목_율_줄(us):
wear = us.line({"묶음": "M", "이름": "유로폼패널", "규격": "600×1200", "품": 0.267,
"손료율": 0.267}) # fmt: skip
assert wear["방법"] == units.WEAR and wear["상태"] == units.LINKED
setup = {"묶음": "R", "이름": "설치비(재료비의 5%)", "품": 0.05, "기준꼴": "자재"}
assert us.line(setup)["단위"] == {"기준": "재료", "비목": "재료비"} # 우리에 없는 율 항목
rest = {"묶음": "R", "이름": "제경비(노무비의 60%)", "품": 0.6, "기준꼴": "노무비"}
assert us.line(rest)["단위"] == {"기준": "노무비", "비목": "경비"}
lost = us.line({"묶음": "R", "이름": "기구손료(노무비의 9%)", "기준": "노무비", "품": 0.09})
assert lost["상태"] == units.PICK and lost["후보"] == [{"기준": "노무비", "비목": "경비"}]
# 단위 SK 셈 — 설치비 = 재료 줄 × 5 % · 제경비 = 노무비 × 60 %
block = {"항목": [
{"묶음": "L", "이름": "보통인부", "직종": "보통인부", "품": 0.5, "행": 1},
{"묶음": "M", "이름": "유로폼패널", "규격": "600×1200", "단위": "매", "품": 2, "행": 2},
{**setup, "행": 3}, {**rest, "행": 4},
]} # fmt: skip
got = skunit.judge(block, us, lambda sub: None, None, "㎡")
assert got["판정"] == skunit.GOOD and [r["상태"] for r in got["줄"]] == [units.LINKED] * 4
labor_, mat = (got["줄"][0]["우리금액"], got["줄"][1]["우리금액"])
assert got["줄"][2]["우리금액"] == pytest.approx(mat * 0.05, abs=0.1)
assert got["줄"][3]["우리금액"] == pytest.approx(labor_ * 0.6, abs=0.1)
doc = {"키": "SK-000001", "이름": "t", "단위": "㎡", "바탕": None, "더한줄": got["더한줄"]}
whole = us.whole
ran = labor._json(calc.run(sd.clean(doc, None, whole), whole))
assert ran["1단위"]["계"] == pytest.approx(got["우리금액"])
def test_여덟_파일_줄_연결_표(us):
rows, full, blocks = [], 0, 0
for tag in golden.FILES:
hp = golden.read(tag)
_, first = sangeun.calls(hp)
for b in hp["단산"]:
if int(b["번"]) not in first:
continue
got = [(o["묶음"], us.line(o)["상태"]) for o in b.get("항목") or [] if not o.get("밖")]
rows += got
blocks += 1
full += all(s == units.LINKED for _, s in got)
assert units.summary(rows) == TABLE and (full, blocks) == (ALL_LINKED, 206)
def _sks(folder) -> dict[str, dict]:
return {p.stem: json.loads(p.read_bytes()) for p in (folder / "sangeun").glob("SK-*.json")}
def test_확인한_블록만_단위_SK_고름_재사용(client):
before = master_hash()
folder = layers.personal_dir(7, 42)
key = _post(client).json()["키"]
url = f"{URL.format('personal')}/{key}/sangeun"
fit.run_job(folder, key)
got = client.post(url, json={}).json()
assert got["만듦"] == [] and not _sks(folder) # 확인 안 한 블록 = 안 만듦
assert len(got["판정"]) == got["블록"] == 49 # 불리는 블록
rows = {r["번"]: r for r in got["판정"]}
good = [n for n, r in rows.items() if r["판정"] == skunit.GOOD and r["근거"]]
pick = next(n for n, r in rows.items() if r["고를줄"])
assert rows[pick]["판정"] == f"고를 줄 {rows[pick]['고를줄']}"
one = rows[good[0]]
assert all("우리금액" in x and "차" in x for x in one["줄"] if x["상태"] == units.LINKED)
# 확인 = 그 블록만 · 고를 줄 남은 블록 = 못만듦
made = client.post(url, json={"확인": [{"번": good[0]}, {"번": pick}]}).json()
assert [x["번"] for x in made["만듦"]] == [good[0]] and made["못만듦"][0]["번"] == pick
docs = _sks(folder)
doc = docs[made["만듦"][0]["키"]]
assert doc["바탕"] is None and doc["본뜬"]["갈래"] == skunit.UNIT and doc["더한줄"]
assert doc["본뜬"]["근거"]["로직"] == one["근거"]["로직"] and doc["본뜬"]["못옮김"] == []
# 고르면 만듦(못 이은 줄마다 첫 후보 · 후보 없으면 뺌)
picks = {}
for x in rows[pick]["줄"]:
if x["상태"] in (units.PICK, units.NONE):
c = (x["후보"] or [{}])[0]
k = c.get("키") or c.get("자재")
field = {"L": "노임", "X": "기계", "M": "자재"}.get(x["묶음"])
picks[str(x["차례"])] = {field: k} if k and field else c or {"뺌": "현장"}
again = client.post(url, json={"확인": [{"번": pick, "고름": picks}]}).json()
assert [x["번"] for x in again["만듦"]] == [pick] and again["그대로"] == 1
mine = next(r for r in again["판정"] if r["번"] == pick)
assert all("내가 고름" in x["꼬리표"] for x in mine["줄"] if str(x["차례"]) in picks)
bad = {"확인": [{"번": pick, "고름": {"0": {"노임": "XX"}}}]}
assert client.post(url, json=bad).status_code == 400
assert client.post(url, json={"확인": [{"번": 9999}]}).status_code == 404
# 일위대가 만들기 — 확인 안 한 단가산출 줄 = 못 이음(확인 전)
calls = {}
for h in hs.read(folder, key)["호표"]:
for x in h.get("줄") or []:
ref = x.get("참조") or {}
if ref.get("종류") == "단산":
calls.setdefault(int(ref["번"]), h["번"])
wait = next(n for n in sorted(calls) if n not in (good[0], pick))
res = client.post(BUILD, json={"층": "personal", "키": key, "번": calls[wait], "줄": []})
left = [x for x in res.json()["detail"]["남은줄"] if x.get("까닭") == sangeun.WAIT_WHY]
assert res.status_code == 409 and left
# 다시 올림(끝내기 뒤) = 같은 내용 SK 재사용
n = len(_sks(folder))
client.delete(f"{URL.format('personal')}/{key}")
key2 = _post(client).json()["키"]
fit.run_job(folder, key2)
url2 = f"{URL.format('personal')}/{key2}/sangeun"
re_ = client.post(url2, json={"확인": [{"번": good[0]}]}).json()
assert re_["같은것씀"] == [{"번": good[0], "키": made["만듦"][0]["키"]}]
assert len(_sks(folder)) == n
assert master_hash() == before