Files
Aislo/resources/tester/test_707_1_bill_import.py
T

245 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""707-1 경쟁사 내역 줄 불러오기 — 읽기(`Bill_Excel`) · 짜기(`Bill_Import`) · 고정 단가 칸 셈 거울.
- 실무 xlsx(STmate 기번3 · 영월 · 봉화 · EST 거창) = 빠짐 0 · 불러온 뒤 직접비 합 = 엑셀 합 ·
어긋난 줄 0 · 조달수수료 = 율(줄로 안 실음) · DRM = 「읽을 수 없음」
- 수량 갈래 (가) take = 붙인 자동 줄 수량 엑셀 · auto_qty = B08 / (나) price = B08 · src_qty = 엑셀
- 두 번 셈 막음 = 이름 같은 자동 줄에 붙임(한 번만 셈) · drop = 남은 자동 줄 지움 ·
같은 파일 다시 = 갈아 끼움 · 조달수수료율 = 빈 때만
- 고정 단가 칸 = 연결이 단가를 못 낼 때만 · 다듬기 · TS 짝 같은 값
"""
import json
import re
import subprocess
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 B09_Estimation import B09_Estimation_Bill_Import as imp # noqa: E402
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
from B09_Estimation.B09_Estimation_Bill_Excel import BillExcelError, read_bill # noqa: E402
from B09_Estimation.B09_Estimation_Calc_Bill import compute_estimation # noqa: E402
K = ROOT / "resources" / "knowledge" / "original" / "실무문서"
FILES = {
"기번3": K / "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx", # noqa: E501
"영월": K / "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx",
"봉화": K / "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx",
"거창": K / "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx", # noqa: E501
} # fmt: skip
#: (꼴, 항목 수, 엑셀 직접비 합)
WANT = {
"기번3": ("STmate", 136, 710359136),
"영월": ("STmate", 108, 767148700),
"봉화": ("STmate", 77, 781283415),
"거창": ("EST", 60, 384916227),
}
EMPTY = {"materials": [], "bill": {"items": []}, "hopyo": {}}
ITEMS = [
{"key": "earth:4", "section": "1.2", "name": "절토(토사)", "unit": "㎥", "qty": 10,
"ref": {"tab": "sg_list", "key": "D01341"}},
{"key": "earth:9", "section": "1.6", "name": "노면 정리X", "unit": "㎡", "qty": 3,
"ref": {"tab": "sg_list", "key": "D01341"}},
{"key": "earth:6", "section": "1.2", "name": "암발파", "unit": "㎥", "qty": None,
"ref": {"tab": "sg_list", "key": "D01341"}},
{"key": "haul:3", "section": "3", "name": "모래", "unit": "㎥", "qty": 2,
"ref": {"tab": "sg_list", "key": "D01341"}},
] # fmt: skip
INPUTS = {"materials": [], "bill": {"items": ITEMS}, "hopyo": {"D01341": {
"labor": 1, "material": 0, "expense": 0}}} # fmt: skip
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
@pytest.fixture(scope="module")
def parsed():
return {k: read_bill(p.name, p.read_bytes()) for k, p in FILES.items()}
def _rows(inputs, user):
return compute_estimation(inputs, user, None, {})["bill"]
def _load(got, inputs=EMPTY, user=None, **kw):
user = user or {}
made, report = imp.apply(got, user, _rows(inputs, user)["rows"], "abcd1234", **kw)
made = store.clean_user(made)
return made, report, _rows(inputs, made)
@pytest.mark.parametrize("name", list(FILES))
def test_실무_내역_빠짐_0_합계_엑셀과_같음(parsed, name):
got = parsed[name]
kind, items, total = WANT[name]
assert (got["꼴"], sum(1 for x in got["줄"] if not x["묶음"])) == (kind, items)
assert got["합계"]["직접"] == total and got["조달수수료율"] == 0.54
made, report, bill = _load(got)
assert report["실음"] == len(got["줄"]) and report["붙임"] == 0
assert bill["totals"]["direct"]["sum"] == total and bill["totals"]["direct"]["blank"] == 0
assert imp.differences(got, report, bill["rows"]) == []
assert made["estimate"]["procurement_fee_pct"] == 0.54
assert max(r["level"] for r in bill["rows"]) <= 5
keys = {r["key"] for r in bill["rows"]}
assert all(k in keys for k in report["줄"].values()) # 실은 줄 모두 보임
# 관급 = 제외(관급자재대 아래 · 직접비 밖)
assert bill["totals"]["owner"]["net_material"] > 0
def test_EST_비고_참조_STmate_코드(parsed):
est = [x["호표"] for x in parsed["거창"]["줄"] if not x["묶음"]]
assert {"종류": "산근", "번": 46} in est and {"종류": "대가", "번": 8} in est
st = next(x for x in parsed["기번3"]["줄"] if x["이름"] == "규준틀설치")
assert st["호표"] == {"종류": "대가", "번": 5, "코드": "B00194"}
def test_DRM_읽을_수_없음():
with pytest.raises(BillExcelError, match="읽을 수 없음"):
read_bill("a.xlsx", b"\x9b DRMONE" + b"\x00" * 300)
with pytest.raises(BillExcelError, match="엑셀"):
read_bill("a.xlsx", b"PK\x03\x04 broken")
def test_가_넘겨받기_붙인_자동_줄_수량_엑셀(parsed):
made, report, bill = _load(parsed["기번3"], INPUTS, mode="take")
rows = {r["key"]: r for r in bill["rows"]}
assert report["붙임"] == 2 # 절토(토사) 첫 줄만(1공구) · 암발파 · 운반 「모래」 = 안 붙음
cut = rows["earth:4"]
assert (cut["qty"], cut["auto_qty"], cut["src_qty"]) == (5675, 10, 5675)
assert cut["fixed"] and cut["sum"] == 11003825 # 엑셀 단가 · 연결 덮개 null
assert cut["parent"].startswith("hand:xabcd1234-") # 불러온 자리로 옮김
assert sum(1 for r in bill["rows"] if r["name"] == "절토(토사)") == 3 # 두 번 셈 없음
# 붙인 줄 금액 = 엑셀 · 안 붙은 자동 줄(노면 정리X 3 · 운반 모래 2) 그대로 = 직접비 = 엑셀 + 5
assert bill["totals"]["direct"]["sum"] == 710359136 + 5
assert "haul:3" in rows and rows["haul:3"]["parent"] == "sec:3"
tops = [r["key"] for r in bill["rows"] if r["parent"] is None]
assert tops.index("hand:xabcd1234-00006") < tops.index("sec:G") # 1공구 = 관급자재대 앞
def test_나_단가만_B08_이김_비교_칸(parsed):
made, report, bill = _load(parsed["기번3"], INPUTS, mode="price")
cut = {r["key"]: r for r in bill["rows"]}["earth:4"]
assert (cut["qty"], cut["src_qty"], "auto_qty" in cut) == (10, 5675, False)
assert cut["sum"] == 1939 * 10 # 엑셀 단가 × B08 수량
blast = {r["key"]: r for r in bill["rows"]}["earth:6"]
assert (blast["qty"], blast["sum"]) == (3889, 43700693) # B08 수량 없음 = 엑셀 수량
diff = imp.differences(parsed["기번3"], report, bill["rows"])
assert [d["까닭"] for d in diff] == ["수량 = B08(단가만)"]
def test_붙이지_않음_지우기_다시_부르기(parsed):
_, report, bill = _load(parsed["기번3"], INPUTS, attach=False, drop=True)
assert (report["붙임"], report["지움"]) == (0, 4) # 운반 줄도 지움
assert bill["totals"]["direct"]["sum"] == 710359136
made, _, _ = _load(parsed["기번3"], INPUTS)
made["bill"]["rows"].append({"key": "hand:mine", "parent": "hand:xabcd1234-00007",
"name": "내 줄", "qty": 1, "ref": None}) # fmt: skip
again, report, bill = _load(parsed["기번3"], INPUTS, user=made)
keys = [e["key"] for e in again["bill"]["rows"]]
assert len(keys) == len(set(keys)) and report["붙임"] == 2
assert bill["totals"]["direct"]["sum"] == 710359136 + 5 # 갈아 끼움 · 두 번 안 셈
mine = next(e for e in again["bill"]["rows"] if e["key"] == "hand:mine")
assert "parent" not in mine # 내 줄 = 남음(맨 위)
def test_조달수수료율_빈_때만(parsed):
made, _, _ = _load(parsed["거창"], user={"estimate": {"procurement_fee_pct": 1.0}})
assert made["estimate"]["procurement_fee_pct"] == 1.0
def test_고정_단가_칸_다듬기_연결이_이김():
raw = {"bill": {"v": 2, "rows": [
{"key": "hand:a", "name": "a", "qty": 2, "ref": {"tab": "sg", "key": "SG:1"},
"price": {"labor": 5, "material": "x"}, "src_qty": 3, "note": "대가 5호표"},
{"key": "earth:4", "price": {"expense": 7}, "src_qty": "y"},
]}} # fmt: skip
user = store.clean_user(raw)
hand, auto = user["bill"]["rows"]
assert hand["price"] == {"labor": 5, "material": 0, "expense": 0} and hand["src_qty"] == 3
assert hand["note"] == "대가 5호표" and auto == {"key": "earth:4", "price": {
"labor": 0, "material": 0, "expense": 7}} # fmt: skip
books = {"sg_list": [{"no": 1, "key": "SG:1", "labor": 100, "material": 0, "expense": 0,
"sum": 100}]} # fmt: skip
rows = {r["key"]: r for r in compute_estimation(INPUTS, user, None, books)["bill"]["rows"]}
assert (rows["hand:a"]["sum"], rows["hand:a"].get("fixed")) == (200, None) # 연결 이김
assert rows["earth:4"]["sum"] == 10 and "fixed" not in rows["earth:4"] # 자동 연결 이김
rows = {r["key"]: r for r in compute_estimation(INPUTS, user, None, {})["bill"]["rows"]}
assert (rows["hand:a"]["sum"], rows["hand:a"]["fixed"]) == (10, True)
assert rows["hand:a"]["note"] == "대가 5호표"
def test_거울_TS_가_같은_값(parsed):
calc = "".join(
IMPORTS.sub("", (ROOT / "B09_Estimation" / f"B09_Estimation_{n}.ts").read_text("utf-8"))
for n in ("Calc_Base", "Calc_Bill")
)
users = [
_load(parsed["거창"], INPUTS, mode="take")[0],
_load(parsed["영월"], INPUTS, mode="price")[0],
{"bill": {"v": 2, "rows": [{"key": "earth:4", "ref": None, "price": {"labor": 3}},
{"key": "hand:a", "qty": 2, "price": {"material": 4}}]}},
] # fmt: skip
body = (
f"\nconst i = {json.dumps(INPUTS, ensure_ascii=False)};"
f"\nconst us = {json.dumps(users, ensure_ascii=False)};"
"\nconsole.log(JSON.stringify(us.map((u) => computeEstimation(i, u, null, {}))));"
)
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript"],
input=calc + body, capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == [compute_estimation(INPUTS, u, None, {}) for u in users]
def test_이음_같은_책_일위대가_이음_단가_어긋남_알림(parsed):
got = parsed["기번3"]
link = {"tab": "dg", "key": "DG:5", "lib": "precombo@personal@1-3@SC-000001"}
made, report, _ = _load(got, links={5: link})
lines = [e for e in made["bill"]["rows"] if e.get("ref")]
assert report["이음"] == 3 and len(lines) == 3 # 대가 5호표(종단규준틀) 공구마다
assert lines[0]["ref"] == link and lines[0]["price"]["labor"] == 21936 # 고정 단가 곁에 남김
same = {"daega": [{"no": 5, "key": "DG:5", "labor": 21936, "material": 3885, "expense": 0,
"sum": 25821}]} # fmt: skip
rows = compute_estimation(EMPTY, made, None, same)["bill"]["rows"]
assert imp.differences(got, report, rows) == [] # 실무자료 사본 = 엑셀 단가 그대로
other = {"daega": [{**same["daega"][0], "labor": 30000, "sum": 33885}]}
rows = compute_estimation(EMPTY, made, None, other)["bill"]["rows"]
diff = imp.differences(got, report, rows)
assert len(diff) == 3 and {d["까닭"] for d in diff} == {"이음 단가 = 우리 셈"}
# 사본이 없어짐(연결이 단가를 못 냄) = 고정 단가
rows = compute_estimation(EMPTY, made, None, {})["bill"]["rows"]
assert imp.differences(got, report, rows) == []
def test_이음_찾기_같은_파일_이름_본뜬만(tmp_path, monkeypatch):
from B09_Estimation import B09_Estimation_Router_Import as route
folder = tmp_path / "precombo"
folder.mkdir()
docs = {
"SC-000001": {"층": "실무자료", "키": "HP-000001 제5호표", "파일": "a.xlsx"},
"SC-000002": {"층": "실무자료", "키": "HP-000001 제5호표 2째 줄", "파일": "a.xlsx"},
"SC-000003": {"층": "실무자료", "키": "HP-000002 제8호표", "파일": "b.xlsx"},
"SC-000004": {"층": "실무자료", "키": "HP-000001 제9호표", "파일": "a.xlsx"},
}
for key, origin in docs.items():
(folder / f"{key}.json").write_text(json.dumps({"본뜬": origin}), encoding="utf-8")
class Got:
layer, owner = "personal", "1/3"
def grant(session, layer):
if layer != "personal":
raise route.HTTPException(status_code=409)
return Got
monkeypatch.setattr(route.access, "grant", grant)
monkeypatch.setattr(route.refs, "folder_of", lambda layer, owner: tmp_path)
found = route._found({}, "a.xlsx", {5, 8})
assert found == {5: ("personal", "1/3", "SC-000001")} # 감싼 줄 · 다른 파일 · 안 쓰는 번 뺌