Files
eomsangdonandClaude Opus 5.5 553c9169cd feat(785-3): 1차 줄 판정 · 엑셀 사전 · 후보 · 수량 환산 + 유가전력 MO 이음
- 새 M02_Hopyo_Row — 단가표 시트(자재단가 · 노임단가 · 기계경비총괄표 · 기계경비적용기준 · 단가산출 · 일위대가 총괄표 · STmate 목록표 코드) → 엑셀 사전(갈래 + M01 키 후보 한 번) · 세부행 4 단계(비고 번호 → 별도계상 → 단위 → 사전) · 후보 차례 장부 → M01 표마다(LB · MT · MO · EQ · FX) → M02 층 SK · SC · 판정 넷(제안 있음 · 2차 필요 · 후보 여럿 · 후보 없음) · 고른 것 꼴 · 수량 = 엑셀 × 몫 × 배
- factor 를 Row 로 옮김(Match.factor 는 부름만)
- Elem 유가 후보(oil_rows · oils · lost 유가) · Ledger 짝 고른 것 꼴 넓힘(갈래 · 키 머리 · M01 있음 · 율 비목 · 기준 재검사 · 옛 꼴 그대로)
- 고정 줄 이음 유가 MO(LINKS · GROUPS 재료 · 재료비 = MO 값) · 일위대가 자재 목록 MO 허용(Labor_Doc 두 줄 · 줄 수 그대로)
- 화면 찾아 넣기 「M01 전체」 탭 유가 줄(oil) · 일위대가 = 고정 줄 이음 유가 · 단가산출 = 더한줄 재료 요소 MO · TS GROUPS 짝
- 시험 새 test_642_3_row(북부청 415 줄 = 단산 268 · 일위 1 · 별도계상 64 · 재료 78 · 기계 3 · 율 1 · 사방 201 · 거창 68 모두 갈래 하나 · 사전 · 장부 짝 · 환산 · MO 고정 셈 · 율 TS 거울)
- 낡은 시험 고침: test_721 의 FindKind 글 대조에 oil 더함(종류 하나 늘어난 까닭)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:27:48 +09:00

229 lines
11 KiB
Python
Raw Permalink 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) · 유가전력(MO · 785-3 · 재료비 = MO 값 · 묶음 재료) 키 ·
못 이음 = 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", "유가": "MO"}
SOURCE_KEYS = ("파일", "호표", "줄", "판")
# 율 기준 묶음(778-4) ← 고정 줄 이음
GROUPS = {"노임": "인력", "자재": "재료", "기계": "장비", "유가": "재료"}
BASES = tuple(dict.fromkeys((*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 에 없음")
if code or k != "유가": # 유가 칸은 이은 줄만(옛 문서 꼴 그대로)
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, "노무비"]
for code in (link.get("자재"), link.get("유가")): # 유가 MO = M01 값(785-3)
try:
got = _num(mm.element(whole, code, {}, fx.get("단위") or None)[0]) if code else None
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))