- 3 연결 찾기 = 엑셀 이름 규격 · 단위 꼬리 뗀 이름으로 맞댐(`M02_Doc_Match_Note.bare` · 보이는 이름 그대로) · STmate 블록 규격도 가져온 칸에
- 4 기계 세 줄 = 기계 한 줄 — STmate X 코드 노 · 재 · 경 세 참조 줄(같은 수량 · 번지) 묶음(묶어서 셈 못 하는 세 줄만 그대로) ·
문서 단계 묶기 넓힘(차례 아무렇게 · 시간당 경비 이음 없으면 엑셀 값 묶음 · 번지 같을 때) · 조합 다음 줄 = 세 줄 뒤 · 같은 기계 한 항목
- 6 공구손료 율 비목 차이(재료비 ↔ 경비) = 참고(`M02_Doc_Match_Value.tool_cost`) — 확정 안 막음
- 7 못 이은 줄 까닭(`M02_San_Link.why` — 자재 없음 · 운임표 없음 · 기계 표에 없음 · 별칭 대기 · 후보 여럿 · 시간당 경비 없음 · 조합) =
문서 설명 · 이음 없는 자원 띠 · 연결 「없음」 띠 = 「우리 기초 없음 — 우리 정보로 셈 n/m 줄」 · 형제만 = 「조합」
- 8 유가 = 전국평균 줄 + 지역 옵션(`MO:경유:{S_지역}`) · 전력 = 일반용(갑)Ⅰ 저압 + 계약종별 옵션 · 엑셀 값 = 검산 칸
- 9 재료 = 지역 꼬리 뗀 이름 · 규격 정확 일치면 이음(지역 여럿 = 줄 지역 › 서울)
- 10 조합 첫 줄 규격 = 그 기계 · 얼린 줄도 소계 식에(북부청임도 33) · 기계 줄 찾기 판정 늘 붙음 · 올린 직후 409(HP 보이기 전 산근 보관) ·
엑셀 블록 「엑셀 읽기 확정」 글 · 전수 도우미 str() 뗌 · 줄 쌓기 이름에 규격이 들면 한 번만
- M02_San_Link 새 파일(San_Emit 700 줄 막음 — 요소 잇기 · 시간당 · 조합 · 까닭)
- 시험: 새 test_814_1 · 812-2 · 810-4 거푸집 41 = 근접 → 확정(공구손료 비목 = 값 · 요구 744 C1) · 바꾸기 거절 견본 = 19 ·
812-9 조합 첫 줄 규격 「0.7+브레이카」 → 「0.7」(칩 글 · 요구 738 흠 12)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
231 lines
10 KiB
Python
231 lines
10 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, who: dict[str, str] | None = None) -> list[int] | None:
|
||
"""at 에서 시작하는 기계 세 줄(노무비 · 재료비 · 경비 · 차례 아무렇게) — 같은 기계 이음 · 같은 수량 ·
|
||
시간당 경비 이음 없음(숫자)이면 같은 기계 자원(`who` 줄 이름 → 자원 · 814-1)."""
|
||
money = doc.get("금액") or []
|
||
three = money[at : at + 3]
|
||
if len(three) < 3 or sorted(str(x.get("비목")) for x in three) != sorted(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 all(x.get("갈래") == "숫자" and not isinstance(x.get("값"), dict) for x in links):
|
||
mine = {(who or {}).get(str(x.get("이름"))) for x in three}
|
||
if len(mine) != 1 or None in mine:
|
||
return None
|
||
elif any(x.get("갈래") != "단가산출" or "기계" not in (x.get("입력") or {}) for x in links):
|
||
return None
|
||
elif len({(x.get("키"), str(x.get("입력"))) for x in links}) != 1 or [
|
||
x.get("칸") for x in links
|
||
] != [x.get("비목") for x in three]:
|
||
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)
|
||
|
||
|
||
AFTER = "_뒤" # 옮긴 조합 다음 줄 — 차림이 원본 행 차례로 다시 섞지 않게(다듬기 사이 임시)
|
||
|
||
|
||
def regroup(doc: dict, who: dict[str, str], parts: set[str]) -> None:
|
||
"""814-1 — 조합 기계 비목 세 줄 사이에 낀 다음 줄(값 0)을 세 줄 뒤로 · 같은 기계끼리 모음
|
||
(첫 줄 · 다음 줄이 엇갈려 항목이 겹치지 않게 · 값 0 줄이라 셈 그대로). `parts` = 조합 다음 줄 이름."""
|
||
money = doc.get("금액") or []
|
||
at = 0
|
||
while at < len(money):
|
||
head = who.get(str(money[at].get("이름")))
|
||
if head is None or str(money[at].get("이름")) in parts:
|
||
at += 1
|
||
continue
|
||
end, mains, rest = at, [], []
|
||
while end < len(money):
|
||
name = str(money[end].get("이름"))
|
||
if name in parts:
|
||
rest.append(money[end])
|
||
elif who.get(name) == head and len(mains) < 3:
|
||
mains.append(money[end])
|
||
else:
|
||
break
|
||
end += 1
|
||
if len(mains) > 1 and rest:
|
||
seen = list(dict.fromkeys(who.get(str(x.get("이름"))) for x in rest))
|
||
rest.sort(key=lambda x: seen.index(who.get(str(x.get("이름"))))) # 같은 기계끼리
|
||
for x in rest:
|
||
x[AFTER] = True # 차림(`NativeLayout.apply`) 원본 행 = 앞 줄 행 뒤
|
||
money[at:end] = mains + rest
|
||
at = max(end, at + 1)
|
||
|
||
|
||
def machines(doc: dict, title: dict[str, str], who: dict[str, str] | None = None) -> None:
|
||
"""② 기계 세 줄 → 한 기계 줄(비목 묶음) · 부르던 식 = 비목 칸. `title` = {줄 이름: 자원 이름} ·
|
||
`who` = {줄 이름: 기계 자원 열쇠}(시간당 경비 이음 없는 세 줄 · 814-1)."""
|
||
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, who)
|
||
if idx is None:
|
||
out.append(money[at])
|
||
at += 1
|
||
continue
|
||
three = sorted((money[i] for i in idx), key=lambda x: COSTS.index(x["비목"]))
|
||
first = vars_[str(money[idx[0]]["단가"])]
|
||
if first["연결"].get("갈래") == "숫자": # 시간당 경비 이음 없음 = 엑셀 값 묶음
|
||
link = {
|
||
"갈래": "숫자",
|
||
"값": {c: (vars_[str(x["단가"])]["연결"]).get("값") for c, x in zip(COSTS, three)},
|
||
}
|
||
else:
|
||
link = {k: v for k, v in first["연결"].items() if k not in ("칸", "엑셀")}
|
||
link["엑셀"] = {
|
||
c: Decimal(str((vars_[str(x["단가"])]["연결"]).get("엑셀") or 0))
|
||
for c, x in zip(COSTS, three)
|
||
}
|
||
lead = money[idx[0]]
|
||
name = title.get(str(lead.get("이름"))) or str(lead.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 lead.items() if k != "기호"} | {"이름": name, "비목": "묶음"}
|
||
line["단가"] = lead["단가"]
|
||
_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} - {str(lead["단가"])}
|
||
]
|
||
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 문서 다듬기 — 원본 글줄 · 엔진 줄 자원 이름."""
|
||
from M02_MasterTemplete import M02_Doc_NativeLayout as lay
|
||
|
||
src = meta.get("원본") or {}
|
||
lay.tag(doc, meta) # 812-3 원파일 차림 — 금액 줄 ↔ 로직 줄(끝에 `apply`)
|
||
names(doc, list(src.get("글줄") or []))
|
||
title, who, parts = {}, {}, set()
|
||
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)
|
||
key = (res.get("코드"), res.get("이름"), res.get("규격"))
|
||
who[str(item.get("이름"))] = repr(key)
|
||
if res.get("조합"):
|
||
parts.add(str(item.get("이름")))
|
||
regroup(doc, who, parts)
|
||
machines(doc, title, who)
|
||
lay.apply(doc, meta)
|
||
for line in doc.get("금액") or []:
|
||
line.pop(AFTER, None)
|
||
return doc
|