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

201 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""B09 원가 바탕 단가 셈(PLAN 144) — 화면 없음 · 저장 안 함 · 파일 · M01 안 읽음.
⚠ 짝 = `B09_Estimation_Calc_Base.ts`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 ·
거울 시험 `resources/tester/test_144_b09_base_calc.py`.
조작 중은 화면 짝이 바로 셈 · [저장] 때 서버가 이 파일로 정본을 다시 셈.
실무 엑셀(기번3 공통 xlsx) 탭 꼴 · 끝수 = 엑셀 자리 그대로:
노무비목록표 · 중기목록표 = 대가 · 산근 줄 엔진 단가(사본 값 · 서버가 `inputs.labor` ·
`inputs.machines` 로 넣어 줌 · `B09_Estimation_Supply.price_lists`) 나열 — 다시 안 셈(841-1)
경비목록표 · 중기사용료 · 환율및기초자료 = 걷음(841-1 · B09 가 기계 시간당 · 운전사 노임을
다시 셈하던 곳 · 값 = M02 단가산출 그대로) — 빈 표
자재단가대비표 = 출처 단가 중 최저(0 · 빈칸 뺌 · 견적 포함 = 엑셀 MIN) 자동 ·
적용 차례 = 고른 출처(pick · 견적이 높아도) › 사용자 적용 단가 › 최저
· 견적 = 사용자 손입력(quote · quote_page · 견적서 파일은 `B09_Estimation_Router_Quotes`)
· 관급 · 별도계상 = 사용자 표시(없으면 입력 줄 기본 · 로직 자재 = M02 자재표시 · B08 파형강관 = 관급)
→ 재료비목록표 단가 = 적용 단가
재료비목록표 = 수량 × 적용 단가 = 총액 ROUND(·, 0)
(엑셀 재료비수량금액집계표 G 열 자리 · 수량 · 단가 하나라도 빈칸이면 빈칸) · 수량 =
입력 `material_qty`(183-1 · 서버 1차 셈 역전개 총수량 = 재료비수량금액집계표 수량 ·
없는 키 빈칸) ·
`material_qty` 가 없으면 입력 줄 수량(B08 총괄 합계 열)
빈칸 규칙 = 값을 낸 원천이 없으면 None(0 을 채우지 않음) · 그 칸을 쓰는 합도 None ·
「연결 없음」 목록(`gaps`)에 탭 · 줄 키 · 까닭을 남김(목차 탭이 이동 링크로 씀).
"""
from __future__ import annotations
import math
from typing import Any
#: 떠도는 끝수(부동소수) — 이만큼은 자리 올림 · 버림에서 무시
EPS = 1e-7
#: 자재단가대비표 출처 칸 차례 — 엑셀 조달 · 물가정보 · 물가자료(같은 값이면 앞 칸)
# ↔ M01 재료_자재품목 값 열 · 유가 = 오피넷 · 견적 = 사용자 손입력(엑셀 M/N 열)
SOURCES = ("관급", "물가정보", "물가자료", "유통물가", "거래가격", "유가", "견적")
#: 자재 줄 사용자 표시
SUPPLY = ("관급", "별도")
_PARTS = ("labor", "material", "expense")
def _scaled(value: float, places: int, fn) -> float:
a = abs(value)
if places >= 0:
scale = 10**places
out = fn(a * scale) / scale
else:
unit = 10 ** (-places)
out = fn(a / unit) * unit
return -out if value < 0 else out
def rounddown(value: float, places: int = 0) -> float:
"""엑셀 ROUNDDOWN — 0 쪽으로 버림."""
return _scaled(value, places, lambda x: math.floor(x + EPS))
def roundup(value: float, places: int = 0) -> float:
"""엑셀 ROUNDUP — 0 에서 먼 쪽으로 올림."""
return _scaled(value, places, lambda x: math.ceil(x - EPS))
def round_half(value: float, places: int = 0) -> float:
"""엑셀 ROUND — 반 올림(0 에서 먼 쪽)."""
return _scaled(value, places, lambda x: math.floor(x + 0.5 + EPS))
def num(value: Any) -> float | None:
"""수 · 빈칸 — 수가 아니면 None."""
if isinstance(value, bool) or not isinstance(value, (int, float)):
return None
return float(value) if math.isfinite(value) else None
def total(values: list[float | None]) -> float | None:
"""합 — 하나라도 빈칸이면 빈칸(연결 안 된 값은 비움)."""
out = 0.0
for v in values:
if v is None:
return None
out += v
return out
def gap(tab: str, key: str, name: str, why: str) -> dict[str, str]:
return {"tab": tab, "key": key, "name": name, "why": why}
def labor_sheet(labor: list[dict[str, Any]], gaps: list) -> list[dict[str, Any]]:
rows = []
for no, row in enumerate(labor, 1):
price = num(row.get("price"))
if price is None:
gaps.append(gap("labor", row["key"], row["name"], "노임 단가 없음"))
rows.append(
{
**_head(row, no),
"code": row.get("code"),
"unit": row.get("unit") or "인",
"price": price,
}
)
return rows
def _head(row: dict[str, Any], no: int) -> dict[str, Any]:
return {
"no": no,
"key": row["key"],
"name": row.get("name") or "",
"spec": row.get("spec") or "",
"source": row.get("source") or "",
}
def lowest(prices: dict[str, Any]) -> tuple[float | None, str | None]:
"""출처 단가 중 최저 — 0 · 빈칸 뺌 · 같으면 앞 칸."""
best: tuple[float | None, str | None] = (None, None)
for source in SOURCES:
v = num(prices.get(source))
if v is not None and v > 0 and (best[0] is None or v < best[0]):
best = (v, source)
return best
def compare_sheet(
materials: list[dict[str, Any]], user: dict[str, Any], gaps: list
) -> list[dict[str, Any]]:
"""자재단가대비표 — 출처 · 최저 · 적용(사용자 값이 앞섬) · 관급 · 별도 표시."""
rows = []
for no, m in enumerate(materials, 1):
mine = user.get(m["key"]) or {}
prices = {**(m.get("prices") or {}), "견적": mine.get("quote")}
pages = {**(m.get("pages") or {}), "견적": mine.get("quote_page")}
low, low_from = lowest(prices)
if mine.get("pick") in SOURCES:
applied, applied_from = num(prices.get(mine["pick"])), mine["pick"]
elif "applied" in mine:
applied, applied_from = num(mine["applied"]), "사용자"
else:
applied, applied_from = low, low_from
if applied is None:
gaps.append(gap("compare", m["key"], m["name"], "적용 단가 없음(출처 없음)"))
supply = next((v for v in (mine.get("supply"), m.get("supply")) if v in SUPPLY), None)
if mine.get("supply") == "사급": # 841-1 사용자가 기본 표시(M02 자재표시)를 끔
supply = None
rows.append(
{
**_head(m, no),
"unit": m.get("unit") or "",
"origin": m.get("origin") or "",
"prices": {s: num(prices.get(s)) for s in SOURCES},
"pages": {s: pages.get(s) for s in SOURCES},
"lowest": low,
"lowest_from": low_from,
"applied": applied,
"applied_from": applied_from if applied is not None else None,
"pick": mine.get("pick") if mine.get("pick") in SOURCES else None,
"supply": supply,
"qty": num(m.get("qty")),
}
)
return rows
def material_sheet(
compare: list[dict[str, Any]], qty_of: dict[str, Any] | None = None
) -> list[dict[str, Any]]:
"""재료비목록표 — 단가 = 대비표 적용 단가 · 수량 = `qty_of`(183-1) ?? 줄 수량 ·
총액 = ROUND(수량 × 단가, 0)."""
keep = ("no", "key", "name", "spec", "unit", "supply", "origin")
rows = []
for row in compare:
price = row["applied"]
q = row["qty"] if qty_of is None else num(qty_of.get(row["key"]))
amount = None if price is None or q is None else round_half(q * price, 0)
rows.append({**{k: row[k] for k in keep}, "qty": q, "price": price, "amount": amount})
return rows
def compute_base(inputs: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]:
"""144 탭 + 「연결 없음」 목록 — 841-1 경비목록표 · 중기사용료 · 환율및기초자료 = 빈 표."""
gaps: list[dict[str, str]] = []
compare = compare_sheet(inputs.get("materials") or [], user.get("materials") or {}, gaps)
machine = [
{**_head(m, no), "unit": "시간", **{p: num(m.get(p)) for p in _PARTS},
"sum": total([num(m.get(p)) for p in _PARTS])}
for no, m in enumerate(inputs.get("machines") or [], 1)
] # fmt: skip
return {
"labor": labor_sheet(inputs.get("labor") or [], gaps),
"expense": [],
"basis": {"fx": [], "drivers": []},
"compare": compare,
"material": material_sheet(compare, inputs.get("material_qty")),
"machine_blocks": [],
"machine": machine,
"gaps": gaps,
}