- M02_Doc_Import_Shape(새 · derive 끝에 부름) — 「줄_2 · 노무비_2 · 계_2」 → 「공구손료 및 잡재료비(노무비의 2%) · 노무비 합계 · 계」 · 단계 줄 목록 따라감 - 같은 기계 이음 노무비 · 재료비 · 경비 세 줄 → 묶음 한 줄(자원 이름 · 번지) · 부르던 식 = 번지.X.비목 · 단계가 일부만 담으면 그대로 - 실무 여덟 단산 459 블록 × 엑셀 · 우리 = 다듬기 전후 금액 같음 · 엑셀과 같음 459 - Labor_Table 덧칸 = 명칭 바로 뒤(넓은 표에서도 보임) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
173 lines
7.5 KiB
Python
173 lines
7.5 KiB
Python
"""M02 가져온 식 수식 문서 다듬기(PLAN 809-1 · 요구 704) — `M02_Doc_Import.derive` 끝에 부름 · 금액 그대로.
|
||
|
||
① 소계 · 합계 성격 줄(덧줄 · 단위 「식」 · 수량 없음 · 단가 = 다른 줄 기호 합) 이름 = 엑셀 원 글 머리
|
||
(`가져온.원본.글줄` 자리 = 그 줄 이름 · 「:」 앞 글 · 글이 수식뿐이면 바로 위 설명 글 ·
|
||
「합 계」 절 아래 비목 = 「노무비 합계」 꼴) — 「줄_2 · 노무비_2 · 계_2」 같은 지은 이름 걷음
|
||
② EST 기계 세 줄(같은 자원 · 같은 기계 이음 · 같은 수량 · 노무비 · 재료비 · 경비 하나씩) = 한 기계 줄
|
||
(이름 = 자원 이름 · 비목 묶음 · 단가 = 기계 이음 칸 없음 · 엑셀 = 값묶음) · 세 줄 기호를 부르던 식 =
|
||
그 줄 비목 칸(`C_크레인.노무비`) — 셈 값 그대로(줄 끝수 = 비목마다 같은 규칙)
|
||
엑셀 원 글 · 엔진 줄은 안 고침(문서 겉꼴만) · 못 맞추면 그대로 둠.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
COSTS = ("노무비", "재료비", "경비")
|
||
_MADE = re.compile(r"^(?:줄|노무비|재료비|경비|계|소계|합계)?_\d+$")
|
||
_SECTION = re.compile(r"^\s*\d+\s*\.\s*(.+?)\s*$")
|
||
|
||
|
||
def _flat(text: Any) -> str:
|
||
return re.sub(r"\s+", "", str(text or ""))
|
||
|
||
|
||
def _head(text: str) -> str:
|
||
"""「노무비 : 13,944.5 + …」 → 「노무비」 · 머리 없음 = 빈 글."""
|
||
head = text.split(":", 1)[0] if ":" in text else ""
|
||
return _flat(head)
|
||
|
||
|
||
def names(doc: dict, lines: list[dict]) -> None:
|
||
"""① 지은 이름(「줄_2 · 노무비_2 · 계_2」) → 엑셀 원 글 머리."""
|
||
section, note, by = "", "", {}
|
||
for one in lines:
|
||
text = str(one.get("글") or "")
|
||
if hit := _SECTION.match(text):
|
||
section, note = _flat(hit.group(1)), ""
|
||
continue
|
||
spot = one.get("자리")
|
||
if not spot:
|
||
note = text.strip()
|
||
continue
|
||
head = _head(text) or note
|
||
if head and section in ("합계", "계") and head in COSTS:
|
||
head = f"{head} 합계"
|
||
by[str(spot)] = head
|
||
note = ""
|
||
taken = {str(x.get("이름")) for x in doc.get("금액") or []}
|
||
for line in doc.get("금액") or []:
|
||
name = str(line.get("이름") or "")
|
||
new = by.get(name)
|
||
if _MADE.match(name) and new and new not in taken: # 단계 · 다른 줄 이름과 안 겹칠 때만
|
||
line["이름"] = new
|
||
taken.add(new)
|
||
_rename_steps(doc, {name: new})
|
||
|
||
|
||
def _rename_steps(doc: dict, swap: dict[str, str]) -> None:
|
||
"""단계 줄 목록(이름으로 묶음) — 바뀐 이름 따라감 · 합친 줄 = 한 번."""
|
||
for st in doc.get("단계") or []:
|
||
got = []
|
||
for n in st.get("줄") or []:
|
||
n = swap.get(n, n)
|
||
if n not in got:
|
||
got.append(n)
|
||
st["줄"] = got
|
||
|
||
|
||
def _bundle_of(doc: dict, at: int) -> list[int] | None:
|
||
"""at 에서 시작하는 기계 세 줄(노무비 · 재료비 · 경비) — 같은 기계 이음 · 같은 수량."""
|
||
money = doc.get("금액") or []
|
||
three = money[at : at + 3]
|
||
if len(three) < 3 or [x.get("비목") for x in three] != list(COSTS):
|
||
return None
|
||
vars_ = {v.get("열쇠"): v for v in doc.get("변수") or []}
|
||
links = [(vars_.get(str(x.get("단가"))) or {}).get("연결") or {} for x in three]
|
||
if any(x.get("갈래") != "단가산출" or "기계" not in (x.get("입력") or {}) for x in links):
|
||
return None
|
||
same = {(x.get("키"), str(x.get("입력"))) for x in links}
|
||
if len(same) != 1 or [x.get("칸") for x in links] != list(COSTS):
|
||
return None
|
||
for k in ("수량", "규격", "자리", "합밖"):
|
||
if len({str(x.get(k) or "") for x in three}) != 1:
|
||
return None
|
||
names = {str(x.get("이름")) for x in three}
|
||
for st in doc.get("단계") or []: # 단계가 셋 중 일부만 담으면 못 합침
|
||
if 0 < len(names & set(st.get("줄") or [])) < 3:
|
||
return None
|
||
return [at, at + 1, at + 2]
|
||
|
||
|
||
def _place(doc: dict, name: str, used: set[str]) -> str:
|
||
"""기계 줄 번지 — 자원 이름 첫 낱말(괄호 · 규격 · 기호 뺌) · 겹치면 숫자."""
|
||
word = re.sub(r"[^\w]|_", "", re.split(r"[\s(]", name.strip())[0]) or "기계"
|
||
if word[0].isdigit():
|
||
word = f"기계{word}"
|
||
have = used | {str(x.get("번지")) for x in doc.get("금액") or [] if x.get("번지")}
|
||
got, n = word, 1
|
||
while got in have:
|
||
n += 1
|
||
got = f"{word}{n}"
|
||
used.add(got)
|
||
return got
|
||
|
||
|
||
def _rewrite(text: Any, swap: dict[str, str]) -> Any:
|
||
if not isinstance(text, str) or not swap:
|
||
return text
|
||
keys = "|".join(re.escape(k) for k in sorted(swap, key=len, reverse=True))
|
||
pat = re.compile(rf"(?<![^\W_])(?:{keys})(?![^\W_]|[′'.])")
|
||
return pat.sub(lambda m: swap[m.group(0)], text)
|
||
|
||
|
||
def machines(doc: dict, title: dict[str, str]) -> None:
|
||
"""② EST 기계 세 줄 → 한 기계 줄(비목 묶음) · 부르던 식 = 비목 칸. `title` = {줄 이름: 자원 이름}."""
|
||
money = doc.get("금액") or []
|
||
vars_ = {v.get("열쇠"): v for v in doc.get("변수") or []}
|
||
out, swap, at, used = [], {}, 0, set()
|
||
while at < len(money):
|
||
idx = _bundle_of(doc, at)
|
||
if idx is None:
|
||
out.append(money[at])
|
||
at += 1
|
||
continue
|
||
three = [money[i] for i in idx]
|
||
first = vars_[str(three[0]["단가"])]
|
||
link = {k: v for k, v in first["연결"].items() if k not in ("칸", "엑셀")}
|
||
excel = {
|
||
c: Decimal(str((vars_[str(x["단가"])]["연결"]).get("엑셀") or 0))
|
||
for c, x in zip(COSTS, three)
|
||
}
|
||
link["엑셀"] = excel
|
||
name = title.get(str(three[0].get("이름"))) or str(three[0].get("이름"))
|
||
first["연결"], first["이름"] = link, f"{name} 단가"
|
||
if name in {str(x.get("이름")) for x in money} - {str(x.get("이름")) for x in three}:
|
||
name = f"{name} ({at + 1})" # 같은 이름 줄이 있으면 겹치지 않게
|
||
line = {k: v for k, v in three[0].items() if k != "기호"} | {"이름": name, "비목": "묶음"}
|
||
_rename_steps(doc, {str(x.get("이름")): name for x in three})
|
||
if any(x.get("기호") for x in three): # 부르던 식 = 번지 칸(엔진 `번지.X.노무비`)
|
||
place = _place(doc, name, used)
|
||
line["번지"] = place
|
||
for c, x in zip(COSTS, three):
|
||
if x.get("기호"):
|
||
swap[str(x["기호"])] = f"번지.{place}.{c}"
|
||
doc["변수"] = [
|
||
v
|
||
for v in doc["변수"]
|
||
if v is first or v.get("열쇠") not in {str(x["단가"]) for x in three[1:]}
|
||
]
|
||
out.append(line)
|
||
at += 3
|
||
doc["금액"] = out
|
||
for line in out:
|
||
for k in ("단가", "수량"):
|
||
line[k] = _rewrite(line.get(k), swap)
|
||
for one in doc.get("식") or []:
|
||
one["식"] = _rewrite(one.get("식"), swap)
|
||
|
||
|
||
def shape(doc: dict, meta: dict) -> dict:
|
||
"""derive 문서 다듬기 — 원본 글줄 · 엔진 줄 자원 이름."""
|
||
src = meta.get("원본") or {}
|
||
names(doc, list(src.get("글줄") or []))
|
||
title = {}
|
||
for item in (meta.get("로직") or {}).get("호표") or []:
|
||
res = item.get("자원") or {}
|
||
if res.get("갈래") == "기계" and res.get("이름"):
|
||
title[str(item.get("이름"))] = " ".join(x for x in (res["이름"], res.get("규격")) if x)
|
||
machines(doc, title)
|
||
return doc
|