feat(812-1): 엑셀 읽기기 새로 — xlsx · xls → 810-1 구조 줄 꼴 → 원파일 길 한 벌 · 골든 시험 · 올림 길 교체
- M02_San_Xlsx(공통 · 글자 칸 배치 · 금액 칸 · 자원 표) · M02_San_Xlsx_Est(EST 글 → 표지: 변수 = 가장 늦은 대입 값 하나 · 칸 나눔 꼴은 닫는 표지 2 칸 · 합계 줄 · 세 줄 머리 · 노임 · 자재 단가 짝 · 소계 61 · 율 58 · 62 · 구간 글자) · M02_San_Xlsx_Stmate(숨은 G 열 원문 = stc_line · 끝전 파일 투표) - M02_Doc_FromNative: 엑셀 둘 꼴(번 · 시트 행 · 원본 엑셀 참 · 원파일 거짓) · M02_Doc_Import: 엑셀 길도 소계 비목 칸 비움 · 추정 칸(출처 = 엑셀 꼬리표) - M02_Sangeun_FromHopyo: 산근 블록 = 새 읽기기(옛 San_Est · Emit.est 부름 걷음 · 옛 저장은 한 번 지어 둠) - test_812_1: 실제 짝(거창 · STC 다섯) 같음 · 합성 배치 = 거창 실제 · 합성 EST 1,849 · STC 371 · 금액 실무 11(엇갈림 고정 목록) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
"""812-1 골든 — 엑셀 읽기기(`M02_San_Xlsx`) = 810-1 원파일 파서와 같은 구조 줄(읽기만).
|
||||
|
||||
① 실제 짝 — 거창 xlsx ↔ 거창 .estx(블록 머리 · 줄마다 연결뜻 · 비목 · 변수 · 줄참조 · 결과 · 합계 · 저장 ·
|
||||
자원 이름 · 규격 · 단위) · STC 다섯 xlsx ↔ .stc(같은 블록 · 원문 · 줄 칸 · 자원 이름 · 규격 · 끝전 = SAN_TYP)
|
||||
② 합성 — 원파일 구조 줄 보임 글 + 저장 금액으로 경쟁사 내보내기 꼴 시트를 지어(EST = 거창 꼴 글자 칸 나눔 ·
|
||||
닫는 표지 2 칸 · 합계 표지 1 칸 · 소계 61 = 비목 칸만 — 거창 실제와 글 배치 같음(수 꼴 빼고) 먼저 확인) →
|
||||
읽기기 → 원파일 구조 줄과 같음(EST 1,849 · STC 371 블록) · 원파일 자체 엇갈림(같은 글 · 다른 표지)은
|
||||
`ODD` 로 고정
|
||||
③ 금액 — 실무 엑셀 11 = 읽기기 → `M02_Doc_FromNative` → 수식 문서 셈 = 엑셀 블록 금액(`ODD_MONEY` 고정)
|
||||
빠른 견본 기본 · 전수 = 환경 S8121_ALL=1 · 파일 없으면 skip(다른 PC).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from collections import Counter
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HERE = Path(__file__).resolve().parent
|
||||
for p in (ROOT, HERE):
|
||||
if str(p) not in sys.path:
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
import helper_m02_hopyo_golden as golden # noqa: E402
|
||||
import test_797_1a_import_flags as practice # noqa: E402
|
||||
import test_810_1_estx as estx # noqa: E402
|
||||
import test_810_1_stc as stc # noqa: E402
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_FromNative as fn # noqa: E402
|
||||
from M02_MasterTemplete import M02_San_Book as book # noqa: E402
|
||||
from M02_MasterTemplete import M02_San_Xlsx as sx # noqa: E402
|
||||
|
||||
ALL = os.environ.get("S8121_ALL") == "1"
|
||||
F = sx.flat
|
||||
FIELDS = ("연결뜻", "비목", "변수", "줄참조", "결과", "합계")
|
||||
#: 원파일 자체 엇갈림 — 같은 보인 글 · 금액 칸인데 원파일 표지가 다른 줄(엑셀이 못 가름 · 고정)
|
||||
ODD = {
|
||||
("main", 638, 30), # 산근 세 줄 뒤 「소 계」 연결 없음(같은 꼴 다른 곳 = 104)
|
||||
("main", 1249, 230), # 같음
|
||||
("main", 1199, 40), # 「0 × 1% = A0」 금액 0 — 비목 S 가 금액 칸에 없음
|
||||
("main", 1438, 4), # 「{ 골재장 상차도 }」 글 줄에 비목 M
|
||||
("main", 1286, 4), # 괄호 속 「L = … = 46.00 m」 결과 표지 없음(같은 꼴 다른 곳 = 결과)
|
||||
("main", 1391, 6), # 「= 5.714㎡ / 100㎡ =」 중간 값에 결과 표지
|
||||
*(("main", 1308, r) for r in (8, 16, 26)), # 금액 칸 없는 「소 계 : 수 kg」 = 61(861 = 글)
|
||||
*(("main", 1441, r) for r in (64, 65, 66, 67)), # 단가가 기계표와 다른 세 줄 · 비목 없음
|
||||
}
|
||||
#: 금액 — 셈 ≠ 엑셀 블록(까닭 고정)
|
||||
ODD_MONEY = {
|
||||
(
|
||||
"북부청임도",
|
||||
33,
|
||||
), # 노무비 줄 「36,248 ÷ 0.096」 오타 — 절 소계 덧셈이 그 줄을 뺌(글 덧셈만 앎)
|
||||
(
|
||||
"북부청임도",
|
||||
3,
|
||||
), # 아래 넷 = `M02_Doc_NativeEst.triple` 자리 규칙 dict 묶음 TypeError(812-3 몫)
|
||||
("북부청임도", 54),
|
||||
("북부청임도", 55),
|
||||
("북부청사방", 24),
|
||||
}
|
||||
|
||||
|
||||
# ── 합성(경쟁사 내보내기 꼴) ─────────────────────────────────────────────
|
||||
W, MC = 146, [141, 142, 143, 144]
|
||||
_OPEN = dict.fromkeys("\x03\x0e\x11\x12\x13\x15\x0c\x06\x07", 0) | {"\x1a": 1}
|
||||
_CLOSE = dict.fromkeys("\x03\x0e\x11\x12\x13\x15\x0c\x06\x07", 2) | {"\x1a": 1}
|
||||
|
||||
|
||||
def _w(ch: str) -> int:
|
||||
return 2 if unicodedata.east_asian_width(ch) in "WFA" else 1
|
||||
|
||||
|
||||
def _cls(ch: str) -> str:
|
||||
if ch.isdigit() or ch in ",.":
|
||||
return "n"
|
||||
o = ord(ch)
|
||||
if 0xAC00 <= o <= 0xD7A3 or 0x3131 <= o <= 0x318E or 0x3300 <= o <= 0x33FF:
|
||||
return "h"
|
||||
return "a" if ch.isascii() and ch.isalpha() else "s"
|
||||
|
||||
|
||||
def cells(text: str) -> list[tuple[int, str]]:
|
||||
"""LineData → 글자 칸(열 = 화면 자리 · 여는 표지 0 · 닫는 표지 2 · 합계 표지 1 칸 · 표지 안 = 한 칸)."""
|
||||
out: list[list] = []
|
||||
col, cur, opened, angle = 2, None, set(), False
|
||||
for i, ch in enumerate(text):
|
||||
if ord(ch) < 32:
|
||||
col += _CLOSE.get(ch, 0) if ch in opened else _OPEN.get(ch, 0)
|
||||
opened ^= {ch}
|
||||
cur = None
|
||||
continue
|
||||
whole = bool(opened & set("\x0e\x13\x15")) or angle
|
||||
if ch == "<" and ">" in text[i:]:
|
||||
angle, whole = True, False
|
||||
if ch == " " and not (whole and cur):
|
||||
cur, col = None, col + 1
|
||||
continue
|
||||
c = _cls(ch)
|
||||
joined = cur is not None and (
|
||||
whole
|
||||
or (c == cur[2] and c != "s")
|
||||
or (cur[2] == "a" and ch.isdigit())
|
||||
or (ch == ":" and cur[2] == "h")
|
||||
)
|
||||
if joined:
|
||||
cur[1] += ch
|
||||
else:
|
||||
cur = [col, ch, c]
|
||||
out.append(cur)
|
||||
angle = angle and ch != ">"
|
||||
col += _w(ch)
|
||||
return [(a, b.rstrip()) for a, b, _ in out]
|
||||
|
||||
|
||||
def _fmt(v: float | None) -> str | None:
|
||||
return None if not v else f"{v:,.6f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
def _money(row: list, saved: dict | None, link: int | None) -> None:
|
||||
s = saved or {}
|
||||
if not any(s.get(c) for c in "TLMS"):
|
||||
return
|
||||
if link != 61 and s.get("T"):
|
||||
row[MC[0]] = int(s["T"]) if s["T"] == int(s["T"]) else s["T"]
|
||||
for c, j in zip("LMS", MC[1:]):
|
||||
row[j] = _fmt(s.get(c))
|
||||
|
||||
|
||||
def _table(title: str, head: dict, recs: list[dict]) -> list[list]:
|
||||
rows = [[None] * 30 for _ in range(3)]
|
||||
rows[0][1] = title
|
||||
for j, h in head.items():
|
||||
rows[2][j] = h
|
||||
for r in recs:
|
||||
rows.append([r.get(j) for j in range(30)])
|
||||
return rows
|
||||
|
||||
|
||||
def synth_est(d: dict) -> dict[str, list]:
|
||||
"""원파일 EST → 엑셀 꼴 시트(단가산출 · 총괄표 · 노임 · 자재 · 기계 · 견적)."""
|
||||
rows = [[None] * W for _ in range(3)]
|
||||
rows[0][1], rows[2][1] = "단가산출", "산 출 근 거"
|
||||
labels = ("합 계", "노무비", "재료비", "경 비")
|
||||
for j, h in [*zip(MC, labels), *zip((2, 3, 4, 5), labels)]:
|
||||
rows[2][j] = h
|
||||
tot = []
|
||||
for n, b in enumerate(d["산근"], 1):
|
||||
head = [None] * W
|
||||
size = f"({b['규격']})" if b.get("규격") else ""
|
||||
head[1] = f"{n}.{b['이름'] or ''}{size}[{b.get('단위') or ''}]"
|
||||
_money(head, b["저장"], None)
|
||||
rows.append(head)
|
||||
for ln in b["줄"]:
|
||||
row = [None] * W
|
||||
for j, v in cells(ln["글"]):
|
||||
if j < MC[0]:
|
||||
row[j] = v
|
||||
_money(row, ln["저장"], ln["연결"])
|
||||
rows.append(row)
|
||||
s = b["저장"] or {}
|
||||
one = {1: str(n), 2: b["이름"], 3: b.get("규격"), 4: b.get("단위"), 5: s.get("T")}
|
||||
one |= {6: _fmt(s.get("L")), 7: _fmt(s.get("M")), 8: _fmt(s.get("S"))}
|
||||
tot.append(one | {25: s.get("T"), 26: s.get("L"), 27: s.get("M"), 28: s.get("S")})
|
||||
head4 = {
|
||||
1: "No",
|
||||
2: "품명",
|
||||
3: "규격",
|
||||
4: "단위",
|
||||
5: "합계",
|
||||
6: "노무비",
|
||||
7: "재료비",
|
||||
8: "경비",
|
||||
}
|
||||
out = {"단가산출": rows, "단가산출총괄표": _table("단가산출총괄표", head4, tot)}
|
||||
recs: dict[str, list] = {"노임": [], "자재": [], "기계": [], "견적": []}
|
||||
for key, v in d["자원"].items():
|
||||
kind, s = key.split(":")[0], v["저장"] or {}
|
||||
base = {2: v["이름"], 3: v["규격"], 4: v["단위"]}
|
||||
if kind == "노임":
|
||||
recs[kind].append(base | {7: s.get("T")})
|
||||
elif kind == "자재":
|
||||
recs[kind].append(base | {19: s.get("T")})
|
||||
elif kind in ("기계", "견적"):
|
||||
one = base | {5: s.get("T"), 6: s.get("L"), 7: s.get("M"), 8: s.get("S")}
|
||||
recs[kind].append(
|
||||
one | {25: s.get("T"), 26: s.get("L"), 27: s.get("M"), 28: s.get("S")}
|
||||
)
|
||||
for kind in recs:
|
||||
for i, r in enumerate(recs[kind], 1):
|
||||
r[1] = str(i)
|
||||
one_price = {1: "No", 2: "품명", 3: "규격", 4: "단위"}
|
||||
out["노임단가"] = _table("노임단가", one_price | {7: "적용단가"}, recs["노임"])
|
||||
out["자재단가"] = _table("자재단가", one_price | {19: "적용단가"}, recs["자재"])
|
||||
out["기계경비총괄표"] = _table("기계경비총괄표", head4, recs["기계"])
|
||||
out["견적단가"] = _table("견적단가", head4, recs["견적"])
|
||||
return out
|
||||
|
||||
|
||||
def synth_stmate(n: dict) -> dict[str, list]:
|
||||
"""원파일 STC → 엑셀 꼴 시트(단가산출근거 숨은 G 열 원문 · 목록표 숨은 메타)."""
|
||||
rows: list[list] = [[None] * 12 for _ in range(3)]
|
||||
for b in n["산근"]:
|
||||
s, at = b["저장"], len(rows) + 3
|
||||
head = [b["코드"], None, s["T"], s["L"], s["M"], s["S"], None, None, None, None, None, None]
|
||||
head[6] = f"BDCOD|{b['코드']}\x05POSS|{at}\x05POSE|{at + len(b['줄']) - 1}"
|
||||
rows += [head, [None, b["이름"]] + [None] * 10]
|
||||
rows += [[None] * 6 + [ln["글"]] + [None] * 5 for ln in b["줄"]]
|
||||
out = {"단가산출근거": rows}
|
||||
lists: dict[str, list] = {k: [] for k in ("노무비", "재료비", "경비", "단가산출근거")}
|
||||
for code, v in n["자원"].items():
|
||||
p = n["단가"].get(code) or {}
|
||||
meta = "\x05".join(
|
||||
[f"COD|{code}"]
|
||||
+ [f"{k}|{x}" for k, x in (p.get("meta") or {}).items() if x is not None]
|
||||
)
|
||||
r = [None, v["이름"], v["규격"], v["단위"], p.get("T")] + [None] * 7
|
||||
if code[:1] in "LMS":
|
||||
r[6] = meta
|
||||
lists[{"L": "노무비", "M": "재료비", "S": "경비"}[code[:1]]].append(r)
|
||||
else:
|
||||
r[5:8], r[9] = [p.get("L"), p.get("M"), p.get("S")], meta
|
||||
lists["단가산출근거"].append(r)
|
||||
for k, v in lists.items():
|
||||
out[f"{k}목록표"] = [[None] * 12] * 3 + v
|
||||
return out
|
||||
|
||||
|
||||
# ── 맞대기 ─────────────────────────────────────────────────────────────
|
||||
def _res(r: dict | None, unit: bool = True) -> tuple | None:
|
||||
if not r:
|
||||
return None
|
||||
return (r.get("종류"), F(r.get("이름")), F(r.get("규격")), F(r.get("단위")) if unit else "")
|
||||
|
||||
|
||||
def _vals(v: list) -> list:
|
||||
return [round(x, 6) for x in v]
|
||||
|
||||
|
||||
def _saved(s: dict | None) -> tuple:
|
||||
return tuple(round(float((s or {}).get(c) or 0), 6) for c in "TLMS")
|
||||
|
||||
|
||||
def diff_est(tag: str, mine: dict, nat: dict) -> set[tuple]:
|
||||
"""EST 블록 · 줄 맞대기 → 다른 (파일, 블록, 행) · 블록 머리 다름 = 행 0."""
|
||||
bad = set()
|
||||
assert len(mine["산근"]) == len(nat["산근"])
|
||||
for n, (a, b) in enumerate(zip(mine["산근"], nat["산근"]), 1):
|
||||
if (F(a["이름"]), F(a["규격"]), a["단위"], _saved(a["저장"])[:4]) != (
|
||||
F(b["이름"]),
|
||||
F(b["규격"]),
|
||||
b["단위"],
|
||||
_saved(b["저장"])[:4],
|
||||
):
|
||||
bad.add((tag, n, 0))
|
||||
body = [x["보임"].strip() for x in b["줄"] if x["보임"].strip()]
|
||||
assert len([x for x in a["줄"] if x["보임"].strip()]) == len(body)
|
||||
for x, y in zip(a["줄"], b["줄"]):
|
||||
same = all(
|
||||
_vals(x[f]) == _vals(y[f]) if f in ("변수", "줄참조", "결과") else x[f] == y[f]
|
||||
for f in FIELDS
|
||||
)
|
||||
same = same and _saved(x["저장"]) == _saved(y["저장"])
|
||||
mine_r, want_r = _res(x["자원"]), _res(y["자원"])
|
||||
same = same and (mine_r == want_r or (want_r and want_r[0] == "산근" and mine_r))
|
||||
if not same:
|
||||
bad.add((tag, n, y["행"]))
|
||||
return bad
|
||||
|
||||
|
||||
def diff_stc(mine: dict, nat: dict) -> Counter:
|
||||
"""STmate — 같은 코드 블록 · 원문 · 줄 칸 · 자원 이름 · 규격(단위 = 원파일 COSTN 은 기호 글자) 다른 수."""
|
||||
got: Counter = Counter()
|
||||
have = {b["코드"]: b for b in nat["산근"]}
|
||||
for b in mine["산근"]:
|
||||
y = have.get(b["코드"])
|
||||
if y is None:
|
||||
got["원파일 없는 블록"] += 1
|
||||
continue
|
||||
got["블록"] += 1
|
||||
if [ln["글"].strip() for ln in b["줄"]] != [ln["글"].strip() for ln in y["줄"]]:
|
||||
got["원문 다름"] += 1
|
||||
continue
|
||||
got["저장 다름"] += _saved(b["저장"])[:4] != _saved(y["저장"])[:4]
|
||||
for x, z in zip(b["줄"], y["줄"]):
|
||||
got["줄"] += 1
|
||||
fields = ("연결뜻", "비목", "변수", "줄참조", "합계", "기계", "산근")
|
||||
got["줄 다름"] += any(x[f] != z[f] for f in fields)
|
||||
got["자원 다름"] += _res(x["자원"], False) != _res(z["자원"], False)
|
||||
return got
|
||||
|
||||
|
||||
# ── ① 실제 짝 ─────────────────────────────────────────────────────────
|
||||
@lru_cache(maxsize=4)
|
||||
def excel(tag: str) -> dict:
|
||||
path = practice.FILES.get(tag)
|
||||
if not path or not path.exists():
|
||||
pytest.skip(f"실무 엑셀 없음 {tag}")
|
||||
return sx.read(path.name, path.read_bytes())
|
||||
|
||||
|
||||
def test_real_pair_geochang_equals_estx() -> None:
|
||||
assert diff_est("거창", excel("거창"), estx.parsed("거창")) == set()
|
||||
|
||||
|
||||
STC_PAIRS = ("봉화", "동부청영월", "울진대흥", "영덕계류", "울진소광")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag", STC_PAIRS)
|
||||
def test_real_pair_stmate_equals_stc(tag: str) -> None:
|
||||
mine, nat = excel(tag), stc.parsed(tag)
|
||||
got = diff_stc(mine, nat)
|
||||
assert got["블록"] and got["줄"]
|
||||
assert (got["원문 다름"], got["저장 다름"], got["줄 다름"], got["자원 다름"]) == (0, 0, 0, 0), (
|
||||
got
|
||||
)
|
||||
assert mine["옵션"]["SAN_TYP"] == nat["옵션"]["SAN_TYP"] # 끝전 파일 투표 = 원파일 옵션
|
||||
|
||||
|
||||
# ── ② 합성 ────────────────────────────────────────────────────────────
|
||||
def _num_norm(text: str) -> str:
|
||||
num = re.compile(r"\d[\d,]*(?:\.\d*)?|\.\d+")
|
||||
out = num.sub(lambda m: repr(float(m.group(0).replace(",", "").rstrip(".") or 0)), text)
|
||||
return re.sub(r"\s+", "", out)
|
||||
|
||||
|
||||
def test_synth_layout_equals_real_geochang() -> None:
|
||||
"""합성 규칙 검증 — 거창 실제 시트 줄 = 원파일 LineData 합성 칸(글 배치 · 수 꼴 「8.」 · 「.1」 빼고)."""
|
||||
path = golden.FILES["거창"]
|
||||
if not path.exists():
|
||||
pytest.skip("거창 엑셀 없음")
|
||||
rows = book.sheets(path.name, path.read_bytes())["단가산출"]
|
||||
heads = [
|
||||
i
|
||||
for i, r in enumerate(rows)
|
||||
if isinstance(r[1], str) and re.match(r"^\d+\.", r[1]) and r[141]
|
||||
]
|
||||
nat = estx.parsed("거창")["산근"]
|
||||
bad = total = 0
|
||||
for k, (h, b) in enumerate(zip(heads, nat)):
|
||||
end = heads[k + 1] if k + 1 < len(heads) else len(rows)
|
||||
for ln, r in zip(b["줄"], rows[h + 1 : end]):
|
||||
real = [(j, sx._cell(v)) for j, v in enumerate(r[2:141], 2) if sx._cell(v).strip()]
|
||||
mine = sx.layout(real, 2)
|
||||
total += 1
|
||||
bad += _num_norm(mine) != _num_norm(sx.layout(cells(ln["글"]), 2))
|
||||
got = sx.money_of(r, MC) or {}
|
||||
want = {c: v for c, v in (ln["저장"] or {}).items() if v and c in "TLMS"}
|
||||
if ln["연결"] == 61:
|
||||
want.pop("T", None)
|
||||
bad += {c: round(v, 6) for c, v in got.items()} != {
|
||||
c: round(v, 6) for c, v in want.items()
|
||||
}
|
||||
assert total > 2000 and bad == 0
|
||||
|
||||
|
||||
EST_SAMPLE = ("거창", "통영")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag", list(estx.FILES) if ALL else EST_SAMPLE)
|
||||
def test_synth_est_equals_native(tag: str) -> None:
|
||||
nat = estx.parsed(tag)
|
||||
got = diff_est(tag, sx.read_sheets(synth_est(nat)), nat)
|
||||
assert got == {x for x in ODD if x[0] == tag}, sorted(got)[:10]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag", list(stc.FILES) if ALL else ("봉화",))
|
||||
def test_synth_stmate_equals_native(tag: str) -> None:
|
||||
nat = stc.parsed(tag)
|
||||
mine = sx.read_sheets(synth_stmate(nat))
|
||||
got = diff_stc(mine, nat)
|
||||
assert got["블록"] == len(nat["산근"]) and got["원파일 없는 블록"] == 0
|
||||
assert (got["원문 다름"], got["저장 다름"], got["줄 다름"], got["자원 다름"]) == (0, 0, 0, 0), (
|
||||
got
|
||||
)
|
||||
assert mine["옵션"]["SAN_TYP"] == nat["옵션"]["SAN_TYP"]
|
||||
|
||||
|
||||
# ── ③ 금액 ────────────────────────────────────────────────────────────
|
||||
def sweep(tag: str) -> dict:
|
||||
"""엑셀 → 읽기기 → 둘 꼴 → 블록마다 수식 문서 셈 = 엑셀 블록 금액(낙찰률 = 계약단가 뒤)."""
|
||||
import test_810_2_native_doc as nd
|
||||
|
||||
path = practice.FILES[tag]
|
||||
saved = fn.pack(excel(tag), path.name)
|
||||
out = {"블록": 0, "같음": 0, "다름": set(), "변수줄": 0, "자원줄": 0, "자원추정": 0}
|
||||
for ln in (x for b in saved["산근"] for x in b["줄"]):
|
||||
out["변수줄"] += bool(ln.get("변수"))
|
||||
out["자원줄"] += bool(ln.get("자원"))
|
||||
for no, blk in fn.numbers(saved).items():
|
||||
out["블록"] += 1
|
||||
try:
|
||||
emitted = fn.emit(saved, no)
|
||||
_, _, got = nd.doc_and_money(emitted)
|
||||
except TypeError:
|
||||
out["다름"].add((tag, no))
|
||||
continue
|
||||
mine = {c: got.get(k) for c, k in nd.COST}
|
||||
nag = (emitted["원본"].get("계약") or {}).get("낙찰률")
|
||||
if nag:
|
||||
for c in "LMS":
|
||||
mine[c] = math.floor(round(float(mine[c]) * nag / 100, 6))
|
||||
mine["T"] = mine["L"] + mine["M"] + mine["S"]
|
||||
want = blk.get("저장") or {}
|
||||
if all(abs(float(mine[c] or 0) - float(want.get(c) or 0)) < 0.5 for c, _ in nd.COST):
|
||||
out["같음"] += 1
|
||||
else:
|
||||
out["다름"].add((tag, no))
|
||||
assert emitted["원본"]["엑셀"] is True and not emitted["원본"]["원파일"]
|
||||
return out
|
||||
|
||||
|
||||
MONEY_SAMPLE = ("거창", "봉화")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag", list(practice.FILES) if ALL else MONEY_SAMPLE)
|
||||
def test_money_equals_excel(tag: str) -> None:
|
||||
got = sweep(tag)
|
||||
assert got["블록"] and got["다름"] == {x for x in ODD_MONEY if x[0] == tag}, sorted(got["다름"])
|
||||
Reference in New Issue
Block a user