- 단가 책 = 자재만(자재단가대비표 · 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>
235 lines
10 KiB
Python
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 뒤 — 사전 차례대로 셈
|
|
}
|