Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1
This commit is contained in:
@@ -42,6 +42,7 @@ from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
|
||||
from M02_MasterTemplete import M02_Labor_Patch as patch
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Labor_Spec as spec
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
@@ -127,6 +128,7 @@ def clean(doc: Any) -> dict[str, Any]:
|
||||
"별칭": alias,
|
||||
"메모": str(memo) if memo else None,
|
||||
"수량": _qty(one.get("수량"), where),
|
||||
**patch.ref_clean(one, ref, where), # 그 줄 덮개(715-3)
|
||||
}
|
||||
)
|
||||
continue
|
||||
@@ -155,6 +157,7 @@ def clean(doc: Any) -> dict[str, Any]:
|
||||
**({"켜쌓기율": rate} if rate is not None else {}),
|
||||
**({"고름": picks} if picks else {}),
|
||||
**({"승격": True} if one.get("승격") is True else {}),
|
||||
**patch.clean(one, where, whole, logic), # 그 줄 덮개(715-3)
|
||||
**(
|
||||
{"자재표시": shown} if (shown := _mat_marks(one.get("자재표시"), logic)) else {}
|
||||
),
|
||||
@@ -512,7 +515,7 @@ def calc(
|
||||
lines = []
|
||||
for one in doc.get("담은로직") or []:
|
||||
if _is_ref(one):
|
||||
lines.append(ref_line(one["참조"], expand(one["참조"])))
|
||||
lines.append(ref_line(one["참조"], patch.ref(one, expand)))
|
||||
continue
|
||||
if fixed.is_fixed(one):
|
||||
lines.append(fixed.line(whole, one[fixed.FIXED]))
|
||||
@@ -525,6 +528,9 @@ def calc(
|
||||
"분모": pill.get("단위"),
|
||||
"배": pill.get("배") or 1,
|
||||
}
|
||||
if one.get(patch.FIELD): # 줄 덮개(715-3) = 이름 없는 단가산출 셈 · 별도계상까지
|
||||
lines.append({**line, **patch.run(whole, one, read)})
|
||||
continue
|
||||
got = run_line(whole, code, one.get("입력값"), one.get("켜쌓기율"), one.get("고름"))
|
||||
lines.append({**line, **drop_apart(got, one.get("자재표시"))})
|
||||
mats = [
|
||||
|
||||
@@ -11,6 +11,7 @@ from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Patch as patch
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
@@ -81,10 +82,13 @@ class Expand:
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
pill = pill or {}
|
||||
got = labor.run_line(
|
||||
self.whole, one["로직"], one.get("입력값"), one.get("켜쌓기율"), one.get("고름")
|
||||
)
|
||||
got = labor.drop_apart(got, one.get("자재표시")) # 별도계상 로직 자재 뺌(222-1)
|
||||
if one.get(patch.FIELD): # 줄 덮개(715-3) = 이름 없는 단가산출 · 별도계상까지
|
||||
got = patch.run(self.whole, one, self.read)
|
||||
else:
|
||||
got = labor.run_line(
|
||||
self.whole, one["로직"], one.get("입력값"), one.get("켜쌓기율"), one.get("고름")
|
||||
)
|
||||
got = labor.drop_apart(got, one.get("자재표시")) # 별도계상 로직 자재 뺌(222-1)
|
||||
times = labor._number(pill.get("배")) or Decimal(1)
|
||||
base = got.get("기준")
|
||||
return {
|
||||
@@ -109,9 +113,10 @@ class Expand:
|
||||
inner, rows, why = [], [], None
|
||||
for one in lines:
|
||||
ref = one.get("참조")
|
||||
if isinstance(ref, dict):
|
||||
head, sub = {"참조": ref}, self(ref, trail)
|
||||
elif one.get("로직"): # 로직 줄(51-11) = 1 단위 값(기준 ÷ 배 · 승격과 같음)
|
||||
if isinstance(ref, dict): # 줄 덮개(715-3) 있는 SK 참조 = 그 줄만 다시 셈
|
||||
head = {"참조": ref}
|
||||
sub = self(ref, trail) if not one.get(patch.FIELD) else patch.ref(one, self)
|
||||
elif one.get("로직"): # 로직 줄(51-11) = 1 단위 값(기준 ÷ 배 · 승격과 같음) · 덮개 있음
|
||||
head, sub = {"로직": one["로직"]}, self._logic(one, by_logic.get(one["로직"]))
|
||||
elif fixed.is_fixed(one): # 고정 줄(705-3) = 줄 단가 · 비율 줄 = 끝에(`ratio_row`)
|
||||
head, sub = {"고정": one["고정"]}, fixed.line(self.whole, one["고정"])
|
||||
@@ -156,7 +161,14 @@ class Expand:
|
||||
return {
|
||||
"이름": doc.get("이름"),
|
||||
"단위": doc.get("단위"),
|
||||
"머리": head_of(doc),
|
||||
"기준": None if why else labor._sum(rows),
|
||||
"까닭": why,
|
||||
"안": inner,
|
||||
}
|
||||
|
||||
|
||||
def head_of(doc: dict) -> str | None:
|
||||
"""상세 머리 「1 ㎡ 당」(715-3 · 일위대가 = 기준단위 1 당) — 단위 빈 옛 문서는 None."""
|
||||
unit = str(doc.get("단위") or "").strip()
|
||||
return f"1 {unit} 당" if unit else None
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"""일위대가 · 사전조합 줄 덮개(PLAN 715-3 · 요구서 내역12 4장 16 · 4-1 확정 5).
|
||||
|
||||
줄 칸 `패치` = 단가산출(SK) 문서 꼴 `{덮개, 뺀줄, 더한줄}`(한 벌 · `M02_Sangeun_Doc`) — 그 줄에만 ·
|
||||
로직 줄(`{로직, 입력값, 고름, 자재표시, 패치}`) · 단가산출 참조 줄(`{참조: SK, 패치}`)만 받음 ·
|
||||
줄 `고름` · `자재표시`(호표 차례 열쇠)는 바탕 줄 차례 그대로 — 셈 때 cid 로 바꿔 뺀줄 뒤에도 제자리.
|
||||
더한줄은 인력 · 재료 · 기계 · 로직 · 고정만(참조는 일위대가에 참조 줄로).
|
||||
셈 = 줄을 이름 없는 SK 문서로(`as_doc`) → `M02_Sangeun_Calc` 한 벌 · 로직 줄 기준 = 결과단위 값(배는
|
||||
알약 · `settle` 그대로) · 참조 줄 = 1 단위 값. [단가산출로 저장] = `as_doc` + 이름 · 단위 → 층 SK.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
|
||||
FIELD = "패치"
|
||||
KEEP = ("덮개", "뺀줄", "더한줄")
|
||||
TEMP = "SK-000000" # 이름 없는 줄 단가산출 — 저장 안 함
|
||||
|
||||
|
||||
def _bad(text: str):
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
return store.StoreError(400, text)
|
||||
|
||||
|
||||
def clean(one: dict, where: str, whole: Any, row: dict | None, opened: list | None = None) -> dict:
|
||||
"""줄 칸 `패치` 정리 — 없거나 비면 `{}`(칸 안 둠) · 틀리면 400."""
|
||||
raw = one.get(FIELD)
|
||||
if not raw:
|
||||
return {}
|
||||
if not isinstance(raw, dict):
|
||||
raise _bad(f"{where} · 패치는 묶음")
|
||||
if any("참조" in x for x in raw.get("더한줄") or [] if isinstance(x, dict)):
|
||||
raise _bad(f"{where} · 줄 더한줄엔 참조 못 넣음 — 일위대가에 참조 줄로")
|
||||
body = {k: raw.get(k) for k in KEEP}
|
||||
try:
|
||||
got = sd.clean(
|
||||
{"키": TEMP, "이름": "줄", "단위": "식", **body, "열린입력": opened or []}, row, whole
|
||||
)
|
||||
except sd.DocError as e:
|
||||
raise _bad(f"{where} · {e}") from e
|
||||
out = {k: got[k] for k in KEEP if got[k]}
|
||||
return {FIELD: out} if out else {}
|
||||
|
||||
|
||||
def ref_clean(one: dict, ref: dict, where: str) -> dict:
|
||||
"""단가산출 참조 줄 패치 — 그 SK 의 열린입력이면 품 배율만."""
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
if not one.get(FIELD):
|
||||
return {}
|
||||
if refs.kind_of(ref["키"]) != sd.KIND:
|
||||
raise _bad(f"{where} · 줄 덮개는 로직 줄 · 단가산출 참조 줄만")
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
doc = refs.read_ref(ref) or {}
|
||||
return clean(one, where, m01.loaded()[1], None, doc.get("열린입력"))
|
||||
|
||||
|
||||
def _keyed(value: Any, ids: list[str]) -> dict:
|
||||
"""차례 열쇠 → cid 열쇠(바탕 줄 차례)."""
|
||||
out = {}
|
||||
for n, v in (value or {}).items() if isinstance(value, dict) else ():
|
||||
at = int(n) if str(n).isdigit() else -1
|
||||
if 0 <= at < len(ids):
|
||||
out[ids[at]] = v
|
||||
return out
|
||||
|
||||
|
||||
def as_doc(one: dict, whole: Any, name: str = "줄", unit: str = "식") -> dict[str, Any]:
|
||||
"""일위대가 줄 → SK 문서(바탕 = 그 로직 · 그 SK) — 고름 · 자재표시는 cid 로."""
|
||||
body = {k: (one.get(FIELD) or {}).get(k) or ([] if k != "덮개" else {}) for k in KEEP}
|
||||
head = {"키": TEMP, "이름": name, "단위": unit, **body}
|
||||
if "참조" in one:
|
||||
return {**head, "바탕": {"참조": one["참조"]}}
|
||||
try:
|
||||
ids = sd.line_ids(whole.logic(one["로직"]))
|
||||
except Exception: # noqa: BLE001 — 없는 로직 = 셈이 까닭
|
||||
ids = []
|
||||
extra = {"켜쌓기율": one["켜쌓기율"]} if one.get("켜쌓기율") is not None else {}
|
||||
return {
|
||||
**head,
|
||||
**extra,
|
||||
"바탕": {"로직": one["로직"]},
|
||||
"입력값": dict(one.get("입력값") or {}),
|
||||
"고름": _keyed(one.get("고름"), ids),
|
||||
"자재표시": _keyed(one.get("자재표시"), ids),
|
||||
}
|
||||
|
||||
|
||||
def run(whole: Any, one: dict, read: Any = None) -> dict[str, Any]:
|
||||
"""패치 있는 로직 줄 기준(`run_line` 꼴 · 별도계상 뺀 뒤 · 기준 = 결과단위 값)."""
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
try:
|
||||
doc = sd.clean(as_doc(one, whole), None, whole)
|
||||
return calc.run(doc, whole, None, read or refs.read_ref)
|
||||
except sd.DocError as e:
|
||||
return {"기준": None, "까닭": str(e)}
|
||||
|
||||
|
||||
def ref(one: dict, expand: Any) -> dict[str, Any]:
|
||||
"""참조 줄 펼침 — 패치 있는 SK 참조는 그 줄만 다시 셈(캐시 안 씀) · 그 밖 `expand` 그대로."""
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
if not one.get(FIELD):
|
||||
return expand(one["참조"])
|
||||
doc = as_doc(one, expand.whole)
|
||||
base = expand.read(one["참조"]) or {}
|
||||
doc = {**doc, "이름": base.get("이름") or doc["이름"], "단위": base.get("단위") or doc["단위"]}
|
||||
return calc.unit(doc, expand.whole, expand, ())
|
||||
|
||||
|
||||
def to_sangeun(one: dict, name: str, unit: str, whole: Any) -> dict[str, Any]:
|
||||
"""[단가산출로 저장] 문서 — 줄의 바탕 · 입력값 · 고름 · 덮개 → 이름 붙인 SK(키는 서버)."""
|
||||
if "로직" not in one and "참조" not in one:
|
||||
raise _bad("로직 줄 · 단가산출 참조 줄만 단가산출로 저장")
|
||||
doc = as_doc(one, whole, name, unit)
|
||||
doc.pop("키")
|
||||
return doc
|
||||
@@ -163,7 +163,10 @@ def clean_doc(kind: str, doc: Any) -> dict[str, Any]:
|
||||
out: dict[str, Any] = {"종류": TITLE[kind], "키": key}
|
||||
if not name:
|
||||
raise _bad("사전조합 이름이 빔")
|
||||
out.update(이름=name, 단위=unit or None, 담은로직=_combo_lines(doc, key))
|
||||
lines = _combo_lines(doc, key)
|
||||
if not unit and (lines or doc.get("자재")): # 일위대가 = 기준단위 1 당(요구 27 · 715-3) ·
|
||||
raise _bad("일위대가 단위가 빔 — 1 단위 값의 단위") # 빈 새 문서(이름만)는 받음
|
||||
out.update(이름=name, 단위=unit or None, 담은로직=lines)
|
||||
out["자재"] = _combo_mats(doc, out["담은로직"])
|
||||
out.update({k: str(doc.get(k) or "").strip() for k in ("구분", "상세구분")})
|
||||
out["본뜬"] = doc["본뜬"] if isinstance(doc.get("본뜬"), dict) else None
|
||||
@@ -226,9 +229,9 @@ def _combo_lines(doc: dict, key: str) -> list[dict[str, Any]]:
|
||||
|
||||
lines = []
|
||||
for one in labor.clean({"담은로직": doc.get("담은로직") or []})["담은로직"]:
|
||||
head = {k: one[k] for k in ("참조", "고정") if k in one} or {"로직": one["로직"]}
|
||||
head = {k: one[k] for k in ("참조", "고정", "패치") if k in one} # 패치 = 줄 덮개(715-3)
|
||||
if "로직" in one:
|
||||
head["입력값"] = one["입력값"]
|
||||
head.update(로직=one["로직"], 입력값=one["입력값"])
|
||||
head.update({k: one[k] for k in ("켜쌓기율", "고름", "자재표시") if k in one})
|
||||
head.update({"별칭": one["별칭"]} if one["별칭"] else {}) # 실무 품명(324-3)
|
||||
lines.append({**head, "수량": one["수량"] or {"손": 1}, "메모": one["메모"]})
|
||||
@@ -331,7 +334,8 @@ def users(ref: Ref) -> list[str]:
|
||||
"""`ref` 를 담은 일위대가 · 사전조합 · 단가산출(바탕 · 더한줄) — 「어디 이름」 목록(모든 층 훑음 ·
|
||||
프로젝트 뺌)."""
|
||||
rid, out = ref_id(ref), []
|
||||
for kind, what in (("labor", "일위대가"), ("precombo", "사전조합"), ("sangeun", "단가산출")):
|
||||
# 「일위대가」 = 사전조합 새 이름(내역 14 확정 1) · 구조물 조합 일위대가 = 「구조물 조합」
|
||||
for kind, what in (("labor", "구조물 조합"), ("precombo", "일위대가"), ("sangeun", "단가산출")):
|
||||
for at, path in layers.layer_files(kind):
|
||||
try:
|
||||
doc = json.loads(path.read_bytes())
|
||||
|
||||
@@ -170,6 +170,10 @@ def unit(doc: dict, whole: Any, expand: Any, trail: tuple) -> dict[str, Any]:
|
||||
return {**head, "기준": got["1단위"], "까닭": why, "줄": lines, "짝잃음": got["짝잃음"]}
|
||||
|
||||
|
||||
def head_of(doc: dict) -> str:
|
||||
return f"1 {doc.get('단위')} 당"
|
||||
|
||||
|
||||
# ── 저장 ──────────────────────────────────────────────────────────────
|
||||
def _check_refs(doc: dict, here_: refs.Here) -> None:
|
||||
me = {"층": here_[0], "주인": here_[1], "키": doc["키"]}
|
||||
@@ -227,4 +231,22 @@ def calc_doc(doc: Any, given: dict | None = None) -> dict[str, Any]:
|
||||
got = run(clean, whole, given, trail=(me,))
|
||||
except sd.DocError as e:
|
||||
raise _fail(e) from e
|
||||
return labor._json(got)
|
||||
return labor._json({**got, "머리": head_of(clean)})
|
||||
|
||||
|
||||
def from_line(line: Any, name: str, unit: str) -> dict[str, Any]:
|
||||
"""[단가산출로 저장](715-3) — 일위대가 줄(바탕 · 입력값 · 고름 · 줄 덮개) → 지금 층 새 SK ·
|
||||
답 = `{이름: 새 키, 줄: 그 SK 참조 줄}`(일위대가 문서는 화면 [저장] 때 바뀜)."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Labor_Patch as patch
|
||||
|
||||
if not isinstance(line, dict):
|
||||
raise store.StoreError(400, "줄이 묶음 아님")
|
||||
doc = patch.to_sangeun(line, name, unit, m01.loaded()[1])
|
||||
made = refs.create(sd.KIND, doc)
|
||||
at = refs.here()
|
||||
keep = {k: line[k] for k in ("별칭", "메모", "수량") if k in line}
|
||||
return {
|
||||
"이름": made["이름"],
|
||||
"줄": {"참조": {"층": at[0], "주인": at[1], "키": made["이름"]}, **keep},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""M02 단가산출(SK) 셈 길(PLAN 715-1b) — `POST /api/m02/layers/{층}/sangeun/calc`.
|
||||
"""M02 단가산출(SK) 셈 길(PLAN 715-1b) — `POST /api/m02/layers/{층}/sangeun/calc` ·
|
||||
[단가산출로 저장](715-3) `POST /api/m02/layers/{층}/sangeun/from-line`(층 쓰기 권한).
|
||||
|
||||
저장 안 함 · 로그인 사용자 · 층 읽기 권한(`Router_Items._root`) · 화면은 최신 응답만.
|
||||
몸 = `문서`(저장 전 화면 문서) 또는 `키`(그 층 저장본) + `입력값`(열린입력 · 시험 값).
|
||||
@@ -37,6 +38,24 @@ def _calc(body: SangeunCalcBody) -> dict[str, Any]:
|
||||
return calc.calc_doc(doc, body.입력값)
|
||||
|
||||
|
||||
class FromLineBody(BaseModel):
|
||||
줄: dict[str, Any]
|
||||
이름: str
|
||||
단위: str
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/sangeun/from-line")
|
||||
def sangeun_from_line(
|
||||
layer: Layer,
|
||||
body: FromLineBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""[단가산출로 저장](715-3) — 일위대가 줄 → 그 층 새 단가산출 · 답 = 새 키 + 바꿀 참조 줄."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, calc.from_line, body.줄, body.이름, body.단위)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/sangeun/calc")
|
||||
def calc_sangeun(
|
||||
layer: Layer,
|
||||
|
||||
@@ -30,7 +30,7 @@ SYS = {"층": "system", "주인": None}
|
||||
|
||||
def _save(folder, doc):
|
||||
with store.at(folder):
|
||||
key = refs.create("precombo", {"이름": "자재 조합", **doc})["이름"]
|
||||
key = refs.create("precombo", {"이름": "자재 조합", "단위": "㎡", **doc})["이름"]
|
||||
return key, json.loads((folder / "precombo" / f"{key}.json").read_bytes())
|
||||
|
||||
|
||||
@@ -82,9 +82,9 @@ def _world_combo(world): # noqa: F811
|
||||
# MAT = 직접계산(222-1 · 기본 별도계상이면 셈에서 빠짐)
|
||||
mats = [{"코드": MAT, "수량": {"손": 2}, "별도계상": "직접계산"}, {"코드": WIRE, "수량": None}]
|
||||
with store.at(world["system"]):
|
||||
key = refs.create("precombo", {"이름": "자재 조합", "담은로직": lines, "자재": mats})[
|
||||
"이름"
|
||||
]
|
||||
key = refs.create(
|
||||
"precombo", {"이름": "자재 조합", "단위": "㎡", "담은로직": lines, "자재": mats}
|
||||
)["이름"]
|
||||
stone = Decimal(str(labor.run_line(m01.loaded()[1], STONE, {})["기준"]["계"]))
|
||||
return key, stone + _price(MAT) * 2
|
||||
|
||||
@@ -114,7 +114,11 @@ def test_B08_참조_줄_재료에_사전조합_자재_손_곱(world): # noqa: F
|
||||
with store.at(world["system"]):
|
||||
outer = refs.create(
|
||||
"precombo",
|
||||
{"이름": "겹", "담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}]},
|
||||
{
|
||||
"이름": "겹",
|
||||
"단위": "㎡",
|
||||
"담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}],
|
||||
},
|
||||
)["이름"]
|
||||
got = _ref_mats(m01.loaded()[1], refs.read_ref, {**SYS, "키": outer})
|
||||
mats = [(m["품목"]["키"], q) for m, q in got if m.get("자재")]
|
||||
|
||||
@@ -62,7 +62,7 @@ def world(tmp_path, monkeypatch):
|
||||
company = layers.company_dir(7)
|
||||
company.mkdir(parents=True)
|
||||
with store.at(company):
|
||||
other = refs.create("precombo", {"이름": "회사 조합", "담은로직": lines[:1]})["이름"]
|
||||
other = refs.create("precombo", {"이름": "회사 조합", "단위": "㎡", "담은로직": lines[:1]})["이름"]
|
||||
project = tmp_path / "storage" / "7" / "1" / "proj"
|
||||
project.mkdir(parents=True)
|
||||
return {"system": system, "key": key, "company": other, "root": project}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""일위대가 · 사전조합 줄 덮개 · [단가산출로 저장] · 단위 빈칸 막기(PLAN 715-3) — 임시 storage.
|
||||
|
||||
줄 덮개 = 그 줄에만(단가산출 원본 그대로) · 값 = 같은 덮개 SK 값 = 손셈 ·
|
||||
줄 고름(차례)이 뺀줄 뒤 제자리 ·
|
||||
[단가산출로 저장] = 새 SK + 참조 줄 · 같은 값 · 단위 빈칸 400 · 덮개 없는 줄 = 옛 값.
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
from resources.tester.test_m02_labor_ref import MINE, world # noqa: F401 — 같은 임시 층
|
||||
|
||||
PCT = "GC001109" # 원/㎡ · 인력 둘(LB000036 · LB000002) + 「노무비 * 3 / 100」
|
||||
TWO_MATS = "GC000565" # 원/m · 재료 줄 둘
|
||||
COVER = {"덮개": {"labor:LB000036#0": {"qty": 0.5}}}
|
||||
|
||||
|
||||
def _whole():
|
||||
return m01.loaded()[1]
|
||||
|
||||
|
||||
def _sc(folder, lines, unit="㎡"):
|
||||
with store.at(folder):
|
||||
return refs.create("precombo", {"이름": "조합", "단위": unit, "담은로직": lines})["이름"]
|
||||
|
||||
|
||||
def _value(folder, key):
|
||||
with store.at(folder):
|
||||
return refs.Expand(_whole())({**MINE, "키": key})
|
||||
|
||||
|
||||
def test_logic_line_cover_in_precombo(world):
|
||||
"""사전조합 로직 줄 덮개 = 같은 덮개 SK 값 · 손셈(품 0.5 × 단가 + 둘째 줄 · 3 % 덧줄)."""
|
||||
bare = labor.run_line(_whole(), PCT, {})
|
||||
sc = _sc(world["personal"], [{"로직": PCT, "패치": COVER}])
|
||||
with store.at(world["personal"]):
|
||||
kept = store.read("precombo", sc)["문서"]["담은로직"][0]
|
||||
assert kept["패치"] == COVER
|
||||
got = _value(world["personal"], sc)["기준"]
|
||||
labor_sum = Decimal("0.5") * Decimal(str(bare["줄"][0]["단가"])) + Decimal(
|
||||
str(bare["줄"][1]["금액"])
|
||||
)
|
||||
assert got["노무비"] == pytest.approx(labor_sum)
|
||||
assert got["경비"] == pytest.approx(labor_sum * 3 / 100, abs=1)
|
||||
|
||||
|
||||
def test_line_pick_by_order_stays_after_drop(world):
|
||||
"""줄 고름 = 바탕 호표 차례(2) · 패치 뺀줄로 첫 재료 줄을 빼도 그 자재가 그 줄에."""
|
||||
import master_pick as mp
|
||||
|
||||
whole = _whole()
|
||||
row = whole.logic(TWO_MATS)
|
||||
item = row["호표"][2]
|
||||
want = mp.search(whole, item["요소"], {}, item.get("단위"))[1]
|
||||
one = {
|
||||
"로직": TWO_MATS,
|
||||
"고름": {"2": want},
|
||||
"자재표시": {"2": {"별도계상": "직접계산"}},
|
||||
"패치": {"뺀줄": ["material:" + row["호표"][1]["이름"] + "#0"]},
|
||||
}
|
||||
with store.at(world["personal"]):
|
||||
doc = labor.clean({"담은로직": [one]})
|
||||
got = labor.calc(doc, {})["줄"][0]
|
||||
picked = [x for x in got["줄"] if x.get("품목")]
|
||||
assert [x["품목"]["키"] for x in picked] == [want]
|
||||
|
||||
|
||||
def test_ref_line_cover_only_that_line(world):
|
||||
"""SK 참조 줄 덮개 = 그 줄만 · SK 원본 · 다른 줄 값 그대로."""
|
||||
with store.at(world["personal"]):
|
||||
sk = refs.create(
|
||||
"sangeun", {"이름": "밑", "단위": "㎡", "바탕": {"로직": PCT}, "더한줄": []}
|
||||
)["이름"]
|
||||
before = store.read("sangeun", sk)["판"]
|
||||
ref = {**MINE, "키": sk}
|
||||
sc = _sc(
|
||||
world["personal"],
|
||||
[{"참조": ref, "수량": {"손": 1}, "패치": COVER}, {"로직": "GC001111", "수량": {"손": 1}}],
|
||||
)
|
||||
inner = _value(world["personal"], sc)["안"]
|
||||
plain = _value(world["personal"], _sc(world["personal"], [{"참조": ref}]))["기준"]
|
||||
covered = calc.calc_doc(
|
||||
{"키": "SK-000999", "이름": "x", "단위": "㎡", "바탕": {"로직": PCT}, **COVER}
|
||||
)
|
||||
assert inner[0]["기준"]["계"] == pytest.approx(Decimal(str(covered["1단위"]["계"])))
|
||||
assert inner[0]["기준"]["계"] != plain["계"]
|
||||
with store.at(world["personal"]):
|
||||
assert store.read("sangeun", sk)["판"] == before
|
||||
|
||||
|
||||
def test_save_as_sangeun(world):
|
||||
"""[단가산출로 저장] — 새 SK(바탕 · 입력값 · 고름 cid · 덮개) · 참조 줄 · 같은 1 단위 값."""
|
||||
one = {"로직": PCT, "패치": COVER, "수량": {"손": 2}, "별칭": "내 줄"}
|
||||
with store.at(world["personal"]):
|
||||
line_value = labor.calc(labor.clean({"담은로직": [one]}), {})["줄"][0]["기준"]
|
||||
got = calc.from_line(one, "줄로 만든", "㎡")
|
||||
doc = store.read("sangeun", got["이름"])["문서"]
|
||||
value = calc.calc_doc(doc)["기준"]
|
||||
assert got["줄"] == {"참조": {**MINE, "키": got["이름"]}, "수량": {"손": 2}, "별칭": "내 줄"}
|
||||
assert doc["바탕"] == {"로직": PCT} and doc["덮개"]["labor:LB000036#0"]["qty"] == 0.5
|
||||
assert value["계"] == pytest.approx(line_value["계"])
|
||||
|
||||
|
||||
def test_refusals(world):
|
||||
with store.at(world["personal"]):
|
||||
with pytest.raises(store.StoreError, match="단위가 빔"):
|
||||
refs.create("precombo", {"이름": "x", "단위": " ", "담은로직": [{"로직": PCT}]})
|
||||
assert refs.create("precombo", {"이름": "빈 새 문서", "단위": None, "담은로직": []})["이름"]
|
||||
bad = {"로직": PCT, "패치": {"더한줄": [{"참조": {**MINE, "키": "SK-000001"}, "수량": 1}]}}
|
||||
with pytest.raises(store.StoreError, match="참조 못 넣음"):
|
||||
labor.clean({"담은로직": [bad]})
|
||||
with pytest.raises(store.StoreError, match="수가 아님"):
|
||||
labor.clean(
|
||||
{"담은로직": [{"로직": PCT, "패치": {"덮개": {"labor:LB000036#0": {"qty": "식"}}}}]}
|
||||
)
|
||||
sc = _sc(world["personal"], [{"로직": PCT}])
|
||||
with pytest.raises(store.StoreError, match="줄 덮개는"):
|
||||
labor.clean({"담은로직": [{"참조": {**MINE, "키": sc}, "패치": COVER}]})
|
||||
|
||||
|
||||
def test_no_patch_same_value(world):
|
||||
"""덮개 없는 줄 = run_line 값 그대로(옛 셈 길)."""
|
||||
sc = _sc(world["personal"], [{"로직": PCT, "패치": {}}])
|
||||
with store.at(world["personal"]):
|
||||
assert "패치" not in store.read("precombo", sc)["문서"]["담은로직"][0]
|
||||
assert _value(world["personal"], sc)["기준"]["계"] == pytest.approx(
|
||||
labor.run_line(_whole(), PCT, {})["기준"]["계"]
|
||||
)
|
||||
@@ -1,4 +1,5 @@
|
||||
"""M02 단가산출(SK) 서고 · 셈(PLAN 715-1b · 715-2 · `M02_Sangeun_Calc` · `M02_Labor_Ref`) — 임시 storage.
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""M02 단가산출(SK) 서고 · 셈(PLAN 715-1b · 715-2) — 임시 storage.
|
||||
|
||||
고친 줄 값 = 손셈 · 덧줄 % 엔진이 다시 셈 · 뺀줄 뒤 고름 제자리 · 짝 잃음 남김 · 원값 서버 ·
|
||||
순환 422 · 쓰는 곳 지우기 409 · 사전조합이 SK 를 담음 · 층 복사 왕복 같은 값.
|
||||
@@ -46,7 +47,7 @@ def _bare(code):
|
||||
|
||||
|
||||
def test_qty_cover_hand_and_pct_recomputed(world):
|
||||
"""품 덮개 = 손셈 · 「노무비 * 3 / 100」 덧줄이 덮인 노무비에서 다시 셈 · 원값 = 서버가 그때 M01 값."""
|
||||
"""품 덮개 = 손셈 · 「노무비 * 3 / 100」 덧줄 다시 셈 · 원값 = 서버가 셈."""
|
||||
bare = _bare(PCT)
|
||||
first, second = bare["줄"][0], bare["줄"][1]
|
||||
key = _sk(
|
||||
@@ -134,14 +135,18 @@ def test_precombo_holds_sangeun(world):
|
||||
with store.at(world["personal"]):
|
||||
sc = refs.create(
|
||||
"precombo",
|
||||
{"이름": "조합", "담은로직": [{"참조": {**MINE, "키": sk}, "수량": {"손": 2}}]},
|
||||
{
|
||||
"이름": "조합",
|
||||
"단위": "㎡",
|
||||
"담은로직": [{"참조": {**MINE, "키": sk}, "수량": {"손": 2}}],
|
||||
},
|
||||
)["이름"]
|
||||
got = labor.calc({"담은로직": [{"참조": {**MINE, "키": sc}, "수량": {"손": 1}}]}, {})
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
store.delete("sangeun", sk)
|
||||
one = _calc(world["personal"], sk)["1단위"]["계"]
|
||||
assert got["반영합"]["계"] == pytest.approx(one * 2, abs=1)
|
||||
assert e.value.status == 409 and "사전조합 개인 7/42" in str(e.value.detail)
|
||||
assert e.value.status == 409 and "일위대가 개인 7/42" in str(e.value.detail)
|
||||
|
||||
|
||||
def test_layer_copy_round_trip(world):
|
||||
|
||||
@@ -70,7 +70,7 @@ def _promote(folder, code=TEN, values=None, **extra):
|
||||
|
||||
def _combo(folder, lines, name="조합"):
|
||||
with store.at(folder):
|
||||
return refs.create("precombo", {"이름": name, "담은로직": lines})["이름"]
|
||||
return refs.create("precombo", {"이름": name, "단위": "㎡", "담은로직": lines})["이름"]
|
||||
|
||||
|
||||
def _read(folder, kind, key):
|
||||
@@ -225,11 +225,15 @@ def test_calc_는_참조를_펼침_승격은_기준_나누기_배_사전조합
|
||||
one = got["줄"][0]
|
||||
want = Decimal(str(_base(TEN)["계"])) / 10 * 2
|
||||
assert one["기준"]["계"] == pytest.approx(float(want))
|
||||
assert (one["분모"], one["배"], one["알약"]) == (None, 1, refs.ref_id(ref))
|
||||
assert (one["분모"], one["배"], one["알약"]) == (
|
||||
"㎡",
|
||||
1,
|
||||
refs.ref_id(ref),
|
||||
) # 단위 = 그 사전조합 것
|
||||
assert one["반영"]["계"] == pytest.approx(float(want * 5))
|
||||
assert one["안"][0]["수량"] == 2 and one["안"][0]["참조"]["키"] == sp
|
||||
pills = labor.pills(doc)
|
||||
assert pills == [{"키": refs.ref_id(ref), "이름": "둘", "단위": "", "로직": [], "참조": ref}]
|
||||
assert pills == [{"키": refs.ref_id(ref), "이름": "둘", "단위": "㎡", "로직": [], "참조": ref}]
|
||||
|
||||
|
||||
def test_승격_줄_상세는_원_로직_키_표_사전조합은_안_줄만(world):
|
||||
@@ -364,7 +368,7 @@ def test_길_새로_복사_권한(world):
|
||||
}
|
||||
client = TestClient(app)
|
||||
url = "/api/m02/layers/{}/library/{}"
|
||||
body = {"문서": {"이름": "가", "담은로직": [{"로직": TEN}]}}
|
||||
body = {"문서": {"이름": "가", "단위": "㎡", "담은로직": [{"로직": TEN}]}}
|
||||
got = client.post(url.format("personal", "precombo"), json=body).json()
|
||||
assert got["이름"] == "SC-000001"
|
||||
assert client.post(url.format("system", "precombo"), json=body).status_code == 403
|
||||
|
||||
Reference in New Issue
Block a user