Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
후보 93 → GC000083 · 21 → GC000198 · 64 → GC000143 위 둘 · 49 → GC000085 ·
|
||||
잔토처리에 철골재 철거 없음 · 근거 없는 줄 그대로 · 상태 「후보」(자동 확정 없음) ·
|
||||
블록 글 HP(338-2a · 268 · 139 이음 그대로) · `clause` 윗마디 0.9 · 조항 오타 13-114 ·
|
||||
거창 꼴 흩어진 칸 이어 붙임(단산 줄 21 · 출처 17 · 직종 21 · 338-2b) ·
|
||||
master_data 그대로.
|
||||
"""
|
||||
|
||||
@@ -152,3 +153,19 @@ def test_조항_윗마디_오타(north):
|
||||
assert hb.sources("[산림사업표준품셈 9-14]") == [["산림품셈", "9-14"]]
|
||||
stone = next(b for b in north["단산"] if b["번"] == 63)
|
||||
assert stone["출처"] == [["산림품셈", "13-11-4"]]
|
||||
|
||||
|
||||
def test_거창_흩어진_칸():
|
||||
path = (
|
||||
base.ROOT
|
||||
/ "resources/knowledge/original/실무문서/01.산불거창(거창북상병곡)/01.내역서"
|
||||
/ "00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx"
|
||||
)
|
||||
book = hx.read_book(path.name, path.read_bytes())
|
||||
lines = [x for h in book["호표"] for x in h["줄"] if x["갈래"] == "단가산출"]
|
||||
assert len(lines) == 21 and all(x.get("근거") for x in lines)
|
||||
assert sum(1 for x in lines if x["근거"]["출처"]) == 17
|
||||
assert sum(1 for x in lines if x["근거"]["직종"]) == 21
|
||||
first = book["단산"][0]
|
||||
assert first["이름"] == "측구터파기(토사)" and first["직종"] == ["보통인부"]
|
||||
assert any(t.startswith("보통인부 : 172,068") for t in first["글"])
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
견본 = `resources/knowledge/original/실무문서/2026_실무일위대가 모음/`(북부청 임도 · 사방) ·
|
||||
검산 어긋남 0 · 호표 값 · 시트 없음 400 · xlsx 길 · 역할 표 · 엉터리 · 20 MB · 지우기 ·
|
||||
양식 목록에 안 섞임 ·
|
||||
양식 목록에 안 섞임 · 이음 넓힘(산근 · 대가 · 호표 · 이름으로 이음) · 노임 갈래 ·
|
||||
문서 보안 400(338) ·
|
||||
master_data 원본 그대로(sha256). 층은 임시 폴더.
|
||||
"""
|
||||
|
||||
@@ -207,3 +208,54 @@ def test_마스터_층은_마스터_폴더(client):
|
||||
client.who.update(role="USER")
|
||||
assert client.get(URL.format("system")).json()["줄"][0]["키"] == "HP-000001" # 읽기 = 누구나
|
||||
assert client.delete(URL.format("system") + "/HP-000001").status_code == 403
|
||||
|
||||
|
||||
def _linked_xlsx() -> bytes:
|
||||
"""338 — 비고 「산근 2 호표」 · 「대가1」 · 빈 비고 + 이름 같은 블록 · 노임 줄."""
|
||||
import openpyxl
|
||||
|
||||
book = openpyxl.Workbook()
|
||||
ws = book.active
|
||||
ws.title = "일위대가"
|
||||
ws.append([None, "호표", "품명", "규격", "수량", "단위", "합계", None, "노무비", None, "비고"])
|
||||
ws.append([None, None, None, None, None, None, "단가", "금액", "단가", "금액"])
|
||||
ws.append([None, "제1호표", "표지석", "", None, "개소", None, 1000, None, 1000])
|
||||
ws.append([None, None, "터파기", "토사", 1, "㎥", 100, 100, 100, 100, "산근 2 호표"])
|
||||
ws.append([None, None, "되메우기", "", 1, "㎥", 100, 100, 100, 100, None])
|
||||
ws.append([None, None, "보통인부", "", 1, "인", 100, 100, 100, 100, None])
|
||||
ws.append([None, None, "호표 줄", "", 1, "개소", 100, 100, 100, 100, "대가1"])
|
||||
calc = book.create_sheet("단가산출")
|
||||
calc.append([None, "산출근거", "합계", "노무비"])
|
||||
calc.append([None, "2.터파기[㎥]"])
|
||||
calc.append([None, " [건설공사표준품셈 3-2-1]"])
|
||||
calc.append([None, "3.되메우기[㎥]"])
|
||||
calc.append([None, " 보통인부 : 100 × 1 = 100"])
|
||||
out = io.BytesIO()
|
||||
book.save(out)
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def test_이음_넓힘_노임():
|
||||
got = hx.read_book("a.xlsx", _linked_xlsx())
|
||||
lines = {x["이름"]: x for x in got["호표"][0]["줄"]}
|
||||
assert lines["터파기"]["참조"] == {"종류": "단산", "번": 2} and "이음" not in lines["터파기"]
|
||||
assert lines["터파기"]["근거"]["출처"] == [["건설품셈", "3-2-1"]]
|
||||
assert lines["되메우기"]["참조"] == {"종류": "단산", "번": 3} # 빈 비고 · 이름 같은 블록 하나
|
||||
assert lines["되메우기"]["이음"] == "이름" and lines["되메우기"]["갈래"] == "단가산출"
|
||||
assert lines["되메우기"]["근거"]["직종"] == ["보통인부"]
|
||||
assert lines["보통인부"]["갈래"] == "노임" and "이음" not in lines["보통인부"]
|
||||
assert lines["호표 줄"]["참조"] == {"종류": "일위", "번": 1}
|
||||
for note, want in (
|
||||
("단산12참조", ("단산", 12)),
|
||||
("단가산출 3", ("단산", 3)),
|
||||
("일위대가4호표", ("일위", 4)),
|
||||
("일위 5 참조", ("일위", 5)),
|
||||
):
|
||||
hit = hx._REF.match(hx._flat(note))
|
||||
assert ("단산" if hit.group(1) else "일위", int(hit.group(3))) == want
|
||||
assert hx._REF.match("단산참조") is None and hx._REF.match("D96") is None
|
||||
|
||||
|
||||
def test_문서_보안_400():
|
||||
with pytest.raises(hx.ExcelError, match="문서 보안"):
|
||||
hx.read_book("a.xlsx", bytes([0x9B]) + b" DRMONE This Document is secured" + bytes(64))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
북부청 임도 xls — 병합 11 · 15열 · B20 「제3호표」 · `#,##0` · 호표 68 행 범위 안 겹침 ·
|
||||
제3호표 [20, 24] · 올리면 격자도 · 200 / 옛 파일 404 · 지우면 격자도 · 역할 · xlsx 값만 ·
|
||||
STmate 격자(일위대가표 · 단가산출근거 · 숨은 칸 글 뺌 · 338) ·
|
||||
master_data 그대로.
|
||||
"""
|
||||
|
||||
@@ -131,3 +132,22 @@ def test_xlsx_값만():
|
||||
assert sheet["칸"]["B5"] == {"값": "제 1 호표"} and sheet["칸"]["E6"] == {"값": 0.5}
|
||||
assert "K6" not in sheet["칸"] # 머리 글(앞 4 줄) 마지막 열 J 까지
|
||||
assert grid["호표"] == [{"번": 1, "이름": "표지석", "행": [5, 6]}] and grid["단산"] == []
|
||||
|
||||
|
||||
def test_STmate_격자():
|
||||
from resources.tester.test_m02_hopyo_stmate import _path
|
||||
|
||||
path = _path("영월")
|
||||
data = path.read_bytes()
|
||||
parsed = hx.read_book(path.name, data)
|
||||
grid = hg.read(path.name, data, parsed)
|
||||
assert [s["이름"] for s in grid["책"]["시트"]] == ["일위대가표", "단가산출근거"]
|
||||
cells = _sheet(grid, "일위대가표")["칸"]
|
||||
one = grid["호표"][0]
|
||||
assert one == {"번": 1, "이름": "녹생토(종자배합)", "행": [5, 10]}
|
||||
assert cells[f"A{one['행'][0]}"]["값"].strip().startswith("제")
|
||||
assert cells["A10"]["값"] == "보통인부"
|
||||
calc = _sheet(grid, "단가산출근거")["칸"]
|
||||
block = next(b for b in grid["단산"] if b["번"] == 2)
|
||||
assert calc[f"B{block['행'][0] + 1}"]["값"].startswith("구조물터파기(인력)")
|
||||
assert not any(str(c.get("값")).startswith("_x000") for c in {**cells, **calc}.values())
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
"""일위대가 실무자료 STmate 꼴 읽기(PLAN 338-2b · `M02_Hopyo_Stmate`).
|
||||
|
||||
여섯 파일 올라감(봉화 45 · 영월 23 · 울진대흥 설계 · 착공 34 · 영덕 9 · 울진소광 25 호표) ·
|
||||
봉화 줄 216(구성 209 + 근거글 7) · 단산 103 · 노임 30 · 호표 22 · 줄 코드 갈래 ·
|
||||
노임 이름 어긋남 0 · 블록 자원 품 · 이음 열쇠(M~Z 열 코드 꼴 첫 것) ·
|
||||
문서 보안(장수 · 진안) 400 글 · 원본 엑셀 · master_data 그대로.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hb
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hx
|
||||
from M02_MasterTemplete import M02_Hopyo_Stmate as st
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
from resources.tester import test_m02_hopyo_excel as base
|
||||
from resources.tester.test_m02_hopyo_excel import ROOT, _post, master_hash
|
||||
|
||||
client = base.client # 공용 고정물(임시 층)
|
||||
DOCS = ROOT / "resources" / "knowledge" / "original" / "실무문서"
|
||||
FILES = {
|
||||
"봉화": "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx",
|
||||
"영월": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx",
|
||||
"대흥설계": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/"
|
||||
"2024년 간선임도사업(기번3).xlsx",
|
||||
"대흥착공": "2024년 간선임도(기번3-울진.대흥)(공통)/"
|
||||
"2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx",
|
||||
"영덕": "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/"
|
||||
"2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx",
|
||||
"소광": "25년 산불진화임도(기번8)(울진소광)/"
|
||||
"2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx",
|
||||
}
|
||||
HOPYOS = {"봉화": 45, "영월": 23, "대흥설계": 34, "대흥착공": 34, "영덕": 9, "소광": 25}
|
||||
LOCKED = {
|
||||
"장수": "02.산불 장수/01.내역서/"
|
||||
"00.단가변경_26년_산불_무주(장수,장수,덕산,산54-1)내역+수량.xlsx",
|
||||
"진안": "03.산불진안/01.내역서/00.단가변경_26년_산불_무주(진안,백운,백암,산1-2)내역+수량.xlsx",
|
||||
}
|
||||
|
||||
|
||||
def _path(tag: str) -> Path:
|
||||
return DOCS / {**FILES, **LOCKED}[tag]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def books() -> dict[str, dict]:
|
||||
return {tag: hx.read_book(_path(tag).name, _path(tag).read_bytes()) for tag in FILES}
|
||||
|
||||
|
||||
def _lines(book: dict) -> list[dict]:
|
||||
return [x for h in book["호표"] for x in h["줄"]]
|
||||
|
||||
|
||||
def test_여섯_파일_호표_수(books):
|
||||
assert {tag: len(b["호표"]) for tag, b in books.items()} == HOPYOS
|
||||
for book in books.values():
|
||||
assert (book["꼴"], book["시트"]) == ("STmate", "일위대가표")
|
||||
assert book["검산"] == {"어긋남": [], "총괄표": 0}
|
||||
assert len(_lines(books["대흥설계"])) == len(_lines(books["대흥착공"])) == 171 # 같은 설계
|
||||
|
||||
|
||||
def test_봉화_줄_갈래(books):
|
||||
bong = books["봉화"]
|
||||
lines = _lines(bong)
|
||||
kinds = {k: sum(1 for x in lines if x["갈래"] == k) for k in ("단가산출", "노임", "호표", "%")}
|
||||
assert kinds == {"단가산출": 103, "노임": 30, "호표": 22, "%": 7}
|
||||
assert len(lines) + sum(len(h.get("근거글") or []) for h in bong["호표"]) == 216
|
||||
assert all(x["코드"] for x in lines)
|
||||
for book in books.values():
|
||||
for x in _lines(book):
|
||||
if x["갈래"] == "호표":
|
||||
assert x["참조"]["번"] in {h["번"] for h in book["호표"]}
|
||||
if x["갈래"] == "단가산출":
|
||||
assert x["참조"] == {"종류": "단산", "번": int(x["코드"][1:]), "코드": x["코드"]}
|
||||
assert "근거" in x # 블록 모두 이어짐
|
||||
by = {h["번"]: h for h in bong["호표"]}
|
||||
assert by[13]["근거글"] == ["건설표준품셈 7-2-2(찰붙임)"] # 코드 없는 줄 = 호표 근거글
|
||||
assert hb.sources(" ".join(by[13]["근거글"])) == [["건설품셈", "7-2-2"]]
|
||||
|
||||
|
||||
def test_영월_줄과_블록(books):
|
||||
yw = books["영월"]
|
||||
one = yw["호표"][0]
|
||||
assert (one["번"], one["코드"], one["이름"], one["단위"]) == (
|
||||
1,
|
||||
"B00260",
|
||||
"녹생토(종자배합)",
|
||||
"M3",
|
||||
)
|
||||
assert one["행"] == [5, 10] and one["금액"]["계"] == 155184
|
||||
assert [(x["이름"], x["코드"], x["갈래"], x["수량"]) for x in one["줄"]] == [
|
||||
("산림종자", "M00024", "자재", 0.25),
|
||||
("산림종자", "M00211", "자재", 0.25),
|
||||
("인공토양(녹생토)", "M00832", "자재", 1.1),
|
||||
("보통인부", "L00002", "노임", 0.43),
|
||||
]
|
||||
two = {x["이름"]: x for x in yw["호표"][1]["줄"]}
|
||||
assert two["국가지점번호판 검증수수료"]["갈래"] == "별도계상" # 비고 「별산」
|
||||
assert two["구조물터파기(인력)"]["근거"] == {
|
||||
"출처": [["건설품셈", "3-1-3"]],
|
||||
"직종": ["보통인부"],
|
||||
"행": [5, 54],
|
||||
}
|
||||
block = next(b for b in yw["단산"] if b["코드"] == "D00002")
|
||||
assert (block["번"], block["이름"], block["규격"], block["단위"]) == (
|
||||
2,
|
||||
"구조물터파기(인력)",
|
||||
"경질토사",
|
||||
"M3",
|
||||
)
|
||||
assert block["자원"] == [
|
||||
{"갈래": "L", "코드": "L00002", "이름": "보통인부", "규격": "", "품": 0.26, "절": ""}
|
||||
]
|
||||
assert any("건설표준품셈3-1-3" in t for t in block["글"])
|
||||
|
||||
|
||||
def test_기계_자원_하나로(books):
|
||||
block = next(b for b in books["봉화"]["단산"] if b["코드"] == "D00002")
|
||||
machines = [r for r in block["자원"] if r["갈래"] == "X"]
|
||||
assert len(machines) == 1 # 노무비 · 재료비 · 경비 셋 줄
|
||||
assert machines[0]["이름"] == "굴삭기(무한궤도)" and machines[0]["규격"] == "0.2㎥"
|
||||
assert machines[0]["품"] == pytest.approx(0.0636537237529)
|
||||
|
||||
|
||||
def test_노임_이름_어긋남_0(books):
|
||||
names = hb.trade_names()
|
||||
for tag in FILES:
|
||||
sheets = hx._sheets(_path(tag).name, _path(tag).read_bytes())
|
||||
table = st._lists(sheets)
|
||||
res = [r for b in books[tag]["단산"] for r in b["자원"] if r["갈래"] == "L" and r["이름"]]
|
||||
assert res
|
||||
assert sum(1 for r in res if r["코드"] in table) >= len(res) // 2
|
||||
for r in res:
|
||||
if r["코드"] in table: # 목록표에 있는 직종 — X 열 이름과 같음
|
||||
assert st._flat(r["이름"]) == st._flat(table[r["코드"]]["이름"])
|
||||
if table.get(r["코드"], {}).get("단위") == "인": # L 코드 % 줄(기구손료) 뺌
|
||||
assert st._flat(r["이름"]) in names, (tag, r)
|
||||
labor = [x for x in _lines(books[tag]) if x["갈래"] == "노임"]
|
||||
assert all(st._flat(x["이름"]) in names for x in labor)
|
||||
|
||||
|
||||
def test_이음_열쇠_코드_칸():
|
||||
row = [None] * 26
|
||||
row[12], row[14], row[15] = "별산M00985", "M00985_1", "T7_1"
|
||||
assert st._code(row) == "M00985"
|
||||
row = [None] * 26
|
||||
row[25] = "D01352 →" # O 열 없는 판
|
||||
assert st._code(row) == "D01352"
|
||||
row = [None] * 47
|
||||
row[14], row[13] = "PRA01_1", "_x0007_`COD|PR_x0005_`EXI"
|
||||
assert st._code(row) == "PRA01"
|
||||
assert (
|
||||
st._code(["계", None, None, None, None, None, None, None] + [None] * 6 + ["T7_1"]) == "T7"
|
||||
)
|
||||
assert st._code([None] * 12 + ["D96"]) is None # 비고 글은 코드 아님
|
||||
|
||||
|
||||
def test_문서_보안_400(client):
|
||||
for tag in LOCKED:
|
||||
data = _path(tag).read_bytes()
|
||||
with pytest.raises(hx.ExcelError, match="문서 보안"):
|
||||
hx.read_book(_path(tag).name, data)
|
||||
res = _post(client, data=data, name=_path(tag).name)
|
||||
assert res.status_code == 400 and res.json()["detail"] == hx.DRM
|
||||
|
||||
|
||||
def test_여섯_파일_올라감(client):
|
||||
before = master_hash()
|
||||
sources = {tag: hashlib.sha256(_path(tag).read_bytes()).hexdigest() for tag in FILES}
|
||||
for at, tag in enumerate(FILES, 1):
|
||||
res = _post(client, data=_path(tag).read_bytes(), name=_path(tag).name)
|
||||
assert res.status_code == 200, res.text
|
||||
assert res.json()["호표수"] == HOPYOS[tag]
|
||||
doc = hs.read(layers.personal_dir(7, 42), f"HP-{at:06d}")
|
||||
assert doc["꼴"] == "STmate"
|
||||
assert sum(1 for b in doc["단산"] if not b["글"]) <= 3 # 봉화 무대처리 · 흙 쌓 기 = 값 0
|
||||
assert any(b["자원"] for b in doc["단산"])
|
||||
first = doc["호표"][0]["번"]
|
||||
view = client.get(f"{base.URL.format('personal')}/HP-{at:06d}/{first}")
|
||||
assert view.status_code == 200, view.text # 맞춤 보기가 STmate 줄을 받음
|
||||
assert {tag: hashlib.sha256(_path(tag).read_bytes()).hexdigest() for tag in FILES} == sources
|
||||
assert master_hash() == before
|
||||
Reference in New Issue
Block a user