Files
Aislo/B09_Estimation/B09_Estimation_Calc_Base.py
T

321 lines
12 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) 탭 꼴 · 끝수 = 엑셀 자리 그대로:
노무비목록표 · 경비목록표 = 굳힌 단가 그대로(사용자 고침 없음 · [단가 갱신] 때만 새로)
환율및기초자료 = 환율 · 운전사 시간 노임 = ROUNDDOWN(일당 × 분자 ÷ 분모, 0)
(분자 · 분모 = M01 CC000336 1/8 × 16/12 × 25/20 — 서버가 넣어 줌)
자재단가대비표 = 출처 단가 중 최저(0 · 빈칸 뺌) 자동 · 사용자 적용 단가가 있으면 그 값
· 관급 · 별도계상 = 사용자 표시 → 재료비목록표 단가 = 적용 단가
중기사용료 = 손료(경비 · 기준가 천원 × 손료계수 ÷ 10,000) + 운전사(노무 · 인 × 시간 노임)
+ 연료(재료 · ℓ/hr × 적용 단가) + 잡품(재료 · 연료 금액 × % ÷ 100)
줄 = ROUNDDOWN(단가 × 수량, 1) · 비목 합 = ROUNDDOWN(Σ, 0) → 중기목록표
빈칸 규칙 = 값을 낸 원천이 없으면 None(0 을 채우지 않음) · 그 칸을 쓰는 합도 None ·
「연결 없음」 목록(`gaps`)에 탭 · 줄 키 · 까닭을 남김(목차 탭이 이동 링크로 씀).
"""
from __future__ import annotations
import math
from typing import Any
#: 떠도는 끝수(부동소수) — 이만큼은 자리 올림 · 버림에서 무시
EPS = 1e-7
#: 자재단가대비표 출처 칸 차례 — 엑셀 조달 · 물가정보 · 물가자료(같은 값이면 앞 칸)
# ↔ M01 재료_자재품목 값 열 · 유가 = 오피넷
SOURCES = ("관급", "물가정보", "물가자료", "유통물가", "거래가격", "유가")
#: 자재 줄 사용자 표시
SUPPLY = ("관급", "별도")
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 driver_hour(day: float | None, factor: dict[str, Any] | None) -> float | None:
"""운전사 시간 노임 = ROUNDDOWN(일당 × 분자 ÷ 분모, 0)."""
if day is None or not factor or not factor.get("den"):
return None
return rounddown(day * factor["num"] / factor["den"], 0)
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 expense_sheet(machines: list[dict[str, Any]], gaps: list) -> list[dict[str, Any]]:
"""경비목록표 — 기계 손료 기준가(천원)."""
rows = []
for no, m in enumerate(machines, 1):
price = num(m.get("price"))
if price is None:
gaps.append(gap("expense", m["key"], m["name"], "기계 기준가 없음"))
rows.append({**_head(m, no), "unit": "천원", "price": price})
return rows
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 {}
low, low_from = lowest(prices)
if "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 = mine.get("supply") if mine.get("supply") in SUPPLY else 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: (m.get("pages") or {}).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,
"supply": supply,
}
)
return rows
def material_sheet(compare: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""재료비목록표 — 단가 = 대비표 적용 단가."""
keep = ("no", "key", "name", "spec", "unit", "supply", "origin")
return [{**{k: row[k] for k in keep}, "price": row["applied"]} for row in compare]
def _line(name, spec, qty, unit, kind, price, ref, pct=False) -> dict[str, Any]:
if price is None or qty is None:
amount = None
elif pct:
amount = rounddown(price * qty / 100, 1)
else:
amount = rounddown(price * qty, 1)
return {
"name": name,
"spec": spec,
"qty": qty,
"unit": unit,
"kind": kind,
"price": price,
"amount": amount,
"ref": ref,
}
def machine_block(
m: dict[str, Any],
drivers: dict[str, dict[str, Any]],
fuels: dict[str, dict[str, Any]],
gaps: list,
) -> dict[str, Any]:
"""중기사용료 한 블록 — 손료 · 운전사 · 연료 · 잡품 줄 + 비목 합."""
lines = []
loss = num(m.get("loss"))
coef = loss / 10000 if loss is not None else None
price = num(m.get("price"))
lines.append(_line(m["name"], m.get("spec") or "", coef, "천원", "expense", price,
{"tab": "expense", "key": m["key"]})) # fmt: skip
crew = num(m.get("crew"))
if crew:
code = str(m.get("crew_code") or "")
driver = drivers.get(code) or {}
hour = driver.get("hour")
if hour is None:
gaps.append(gap("machine_blocks", m["key"], m["name"], "운전사 노임 없음"))
lines.append(_line(driver.get("name") or "운전사", "", crew, "인", "labor", hour,
{"tab": "basis", "key": code})) # fmt: skip
lph = num(m.get("fuel_lph"))
if lph:
kind = str(m.get("fuel") or "")
fuel = fuels.get(kind) or {}
fprice = fuel.get("applied")
if fprice is None:
gaps.append(gap("machine_blocks", m["key"], m["name"], f"연료({kind}) 단가 없음"))
fuel_line = _line(fuel.get("name") or kind, fuel.get("spec") or "", lph, "ℓ", "material",
fprice, {"tab": "compare", "key": fuel.get("key") or ""}) # fmt: skip
lines.append(fuel_line)
misc = num(m.get("misc_pct"))
if misc:
lines.append(_line("잡품", "주연료의 %", misc, "%", "material", fuel_line["amount"],
None, pct=True)) # fmt: skip
if price is None or coef is None:
gaps.append(gap("machine_blocks", m["key"], m["name"], "손료 기준가 · 계수 없음"))
sums: dict[str, float | None] = {}
for part in ("labor", "material", "expense"):
s = total([ln["amount"] for ln in lines if ln["kind"] == part])
sums[part] = None if s is None else rounddown(s, 0)
return {
"key": m["key"],
"name": m["name"],
"spec": m.get("spec") or "",
"unit": "시간",
"lines": lines,
"labor": sums["labor"],
"material": sums["material"],
"expense": sums["expense"],
"sum": total([sums["labor"], sums["material"], sums["expense"]]),
}
def basis_sheet(
inputs: dict[str, Any], labor_rows: list[dict[str, Any]]
) -> dict[str, list[dict[str, Any]]]:
"""환율및기초자료 — 환율 · 운전사 시간 노임(기계가 부르는 직종만)."""
factor = inputs.get("driver_factor")
by_code = {str(row.get("code")): row for row in labor_rows if row.get("code") is not None}
codes: list[str] = []
for m in inputs.get("machines") or []:
code = str(m.get("crew_code")) if m.get("crew_code") else None
if code and num(m.get("crew")) and code not in codes:
codes.append(code)
drivers = []
for code in codes:
row = by_code.get(code) or {}
day = row.get("price")
drivers.append(
{
"key": code,
"labor_key": row.get("key"),
"name": row.get("name") or f"직종 {code}",
"day": day,
"factor": (factor or {}).get("text") or "",
"hour": driver_hour(day, factor),
}
)
fx = [
{"key": f["key"], "name": f.get("name") or "", "unit": f.get("unit") or "",
"value": num(f.get("value")), "date": f.get("date") or ""}
for f in inputs.get("fx") or []
] # fmt: skip
return {"fx": fx, "drivers": drivers}
def compute_base(inputs: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]:
"""144 탭 일곱 + 「연결 없음」 목록."""
gaps: list[dict[str, str]] = []
labor_rows = labor_sheet(inputs.get("labor") or [], gaps)
expense = expense_sheet(inputs.get("machines") or [], gaps)
compare = compare_sheet(inputs.get("materials") or [], user.get("materials") or {}, gaps)
basis = basis_sheet(inputs, labor_rows)
drivers = {d["key"]: d for d in basis["drivers"]}
by_key = {row["key"]: row for row in compare}
fuels = {
kind: by_key[key] for kind, key in (inputs.get("fuel_keys") or {}).items() if key in by_key
}
blocks = [machine_block(m, drivers, fuels, gaps) for m in inputs.get("machines") or []]
machine = [
{
"no": no,
"key": b["key"],
"name": b["name"],
"spec": b["spec"],
"unit": b["unit"],
"sum": b["sum"],
"labor": b["labor"],
"material": b["material"],
"expense": b["expense"],
}
for no, b in enumerate(blocks, 1)
]
return {
"labor": labor_rows,
"expense": expense,
"basis": basis,
"compare": compare,
"material": material_sheet(compare),
"machine_blocks": blocks,
"machine": machine,
"gaps": gaps,
}