Merge remote-tracking branch 'origin/sub_desktop_1' into sub_laptop_1

This commit is contained in:
2026-09-14 23:16:10 +09:00
2 changed files with 92 additions and 20 deletions
+53 -20
View File
@@ -25,7 +25,6 @@ from B09_Estimation.B09_Estimation_Engine_Cost_Options import (
VAT_MODES,
cut_gap,
overhead_base,
profit_cut,
vat_base,
)
from B09_Estimation.B09_Estimation_RateOverride import apply_overrides
@@ -368,9 +367,9 @@ def calculate_cost(data: CostInput) -> CostResult:
)
#: 절사 뒤 잔차 맞춤 반복 상한. 실무 6건은 **두 걸음 안에** 앉음
#: (다섯 건 한 걸음 · 울진 신설 두 걸음).
_CUT_MAX_PASSES = 3
#: 절사 목표를 몇 칸까지 내려 볼 것인가. 못 밟는 1,000 배수를 만나면 한 칸 내린다 —
#: 실측(거창 꼴 40 자리)에서는 **한 칸이면 다 앉았다**. 여유로 셋까지 본다.
_CUT_DESCENT_MAX = 3
#: 11. 절사 끔 — 고르개가 보내는 값. 빈 값도 같이 받는다(저장 안 된 옛 프로젝트).
CUT_OFF = ("", "none")
@@ -386,30 +385,64 @@ def _calculate_with_scale(
⚠ **걸음마다 ÷1.1 을 해야 한다**(2026-09-14 고침). 이윤을 1원 깎으면 부가세가 따라 줄어
총공사비는 **1.1원** 줄므로, 잔차를 그대로 빼면 경계를 **지나쳐** 진동한다 —
울진 신설 실측 `654 → 999 → 900 → 910 → 909 …` 로 안 앉았다. 늘 ÷1.1 하면 `654 → 999 → 0`.
⚠ **못 밟는 배수가 있다**(2026-09-14 둘째 고침). 한 걸음 낙차가 1원일 때도 2원일 때도 있어
(부가세가 버림이라 열 걸음에 한 번쯤 2원) 어떤 1,000 배수는 **건너뛴다**. 어림으로 좇으면
지나쳐 놓고 못 돌아온다 — 거창 꼴 40 자리 중 16 이 끝자리 999 로 남았다. 그래서
**목표를 못 박고 가장 작은 보정액을 이분으로 찾는다**(총공사비는 보정액에 대해 비증가).
그 배수를 못 밟으면 **한 칸 아래 배수**로 목표를 내려 다시 찾는다(뒷받침일 뿐 — 실측 거창 꼴
40 자리에서는 한 칸도 안 내려갔다). 이윤은 늘 **가장 적게** 깎인다 — 실무 식(÷1.1 한 걸음)이
앉는 자리에서는 같은 값이 나온다(봉화 339 · 영월 632).
"""
result = _calculate_once(data, dataset, scale, notes)
if data.cut_basis in CUT_OFF or data.cut_unit_krw <= 0:
return result
key = "grand_total" if data.cut_basis == "grand_total" else "total_cost"
label = (
f"{CUT_BASES.get(data.cut_basis, data.cut_basis)} {data.cut_unit_krw:,}원 미만"
" 절사 자동보정(산림청고시 2025-82호)"
+ (" + 설계자 입력" if data.profit_adjustment_krw else "")
)
def attempt(extra: Decimal) -> CostResult:
"""이윤을 `extra` 만큼 더 깎아 한 번 셈. `0` 이면 안 자른 결과 그대로."""
if extra == 0:
return result
return _calculate_once(
replace(
data,
profit_adjustment_krw=data.profit_adjustment_krw + extra,
cut_basis="none",
profit_adjustment_label=label,
),
dataset,
scale,
notes,
)
raw = result.totals[key]
target = raw - cut_gap(raw, data.cut_unit_krw)
automatic = _ZERO
gap = _ZERO
for _ in range(_CUT_MAX_PASSES):
gap = cut_gap(result.totals[key], data.cut_unit_krw)
landed = result
for _ in range(_CUT_DESCENT_MAX):
# 목표 이하로 내려가는 **가장 작은** 보정액을 이분으로 찾는다. 한 걸음이 적어도 1원을
# 깎으므로 `raw - target` 이면 반드시 목표 아래로 간다.
low, high = _ZERO, raw - target
while low < high:
middle = (low + high) // 2
if attempt(middle).totals[key] <= target:
high = middle
else:
low = middle + 1
landed = attempt(low)
gap = landed.totals[key] - target
if gap == 0:
automatic = low
break
# 실무 식 — 총공사비 절사 + 부가세가 공급가액 비례면 ÷1.1(잔차 걸음도 같음).
automatic += profit_cut(gap, data.cut_basis, data.vat_mode)
adjusted = replace(
data,
profit_adjustment_krw=data.profit_adjustment_krw + automatic,
cut_basis="none",
profit_adjustment_label=(
f"{CUT_BASES.get(data.cut_basis, data.cut_basis)} {data.cut_unit_krw:,}원 미만"
" 절사 자동보정(산림청고시 2025-82호)"
+ (" + 설계자 입력" if data.profit_adjustment_krw else "")
),
)
result = _calculate_once(adjusted, dataset, scale, notes)
# 지나쳤다 = 그 배수는 못 밟는 자리. 한 칸 아래를 노린다.
target -= data.cut_unit_krw
result = landed
result.notes.append(
f"{CUT_BASES.get(data.cut_basis, data.cut_basis)} {data.cut_unit_krw:,}원 미만 절사 —"
f" 이윤에서 {automatic:,}원 자동보정."
@@ -419,7 +452,7 @@ def _calculate_with_scale(
if gap:
# 조용히 남기지 않는다 — 안 앉았으면 끝자리가 남았다는 것을 화면에 보인다.
result.notes.append(
f"⚠ 절사가 {_CUT_MAX_PASSES}걸음 안에 안 앉음 —"
f"⚠ 절사가 {_CUT_DESCENT_MAX}칸 안에 안 앉음 —"
f" 끝자리 {gap:,}원이 남음(설계자 확인 필요)"
)
return result