Files
Aislo/B09_Estimation/B09_Estimation_Import_Prices.py
T
eomsangdonandClaude Opus 5.5 193ce92806 refactor(841-1): B09 단가 책 · 바탕 셈 걷음 — B 걸음
- 단가 책 = 자재만(자재단가대비표 · 4 자재대 · 관급자재대) · 노무 · 기계 · 운전사 계수(CC000336 세 줄) · 연료 · 환율 줄 걷음
- 바탕 셈(py · ts 짝) 운전사 시간 노임 · 중기사용료(손료 · 운전사 · 연료 · 잡품 재현) · 경비목록표 · 노임 · 기계 덮개 걷음
- 노무비목록표 · 중기목록표 = 대가 · 산근 줄 엔진 단가(프로젝트 사본 M02 값) 나열 · 다시 안 셈(price_lists)
- 경비목록표 · 중기사용료 · 환율및기초자료 탭 = 「—」 한 줄 + 까닭 한 줄(탭은 둠)
- [단가 갱신] = 프로젝트 사본 자원 단가 줄(단가.json LB · MT · EQ · MO · FX)을 지금 M01 로 + 자재 단가 책(새 파일 B09_Estimation_Copy_Prices.py · 사용자가 누를 때만) · 미리 보기 = 덮개로 같은 셈(파일 안 씀)
- 산근 · 대가 단가 고친 칸 걷음(품만) · 단가표 불러오기(707-3) 노임 · 기계 덮개 걷음(자재만)
- 낡은 시험은 C 걸음에서 정리

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-15 16:02:11 +09:00

131 lines
6.2 KiB
Python

"""B09 경쟁사 기초 단가표 불러오기(PLAN 707-3) — 서버 단독 · 저장 안 함
(덮개 제안 → 화면 캐시 → [저장]).
읽는 시트(꼴마다 첫 것) — 머리 = 「명칭」 · 「품명」 칸이 든 줄:
자재 = STmate 「재료비목록표」 › 「자재단가대비표」 · EST 「자재단가」 → 적용 단가(단위 % 줄 뺌)
(841-1 노임 · 기계 단가표 걷음 — B09 노무 · 기계 단가 = M02 사본 값 · 덮지 않음)
이음 = 이 공사 자재단가대비표 줄에 이름 · 규격 · 단위로 — 705-3 이음 낱말 그대로(「별도계상」 글 뗀
이름 · 규격 · 단위 › 이름 · 단위) · 한 줄에만 맞을 때만 · 한 책 줄에 엑셀 줄 여럿 = 「짝 여럿」.
덮개 = 자재 = 지금 덮개 `materials[키].applied`(고른 출처 `pick` 은 뺌 — 적용 차례가 pick 먼저) ·
책 값과 같으면 덮지 않음(옛 덮개 걷음).
못 이은 줄 = 「짝 없음 n」(이름 목록).
"""
from __future__ import annotations
import unicodedata
from collections import Counter
from typing import Any
from B09_Estimation.B09_Estimation_Import_Rates import flat, money, read_sheets
from M02_MasterTemplete import M02_Hopyo_Material as hmat
from M02_MasterTemplete.M02_Hopyo_Excel import ExcelError
#: 841-1 노임 · 기계 단가 덮개 걷음(B09 노무 · 기계 단가 = M02 사본 값) — 자재만
KINDS = {"material": ("재료비목록표", "자재단가대비표", "자재단가")}
LIST_MAX = 40
def _n(text: Any) -> str:
return flat(unicodedata.normalize("NFKC", str(text or ""))).lower()
def table(rows: list[list[Any]]) -> list[dict[str, Any]]:
"""목록 시트 → [{name, spec, unit, price}]."""
for at, row in enumerate(rows[:12]):
cols = [flat(v) for v in row]
name = next((cols.index(h) for h in ("명칭", "품명") if h in cols), None)
if name is None:
continue
spec, unit = (cols.index(h) if h in cols else None for h in ("규격", "단위"))
price = next((cols.index(h) for h in ("적용단가", "단가") if h in cols), None)
if price is None:
continue
out = []
for line in rows[at + 1 :]:
def cell(col: int | None, line: list[Any] = line) -> Any:
return line[col] if col is not None and col < len(line) else None
item = {"name": str(cell(name) or "").strip(), "spec": str(cell(spec) or "").strip(),
"unit": str(cell(unit) or "").strip()} # fmt: skip
item["price"] = money(cell(price))
ok = item["price"] is not None and item["unit"] != "%"
if item["name"] and ok:
out.append(item)
return out
return []
def read_prices(name: str, data: bytes) -> dict[str, list[dict[str, Any]]]:
"""파일 → {material: 줄 목록} · 시트 없음 = 막힘."""
sheets = read_sheets(name, data, tuple(s for names in KINDS.values() for s in names))
out = {}
for kind, names in KINDS.items():
sheet = next((s for s in names if s in sheets), None)
out[kind] = table(sheets[sheet]) if sheet else []
out[f"{kind}_sheet"] = sheet
if not any(out[k] for k in KINDS):
raise ExcelError("재료비목록표 · 자재단가대비표(STmate) · 자재단가(EST) 시트가 없음")
return out
def _material_keys(row: dict[str, Any]) -> list[str]:
me, _ = hmat.clean({"이름": row.get("name"), "규격": row.get("spec")})
unit = _n(row.get("unit"))
return [f"{_n(me['이름'])}|{_n(me['규격'])}|{unit}", f"{_n(me['이름'])}||{unit}"]
def _match(book: list[dict[str, Any]], keys_of, item: dict[str, Any]) -> dict[str, Any] | None:
"""책 줄 하나 — 단계마다 한 줄에만 맞으면 그 줄 · 여럿 · 없음이면 다음 단계."""
for step, want in enumerate(keys_of(item)):
hits = [r for r in book if keys_of(r)[step] == want] if want else []
if len(hits) == 1:
return hits[0]
return None
def _same(a: Any, b: Any) -> bool:
return a is not None and b is not None and abs(a - b) < 0.5
def propose(read: dict[str, Any], sheets: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]:
"""읽은 단가표 + 이 공사 단가 책(덮개 뺀 바탕 셈) → 덮개 patch · 이은 줄 · 짝 없음."""
base = {"material": sheets.get("compare") or []}
keys = {"material": _material_keys}
materials = {k: dict(v) for k, v in (user.get("materials") or {}).items()}
linked: list[dict[str, Any]] = []
missing: dict[str, list[str]] = {k: [] for k in KINDS}
for kind in KINDS:
items = read.get(kind) or []
rows = [_match(base[kind], keys[kind], item) for item in items]
hits = Counter(r["key"] for r in rows if r)
for item, row in zip(items, rows, strict=True):
label = " ".join(x for x in (item["name"], item["spec"]) if x)
if row is None or hits[row["key"]] > 1: # 한 책 줄에 엑셀 줄 여럿 = 못 고름
missing[kind].append(label if row is None else f"{label}(짝 여럿)")
continue
got, book = item["price"], row.get("applied")
linked.append({"kind": kind, "key": row["key"], "name": row.get("name") or "",
"from": label, "excel": got, "book": book,
"same": _same(got, book)}) # fmt: skip
same = linked[-1]["same"] # 책 값 = 엑셀 → 옛 덮개 걷음(값 = 책)
drop = ("pick", "applied") if same else ("pick",)
cell = {k: v for k, v in materials.get(row["key"], {}).items() if k not in drop}
materials[row["key"]] = cell if same else {**cell, "applied": got}
return {
"patch": {"materials": materials},
"linked": linked,
"missing": {k: v[:LIST_MAX] for k, v in missing.items()},
"counts": {
k: {
"read": len(read.get(k) or []),
"linked": sum(x["kind"] == k for x in linked),
"covered": sum(x["kind"] == k and not x["same"] for x in linked),
"book": len(base[k]),
"missing": len(missing[k]),
}
for k in KINDS
}, # fmt: skip
}