Merge remote-tracking branch 'origin/dev' into sub_laptop_1
This commit is contained in:
@@ -0,0 +1,164 @@
|
|||||||
|
"""원가계산서 4형식 중 나머지 셋 — **서식 차례만** (수공 S · 실적일반 TA · 실적수공 SA).
|
||||||
|
|
||||||
|
근거: STmate 분석 `12_원가계산_4형식과_RATE표_복원.md` §4~6 (DFM 복원 서식) ·
|
||||||
|
`35_형식과_단계의_공통과_차이.md` §1·§2 (공통 사슬 · 형식별 차이 · 12번 정정 반영).
|
||||||
|
|
||||||
|
⚠ **금액을 안 낸다** — 요율 데이터가 없다(2026-09-14 확인: 수공 세부 경비 율표 `RateB` ·
|
||||||
|
표준시장단가 제비율 `RATE_RA`·`RATE_RA1`·실적 안전표 `An_A`). 줄마다 무엇이 막혔는지만 적는다.
|
||||||
|
**0 원으로 채우지 않는다** — 0 원은 「없다」가 아니라 「공짜」로 읽혀 총액이 조용히 작아진다.
|
||||||
|
⚠ 35번 §5 미확인(실적형 이윤 밑수 · 실적수공 안전관리비 존재)은 「확인 대기」로 세운다.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
#: 줄 상태 — 화면이 그대로 보인다.
|
||||||
|
COMMON = "공통 요율 — 일반형식과 같은 표(형식 전체가 서면 금액이 섬)"
|
||||||
|
SEPARATE = "별도내역 — 요율이 아니라 따로 산출한 금액"
|
||||||
|
RATE_B = "요율표 미확보 — 수공 세부 경비 율표(STmate RateB)"
|
||||||
|
RATE_RA = "요율표 미확보 — 표준시장단가(실적공사비) 제비율(STmate RATE_RA·RATE_RA1)"
|
||||||
|
RATE_AN_A = "요율표 미확보 — 실적 산업안전보건관리비 표(STmate An_A·An_A1)"
|
||||||
|
TOTAL = "합계 — 위 줄이 다 서야 섬"
|
||||||
|
PENDING_PROFIT = "확인 대기 — 실적형 이윤 밑수가 복원식과 화면 문구가 충돌(35번 §5-1)"
|
||||||
|
PENDING_SAFETY = "확인 대기 — 실적수공에 안전관리비가 실제로 있는지 미확인(35번 §5-2)"
|
||||||
|
DIRECT = "설계내역서 합계에서 옴"
|
||||||
|
|
||||||
|
_SUGONG_EXPENSES = (
|
||||||
|
"수도광열비",
|
||||||
|
"복리후생비",
|
||||||
|
"소모품·사무용품비",
|
||||||
|
"여비·교통비·통신비",
|
||||||
|
"세금·공과금",
|
||||||
|
"폐기물처리비",
|
||||||
|
"도서인쇄비",
|
||||||
|
"지급수수료",
|
||||||
|
)
|
||||||
|
|
||||||
|
FORM_LABELS = {
|
||||||
|
"general": "일반형식",
|
||||||
|
"sugong": "수공형식",
|
||||||
|
"actual_general": "실적일반",
|
||||||
|
"actual_sugong": "실적수공",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _r(mark: str, label: str, formula: str, status: str, level: int) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"key": "",
|
||||||
|
"mark": mark,
|
||||||
|
"label": label,
|
||||||
|
"level": level,
|
||||||
|
"formula": formula,
|
||||||
|
"rate_percent": None,
|
||||||
|
"amount_krw": None,
|
||||||
|
"note": status,
|
||||||
|
"total": level == 0,
|
||||||
|
"blocked": status not in (COMMON, DIRECT),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _sugong() -> list[dict[str, Any]]:
|
||||||
|
"""수공 S — 12번 §4.1."""
|
||||||
|
rows = [
|
||||||
|
_r("1.", "순공사원가(가+나+다)", "", TOTAL, 0),
|
||||||
|
_r("가.", "순공사비", "", DIRECT, 1),
|
||||||
|
_r("나.", "간접노무비", "직접노무비 × 율", COMMON, 1),
|
||||||
|
_r("다.", "경 비", "", TOTAL, 1),
|
||||||
|
_r("1)", "산재보험료", "(직접노무비+간접노무비) × 율", COMMON, 2),
|
||||||
|
_r("2)", "고용보험료", "(직접노무비+간접노무비) × 율", COMMON, 2),
|
||||||
|
_r("3)", "건강보험료", "직접노무비 × 율", COMMON, 2),
|
||||||
|
_r("4)", "장기요양보험료", "건강보험료 × 율", COMMON, 2),
|
||||||
|
_r("5)", "연금보험료", "직접노무비 × 율", COMMON, 2),
|
||||||
|
_r("6)", "퇴직공제부금비", "직접노무비 × 율", COMMON, 2),
|
||||||
|
_r("7)", "산업안전보건관리비", "(재료비+직접노무비+도급자관급자재) × 율", COMMON, 2),
|
||||||
|
_r("8)", "안전관리비", "", SEPARATE, 2),
|
||||||
|
_r("9)", "품질관리비", "", SEPARATE, 2),
|
||||||
|
]
|
||||||
|
rows += [
|
||||||
|
_r(f"{10 + i})", name, "(직노·노무·재료 중 고른 밑수) × 율", RATE_B, 2)
|
||||||
|
for i, name in enumerate(_SUGONG_EXPENSES)
|
||||||
|
]
|
||||||
|
rows += [
|
||||||
|
_r("18)", "공사이행 수수료", "[기초액 + (직공비−기준액) × 율] × 공기", COMMON, 2),
|
||||||
|
_r("19)", "하도대금 수수료", "(재료비+직노+산출경비) × 율", COMMON, 2),
|
||||||
|
_r("20)", "환 경 보 전 비", "(재료비+직노+산출경비) × 율", COMMON, 2),
|
||||||
|
_r("21)", "건설기계 대여금", "(재료비+직노+산출경비) × 율", COMMON, 2),
|
||||||
|
_r("22)", "석면분담금", "(직노+간노) × 율", COMMON, 2),
|
||||||
|
_r("23)", "임금채권부담금", "(직노+간노) × 율", COMMON, 2),
|
||||||
|
_r(
|
||||||
|
"2.",
|
||||||
|
"일반관리비",
|
||||||
|
"(순공사비+간접노무비+경비) × 율 — 밑수에 수공 경비가 들어 미산출",
|
||||||
|
RATE_B,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
_r("3.", "이 윤", "(순공사원가+일반관리비−재료비) × 율 — 밑수 미산출", RATE_B, 1),
|
||||||
|
_r("4.", "공급가액", "", TOTAL, 0),
|
||||||
|
_r("5.", "도급공사비", "", TOTAL, 0),
|
||||||
|
_r("6.", "총공사비", "", TOTAL, 0),
|
||||||
|
]
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _actual(sugong: bool) -> list[dict[str, Any]]:
|
||||||
|
"""실적일반 TA(§5 · 35번 정정) / 실적수공 SA(§6)."""
|
||||||
|
labor_base = "(직접노무비+간접노무비)" if sugong else "노무비"
|
||||||
|
rows = [
|
||||||
|
_r("1.", "직접공사비", "", DIRECT, 0),
|
||||||
|
_r("2.", "간접공사비", "", TOTAL, 0),
|
||||||
|
_r("1)", "간접노무비", "직접노무비 × 율", RATE_RA, 1),
|
||||||
|
_r("2)", "산재보험료", f"{labor_base} × 율", COMMON, 1),
|
||||||
|
_r("3)", "고용보험료", f"{labor_base} × 율", COMMON, 1),
|
||||||
|
_r("4)", "건강보험료", "직접노무비 × 율", COMMON, 1),
|
||||||
|
_r("5)", "장기요양보험료", "건강보험료 × 율", COMMON, 1),
|
||||||
|
_r("6)", "연금보험료", "직접노무비 × 율", COMMON, 1),
|
||||||
|
_r("7)", "퇴직공제부금비", "직접노무비 × 율", COMMON, 1),
|
||||||
|
_r("8)", "산업안전보건관리비", "(직접공사비+도급자관급자재) × 율", RATE_AN_A, 1),
|
||||||
|
_r("9)", "안전관리비", "", PENDING_SAFETY if sugong else SEPARATE, 1),
|
||||||
|
_r("10)", "품질관리비", "", SEPARATE, 1),
|
||||||
|
]
|
||||||
|
if sugong:
|
||||||
|
rows += [
|
||||||
|
_r(f"{11 + i})", name, "(직노·노무·재료 중 고른 밑수) × 율", RATE_B, 1)
|
||||||
|
for i, name in enumerate(_SUGONG_EXPENSES)
|
||||||
|
]
|
||||||
|
guarantee_base = "(재료비+직노+산출경비)"
|
||||||
|
tail = 19
|
||||||
|
else:
|
||||||
|
rows.append(_r("11)", "기타경비", "직접공사비 × 율", RATE_RA, 1))
|
||||||
|
guarantee_base = "직접공사비"
|
||||||
|
tail = 12
|
||||||
|
rows += [
|
||||||
|
_r(f"{tail})", "공사이행 수수료", "[기초액 + (직공비−기준액) × 율] × 공기", COMMON, 1),
|
||||||
|
_r(f"{tail + 1})", "하도대금 수수료", f"{guarantee_base} × 율", COMMON, 1),
|
||||||
|
_r(f"{tail + 2})", "환 경 보 전 비", "(재료비+직노+산출경비) × 율", COMMON, 1),
|
||||||
|
_r(f"{tail + 3})", "건설기계 대여금", f"{guarantee_base} × 율", COMMON, 1),
|
||||||
|
_r(f"{tail + 4})", "석면분담금", f"{labor_base} × 율", COMMON, 1),
|
||||||
|
_r(f"{tail + 5})", "임금채권부담금", f"{labor_base} × 율", COMMON, 1),
|
||||||
|
_r(
|
||||||
|
"3.",
|
||||||
|
"일반관리비",
|
||||||
|
"(직접공사비+간접노무비) × 율"
|
||||||
|
if sugong
|
||||||
|
else "(직접공사비+간접공사비+관리품목자재) × 율",
|
||||||
|
RATE_RA,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
_r("4.", "이 윤", "", PENDING_PROFIT, 0),
|
||||||
|
_r("5.", "공급가액", "", TOTAL, 0),
|
||||||
|
_r("6.", "도급공사비", "", TOTAL, 0),
|
||||||
|
_r("7.", "총공사비", "", TOTAL, 0),
|
||||||
|
]
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def form_rows(form: str) -> list[dict[str, Any]]:
|
||||||
|
"""형식별 서식 줄(금액 없음). 일반형식은 엔진이 셈하므로 여기 없음."""
|
||||||
|
if form == "sugong":
|
||||||
|
return _sugong()
|
||||||
|
if form == "actual_general":
|
||||||
|
return _actual(sugong=False)
|
||||||
|
if form == "actual_sugong":
|
||||||
|
return _actual(sugong=True)
|
||||||
|
raise ValueError(f"서식이 없는 형식입니다: {form}")
|
||||||
@@ -30,6 +30,7 @@ from B09_Estimation.B09_Estimation_CostSheet import (
|
|||||||
sheet_rows,
|
sheet_rows,
|
||||||
status_line,
|
status_line,
|
||||||
)
|
)
|
||||||
|
from B09_Estimation.B09_Estimation_CostSheet_Forms import FORM_LABELS, form_rows
|
||||||
from B09_Estimation.B09_Estimation_Engine_Cost import calculate_cost
|
from B09_Estimation.B09_Estimation_Engine_Cost import calculate_cost
|
||||||
from B09_Estimation.B09_Estimation_RateOverride import apply_overrides, clean_overrides
|
from B09_Estimation.B09_Estimation_RateOverride import apply_overrides, clean_overrides
|
||||||
from B09_Estimation.B09_Estimation_Rates import RateLookupError, load_rate_dataset
|
from B09_Estimation.B09_Estimation_Rates import RateLookupError, load_rate_dataset
|
||||||
@@ -69,8 +70,12 @@ def _waste(bill: dict[str, Any], quantity: dict[str, Any]) -> tuple[Decimal, boo
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/{project_id}/estimation/cost-sheet")
|
@router.get("/{project_id}/estimation/cost-sheet")
|
||||||
async def get_cost_sheet(project_id: UUID) -> JSONResponse:
|
async def get_cost_sheet(project_id: UUID, form: str = "general") -> JSONResponse:
|
||||||
"""원가계산서(일반형식) 한 장 — 서식 줄 · 기준 입력 선택지 · 상태줄."""
|
"""원가계산서 한 장 — 서식 줄 · 기준 입력 선택지 · 상태줄.
|
||||||
|
|
||||||
|
`form` — 일반형식만 금액을 냄. 수공·실적일반·실적수공은 **서식 차례만**(요율표 미확보 줄은
|
||||||
|
막고 사유 · 0 원으로 안 채움).
|
||||||
|
"""
|
||||||
from B09_Estimation.B09_Estimation_Router import get_bill
|
from B09_Estimation.B09_Estimation_Router import get_bill
|
||||||
from common_util.common_util_project_settings import estimation_settings, quantity_settings
|
from common_util.common_util_project_settings import estimation_settings, quantity_settings
|
||||||
|
|
||||||
@@ -125,6 +130,9 @@ async def get_cost_sheet(project_id: UUID) -> JSONResponse:
|
|||||||
"rate_version": result.rate_version,
|
"rate_version": result.rate_version,
|
||||||
"notes": result.notes,
|
"notes": result.notes,
|
||||||
"rate_override_count": len(overrides),
|
"rate_override_count": len(overrides),
|
||||||
|
"form_key": "general",
|
||||||
|
"form_labels": FORM_LABELS,
|
||||||
|
**(_other_form(form) if form != "general" else {}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -156,6 +164,22 @@ async def put_cost_sheet(project_id: UUID, body: dict[str, Any]) -> JSONResponse
|
|||||||
OVERRIDES_KEY = "rate_overrides"
|
OVERRIDES_KEY = "rate_overrides"
|
||||||
|
|
||||||
|
|
||||||
|
def _other_form(form: str) -> dict[str, Any]:
|
||||||
|
"""나머지 세 형식 — 금액 없는 서식 줄로 갈아 끼움(기준 입력판 자료는 그대로)."""
|
||||||
|
if form not in FORM_LABELS:
|
||||||
|
return {"form_error": f"모르는 형식입니다: {form}"}
|
||||||
|
return {
|
||||||
|
"form_key": form,
|
||||||
|
"form": FORM_LABELS[form],
|
||||||
|
"rows": form_rows(form),
|
||||||
|
"status_line": {},
|
||||||
|
"form_note": (
|
||||||
|
"금액 미산출 — 이 형식의 요율표(수공 세부 경비·표준시장단가 제비율)가 없어"
|
||||||
|
" 서식 차례만 보임"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{project_id}/estimation/rate-table")
|
@router.get("/{project_id}/estimation/rate-table")
|
||||||
async def get_rate_table(project_id: UUID) -> JSONResponse:
|
async def get_rate_table(project_id: UUID) -> JSONResponse:
|
||||||
"""제비율 요율표 — 마스터 요율에 **이 프로젝트의 덮어쓰기**를 얹어 DFM 첫 탭 차례로.
|
"""제비율 요율표 — 마스터 요율에 **이 프로젝트의 덮어쓰기**를 얹어 DFM 첫 탭 차례로.
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ interface SheetRow {
|
|||||||
level: number;
|
level: number;
|
||||||
formula: string;
|
formula: string;
|
||||||
rate_percent: string | null;
|
rate_percent: string | null;
|
||||||
amount_krw: string;
|
/** 형식 서식만 선 줄(수공·실적)은 null — 0 원으로 안 채움. */
|
||||||
|
amount_krw: string | null;
|
||||||
|
blocked?: boolean;
|
||||||
note: string;
|
note: string;
|
||||||
total: boolean;
|
total: boolean;
|
||||||
}
|
}
|
||||||
@@ -51,6 +53,9 @@ interface CostSheetDto {
|
|||||||
hints: Record<string, string>;
|
hints: Record<string, string>;
|
||||||
};
|
};
|
||||||
waste_note: string;
|
waste_note: string;
|
||||||
|
form_key: string;
|
||||||
|
form_labels: Record<string, string>;
|
||||||
|
form_note?: string;
|
||||||
bill_missing_count: number;
|
bill_missing_count: number;
|
||||||
bill_unconfirmed_count: number;
|
bill_unconfirmed_count: number;
|
||||||
rate_version: { dataset_id: string; effective_date: string };
|
rate_version: { dataset_id: string; effective_date: string };
|
||||||
@@ -67,7 +72,9 @@ const FORMS = [
|
|||||||
"고용개선지원(서울)",
|
"고용개선지원(서울)",
|
||||||
];
|
];
|
||||||
/** DFM 본문 아래 탭 이름 그대로. */
|
/** DFM 본문 아래 탭 이름 그대로. */
|
||||||
const FORM_TABS = ["일반형식", "수공형식", "실적일반", "실적수공"];
|
const FORM_TABS = ["general", "sugong", "actual_general", "actual_sugong"];
|
||||||
|
/** 프로젝트별 고른 형식 탭 — 일반형식만 금액이 섬. */
|
||||||
|
const formOf = new Map<string, string>();
|
||||||
|
|
||||||
const STYLE_ID = "b09-cost-sheet-styles";
|
const STYLE_ID = "b09-cost-sheet-styles";
|
||||||
function injectStyles(): void {
|
function injectStyles(): void {
|
||||||
@@ -121,9 +128,9 @@ function el<K extends keyof HTMLElementTagNameMap>(
|
|||||||
/** 프로젝트별 입력 캐시 — 탭을 다시 골라도 [저장] 전 값이 남음. */
|
/** 프로젝트별 입력 캐시 — 탭을 다시 골라도 [저장] 전 값이 남음. */
|
||||||
const drafts = new Map<string, Record<string, string>>();
|
const drafts = new Map<string, Record<string, string>>();
|
||||||
|
|
||||||
async function fetchSheet(projectId: string): Promise<CostSheetDto> {
|
async function fetchSheet(projectId: string, form: string): Promise<CostSheetDto> {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/estimation/cost-sheet`,
|
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/estimation/cost-sheet?form=${encodeURIComponent(form)}`,
|
||||||
{ credentials: "include" },
|
{ credentials: "include" },
|
||||||
);
|
);
|
||||||
const body = (await response.json()) as CostSheetDto;
|
const body = (await response.json()) as CostSheetDto;
|
||||||
@@ -278,11 +285,11 @@ function drawPanel(ctx: B09TabContext, sheet: CostSheetDto, reload: () => void):
|
|||||||
ctx.panel.append(box);
|
ctx.panel.append(box);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawBody(ctx: B09TabContext, sheet: CostSheetDto): void {
|
function drawBody(ctx: B09TabContext, sheet: CostSheetDto, reload: () => void): void {
|
||||||
const wrap = el("div", "b09cs");
|
const wrap = el("div", "b09cs");
|
||||||
const head = el("div", "b09cs__head");
|
const head = el("div", "b09cs__head");
|
||||||
head.append(
|
head.append(
|
||||||
el("span", "b09cs__title", "제잡비 계산 — 일반형식"),
|
el("span", "b09cs__title", `제잡비 계산 — ${sheet.form_labels[sheet.form_key] ?? ""}`),
|
||||||
el(
|
el(
|
||||||
"span",
|
"span",
|
||||||
"b09cs__meta",
|
"b09cs__meta",
|
||||||
@@ -301,6 +308,7 @@ function drawBody(ctx: B09TabContext, sheet: CostSheetDto): void {
|
|||||||
}
|
}
|
||||||
if (sheet.bill_unconfirmed_count) head.append(unconfirmedBadge(sheet.bill_unconfirmed_count));
|
if (sheet.bill_unconfirmed_count) head.append(unconfirmedBadge(sheet.bill_unconfirmed_count));
|
||||||
if (sheet.waste_note) head.append(el("span", "b09cs__meta", `폐기물처리비: ${sheet.waste_note}`));
|
if (sheet.waste_note) head.append(el("span", "b09cs__meta", `폐기물처리비: ${sheet.waste_note}`));
|
||||||
|
if (sheet.form_note) head.append(el("span", "b09cs__warn", `⚠ ${sheet.form_note}`));
|
||||||
wrap.append(head);
|
wrap.append(head);
|
||||||
|
|
||||||
const body = el("div", "b09cs__sheet");
|
const body = el("div", "b09cs__sheet");
|
||||||
@@ -312,19 +320,27 @@ function drawBody(ctx: B09TabContext, sheet: CostSheetDto): void {
|
|||||||
label.append(el("span", "", row.label));
|
label.append(el("span", "", row.label));
|
||||||
if (row.formula) label.append(el("span", "b09cs__formula", `<${row.formula}>`));
|
if (row.formula) label.append(el("span", "b09cs__formula", `<${row.formula}>`));
|
||||||
label.title = [row.formula, row.note].filter(Boolean).join(" · ");
|
label.title = [row.formula, row.note].filter(Boolean).join(" · ");
|
||||||
line.append(label, el("span", "b09cs__amount", won(row.amount_krw)));
|
// 금액 없는 줄 — 「-」 + 막힌 까닭(요율표 미확보 · 확인 대기). 0 원으로 안 보임.
|
||||||
|
if (row.amount_krw === null && row.blocked) label.append(el("span", "b09cs__warn", row.note));
|
||||||
|
line.append(
|
||||||
|
label,
|
||||||
|
el("span", "b09cs__amount", row.amount_krw === null ? "-" : won(row.amount_krw)),
|
||||||
|
);
|
||||||
body.append(line);
|
body.append(line);
|
||||||
}
|
}
|
||||||
wrap.append(body);
|
wrap.append(body);
|
||||||
|
|
||||||
const tabs = el("div", "b09cs__tabs");
|
const tabs = el("div", "b09cs__tabs");
|
||||||
FORM_TABS.forEach((name, i) => {
|
for (const key of FORM_TABS) {
|
||||||
const tab = el("button", "b09cs__tab", name);
|
const tab = el("button", "b09cs__tab", sheet.form_labels[key] ?? key);
|
||||||
tab.setAttribute("aria-selected", String(i === 0));
|
tab.setAttribute("aria-selected", String(key === sheet.form_key));
|
||||||
tab.disabled = i > 0;
|
if (key !== "general") tab.title = "서식 차례만 — 요율표 미확보로 금액 안 섬";
|
||||||
if (i > 0) tab.title = "준비 중 — 일반형식부터 세움";
|
tab.addEventListener("click", () => {
|
||||||
tabs.append(tab);
|
formOf.set(ctx.projectId as string, key);
|
||||||
|
reload();
|
||||||
});
|
});
|
||||||
|
tabs.append(tab);
|
||||||
|
}
|
||||||
wrap.append(tabs);
|
wrap.append(tabs);
|
||||||
|
|
||||||
const status = el("div", "b09cs__status");
|
const status = el("div", "b09cs__status");
|
||||||
@@ -346,11 +362,11 @@ function render(ctx: B09TabContext): void {
|
|||||||
const load = (): void => {
|
const load = (): void => {
|
||||||
ctx.body.replaceChildren(el("div", "b09cs__meta", "원가계산서 계산 중…"));
|
ctx.body.replaceChildren(el("div", "b09cs__meta", "원가계산서 계산 중…"));
|
||||||
ctx.panel.replaceChildren();
|
ctx.panel.replaceChildren();
|
||||||
fetchSheet(ctx.projectId as string)
|
fetchSheet(ctx.projectId as string, formOf.get(ctx.projectId as string) ?? "general")
|
||||||
.then((sheet) => {
|
.then((sheet) => {
|
||||||
ctx.body.replaceChildren();
|
ctx.body.replaceChildren();
|
||||||
drawPanel(ctx, sheet, load);
|
drawPanel(ctx, sheet, load);
|
||||||
drawBody(ctx, sheet);
|
drawBody(ctx, sheet, load);
|
||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
ctx.body.replaceChildren(
|
ctx.body.replaceChildren(
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""원가계산서 나머지 세 형식 — 서식 차례만 · 0 원 안 채움 · 막힌 까닭 · 확인 대기 (2026-09-14 판정).
|
||||||
|
|
||||||
|
근거: STmate 분석 12번 §4~6 · 35번 §1·§2·§5.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from B09_Estimation.B09_Estimation_CostSheet_Forms import ( # noqa: E402
|
||||||
|
PENDING_PROFIT,
|
||||||
|
PENDING_SAFETY,
|
||||||
|
RATE_B,
|
||||||
|
RATE_RA,
|
||||||
|
form_rows,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("form", ["sugong", "actual_general", "actual_sugong"])
|
||||||
|
def test_금액은_하나도_안_내고_0원도_없다(form) -> None:
|
||||||
|
rows = form_rows(form)
|
||||||
|
assert rows and all(row["amount_krw"] is None for row in rows)
|
||||||
|
assert all(row["note"] for row in rows)
|
||||||
|
|
||||||
|
|
||||||
|
def test_수공은_세부_경비_여덟이_율표_미확보() -> None:
|
||||||
|
rows = form_rows("sugong")
|
||||||
|
blocked = [row["label"] for row in rows if row["note"] == RATE_B]
|
||||||
|
for name in ("수도광열비", "복리후생비", "지급수수료", "폐기물처리비"):
|
||||||
|
assert name in blocked
|
||||||
|
assert "기타경비" not in [row["label"] for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def test_실적형은_이윤이_확인_대기_실적수공은_안전관리비도() -> None:
|
||||||
|
general = {row["label"]: row["note"] for row in form_rows("actual_general")}
|
||||||
|
sugong = {row["label"]: row["note"] for row in form_rows("actual_sugong")}
|
||||||
|
assert general["이 윤"] == PENDING_PROFIT and sugong["이 윤"] == PENDING_PROFIT
|
||||||
|
assert sugong["안전관리비"] == PENDING_SAFETY
|
||||||
|
assert general["간접노무비"] == RATE_RA and general["기타경비"] == RATE_RA
|
||||||
|
|
||||||
|
|
||||||
|
def test_실적일반_보증_밑수는_직접공사비_환경은_재료직노산출경비() -> None:
|
||||||
|
"""35번 §1 — 12번의 「TA 환경보전비 = 직접공사비」는 06번 정정에 따라 재+직노+산출경비."""
|
||||||
|
rows = {row["label"]: row["formula"] for row in form_rows("actual_general")}
|
||||||
|
assert rows["하도대금 수수료"].startswith("직접공사비")
|
||||||
|
assert rows["환 경 보 전 비"].startswith("(재료비+직노+산출경비)")
|
||||||
|
|
||||||
|
|
||||||
|
def test_모르는_형식은_멈춘다() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
form_rows("general")
|
||||||
Reference in New Issue
Block a user