"""일위대가 실무자료 → 사전조합 만들기(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