- K1 EST 원파일 자재 = 우리 자재 출처 칸 EST 코드로 바로 이음(새 M02_Elem_Alias · 자재 자원 24,219 중 코드 같음 23,981 모두 그 줄) - K2 M01 요소 줄 「별칭」 칸 읽기 — 기계 찾기(옛 별칭 장부 읽기 걷음) · 재료 · 인력 · 유가 · 일위대가 호표 줄(Units · Hopyo_Sangeun 장부 import 뗌) - K3 재료 표기 정규화 정확 일치(M/M · 곱셈표 · 지름 · 괄호 · 빈칸 · 전각 · 지역 꼬리) — 바탕 이름 길보다 먼저(치즐(0.7m3) → 0.2㎥ 줄로 잘못 잇던 것 바로잡음) - K5 견적 · 운임 · 수수료 줄 = 우리 자재면 재료 줄(우리 값) · 아니면 그대로 + 「견적」 표지 · 문서 설명 · 화면 「견적 n」 배지(못 이음 수에서 뺌) - 시험 test_815_1_import_link 새로 · test_812_9 = 옛 장부 별칭 대신 기계 줄 별칭 칸(마스터 다섯은 칸이 비면 시험 동안만 넣음) · 「디젤 엔진」 은 마스터 별칭 묶음으로 옮김 - test_778_5 기계 표(162 · 3 · 25)는 고치기 전 판과 같음 — 812-9 뒤부터 굳힌 수와 다름(이번 고침 무관 · 안 고침) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
302 lines
16 KiB
Python
302 lines
16 KiB
Python
"""M02 일위대가 실무자료 줄 잇기 한 벌(PLAN 778-5 · D-2 · 요구 622 · 623 · 624) — 단위 줄
|
||
(`M02_Hopyo_UnitLine`) → 우리 계산 단위 + 상태. 형식(EST · STmate) · 대상(단가산출 블록 · 일위대가 줄)을
|
||
모름 — 일위대가 줄 판(`M02_Hopyo_Row`)도 씀.
|
||
|
||
잇기 = `{상태 이어짐|고를 것|못 이음, 방법, 단위, 후보, 꼬리표, 글}` · 자동 없음(제안만 · 사용자가 확인).
|
||
L 노무 = 직종 LB(이름 · 옛이름) · 같은 이름 여럿 = 건설업 먼저(작업반장 · 요구 624-1) · 단위 `{노임}`
|
||
X 기계 = EQ(`Fixed.Linker._machine` · 778-1 괄호 낱말) → 기계경비 기초 로직(`mach_logics` ·
|
||
778-2) · 단위 `{기계, 로직, 할증?}` · 없는 규격 = 가까운 규격 후보만(`Elem.machines` · 자동 고름 없음)
|
||
M 자재 = MT(`material` — M01 별칭 칸 › 이름 · 규격 같음 › 바탕 이름 · 숫자 규격 같음(치수 차례 · 구분자
|
||
무관) · 글자 든 규격 = 후보로만) · 손료율 = 방법 「수량 × 손료율 × 단가」 · 별도 = 단위 `{자재?,
|
||
별도계상}` · 단위 `{자재}`
|
||
R 율 = 778-4 꼴 `{기준, 비목}` — 기준 = `기준꼴`(인력 · 재료 묶음 · 노무비 · 줄 목록 `{행: [...]}`) ·
|
||
우리에 없는 율 항목(설치비 · 제경비)도 율 줄 · 기준 못 찾음 = 고를 것(글 기준 제안)
|
||
S 하위 = `{하위 {종류 단산|일위, 번}}`(SK · SC 참조) · E 값 = `{값: 금액}`(고정 금액 줄)
|
||
M01 줄 「별칭」 칸(자재 · 노임 · 기계 = 기계 찾기 안) = 숫자보다 먼저(815-1). 서버 단독 · 읽기만.
|
||
"""
|
||
|
||
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_Elem_Alias as ea
|
||
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_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 = (("공구손료", "경비"), ("재료", "재료비"), ("자재", "재료비"), ("소모", "재료비"))
|
||
MACH_IN = "기계" # 기계경비 기초 기계 입력 칸
|
||
MACHINE = {"굴착기": "GC000267"} # 굴착기(무한궤도) 시간당 기계경비 · 그 밖 장비 = GC000268
|
||
MACHINE_ANY = "GC000268"
|
||
MACHINE_SET = {"GC000267", "GC000268", "GC000269", "GC000275"}
|
||
# (주연료, 조종원) 없는 기종 → 그 기계경비 기초(M01 로직이 부르는 것과 같음 · 778-2)
|
||
SOLO = {(False, False): "GC000273", (True, False): "GC000272", (False, True): "GC000276"}
|
||
|
||
|
||
def mach_logics(whole: Any, kinds: set, eq: dict | None = None) -> list[str]:
|
||
"""기계경비 기초 차례 — 갈래 첫 기초(굴착기 = 무한궤도) › 그 밖 · `eq` 가 있으면
|
||
연료 · 조종원 없는 기종 = 그 기초 하나 · 아니면 그 기계(또는 같은 이름 기계)를 고르기에 둔 기초가 앞."""
|
||
first = next((c for k, c in MACHINE.items() if k in kinds), MACHINE_ANY)
|
||
order = [first, *sorted(MACHINE_SET - {first})]
|
||
if not eq:
|
||
return order
|
||
has = eq.get("값") if isinstance(eq.get("값"), dict) else {}
|
||
if solo := SOLO.get((bool(has.get("주연료")), bool(has.get("조종원")))):
|
||
return [solo]
|
||
eqs = whole.numbers.get("EQ", {})
|
||
|
||
def takes(code: str) -> bool:
|
||
ins = whole.logic(code).get("입력") or []
|
||
opts = next((s.get("고르기") or [] for s in ins if s.get("이름") == MACH_IN), [])
|
||
same = any((eqs.get(o) or {}).get("이름") == eq.get("이름") for o in opts)
|
||
return eq.get("원문번호") in opts or same
|
||
|
||
hit = [c for c in order if takes(c)]
|
||
return hit + [c for c in order if c not in hit]
|
||
|
||
|
||
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, "후보": []}
|
||
hits = ea.exact(
|
||
me.get("이름"), me.get("규격")
|
||
) # 815-1 K3 표기 정규화 같음 한 줄(지역 변형 = 아래 길)
|
||
if len(hits) == 1:
|
||
key = hits[0]
|
||
one = ea.row(ea.MARKET, key)
|
||
found = [
|
||
{"자재": key, "이름": one.get("이름"), "규격": one.get("규격"), "단위": one.get("단위")}
|
||
]
|
||
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` · `doc` = 옛 장부 자리 · 815-1 뒤 안 씀)."""
|
||
|
||
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]:
|
||
named = ea.keys("LB", one.get("직종") or one.get("이름")) # 815-1 M01 별칭 칸 먼저
|
||
if len(named) == 1:
|
||
return {"상태": LINKED, "단위": {"노임": named[0]}, "꼬리표": ["별칭"]}
|
||
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, "로직": 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:
|
||
"""M01 자재 줄 「별칭」 칸(815-1 · 이름 + 규격 · 같은 품목 지역 줄은 하나) — 숫자보다 먼저."""
|
||
keys = ea.keys(ea.MARKET, line.get("이름"), line.get("규격"))
|
||
return keys[0] if len(keys) == 1 else ea.one(line, keys) if keys 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
|