"""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` — 별칭 장부 › 이름 · 규격 같음 › 바탕 이름 · 숫자 규격 같음(치수 차례 · 구분자 무관) · 글자 든 규격 = 후보로만) · 손료율 = 방법 「수량 × 손료율 × 단가」 · 별도 = 단위 `{자재?, 별도계상}` · 단위 `{자재}` R 율 = 778-4 꼴 `{기준, 비목}` — 기준 = `기준꼴`(인력 · 재료 묶음 · 노무비 · 줄 목록 `{행: [...]}`) · 우리에 없는 율 항목(설치비 · 제경비)도 율 줄 · 기준 못 찾음 = 고를 것(글 기준 제안) S 하위 = `{하위 {종류 단산|일위, 번}}`(SK · SC 참조) · E 값 = `{값: 금액}`(고정 금액 줄) 별칭 장부 짝(자재) = 숫자보다 먼저. 서버 단독 · 읽기만. """ 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_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, "후보": []} 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, "로직": 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