Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
256 lines
9.2 KiB
Python
256 lines
9.2 KiB
Python
"""B09 원가 단가 한 벌 · 수요 모음(PLAN 144) — 서버 단독(M01 을 읽는 곳은 여기 하나).
|
||
|
||
원가 단가 = 프로젝트 한 벌 — 원가산출에서 M01 단가를 한 번 복사해 굳힘(`price_book.json`) ·
|
||
[단가 갱신] 때만 새로 · 대가 · 산근도 이 단가로 셈(한 직종 · 한 자재 = 한 단가).
|
||
|
||
줄 = 수요 따라(사용자 승인 ① · 10-9) — 엑셀 목록을 기본으로 깔지 않음:
|
||
연결된 사본이 부른 자원 키(`B09_Estimation_Linked` — 143-1 「연결된 사본」 원천 · 없으면 빈 표)
|
||
+ 기계가 부르는 운전사(조종원직종 → 인력 원문번호) · 연료(주연료종류 → 유가 「종류:전국평균」)
|
||
+ B08 총괄자재수량집계표 자재(M01 짝 없음 → 출처 빈칸 · 적용 단가는 사용자 입력)
|
||
운전사 시간 계수 = M01 CC000336(시간환산 · 상여계수 · 휴지계수 분자 · 분모 곱).
|
||
연료 지역 = 전국평균(M01 기계경비 로직 지역 첫 값) — 다른 지역 값은 자재 적용 단가를 고쳐 씀.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from M01_MasterData import M01_MasterData_Store as m01
|
||
|
||
#: 운전사 시간 노임 계수 표(건설품셈 공통 8-1-3 · 건협기술-2230)
|
||
DRIVER_TABLE = "CC000336"
|
||
DRIVER_PARTS = ("시간환산", "상여계수", "휴지계수")
|
||
#: 연료 지역 — M01 기계경비 로직 지역 고르기 첫 값
|
||
FUEL_REGION = "전국평균"
|
||
#: 자재품목 값 열 → 대비표 출처 칸
|
||
MT_SOURCES = ("관급", "물가자료", "유통물가", "물가정보", "거래가격")
|
||
|
||
|
||
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 _row(master, key: str) -> dict[str, Any] | None:
|
||
return master.index.get(key[:2], {}).get(key)
|
||
|
||
|
||
def labor_input(row: dict[str, Any]) -> dict[str, Any]:
|
||
return {
|
||
"key": row["키"],
|
||
"code": str(row.get("원문번호") or ""),
|
||
"name": row.get("이름") or "",
|
||
"spec": row.get("규격") or "",
|
||
"unit": "인",
|
||
"price": _f(row.get("값")),
|
||
"source": row.get("출처") or "",
|
||
}
|
||
|
||
|
||
def machine_input(row: dict[str, Any]) -> dict[str, Any]:
|
||
v = row.get("값") or {}
|
||
return {
|
||
"key": row["키"],
|
||
"code": str(row.get("원문번호") or ""),
|
||
"name": row.get("이름") or "",
|
||
"spec": row.get("규격") or "",
|
||
"price": _f(v.get("가격")),
|
||
"loss": _f(v.get("손료계수")),
|
||
"fuel": v.get("주연료종류") or None,
|
||
"fuel_lph": _f(v.get("주연료")),
|
||
"misc_pct": _f(v.get("잡재료율")),
|
||
"crew": _f(v.get("조종원")),
|
||
"crew_code": str(v["조종원직종"]) if v.get("조종원직종") else None,
|
||
"source": (row.get("출처") or {}).get("가격") or row.get("원문") or "",
|
||
}
|
||
|
||
|
||
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(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 driver_factor(master) -> dict[str, Any] | None:
|
||
table = _row(master, DRIVER_TABLE)
|
||
if not table:
|
||
return None
|
||
parts = {r.get("구분"): r for r in table.get("줄") or []}
|
||
if any(p not in parts for p in DRIVER_PARTS):
|
||
return None
|
||
top = bottom = 1
|
||
text = []
|
||
for p in DRIVER_PARTS:
|
||
top *= int(parts[p]["분자"])
|
||
bottom *= int(parts[p]["분모"])
|
||
text.append(f"{parts[p]['분자']}/{parts[p]['분모']}")
|
||
return {"num": top, "den": bottom, "text": " × ".join(text)}
|
||
|
||
|
||
def fx_inputs(master) -> list[dict[str, Any]]:
|
||
return [
|
||
{"key": key, "name": row.get("이름") or "", "unit": row.get("단위") or "",
|
||
"value": _f(row.get("값")), "date": row.get("규격") or ""}
|
||
for key, row in sorted(master.index.get("FX", {}).items())
|
||
] # fmt: skip
|
||
|
||
|
||
def _fuel_row(master, kind: str) -> dict[str, Any] | None:
|
||
return master.numbers.get("MO", {}).get(f"{kind}:{FUEL_REGION}")
|
||
|
||
|
||
def demand(master, keys: list[str]) -> dict[str, Any]:
|
||
"""수요 키 → 줄 차례(노무 · 기계 · 자재) + 연료 짝 · 찾지 못한 키."""
|
||
labor: list[str] = []
|
||
machines: list[str] = []
|
||
materials: list[str] = []
|
||
missing: list[str] = []
|
||
fuel_keys: dict[str, str] = {}
|
||
|
||
def add(bucket: list[str], key: str) -> None:
|
||
if key not in bucket:
|
||
bucket.append(key)
|
||
|
||
for key in keys:
|
||
if _row(master, key) is None:
|
||
missing.append(key)
|
||
elif key.startswith("LB"):
|
||
add(labor, key)
|
||
elif key.startswith("EQ"):
|
||
add(machines, key)
|
||
elif key.startswith(("MT", "MO")):
|
||
add(materials, key)
|
||
for key in machines:
|
||
v = _row(master, key).get("값") or {}
|
||
code = v.get("조종원직종")
|
||
driver = master.numbers.get("LB", {}).get(str(code)) if code and v.get("조종원") else None
|
||
if driver:
|
||
add(labor, driver["키"])
|
||
kind = v.get("주연료종류")
|
||
fuel = _fuel_row(master, kind) if kind and v.get("주연료") else None
|
||
if fuel:
|
||
add(materials, fuel["키"])
|
||
fuel_keys[kind] = fuel["키"]
|
||
return {
|
||
"labor": labor,
|
||
"machines": machines,
|
||
"materials": materials,
|
||
"fuel_keys": fuel_keys,
|
||
"missing": missing,
|
||
}
|
||
|
||
|
||
def _convert(master, key: str) -> dict[str, Any]:
|
||
row = _row(master, key)
|
||
if key.startswith("LB"):
|
||
return labor_input(row)
|
||
if key.startswith("EQ"):
|
||
return machine_input(row)
|
||
return material_input(row)
|
||
|
||
|
||
def refresh_book(keys: list[str]) -> dict[str, Any]:
|
||
"""[단가 갱신] — 수요 키 전부를 지금 M01 값으로 새로 굳힘."""
|
||
master = _master()
|
||
want = demand(master, keys)
|
||
rows = {
|
||
key: _convert(master, key)
|
||
for key in [*want["labor"], *want["machines"], *want["materials"]]
|
||
}
|
||
return {
|
||
"made": datetime.now().isoformat(timespec="seconds"),
|
||
"rows": rows,
|
||
"driver_factor": driver_factor(master),
|
||
"fx": fx_inputs(master),
|
||
}
|
||
|
||
|
||
def extend_book(book: dict[str, Any] | None, keys: list[str]) -> dict[str, Any]:
|
||
"""[저장] — 책에 없는 새 줄만 더함(있는 줄 · 계수 · 환율은 안 바꿈) · 책이 없으면 새로 굳힘."""
|
||
if book is None:
|
||
return refresh_book(keys)
|
||
master = _master()
|
||
want = demand(master, keys)
|
||
rows = dict(book["rows"])
|
||
for key in [*want["labor"], *want["machines"], *want["materials"]]:
|
||
if key not in rows:
|
||
rows[key] = _convert(master, key)
|
||
return {**book, "rows": rows}
|
||
|
||
|
||
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:
|
||
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"),
|
||
}
|
||
)
|
||
return out
|
||
|
||
|
||
def build_inputs(
|
||
book: dict[str, Any] | None, keys: list[str], b08_sheets: dict[str, Any] | None = None
|
||
) -> dict[str, Any]:
|
||
"""표 셈 입력 — 줄 = 수요 · 값 = 굳힌 책(없는 줄은 지금 M01 값 · `fresh` 표시)."""
|
||
master = _master()
|
||
want = demand(master, keys)
|
||
saved = (book or {}).get("rows") or {}
|
||
|
||
def pick(key: str) -> dict[str, Any]:
|
||
if key in saved:
|
||
return saved[key]
|
||
return {**_convert(master, key), "fresh": True}
|
||
|
||
factor = (book or {}).get("driver_factor") if book else driver_factor(master)
|
||
return {
|
||
"labor": [pick(k) for k in want["labor"]],
|
||
"machines": [pick(k) for k in want["machines"]],
|
||
"materials": [pick(k) for k in want["materials"]] + b08_material_rows(b08_sheets),
|
||
"fuel_keys": want["fuel_keys"],
|
||
"driver_factor": factor,
|
||
"fx": (book or {}).get("fx") if book else fx_inputs(master),
|
||
"missing": want["missing"],
|
||
}
|