Files
Aislo/M02_MasterTemplete/M02_Labor_Fixed.py
T
eomsangdonandClaude Opus 5.5 fd9cce0b12 feat(778-4): 율 기준 줄 목록 열쇠@비목 — 그 줄 그 비목 몫(절 노무비 · 기계 노무 포함)
- 밑 공용 함수 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>
2026-11-04 13:15:13 +09:00

226 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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))