feat(B09): 일위대가 탭 — 목록표+본표 2단, 원천 표시, 층 파고들기
**API 2종** (`B09_Estimation_Router.py`)
- `GET …/estimation/unit-prices` — **목록표** + **산출 요약**. 요약을 같이 보내는 까닭은
사용자가 「무엇이 안 선 상태인가」를 화면에서 알아야 하기 때문임(자재 카탈로그
미확보로 구조물 계열이 안 섬).
- `GET …/estimation/unit-prices/{code}` — **본표**. 줄마다 원천(자재5·노임6·기계경비105·
일위대가103·단가산출104)과 **파고들기 가능 여부**가 붙음. 없는 코드는 404.
- 품셈 3 MB 를 요청마다 다시 안 읽게 `cached_build()` 로 한 번만 조립.
**화면** (`B09_Estimation_UI_Page.ts`)
- 일위대가 탭 활성화. **목록표(위) + 본표(아래) 2단** — 9-3 「제목+상세 한 쌍」이 화면에도
그대로 섬.
- 본표 줄마다 `원천(번호)` 표시, **기계 줄을 누르면 그 시간당 사용료 본표로 파고듦**
(거기서 취득가·연료·조종원까지 보임). 값을 못 믿을 때 사람이 하는 일이 이것임.
- **재료·노무·경비 3분할 + 합계 줄**, `TC = NC + GC + JC` 성립 여부를 화면 문구로 냄.
- **산출 요약을 화면에 표시** — 자재가 없어 구조물 계열이 못 선다는 것을 그 자리에 적음.
locale 은 **B09 키만** 추가(16줄), 공용 파일 다른 줄 무수정.
⚠ 화면 조작 검증은 다음 단계 — `tsc` 는 통과했고(남은 오류 1건은 메인 창 B08 파일),
백엔드 재시작·클릭 검증은 이어서 함.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from decimal import Decimal
|
||||
from functools import lru_cache
|
||||
|
||||
from B09_Estimation.B09_Estimation_Guards import check_surcharge_once
|
||||
from B09_Estimation.B09_Estimation_MachineCost import load_machine_catalog
|
||||
@@ -177,8 +178,11 @@ def build_unit_prices(axis: AxisResult | None = None) -> UnitPriceBuild:
|
||||
공종 하나에 붙은 자원 줄들을 그 공종의 상세로 삼는다. 자원이 하나도 안 붙은
|
||||
공종은 **빈 줄로 세우지 않고 건너뛴다** — 0 원 일위대가가 내역에 서면 안 된다.
|
||||
"""
|
||||
master = load_work_item_master()
|
||||
if axis is None:
|
||||
axis = build_resource_axis(load_work_item_master(), load_combined_catalog())
|
||||
axis = build_resource_axis(master, load_combined_catalog())
|
||||
# 일위대가 이름은 **공종명**이어야 한다 — 코드만 보이면 사람이 못 읽는다.
|
||||
names = {w["work_item_code"]: w.get("name", "") for w in master.get("work_items", [])}
|
||||
|
||||
build = UnitPriceBuild()
|
||||
wages = load_operator_wages()
|
||||
@@ -197,7 +201,13 @@ def build_unit_prices(axis: AxisResult | None = None) -> UnitPriceBuild:
|
||||
continue
|
||||
unit = next((r.amount_unit for r in rows if r.amount_unit), "")
|
||||
build.book.add_title(
|
||||
PriceTitle(code=title_code, kind=PriceKind.UNIT_PRICE, name=work_item_code, unit=unit)
|
||||
PriceTitle(
|
||||
code=title_code,
|
||||
kind=PriceKind.UNIT_PRICE,
|
||||
name=names.get(work_item_code) or work_item_code,
|
||||
spec=work_item_code,
|
||||
unit=unit,
|
||||
)
|
||||
)
|
||||
added = 0
|
||||
for row in rows:
|
||||
@@ -230,3 +240,122 @@ def verify_surcharge_once(
|
||||
surcharge_rate_percent=surcharge_rate_percent,
|
||||
label=code,
|
||||
)
|
||||
|
||||
|
||||
#: 상세 줄이 **어느 층에서 왔는지** 보이는 표시 (PLAN 9-3, ESTX `LinkIndex` 와 같은 축).
|
||||
SOURCE_INDEX: dict[PriceKind, int] = {
|
||||
PriceKind.MATERIAL: 5,
|
||||
PriceKind.LABOR: 6,
|
||||
PriceKind.MACHINE_BASE: 105,
|
||||
PriceKind.MACHINE_HOURLY: 105,
|
||||
PriceKind.UNIT_PRICE: 103,
|
||||
PriceKind.PRICE_BASIS: 104,
|
||||
PriceKind.LUMPSUM: 0,
|
||||
}
|
||||
SOURCE_LABEL: dict[PriceKind, str] = {
|
||||
PriceKind.MATERIAL: "자재",
|
||||
PriceKind.LABOR: "노임",
|
||||
PriceKind.MACHINE_BASE: "기계경비",
|
||||
PriceKind.MACHINE_HOURLY: "기계경비",
|
||||
PriceKind.UNIT_PRICE: "일위대가",
|
||||
PriceKind.PRICE_BASIS: "단가산출",
|
||||
PriceKind.LUMPSUM: "일식·견적",
|
||||
}
|
||||
|
||||
#: 상세를 파고들 수 있는 층 — 이 종류의 줄을 누르면 그 본표가 열린다.
|
||||
DRILLABLE_KINDS = frozenset({PriceKind.MACHINE_HOURLY, PriceKind.UNIT_PRICE, PriceKind.PRICE_BASIS})
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def cached_build() -> UnitPriceBuild:
|
||||
"""조립 결과를 한 번만 만든다 — 품셈 3 MB 를 요청마다 다시 읽지 않는다."""
|
||||
return build_unit_prices()
|
||||
|
||||
|
||||
def build_summary(build: UnitPriceBuild) -> dict:
|
||||
"""산출 요약 — **화면에도 낸다.** 사용자가 「무엇이 안 선 상태인가」를 알아야 한다."""
|
||||
kinds: dict[str, int] = {}
|
||||
for title in build.book.titles.values():
|
||||
kinds[title.kind.value] = kinds.get(title.kind.value, 0) + 1
|
||||
return {
|
||||
"titles": len(build.book.titles),
|
||||
"unit_prices": kinds.get(PriceKind.UNIT_PRICE.value, 0),
|
||||
"machine_hourly": kinds.get(PriceKind.MACHINE_HOURLY.value, 0),
|
||||
"skipped_work_items": len(build.skipped),
|
||||
"incomplete_machines": len(build.incomplete_machines),
|
||||
"kinds": kinds,
|
||||
# ⚠ 지금 상태를 화면에 그대로 알린다 (PLAN 9-6 미결).
|
||||
"notes": [
|
||||
"자재 카탈로그가 아직 없어 **구조물 계열 일위대가가 서지 않습니다** — "
|
||||
"지금 선 것은 노무·기계 성분뿐입니다(연료만 자재로 섭니다).",
|
||||
"사급 잡자재 단가는 미결입니다 — 값을 지어내지 않고 비워 둡니다.",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def list_unit_prices(build: UnitPriceBuild) -> list[dict]:
|
||||
"""목록표 — 「무엇이 있나」 한 줄씩."""
|
||||
rows: list[dict] = []
|
||||
for code, title in sorted(build.book.titles.items()):
|
||||
if title.kind is not PriceKind.UNIT_PRICE:
|
||||
continue
|
||||
money = build.book.resolve(code)
|
||||
rows.append(
|
||||
{
|
||||
"code": code,
|
||||
"name": title.name,
|
||||
"spec": title.spec,
|
||||
"unit": title.unit,
|
||||
"material": str(money.material),
|
||||
"labor": str(money.labor),
|
||||
"expense": str(money.expense),
|
||||
"total": str(money.total),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def detail_of(build: UnitPriceBuild, code: str) -> dict:
|
||||
"""본표 — 「그것이 무엇으로 이루어졌나」. 줄마다 원천과 파고들기 여부를 함께 낸다."""
|
||||
title = build.book.title(code)
|
||||
money = build.book.resolve(code)
|
||||
rows: list[dict] = []
|
||||
for detail in build.book.details.get(code, []):
|
||||
child = build.book.title(detail.ref_code)
|
||||
unit_money = build.book.resolve(detail.ref_code)
|
||||
line = unit_money.scaled(detail.quantity)
|
||||
rows.append(
|
||||
{
|
||||
"ref_code": detail.ref_code,
|
||||
"name": child.name,
|
||||
"spec": child.spec,
|
||||
"unit": child.unit,
|
||||
"source_index": SOURCE_INDEX.get(child.kind, 0),
|
||||
"source_label": SOURCE_LABEL.get(child.kind, ""),
|
||||
"drillable": child.kind in DRILLABLE_KINDS,
|
||||
"quantity": str(detail.quantity),
|
||||
"unit_material": str(unit_money.material),
|
||||
"unit_labor": str(unit_money.labor),
|
||||
"unit_expense": str(unit_money.expense),
|
||||
"unit_total": str(unit_money.total),
|
||||
"material": str(line.material),
|
||||
"labor": str(line.labor),
|
||||
"expense": str(line.expense),
|
||||
"total": str(line.total),
|
||||
"note": detail.note,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"code": code,
|
||||
"name": title.name,
|
||||
"spec": title.spec,
|
||||
"unit": title.unit,
|
||||
"kind": title.kind.value,
|
||||
"material": str(money.material),
|
||||
"labor": str(money.labor),
|
||||
"expense": str(money.expense),
|
||||
"total": str(money.total),
|
||||
# TC = NC + GC + JC 가 성립하는지 화면이 스스로 보이게 한다.
|
||||
"sum_matches": money.total == money.material + money.labor + money.expense,
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user