Files
Aislo/M02_MasterTemplete/M02_Hopyo_Units.py
T
eomsangdonandClaude Opus 5.5 215cc1531d feat(778-5): 줄 잇기 한 벌 — 단위 줄 → 우리 계산 단위 + 상태(이어짐 · 고를 것 · 못 이음) · 일위대가 자재 맞춤 같은 함수(D-2 · 요구 622 · 623 · 624)
- 새 M02_Hopyo_Units: 노무 LB(같은 이름 여럿 = 건설업 먼저 · 작업반장) · 기계 EQ(Fixed.Linker) → 기계경비 기초 로직 줄(778-2) · 없는 규격 = 가까운 후보만 · 자재 MT(별칭 장부 › 이름 · 규격 › 바탕 이름 · 숫자 규격 · 이름에 붙은 규격 · 글자 규격 = 고를 것) · 손료율 방법 · 율 778-4 꼴(기준꼴 → 묶음 · 비목 · 줄 목록) · 하위 · 값
- Material: 숫자 규격 치수 차례 · 구분자 무관 견줌(dims) · 글자 규격 판별(lettered) · 단위 없는 줄(EST 블록) = 단위 안 봄
- Line._material = Units.material — 일위대가 자재 체크 제안 147 → 146(봉화 광명단조합페인트 「1종2류(철재)」 = 글자 규격 → 후보 · 요구 624-2)
- 여덟 파일 불리는 블록 줄 842: 노무 366/366 · 기계 180/190 · 자재 71/130 · 율 150/152 · 하위 3/3 · 값 1/1 · 줄 다 이어진 블록 156/206

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 13:43:51 +09:00

267 lines
14 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 778-5 · D-2 · 요구 622 · 623 · 624) — 단위 줄
(`M02_Hopyo_UnitLine`) → 우리 계산 단위 + 상태. 형식(EST · STmate) · 대상(단가산출 블록 · 일위대가 줄)을
모름 — 일위대가 자재 줄(`M02_Hopyo_Line._material`)도 `material` 을 씀.
잇기 = `{상태 이어짐|고를 것|못 이음, 방법, 단위, 후보, 꼬리표, 글}` · 자동 없음(제안만 · 사용자가 확인).
L 노무 = 직종 LB(이름 · 옛이름) · 같은 이름 여럿 = 건설업 먼저(작업반장 · 요구 624-1) · 단위 `{노임}`
X 기계 = EQ(`Fixed.Linker._machine` · 778-1 괄호 낱말) → 기계경비 기초 로직(`SkPatch._mach_logics` ·
778-2) · 단위 `{기계, 로직, 할증?}` · 없는 규격 = 가까운 규격 후보만(`Elem.machines` · 자동 고름 없음)
M 자재 = MT(`material` — 별칭 장부 › 이름 · 규격 같음 › 바탕 이름 · 숫자 규격 같음(치수 차례 · 구분자
무관) · 글자 든 규격 = 후보로만) · 손료율 = 방법 「수량 × 손료율 × 단가」 · 별도 = 단위 `{자재?,
별도계상}` · 단위 `{자재}`
R 율 = 778-4 꼴 `{기준, 비목}` — 기준 = `기준꼴`(인력 · 재료 묶음 · 노무비 · 줄 목록 `{행: [...]}`) ·
우리에 없는 율 항목(설치비 · 제경비)도 율 줄 · 기준 못 찾음 = 고를 것(글 기준 제안)
S 하위 = `{하위 {종류 단산|일위, 번}}`(SK · SC 참조) · E 값 = `{값: 금액}`(고정 금액 줄)
별칭 장부 짝(자재) = 숫자보다 먼저(`Line.match_line` 규칙). 서버 단독 · 읽기만.
"""
from __future__ import annotations
import re
from typing import Any
from M02_MasterTemplete import M02_Hopyo_Elem as elem
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_Match as hm
from M02_MasterTemplete import M02_Hopyo_Material as hmat
from M02_MasterTemplete import M02_Hopyo_Shares as shares
from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch
from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line
from M02_MasterTemplete.M02_Hopyo_Words import mtypes
LINKED, PICK, NONE = "이어짐", "고를 것", "못 이음"
HOW = {
"L": "노무 = 직종 × 품 × 노임",
"X": "기계 = 시간 × 시간당 기계경비",
"M": "자재 = 수량 × 단가",
"R": "율 = 기준 × 율",
"S": "하위 = 하위 값 × 수량",
"E": "값 = 고정 금액",
}
WEAR = "자재 = 수량 × 손료율 × 단가"
APART = "자재 = 별도계상(0 원)"
TRADE_FIRST = "건설업" # 같은 이름 노임 여럿 = 건설업 먼저(임도 = 토목 · 요구 624-1)
LETTERS = "글자 규격 — 확인"
# 율 기준꼴(`UnitLine.base_rows`) → 778-4 기준(묶음 · 비목) · 그 밖 = 줄 목록
BASES = {"인력": "인력", "인력(블록)": "인력", "자재": "재료", "자재(블록)": "재료"}
BASES |= {k: k for k in ("노무비", "재료비", "경비")} # 일위대가 % 줄 기준(규격 글)
_PART_WORDS = (("공구손료", "경비"), ("재료", "재료비"), ("자재", "재료비"), ("소모", "재료비"))
def _cands(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [{**hm._public(r), "점수": r.get("점수"), "판정": "후보"} for r in rows]
def _split(me: dict[str, Any]) -> list[dict[str, Any]]:
"""이름에 붙은 규격 · 괄호 이름 — 「이름 규격」 · 「이름(규격)」(끝 괄호에 수) · 「유로폼(패널)」 =
유로폼패널 · 「패널(유로폼)」 = 유로폼패널 · 끝 「[㎥]」 뗌."""
name = re.sub(r"\[[^\]]*\]\s*$", "", str(me.get("이름") or "")).strip()
spec = str(me.get("규격") or "")
pairs = []
if not spec:
head, _, rest = name.partition(" ")
pairs += [(head, rest)] if rest else []
if hit := re.match(r"^(.+?)\s*[((]([^()()]*\d[^()()]*)[))]\s*$", name):
pairs.append((hit.group(1), hit.group(2)))
out = []
for n, sp in [*pairs, (name, spec)]:
if hit := re.fullmatch(r"([^()]+)\(([^()\d]+)\)", n.strip()):
a, b = hit.group(1).strip(), hit.group(2).strip()
out += [{**me, "이름": a + b, "규격": sp}, {**me, "이름": b + a, "규격": sp}]
elif (n, sp) != (me.get("이름"), spec):
out.append({**me, "이름": n, "규격": sp})
return out
def material(line: dict[str, Any], got: dict[str, Any], full: bool = True) -> dict[str, Any]:
"""자재 줄(별도계상 글 뗌) → `{상태, 찾음 [M01 자재], 글, 꼬리표, 후보}` — 이름 · 규격 같음 하나 ›
바탕 이름 · 숫자 규격 같음(이름에 붙은 규격 「이름 규격」 · 「이름(규격)」도) · 글자 규격 = 고를 것."""
me, apart = hmat.clean(line)
tags = [hmat.APART] if apart else []
found = hm._exact(me, "자재", got)
if len(found) == 1:
return {"상태": LINKED, "찾음": found, "글": "이름 · 규격 같음", "꼬리표": tags, "후보": []}
tries = [me, *_split(me)]
for one in tries:
sure, more = hmat.sure(one)
if not sure:
continue
rows = [r["one"] for r in sure]
if hmat.lettered(one.get("규격")):
cands = [{**hm._public(r), "점수": None, "판정": "후보", "글": LETTERS} for r in rows]
return {"상태": PICK, "찾음": [], "글": LETTERS, "꼬리표": tags + more, "후보": cands}
return {"상태": LINKED, "찾음": rows, "글": "바탕 이름 · 규격 같음", "꼬리표": tags + more,
"후보": []} # fmt: skip
cands = hmat.candidates(me) if full else []
state = PICK if cands or not full else NONE
return {
"상태": state,
"찾음": [],
"글": "후보만" if cands else "없음",
"꼬리표": tags,
"후보": cands,
}
def rate_part(name: Any) -> str:
"""율 금액이 드는 비목 — 공구손료 = 경비 · 재료 · 자재 · 소모 = 재료비 · 그 밖 경비."""
flat = re.sub(r"\s", "", str(name or ""))
return next((part for word, part in _PART_WORDS if word in flat), "경비")
def _text_base(one: dict[str, Any]) -> str:
"""율 줄 기준 글(「노무비의」 · 「주재료의」 · STmate 표지) → 778-4 기준 제안."""
text = re.sub(r"\s", "", f"{one.get('기준') or ''} {one.get('기준글') or ''} {one.get('이름')}")
if "주재료" in text or "주자재" in text or "M:" in text:
return "재료"
if "재료비" in text:
return "재료비"
if "경비" in text and "노무" not in text:
return "경비"
return "노무비"
class Units:
"""한 번 잇기 — M01 · 목록 · 장부를 한 번 읽음(`books` = `Ledger.books` · 없으면 장부 안 봄)."""
def __init__(self, books: list | None = None, doc: str | None = None) -> None:
self.linker = hfix.Linker(by_name=True)
self.whole, self.got = self.linker.whole, self.linker.got
self.books, self.doc = books, doc
self.eqs = {str(r.get("키")): r for r in self.whole.numbers.get("EQ", {}).values()}
self.lb: dict[str, list[dict]] = {}
for row in self.whole.numbers.get("LB", {}).values():
self.lb.setdefault(str(row.get("이름") or ""), []).append(row)
def line(self, one: dict[str, Any]) -> dict[str, Any]:
kind = one.get("묶음")
how = HOW.get(str(kind), HOW["E"])
got = getattr(self, f"_{kind}", self._E)(one)
return {"방법": how, "후보": [], "꼬리표": [], "글": "", **got}
def _L(self, one: dict[str, Any]) -> dict[str, Any]:
name = shares.trade(one.get("직종") or one.get("이름")) or ""
rows = self.lb.get(name, [])
cands = [{"키": str(r.get("키")), "갈래": "노임", "이름": r.get("이름"),
"구분": r.get("구분"), "단가": elem._num(r.get("값"))} for r in rows] # fmt: skip
if len(rows) == 1:
return {"상태": LINKED, "단위": {"노임": cands[0]["키"]}}
first = [c for c in cands if c["구분"] == TRADE_FIRST]
if len(first) == 1:
rest = [c for c in cands if c is not first[0]]
tag = f"기본 — {TRADE_FIRST} {name}"
return {"상태": LINKED, "단위": {"노임": first[0]["키"]}, "후보": rest, "꼬리표": [tag]}
named = elem.labor(str(one.get("이름") or ""))
return {"상태": PICK if named or cands else NONE, "단위": None, "후보": cands or named}
def _X(self, one: dict[str, Any]) -> dict[str, Any]:
line = {"이름": one.get("이름"), "규격": one.get("규격") or ""}
code = self.linker._machine(line)
if code is None:
cands = elem.machines(f"{line['이름']} {line['규격']}")
return {"상태": PICK if cands else NONE, "단위": None, "후보": cands,
"글": "가까운 규격 — 고름" if cands else "같은 기계 없음"} # fmt: skip
eq = self.eqs.get(code) or {}
kinds = mtypes(f"{eq.get('상세구분')} {eq.get('이름')}")
unit = {"기계": code, "로직": skpatch._mach_logics(self.whole, kinds, eq)[0]}
unit |= {"할증": one["할증"]} if one.get("할증") else {}
return {
"상태": LINKED,
"단위": unit,
"글": f"{eq.get('이름')} {eq.get('규격') or ''}".strip(),
}
def _M(self, one: dict[str, Any]) -> dict[str, Any]:
how = WEAR if one.get("손료율") else HOW["M"]
apart = one.get("어떻게") == "별도" or one.get("별도")
line = {
"이름": one.get("이름"),
"규격": one.get("규격") or "",
"단위": one.get("단위") or "",
}
line |= {"갈래": hmat.APART} if apart else {}
pair = self._alias(line)
if pair:
unit = {"자재": pair} | ({"별도계상": True} if apart else {})
return {"상태": LINKED, "방법": how, "단위": unit, "꼬리표": ["별칭 장부 짝"]}
got = material(line, self.got)
apart = apart or hmat.APART in got["꼬리표"]
found = [r for r in got["찾음"] if r.get("자재")]
if got["상태"] == LINKED and found:
unit = {"자재": found[0]["자재"]} | ({"별도계상": True} if apart else {})
return {
"상태": LINKED,
"방법": how,
"단위": unit,
"글": got["글"],
"꼬리표": got["꼬리표"],
}
if apart: # 별도계상 = 0 원 줄(우리 자재 못 찾아도 셈 같음)
return {"상태": LINKED, "방법": APART, "단위": {"별도계상": True}, "후보": got["후보"]}
state = got["상태"] if got["상태"] != LINKED else PICK # 「전국」 자리표 = 지역 고름
return {"상태": state, "방법": how, "단위": None, "후보": got["후보"] or _cands(got["찾음"]),
"글": got["글"], "꼬리표": got["꼬리표"]} # fmt: skip
def _alias(self, line: dict[str, Any]) -> str | None:
"""별칭 장부 자재 짝(자동 정확 열쇠 · 하나) — 숫자보다 먼저."""
if self.books is None:
return None
got = ledger.recall(line, "", self.books, doc=self.doc)
if not got or got.get("제안") or got.get("규격다름") or got.get("갈래") != "짝":
return None
pairs = [p.get("자재") for p in got.get("짝") or []]
return pairs[0] if len(pairs) == 1 and pairs[0] else None
def _R(self, one: dict[str, Any]) -> dict[str, Any]:
part = rate_part(one.get("이름"))
shape = one.get("기준꼴")
tags = ["이름 없음"] if one.get("이름없음") else []
if shape in BASES:
return {"상태": LINKED, "단위": {"기준": BASES[shape], "비목": part}, "꼬리표": tags}
if shape and one.get("기준줄"):
unit = {"기준": {"행": list(one["기준줄"])}, "비목": part}
return {"상태": LINKED, "단위": unit, "글": shape, "꼬리표": tags}
guess = {"기준": _text_base(one), "비목": part}
return {"상태": PICK, "단위": None, "후보": [guess], "글": "기준 못 찾음 — 기준 고름",
"꼬리표": tags} # fmt: skip
def _S(self, one: dict[str, Any]) -> dict[str, Any]:
sub = one.get("하위") or {}
if not sub.get("번"):
return {"상태": NONE, "단위": None, "글": "하위 번 없음"}
return {"상태": LINKED, "단위": {"하위": {"종류": sub.get("종류"), "번": int(sub["번"])}}}
def _E(self, one: dict[str, Any]) -> dict[str, Any]:
got = one.get("금액")
if got is None:
got = unit_line.money(one.get("단가"))
if got is None:
return {"상태": NONE, "단위": None, "글": "금액 없음"}
return {"상태": LINKED, "방법": HOW["E"], "단위": {"값": float(got)}}
def hopyo_unit(line: dict[str, Any]) -> dict[str, Any]:
"""일위대가 줄(`UnitLine.check_hopyo` 투영 뒤) → 단위 줄 꼴 — 이름 · 규격 · 단위 · 품 = 수량 · 별도."""
one = {k: line.get(k) for k in ("묶음", "이름", "규격", "단위", "할증")}
one.update(품=line.get("수량"), 금액=unit_line.money(line.get("금액")), 직종=None)
if line.get("갈래") == "별도계상":
one["어떻게"] = "별도"
if one["묶음"] == "R":
base = str(line.get("기준") or "노무비")
one.update(기준=base, 기준꼴={"주재료비": "자재"}.get(base, base))
one["품"] = float(line["수량"]) / 100 if line.get("수량") is not None else None
ref = line.get("참조") or {}
if one["묶음"] == "S" and ref.get("번"):
one["하위"] = {"종류": ref.get("종류"), "번": ref.get("번")}
return one
def summary(rows: list[dict[str, Any]]) -> dict[str, dict[str, int]]:
"""묶음별 `{묶음: {이어짐, 고를 것, 못 이음}}` — 줄 연결 표."""
out: dict[str, dict[str, int]] = {}
for kind, state in rows:
one = out.setdefault(kind, dict.fromkeys((LINKED, PICK, NONE), 0))
one[state] += 1
return out