"""M02 실무자료 줄 → 고정 줄(PLAN 705-3 · 요구서 1장 · 4장 2 · 3 · 7) — 엑셀 숫자 그대로 · 빠짐 0. 맞춤 확인 안 한 줄 · 노임 · % · 「우리에 없음」 줄 = 빠진 줄 대신 고정 줄(705-1 그릇 `M02_Labor_Fixed`). 숫자는 서버가 실무자료 파일에서 읽음(브라우저 값 안 받음). 단가 = 엑셀 단가 노무비 · 재료비 · 경비(빈 칸 0) · 합계만 있는 줄 = 재료비(4장 3) · 수량 없음 = 수량 1 · 단가 = 엑셀 금액(「금액만」). 이음 = 이름 · 규격 · 단위 · 단가가 다 같은 노임(LB) · 자재(MT)만 저절로 → 엑셀 합계 = 우리 합계 · 단가가 다른 짝 = `후보` 수만(바꾸기는 705-2 「이 공사 단가로 다시 잇기」). % 줄 = 비율 줄 `비율 {기준, 비목}` — 엑셀 밑수(단가 계) = 다른 줄 금액[기준] 합일 때만 · 아니면(주재료비 등) 수량 1 · 「식」 · 단가 = 엑셀 금액. 같은 이름 · 규격 · 단위 둘째부터 = 이름 끝 「 (2)」(줄 가름 · 705-1 400 피함). 출처 = `{파일, 호표, 줄}`(줄 = 엑셀 줄 차례 0부터 · [품셈으로 바꾸기] 짝). """ from __future__ import annotations import re from decimal import Decimal from typing import Any from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Hopyo_Finger as finger 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_Labor_Doc as labor from M02_MasterTemplete import M02_Labor_Fixed as fixed COSTS = ("노무비", "재료비", "경비") _BASE = re.compile(r"(? dict[str, Decimal]: """엑셀 단가 · 금액 → 세 비목(빈 칸 0) · 합계만 있으면 재료비(4장 3).""" money = money if isinstance(money, dict) else {} out = {k: _d(money.get(k)) or Decimal(0) for k in COSTS} total = _d(money.get("계")) if total and not any(_d(money.get(k)) is not None for k in COSTS): out["재료비"] = total return out def _ratio(line: dict[str, Any], others: list[dict[str, Any]]) -> dict[str, str] | None: """% 줄 → `{기준, 비목}` — 밑수 = 다른 줄 엑셀 금액[기준] 합(끝수 1 원 안) · 금액 비목 하나.""" hit = _BASE.search(f"{line.get('규격') or ''} {line.get('이름') or ''}") under = _d((line.get("단가") or {}).get("계")) paid = [k for k, v in _parts(line.get("금액")).items() if v] if not hit or under is None or len(paid) != 1: return None base = "노무비" if hit.group(1) == "인건비" else hit.group(1) total = sum((_parts(x.get("금액"))[base] for x in others), Decimal(0)) return {"기준": base, "비목": paid[0]} if abs(total - under) <= GAP else None class Linker: """이음 — 노임(LB 이름 같음) · 자재(이름 · 규격 같은 한 품목 · 단위 같음)가 단가까지 같을 때만.""" def __init__(self) -> None: self.whole = m01.loaded()[1] self.got = hm.targets() self.labor: dict[str, list[tuple[str, Decimal | None]]] = {} for row in self.whole.numbers.get("LB", {}).values(): pair = (str(row.get("키")), _d(row.get("값"))) self.labor.setdefault(str(row.get("이름") or ""), []).append(pair) self.near = 0 # 단가 다른 이음 후보 수 def link(self, line: dict[str, Any], price: dict[str, Decimal]) -> dict[str, str | None]: out: dict[str, str | None] = {"노임": None, "자재": None, "기계": None} kind = finger.kind_of(line) if kind == "노임": pairs = self.labor.get(shares.trade(line.get("이름")) or "", []) out["노임"] = self._same(pairs, price["노무비"]) elif kind in hm.MATERIAL_KINDS and price["재료비"]: out["자재"] = self._same(self._mats(line), price["재료비"]) # ponytail: 기계 이음 없음 — 기계 시간 단가는 B09 중기목록표 셈 · 같은 값 판정 못 함 return out def _same(self, pairs: list[tuple[str, Decimal | None]], want: Decimal) -> str | None: same = [code for code, value in pairs if value is not None and abs(value - want) < GAP] if len(same) == 1: return same[0] self.near += bool(pairs) return None def _mats(self, line: dict[str, Any]) -> list[tuple[str, Decimal | None]]: me, _ = hmat.clean(line) found = hm._exact(me, "자재", self.got) if len(found) != 1: found = [r["one"] for r in hmat.sure(me)[0]] out = [] for one in found: code = one.get("자재") if not code or hm.factor(line.get("단위"), one.get("단위"))["배"] != 1: continue try: price = fixed.mm.element(self.whole, code, {}, line.get("단위") or None)[0] except fixed.mf.FormulaError: price = None out.append((code, _d(price))) return out def line_of( line: dict[str, Any], others: list[dict[str, Any]], where: dict[str, Any], taken: set[str], linker: Linker, ) -> dict[str, Any]: """엑셀 줄 하나 → 일위대가 고정 줄 `{고정, 별칭, 메모, 수량}` — `taken` = 이 호표 줄 가름(채워 감).""" qty = _d(line.get("수량")) spec, unit = str(line.get("규격") or "").strip(), str(line.get("단위") or "").strip() rate = _ratio(line, others) if finger.kind_of(line) == "%" else None memo = f"실무 {qty if qty is not None else ''}{unit} 그대로" if rate: price = dict.fromkeys(COSTS, Decimal(0)) elif qty is None or finger.kind_of(line) == "%": # 금액만 · 밑수 다른 % 줄 price, qty, memo = _parts(line.get("금액")), Decimal(1), f"{memo} · 금액만" unit = "식" if finger.kind_of(line) == "%" else unit else: price = _parts(line.get("단가")) name = str(line.get("이름") or "").strip() or "이름 없음" head, n = name, 1 while fixed.fixed_id({"이름": name, "규격": spec, "단위": unit}) in taken: n += 1 name = f"{head} ({n})" fx = { "이름": name, "규격": spec, "단위": unit, "단가": {k: _plain(v) for k, v in price.items()}, } fx["이음"] = dict.fromkeys(fixed.LINKS) if rate else linker.link(line, price) fx["출처"] = {**where, "줄": int(line.get("차례", 0))} if rate: fx["비율"] = rate taken.add(fixed.fixed_id(fx)) alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None return {"고정": fx, "별칭": alias, "메모": memo, "수량": {"손": _plain(qty)}}