Files
Aislo/B09_Estimation/draft/B09_Draft_Schedule_Calc.py
T
eomsangdonandClaude Opus 5.5 f87831533c feat(b09): 공기 초안 — 메인 탭 공기 · 공기집계 · 공기산출 (draft 폴더 · 252)
설계내역서 항목 줄 역전개(단위당 장비 시간 · 인) → 작업일수 · 착공 연월부터 달마다 작업가능일 채움 · 공휴일 표 2024~2040 · 본 코드 훅은 draft 없으면 건너뜀

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
2026-10-11 01:51:32 +09:00

258 lines
10 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 252-1 · 사용자 161번 · 초안 — draft 폴더째 지우면 사라짐).
짝 = `B09_Draft_Schedule_Calc.ts`(같은 식 · 거울 시험 `test_252_1_b09_schedule.py`).
근거 = 실무 설계서 공기 탭 식(거창 25 · 울진 24 · 울진소광 25) — 값 베끼기가 아니라 식만:
공기산출 설계내역서 항목 줄마다 작업일수 = ROUNDUP(시공량 × 반영률 ÷ 일작업량, 1)
일작업량 = 장비 — 일 작업시간 × 대수 ÷ 단위당 주 장비 시간
(= 일 작업시간 × Q · Q = 1 ÷ 단위당 시간)
인력 — 투입 인원 ÷ 단위당 인(장비 없는 줄)
입력 — 사용자 값(연결 없는 줄 · 덮어쓰기)
단위당 자원 = 서버 역전개(`…_Source.unit_resources` · 호표 구성 1 단위)
반영률 = 다른 공종과 겹침(100 = 혼자 · 50 = 반 겹침 · 0 = 다 겹침) · 운반공 기본 0
소계 = ROUND(Σ 작업일수 + 기타, 0) · 총작업일수 = ROUND(준비 + 소계 + 정리, 0)
공기집계 착공 연월부터 달마다 — 달력일수 · 기후여건 A = ROUNDUP(강수일 + 혹서일, 1) ·
법정공휴일 B = 일요일 + 공휴일(대체 · 선거일 포함 · 사용자 덮개) ·
중복 C = A × B ÷ 달력 ·
비작업일수 = ROUNDDOWN(A + B − C, 0) · 작업가능일수 = 달력 − 비작업
준비 → 작업 → 정리 차례로 작업가능일수를 채움 · 다 쓴 달 = 그 달 비작업 전부 ·
마지막 달(덜 쓴 달) = ROUNDUP(비작업 × 쓴 날 ÷ 작업가능일, 0)
공사기간 = Σ(쓴 날 + 비작업) · 준공 예정일 = 착공(그 달 1일) + 공사기간 − 1
"""
from __future__ import annotations
import datetime
from typing import Any
from B09_Estimation.B09_Estimation_Calc_Base import num, round_half, rounddown, roundup
HOURS = 8
CREW = 10
PREP_DOCS = 2
PREP_SETUP = 2
TIDY = 4
#: 공사 아닌 공종(자재대 · 관급자재대) · 기본 반영률 0(운반 = 다른 공종과 겹침)
SKIP_TOP = ("4", "G")
OVERLAP_TOP = ("3",)
MAX_MONTHS = 120
def _top(section: Any) -> str:
return str(section or "").split(".")[0]
def _cfg(user: dict[str, Any], key: str, default: float) -> float:
value = num(user.get(key))
return default if value is None or value < 0 else value
def _work_row(item: dict[str, Any], unit: dict[str, Any] | None, cfg: dict, hours: float) -> dict:
qty = num(item.get("qty")) or 0.0
rate = num(cfg.get("rate"))
if rate is None:
rate = 0.0 if _top(item.get("section")) in OVERLAP_TOP else 100.0
daily_in = num(cfg.get("daily"))
machine = (unit or {}).get("machine")
labor = num((unit or {}).get("labor"))
out = {
"kind": "item",
"key": item["key"],
"name": item.get("name") or "",
"spec": item.get("spec") or "",
"unit": item.get("unit") or "",
"lv": item.get("level") or 1,
"qty": qty,
"rate": rate,
"basis": "",
"who": "",
"per": None,
"put": None,
"q": None,
"daily": None,
"daily_in": daily_in,
"days": None,
}
if daily_in is not None and daily_in > 0:
out.update(basis="입력", daily=daily_in)
need = qty * rate / 100 / daily_in
elif machine and (num(machine.get("hr")) or 0) > 0:
hr = num(machine["hr"])
units = num(cfg.get("units")) or 1.0
out.update(basis="장비", who=machine.get("name") or "", per=hr, put=units)
out.update(q=1 / hr, daily=hours * units / hr)
need = qty * rate / 100 * hr / (hours * units)
elif labor is not None and labor > 0:
crew = num(cfg.get("crew")) or float(CREW)
out.update(basis="인력", who="인력", per=labor, put=crew, daily=crew / labor)
need = qty * rate / 100 * labor / crew
else:
out["basis"] = "연결 없음"
out["days"] = 0.0 if rate == 0 else None # 다 겹침 = 일작업량 없어도 0
return out
out["days"] = roundup(need, 1)
return out
def work_table(src: dict[str, Any], bill_rows: list[dict[str, Any]], user: dict) -> dict:
"""공기산출 — 설계내역서 차례(묶음 줄 = 그 아래 셀 줄이 있을 때만) · 준비 · 기타 · 정리."""
hours = _cfg(user, "hours", HOURS) or HOURS
units = src.get("units") or {}
cfgs = user.get("rows") or {}
rows: list[dict[str, Any]] = []
pending: list[list[Any]] = [] # [줄, 넣음]
for row in bill_rows:
level = row.get("level") or 1
if row.get("kind") != "item":
pending = [p for p in pending if (p[0].get("level") or 1) < level]
pending.append([row, False])
continue
key = str(row.get("key") or "")
qty = num(row.get("qty"))
if (
_top(row.get("section")) in SKIP_TOP
or key.startswith("mat:")
or key == "fee"
or qty is None
or qty <= 0
):
continue
for p in pending:
if not p[1]:
p[1] = True
head = p[0]
rows.append(
{
"kind": "head",
"key": head["key"],
"name": head.get("name") or "",
"lv": head.get("level") or 1,
} # fmt: skip
)
rows.append(_work_row(row, units.get(key), cfgs.get(key) or {}, hours))
docs = _cfg(user, "prep_docs", PREP_DOCS)
setup = _cfg(user, "prep_setup", PREP_SETUP)
etc = _cfg(user, "etc", 0)
tidy = _cfg(user, "tidy", TIDY)
days = sum(r["days"] or 0 for r in rows if r["kind"] == "item")
subtotal = round_half(days + etc, 0)
missing = sum(1 for r in rows if r["kind"] == "item" and r["days"] is None)
return {
"hours": hours,
"rows": rows,
"prep_docs": docs,
"prep_setup": setup,
"prep": docs + setup,
"etc": etc,
"subtotal": subtotal,
"tidy": tidy,
"total": round_half(docs + setup + subtotal + tidy, 0),
"missing": missing,
}
def _month_days(year: int, month: int) -> int:
nxt = datetime.date(year + month // 12, month % 12 + 1, 1)
return (nxt - datetime.date(year, month, 1)).days
def _holidays(src: dict[str, Any], year: int, month: int) -> tuple[int, list[str]]:
"""그 달 법정공휴일 수(일요일 + 공휴일 겹침 한 번) · 공휴일 이름(「일 이름」)."""
head = f"{year:04d}-{month:02d}-"
named = {
d: n for d, n in (src.get("holidays") or {}).get(str(year)) or [] if d.startswith(head)
}
count = 0
for day in range(1, _month_days(year, month) + 1):
iso = f"{head}{day:02d}"
if datetime.date(year, month, day).weekday() == 6 or iso in named:
count += 1
return count, [f"{int(d[-2:])}일 {n}" for d, n in sorted(named.items())]
def _climate(src: dict[str, Any], user: dict[str, Any], kind: str, month: int) -> float:
mine = ((user.get("climate") or {}).get(kind) or [None] * 12)[month - 1]
if num(mine) is not None:
return num(mine)
return num(((src.get("climate") or {}).get(kind) or [0] * 12)[month - 1]) or 0.0
def month_table(src: dict[str, Any], user: dict[str, Any], parts: list[float]) -> dict[str, Any]:
"""공기집계 — parts = [준비, 작업, 정리] 일수 · 착공 연월부터 채움."""
start = {**(src.get("start") or {}), **(user.get("start") or {})}
year, month = int(start.get("year") or 2000), int(start.get("month") or 1)
left = [max(p, 0) for p in parts]
months: list[dict[str, Any]] = []
total = 0
while sum(left) > 0 or not months:
if len(months) >= MAX_MONTHS:
break
days = _month_days(year, month)
rain = _climate(src, user, "rain", month)
heat = _climate(src, user, "heat", month)
a = roundup(rain + heat, 1)
auto, names = _holidays(src, year, month)
mine = num((user.get("holiday") or {}).get(f"{year:04d}-{month:02d}"))
b = mine if mine is not None else auto
c = a * b / days
off = rounddown(a + b - c, 0)
avail = days - off
cap = max(avail, 0)
use = [0.0, 0.0, 0.0]
room = cap
for i in range(3):
take = min(room, left[i])
use[i] = take
left[i] -= take
room -= take
used = sum(use)
if cap <= 0:
off_used = days
elif used >= cap:
off_used = off
else:
off_used = roundup(off * used / cap, 0)
span = used + off_used
total += span
months.append(
{
"year": year,
"month": month,
"days": days,
"rain": rain,
"heat": heat,
"a": a,
"b": b,
"b_auto": auto,
"names": names,
"c": c,
"off": off,
"avail": avail,
"prep": use[0],
"work": use[1],
"tidy": use[2],
"used": used,
"off_used": off_used,
"span": span,
} # fmt: skip
)
year, month = (year + 1, 1) if month == 12 else (year, month + 1)
first = months[0]
begin = datetime.date(first["year"], first["month"], 1)
end = begin + datetime.timedelta(days=max(total, 1) - 1)
return {
"start": {"year": first["year"], "month": first["month"]},
"months": months,
"days": total,
"months_n": round_half(total / 30, 1),
"end": end.isoformat(),
}
def compute_schedule(src: dict[str, Any], bill_rows: list[dict[str, Any]], user: dict) -> dict:
"""공기 한 벌 — {work: 공기산출, calendar: 공기집계}."""
user = user or {}
work = work_table(src or {}, bill_rows or [], user)
parts = [work["prep"], work["subtotal"], work["tidy"]]
return {"work": work, "calendar": month_table(src or {}, user, parts)}