- 끝수 한 곳 `master_cut.py`(B09 `rounddown` 재사용) — 엔진 답에 입힘: 줄 비목마다 0.1 원 버림 · 하위 로직 줄 = 하위 끝수 계 × 수량 · 덧줄 = 버린 줄 합으로 다시 셈 · 비목 합 1 원 버림 · 중간 칸도 버린 값 - 쓰는 곳: M01 시험 계산(`check_master.calc`) · 읽는 식(`master_text.lines`) · 옛 조합 미리보기(`master_combo`) · M02 `run_line`(단가산출 · 일위대가 · 펼침) · 별도계상 뺀 기준 · 켜쌓기율 곱 뒤 · 일위대가 반영 · 반영합 · 비율 줄 · 사전조합 안 줄 - 일위대가 반영 TS 짝(`Labor_Calc.ts`) 같은 끝수 · 거울 헬퍼가 상대 경로 .ts 가져옴 - B09 = `run_line(raw=True)` 엔진 날 값(제 단가로 다시 버림 · % 덧줄 율 비 그대로) — 검증_sub3 B09 247 칸 전후 같음 - `master_text` 꼴 셋 = `common_util_money` 로 갈음(줄 금액 `fmt_line_money` · 식 안 금액 = 버린 0.1 원) - 화면: 상세 줄 금액 · 시험 계산 줄 · 중간 돈 = `formatLineMoney` · 일위대가 반영 칸 0.1 원 · 상세 바닥 · 박스 · 인력재료경비 비목 소계 = 줄 합 1 원 버림 - 테스트1 네 보기 · 서고 목록 = 124,419(옛 124,420 / 124,419.85) · 재료 줄 11,694.6 + 23,425.6 + 1,404.8 = 소계 36,525 - 새 지킴 시험 test_751_3_money_cut - 옛 날 금액(끝수 없음)을 대조하던 시험 24 파일 고침 — 까닭 = 금액 규칙이 줄 0.1 원 · 소계 1 원 버림으로 바뀜(고정 값 → 새 값 · 줄 합 = 계 → 줄 합 아래 비목마다 1 원 안 · 곱 비 = 끝수 폭 안) · 실무 xlsx 영월 엑셀 같음 22 → 23 · 이어 붙이는 TS 시험(704_3 · 717_2b)에 `rounddown` 같은 식 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
253 lines
12 KiB
Python
253 lines
12 KiB
Python
"""M02 서고 참조 펼침(계산) — `M02_Labor_Ref.Expand` 를 떼어 둠(PLAN 715-2 · 700 줄).
|
|
|
|
참조 → `{이름, 단위, 기준(1 단위), 까닭, 안?}` — 단가승격(옛 SP) · 사전조합(SC) · 단가산출(SK ·
|
|
`M02_Sangeun_Calc.unit`). 이름은 `M02_Labor_Ref.Expand` 로 그대로 부름.
|
|
사전조합 안 줄 수량(742-1) 차례 — 층 문서 = 알약 값(쓰는 쪽 `given` 「참조|층|주인|키#차례」 · 자재 =
|
|
「…#자재|코드」 · `inner_pills`) → 저장 손 · 굳힌 사본(`hands` · 프로젝트) = 굳힌 손(옛 사본 · 707-7) →
|
|
알약 값 → 저장(`refs.SAVED`) · 비율 줄 = 율 · 모두 없으면 「수량 없음」(1 아님 · 그 사전조합 기준 없음).
|
|
"""
|
|
|
|
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]
|
|
NO_QTY = "수량 없음"
|
|
MAT = "자재|"
|
|
|
|
|
|
class Expand:
|
|
"""참조 → `{이름, 단위, 기준(1 단위), 까닭, 안?}` — 한 번 셈 안에서 키별 캐시 ·
|
|
깊이 상한 · 순환은 까닭(통째로 멈추지 않음). `read` = 참조 → 문서(프로젝트는 굳힌 사본)."""
|
|
|
|
def __init__(
|
|
self,
|
|
whole: Any,
|
|
read: Callable[[Ref], dict | None] = refs.read_ref,
|
|
given: dict[str, Any] | None = None,
|
|
hands: bool | None = None,
|
|
) -> None:
|
|
self.whole, self.read, self.memo = whole, read, {}
|
|
self.given = given or {}
|
|
self.hands = (
|
|
read is not refs.read_ref if hands is None else hands
|
|
) # 층 원본 손 = 옛 값(안 씀)
|
|
|
|
def _qty(self, one: dict, key: str) -> Decimal | None:
|
|
"""안 줄 수량 — 굳힌 손(사본) · 비율 줄 율 → 알약 값(given) → 저장 수량(층 손 · 사본 `저장`)."""
|
|
from M02_MasterTemplete import M02_Labor_Doc as labor
|
|
|
|
qty = one.get("수량") if isinstance(one.get("수량"), dict) else {}
|
|
hand = labor._number(qty.get("손"))
|
|
if hand is not None and (self.hands or refs.is_rate(one)):
|
|
return hand
|
|
got = labor._number(self.given.get(key))
|
|
if got is not None:
|
|
return got
|
|
return labor._number(qty.get(refs.SAVED)) if self.hands else hand
|
|
|
|
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, fz = pill or {}, one.get("굳힌")
|
|
if isinstance(fz, dict) and fz.get("키"): # 사본 굳힌 덮개 줄(740-1 · `Item_Sangeun`)
|
|
got = labor.run_line(
|
|
self.whole, fz["키"], fz.get("입력값"), fz.get("켜쌓기율"), fz.get("고름") or None
|
|
)
|
|
got = labor.drop_apart(got, fz.get("자재표시"))
|
|
elif 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, rid = [], [], None, trail[-1] if trail else ""
|
|
for at, one in enumerate(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 = self._qty(one, f"{rid}#{at}")
|
|
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
|
|
if qty is None: # 742-1 — 저장 · 이은 값 없음 = 금액 빠짐(1 로 안 셈)
|
|
inner[-1]["까닭"] = NO_QTY
|
|
why = why or f"「{sub.get('이름')}」 {NO_QTY}"
|
|
continue
|
|
rows.append(labor.mcut.row({k: v * qty for k, v in sub["기준"].items()}))
|
|
for mat in doc.get("자재") or []: # 사용자 자재(175-2) = 기준단가 × 수량(이은 값)
|
|
if labor.apart(mat): # 별도계상 = 안 셈 · 안 줄에도 없음(222-1)
|
|
continue
|
|
qty = self._qty(mat, f"{rid}#{MAT}{mat['코드']}")
|
|
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 qty is None or base is None:
|
|
inner[-1]["까닭"] = got["까닭"] or NO_QTY # 단가 까닭 먼저
|
|
why = why or f"「{name}」 {inner[-1]['까닭']}"
|
|
continue
|
|
rows.append(labor.mcut.row({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.mcut.sums(rows), # 줄 0.1 원 · 합 1 원 버림(751-3)
|
|
"까닭": 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
|
|
|
|
|
|
def inner_pills(
|
|
ref: Ref, read: Callable[[Ref], dict | None] = refs.read_ref, depth: int = 1
|
|
) -> list[dict[str, Any]]:
|
|
"""사전조합 안 줄 알약(723-1) — 키 「참조|…#차례」 · 「참조|…#자재|코드」 · 이름 = 「일위대가 · 줄」 ·
|
|
단위 = 그 줄 분모 · 비율 줄 뺌 · 안의 사전조합도 한 번씩(깊이 상한) · 사전조합 아니면 빈 것."""
|
|
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
|
|
|
doc = read(ref) if refs.kind_of(ref["키"]) == "precombo" and depth <= refs.DEPTH else None
|
|
if not isinstance(doc, dict):
|
|
return []
|
|
rid, top = refs.ref_id(ref), doc.get("이름") or ref["키"]
|
|
lines = [one for one in doc.get("담은로직") or [] if isinstance(one, dict)]
|
|
by_logic = {
|
|
code: pill
|
|
for pill in combo_store.combo_pills({"담은로직": lines})["물량"]
|
|
for code in pill["로직"]
|
|
}
|
|
out: list[dict[str, Any]] = []
|
|
for at, one in enumerate(lines):
|
|
if refs.is_rate(one):
|
|
continue
|
|
sub = one.get("참조")
|
|
if isinstance(sub, dict):
|
|
got = read(sub) or {}
|
|
name, unit = got.get("이름") or sub["키"], got.get("단위")
|
|
out += inner_pills(sub, read, depth + 1)
|
|
elif fixed.is_fixed(one):
|
|
name, unit = one["고정"].get("이름"), one["고정"].get("단위")
|
|
else:
|
|
pill = by_logic.get(one.get("로직")) or {}
|
|
name, unit = pill.get("이름") or one.get("로직"), pill.get("단위")
|
|
name = one.get("별칭") or name
|
|
out.append({"키": f"{rid}#{at}", "이름": f"{top} · {name}", "단위": unit or "", "로직": [], "참조": ref}) # fmt: skip
|
|
for mat in doc.get("자재") or []:
|
|
name = mat.get("이름") or mat.get("코드")
|
|
key = f"{rid}#{MAT}{mat.get('코드')}"
|
|
out.append({"키": key, "이름": f"{top} · {name}", "단위": mat.get("단위") or "", "로직": [], "참조": ref}) # fmt: skip
|
|
seen: set[str] = set()
|
|
return [p for p in out if not (p["키"] in seen or seen.add(p["키"]))]
|