Files
Aislo/resources/tester/test_705_3_hopyo_fixed.py
T
eomsangdonandClaude Opus 5.5 2789afb102 feat(M02 실무자료 빠짐 0): 확인 안 한 줄 · 노임 · % = 고정 줄 · [모두 고정으로 들여오기] · [품셈으로 바꾸기] (PLAN 705-3)
- 만들기 — 체크 안 한 줄 · 노임 · 「우리에 없음」 = 고정 줄(엑셀 숫자 그대로 · 서버가 파일에서) · % 줄 = 비율 고정 줄(밑수 = 다른 줄 합일 때 · 아니면 금액 그대로) · 합계만 = 재료비 · 같은 이름 = 차례 가름 · 하위 호표 = 확인된 사전조합 참조
- 이음 = 이름 · 규격 · 단위 · 단가 다 같은 노임 · 자재만 저절로 · 다른 단가 = 이음 후보 수
- 시트 통째 — /hopyo-build-all(하위 먼저 · 확인된 호표 그대로) · 알림 「호표 · 줄 · 품셈 줄 · 고정 줄 · 빠짐 · 엑셀 합계 = 우리 합계」 · 비교 = 엑셀 끝수 규칙(EST 내림 · STmate 반올림)
- 비율 고정 줄 셈 — M02 settle 짝(파이썬 · TS ratioLines) · 사전조합 셈 · B09 pct 줄
- 확인 창 — 만든 사전조합 품셈 줄 = 첫 체크 · [품셈으로 바꾸기] · [모두 고정으로] · 생성 물음에 전후 금액 · 품 다른 줄 수
- 낡은 시험 고침 — 빈 몸 400 · 빠진 줄(확인 안 함 · %) 단정 = 고정 줄로 바뀐 규칙

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:10:16 +09:00

227 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.
"""705-3 실무자료 불러오기 빠짐 0 · 시트 통째 한 번 · [품셈으로 바꾸기](요구서 1장 · 4장 2 · 3 · 7).
- 확인 안 한 줄 · 노임 · % · 「우리에 없음」 = 고정 줄(엑셀 숫자 그대로) · 체크한 줄만 품셈 · 하위 호표 = 참조
- % 줄 = 비율 고정 줄 — M02 settle 거울(파이썬 ↔ TS) · 사전조합 셈 · B09 `pct` 줄
- [모두 고정으로 들여오기] — 북부청 · 실무 xlsx 넷: 빠짐 0 · 엑셀 합계 = 우리 합계(호표마다) · 줄 꼬리표 수 ·
확인된 호표 그대로 · 이음 = 단가까지 같은 줄만 · 같은 이름 줄 차례 가름 · 몸에 숫자 칸 없음(422)
- [품셈으로 바꾸기] 전후 — 체크 한 줄 = 품셈 · 나머지 고정 · 되돌리기(빈 몸) = 모두 고정
층은 임시 폴더(공용 storage 안 씀).
"""
import importlib.util
import json
from pathlib import Path
import pytest
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor
from B09_Estimation.B09_Estimation_Labor_Source import fixed_line
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Template_Layers as layers
# 올리기 길 시험의 `client` 마당을 같이 씀
# ruff: noqa: F811
from resources.tester.test_m02_hopyo_excel import NORTH, _post, client, master_hash # noqa: F401
def _load(name):
spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(f"{name}.py"))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
m02 = _load("test_m02_labor_calc_mirror")
ALL = "/api/m02/layers/{}/hopyo-build-all"
BUILD = "/api/m02/layers/{}/hopyo-build"
DOCS = Path(__file__).resolve().parents[2] / "resources" / "knowledge" / "original" / "실무문서"
FILES = { # 실무 xlsx 넷 — EST 꼴 거창 · STmate 꼴 영월 · 울진 대흥 · 울진 소광
"거창": "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx",
"영월": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx",
"대흥": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/"
"2024년 간선임도사업(기번3).xlsx",
"소광": "25년 산불진화임도(기번8)(울진소광)/"
"2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx",
}
# (호표, 줄, 고정, 하위 참조, 이음, 엑셀 = 우리) — 영월 제22호표 = STmate 하위 호표 비목 반올림 1 원 끝수
EXPECT = {
"거창": (14, 68, 56, 12, 11, 14),
"영월": (23, 138, 135, 3, 0, 22),
"대흥": (34, 171, 143, 28, 0, 34),
"소광": (25, 117, 98, 19, 0, 25),
}
def _fx(name, price, rate=None, unit="식"):
out = {"이름": name, "규격": "", "단위": unit, "단가": price, "이음": {}}
return {**out, "비율": rate} if rate else out
def _all(client, key="HP-000001"):
res = client.post(ALL.format("personal"), json={"층": "personal", "키": key})
assert res.status_code == 200, res.text
return res.json()
def _docs():
folder = layers.personal_dir(7, 42) / "precombo"
return [json.loads(p.read_bytes()) for p in sorted(folder.glob("SC-*.json"))]
# ── 비율 고정 줄 셈 ──────────────────────────────────────────────────────────
def _ratio_doc():
return labor.clean(
{
"열": "rv",
"담은로직": [
{"고정": _fx("고화매트", {"재료비": 37000}, unit="㎡"), "수량": {"손": 1}},
{"고정": _fx("고정핀", {"재료비": 200}, unit="EA"), "수량": {"손": 4.95}},
{"고정": _fx("인부", {"노무비": 1000}, unit="인"), "수량": None},
{"로직": m02.PLAIN, "수량": None},
{
"고정": _fx("잡재료비", {}, {"기준": "재료비", "비목": "재료비"}),
"수량": {"손": 3},
},
{
"고정": _fx("공구손료", {}, {"기준": "노무비", "비목": "경비"}),
"수량": {"손": 2},
},
],
"자재": [{"코드": m02.MAT, "수량": None}],
}
)
@pytest.mark.parametrize("values", [{}, {f"자재|{m02.MAT}": "3"}, {"사석 수상고르기|㎡": 2}])
def test_비율_고정_줄_파이썬과_TS_가_같은_답(values):
doc = _ratio_doc()
answer = labor.calc(doc, {})
py = labor.settle(answer, doc, values)
m02._same(py, m02._ts(answer, doc, values))
assert py == labor.calc(doc, values)
misc, tool = [ln for ln in py["줄"] if (ln.get("고정") or {}).get("비율")]
others = [ln for ln in py["줄"] if ln not in (misc, tool)]
under = sum((ln["반영"] or {}).get("재료비", 0) for ln in others) # 자재 = 별도계상(합 밖)
assert misc["반영"]["재료비"] == pytest.approx(under * 3 / 100) and misc["배"] == 100
assert misc["반영"]["노무비"] == misc["반영"]["경비"] == 0
assert tool["반영"]["경비"] == 0 # 인부 수량 없음 = 노무비 밑 0
assert py["반영합"]["계"] == pytest.approx(
sum((ln["반영"] or {}).get("계", 0) for ln in py["줄"])
)
def test_비율_정리_막힘():
bad = _fx("잡", {}, {"기준": "합계", "비목": "재료비"})
with pytest.raises(Exception, match="비율"):
labor.clean({"담은로직": [{"고정": bad, "수량": {"손": 1}}]})
def test_B09_비율_고정_줄_pct():
fx = _fx("잡재료비", {}, {"기준": "재료비", "비목": "재료비"})
one = fixed_line(fx, 3)
assert {k: one[k] for k in ("kind", "part", "base", "qty", "unit")} == {
"kind": "pct", "part": "material", "base": "part:material", "qty": 3.0, "unit": "%"
} # fmt: skip
lines = [
{"kind": "fixed", "name": "고화매트", "qty": 1, "own": {"material": 37000.0},
"link": {"labor": None, "material": None, "machine": None}},
{"kind": "fixed", "name": "고정핀", "qty": 4.95, "own": {"material": 200.0},
"link": {"labor": None, "material": None, "machine": None}},
one,
] # fmt: skip
for n, ln in enumerate(lines):
ln["cid"], ln["n"] = f"{ln['kind']}:{ln['name']}#0", n
block = {"id": "DGF", "cover": "S:f", "kind": "조합", "name": "고화매트재료비", "spec": "",
"unit": "㎡", "cite": "", "origin": {}, "lines": lines} # fmt: skip
out = compute_labor({"blocks": [block], "owners": {}}, {}, {})
got = out["daega_blocks"][0]
pct = next(ln for ln in got["lines"] if ln["kind"] == "pct")
assert pct["amounts"]["material"] == 1139.7 # (37000 + 990) × 3 %
assert got["blank"] == 0 and out["gaps"] == []
# ── 줄 하나 → 고정 줄 ────────────────────────────────────────────────────────
def test_같은_이름_차례_가름_합계만_재료비_금액만():
linker, taken = hfix.Linker(), set()
line = {"차례": 0, "이름": "잡석", "규격": "", "단위": "㎥", "수량": 2, "갈래": "자재",
"단가": {"계": 5000, "노무비": None, "재료비": None, "경비": None}} # fmt: skip
a = hfix.line_of(line, [], {"파일": "x", "호표": 1}, taken, linker)
b = hfix.line_of({**line, "차례": 1}, [], {"파일": "x", "호표": 1}, taken, linker)
assert (a["고정"]["이름"], b["고정"]["이름"]) == ("잡석", "잡석 (2)")
assert a["고정"]["단가"] == {"노무비": 0, "재료비": 5000, "경비": 0} # 합계만 = 재료비
assert b["고정"]["출처"] == {"파일": "x", "호표": 1, "줄": 1} and a["수량"] == {"손": 2}
doc = labor.clean({"담은로직": [a, b]}) # 같은 줄 가름 400 안 남
assert len(doc["담은로직"]) == 2
only = {**line, "차례": 2, "이름": "운반", "수량": None,
"금액": {"계": 700, "노무비": 700, "재료비": None, "경비": None}} # fmt: skip
c = hfix.line_of(only, [], {}, taken, linker)
assert c["수량"] == {"손": 1} and c["고정"]["단가"]["노무비"] == 700 # 금액만
def test_퍼센트_밑수_다르면_금액_그대로():
pct = {"차례": 2, "이름": "소모자재", "규격": "주재료비의", "단위": "%", "수량": 11, "갈래": "%",
"단가": {"계": 7937.2}, "금액": {"계": 873, "노무비": 0, "재료비": 873, "경비": 0}} # fmt: skip
got = hfix.line_of(pct, [], {}, set(), hfix.Linker())
assert "비율" not in got["고정"] and got["고정"]["단위"] == "식"
assert got["수량"] == {"손": 1} and got["고정"]["단가"]["재료비"] == 873
# ── 시트 통째 ────────────────────────────────────────────────────────────────
def test_북부청_모두_고정_빠짐_0_합계_같음_그대로(client):
before = master_hash()
assert _post(client).status_code == 200
got = _all(client)
assert {k: got[k] for k in ("호표", "줄", "품셈", "고정", "호표참조", "빠짐", "같음")} == {
"호표": 68, "줄": 415, "품셈": 0, "고정": 414, "호표참조": 1, "빠짐": 0, "같음": 68
} # fmt: skip
assert got["실패"] == [] and got["다름"] == [] and got["그대로"] == 0
assert got["이음"] == 13 and got["이음후보"] == 31
docs = [d for d in _docs() if d["본뜬"]["키"].count(" ") == 1] # 호표 문서(감싼 줄 빼고)
lines = [x for d in docs for x in d["담은로직"]]
assert len(docs) == 68 and all(d["본뜬"]["품셈"] == [] for d in docs)
assert sum("고정" in x for x in lines) == 414 and sum("참조" in x for x in lines) == 1
assert sum(bool(x.get("고정", {}).get("비율")) for x in lines) == 1 # 잡재료비 3 %
again = _all(client) # 다시 = 확인된 호표 덮지 않음
assert again["그대로"] == 68 and again["고정"] == 0 and again["같음"] == 68
assert master_hash() == before
@pytest.mark.parametrize("tag", list(FILES))
def test_실무_xlsx_넷_모두_고정(client, tag):
path = DOCS / FILES[tag]
res = _post(client, data=path.read_bytes(), name=path.name)
assert res.status_code == 200, res.text
got = _all(client)
no, rows, fixed_n, kids, links, same = EXPECT[tag]
assert (got["호표"], got["줄"], got["고정"], got["호표참조"]) == (no, rows, fixed_n, kids)
assert got["빠짐"] == 0 and got["품셈"] == 0 and got["실패"] == []
assert got["고정"] + got["호표참조"] == got["줄"] # 줄 하나도 안 빠짐
assert got["이음"] == links and got["같음"] == same
assert all(abs(x["차이"]) <= 1 for x in got["다름"])
def test_몸에_숫자_칸_없음(client):
assert _post(client).status_code == 200
bad = {"층": "personal", "키": "HP-000001", "금액": 1}
assert client.post(ALL.format("personal"), json=bad).status_code == 422
# ── [품셈으로 바꾸기] 전후 ────────────────────────────────────────────────────
def test_품셈으로_바꾸기_되돌리기(client):
assert _post(client).status_code == 200
_all(client)
body = {"층": "personal", "키": "HP-000001", "번": 3}
one = {"차례": 1, "대상": [{"로직": "GF000101", "입력값": {"지역": "서울"}, "배": 1}]}
res = client.post(BUILD.format("personal"), json={**body, "줄": [one]})
assert res.status_code == 200, res.text
after = res.json()
assert after["다시씀"] is True and after["줄수"]["품셈"] == 1 and after["줄수"]["고정"] == 3
doc = next(d for d in _docs() if d["본뜬"]["키"] == "HP-000001 제3호표")
assert doc["본뜬"]["품셈"] == [1] and doc["담은로직"][1]["로직"] == "GF000101"
assert after["비교"]["같음"] is False # 품셈 줄 = 우리 값(엑셀과 다름)
view = client.get("/api/m02/layers/personal/hopyo/HP-000001/3").json()
assert view["호표"]["만든"]["품셈"] == [1] and view["호표"]["만든"]["고정"] == 3
back = client.post(BUILD.format("personal"), json={**body, "줄": []}).json() # 되돌리기
assert back["줄수"]["품셈"] == 0 and back["비교"]["같음"] is True