Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_1
This commit is contained in:
@@ -52,6 +52,7 @@ CHOICE_FIELDS: tuple[tuple[str, str], ...] = (
|
||||
("vat_mode", "12.부 가 가 치 세"),
|
||||
("private_material_position", "13.사급비 위치"),
|
||||
("bid_method", "14. 낙 찰 방 식"),
|
||||
("subcontract_guarantee", "하도급대금 지급보증수수료"),
|
||||
("performance_guarantee_mode", "15. 이행보증 방식"),
|
||||
("contract_law_basis", "16. 적용기준"),
|
||||
("waste_placement", "폐기물처리비 자리"),
|
||||
@@ -79,6 +80,11 @@ FIELD_HINTS: dict[str, str] = {
|
||||
" 종합심사 대상 = 300억 이상 줄"
|
||||
" · 턴키(대안) = 0.084% · 설계 단계 금액엔 이 한 줄뿐, 계약 단계 단가 적용률로 이어질 칸"
|
||||
),
|
||||
"subcontract_guarantee": (
|
||||
"기본 꺼짐 — 실무 원가계산서 6건에 이 줄이 없음. 건설산업기본법 제34조 대상"
|
||||
"(하도급대금 지급보증서 교부)이면 켤 것 · 실무에 없는 것이 「대상 아님」인지"
|
||||
" 「안 적음」인지는 못 가름"
|
||||
),
|
||||
"performance_guarantee_mode": (
|
||||
"일반계약(기본) = 추정가격 300억 이상일 때만 공사이행보증수수료를 셈 · 최저가·기술제안 ="
|
||||
" 규모와 무관하게 셈 — 근거: 예정가격작성기준 제19조③20호(시행령 제52조제1항 단서) ·"
|
||||
@@ -132,6 +138,10 @@ def choice_options() -> dict[str, list[dict[str, str]]]:
|
||||
{"value": "comprehensive", "label": "종합심사 대상"},
|
||||
{"value": "turnkey", "label": "턴키(대안)"},
|
||||
],
|
||||
"subcontract_guarantee": [
|
||||
{"value": "off", "label": "계상 안 함(기본)"},
|
||||
{"value": "on", "label": "계상"},
|
||||
],
|
||||
"performance_guarantee_mode": [
|
||||
{"value": "general", "label": "일반계약"},
|
||||
{"value": "lowest_price_tech", "label": "최저가,기술제안"},
|
||||
|
||||
@@ -139,6 +139,9 @@ class CostInput:
|
||||
#: 14. 낙찰방식 — `not_comprehensive`(종합심사 외) · `comprehensive`(종합심사 대상) · `turnkey`.
|
||||
#: 하도급대금 지급보증 요율 줄만 고름.
|
||||
bid_method: str = "not_comprehensive"
|
||||
#: 하도급대금 지급보증수수료 — `off`(기본) · `on`. 실무 여섯 건에 줄 없음 · 건설산업기본법
|
||||
#: 제34조 대상이면 켬(「대상 아님」인지 「안 적음」인지는 실무로 못 가름).
|
||||
subcontract_guarantee: str = "off"
|
||||
#: 15. 이행보증 — `general`(일반계약: 추정가격 300억 이상만) ·
|
||||
#: `lowest_price_tech`(최저가·기술제안: 규모 무관).
|
||||
performance_guarantee_mode: str = "general"
|
||||
|
||||
@@ -206,16 +206,46 @@ def safety_management_cost(
|
||||
base_with = ctx.material_cost + ctx.direct_labor_cost + owner_supplied
|
||||
base_without = ctx.material_cost + ctx.direct_labor_cost
|
||||
|
||||
# ⚠ 2026-09-14 고침 — 대상액 50억 이상에서 두 줄(선임 대상 아님/대상)이 겹쳐 멈추던 자리.
|
||||
# 고시 별표1 은 「대상액 50억원 이상」 열과 **따로** 「영 별표5에 따른 보건관리자 선임 대상
|
||||
# 건설공사의 적용비율」 열을 둔다 — 대상액이 아니라 **공사가 선임 대상인가**로 가르는 열.
|
||||
# 선임 대상 금액은 요율 데이터 `manager_thresholds`(800억 · 토목 1,000억, 산업안전보건법
|
||||
# 시행령 별표5 — 원문 미보유). 추정금액 = 규모 기준액으로 견줌.
|
||||
thresholds = variable.get("manager_thresholds") or {}
|
||||
limit = thresholds.get(
|
||||
"civil_main_work_estimated_amount_krw"
|
||||
if data.work_type_safety == "civil"
|
||||
else "default_estimated_amount_krw"
|
||||
)
|
||||
manager_target = limit is not None and ctx.scale_reference >= Decimal(str(limit))
|
||||
|
||||
def evaluate(
|
||||
base: Decimal, multiplier: Decimal, label: str
|
||||
) -> tuple[Decimal, Decimal, Decimal]:
|
||||
row = select_bracket(
|
||||
brackets,
|
||||
amount_field="target_amount_bracket",
|
||||
amount=base,
|
||||
equals={"work_type": data.work_type_safety},
|
||||
label=label,
|
||||
)
|
||||
if manager_target:
|
||||
row = next(
|
||||
(
|
||||
r
|
||||
for r in brackets
|
||||
if str(r["target_amount_bracket"]).endswith("at_or_above_manager_threshold")
|
||||
and r.get("work_type") == data.work_type_safety
|
||||
),
|
||||
None,
|
||||
)
|
||||
if row is None:
|
||||
raise RateLookupError(f"{label}: 보건관리자 선임 대상 요율이 없습니다")
|
||||
else:
|
||||
row = select_bracket(
|
||||
[
|
||||
r
|
||||
for r in brackets
|
||||
if not str(r["target_amount_bracket"]).endswith("at_or_above_manager_threshold")
|
||||
],
|
||||
amount_field="target_amount_bracket",
|
||||
amount=base,
|
||||
equals={"work_type": data.work_type_safety},
|
||||
label=label,
|
||||
)
|
||||
percent = rate_percent(row, label=label)
|
||||
flat = base_amount(row)
|
||||
return (base * percent / _HUNDRED + flat) * multiplier, percent, flat
|
||||
@@ -309,6 +339,12 @@ def statutory_expenses(
|
||||
|
||||
if key == "performance_guarantee_fee" and not _performance_guarantee_applies(data, ctx):
|
||||
continue # 15. 일반계약·추정가격 300억 미만 — 줄 자체를 만들지 않는다
|
||||
if key == "subcontract_payment_guarantee" and (
|
||||
str(getattr(data, "subcontract_guarantee", "off") or "off") != "on"
|
||||
):
|
||||
# 기본 꺼짐(2026-09-14) — 실무 원가계산서 여섯 건에 이 줄이 없음. 건설산업기본법
|
||||
# 제34조 대상(하도급대금 지급보증서 교부)이면 설계자가 켬.
|
||||
continue
|
||||
|
||||
base, percent, note = _base_and_rate(dataset, data, ctx, item)
|
||||
if base is None:
|
||||
|
||||
@@ -234,6 +234,7 @@ function drawPanel(ctx: B09TabContext, sheet: CostSheetDto, reload: () => void):
|
||||
// 13~16 — 사급비 위치·적용기준은 칸만 받음(계산에 안 씀 · 까닭은 칸 밑 안내).
|
||||
typeSelect("private_material_position");
|
||||
typeSelect("bid_method");
|
||||
typeSelect("subcontract_guarantee");
|
||||
typeSelect("performance_guarantee_mode");
|
||||
typeSelect("contract_law_basis");
|
||||
typeSelect("waste_placement");
|
||||
|
||||
@@ -183,12 +183,11 @@ def test_이행보증_70억_이상은_기초액과_기준액을_쓴다() -> None
|
||||
|
||||
|
||||
def test_낙찰방식이_하도급대금_요율_줄을_고른다() -> None:
|
||||
assert calculate_cost(BASE).line("subcontract_payment_guarantee").rate_percent == Decimal(
|
||||
"0.081"
|
||||
)
|
||||
turnkey = calculate_cost(replace(BASE, bid_method="turnkey"))
|
||||
on = replace(BASE, subcontract_guarantee="on")
|
||||
assert calculate_cost(on).line("subcontract_payment_guarantee").rate_percent == Decimal("0.081")
|
||||
turnkey = calculate_cost(replace(on, bid_method="turnkey"))
|
||||
assert turnkey.line("subcontract_payment_guarantee").rate_percent == Decimal("0.084")
|
||||
comp = calculate_cost(replace(BASE, bid_method="comprehensive"))
|
||||
comp = calculate_cost(replace(on, bid_method="comprehensive"))
|
||||
assert comp.line("subcontract_payment_guarantee").rate_percent == Decimal("0.071")
|
||||
|
||||
|
||||
@@ -197,3 +196,26 @@ def test_사급비_위치_적용기준은_계산에_안_쓴다() -> None:
|
||||
replace(BASE, private_material_position="material", contract_law_basis="local")
|
||||
)
|
||||
assert same.totals == calculate_cost(BASE).totals
|
||||
|
||||
|
||||
def test_산업안전_50억_이상은_선임_대상으로_가른다() -> None:
|
||||
"""고시 별표1 — 「50억 이상」 열과 「보건관리자 선임 대상」 열은 공사가 선임 대상인가로 갈림."""
|
||||
big = replace(
|
||||
BASE,
|
||||
direct_material_krw=Decimal(4_000_000_000),
|
||||
direct_labor_krw=Decimal(3_000_000_000),
|
||||
estimated_price_krw=Decimal(20_000_000_000), # 토목 선임 기준 1,000억 미만
|
||||
)
|
||||
assert calculate_cost(big).line("safety_management_cost_b").rate_percent == Decimal("2.6")
|
||||
target = calculate_cost(replace(big, estimated_price_krw=Decimal(120_000_000_000)))
|
||||
assert target.line("safety_management_cost_b").rate_percent == Decimal("2.73")
|
||||
|
||||
|
||||
def test_하도급대금_지급보증은_기본_꺼짐() -> None:
|
||||
assert not calculate_cost(BASE).has("subcontract_payment_guarantee")
|
||||
assert (
|
||||
calculate_cost(replace(BASE, subcontract_guarantee="on")).amount(
|
||||
"subcontract_payment_guarantee"
|
||||
)
|
||||
> 0
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user