Merge remote-tracking branch 'origin/sub_laptop_6' into sub_laptop_8
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
"""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
|
||||
@@ -59,7 +59,12 @@ def test_확인한_줄만_별칭_수량_빠진_줄_장부(client):
|
||||
["personal", "7/42"],
|
||||
)
|
||||
assert origin["확인"]["누가"] == {"id": 42, "이름": "갑"} and origin["확인"]["때"]
|
||||
assert doc["담은로직"] == [
|
||||
stone, *made, soil = doc["담은로직"] # 705-3 — 확인 안 한 줄 · 「우리에 없음」 = 고정 줄
|
||||
assert stone["고정"]["단가"] == {"노무비": 0, "재료비": 1400000, "경비": 0}
|
||||
assert stone["고정"]["출처"]["줄"] == 0 and stone["수량"] == {"손": 1}
|
||||
assert soil["고정"]["이름"] == "잔토처리(인력100%)" and soil["수량"] == {"손": 0.09}
|
||||
assert origin["품셈"] == [1, 2] and got["줄수"]["고정"] == 2 and got["빠진줄"] == []
|
||||
assert made == [
|
||||
{
|
||||
"로직": "GF000101",
|
||||
"입력값": {"지역": "서울"},
|
||||
@@ -75,11 +80,7 @@ def test_확인한_줄만_별칭_수량_빠진_줄_장부(client):
|
||||
"메모": "실무 0.05㎥ × 2",
|
||||
},
|
||||
]
|
||||
assert doc["자재"] == [] # 표지석 = 몸에 없음 → 안 들어감
|
||||
assert [(x["이름"], x["금액"], x["까닭"]) for x in got["빠진줄"]] == [
|
||||
("임도표지석(받침,글새김포함)", 1400000, "확인 안 함"),
|
||||
("잔토처리(인력100%)", 3097.1, "뺌 — 우리에 없음"),
|
||||
]
|
||||
assert doc["자재"] == [] # 표지석 = 몸에 없음 → 고정 줄
|
||||
assert got["비교"]["엑셀"] == 1415169 and got["비교"]["우리"] > 0
|
||||
assert got["다시씀"] is False and got["장부"] == {"씀": 3}
|
||||
book = _book(mine)
|
||||
@@ -104,7 +105,8 @@ def test_로직_여럿_같은_별칭_감쌈_자재_몫_덮음(client):
|
||||
got = _build(client, 3, lines)
|
||||
mine = layers.personal_dir(7, 42)
|
||||
doc = _doc(mine, got["참조"]["키"])
|
||||
first, second, wrapped = doc["담은로직"]
|
||||
first, second, fx, wrapped = doc["담은로직"]
|
||||
assert fx["고정"]["출처"]["줄"] == 2 # 확인 안 한 줄 = 고정(705-3)
|
||||
assert first["별칭"] == second["별칭"] == "구조물터파기(인력100%)" # 한 줄 로직 둘 = 같은 별칭
|
||||
assert second["수량"] == {"손": 0.014} and second["메모"] == "실무 0.14㎥ × 0.1(몫 0.1) · 토사"
|
||||
assert wrapped["별칭"] == "잔토처리(인력100%)" and wrapped["수량"] == {"손": 0.09}
|
||||
@@ -119,7 +121,7 @@ def test_로직_여럿_같은_별칭_감쌈_자재_몫_덮음(client):
|
||||
files = sorted(p.name for p in (mine / "precombo").glob("*.json"))
|
||||
again = _build(client, 3, lines[:1]) # 다시 확인 = 같은 본뜬 덮음
|
||||
assert again["다시씀"] is True and again["참조"] == got["참조"]
|
||||
assert _doc(mine, got["참조"]["키"])["담은로직"] == []
|
||||
assert [x["고정"]["출처"]["줄"] for x in _doc(mine, got["참조"]["키"])["담은로직"]] == [1, 2, 3]
|
||||
assert sorted(p.name for p in (mine / "precombo").glob("*.json")) == files
|
||||
|
||||
|
||||
@@ -177,8 +179,7 @@ def test_전국_자재_키_없이_본뜬에(client):
|
||||
def test_몸_없음_재검사_400(client):
|
||||
assert _post(client).status_code == 200
|
||||
one = {"로직": "GF000101", "배": 1}
|
||||
_build(client, 3, want=400) # 옛 몸(줄 없음) = 확인한 줄이 없음
|
||||
_build(client, 3, [], want=400)
|
||||
_build(client, 3, 통째={"대상": []}, want=400) # 빈 몸 = 모두 고정(705-3 시험) · 빈 통째 = 400
|
||||
bad = [
|
||||
[{"차례": 9, "대상": [one]}], # 호표 밖 줄
|
||||
[{"차례": 1, "대상": [one]}, {"차례": 1, "대상": [one]}], # 겹친 줄
|
||||
@@ -226,7 +227,9 @@ def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client):
|
||||
ref = [{"차례": 0, "대상": [{"호표": 46, "배": 1}]}]
|
||||
assert "제46호표 먼저 확인" in _build(client, 45, ref, want=409)["detail"]
|
||||
child = _build(client, 46, [{"차례": 0, "뺌": "안 씀"}])
|
||||
assert any(x["까닭"].startswith("% 줄") for x in child["빠진줄"])
|
||||
assert [x["까닭"] for x in child["빠진줄"]] == ["뺌 — 안 씀"] # % 줄 = 비율 고정 줄(705-3)
|
||||
rate = _doc(layers.personal_dir(7, 42), child["참조"]["키"])["담은로직"][-1]["고정"]
|
||||
assert rate["비율"] == {"기준": "재료비", "비목": "재료비"}
|
||||
got = _build(client, 45, ref)
|
||||
mine = layers.personal_dir(7, 42)
|
||||
line = _doc(mine, got["참조"]["키"])["담은로직"][0]
|
||||
|
||||
@@ -113,7 +113,8 @@ def test_끝까지_확인한_줄만_별칭(client):
|
||||
k: "별칭" for k in again["철근가공조립(현장)"]["대상"][0]["입력값"]
|
||||
}
|
||||
first = _one(client, "HP-000001", no)
|
||||
assert first["만든"] == {"키": made["참조"]["키"], "확인": True}
|
||||
assert {k: first["만든"][k] for k in ("키", "확인")} == {"키": made["참조"]["키"], "확인": True}
|
||||
assert first["만든"]["품셈"] == doc["본뜬"]["품셈"] # 705-3 — 품셈 줄 차례(첫 체크)
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
@@ -137,7 +138,7 @@ def test_막는_길(client):
|
||||
"/api/m02/layers/personal/hopyo-build",
|
||||
json={"층": "personal", "키": "HP-000001", "번": 1, "줄": []},
|
||||
)
|
||||
assert empty.status_code == 400 and "확인한 줄이 없음" in empty.text
|
||||
assert empty.status_code == 200 and empty.json()["줄수"]["품셈"] == 0 # 빈 몸 = 모두 고정(705-3)
|
||||
money = client.post(
|
||||
"/api/m02/layers/personal/hopyo-build",
|
||||
json={"층": "personal", "키": "HP-000001", "번": 1, "줄": [{"차례": 1, "금액": 1}]},
|
||||
|
||||
Reference in New Issue
Block a user