- 맞춤: 줄 글 지역 = 그 지역 변형(꼬리표 「지역 — 줄 글」) · 지역 없는 변형((전국) 품목 먼저) · 없으면(레미콘) 「전국」 자리표(바탕 이름(전국)-관급/사급 · 규격 · 단위 · 키 · 단가 없음 · 꼬리표 「전국 — 지역은 프로젝트에서」) · 옛 「못 가림 — N개 중 첫째」 없앰
- 후보 = 그 품목 변형 모두(`변형`) · 고르기 창 1위 / 전국 / 지역 변형(접힘) · 대상 꼴 `{전국: true}`
- 미리 셈 · 만들기: 전국 대상은 서버가 줄에서 다시 셈 · 일위대가 자재 칸이 M01 키만 받아 사전조합 `본뜬.전국자재` 에 적고 빠진 줄 까닭 · 장부엔 안 남김 · 만들기 몸 꼴에 `전국` 칸
- 시험: 전국 · 줄 글 지역 · (전국) 품목 · 고르기 지역 → 전국 되돌림 · 만들기 결과 · 화면 도우미
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
289 lines
15 KiB
Python
289 lines
15 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"
|
||
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
|