"""707-7 경쟁사 공사 통째 불러오기 — 한 번 [불러오기] = 일위대가 · 단가산출(이 공사 서고 사본) → 내역 줄 → 요율 → 단가표(`Router_Import` apply · `Bill_Hopyo`). - 실무 xlsx(거창 · 대흥 · 소광 · 영월): 호표 빠짐 0 · 705-3 수(고정 · 하위 · 이음) 그대로 · 직접비 우리 = 엑셀 · 어긋난 줄 0 · 층 쓰기 0(임시 storage 빈 채) · 사본 = `precombo@project@-@{표지}-{번}` · 하위 = 참조사본 - 내역 단가 ≠ 일위대가표 단가(소광) = 잇지 않고 내역 단가 그대로 · 목록 - 다시 부르기 = 같은 사본 · 두 번 셈 0 · .estx · .STC = 일위대가 · 요율 까닭 「같은 공사 엑셀에서」 - 내 자료집에 저장(703-2) = 하위 호표도 지금 층에 같이 씀(한 번만) """ import json import re import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from config import config_system # noqa: E402 K = ROOT / "resources" / "knowledge" / "original" / "실무문서" FILES = { "거창": "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx", # noqa: E501 "대흥": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx", # noqa: E501 "소광": "25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx", # noqa: E501 "영월": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx", "장수": "02.산불 장수/01.내역서/00.단가변경_26년_산불_무주(장수,장수,덕산,산54-1).estx", "기번3": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).STC", # noqa: E501 } # fmt: skip #: (호표, 줄, 고정, 하위, 이음, 사본, 내역 이음, 엑셀 직접비, 끝수 줄 · 차) — #: 영월 = 내역 줄이 코드로만 가리킴(번 채움) · STmate 하위 호표 반올림 끝수 28 줄 #: (줄당 1 원/단위 · 브레인 정함 = 그래도 이음) WANT = { "거창": (14, 68, 56, 12, 11, 8, 8, 384916227, 0, 0), "대흥": (34, 171, 143, 28, 0, 26, 45, 710359136, 0, 0), "소광": (25, 117, 98, 19, 0, 0, 0, 173311386, 0, 0), "영월": (23, 138, 135, 3, 0, 21, 34, 767148700, 28, 2951), } URL = "/api/projects/8b98ca75-c572-4002-8468-2adae4323414/estimation/import/bill" ALL = {"hopyo": True, "rates": True, "prices": True} @pytest.fixture(scope="module") def world(tmp_path_factory): from fastapi import FastAPI from fastapi.testclient import TestClient from B09_Estimation import B09_Estimation_Router as b09 from B09_Estimation import B09_Estimation_Router_Import as ri from common_util.common_util_auth import verify_session from resources.tester.test_145_2_b09_labor_calc import _project base = tmp_path_factory.mktemp("p7077") (base / "storage").mkdir() roots: dict[str, Path] = {} async def root_of(pid): return roots["now"] with pytest.MonkeyPatch.context() as mp: mp.setattr(config_system, "STORAGE_BASE_DIR", str(base / "storage")) mp.setattr(ri, "_project_root", root_of) mp.setattr(b09, "_project_root", root_of) app = FastAPI() app.include_router(b09.router) session = {"user_id": 42, "company_id": 7, "role": "USER", "is_master": False} app.dependency_overrides[verify_session] = lambda: session client = TestClient(app) done: dict[str, dict] = {} def run(name, body=ALL, fresh=True): if fresh and name in done: # 같은 파일 처음 불러온 결과 다시 씀(읽기 느림) roots["now"] = done[name]["root"] return done[name] if fresh: roots["now"] = _project(base / name, []) path = K / FILES[name] seen = client.post(URL, files={"file": (path.name, path.read_bytes())}) assert seen.status_code == 200, seen.text got = client.post(URL + "/apply", json={"file_key": seen.json()["file_key"], **body}) assert got.status_code == 200, got.text out = {**got.json(), "file_key": seen.json()["file_key"], "root": roots["now"]} if fresh: done[name] = out return out yield {"run": run, "storage": base / "storage"} def _libs(root): folder = root / "B09_Estimation/v1/library" return sorted(p.name for p in folder.iterdir()) if folder.is_dir() else [] @pytest.mark.parametrize("name", list(WANT)) def test_실무_엑셀_통째_빠짐_0_합계_같음_층_쓰기_0(world, name): got = world["run"](name) hopyo, lines, fixed, kids, links, copies, billed, direct, tail, gap = WANT[name] step = got["단계"]["일위대가"] assert (step["호표"], step["줄"], step["고정"], step["호표참조"], step["이음"]) == ( hopyo, lines, fixed, kids, links) # fmt: skip assert step["빠짐"] == 0 and step["실패"] == [] and step["사본"] == copies assert got["알림"]["이음"] == billed assert got["합계"]["엑셀직접"] == direct and got["합계"]["우리직접"] == direct - gap assert [x["까닭"] for x in got["어긋남"]] == ["끝수"] * tail libs = _libs(got["root"]) assert len(libs) == copies assert all(re.fullmatch(rf"precombo@project@-@{got['file_key']}-\d+", x) for x in libs) assert "까닭" not in got["단계"]["요율"] and "까닭" not in got["단계"]["단가표"] assert not [p for p in world["storage"].rglob("*") if p.is_file()] # 층 쓰기 0 def test_대흥_총공사비_엑셀과_같음_하위는_참조사본(world): got = world["run"]("대흥") assert got["합계"]["우리총"] == got["합계"]["엑셀총"] == 1201879000 nested = 0 for lib in _libs(got["root"]): path = got["root"] / "B09_Estimation/v1/library" / lib / "일위대가.json" labor = json.loads(path.read_text("utf-8")) copies = labor["참조사본"] for doc in copies.values(): for one in doc["담은로직"]: if "참조" in one: ref = one["참조"] assert ref["층"] == "project" and ref["주인"] == got["file_key"] assert f"참조|project|{got['file_key']}|{ref['키']}" in copies nested += 1 assert nested > 0 def test_소광_내역_단가_다른_줄은_잇지_않음(world): got = world["run"]("소광") other = got["단계"]["일위대가"]["단가다름"] assert len(other) == 12 and other[0]["내역"] == 46564 and other[0]["일위대가"] == 53070 def test_다시_부르기_같은_사본_두_번_셈_0(world): first = world["run"]("거창") again = world["run"]("거창", fresh=False) assert _libs(again["root"]) == _libs(first["root"]) assert again["합계"]["우리직접"] == first["합계"]["우리직접"] == 384916227 rows = again["user"]["bill"]["rows"] assert len({r["key"] for r in rows}) == len(rows) @pytest.mark.parametrize("name", ["장수", "기번3"]) def test_원본_파일_일위대가_요율_까닭(world, name): got = world["run"](name) assert "같은 공사 엑셀에서" in got["단계"]["일위대가"]["까닭"] assert "같은 공사 엑셀에서" in got["단계"]["요율"]["까닭"] assert "labor" in got["단계"]["단가표"] assert got["합계"]["우리직접"] == got["합계"]["엑셀직접"] assert _libs(got["root"]) == [] def test_내_자료집에_저장_하위_호표_같이_한_번(world, monkeypatch): from B09_Estimation import B09_Estimation_Daega_Personal as personal from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs from M02_MasterTemplete import M02_MasterTemplete_Store as m02 from M02_MasterTemplete import M02_Template_Layers as layers got = world["run"]("거창") root = got["root"] blocks = daega_inputs(root)["blocks"] head = f"C:참조|project|{got['file_key']}" tops = {b["cover"]: b for b in blocks if b["cover"].startswith(head)} with_kids = [c for c, b in tops.items() if any(ln["kind"] == "daega" for ln in b["lines"])] assert len(with_kids) >= 2 monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(world["storage"])) folder = layers.personal_dir(7, 42) folder.mkdir(parents=True, exist_ok=True) made = [personal.build(root, c, {}, f"거창 {i}")["문서"] for i, c in enumerate(with_kids[:2])] assert all(personal.SUBS in doc for doc in made) with m02.at(folder): keys = [personal.write(doc)["이름"] for doc in made] saved = {p.stem: json.loads(p.read_text("utf-8")) for p in (folder / "precombo").glob("*.json")} for key in keys: for one in saved[key]["담은로직"]: if "참조" in one: # 740-1 하위 호표 = 사본 블록(본뜬 = 같이 만든 개인 SC) assert one["참조"]["층"] == "사본" and one["본뜬"]["키"] in saved copied = [d["본뜬"]["복사원본"] for d in saved.values() if "복사원본" in (d.get("본뜬") or {})] assert len(copied) == len({json.dumps(x, sort_keys=True) for x in copied}) # 같은 하위 한 번 for p in (folder / "precombo").glob("*.json"): # 다음 시험에 층 쓰기 안 남김 p.unlink() def test_일위대가_시트_없음_까닭_나머지_그대로(tmp_path): import io import openpyxl from B09_Estimation import B09_Estimation_Router_Import as ri from B09_Estimation.B09_Estimation_Bill_Excel import read_bill path = K / FILES["거창"] book = openpyxl.load_workbook(path) del book["일위대가"] out = io.BytesIO() book.save(out) data = out.getvalue() parsed = read_bill(path.name, data) links, step, same = ri._own_links(tmp_path, path.name, data, parsed, "abcd1234", None) assert links == {} and "일위대가 시트 없음" in step["까닭"] and same is parsed