- 만들기 — 체크 안 한 줄 · 노임 · 「우리에 없음」 = 고정 줄(엑셀 숫자 그대로 · 서버가 파일에서) · % 줄 = 비율 고정 줄(밑수 = 다른 줄 합일 때 · 아니면 금액 그대로) · 합계만 = 재료비 · 같은 이름 = 차례 가름 · 하위 호표 = 확인된 사전조합 참조 - 이음 = 이름 · 규격 · 단위 · 단가 다 같은 노임 · 자재만 저절로 · 다른 단가 = 이음 후보 수 - 시트 통째 — /hopyo-build-all(하위 먼저 · 확인된 호표 그대로) · 알림 「호표 · 줄 · 품셈 줄 · 고정 줄 · 빠짐 · 엑셀 합계 = 우리 합계」 · 비교 = 엑셀 끝수 규칙(EST 내림 · STmate 반올림) - 비율 고정 줄 셈 — M02 settle 짝(파이썬 · TS ratioLines) · 사전조합 셈 · B09 pct 줄 - 확인 창 — 만든 사전조합 품셈 줄 = 첫 체크 · [품셈으로 바꾸기] · [모두 고정으로] · 생성 물음에 전후 금액 · 품 다른 줄 수 - 낡은 시험 고침 — 빈 몸 400 · 빠진 줄(확인 안 함 · %) 단정 = 고정 줄로 바뀐 규칙 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
227 lines
12 KiB
Python
227 lines
12 KiB
Python
"""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
|