Files
Aislo/B09_Estimation/B09_Estimation_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

235 lines
10 KiB
Python

"""B09 자재 단가 책 · 수요 모음(PLAN 144 · 841-1) — 서버 단독(M01 을 읽는 곳은 여기 하나).
841-1 — 노무 · 기계 · 운전사 · 연료 · 환율은 책에서 걷음(B09 금액 = 사본을 M02 엔진으로 셈한 값 ·
`B09_Estimation_Copy_Prices`).
책에 남는 것 = 자재단가대비표(4 자재대 · 관급자재대 단가 고르기) 자재 줄.
자재 단가 = 프로젝트 한 벌 — 원가산출에서 한 번 굳힘(`price_book.json`) ·
값 = 사본 단가 기준 시점(795-4 · 구조물 지정 때 `단가.json` 자원 줄 · 없는 자원만 지금 M01 +
`now_keys`) · [단가 갱신] 때만 새로.
줄 = 수요 따라(사용자 승인 ① · 10-9) — 엑셀 목록을 기본으로 깔지 않음:
연결된 사본이 부른 자재 키(`B09_Estimation_Linked` — 143-1 「연결된 사본」 원천 · 없으면 빈 표)
+ B08 총괄자재수량집계표 자재(M01 짝 없음 → 출처 빈칸 · 적용 단가는 사용자 입력)
· 수량 = 합계 열 · 채집석 공제 줄 거름 · 키가 MT 코드(맞춤 없는 사본 자재)면
사본 자원 줄에 수량만 얹음 · 파형강관 = 관급 기본
"""
from __future__ import annotations
import json
from decimal import Decimal
from typing import Any
from B08_Quantity.B08_Quantity_Material_Calc import DEDUCT_KEY
from common_util.common_util_time import utc_now_text
from M01_MasterData import M01_MasterData_Store as m01
#: 자재품목 값 열 → 대비표 출처 칸 · 대비표 「관급」 = M01 「조달청」 칸(830-2)
MT_SOURCES = ("관급", "물가자료", "유통물가", "물가정보", "거래가격")
MT_COLUMN = {"관급": "조달청"}
def _f(value: Any) -> float | None:
if isinstance(value, Decimal):
return float(value)
if isinstance(value, bool) or not isinstance(value, (int, float)):
return None
return float(value)
def _master():
return m01.loaded()[1]
def basis(frozen: dict[str, dict] | None = None):
"""값 원천 — 사본 단가 기준 시점 줄(795-4 · `Linked.frozen_rows`) 먼저 · 없는 키 = 지금 M01."""
if not frozen:
return _master()
from M02_MasterTemplete import M02_Sangeun_Copy as skc
return skc.Copy(_master(), skc._rows({"로직": list(frozen.values())}))
def now_keys(frozen: dict[str, dict] | None, keys: list[str]) -> list[str]:
"""사본 기준 시점에 없는 자원(795-4) — 지금 M01 값으로 넣은 키 · `frozen` None = 안 셈."""
return [] if frozen is None else [k for k in keys if k not in frozen]
def _row(master, key: str) -> dict[str, Any] | None:
return master.index.get(key[:2], {}).get(key)
def material_input(row: dict[str, Any]) -> dict[str, Any]:
"""자재품목(MT) · 유가(MO) 줄 → 대비표 입력 줄."""
if row["키"].startswith("MO"):
prices = {"유가": _f(row.get("값"))}
pages = {"유가": row.get("규격") or None}
origin = "유가"
spec = f"{row.get('상세구분') or ''} {row.get('규격') or ''}".strip()
unit = (row.get("단위") or "").replace("원/", "")
else:
prices = {s: _f(row.get(MT_COLUMN.get(s, s))) for s in MT_SOURCES}
pages = {s: (row.get("면수") or {}).get(s) for s in MT_SOURCES if s != "관급"}
origin = "M01"
spec = row.get("규격") or ""
unit = row.get("단위") or ""
return {
"key": row["키"],
"name": row.get("이름") or "",
"spec": spec,
"unit": unit,
"origin": origin,
"prices": prices,
"pages": pages,
"source": row.get("출처") or "",
}
def demand(master, keys: list[str]) -> dict[str, Any]:
"""수요 키 → 자재 줄 차례 · 찾지 못한 키(841-1 노무 · 기계 = 사본 엔진 값 · 책 밖)."""
materials: list[str] = []
missing: list[str] = []
for key in keys:
if not key.startswith(("MT", "MO")):
continue
if _row(master, key) is None:
missing.append(key)
elif key not in materials:
materials.append(key)
return {"materials": materials, "missing": missing}
def _convert(master, key: str) -> dict[str, Any]:
return material_input(_row(master, key))
def refresh_book(
keys: list[str], book: dict[str, Any] | None = None, frozen: dict[str, dict] | None = None
) -> dict[str, Any]:
"""[단가 갱신] — 수요 키 전부를 지금 M01 값으로 새로 굳힘 ·
M01 원본이 지워진 키(296) = 옛 책 줄 그대로(지우지도 바꾸지도 않음) ·
`frozen` = 첫 굳힘(795-4) — 사본 단가 기준 시점 값 · 없는 자원 = 지금 값(`now_keys`)."""
master = basis(frozen)
want = demand(master, keys)
rows = {key: _convert(master, key) for key in want["materials"]}
old = (book or {}).get("rows") or {}
rows.update({key: old[key] for key in want["missing"] if key in old})
return {
"made": utc_now_text(),
"rows": rows,
"labor_edition": labor_editions(keys), # 703-3 미리 보기 「노임 판 전 → 후」
"now_keys": now_keys(frozen, list(rows)),
}
def labor_editions(keys: list[str]) -> dict[str, str]:
"""노임 판(703-3) — 수요 노무 줄 구분(건설업 · 산림 …)마다 M01 `인력.json` 그 구분 `판`."""
files, master = m01.loaded()
groups = (files.get("인력.json") or {}).get("구분") or {}
out: dict[str, str] = {}
for key in [k for k in keys if k.startswith("LB") and _row(master, k)]:
kind = str((_row(master, key) or {}).get("구분") or "")
if (groups.get(kind) or {}).get("판"):
out[kind] = str(groups[kind]["판"])
return out
def book_diff(old: dict[str, Any] | None, new: dict[str, Any]) -> dict[str, int | bool]:
"""두 책 견줌(저장 꼴 JSON) — 새 줄 · 바뀐 줄 수(203-1 · 703-3 · 자재 줄만)."""
new = json.loads(json.dumps(new, ensure_ascii=False))
rows = (old or {}).get("rows") or {}
return {
"new": sum(key not in rows for key in new["rows"]),
"changed": sum(key in rows and rows[key] != row for key, row in new["rows"].items()),
}
def book_stale(book: dict[str, Any] | None, keys: list[str]) -> str | None:
"""203-1 [업데이트] 배지 — 지금 M01 로 새로 굳힌 책(`refresh_book`)과 견줌 · 다른 것 한 줄(같으면 None)."""
if book is None:
return "단가 아직 안 굳힘"
# 저장 꼴(JSON)로 견줌 · M01 원본이 지워진 줄 = 옛 줄 그대로(낡음 까닭에서 빠짐 · 296)
diff = book_diff(book, refresh_book(keys, book))
parts = [
f"새 단가 줄 {diff['new']}" if diff["new"] else "",
f"M01 에서 바뀐 단가 {diff['changed']}" if diff["changed"] else "",
]
return " · ".join(p for p in parts if p) or None
def extend_book(
book: dict[str, Any] | None, keys: list[str], frozen: dict[str, dict] | None = None
) -> dict[str, Any]:
"""[저장] — 책에 없는 새 줄만 더함(있는 줄 · 계수 · 환율은 안 바꿈) · 책이 없으면 새로 굳힘 ·
새 줄 값 = 사본 단가 기준 시점(795-4 · `frozen`) · 없는 자원만 지금 M01(`now_keys`)."""
if book is None:
return refresh_book(keys, None, frozen or {})
master = basis(frozen)
want = demand(master, keys)
rows = dict(book["rows"])
added = [key for key in want["materials"] if key not in rows]
rows.update({key: _convert(master, key) for key in added})
late = [*(book.get("now_keys") or []), *now_keys(frozen or {}, added)]
return {**book, "rows": rows, "now_keys": list(dict.fromkeys(late))}
def b08_material_rows(b08_sheets: dict[str, Any] | None) -> list[dict[str, Any]]:
"""B08 총괄자재수량집계표 줄(수량 있는 줄만) → 자재 입력 줄(M01 짝 없음 · 출처 빈칸)."""
rows = ((b08_sheets or {}).get("material") or {}).get("rows") or []
out = []
for row in rows:
if row.get("amount") is None or row["key"] == DEDUCT_KEY:
continue
out.append(
{
"key": f"b08:{row['key']}",
"name": row.get("group") or "",
"spec": row.get("spec") or "",
"unit": row.get("unit") or "",
"origin": "B08",
"prices": {},
"pages": {},
"source": "B08 총괄자재수량집계표",
"qty": row.get("amount"),
# 파형강관 = 관급(B08 할증 0 과 같은 뜻) — 사용자가 관급 · 별도 칸으로 바꿈
**({"supply": "관급"} if str(row["key"]).startswith("pipe_") else {}),
}
)
return out
def build_inputs(
book: dict[str, Any] | None,
keys: list[str],
b08_sheets: dict[str, Any] | None = None,
frozen: dict[str, dict] | None = None,
) -> dict[str, Any]:
"""표 셈 입력 — 줄 = 수요 · 값 = 굳힌 책(없는 줄 = [저장] 때 넣을 값 · `fresh` 표시 ·
795-4 사본 단가 기준 시점 `frozen` · 그것도 없으면 지금 M01 · `now` 표시) ·
`now` = 사본 기준 시점에 없는 자원 키(책 `now_keys` + 안 굳힌 줄)."""
master = basis(frozen)
want = demand(master, keys)
saved = (book or {}).get("rows") or {}
late = [k for k in (book or {}).get("now_keys") or [] if k in saved]
def pick(key: str) -> dict[str, Any]:
if key in saved:
return saved[key]
if frozen is not None and key not in frozen:
late.append(key)
return {**_convert(master, key), "fresh": True}
materials = [pick(k) for k in want["materials"]]
at = {row["key"]: i for i, row in enumerate(materials)}
for row in b08_material_rows(b08_sheets):
same = at.get(row["key"].removeprefix("b08:"))
if same is None:
materials.append(row)
else: # 같은 MT 코드 = 같은 자재 — 두 줄 안 만들고 수량만
materials[same] = {**materials[same], "qty": row["qty"]}
return {
"materials": materials,
"missing": want["missing"],
"now": list(dict.fromkeys(late)), # 위 줄들 pick 뒤 — 사전 차례대로 셈
}