- M02_San_Xlsx(공통 · 글자 칸 배치 · 금액 칸 · 자원 표) · M02_San_Xlsx_Est(EST 글 → 표지: 변수 = 가장 늦은 대입 값 하나 · 칸 나눔 꼴은 닫는 표지 2 칸 · 합계 줄 · 세 줄 머리 · 노임 · 자재 단가 짝 · 소계 61 · 율 58 · 62 · 구간 글자) · M02_San_Xlsx_Stmate(숨은 G 열 원문 = stc_line · 끝전 파일 투표) - M02_Doc_FromNative: 엑셀 둘 꼴(번 · 시트 행 · 원본 엑셀 참 · 원파일 거짓) · M02_Doc_Import: 엑셀 길도 소계 비목 칸 비움 · 추정 칸(출처 = 엑셀 꼬리표) - M02_Sangeun_FromHopyo: 산근 블록 = 새 읽기기(옛 San_Est · Emit.est 부름 걷음 · 옛 저장은 한 번 지어 둠) - test_812_1: 실제 짝(거창 · STC 다섯) 같음 · 합성 배치 = 거창 실제 · 합성 EST 1,849 · STC 371 · 금액 실무 11(엇갈림 고정 목록) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
6.7 KiB
Python
175 lines
6.7 KiB
Python
"""경쟁사 산근 엑셀(.xlsx · .xls) 읽기기 — 810-1 원파일 파서와 같은 「구조 줄」 꼴(PLAN 812-1 · 요구 711-2 ·
|
|
712 · 713 · 읽기만 · 서버 단독).
|
|
|
|
`read(name, data)` = `M02_San_Estx.read` · `M02_San_Stc.read` 와 같은 칸 이름 · 같은 뜻
|
|
(칸 뜻 `resources/master_data/_원파일파서_틀.md`) → 그 뒤는 원파일 길(`M02_Doc_FromNative.pack` →
|
|
`emit` → `M02_Doc_NativeEst` · `M02_San_Emit.stmate` → 수식 문서) 그대로.
|
|
EST = `M02_San_Xlsx_Est`(단가산출 시트 글 · 금액 칸 · 같은 책 단가표) ·
|
|
STmate = `M02_San_Xlsx_Stmate`(숨은 G 열 원문 = 원파일 원문 · 단가 = 목록표 · 끝전 = 파일 투표).
|
|
엑셀이 못 주는 칸(자원 코드 · LinkIndex · 자리 Diget)은 비움 + 표지 「엑셀 — …」 · 글에서 읽어 정한 것은
|
|
`연결뜻` 에 적고 줄 `추정` 참 + 블록 표지 「엑셀 추정」.
|
|
여기 = 공통(시트 → 글 줄 · 금액 칸 · 자원 표).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import unicodedata
|
|
from typing import Any
|
|
|
|
from M02_MasterTemplete import M02_San_Book as book
|
|
|
|
GUESS = "엑셀 추정"
|
|
NOTE = "엑셀 — "
|
|
|
|
|
|
class XlsxError(ValueError):
|
|
"""산근 시트가 없는 엑셀."""
|
|
|
|
|
|
def flat(v: Any) -> str:
|
|
return re.sub(r"\s+", "", str(v or ""))
|
|
|
|
|
|
def number(v: Any) -> float | None:
|
|
"""칸 값 → 수(「3,442」 · 「8.」 · 수 칸) · 아니면 None."""
|
|
if isinstance(v, bool) or v is None:
|
|
return None
|
|
if isinstance(v, (int, float)):
|
|
return float(v)
|
|
s = str(v).strip().replace(",", "")
|
|
if not re.fullmatch(r"-?\d+(?:\.\d*)?|-?\.\d+", s):
|
|
return None
|
|
return float(s.rstrip(".") or 0)
|
|
|
|
|
|
def _cell(v: Any) -> str:
|
|
if isinstance(v, float) and v == int(v):
|
|
return str(int(v))
|
|
return "" if v is None else str(v)
|
|
|
|
|
|
def _width(ch: str) -> int:
|
|
return 2 if unicodedata.east_asian_width(ch) in "WFA" else 1
|
|
|
|
|
|
def layout(cells: list[tuple[int, str]], base: int) -> str:
|
|
"""글자 칸 나눔 줄(거창 꼴 · 열 = 화면 자리) → 한 줄 글(열 차례 · 모자란 자리 = 빈칸 · 겹친 자리(수 끝 점)
|
|
= 빈칸 하나)."""
|
|
out, end = "", 0
|
|
for col, text in cells:
|
|
want = col - base
|
|
if want > end:
|
|
out += " " * (want - end)
|
|
elif out and want < end and not out.endswith(" "):
|
|
out += " "
|
|
out += text
|
|
end = want + sum(_width(c) for c in text)
|
|
return out
|
|
|
|
|
|
# ── 시트 머리 ──────────────────────────────────────────────────────────
|
|
MONEY = ("합계", "노무비", "재료비", "경비")
|
|
|
|
|
|
def money_cols(rows: list[list[Any]]) -> list[int] | None:
|
|
"""머리 줄의 맨 오른쪽 「합계 · 노무비 · 재료비 · 경비」 네 칸 열 → [T L M S]."""
|
|
for r in rows[:6]:
|
|
names = [flat(v) for v in r]
|
|
hits = [j for j in range(len(names) - 3) if tuple(names[j : j + 4]) == MONEY]
|
|
if hits:
|
|
return list(range(hits[-1], hits[-1] + 4))
|
|
return None
|
|
|
|
|
|
def money_of(row: list[Any], cols: list[int]) -> dict[str, float] | None:
|
|
got = {c: number(row[j]) if j < len(row) else None for c, j in zip("TLMS", cols)}
|
|
got = {c: v for c, v in got.items() if v is not None}
|
|
return got or None
|
|
|
|
|
|
def head_col(rows: list[list[Any]], *words: str) -> int | None:
|
|
for r in rows[:6]:
|
|
for j, v in enumerate(r):
|
|
if flat(v) in words:
|
|
return j
|
|
return None
|
|
|
|
|
|
# ── 자원 표(EST) ───────────────────────────────────────────────────────
|
|
def _table(rows: list[list[Any]], price: tuple[str, ...]) -> list[dict[str, Any]]:
|
|
"""단가 시트 → [{번, 이름, 규격, 단위, 줄 (원 줄)}](품명 · 규격 · 단위 · 값 열 = 머리 글)."""
|
|
c_no, c_name = head_col(rows, "No"), head_col(rows, "품명")
|
|
c_size, c_unit = head_col(rows, "규격"), head_col(rows, "단위")
|
|
if c_name is None:
|
|
return []
|
|
out = []
|
|
for r in rows:
|
|
no = str(r[c_no]).strip() if c_no is not None and c_no < len(r) else ""
|
|
if not no.isdigit() or c_name >= len(r) or not str(r[c_name] or "").strip():
|
|
continue
|
|
out.append(
|
|
{
|
|
"번": int(no),
|
|
"이름": str(r[c_name]).strip(),
|
|
"규격": str(r[c_size] or "").strip() if c_size is not None else "",
|
|
"단위": str(r[c_unit] or "").strip() if c_unit is not None else "",
|
|
"줄": r,
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def resources(sheets: dict[str, list]) -> dict[str, list[dict[str, Any]]]:
|
|
"""같은 책 단가표 → {종류: [{번 · 이름 · 규격 · 단위 · 표 · 값 {T L M S}}]}(노임 · 자재 = 적용단가 ·
|
|
기계 · 견적 · 산근 = 적용 칸(합이 안 맞으면 당초 · `M02_San_Book.applied`))."""
|
|
out: dict[str, list[dict[str, Any]]] = {}
|
|
for kind, sheet, comp in (("노임", "노임단가", "L"), ("자재", "자재단가", "M")):
|
|
rows = sheets.get(sheet) or []
|
|
c = head_col(rows, "적용단가")
|
|
got = []
|
|
for one in _table(rows, ("적용단가",)):
|
|
v = number(one["줄"][c]) if c is not None and c < len(one["줄"]) else None
|
|
if v is not None:
|
|
got.append({**_drop(one), "표": sheet, "값": {"T": v, comp: v}})
|
|
out[kind] = got
|
|
for kind, sheet in (
|
|
("기계", "기계경비총괄표"),
|
|
("견적", "견적단가"),
|
|
("산근", "단가산출총괄표"),
|
|
):
|
|
got = []
|
|
for one in _table(sheets.get(sheet) or [], ()):
|
|
r = list(one["줄"]) + [None] * 30
|
|
vals = {k: float(v or 0) for k, v in book.applied(r).items()}
|
|
got.append({**_drop(one), "표": sheet, "값": vals})
|
|
out[kind] = got
|
|
return out
|
|
|
|
|
|
def _drop(one: dict) -> dict:
|
|
return {k: v for k, v in one.items() if k != "줄"}
|
|
|
|
|
|
def kind_of(sheets: dict[str, list]) -> str | None:
|
|
return book.kind_of(sheets)
|
|
|
|
|
|
def read(name: str, data: bytes) -> dict[str, Any]:
|
|
"""엑셀 → {꼴, 산근, 일위, 자원, 내역, 표지(, 단가 · 옵션 STmate)} — 산근 시트 없음 = XlsxError."""
|
|
sheets = book.sheets(name, data)
|
|
return read_sheets(sheets)
|
|
|
|
|
|
def read_sheets(sheets: dict[str, list]) -> dict[str, Any]:
|
|
kind = kind_of(sheets)
|
|
if kind == "EST":
|
|
from M02_MasterTemplete import M02_San_Xlsx_Est as xe
|
|
|
|
return xe.read(sheets) | {"엑셀": True}
|
|
if kind == "STmate":
|
|
from M02_MasterTemplete import M02_San_Xlsx_Stmate as xs
|
|
|
|
return xs.read(sheets) | {"엑셀": True}
|
|
raise XlsxError("STmate · EST 산근 시트 없음")
|