Files
Aislo/resources/tester/test_707_1_bill_import.py
T
eomsangdonandClaude Opus 5.5 c0ea73501f feat(B09 설계내역서): 경쟁사 내역 엑셀 줄 불러오기 서버 — 읽기 · 짜기 · 고정 단가 칸 (PLAN 707-1)
- 읽기(Bill_Excel) — STmate 「설계내역서」 숨은 코드 · 부모 코드 열 · EST 「내역서」 공종 번호 깊이 · 비고 대가 · 산근 · 단산 · 일위 · 제외 묶음(관급 · 제외금액) · 조달수수료 = 율 · DRM = 「읽을 수 없음」 · 열 머리 = M02 실무자료 칸 읽기 재사용
- 짜기(Bill_Import) — 공종 = 새 묶음 · 항목 = 새 줄 + 고정 단가 칸 · 수량 갈래 take · price · 이름 같은 자동 줄 붙임 · 남은 자동 줄 지움 · 같은 파일 다시 = 갈아 끼움 · 어긋난 줄 목록
- 길(Router_Import) — import/bill(file · file_key) 미리 보기 · import/bill/apply(같은 파일 이름 실무자료 사전조합 → 서고 사본 연결 · 층 쓰기 0) · 저장 안 함
- 셈 짝(Calc_Bill py · ts) — 줄 price = 연결이 단가를 못 낼 때 금액 · fixed · src_qty · Store 다듬기
- 확인 = 기번3 710,359,136 · 영월 767,148,700 · 봉화 781,283,415 · 거창 384,916,227 직접비 합 = 엑셀 · 빈 줄 0 · 어긋남 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:43:51 +09:00

188 lines
9.4 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",
"영월": K / "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx",
"봉화": K / "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx",
"거창": K / "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx",
} # 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"}},
] # 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["붙임"] == 1 # 절토(토사) 첫 줄만(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
assert bill["totals"]["direct"]["sum"] == 710359136 + 3
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 수량
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, 2)
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["붙임"] == 1
assert bill["totals"]["direct"]["sum"] == 710359136 + 3 # 갈아 끼움 · 두 번 안 셈
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]