feat(B09): 264-2 공기 옵션 셈 짝 · 다듬기 · 시험
- 셈 짝(Py · TS) 새 옵션 — 착공일(그날부터 · A 날수 비례) · 기후 자료 고르기 · 쉬는 날 · 공휴일 빼기 · 투입 기본 · 반영률 기본 · 주 장비 기준(장비 시간 합) · 중복 C 끝수 · 마지막 달 비작업 - 원천 = 기후 셋(거창 25 · 울진 24 · 울진 소광 25 · 0에서 직접) · 단위당 장비 시간 합 - clean_schedule 새 칸(모르는 값 · 범위 밖 버림) - 시험 test_264_2 — 기본값 = 지금 결과 · 옵션마다 값 바뀜 · 거울 TS = Py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"""B09 공기 셈 짝(PLAN 252-1 · 사용자 161번 · 초안 — draft 폴더째 지우면 사라짐).
|
||||
|
||||
짝 = `B09_Draft_Schedule_Calc.ts`(같은 식 · 거울 시험 `test_252_1_b09_schedule.py`).
|
||||
짝 = `B09_Draft_Schedule_Calc.ts`(같은 식 · 거울 시험 `test_252_1_b09_schedule.py` · `test_264_2_b09_schedule_options.py`).
|
||||
근거 = 실무 설계서 공기 탭 식(거창 25 · 울진 24 · 울진소광 25) — 값 베끼기가 아니라 식만:
|
||||
|
||||
공기산출 설계내역서 항목 줄마다 작업일수 = ROUNDUP(시공량 × 반영률 ÷ 일작업량, 1)
|
||||
@@ -11,13 +11,19 @@
|
||||
단위당 자원 = 서버 역전개(`…_Source.unit_resources` · 호표 구성 1 단위)
|
||||
반영률 = 다른 공종과 겹침(100 = 혼자 · 50 = 반 겹침 · 0 = 다 겹침) · 운반공 기본 0
|
||||
소계 = ROUND(Σ 작업일수 + 기타, 0) · 총작업일수 = ROUND(준비 + 소계 + 정리, 0)
|
||||
공기집계 착공 연월부터 달마다 — 달력일수 · 기후여건 A = ROUNDUP(강수일 + 혹서일, 1) ·
|
||||
공기집계 착공일부터 달마다 — 달력일수 · 기후여건 A = ROUNDUP(강수일 + 혹서일, 1) ·
|
||||
법정공휴일 B = 일요일 + 공휴일(대체 · 선거일 포함 · 사용자 덮개) ·
|
||||
중복 C = A × B ÷ 달력 ·
|
||||
비작업일수 = ROUNDDOWN(A + B − C, 0) · 작업가능일수 = 달력 − 비작업
|
||||
준비 → 작업 → 정리 차례로 작업가능일수를 채움 · 다 쓴 달 = 그 달 비작업 전부 ·
|
||||
마지막 달(덜 쓴 달) = ROUNDUP(비작업 × 쓴 날 ÷ 작업가능일, 0)
|
||||
공사기간 = Σ(쓴 날 + 비작업) · 준공 예정일 = 착공(그 달 1일) + 공사기간 − 1
|
||||
공사기간 = Σ(쓴 날 + 비작업) · 준공 예정일 = 착공일 + 공사기간 − 1
|
||||
옵션(264 · user.schedule · 기본값 = 위 식 그대로) — 착공일(1일 아니면 첫 달 = 그날부터 ·
|
||||
A = 날수 비례) · 기후 자료(원천 climates 고르기 · 달마다 덮개가 이김) ·
|
||||
쉬는 날(일 · 토일) · 공휴일 빼기(대체 · 선거일 · 노동절 · 제헌절 — 이름으로 가름) ·
|
||||
투입 기본(장비 대 · 인력 인) · 반영률 기본(운반공 · 그 밖) ·
|
||||
주 장비 기준(가장 긴 장비 · 장비 시간 합) · 중복 C 끝수(그대로 · 소수 1자리 반올림) ·
|
||||
마지막 달 비작업(쓴 비율 올림 · 전부)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,12 +33,26 @@ 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(운반 = 다른 공종과 겹침)
|
||||
#: 사용자 값 기본(264 패널 · 표 칸 같은 값)
|
||||
DEFAULTS = {
|
||||
"hours": 8,
|
||||
"units": 1,
|
||||
"crew": 10,
|
||||
"prep_docs": 2,
|
||||
"prep_setup": 2,
|
||||
"tidy": 4,
|
||||
"etc": 0,
|
||||
"rate_haul": 0,
|
||||
"rate_base": 100,
|
||||
}
|
||||
#: 공휴일 빼기 갈래 — 이름에 든 글
|
||||
HOLIDAY_KINDS = {
|
||||
"substitute": ("대체",),
|
||||
"election": ("선거",),
|
||||
"labor": ("노동절", "근로자"),
|
||||
"constitution": ("제헌절",),
|
||||
}
|
||||
#: 공사 아닌 공종(자재대 · 관급자재대) · 기본 반영률 운반공(= 다른 공종과 겹침)
|
||||
SKIP_TOP = ("4", "G")
|
||||
OVERLAP_TOP = ("3",)
|
||||
MAX_MONTHS = 120
|
||||
@@ -42,18 +62,23 @@ def _top(section: Any) -> str:
|
||||
return str(section or "").split(".")[0]
|
||||
|
||||
|
||||
def _cfg(user: dict[str, Any], key: str, default: float) -> float:
|
||||
def _cfg(user: dict[str, Any], key: str) -> float:
|
||||
value = num(user.get(key))
|
||||
return default if value is None or value < 0 else value
|
||||
return DEFAULTS[key] 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:
|
||||
def _work_row(item: dict[str, Any], unit: dict[str, Any] | None, cfg: dict, base: dict) -> dict:
|
||||
hours = base["hours"]
|
||||
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
|
||||
rate = base["rate_haul"] if _top(item.get("section")) in OVERLAP_TOP else base["rate_base"]
|
||||
daily_in = num(cfg.get("daily"))
|
||||
machine = (unit or {}).get("machine")
|
||||
total = (unit or {}).get("machine_sum")
|
||||
if machine and base["sum"] and total and (num(total.get("hr")) or 0) > 0:
|
||||
many = f" 외 {total['n'] - 1}종" if (total.get("n") or 1) > 1 else ""
|
||||
machine = {**machine, "name": f"{machine.get('name') or ''}{many}", "hr": total["hr"]}
|
||||
labor = num((unit or {}).get("labor"))
|
||||
out = {
|
||||
"kind": "item",
|
||||
@@ -78,12 +103,12 @@ def _work_row(item: dict[str, Any], unit: dict[str, Any] | None, cfg: dict, hour
|
||||
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
|
||||
units = num(cfg.get("units")) or base["units"]
|
||||
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)
|
||||
crew = num(cfg.get("crew")) or base["crew"]
|
||||
out.update(basis="인력", who="인력", per=labor, put=crew, daily=crew / labor)
|
||||
need = qty * rate / 100 * labor / crew
|
||||
else:
|
||||
@@ -96,7 +121,15 @@ def _work_row(item: dict[str, Any], unit: dict[str, Any] | None, cfg: dict, hour
|
||||
|
||||
def work_table(src: dict[str, Any], bill_rows: list[dict[str, Any]], user: dict) -> dict:
|
||||
"""공기산출 — 설계내역서 차례(묶음 줄 = 그 아래 셀 줄이 있을 때만) · 준비 · 기타 · 정리."""
|
||||
hours = _cfg(user, "hours", HOURS) or HOURS
|
||||
hours = _cfg(user, "hours") or DEFAULTS["hours"]
|
||||
base = {
|
||||
"hours": hours,
|
||||
"units": _cfg(user, "units") or DEFAULTS["units"],
|
||||
"crew": _cfg(user, "crew") or DEFAULTS["crew"],
|
||||
"rate_haul": min(_cfg(user, "rate_haul"), 100),
|
||||
"rate_base": min(_cfg(user, "rate_base"), 100),
|
||||
"sum": user.get("machine_basis") == "sum",
|
||||
}
|
||||
units = src.get("units") or {}
|
||||
cfgs = user.get("rows") or {}
|
||||
rows: list[dict[str, Any]] = []
|
||||
@@ -129,11 +162,11 @@ def work_table(src: dict[str, Any], bill_rows: list[dict[str, Any]], user: dict)
|
||||
"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)
|
||||
rows.append(_work_row(row, units.get(key), cfgs.get(key) or {}, base))
|
||||
docs = _cfg(user, "prep_docs")
|
||||
setup = _cfg(user, "prep_setup")
|
||||
etc = _cfg(user, "etc")
|
||||
tidy = _cfg(user, "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)
|
||||
@@ -156,45 +189,70 @@ def _month_days(year: int, month: int) -> int:
|
||||
return (nxt - datetime.date(year, month, 1)).days
|
||||
|
||||
|
||||
def _holidays(src: dict[str, Any], year: int, month: int) -> tuple[int, list[str]]:
|
||||
"""그 달 법정공휴일 수(일요일 + 공휴일 겹침 한 번) · 공휴일 이름(「일 이름」)."""
|
||||
def _counted(name: str, off: list[str]) -> bool:
|
||||
"""공휴일 넣음 — 「;」 로 묶인 이름 중 하나라도 뺀 갈래가 아니면 넣음."""
|
||||
for part in name.split(";"):
|
||||
kinds = [k for k, words in HOLIDAY_KINDS.items() if any(w in part for w in words)]
|
||||
if not any(k in off for k in kinds):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _holidays(
|
||||
src: dict[str, Any], year: int, month: int, first: int, user: dict[str, Any]
|
||||
) -> tuple[int, list[str]]:
|
||||
"""그 달(first 일부터) 법정공휴일 수(쉬는 요일 + 공휴일 겹침 한 번) · 공휴일 이름(「일 이름」)."""
|
||||
head = f"{year:04d}-{month:02d}-"
|
||||
off = user.get("holiday_off") or []
|
||||
named = {
|
||||
d: n for d, n in (src.get("holidays") or {}).get(str(year)) or [] if d.startswith(head)
|
||||
d: n
|
||||
for d, n in (src.get("holidays") or {}).get(str(year)) or []
|
||||
if d.startswith(head) and int(d[-2:]) >= first and _counted(n, off)
|
||||
}
|
||||
rest = (5, 6) if user.get("rest") == "satsun" else (6,)
|
||||
count = 0
|
||||
for day in range(1, _month_days(year, month) + 1):
|
||||
for day in range(first, _month_days(year, month) + 1):
|
||||
iso = f"{head}{day:02d}"
|
||||
if datetime.date(year, month, day).weekday() == 6 or iso in named:
|
||||
if datetime.date(year, month, day).weekday() in rest or iso in named:
|
||||
count += 1
|
||||
return count, [f"{int(d[-2:])}일 {n}" for d, n in sorted(named.items())]
|
||||
|
||||
|
||||
def climate_base(src: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]:
|
||||
"""고른 기후 자료(원천 climates) · 없으면 원천 기본값."""
|
||||
return (src.get("climates") or {}).get(user.get("climate_set")) or src.get("climate") or {}
|
||||
|
||||
|
||||
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
|
||||
return num((climate_base(src, user).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 = [준비, 작업, 정리] 일수 · 착공 연월부터 채움."""
|
||||
"""공기집계 — parts = [준비, 작업, 정리] 일수 · 착공일부터 채움."""
|
||||
start = {**(src.get("start") or {}), **(user.get("start") or {})}
|
||||
year, month = int(start.get("year") or 2000), int(start.get("month") or 1)
|
||||
first = min(max(int(start.get("day") or 1), 1), _month_days(year, month))
|
||||
begin = datetime.date(year, month, first)
|
||||
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)
|
||||
full = _month_days(year, month)
|
||||
days = full - first + 1
|
||||
rain = _climate(src, user, "rain", month)
|
||||
heat = _climate(src, user, "heat", month)
|
||||
a = roundup(rain + heat, 1)
|
||||
auto, names = _holidays(src, year, month)
|
||||
a = roundup(rain + heat if first == 1 else (rain + heat) * days / full, 1)
|
||||
auto, names = _holidays(src, year, month, first, user)
|
||||
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
|
||||
if user.get("c_round") == "round1":
|
||||
c = round_half(c, 1)
|
||||
off = rounddown(a + b - c, 0)
|
||||
avail = days - off
|
||||
cap = max(avail, 0)
|
||||
@@ -208,7 +266,7 @@ def month_table(src: dict[str, Any], user: dict[str, Any], parts: list[float]) -
|
||||
used = sum(use)
|
||||
if cap <= 0:
|
||||
off_used = days
|
||||
elif used >= cap:
|
||||
elif used >= cap or user.get("last_off") == "all":
|
||||
off_used = off
|
||||
else:
|
||||
off_used = roundup(off * used / cap, 0)
|
||||
@@ -237,11 +295,10 @@ def month_table(src: dict[str, Any], user: dict[str, Any], parts: list[float]) -
|
||||
} # 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)
|
||||
first = 1
|
||||
end = begin + datetime.timedelta(days=max(total, 1) - 1)
|
||||
return {
|
||||
"start": {"year": first["year"], "month": first["month"]},
|
||||
"start": {"year": begin.year, "month": begin.month, "day": begin.day},
|
||||
"months": months,
|
||||
"days": total,
|
||||
"months_n": round_half(total / 30, 1),
|
||||
|
||||
Reference in New Issue
Block a user