- 영월 꼴(STmate 엑셀 내역 줄 = 호표 코드만) = 일위대가표 같은 코드 호표 번 채움 → 서고 사본 21 · 이음 34 - 이은 줄 차가 비목마다 1 원/단위 안 = 「끝수」(STmate 하위 호표 반올림) · 끝 알림 한 줄 「엑셀 합계와 차 n 원 = 하위 호표 반올림 끝수 m 줄」(브레인 정함 = 끝수 줄도 이음) - 요율 못 읽은 공사(.estx · .STC) = 총공사비 칸 안 보임 - 시험 test_707_7 11(영월 끝수 · 일위대가 시트 없음 까닭) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
209 lines
9.9 KiB
Python
209 lines
9.9 KiB
Python
"""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:
|
|
assert one["참조"]["층"] == "personal" 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
|