- 밑 공용 함수 rate_base ↔ rateBase · 검사 · 짝잃음 · B09 rows:n@labor(파이썬 · TS) · 굳힘 · 화면 열쇠 옮김이 비목 꼬리 그대로 - 여덟 파일 율 줄 152 중 기준 찾은 147 재현(인력 82 · 재료 35 · 절 소계 16 · 노무비 11 · 장비 2 · 자재+율 1) - 시험 test_778_4_rate_line 29 · 전후 대조 148/148 같음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
226 lines
11 KiB
Python
226 lines
11 KiB
Python
"""M02 일위대가 고정 줄(PLAN 705-1 · 요구서 1장 · 4장 1 · 2 · 3) — 품 · 수량 · 단가 숫자를 그대로 적어 둔 줄.
|
||
|
||
줄 = `{고정: {이름, 규격, 단위, 단가: {노무비, 재료비, 경비}, 이음: {노임, 자재, 기계}, 출처?},
|
||
별칭, 메모, 수량}` — 담은로직 안 로직 · 참조 줄과 섞임(줄 단위 꼬리표 · 기본 = 품셈).
|
||
단가 = 그 줄 단위 1 개 값 · 빈 비목 = 0 · 합계만 있는 줄 = 고른 비목 하나(기본 재료비 · 붙이는 쪽 몫).
|
||
이음 = 이 공사 노임(LB) · 자재(MT) · 기계(EQ) 키 · 못 이음 = null(「손 단가」) — 이은 비목만 단가 책 값
|
||
(노임 새 판 때 따라감) · 품(수량)은 그대로.
|
||
M02 셈(층 · 사본) = 노임 「값」 · 자재 기준단가로 이음 · 기계 = 줄 단가(기계 시간 단가 = B09 단가 책 몫).
|
||
출처 = `{파일, 호표, 줄, 판}`(글 · 수만) · 줄 가름 = 「고정|이름|규격|단위」(짝 `Labor_Calc.ts` `lineId`).
|
||
비율 줄(705-3 · 엑셀 % 줄) = `비율 {기준, 비목}` · 수량 = 율(%) — 반영[비목] = 밑 × 율 ÷ 100
|
||
(`ratios` · 짝 `Labor_Calc.ts` `ratioLines` · B09 = `pct` 줄) · 단가산출(SK) 율 더한줄(778-4)도 이 꼴.
|
||
기준(778-4 · 밑 = `rate_base` · 짝 `Labor_Calc.ts` `rateBase`) = 비목(노무비 · 재료비 · 경비 = 율 줄 뺀
|
||
그 비목 합) · 묶음(인력 · 재료 · 장비 = 그 묶음 줄 노 · 재 · 경 합 · 고정 줄 묶음 = 이음) ·
|
||
줄 목록 [열쇠](그 줄 합 · 앞 율 줄 넣으면 율 위 율 · SC 열쇠 = 줄 가름 · SK = cid).
|
||
B09 대가 갈래 `fixed` = `B09_Estimation_Labor_Source.fixed_line` · 셈 `B09_Estimation_Calc_Labor`.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from decimal import Decimal, InvalidOperation
|
||
from typing import Any
|
||
|
||
from M01_MasterData import M01_MasterData_Store as m01
|
||
|
||
mf = m01.cm.mf
|
||
import master_cut as mcut # noqa: E402 — 끝수(751-3) · scripts 길은 `m01` 이 둠
|
||
|
||
mm = mf.mp.mm
|
||
COSTS = mf.COST_ITEMS # 노무비 · 재료비 · 경비
|
||
FIXED = "고정"
|
||
LINKS = {"노임": "LB", "자재": "MT", "기계": "EQ"}
|
||
SOURCE_KEYS = ("파일", "호표", "줄", "판")
|
||
GROUPS = {"노임": "인력", "자재": "재료", "기계": "장비"} # 율 기준 묶음(778-4) ← 고정 줄 이음
|
||
BASES = (*COSTS, *GROUPS.values())
|
||
|
||
|
||
def _bad(text: str) -> Exception:
|
||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||
|
||
return store.StoreError(400, text)
|
||
|
||
|
||
def _num(raw: Any) -> Decimal | None:
|
||
if raw is None or isinstance(raw, bool):
|
||
return None
|
||
try:
|
||
got = Decimal(str(raw).replace(",", "").strip())
|
||
except InvalidOperation:
|
||
return None
|
||
return got if got.is_finite() else None
|
||
|
||
|
||
def _plain(value: Decimal) -> int | float:
|
||
return int(value) if value == value.to_integral_value() else float(value)
|
||
|
||
|
||
def is_fixed(one: Any) -> bool:
|
||
return isinstance(one, dict) and isinstance(one.get(FIXED), dict)
|
||
|
||
|
||
def fixed_id(fx: dict) -> str:
|
||
return f"고정|{fx.get('이름') or ''}|{fx.get('규격') or ''}|{fx.get('단위') or ''}"
|
||
|
||
|
||
def clean(fx: Any, where: str, whole: Any) -> dict[str, Any]:
|
||
"""고정 칸 정리 — 이름 없음 · 단가가 수 아님 · 이음 키 머리 틀림 · M01 에 없는 이음 = 400."""
|
||
if not isinstance(fx, dict):
|
||
raise _bad(f"{where} · 고정 줄이 묶음 아님")
|
||
name = str(fx.get("이름") or "").strip()
|
||
if not name:
|
||
raise _bad(f"{where} · 고정 줄 이름 없음")
|
||
raw = fx.get("단가") or {}
|
||
if not isinstance(raw, dict):
|
||
raise _bad(f"{where} · 고정 줄 단가는 {{노무비, 재료비, 경비}}")
|
||
price = {}
|
||
for k in COSTS:
|
||
got = _num(raw.get(k))
|
||
if got is None and raw.get(k) not in (None, ""):
|
||
raise _bad(f"{where} · 고정 줄 {k} 단가가 수 아님")
|
||
price[k] = _plain(got) if got is not None else 0
|
||
given = fx.get("이음") or {}
|
||
link = {}
|
||
for k, head in LINKS.items():
|
||
code = str(given.get(k) or "") if isinstance(given, dict) else ""
|
||
if code and (code[:2] != head or not _has(whole, code)):
|
||
raise _bad(f"{where} · 고정 줄 {k} 이음 「{code}」 M01 에 없음")
|
||
link[k] = code or None
|
||
rate = fx.get("비율")
|
||
if rate is not None and not (
|
||
isinstance(rate, dict) and _basis(rate.get("기준")) and rate.get("비목") in COSTS
|
||
):
|
||
raise _bad(f"{where} · 고정 줄 비율은 {{기준, 비목}} — 기준 = 비목 · 묶음 · 줄 목록")
|
||
src = fx.get("출처") if isinstance(fx.get("출처"), dict) else {}
|
||
kept = {k: v for k in SOURCE_KEYS if isinstance(v := src.get(k), (str, int, float))}
|
||
out = {"이름": name, "규격": str(fx.get("규격") or ""), "단위": str(fx.get("단위") or "")}
|
||
out = {**out, "단가": price, "이음": link, **({"출처": kept} if kept else {})}
|
||
return {**out, "비율": {k: rate[k] for k in ("기준", "비목")}} if rate else out
|
||
|
||
|
||
def _basis(raw: Any) -> bool:
|
||
"""율 기준 — 비목 · 묶음 이름 · 줄 열쇠 목록(글 · 빈 목록 = 밑 0 · 기준 줄을 지운 뒤)."""
|
||
if isinstance(raw, list):
|
||
return all(isinstance(x, str) and x for x in raw)
|
||
return raw in BASES
|
||
|
||
|
||
def group_of(fx: dict) -> str | None:
|
||
"""고정 줄 묶음 — 첫 이음(노임 · 자재 · 기계) · 이음 없음 = None."""
|
||
link = fx.get("이음") or {}
|
||
return next((g for k, g in GROUPS.items() if link.get(k)), None)
|
||
|
||
|
||
def _all(money: dict | None) -> Decimal:
|
||
return sum((Decimal(str((money or {}).get(k) or 0)) for k in COSTS), Decimal(0))
|
||
|
||
|
||
def rate_base(basis: Any, rows: list[dict]) -> Decimal:
|
||
"""율 줄 밑(778-4 · 짝 `Labor_Calc.ts` `rateBase`) — `rows` [{키, 묶음, 비목 {노 · 재 · 경}, 율?}] ·
|
||
비목 = 율 줄 뺀 그 비목 합 · 묶음 = 율 줄 뺀 그 묶음 줄 셋 합 · 목록 = 그 열쇠 줄 셋 합 ·
|
||
목록 열쇠 `열쇠@비목` = 그 줄 그 비목 몫(절 노무비 등)."""
|
||
if isinstance(basis, list):
|
||
out = Decimal(0)
|
||
for one in dict.fromkeys(basis):
|
||
key, cost = split_key(one)
|
||
for r in rows:
|
||
if r.get("키") == key:
|
||
out += (
|
||
Decimal(str((r["비목"] or {}).get(cost) or 0)) if cost else _all(r["비목"])
|
||
)
|
||
return out
|
||
rows = [r for r in rows if not r.get("율")]
|
||
if basis in COSTS:
|
||
return sum((Decimal(str((r["비목"] or {}).get(basis) or 0)) for r in rows), Decimal(0))
|
||
return sum((_all(r["비목"]) for r in rows if r.get("묶음") == basis), Decimal(0))
|
||
|
||
|
||
def split_key(one: str) -> tuple[str, str]:
|
||
"""목록 열쇠 → (줄 열쇠, 비목 | "") — `add:#3@노무비` = 그 줄 노무비 몫."""
|
||
key, at, cost = one.rpartition("@")
|
||
return (key, cost) if at and cost in COSTS else (one, "")
|
||
|
||
|
||
def rate_row(one: dict, key: Any, money: dict | None, rate: bool = False) -> dict[str, Any]:
|
||
"""`rate_base` 줄 하나 — 고정 줄 묶음 = 이음 · 그 밖 = 줄 `묶음`(SK 엔진 줄 · 자재 = 재료)."""
|
||
fx = one.get(FIXED)
|
||
group = group_of(fx) if isinstance(fx, dict) else one.get("묶음")
|
||
return {"키": key, "묶음": group, "비목": money, "율": rate}
|
||
|
||
|
||
def _has(whole: Any, code: str) -> bool:
|
||
try:
|
||
return bool(whole.get(code))
|
||
except mf.FormulaError:
|
||
return False
|
||
|
||
|
||
def link_keys(doc: Any) -> list[str]:
|
||
"""문서 고정 줄이 잇는 키 — 사본 `단가.json` · B09 단가 책 수요(705-1)."""
|
||
out: list[str] = []
|
||
for one in (doc or {}).get("담은로직") or []:
|
||
if is_fixed(one):
|
||
out += [v for v in (one[FIXED].get("이음") or {}).values() if v and v not in out]
|
||
return out
|
||
|
||
|
||
def line(whole: Any, fx: dict) -> dict[str, Any]:
|
||
"""고정 줄 계산 답 — 기준 = 줄 단가 · 이은 노임 · 자재는 `whole` 값(없으면 줄 단가 그대로) ·
|
||
분모 = 줄 단위 · 배 1 · `이은` = 단가 책 값을 쓴 비목."""
|
||
base = {k: _num((fx.get("단가") or {}).get(k)) or Decimal(0) for k in COSTS}
|
||
if fx.get("비율"): # 기준 · 반영 = `ratios`(settle 끝)
|
||
return {FIXED: fx, "이름": fx.get("이름"), "알약": None, "분모": "%", "배": 100,
|
||
"기준": {**base, "계": Decimal(0)}, "이은": [], "까닭": None} # fmt: skip
|
||
link, used = fx.get("이음") or {}, []
|
||
if code := link.get("노임"):
|
||
try:
|
||
got = _num((whole.get(code) or {}).get("값"))
|
||
except mf.FormulaError:
|
||
got = None
|
||
if got is not None:
|
||
base["노무비"], used = got, [*used, "노무비"]
|
||
if code := link.get("자재"):
|
||
try:
|
||
got = _num(mm.element(whole, code, {}, fx.get("단위") or None)[0])
|
||
except mf.FormulaError:
|
||
got = None
|
||
if got is not None:
|
||
base["재료비"], used = got, [*used, "재료비"]
|
||
# ponytail: 기계 이음은 M02 셈에서 줄 단가 그대로 — 기계 시간 단가 셈은 B09 중기목록표 몫
|
||
money = {**base, "계": sum(base.values())}
|
||
head = {FIXED: fx, "이름": fx.get("이름"), "알약": None, "분모": fx.get("단위"), "배": 1}
|
||
return {**head, "기준": money, "이은": used, "까닭": None}
|
||
|
||
|
||
def ratio_row(rate: dict, qty: Decimal | None, under: Decimal) -> tuple[dict, dict | None]:
|
||
"""비율 줄 (기준, 반영) — 기준[비목] = 밑(`rate_base`) · 반영 = 기준 × 율 ÷ 100 ·
|
||
0.1 원 버림(751-3) · 수량 없음 = None."""
|
||
base = dict.fromkeys((*COSTS, "계"), Decimal(0))
|
||
base[rate["비목"]] = base["계"] = under
|
||
return base, None if qty is None else mcut.row({k: base[k] * qty / 100 for k in COSTS})
|
||
|
||
|
||
def ratios(lines: list[dict], reflects: list[dict | None], bases: list[dict | None]) -> None:
|
||
"""비율 줄(705-3) — `settle` 끝에 · 밑 = `rate_base`(율 줄 뺀 반영(줄 · 자재) · 앞 율 줄은 목록만) ·
|
||
배 100 · 기준합엔 안 셈 · `lines` · `reflects` · `bases` 앞 줄 = 같은 차례(짝 `Labor_Calc.ts`
|
||
`ratioLines`) · 자재 줄 = 묶음 재료 · 열쇠 없음."""
|
||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||
|
||
at = [i for i, x in enumerate(lines) if (x.get(FIXED) or {}).get("비율")]
|
||
rows = [
|
||
rate_row(lines[i], labor.line_id(lines[i]), r)
|
||
if i < len(lines)
|
||
else {"키": None, "묶음": "재료", "비목": r}
|
||
for i, r in enumerate(reflects)
|
||
if r and i not in at
|
||
]
|
||
for i in at:
|
||
rate = lines[i][FIXED]["비율"]
|
||
base, done = ratio_row(rate, lines[i].get("수량"), rate_base(rate["기준"], rows))
|
||
lines[i], reflects[i], bases[i] = (
|
||
{**lines[i], "기준": base, "배": 100, "반영": done},
|
||
done,
|
||
None,
|
||
)
|
||
if done:
|
||
rows.append(rate_row(lines[i], labor.line_id(lines[i]), done, True))
|