Files
Aislo/M02_MasterTemplete/M02_Labor_Expand.py
T
eomsangdonandClaude Opus 5.5 b62a1d9356 feat(715-3): 일위대가 줄 덮개(패치) · SK 참조 줄 · [단가산출로 저장] 길 · 일위대가 단위 빈칸 막기 · 「1 단위 당」 머리
- 새 M02_Labor_Patch.py — 줄 칸 패치 = SK 꼴 {덮개, 뺀줄, 더한줄}(한 벌) · 로직 줄 · SK 참조 줄만 · 줄 고름 · 자재표시(차례)를 cid 로 바꿔 뺀줄 뒤 제자리 · 셈 = 이름 없는 SK 로 M02_Sangeun_Calc
- M02_Labor_Doc — clean 이 패치 정리 · calc 로직 줄 · 참조 줄 패치 갈래(몇 줄 · 690 줄)
- M02_Labor_Expand — 사전조합 안 줄 패치 갈래 · 머리 「1 ㎡ 당」
- M02_Labor_Ref — 사전조합 줄에 패치 남김 · 줄 있는 일위대가 단위 빈칸 400(빈 새 문서는 받음 — 화면 새로 단추가 단위 null) · 409 글 종류 이름 사전조합 → 일위대가 · labor → 구조물 조합
- POST /api/m02/layers/{층}/sangeun/from-line(층 쓰기 권한) — 새 SK + 바꿀 참조 줄 · 셈 답에 머리
- Labor_Calc.ts 짝은 그대로(settle 은 서버 기준을 받음 · 거울 시험 통과)
- 낡은 시험 고침: test_m02_labor_ref · test_175_precombo_mats · test_204_2_b09_bill_library 의 줄 있는 사전조합에 단위(빈칸 막기) · 참조 줄 분모 = 그 사전조합 단위
- 새 시험 test_715_3_labor_patch.py(임시 storage)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 16:04:31 +09:00

175 lines
8.3 KiB
Python

"""M02 서고 참조 펼침(계산) — `M02_Labor_Ref.Expand` 를 떼어 둠(PLAN 715-2 · 700 줄).
참조 → `{이름, 단위, 기준(1 단위), 까닭, 안?}` — 단가승격(옛 SP) · 사전조합(SC) · 단가산출(SK ·
`M02_Sangeun_Calc.unit`). 이름은 `M02_Labor_Ref.Expand` 로 그대로 부름.
"""
from __future__ import annotations
from collections.abc import Callable
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
Ref = dict[str, Any]
class Expand:
"""참조 → `{이름, 단위, 기준(1 단위), 까닭, 안?}` — 한 번 셈 안에서 키별 캐시 ·
깊이 상한 · 순환은 까닭(통째로 멈추지 않음). `read` = 참조 → 문서(프로젝트는 굳힌 사본)."""
def __init__(self, whole: Any, read: Callable[[Ref], dict | None] = refs.read_ref) -> None:
self.whole, self.read, self.memo = whole, read, {}
def __call__(self, ref: Ref, trail: tuple[str, ...] = ()) -> dict[str, Any]:
rid = refs.ref_id(ref)
if rid in trail:
keys = [t.rsplit("|", 1)[-1] for t in (*trail, rid)]
return self._stop(ref, "순환 — " + " → ".join(keys))
if len(trail) >= refs.DEPTH:
return self._stop(ref, f"참조가 {refs.DEPTH}단을 넘음")
if rid not in self.memo:
doc = self.read(ref)
if not isinstance(doc, dict):
return self._stop(ref, f"없는 참조 「{ref['키']}」")
if refs.kind_of(ref["키"]) == "promote":
self.memo[rid] = self._promote(doc)
elif refs.kind_of(ref["키"]) == "sangeun": # 단가산출(715-2) = 고친 줄 셈 1 단위
self.memo[rid] = calc.unit(doc, self.whole, self, (*trail, rid))
else:
self.memo[rid] = self._combo(doc, (*trail, rid))
return self.memo[rid]
@staticmethod
def _stop(ref: Ref, why: str) -> dict[str, Any]:
return {"이름": ref["키"], "단위": None, "기준": None, "까닭": why}
def _promote(self, doc: dict) -> dict[str, Any]:
from M02_MasterTemplete import M02_Labor_Doc as labor
line = (doc.get("담은로직") or [{}])[0]
code = str(line.get("로직") or "")
got = labor.run_line(
self.whole, code, line.get("입력값"), line.get("켜쌓기율"), line.get("고름")
)
times = labor._number(doc.get("배")) or Decimal(1)
base = got.get("기준")
why = got.get("까닭") or (None if base else "돈 아닌 로직")
return {
"이름": doc.get("이름"),
"단위": doc.get("단위"),
"로직": line.get("로직"),
"기준": {k: Decimal(str(v)) / times for k, v in base.items()} if base else None,
"까닭": why,
# 원 로직 줄(줄 누름 하위 행 · 51-2) — 1 단위로 나눔(하위 합 = 기준)
"줄": [
{
**x,
"수량": Decimal(str(x["수량"])) / times,
"금액": Decimal(str(x["금액"])) / times,
"비목": {k: Decimal(str(v)) / times for k, v in (x.get("비목") or {}).items()},
}
for x in got.get("줄") or []
],
}
def _logic(self, one: dict, pill: dict | None) -> dict[str, Any]:
"""사전조합 안 로직 줄 — 일위대가 로직 줄과 같은 `run_line` · 배는 물량 알약 것."""
from M02_MasterTemplete import M02_Labor_Doc as labor
pill = pill or {}
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 {
"이름": pill.get("이름") or one["로직"],
"단위": pill.get("단위"),
"기준": {k: Decimal(str(v)) / times for k, v in base.items()} if base else None,
"까닭": got.get("까닭") or (None if base else "돈 아닌 로직"),
}
def _combo(self, doc: dict, trail: tuple[str, ...]) -> dict[str, Any]:
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
from M02_MasterTemplete import M02_Labor_Doc as labor
lines = [one for one in doc.get("담은로직") or [] if isinstance(one, dict)]
kept = doc.get("알약") # 프로젝트 참조사본 = 가져온 때 굳힌 알약(142-2) · 층 = 지금 M01
pills = kept if isinstance(kept, list) else None
by_logic = {
code: pill
for pill in pills or combo_store.combo_pills({"담은로직": lines})["물량"]
for code in pill["로직"]
}
inner, rows, why = [], [], None
for one in lines:
ref = one.get("참조")
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["고정"])
else:
continue
qty = labor._number((one.get("수량") or {}).get("손"))
qty = Decimal(1) if qty is None else qty
keep = ("이름", "단위", "기준", "까닭")
inner.append({**head, "수량": qty, **{k: sub.get(k) for k in keep}})
if sub.get("기준") is None:
why = why or f"「{sub.get('이름')}」 {sub.get('까닭')}"
continue
rows.append({k: v * qty for k, v in sub["기준"].items()})
for mat in doc.get("자재") or []: # 사용자 자재(175-2) = 기준단가 × 손 · 빈칸 = 안 셈
qty = labor._number((mat.get("수량") or {}).get("손"))
if qty is None or labor.apart(mat): # 별도계상 = 안 셈 · 안 줄에도 없음(222-1)
continue
got = labor.material_price(self.whole, mat["코드"], mat.get("단위"))
price = labor._number(got["기준단가"])
zero = dict.fromkeys(labor.COSTS, Decimal(0))
base = None if price is None else {**zero, "재료비": price, "계": price}
name = mat.get("이름") or mat["코드"]
inner.append(
{
"자재": mat["코드"],
"수량": qty,
"이름": name,
"단위": mat.get("단위"),
"기준": base,
"까닭": got["까닭"],
}
)
if base is None:
why = why or f"「{name}」 {got['까닭']}"
continue
rows.append({k: v * qty for k, v in base.items()})
under = labor._sum(rows)
for one in inner: # 비율 고정 줄(705-3) — 밑 = 다른 줄 합
if (one.get("고정") or {}).get("비율"):
one["기준"], row = fixed.ratio_row(one["고정"]["비율"], one["수량"], under)
rows.append(row)
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