- M02_Doc_NativeEst.triple: 세 줄 수량 식이 같은지 = repr 로 맞댐(자리 규칙 dict 가 set 에 안 들어가 TypeError — 엑셀 줄은 보인 값이 반올림 자리를 타 원파일에 없던 꼴 · 812-3 끝나 브레인이 맡김) - 엑셀 읽기기: 절 소계 덧셈이 뺀 오타 줄 = 비목 없음(블록 금액도 그래야 맞을 때만) · 세 줄 계 금액 칸 비목 하나 = 옮김 · 소계 61 = 금액 칸 비목 하나 · 「.1」 수 · 총괄표 당초 칸 표지 · STmate 엑셀 줄 금액 · 계 꼬리(검산 줄) - 화면: 가져온 식 규격 칸 꼬리표 = 원파일 · 엑셀 갈림(line_stack_sourced) - 시험: 793_1 지움(옛 EST · STmate 엑셀 글 추정 엔진 · 812_1 골든이 대신) · 793_2_san_emit EST 시험 지움(STmate 남김) · 797_1a 옛 표지 ① ② · 줄 단위 합격 · 표본 밖 수 · 버림1 감시 지움(옛 엔진) · 810_1 (c) 엑셀 엔진 대조 지움 · 793_2 실무 블록 · 793_3b 끝전 = 새 길 · 812_1 금액 엇갈림 없음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
310 lines
16 KiB
Python
310 lines
16 KiB
Python
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
|
"""802-4b-1 엑셀 불러오기 → 수식 문서(요구 664 · 665 · 요구서_664 0 장) — 임시 storage · M01 그대로.
|
|
|
|
① 가져온 식 SK = 수식 문서(손 값 = 숫자 · 이음 = 연결 + 엑셀 값 기억 · 중간 = 식 줄 ·
|
|
얼린 변수 = 숫자 · 금액 줄 = 노무비 · 재료비 · 경비 · 단계 · 끝전 · 머리 `가져온` 표지)
|
|
② [엑셀 단가 그대로] = 숫자 값 셈 = 엑셀 · [우리 단가로] = 연결 값 셈(기계 = 시간당 그 비목)
|
|
③ 산근 속 산근 = 형제 SK 단가산출 연결(안쪽이 바뀌면 따라감 · 부르는 쪽 단가기준)
|
|
④ 소요량 · 계수 고른 칸 = 그 수 자리에 표 연결 기호(금액 그대로)
|
|
⑤ SK 금액 = 문서 셈 = 옛 엔진 셈 · 수식 길 읽기 · 고침(따로) · 옛 칸 고침 409 · 옮기기 대조
|
|
실무 엑셀 없으면 그 시험만 skip.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
import subprocess
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
import helper_m02_hopyo_golden as golden
|
|
import pytest
|
|
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
from M02_MasterTemplete import M02_Doc_Import as dim
|
|
from M02_MasterTemplete import M02_Doc_Migrate as mig
|
|
from M02_MasterTemplete import M02_Doc_Sk as dsk
|
|
from M02_MasterTemplete import M02_Doc_Sk_Path as path
|
|
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
|
from M02_MasterTemplete import M02_Machine_Hourly as machine
|
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
|
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh
|
|
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
|
from M02_MasterTemplete import M02_Sangeun_QtyLinks as qty
|
|
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
|
|
|
KEY = "HP-000001"
|
|
|
|
|
|
def _doc(items: list[dict], extra: dict | None = None, src: dict | None = None) -> dict:
|
|
row = {"이름": "견본 sub2임시", "결과단위": "원/식", "호표": items, **(extra or {})}
|
|
meta = {"로직": row, "단가기준": "엑셀", "원본": {"엑셀금액": {}, **(src or {})}}
|
|
return {"키": "SK-000001", "이름": "견본", "단위": "식", "바탕": None, imp.KEY: meta}
|
|
|
|
|
|
def _item(kind: str, name: str, price: float, part: str, link: str | None = None, qty_="1"):
|
|
out = {"종류": kind, "이름": name, "단위": "식", "수량": qty_, "비목": part, "단가": price}
|
|
return out | ({"이음": link} if link else {})
|
|
|
|
|
|
def _ours(doc: dict) -> dict:
|
|
return {**doc, imp.KEY: {**doc[imp.KEY], "단가기준": "우리"}}
|
|
|
|
|
|
def _var(doc: dict, part: str) -> dict:
|
|
"""금액 줄 `이름` 의 단가 기호 변수."""
|
|
line = next(x for x in doc["금액"] if x["이름"] == part)
|
|
sym = str(line["단가"]).removesuffix(".계")
|
|
return next((v for v in doc["변수"] if dim.dc.md.var_id(v) == sym), {})
|
|
|
|
|
|
def _eq(good: bool) -> str:
|
|
for row in m01.loaded()[1].numbers["EQ"].values():
|
|
if bool(machine.hourly(row["키"]).get("ok")) is good:
|
|
return row["키"]
|
|
pytest.skip("EQ 견본 없음")
|
|
|
|
|
|
# ── ① ② 꼴 · 두 단가기준 ──────────────────────────────────────────────────
|
|
def test_doc_form_numbers_links_and_both_bases() -> None:
|
|
eq = _eq(True)
|
|
items = [
|
|
_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5"),
|
|
_item("인력", "작업반장", 150000, "노무비", None, "0.1"),
|
|
_item("기계", "굴착기", 30000, "경비", eq, "2"),
|
|
]
|
|
mids = {"중간": [{"이름": "Qd", "식": "600"}, {"이름": "Q", "식": "Qd / 8"}]}
|
|
marks = ["참고 · 금액 줄 못 읽음 · 보인 값 얼림", "추정 · 단가 후보 여럿"]
|
|
doc = _doc(items, mids, {"표지": marks, "갈래": "EST", "파일": "견본.xls"})
|
|
root = dim.derive(doc)
|
|
assert root["꼴"] == "수식문서" and root["키"] == "SK-000001"
|
|
assert root["가져온"]["표지"] == marks and root["가져온"]["갈래"] == "EST" # 까닭 그대로
|
|
labor = _var(root, "보통인부")["연결"]
|
|
assert labor == {"갈래": "요소", "키": "LB000002", "엑셀": Decimal(100000)}
|
|
assert _var(root, "작업반장")["연결"] == {"갈래": "숫자", "값": Decimal(150000)} # 이음 없음
|
|
mach = _var(root, "굴착기")["연결"]
|
|
assert mach["갈래"] == "단가산출" and mach["칸"] == "경비" and mach["엑셀"] == Decimal(30000)
|
|
qd = next(v for v in root["변수"] if v["열쇠"] == "Qd") # 얼린 변수 = 숫자 · 식 = 식 줄
|
|
assert qd["연결"] == {"갈래": "숫자", "값": Decimal(600)}
|
|
assert [x["식"] for x in root["식"]] == [f"{dim.dc.md.var_id(qd)} ÷ 8"]
|
|
excel = dim.run(root, doc)
|
|
assert excel["계"] == Decimal(50000 + 15000 + 60000) # 엑셀 단가 그대로
|
|
ours = dim.run(root, _ours(doc))
|
|
lb = Decimal(str(m01.loaded()[1].get("LB000002")["값"]))
|
|
hour = machine.hourly(eq)["경비"]
|
|
assert ours["노무비"] == int(lb * Decimal("0.5") * 10) / Decimal(10) + 15000
|
|
assert float(ours["경비"]) == pytest.approx(hour * 2, abs=1) # 기계 = 시간당 그 비목
|
|
|
|
|
|
def test_machine_without_hourly_keeps_number_with_note() -> None:
|
|
eq = _eq(False)
|
|
doc = _doc([_item("기계", "리퍼", 5000, "경비", eq)])
|
|
var = _var(dim.derive(doc), "리퍼")
|
|
assert var["연결"] == {"갈래": "숫자", "값": Decimal(5000)} and eq in var["설명"]
|
|
assert dim.run(dim.derive(doc), _ours(doc))["계"] == 5000
|
|
|
|
|
|
def test_sk_money_is_doc_calc_and_equals_old_engine(world) -> None:
|
|
"""⑤ SK 금액 = 문서 셈(수식셈 표지) = 옛 엔진 셈 — 엑셀 · 우리 둘 다 · 덧줄 율 · 합밖."""
|
|
items = [
|
|
_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.33"),
|
|
_item("재료", "경유", 1700, "재료비", "MO000002", "3") | {"합밖": True},
|
|
]
|
|
extra = {"덧줄": [{"이름": "공구손료", "식": "노무비 * 0.03", "비목": "경비"}]}
|
|
doc = _doc(items, extra)
|
|
for basis in ("엑셀", "우리"):
|
|
mine = imp._with(doc, 단가기준=basis)
|
|
new = imp._money(imp._run(mine, imp._whole()))
|
|
token = dsk.OLD.set(True)
|
|
try:
|
|
old = imp._money(imp._run(mine, imp._whole()))
|
|
finally:
|
|
dsk.OLD.reset(token)
|
|
assert new == old, basis
|
|
got = imp.to_price(doc, "우리")
|
|
assert got["전"]["계"] == 33000 + 990 and got["후"]["계"] != got["전"]["계"]
|
|
|
|
|
|
# ── ③ 산근 속 산근 · 실무 STmate · EST ──────────────────────────────────────
|
|
def _hp(world, tag: str):
|
|
path_ = golden.FILES.get(tag)
|
|
if path_ is None or not path_.exists():
|
|
pytest.skip(f"실무 엑셀 없음 {tag}")
|
|
folder = world["personal"]
|
|
hs._dir(folder).mkdir(parents=True, exist_ok=True)
|
|
(hs._dir(folder) / f"{KEY}.json").write_text(f'{{"키": "{KEY}", "판": {hs.BOOK}}}', "utf-8")
|
|
fh.keep(folder, KEY, path_.name, path_.read_bytes())
|
|
return folder
|
|
|
|
|
|
def _read(key: str) -> dict:
|
|
return store.read("sangeun", key)["문서"]
|
|
|
|
|
|
def test_stmate_nested_is_sibling_link(world) -> None:
|
|
folder = _hp(world, "울진대흥")
|
|
with store.at(folder):
|
|
made = fh.make(folder, KEY, 2200) # D02200 안에 D00075 콘크리트믹서사용
|
|
doc = _read(made["이름"])
|
|
root = dim.derive(doc)
|
|
assert root["가져온"]["갈래"] == "STmate" and root["가져온"]["코드"] == "D02200"
|
|
inner = _var(root, "콘크리트믹서사용")["연결"]
|
|
assert inner["갈래"] == "단가산출" and inner["키"].startswith("SK-")
|
|
assert inner["엑셀"]["노무비"] == 6216 # 엑셀 얼린 값 기억
|
|
assert dim.run(root, doc)["계"] == 67383 == made["검산"]["엑셀"]["계"]
|
|
assert dim.run(root, _ours(doc))["셈됨"] is True
|
|
sub = _read(inner["키"]) # 안쪽 단가 줄 하나 두 배 → 바깥이 따라감(엑셀 기준도)
|
|
one = next(x for x in sub[imp.KEY]["로직"]["호표"] if "단가" in x)
|
|
one["단가"] = one["단가"] * 2
|
|
store.write("sangeun", inner["키"], store.read("sangeun", inner["키"])["판"], sub)
|
|
now = dim.run(dim.derive(_read(inner["키"])), _read(inner["키"]))["계"]
|
|
assert now != 7528
|
|
assert dim.run(root, doc)["계"] == pytest.approx(67383 - 7528 + now, abs=1)
|
|
|
|
|
|
def test_est_by_part_and_frozen_vars(world) -> None:
|
|
folder = _hp(world, "북부청임도")
|
|
with store.at(folder):
|
|
made = fh.make(
|
|
folder, KEY, 67
|
|
) # 단산68 세 줄(노 · 재 · 경) = 묶음 한 줄(812-1 · 원파일 길)
|
|
doc = _read(made["이름"])
|
|
root = dim.derive(doc)
|
|
parts = [
|
|
(x["비목"], _var(root, x["이름"])["연결"].get("칸"))
|
|
for x in root["금액"]
|
|
if (_var(root, x["이름"]).get("연결") or {}).get("키", "").startswith("SK-")
|
|
]
|
|
assert parts == [("묶음", None)]
|
|
assert not any(x["식"].strip().replace(".", "").isdigit() for x in root["식"])
|
|
excel = dim.run(root, doc)
|
|
assert float(excel["계"]) == pytest.approx(float(made["검산"]["엑셀"]["계"]), abs=1)
|
|
assert dim.run(root, _ours(doc))["셈됨"] is True
|
|
|
|
|
|
# ── ④ 소요량 · 계수 고른 칸 ─────────────────────────────────────────────────
|
|
def test_qty_pick_becomes_table_link(world) -> None:
|
|
folder = _hp(world, "북부청임도")
|
|
found = None
|
|
for no in range(1, 200):
|
|
try:
|
|
out = fh.emit(folder, KEY, no)
|
|
except store.StoreError:
|
|
continue
|
|
if "야면석채집및운반(계곡내)" in out["로직"]["이름"]:
|
|
found = out
|
|
break
|
|
if found is None:
|
|
pytest.skip("야면석채집및운반(계곡내) 블록 없음")
|
|
row = found["로직"]
|
|
doc = _doc(row["호표"], {k: row[k] for k in ("중간", "덧줄", "단계", "끝전") if row.get(k)})
|
|
doc[imp.KEY]["로직"] |= {k: row[k] for k in ("이름", "결과단위")}
|
|
at = next(i for i, x in enumerate(row["호표"]) if x["이름"] == "보통인부")
|
|
cell = next(c for c in qty.view(doc)["줄"] if c["자리"] == f"호표:{at}:0")["후보"][0]
|
|
kept = qty.pick(doc, f"호표:{at}:0", {k: cell[k] for k in ("표", "줄", "칸")})["문서"]
|
|
root = dim.derive(kept)
|
|
pick = next(v for v in root["변수"] if v["열쇠"].startswith(dim.PICK))
|
|
assert pick["연결"]["갈래"] == "표" and pick["연결"]["표"] == cell["표"]
|
|
assert pick["연결"]["칸"] == cell["칸"] and pick["연결"][dim.XL] == Decimal(str(cell["값"]))
|
|
line = next(x for x in root["금액"] if x["이름"] == "보통인부")
|
|
assert dim.dc.md.var_id(pick) in line["수량"]
|
|
before = dim.run(dim.derive(doc), doc)["계"]
|
|
assert dim.run(root, kept)["계"] == before # 엑셀 그대로 금액 같음
|
|
assert dim.run(root, _ours(kept))["셈됨"] is True # 표 칸 = 같은 값
|
|
|
|
|
|
# ── ⑤ 수식 길 · 옮기기 ───────────────────────────────────────────────────
|
|
def test_formula_route_edit_then_old_edit_409_and_regen(world) -> None:
|
|
folder = world["personal"]
|
|
doc = _doc([_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5")])
|
|
with store.at(folder):
|
|
key = imp.create(doc[imp.KEY]["로직"], {"엑셀금액": {"계": 50000}}, "sub2임시 견본")["이름"]
|
|
read = path.read(key)
|
|
assert read["셈"]["계"] == 50000 and read["사본"]["단가기준"] == "엑셀"
|
|
edit = copy.deepcopy(read["문서"])
|
|
edit["금액"][0]["수량"] = "1"
|
|
pre = path.preview(key, {"문서": edit})
|
|
assert pre["전"]["계"] == 50000 and pre["후"]["계"] == 100000
|
|
path.save(key, {"문서": edit, "판": read["판"]})
|
|
again = path.read(key)
|
|
assert again["사본"]["따로"] is True and again["셈"]["계"] == 100000
|
|
sk = _read(key)
|
|
moved = imp.link(sk, sd_id(sk), None)["문서"] # 이음 풂 = 옛 칸 고침 → 409
|
|
with pytest.raises(store.StoreError) as why:
|
|
store.write("sangeun", key, store.read("sangeun", key)["판"], moved)
|
|
assert why.value.status == 409
|
|
ours = imp.to_price(sk, "우리")["문서"] # 단가기준만 바꿈 = 됨(문서 그대로 · 연결 값)
|
|
store.write("sangeun", key, store.read("sangeun", key)["판"], ours)
|
|
lb = Decimal(str(m01.loaded()[1].get("LB000002")["값"]))
|
|
assert path.read(key)["셈"]["노무비"] == int(lb * 10) / Decimal(10)
|
|
|
|
|
|
def sd_id(sk: dict) -> str:
|
|
return imp.sd.line_ids(imp.row_of(sk))[0]
|
|
|
|
|
|
def test_migrate_imported_same(world) -> None:
|
|
folder = world["personal"]
|
|
doc = _doc([_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5")])
|
|
with store.at(folder):
|
|
key = imp.create(doc[imp.KEY]["로직"], {"엑셀금액": {"계": 50000}}, "sub2임시 옮김")["이름"]
|
|
file = folder / "sangeun" / f"{key}.json"
|
|
got = mig.check("sangeun", file)
|
|
assert got["상태"] in ("같음", "있음")
|
|
if got["상태"] == "같음":
|
|
assert got["수식"]["문서"]["키"] == key and got["전"] == got["후"]
|
|
|
|
|
|
# ── TS 짝(importView) 거울 ─────────────────────────────────────────────
|
|
_NODE = """
|
|
const fs = require("fs"), path = require("path");
|
|
const ts = require(path.join(process.cwd(), "config", "node_modules", "typescript"));
|
|
const at = path.join(process.cwd(), "common_util", "common_util_formula_doc_import.ts");
|
|
const js = ts.transpileModule(fs.readFileSync(at, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
new Function("exports", "module", "require", js)(box.exports, box, () => ({}));
|
|
const got = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
process.stdout.write(JSON.stringify(got.map(([d, b]) => box.exports.importView(d, b))));
|
|
"""
|
|
|
|
|
|
def test_ts_view_mirror() -> None:
|
|
eq = _eq(True)
|
|
items = [
|
|
_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5"),
|
|
_item("인력", "작업반장", 150000, "노무비", None, "0.1"),
|
|
_item("기계", "굴착기", 30000, "경비", eq, "2"),
|
|
]
|
|
root = dim.derive(_doc(items))
|
|
root["변수"].append(
|
|
{"열쇠": "S", "기호": "S", "연결": {"갈래": "단가산출", "키": "SK-000009", "엑셀": 5}}
|
|
)
|
|
plain = json.loads(json.dumps(root, ensure_ascii=False, default=float))
|
|
cases = [[plain, b] for b in ("엑셀", "우리")]
|
|
out = subprocess.run(
|
|
["node", "-e", _NODE],
|
|
input=json.dumps(cases, ensure_ascii=False),
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
cwd=Path(__file__).resolve().parents[2],
|
|
check=True,
|
|
)
|
|
ts_ = json.loads(out.stdout)
|
|
py = [json.loads(json.dumps(dim.view(plain, b), ensure_ascii=False)) for b in ("엑셀", "우리")]
|
|
assert ts_ == py
|
|
assert _var(ts_[0], "보통인부")["연결"] == {"갈래": "숫자", "값": 100000.0}
|
|
assert ts_[0]["변수"][-1]["연결"]["갈래"] == "단가산출" # 형제 SK = 늘 연결
|
|
|
|
|
|
def test_empty_block_is_excel_zero_not_dash() -> None:
|
|
"""줄 없는 엑셀 산근 = 0 원(옛 엔진 · 엑셀과 같음) · 표지 · 우리 단가로도 0."""
|
|
doc = _doc([])
|
|
root = dim.derive(doc)
|
|
assert dim.EMPTY in root["가져온"]["표지"]
|
|
assert dim.run(root, doc)["계"] == 0 == dim.run(root, _ours(doc))["계"]
|
|
assert imp.check(doc)["셈"]["계"] == 0
|