145-1 · 145-3 · 145-4 · 145-5 받음 · 충돌 = B09 이음 자리 넷 — Router · Shell 짝 셈 한 벌(바탕 → 산근 → 대가 → 내역 → 총괄) · used_keys 는 사본 셈 길(참조사본 포함) · 144 · 145-1 시험 단언 이음에 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
136 lines
6.9 KiB
Python
136 lines
6.9 KiB
Python
"""144-1 B09 뼈대 — 탭 차례 · 저장 자리 · 길 · 빈칸 규칙 · 「연결 없음」 목록 · 단가 굳히기.
|
||
|
||
- 탭 = 기번3 공통 엑셀 보이는 탭 차례(목차 · 안내 = 「연결 점검」 한 탭 · INITIAL 안 옮김)
|
||
- 저장 = 프로젝트 안 `B09_Estimation/v1/`(사용자 값 · 굳힌 단가) · 결과는 저장 안 함
|
||
- 줄 = 수요 따라(사용자 승인 ①) — 연결된 사본 없으면 노무 · 경비 · 중기 빈 표
|
||
· B08 자재만 줄 · 출처 빈칸
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
if str(ROOT) not in sys.path:
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402
|
||
from B09_Estimation import B09_Estimation_Router as router_mod # noqa: E402
|
||
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
|
||
|
||
#: 엑셀 보이는 탭(목차 · 안내 빼고) — 조사 F 1절 차례
|
||
EXCEL_TABS = [
|
||
"공사원가계산서", "총괄설계내역서", "설계내역서", "일위대가목록표", "일위대가표",
|
||
"단가산출근거목록표", "단가산출근거", "환율및기초자료", "중기목록표", "중기사용료",
|
||
"재료비목록표", "노무비목록표", "경비목록표", "자재단가대비표", "재료비수량금액집계표",
|
||
"노무비수량금액집계표", "경비수량금액집계표", "중기시간금액집계표",
|
||
] # fmt: skip
|
||
|
||
B08_SHEETS = {
|
||
"material": {
|
||
"rows": [
|
||
{"key": "sand", "group": "골재", "spec": "모래", "unit": "㎥", "amount": 16},
|
||
{"key": "rubble", "group": "골재", "spec": "막자갈", "unit": "㎥", "amount": None},
|
||
{"key": "cement", "group": "시멘트", "spec": "40kg/대", "unit": "대", "amount": 244},
|
||
]
|
||
}
|
||
}
|
||
|
||
|
||
def test_탭_차례가_엑셀과_같다():
|
||
source = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs.ts").read_text(encoding="utf-8")
|
||
block = source[
|
||
source.index("export const TABS") : source.index("];", source.index("export const TABS"))
|
||
]
|
||
names = re.findall(r'\["\w+", "([^"]+)"\]', block)
|
||
assert names[0] == "연결 점검"
|
||
assert names[1:] == EXCEL_TABS
|
||
|
||
|
||
def test_길_셋():
|
||
routes = {(r.path, tuple(sorted(r.methods))) for r in router_mod.router.routes}
|
||
assert ("/api/projects/{project_id}/estimation", ("GET",)) in routes
|
||
assert ("/api/projects/{project_id}/estimation", ("PUT",)) in routes
|
||
assert ("/api/projects/{project_id}/estimation/prices", ("POST",)) in routes
|
||
main = (ROOT / "main.py").read_text(encoding="utf-8")
|
||
assert "app.include_router(b09_estimation_router" in main
|
||
|
||
|
||
def test_사용자_값_다듬기_저장_자리(tmp_path):
|
||
raw = {
|
||
"materials": {"MT1": {"applied": 1200, "supply": "관급", "x": 1}, "MT2": {"applied": None},
|
||
"MT3": {"supply": "엉뚱"}},
|
||
"estimate": {"estimated_price": "abc", "profit_adjust": 905980, "work_type": "토목",
|
||
"rates": {"RT000001": 3.56, "RT000002": None}, "zzz": 1},
|
||
"other": 1,
|
||
} # fmt: skip
|
||
clean = store.write_user(tmp_path, raw)
|
||
assert clean == {
|
||
"materials": {"MT1": {"applied": 1200.0, "supply": "관급"}, "MT2": {"applied": None}},
|
||
"estimate": {"estimated_price": None, "profit_adjust": 905980.0, "work_type": "토목",
|
||
"rates": {"RT000001": 3.56, "RT000002": None}},
|
||
} # fmt: skip
|
||
path = tmp_path / "B09_Estimation" / "v1" / "user_values.json"
|
||
assert json.loads(path.read_text(encoding="utf-8")) == clean
|
||
assert store.read_user(tmp_path) == clean
|
||
assert store.read_book(tmp_path) is None
|
||
|
||
|
||
def test_단가_굳히기_운전사_연료_따라옴():
|
||
book = prices.refresh_book(["EQ000004"]) # 불도저(무한궤도) 19ton · 조종원 1048 · 경유
|
||
rows = book["rows"]
|
||
assert set(rows) == {"EQ000004", "LB000048", "MO000002"}
|
||
assert rows["EQ000004"]["loss"] == 1811 and rows["EQ000004"]["crew_code"] == "1048"
|
||
assert rows["LB000048"]["code"] == "1048" and rows["MO000002"]["prices"]["유가"] > 0
|
||
assert book["driver_factor"] == {"num": 400, "den": 1920, "text": "1/8 × 16/12 × 25/20"}
|
||
# [저장] = 책에 없는 새 줄만 더함 — 있는 줄 값은 안 바꿈
|
||
stale = {**book, "rows": {**rows, "LB000048": {**rows["LB000048"], "price": 1}}}
|
||
grown = prices.extend_book(stale, ["EQ000004", "LB000002"])
|
||
assert grown["rows"]["LB000048"]["price"] == 1 and "LB000002" in grown["rows"]
|
||
# 책 밖 줄 = 지금 M01 값 + fresh 표시
|
||
inputs = prices.build_inputs(stale, ["EQ000004", "LB000002"])
|
||
fresh = {row["key"]: row.get("fresh", False) for row in inputs["labor"]}
|
||
assert fresh == {"LB000002": True, "LB000048": False}
|
||
assert inputs["fuel_keys"] == {"경유": "MO000002"}
|
||
|
||
|
||
def test_연결_없으면_빈_표와_연결_없음(tmp_path, monkeypatch):
|
||
async def fake_materials(_project_id):
|
||
return B08_SHEETS
|
||
|
||
monkeypatch.setattr(router_mod, "material_sheets", fake_materials)
|
||
body = asyncio.run(router_mod.payload("p", tmp_path))
|
||
sheets = body["sheets"]
|
||
assert sheets["labor"] == [] and sheets["expense"] == [] and sheets["machine"] == []
|
||
assert [row["key"] for row in sheets["compare"]] == ["b08:sand", "b08:cement"] # 수량 있는 줄만
|
||
assert all(row["applied"] is None for row in sheets["compare"]) # 출처 없음 = 빈칸(0 아님)
|
||
assert [(g["tab"], g["key"]) for g in sheets["gaps"] if g["tab"] == "compare"] == [
|
||
("compare", "b08:sand"),
|
||
("compare", "b08:cement"),
|
||
]
|
||
assert any("연결된 구조물 조합 사본 없음" in n for n in body["notes"])
|
||
assert body["book"] is None # GET 은 아무것도 쓰지 않음
|
||
assert not (tmp_path / "B09_Estimation").exists()
|
||
|
||
|
||
def test_연결된_사본이_부른_자원만(tmp_path):
|
||
"""사본 단가 묶음 통째(고르기 후보 기계 100여)가 아니라 로직이 실제로 부른 줄만."""
|
||
from B09_Estimation.B09_Estimation_Linked import used_keys
|
||
from M02_MasterTemplete import M02_Item_Prices as item_prices
|
||
|
||
bundle = item_prices.to_json(item_prices.collect(["GC000268"])) # 기계 시간당 기계경비
|
||
files = {
|
||
"단가.json": bundle,
|
||
"일위대가.json": {"줄": {"종류": "일위대가", "담은로직": [{"로직": "GC000268"}]}},
|
||
"입력값.json": {"GC000268": {"기계": {"값": "0101-0019"}, "지역": {"값": "전국평균"}}},
|
||
}
|
||
for name, data in files.items():
|
||
(tmp_path / name).write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||
assert used_keys(tmp_path) == ["EQ000004", "MO000002", "LB000048"]
|
||
(tmp_path / "입력값.json").write_text("{}", encoding="utf-8")
|
||
# 빈 입력 = 자동값(142-1 사본 셈 한 벌 · 기계 고르기 첫 값 1730-0015) — 그 로직이 부른 줄
|
||
assert used_keys(tmp_path) == ["EQ000112", "MO000001", "LB000050"]
|
||
assert used_keys(tmp_path / "없음") == []
|