- 충돌 시험 셋(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>
315 lines
17 KiB
Python
315 lines
17 KiB
Python
"""일위대가 실무자료 → 사전조합 만들기(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
|