Files
Aislo/resources/tester/test_m02_hopyo_build.py
T
eomsangdonandClaude Opus 5.5 a2c2ad92de fix(M02 실무자료): 361-1 지역만 다른 자재 = 전국 — 줄 글 지역 · (전국) 품목 · 없으면 키 없는 전국 자리표 · 고르기 창 지역 변형 접음 · 전국 되돌림 · 만들기 본뜬.전국자재
- 맞춤: 줄 글 지역 = 그 지역 변형(꼬리표 「지역 — 줄 글」) · 지역 없는 변형((전국) 품목 먼저) · 없으면(레미콘) 「전국」 자리표(바탕 이름(전국)-관급/사급 · 규격 · 단위 · 키 · 단가 없음 · 꼬리표 「전국 — 지역은 프로젝트에서」) · 옛 「못 가림 — N개 중 첫째」 없앰
- 후보 = 그 품목 변형 모두(`변형`) · 고르기 창 1위 / 전국 / 지역 변형(접힘) · 대상 꼴 `{전국: true}`
- 미리 셈 · 만들기: 전국 대상은 서버가 줄에서 다시 셈 · 일위대가 자재 칸이 M01 키만 받아 사전조합 `본뜬.전국자재` 에 적고 빠진 줄 까닭 · 장부엔 안 남김 · 만들기 몸 꼴에 `전국` 칸
- 시험: 전국 · 줄 글 지역 · (전국) 품목 · 고르기 지역 → 전국 되돌림 · 만들기 결과 · 화면 도우미

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
2026-10-16 23:03:56 +09:00

289 lines
15 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"
LEDGER = "/api/m02/layers/{}/hopyo-ledger"
def _build(client, no, lines=None, layer="personal", want=200, **extra):
body = {"층": "personal", "키": "HP-000001", "번": no, **extra}
if lines is not None:
body["줄"] = lines
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["확인"]["때"]
assert doc["담은로직"] == [
{
"로직": "GF000101",
"입력값": {"지역": "서울"},
"별칭": "구조물터파기(인력100%)",
"수량": {"손": 0.14},
"메모": "실무 0.14㎥ × 1 · 토사",
},
{
"로직": "GF000113",
"입력값": {},
"별칭": "구조물되메우기(인력100%)",
"수량": {"손": 0.1}, # 엑셀 0.05 × 배 2
"메모": "실무 0.05㎥ × 2",
},
]
assert doc["자재"] == [] # 표지석 = 몸에 없음 → 안 들어감
assert [(x["이름"], x["금액"], x["까닭"]) for x in got["빠진줄"]] == [
("임도표지석(받침,글새김포함)", 1400000, "확인 안 함"),
("잔토처리(인력100%)", 3097.1, "뺌 — 우리에 없음"),
]
assert got["비교"]["엑셀"] == 1415169 and got["비교"]["우리"] > 0
assert got["다시씀"] is False and got["장부"] == {"씀": 3}
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, wrapped = doc["담은로직"]
assert first["별칭"] == second["별칭"] == "구조물터파기(인력100%)" # 한 줄 로직 둘 = 같은 별칭
assert second["수량"] == {"손": 0.014} and second["메모"] == "실무 0.14㎥ × 0.1(몫 0.1) · 토사"
assert wrapped["별칭"] == "잔토처리(인력100%)" and wrapped["수량"] == {"손": 0.09}
inner = _doc(mine, wrapped["참조"]["키"])
assert inner["본뜬"]["키"] == "HP-000001 제3호표 4째 줄 GF000101"
assert inner["담은로직"][0]["로직"] == "GF000101" and inner["담은로직"][0]["수량"] == {"손": 1}
assert [(m["코드"], m["수량"], m["별도계상"]) for m in 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 _doc(mine, got["참조"]["키"])["담은로직"] == []
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]
assert got["장부"]["씀"] == 0
_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) # 옛 몸(줄 없음) = 확인한 줄이 없음
_build(client, 3, [], want=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 any(x["까닭"].startswith("% 줄") for x in child["빠진줄"])
got = _build(client, 45, ref)
mine = layers.personal_dir(7, 42)
line = _doc(mine, got["참조"]["키"])["담은로직"][0]
assert line["참조"] == child["참조"] and line["별칭"] == "고화매트재료비"
assert line["수량"] == {"손": 2.3}
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}
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 [(r["열쇠"], r["층"], r["고침"]) for r in rows] == [(DIG, "personal", True)]
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