Files
Aislo/resources/tester/test_m02_hopyo_build.py
T
eomsangdonandClaude Opus 5.5 a2da6cb069 Merge remote-tracking branch 'origin/dev' into sub_laptop_8
- 충돌 시험 셋(717-4 · 719-2 · hopyo build) = 740-1 펼침 단언 + 742-1 저장 수량 단언 합침
- 740-2 일위대가표 풍선 줄의 body(742-1 이 걷은 휘발 셈 몸) → cur

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:47:51 +09:00

315 lines
17 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.
"""일위대가 실무자료 → 사전조합 만들기(PLAN 324-3 · 338-3 · `M02_Hopyo_Build`) — 확인한 줄만 ·
몸 없음 400 · 빠진 줄 까닭 · 로직 여럿 같은 별칭 · 같은 로직 감쌈 · 배 · 몫 · 자재 · 통째 ·
재검사 400(없는 로직 · 모르는 입력 · 배 ≤ 0 · 금액 칸) · 하위 호표 409 → 확인 뒤 참조 · 장부 씀 ·
`본뜬.확인` · 덮음 · 옛 즉시 확정 길 404 · 장부 길(보기 · 고치기 · 권한) · picks 확인 ·
master_data 원본 그대로. 층은 임시 폴더.
"""
import json
from M02_MasterTemplete import M02_Hopyo_Build as hb
from M02_MasterTemplete import M02_Hopyo_Store as hs
from M02_MasterTemplete import M02_Template_Layers as layers
# 올리기 길 시험의 `client` 마당을 같이 씀
# ruff: noqa: F811
from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401
DIG = "구조물터파기(인력100%)|토사|m3"
FILL = "구조물되메우기(인력100%)||m3"
SOIL = "잔토처리(인력100%)||m3"
BUILD = "/api/m02/layers/{}/hopyo-build"
LEFT = "/api/m02/layers/{}/hopyo-unlinked"
LEDGER = "/api/m02/layers/{}/hopyo-ledger"
def _build(client, no, lines=None, layer="personal", want=200, decide=True, **extra):
"""`decide` = 남은 줄(729-2 · 이음 없는 고정 줄)을 「우리에 없음」으로 고른 뒤 만듦(옛 고정 줄 꼴 그대로)."""
body = {"층": "personal", "키": "HP-000001", "번": no, **extra}
if lines is not None:
body["줄"] = lines
if decide and want == 200 and "통째" not in extra:
left = client.post(LEFT.format(layer), json=body).json().get("남은줄") or []
body["줄"] = [*(lines or []), *({"차례": x["차례"], "뺌": "우리에 없음"} for x in left)]
res = client.post(BUILD.format(layer), json=body)
assert res.status_code == want, res.text
return res.json()
def _doc(layer_dir, key):
return json.loads((layer_dir / "precombo" / f"{key}.json").read_bytes())
def _book(layer_dir):
return json.loads((layer_dir / hs.DIRNAME / f"{hs.MEMORY}.json").read_bytes())["줄"]
def test_확인한_줄만_별칭_수량_빠진_줄_장부(client):
before = master_hash()
assert _post(client).status_code == 200
lines = [
{"차례": 1, "대상": [{"로직": "GF000101", "입력값": {"지역": "서울"}, "배": 1}]},
{"차례": 2, "대상": [{"로직": "GF000113", "배": 2}]},
{"차례": 3, "뺌": "우리에 없음"},
]
got = _build(client, 3, lines)
mine = layers.personal_dir(7, 42)
doc = _doc(mine, got["참조"]["키"])
assert (doc["이름"], doc["단위"], doc["구분"]) == ("임도표지석 910×600×150", "개소", "실무자료")
assert doc["상세구분"].endswith("(공통단가).xls")
origin = doc["본뜬"]
assert (origin["층"], origin["키"], origin["원본"]) == (
"실무자료",
"HP-000001 제3호표",
["personal", "7/42"],
)
assert origin["확인"]["누가"] == {"id": 42, "이름": "갑"} and origin["확인"]["때"]
stone, *made, soil = doc["담은로직"] # 705-3 — 확인 안 한 줄 · 「우리에 없음」 = 고정 줄
assert stone["고정"]["단가"] == {"노무비": 0, "재료비": 1400000, "경비": 0}
hands = origin["실무수량"]["줄"] # 742-1 — 엑셀 수량 = 저장 수량 · 출처 기록도 같은 값
assert stone["고정"]["출처"]["줄"] == 0 and stone["수량"] == {"손": hands["0"]} == {"손": 1}
assert soil["고정"]["이름"] == "잔토처리(인력100%)"
assert soil["수량"] == {"손": hands[str(len(doc["담은로직"]) - 1)]}
assert hands[str(len(doc["담은로직"]) - 1)] == 0.09
assert origin["품셈"] == [1, 2] and got["줄수"]["고정"] == 2 and got["빠진줄"] == []
assert made == [
{
"로직": "GF000101",
"입력값": {"지역": "서울"},
"별칭": "구조물터파기(인력100%)",
"수량": {"손": 0.14}, # 742-1 — 엑셀 0.14 = 저장 수량
"메모": "실무 0.14㎥ × 1 · 토사",
},
{
"로직": "GF000113",
"입력값": {},
"별칭": "구조물되메우기(인력100%)",
"수량": {"손": 0.1}, # 엑셀 0.05 × 배 2 = 저장 수량(742-1)
"메모": "실무 0.05㎥ × 2",
},
]
assert doc["자재"] == [] # 표지석 = 몸에 없음 → 고정 줄
assert got["비교"]["엑셀"] == 1415169 and got["비교"]["우리"] > 0
assert got["다시씀"] is False and got["장부"] == {
"씀": 4
} # + 표지석 「우리에 없음」(729-2 고름)
book = _book(mine)
assert book[DIG]["갈래"] == "짝" and book[DIG]["짝"] == [
{"로직": "GF000101", "입력값": {"지역": "서울"}, "배": 1, "몫": None}
]
assert book[DIG]["처음파일"].endswith("(공통단가).xls")
assert book[DIG]["원래"] == {"품명": "구조물터파기(인력100%)", "규격": "토사", "단위": "㎥"}
assert book[SOIL]["갈래"] == "우리에 없음" and book[FILL]["짝"][0]["배"] == 2
assert hb.confirmed(mine) == {"HP-000001 제3호표": got["참조"]["키"]}
assert hb.confirmed(mine, ("system", None)) == {} # 다른 파일 층 = 따로
assert master_hash() == before
def test_로직_여럿_같은_별칭_감쌈_자재_몫_덮음(client):
assert _post(client).status_code == 200
lines = [
{"차례": 0, "대상": [{"자재": "MT004258", "배": 1}]},
{"차례": 1, "대상": [{"로직": "GF000101"}, {"로직": "GF000113", "배": 0.1, "몫": 0.1}]},
{"차례": 3, "대상": [{"로직": "GF000101"}]},
]
got = _build(client, 3, lines)
mine = layers.personal_dir(7, 42)
doc = _doc(mine, got["참조"]["키"])
first, second, fx, wrapped = doc["담은로직"]
assert fx["고정"]["출처"]["줄"] == 2 # 확인 안 한 줄 = 고정(705-3)
assert first["별칭"] == second["별칭"] == "구조물터파기(인력100%)" # 한 줄 로직 둘 = 같은 별칭
hands = doc["본뜬"]["실무수량"]["줄"] # 742-1 — 엑셀 수량 = 저장 수량 · 출처 기록도 같은 값
assert second["수량"] == {"손": 0.014} and hands["1"] == 0.014
assert second["메모"] == "실무 0.14㎥ × 0.1(몫 0.1) · 토사"
assert (
wrapped["별칭"] == "잔토처리(인력100%)"
and wrapped["수량"] == {"손": 0.09}
and hands["3"] == 0.09
)
inner = _doc(mine, wrapped["참조"]["키"])
assert inner["본뜬"]["키"] == "HP-000001 제3호표 4째 줄 GF000101"
assert inner["담은로직"][0]["로직"] == "GF000101" and inner["담은로직"][0]["수량"] == {"손": 1}
assert inner["본뜬"]["실무수량"]["줄"] == {"0": 1}
assert [(m["코드"], m["수량"], m["별도계상"]) for m in doc["자재"]] == [
("MT004258", {"손": 1}, "직접계산") # 742-1 저장 수량
]
assert doc["본뜬"]["실무수량"]["자재"] == {"MT004258": 1}
stone = next(v for k, v in _book(mine).items() if k.startswith("임도표지석(받침"))
assert stone["짝"][0]["자재"] == "MT004258"
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 [x["고정"]["출처"]["줄"] for x in _doc(mine, got["참조"]["키"])["담은로직"]] == [1, 2, 3]
assert sorted(p.name for p in (mine / "precombo").glob("*.json")) == files
def test_자재_세_칸_로직_자재_두_번_막음(client):
"""358-2 — 별도계상 줄 자재 = 세 칸(별도계상 · 관급) · 로직 자재와 같은 품목 = 400 아니고
사용자 자재에서 빼고 그 로직 `자재표시` 로 + 빠진 줄 까닭."""
assert _post(client).status_code == 200
apart = {"차례": 0, "대상": [{"자재": "MT004258", "배": 1}]} # 레미콘(25-21-120) 별도계상 줄
got = _build(client, 1, [apart])
mine = layers.personal_dir(7, 42)
mats = _doc(mine, got["참조"]["키"])["자재"]
assert [{k: m.get(k) for k in ("코드", "별도계상", "자재구분", "운반비")} for m in mats] == [
{"코드": "MT004258", "별도계상": "별도계상", "자재구분": "사급", "운반비": None}
] # 자재구분 = 고른 자재 이름(-사급) 먼저
twice = [
{"차례": 1, "대상": [{"로직": "GC000207", "배": 1}]}, # 철근 현장조립 = 로직 자재 결속선
{"차례": 0, "대상": [{"자재": "MT000209", "배": 1}]}, # 같은 결속선을 별도계상 줄에
]
got = _build(client, 1, twice)
doc = _doc(mine, got["참조"]["키"])
assert doc["자재"] == []
line = next(x for x in doc["담은로직"] if x.get("로직") == "GC000207")
assert {"별도계상": "별도계상", "자재구분": "관급"} in line["자재표시"].values()
assert any(m["까닭"] == hb.TWICE and m["차례"] == 0 for m in got["빠진줄"])
def test_전국_자재_키_없이_본뜬에(client):
"""361-1 — 「전국」 자재 = 자재 칸(M01 키만) 아니고 `본뜬.전국자재` · 이름 · 규격 · 단위 =
서버가 줄에서 다시 셈(몸은 이름 칸 못 받음) · 빠진 줄 까닭 · 장부엔 안 남김 · 자리표 없는 줄 = 400."""
assert _post(client).status_code == 200
line = {"차례": 0, "대상": [{"전국": True, "배": 1}]} # 레미콘 별도계상 줄
got = _build(client, 1, [line])
mine = layers.personal_dir(7, 42)
doc = _doc(mine, got["참조"]["키"])
assert doc["자재"] == [] and not any(m.get("코드") for m in doc["자재"])
qty = hs.read(mine, "HP-000001")["호표"][0]["줄"][0]["수량"]
assert doc["본뜬"]["전국자재"] == [
{
"이름": "레미콘(전국)-관급",
"규격": "25-21-120",
"단위": "㎥",
"수량": {"손": qty},
"별도계상": "별도계상",
"자재구분": "관급",
}
]
assert [m["까닭"] for m in got["빠진줄"] if m["차례"] == 0] == [hb.NATIONAL]
national = hs.read(mine, "HP-000001")["호표"][0]["줄"][0]
assert hb.ledger.key(national) not in _book(mine) # 전국 줄 = 장부 안 씀
_build(client, 1, [{"차례": 1, "대상": [{"전국": True, "배": 1}]}], want=400) # 로직 줄
both = [{"차례": 0, "대상": [{"전국": True, "자재": "MT004258", "배": 1}]}]
_build(client, 1, both, want=400)
_build(client, 1, [{"차례": 0, "대상": [{"전국": True, "입력값": {"x": 1}}]}], want=400)
def test_몸_없음_재검사_400(client):
assert _post(client).status_code == 200
one = {"로직": "GF000101", "배": 1}
_build(client, 3, 통째={"대상": []}, want=400) # 빈 몸 = 모두 고정(705-3 시험) · 빈 통째 = 400
bad = [
[{"차례": 9, "대상": [one]}], # 호표 밖 줄
[{"차례": 1, "대상": [one]}, {"차례": 1, "대상": [one]}], # 겹친 줄
[{"차례": 1, "대상": []}], # 대상 · 뺌 없음
[{"차례": 1, "대상": [one], "뺌": "안 씀"}],
[{"차례": 1, "대상": [{"로직": "XX"}]}],
[{"차례": 1, "대상": [{"로직": "GC000422"}]}], # 돈 아닌 로직
[{"차례": 1, "대상": [{**one, "입력값": {"없는칸": 1}}]}],
[{"차례": 1, "대상": [{**one, "배": 0}]}],
[{"차례": 1, "대상": [{**one, "배": -1}]}],
[{"차례": 1, "대상": [{**one, "몫": 1.5}]}],
[{"차례": 1, "대상": [{**one, "자재": "MT004258"}]}],
[{"차례": 1, "대상": [{"자재": "MT000000"}]}],
[{"차례": 1, "대상": [{"호표": 3}]}], # 자기를 담음
]
for lines in bad:
_build(client, 3, lines, want=400)
for extra in ({"금액": 100}, {"품": {"보통인부": 1}}): # 금액 · 품 칸 = 받지 않음
assert (
client.post(
BUILD.format("personal"),
json={
"층": "personal",
"키": "HP-000001",
"번": 3,
"줄": [{"차례": 1, "대상": [{**one, **extra}]}],
},
).status_code
== 422
)
assert client.post(BUILD.format("personal"), json={
"층": "personal", "키": "HP-000001", "번": 3, "금액": 1,
"줄": [{"차례": 1, "대상": [one]}]}).status_code == 422 # fmt: skip
assert not (layers.personal_dir(7, 42) / "precombo").exists() or not list(
(layers.personal_dir(7, 42) / "precombo").glob("SC-*.json")
) # 틀린 몸은 아무것도 안 씀
assert client.post("/api/m02/layers/personal/hopyo-memory", json={"줄": {}}).status_code in (
404,
405,
) # 옛 즉시 확정 길 없음
def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client):
assert _post(client).status_code == 200
ref = [{"차례": 0, "대상": [{"호표": 46, "배": 1}]}]
assert "제46호표 먼저 확인" in _build(client, 45, ref, want=409)["detail"]
child = _build(client, 46, [{"차례": 0, "뺌": "안 씀"}])
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)
made = _doc(mine, got["참조"]["키"])
line = made["담은로직"][0]
# 740-1 하위 호표 = 사본 블록(원본 연결 없음 · 키 = 출처)
assert line["참조"]["층"] == "사본" and line["참조"]["키"] == child["참조"]["키"]
assert line["본뜬"]["키"] == child["참조"]["키"] and line["별칭"] == "고화매트재료비"
assert line["수량"] == {"손": 2.3} and made["본뜬"]["실무수량"]["줄"]["0"] == 2.3 # 742-1
assert "고화매트재료비||m2" not in _book(mine) # 호표 참조 줄 = 장부 안 씀
whole = _build(client, 3, 통째={"대상": [{"로직": "GF000101", "배": 1}]})
doc = _doc(mine, whole["참조"]["키"])
assert [x["로직"] for x in doc["담은로직"]] == ["GF000101"]
assert doc["담은로직"][0]["수량"] == {"손": 1} and doc["본뜬"]["실무수량"]["줄"] == {"0": 1}
assert {x["까닭"] for x in whole["빠진줄"]} == {"호표 통째로 대신"}
head = next(v for k, v in _book(mine).items() if k.startswith("호표|임도표지석|"))
assert head["짝"][0]["로직"] == "GF000101" and head["원래"]["품명"] == "임도표지석"
_build(client, 3, [ref[0] | {"차례": 1}], 통째={"대상": [{"로직": "GF000101"}]}, want=400)
def test_길_권한과_위_층_파일_picks_확인(client):
client.who.update(role="SYSTEM_ADMIN")
assert _post(client, "system").status_code == 200
client.who.update(role="USER")
lines = [{"차례": 1, "대상": [{"로직": "GF000101"}]}]
_build(client, 3, lines, layer="system", 층="system", want=403) # 마스터 쓰기 = 시스템 관리자
got = _build(client, 3, lines, 층="system") # 마스터 파일 → 내 층 사전조합
assert got["참조"]["층"] == "personal" and got["참조"]["주인"] == "7/42"
_build(client, 99, lines, 층="system", want=404)
assert client.get(URL.format("personal")).json()["줄"] == [] # 파일은 마스터에만
rows = client.get("/api/m02/layers/personal/hopyo-picks").json()["줄"]
three = next(r for r in rows if r["번"] == 3)
assert three["확인"] == {"층": "personal", "주인": "7/42", "키": got["참조"]["키"]}
assert next(r for r in rows if r["번"] == 4)["확인"] is None
def test_장부_길_보기_고치기_권한(client):
before = master_hash()
assert _post(client).status_code == 200
_build(client, 3, [{"차례": 1, "대상": [{"로직": "GF000101"}]}])
rows = client.get(LEDGER.format("personal")).json()["줄"]
assert (DIG, "personal", True) in [(r["열쇠"], r["층"], r["고침"]) for r in rows]
assert {r["갈래"] for r in rows if r["열쇠"] != DIG} == {"우리에 없음"} # 729-2 고른 남은 줄
put = {
"줄": {DIG: {"갈래": "짝", "짝": [{"로직": "GF000103", "배": 2}]}, SOIL: {"갈래": "안 씀"}}
}
res = client.put(LEDGER.format("personal"), json=put)
assert res.status_code == 200, res.text
assert res.json()["씀"] == 2
book = _book(layers.personal_dir(7, 42))
assert book[DIG]["짝"][0] == {"로직": "GF000103", "입력값": {}, "배": 2, "몫": None}
assert book[DIG]["처음파일"].endswith("(공통단가).xls") # 처음파일 둠
bad = {"줄": {DIG: {"갈래": "짝", "짝": [{"로직": "XX"}]}}}
assert client.put(LEDGER.format("personal"), json=bad).status_code == 400
assert client.put(LEDGER.format("personal"), json={"줄": {DIG: None}}).status_code == 200
assert DIG not in _book(layers.personal_dir(7, 42))
assert client.put(LEDGER.format("system"), json=put).status_code == 403 # 위 층 = 읽기만
client.who.update(role="SYSTEM_ADMIN")
assert client.put(LEDGER.format("system"), json=put).status_code == 200
client.who.update(role="USER")
rows = client.get(LEDGER.format("personal")).json()["줄"]
assert {(r["층"], r["고침"]) for r in rows} == {("personal", True), ("system", False)}
assert client.get(LEDGER.format("system")).json()["줄"][0]["고침"] is False
assert master_hash() == before