- /api/m01/matsets · matset · matset/new|edit|delete — sub4 의 자재조합.json(UM) 위에 얹음 · 담은 자재 코드가 마스터에 없으면 422 · 본뜬키는 있는 다른 UM 만 - 지우기 전 집계표 · 구조물집계표조합의 「자재조합」 칸을 훑어 쓰는 열이 있으면 409 + 구조물 이름들 - 일위대가 · 자재 조합 지우기 검사가 users_of 한 곳을 씀 · 계약 9장 「자재 조합」 표 더함 - 시험 다섯(resources/tester/test_m01_matset.py) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoyUwuTFiDJfvCfHpvKKzp
137 lines
5.6 KiB
Python
137 lines
5.6 KiB
Python
"""M01 자재 조합 저장소 — 목록 · 하나 읽기 · 만들기 · 고치기 · 지우기.
|
|
|
|
계약: `_화면_계약.md` 9장 끝 · 파일 모양: `_틀.md` 11장 · 엔진: `scripts/master_matset.py`.
|
|
조합은 담은 자재 목록(코드 · 이름 · 규격 · 단위)만 지님 — 수량 · 단가는 두지 않음(재료산출 몫).
|
|
자리는 `자재조합.json` 한 파일 · 키는 `_키대장.json` 의 `UM` 다음 번호(서버가 줌).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from pathlib import Path
|
|
|
|
from M01_MasterData import M01_MasterData_Store as store
|
|
from M01_MasterData import M01_MasterData_Store_Combo as combos
|
|
|
|
cm = store.cm
|
|
msk = cm.mmc # sub4 가 세운 자재 조합 엔진(`scripts/master_material_combo.py`)
|
|
|
|
FILE = msk.FILE
|
|
OWNERS = msk.OWNERS
|
|
|
|
|
|
def _path() -> Path:
|
|
return store.FOLDER / FILE
|
|
|
|
|
|
def _make_file() -> None:
|
|
"""파일이 없으면 머리만 두고 만듦."""
|
|
if not _path().is_file():
|
|
head = {"그룹": msk.GROUP, "판": str(date.today().year), "줄": []}
|
|
_path().write_text(store.dump(head), encoding="utf-8", newline="\n")
|
|
|
|
|
|
def _read() -> tuple[dict, str]:
|
|
_make_file()
|
|
return store.read(FILE)
|
|
|
|
|
|
def _keys() -> list[str]:
|
|
return [str(r.get("키")) for r in _read()[0].get("줄") or []]
|
|
|
|
|
|
def _find(data: dict, key: str) -> dict:
|
|
for row in data.get("줄") or []:
|
|
if str(row.get("키")) == key:
|
|
return row
|
|
raise store.StoreError(404, f"없는 자재 조합 「{key}」")
|
|
|
|
|
|
def _clean(one: dict, owner: str, key: str | None = None) -> dict:
|
|
"""서버가 잡는 칸 — 키 · 소유 · 열 한 벌(안 준 칸은 null · 자재 줄은 네 칸만)."""
|
|
if not isinstance(one, dict):
|
|
raise store.StoreError(400, "자재 조합 줄이 묶음 아님")
|
|
name = one.get("이름")
|
|
if not isinstance(name, str) or not name.strip():
|
|
raise store.StoreError(400, "자재 조합 줄에 「이름」 이 없거나 글자 아님")
|
|
if owner not in OWNERS:
|
|
raise store.StoreError(400, f"모르는 소유 「{owner}」 — {' · '.join(OWNERS)}")
|
|
origin = one.get("본뜬키")
|
|
if origin is not None and (origin == key or origin not in _keys()):
|
|
raise store.StoreError(400, f"본뜬키 「{origin}」 — 비었거나 있는 다른 자재 조합 키여야 함")
|
|
got = one.get("담은자재")
|
|
if isinstance(got, list): # 화면이 덤을 실어도 자재 줄은 네 칸만 · 안 준 칸은 빈 글
|
|
got = [{c: m.get(c, "") for c in msk.ITEM_COLS} if isinstance(m, dict) else m for m in got]
|
|
rest = {k: v for k, v in one.items() if k not in ("키", "원문번호", "소유")}
|
|
_check_codes(got)
|
|
row = {**({"키": key} if key else {}), "소유": owner, **rest, "담은자재": got or []}
|
|
for col in msk.COLS:
|
|
row.setdefault(col, None)
|
|
return {k: row[k] for k in msk.COLS if k in row}
|
|
|
|
|
|
def _check_codes(got) -> None:
|
|
"""담은 자재 코드가 마스터 어느 줄에도 없으면 422 — 틀 검사는 저장 길(`store.save`)이 함."""
|
|
if not isinstance(got, list) or not got:
|
|
return
|
|
known = {str(r.get("키")) for d in store.loaded()[0].values() for r in d.get("줄") or []}
|
|
lost = [str(m.get("코드")) for m in got if isinstance(m, dict) and m.get("코드") not in known]
|
|
if lost:
|
|
raise store.StoreError(422, {"errors": [f"마스터에 없는 자재 {' · '.join(lost)}"]})
|
|
|
|
|
|
def _write(changes: list[dict], version: str | None = None) -> dict:
|
|
_make_file()
|
|
return store.save(
|
|
[{"file": FILE, "version": version or store.read(FILE)[1], "changes": changes}]
|
|
)[0]
|
|
|
|
|
|
def matsets(q: str = "", owner: str = "") -> list[dict]:
|
|
"""목록 — 찾기(이름 · 키)와 소유로 거름 · 줄마다 자재 수."""
|
|
out = []
|
|
for row in store.loaded()[0].get(FILE, {}).get("줄") or []:
|
|
if owner and row.get("소유") != owner:
|
|
continue
|
|
if not store._hit(row, q):
|
|
continue
|
|
out.append(
|
|
{k: row.get(k) for k in msk.COLS if k != "담은자재"}
|
|
| {"file": FILE, "count": len(msk.items(row))}
|
|
)
|
|
return out
|
|
|
|
|
|
def matset(key: str) -> dict:
|
|
data, version = _read()
|
|
return {"file": FILE, "version": version, "matset": _find(data, key)}
|
|
|
|
|
|
def matset_new(one: dict, owner: str = OWNERS[0]) -> dict:
|
|
"""새 자재 조합 — 키는 서버가 `UM` 다음 번호로 줌."""
|
|
before = set(_keys())
|
|
done = _write([{"op": "add", "row": _clean(one, owner)}])
|
|
key = next((k for k in reversed(_keys()) if k not in before), "")
|
|
return {**done, "key": key, "matset": _find(_read()[0], key) if key else {}}
|
|
|
|
|
|
def matset_edit(key: str, version: str, one: dict, owner: str = "") -> dict:
|
|
data, _ = _read()
|
|
was = _find(data, key)
|
|
if store.read(FILE)[1] != version:
|
|
raise store.StoreError(409, {"stale": [FILE]})
|
|
fixed = _clean(one, owner or str(was.get("소유") or OWNERS[0]), key)
|
|
done = _write([{"op": "edit", "key": key, "row": fixed}], version)
|
|
return {**done, "matset": _find(_read()[0], key)}
|
|
|
|
|
|
def matset_delete(key: str, version: str) -> dict:
|
|
"""지우기 — 구조물 열이 「자재조합」 칸으로 쓰면 409 + 구조물 이름들(지우지 않음)."""
|
|
_find(_read()[0], key)
|
|
if users := combos.users_of("자재조합", key):
|
|
msg = f"구조물 {len(users)} 곳이 쓰는 자재 조합이라 못 지움 — {' · '.join(users)}"
|
|
raise store.StoreError(409, {"errors": [msg], "users": users})
|
|
if store.read(FILE)[1] != version:
|
|
raise store.StoreError(409, {"stale": [FILE]})
|
|
return _write([{"op": "delete", "key": key}], version)
|