Merge remote-tracking branches 'origin/sub_laptop_1', 'origin/sub_laptop_6' and 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
"""M01 시트 로직 가져오기 시험(35-5 · 정함 9) — Node 로 `M01_SL_Import.ts` 를 바로 돌림.
|
||||
|
||||
· 견본 로직 줄 여섯(정함 9 시험 로직)은 이 시험 안에 둠 — 품셈 표 · 노임 · 자재 · 기존 로직 값은 마스터에서 읽기만
|
||||
· 가져온 시트 로직 → 바깥 값 넣기(파이썬 엔진이 마스터에서) → 우리 스프레드시트 엔진(Node 번들)으로 셈
|
||||
→ 합계 · 비목 = 지금 `/calc`(`M01_LB_Calc.calc` = `/calc` 와 같은 길) · 끝수 차이는 원 미만
|
||||
· 마스터 로직 1,469 개(자체 빼고) 모두 가져와 다 바뀌는 수를 세고 · 바뀐 통합문서를 엔진이 다 풂(#NAME? 없음)
|
||||
· 파일 안 씀 — 마스터는 읽기만 · 넘김은 표준입력
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
IMPORT = ROOT / "M01_MasterData" / "SheetLogic" / "M01_SL_Import.ts"
|
||||
|
||||
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
|
||||
# ── Node 고리 — 허락한 파일만 실제로 읽고 나머지 import 는 빈 것으로 ────────────
|
||||
HOOKS = r"""
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { stripTypeScriptTypes } from "node:module";
|
||||
const ALLOW = /(EasyCalc_(Model|Table|Cond)|M01_LB_Blocks|M01_SL_Import(_Parse)?|spreadsheet_(address|types))(\.ts)?$/;
|
||||
export async function resolve(spec, ctx, next) {
|
||||
if (spec.startsWith(".") && ctx.parentURL && !spec.endsWith(".ts")) {
|
||||
const u = new URL(spec + ".ts", ctx.parentURL);
|
||||
if (existsSync(fileURLToPath(u))) return { url: u.href, shortCircuit: true };
|
||||
}
|
||||
return next(spec, ctx);
|
||||
}
|
||||
export async function load(url, ctx, next) {
|
||||
if (!url.endsWith(".ts")) return next(url, ctx);
|
||||
let src = readFileSync(fileURLToPath(url), "utf8");
|
||||
src = src.replace(/import\s+"[^"]+\.css";/g, "");
|
||||
src = src.replace(/import\.meta\.glob(?:<[\s\S]*?>)?\(\s*"[^"]*"\s*\)/g, "({})");
|
||||
src = src.replace(/import\s*\{([^}]*)\}\s*from\s*"([^"]+)";/g, (m, names, from) => {
|
||||
if (from.startsWith(".") && ALLOW.test(from)) return m;
|
||||
const list = names.split(",").map((x) => x.trim()).filter((x) => x && !x.startsWith("type "));
|
||||
return list.length ? `const { ${list.join(", ")} } = globalThis.__stub;` : "";
|
||||
});
|
||||
return { format: "module", source: stripTypeScriptTypes(src, { mode: "transform" }), shortCircuit: true };
|
||||
}
|
||||
"""
|
||||
STUB = (
|
||||
"globalThis.__stub = new Proxy({}, { get: (_, k) => k === 'currentLanguageIndex' ? 0 : "
|
||||
"() => undefined });\n"
|
||||
)
|
||||
|
||||
|
||||
def _plain(x: object) -> object:
|
||||
return float(x) if isinstance(x, Decimal) else str(x)
|
||||
|
||||
|
||||
def _node(body: str, payload: object) -> object:
|
||||
"""`I` = Import 모듈 · `A` = 넘긴 것(표준입력) · `out(x)` 로 답"""
|
||||
script = (
|
||||
"import { register } from 'node:module';\n"
|
||||
"import { readFileSync } from 'node:fs';\n"
|
||||
f"register('data:text/javascript,' + encodeURIComponent({json.dumps(HOOKS)}));\n"
|
||||
+ STUB
|
||||
+ f"const I = await import({json.dumps(IMPORT.as_uri())});\n"
|
||||
"const A = JSON.parse(readFileSync(0, 'utf8'));\n"
|
||||
"const out = (x) => process.stdout.write(JSON.stringify(x));\n" + body
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "--no-warnings", "--input-type=module", "-e", script],
|
||||
input=json.dumps(payload, ensure_ascii=False, default=_plain),
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=600, cwd=ROOT,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr[-3000:]
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
# ── 견본 줄 여섯(정함 9) + 넣을 값 ────────────────────────────────────────────
|
||||
H = {"원문번호": "", "출처": "자체", "소유": "공용", "끝수": None}
|
||||
SAMPLES = [
|
||||
(
|
||||
{**H, "키": "GF000208", "이름": "암거이음받침 채움재(T=20㎜)", "결과단위": "원/㎡", "입력": [], "중간": [],
|
||||
"호표": [{"종류": "재료", "요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "JOINT FILLER"},
|
||||
"이름": "JOINT FILLER(T=20㎜)", "단위": "㎡", "수량": "1", "비목": "재료비"}],
|
||||
"덧줄": []},
|
||||
{},
|
||||
),
|
||||
(
|
||||
{**H, "키": "GF000219", "이름": "돌쌓기 메쌓기(인력)", "결과단위": "원/㎡",
|
||||
"입력": [{"이름": "뒷길이", "단위": "㎝", "고르기": [25, 30, 35, 45, 55, 60, 75]},
|
||||
{"이름": "돌", "고르기": ["깬잡석", "견치돌", "깬돌", "호박돌 및 야면석"]},
|
||||
{"이름": "쌓기", "고르기": ["골쌓기", "켜쌓기"]},
|
||||
{"이름": "높이", "단위": "m"}, {"이름": "초과증가율", "단위": "%", "범위": [80, 100]}],
|
||||
"중간": [{"이름": "증가율", "식": "만약(높이 <= 3, 0, 만약(높이 <= 7.5, 찾기(QF000422, 높이=높이).증가율, 초과증가율))"}],
|
||||
"호표": [{"종류": "인력", "요소": "LB000033", "이름": "석공", "단위": "인", "비목": "노무비",
|
||||
"수량": "찾기(QF000421, 뒷길이=뒷길이, 돌=돌, 쌓기=쌓기).석공 * (1 + 증가율 / 100)"},
|
||||
{"종류": "인력", "요소": "LB000002", "이름": "보통인부", "단위": "인", "비목": "노무비",
|
||||
"수량": "찾기(QF000421, 뒷길이=뒷길이, 돌=돌, 쌓기=쌓기).보통인부 * (1 + 증가율 / 100)"}],
|
||||
"덧줄": []},
|
||||
{"뒷길이": 30, "돌": "깬돌", "쌓기": "골쌓기", "높이": 4.5, "초과증가율": 80},
|
||||
),
|
||||
(
|
||||
{**H, "키": "GF000025", "이름": "떼붙임(재배잔디)", "결과단위": "원/100㎡",
|
||||
"입력": [{"이름": "품", "단위": "인/100㎡"}, {"이름": "비탈구배", "고르기": ["해당 없음", "평균 비탈구배 20˚~45˚ 이하"]}],
|
||||
"중간": [],
|
||||
"호표": [{"종류": "인력", "요소": "LB000002", "이름": "보통인부", "단위": "인", "비목": "노무비",
|
||||
"수량": "품 * (1 + 만약(비탈구배 == '해당 없음', 0, 찾기(QF000099, 구분='비탈구배 해당 시 품 할증').값) / 100)"}],
|
||||
"덧줄": []},
|
||||
{"품": 2.5, "비탈구배": "평균 비탈구배 20˚~45˚ 이하"},
|
||||
),
|
||||
(
|
||||
{**H, "키": "GF000249", "이름": "다짐(플레이트콤펙터 1.5ton)", "결과단위": "원/㎥",
|
||||
"입력": [{"이름": "지역", "고르기": ["전국평균", "전국", "서울", "경기"]}],
|
||||
"중간": [{"이름": "작업량", "식": "로직(GF000114)"}],
|
||||
"호표": [{"종류": "로직", "요소": "로직(GC000272, 기계='1730-0015', 지역=지역)", "이름": "플레이트 콤팩터 1.5ton",
|
||||
"단위": "시간", "수량": "1 / 작업량"}],
|
||||
"덧줄": []},
|
||||
{"지역": "서울"},
|
||||
),
|
||||
(
|
||||
{**H, "키": "GF000133", "이름": "골재운반 덤프트럭 시간당 작업량", "결과단위": "㎥/시간",
|
||||
"입력": [{"이름": "qt", "단위": "㎥"}, {"이름": "f"}, {"이름": "E"}, {"이름": "Cm", "단위": "분"}],
|
||||
"중간": [], "결과": {"식": "반올림(60 * qt * f * E / Cm, 3)"}},
|
||||
{"qt": 6, "f": 1.2, "E": 0.9, "Cm": 25},
|
||||
),
|
||||
(
|
||||
{**H, "키": "GC000051", "이름": "보행자 안전통로 설치 및 해체", "결과단위": "원/m", "입력": [],
|
||||
"중간": [{"이름": "비계공", "식": "찾기(QC000115, 구분='비계공').수량"}],
|
||||
"호표": [{"종류": "인력", "요소": "LB000006", "이름": "비계공", "단위": "인", "수량": "비계공", "비목": "노무비"}],
|
||||
"덧줄": [{"이름": "공구손료 및 경장비", "비목": "경비",
|
||||
"식": "(비계공 * LB000006) * 찾기(QC000116, 구분='공구손료 및 경장비 기계경비(인력품 기준)').값 / 100"}]},
|
||||
{},
|
||||
),
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
# ── 마스터(읽기만) ─────────────────────────────────────────────────────────
|
||||
@pytest.fixture(scope="module")
|
||||
def master():
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
cm = store.cm
|
||||
files = cm.load(folder=store.FOLDER)
|
||||
tables = {
|
||||
str(t["키"]): t
|
||||
for data in files.values()
|
||||
if data.get("그룹") in cm.mf.TABLE_GROUPS
|
||||
for t in data.get("표") or []
|
||||
}
|
||||
return cm, files, tables
|
||||
|
||||
|
||||
def _tables_of(row: dict, tables: dict) -> dict:
|
||||
keys = set(re.findall(r"Q[A-Z]\d{6}", json.dumps(row, ensure_ascii=False)))
|
||||
return {k: tables[k] for k in keys if k in tables}
|
||||
|
||||
|
||||
def _num(v) -> float:
|
||||
return float(v["수"]) if isinstance(v, dict) and "수" in v else float("nan")
|
||||
|
||||
|
||||
def _fill(sl: dict, places: list, py_lines: list, master) -> None:
|
||||
"""바깥 값 넣기 — 표는 가져올 때 이미 · 노임 = 마스터 · 자재 = `/calc` 줄 단가 · 장비 · 로직 = 그 로직 `/calc`"""
|
||||
cm, files, _ = master
|
||||
m = cm.mf.Master(files)
|
||||
nodes = {n["id"]: n for n in sl["nodes"]}
|
||||
for at in places:
|
||||
item = at["item"]
|
||||
kind = item["kind"]
|
||||
if kind == "표":
|
||||
continue
|
||||
if kind == "노임":
|
||||
value = cm.mf.element(m, item["key"], {})[0]
|
||||
elif kind == "자재":
|
||||
name = nodes[item["id"].split(":")[0]]["name"]
|
||||
value = next(x["단가"] for x in py_lines if x["이름"] == name)
|
||||
else:
|
||||
key = (
|
||||
item["key"]
|
||||
if kind == "로직"
|
||||
else re.match(r"로직\((\w+)", item["element"]).group(1)
|
||||
)
|
||||
got = cm.mf.run(m, key, {k: Decimal(str(v)) if isinstance(v, (int, float)) else v
|
||||
for k, v in item["inputs"].items()}) # fmt: skip
|
||||
value = got["결과"] if "결과" in got else got[item.get("field") or "계"]
|
||||
if value is None: # 마스터에 값이 없음 — 바깥 값 칸을 비움(`/calc` 도 그 줄을 비우고 0)
|
||||
continue
|
||||
sheet = next(s for s in sl["workbook"]["시트"] if s["id"] == at["sheet"])
|
||||
sheet["칸"][at["cell"]] = {"값": float(value)}
|
||||
|
||||
|
||||
def _result_cells(sl: dict) -> dict:
|
||||
node = next(n for n in sl["nodes"] if n["kind"] == "result")
|
||||
return {p["name"]: (node["sheetId"], p["cell"]) for p in node["pillsOut"]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("row,given", SAMPLES, ids=[r["키"] for r, _ in SAMPLES])
|
||||
def test_가져온_시트로직_값이_calc_와_같음(row: dict, given: dict, master) -> None:
|
||||
from common_util.common_util_spreadsheet import recalc_workbooks
|
||||
from M01_MasterData.LogicBuilder import M01_LB_Calc as lb
|
||||
|
||||
rep = _node(
|
||||
"const r = I.importLogicReport(A.row, { tables: A.tables, examples: A.given });"
|
||||
"out({ ...r, places: I.outsideOf(r.sl) });",
|
||||
{"row": row, "tables": _tables_of(row, master[2]), "given": given},
|
||||
)
|
||||
assert rep["problems"] == []
|
||||
sl = rep["sl"]
|
||||
# 모양 — 상자 = 시트 · 선은 있는 상자 · 알약끼리
|
||||
sheets = {s["id"] for s in sl["workbook"]["시트"]}
|
||||
assert len(sheets) == len(sl["nodes"]) and all(n["sheetId"] in sheets for n in sl["nodes"])
|
||||
ids = {n["id"]: n for n in sl["nodes"]}
|
||||
for e in sl["edges"]:
|
||||
assert any(p["name"] == e["from"]["pill"] for p in ids[e["from"]["node"]]["pillsOut"])
|
||||
assert any(p["name"] == e["to"]["pill"] for p in ids[e["to"]["node"]]["pillsIn"])
|
||||
|
||||
want = lb.calc(row, given, {})
|
||||
assert want["ok"], want
|
||||
# 1차 셈 → 넘길 값(장비 · 로직 인자) 읽기 → 바깥 값 넣기 → 2차 셈
|
||||
first = recalc_workbooks([sl["workbook"]])[0]
|
||||
places = _node("out(I.outsideOf(A.sl, A.calc));", {"sl": sl, "calc": first})
|
||||
_fill(sl, places, want.get("lines", []), master)
|
||||
got = recalc_workbooks([sl["workbook"]])[0]
|
||||
cells = _result_cells(sl)
|
||||
read = {k: _num(got[s].get(c)) for k, (s, c) in cells.items()}
|
||||
print(f"\n{row['키']} 시트 {read} · /calc {want.get('result', want.get('sums'))}")
|
||||
if "result" in want:
|
||||
assert abs(read["결과"] - float(want["result"])) < 1e-9, (read, want["result"])
|
||||
return
|
||||
for k in ("노무비", "재료비", "경비", "계"):
|
||||
diff = abs(read[k] - float(want["sums"][k]))
|
||||
assert diff < 1, f"{row['키']} {k}: 시트 {read[k]} · /calc {want['sums'][k]}"
|
||||
|
||||
|
||||
def test_마스터_1469_가져오기_세기(master) -> None:
|
||||
from common_util.common_util_spreadsheet import recalc_workbooks
|
||||
|
||||
cm, files, tables = master
|
||||
rows = [
|
||||
r
|
||||
for name, data in files.items()
|
||||
if data.get("그룹") == "로직" and name != "로직_자체.json"
|
||||
for r in data.get("줄") or []
|
||||
]
|
||||
assert len(rows) == 1469
|
||||
got = _node(
|
||||
"const res = A.rows.map((row) => { try { const r = I.importLogicReport(row, { tables: A.tables });"
|
||||
"return { key: row.키, problems: r.problems, book: r.problems.length ? null : r.sl.workbook }; }"
|
||||
"catch (e) { return { key: row.키, problems: ['던짐 ' + e.message], book: null }; } });"
|
||||
"out(res);",
|
||||
{"rows": rows, "tables": tables},
|
||||
)
|
||||
thrown = [g["key"] for g in got if any(p.startswith("던짐") for p in g["problems"])]
|
||||
assert thrown == []
|
||||
full = [g for g in got if not g["problems"]]
|
||||
# 다 바뀐 것은 엔진이 다 풂 — 모르는 함수 · 이름(#NAME?) 없음
|
||||
values = recalc_workbooks([g["book"] for g in full])
|
||||
named = [
|
||||
g["key"] for g, v in zip(full, values) if "#NAME?" in json.dumps(v, ensure_ascii=False)
|
||||
]
|
||||
assert named == []
|
||||
reasons: dict[str, int] = {}
|
||||
for g in got:
|
||||
for p in g["problems"]:
|
||||
why = re.sub(r"「[^」]*」", "「…」", p.split(" — ", 1)[-1])
|
||||
reasons[why] = reasons.get(why, 0) + 1
|
||||
print(
|
||||
f"\n다 바뀜 {len(full)} / {len(rows)} · 못 바꾼 까닭 {sorted(reasons.items(), key=lambda x: -x[1])}"
|
||||
)
|
||||
assert len(full) > 1000
|
||||
@@ -71,7 +71,7 @@ let sl = M.newLogic("시험 하나", "원/㎡");
|
||||
let r = M.addNode(sl, "input", "품"); sl = r.sl; const pum = r.node;
|
||||
r = M.addNode(sl, "item", "보통 인부"); sl = r.sl; const man = r.node; man.ref = "LB000002";
|
||||
r = M.addNode(sl, "result", "결과"); sl = r.sl; const res = r.node;
|
||||
sl = E.applyOutside(sl, [{ node: pum.id, cell: "B2", value: 0.5 }, { node: man.id, cell: "B3", value: 100000 }]);
|
||||
sl = E.applyOutside(sl, [{ node: pum.id, cell: "B2", value: 0.5 }, { node: man.id, cell: "B2", value: 100000 }]);
|
||||
sl = link(sl, pum, "품", man, "수량");
|
||||
sl = link(sl, man, "노무비", res, "노무비");
|
||||
"""
|
||||
@@ -87,7 +87,7 @@ r = M.addNode(sl, "item", "보통인부"); sl = r.sl; const man = r.node;
|
||||
r = M.addNode(sl, "result", "결과"); sl = r.sl; const res = r.node;
|
||||
sl = E.applyOutside(sl, [
|
||||
{ node: pum.id, cell: "B2", value: 0.2 }, { node: rate.id, cell: "B2", value: 1.3 },
|
||||
{ node: stone.id, cell: "B3", value: 200000 }, { node: man.id, cell: "B3", value: 100000 },
|
||||
{ node: stone.id, cell: "B2", value: 200000 }, { node: man.id, cell: "B2", value: 100000 },
|
||||
]);
|
||||
sl = link(sl, pum, "품", calc, "값1");
|
||||
sl = link(sl, rate, "할증률", calc, "값2");
|
||||
@@ -101,12 +101,12 @@ sl = link(sl, man, "노무비", res, "노무비");
|
||||
def test_입력값_항목_결과_수식과_값() -> None:
|
||||
got = _run(
|
||||
ONE
|
||||
+ "out([sheet(sl, man.id).칸.B2, sheet(sl, res.id).칸.B2, E.evalLogic(sl), sl.edges, sl.nodes.map(n => [n.kind, n.stage]),"
|
||||
+ "out([sheet(sl, man.id).칸.B1, sheet(sl, res.id).칸.B2, E.evalLogic(sl), sl.edges, sl.nodes.map(n => [n.kind, n.stage]),"
|
||||
" sl.workbook.시트.map(s => s.이름)]);"
|
||||
)
|
||||
qty, total_in, ev, edges, kinds, names = got
|
||||
assert qty == {"식": "품!B2"}
|
||||
assert total_in == {"식": "'보통 인부'!B5"}
|
||||
assert total_in == {"식": "'보통 인부'!B7"}
|
||||
assert ev["result"] == {"노무비": 50000, "재료비": None, "경비": None, "계": 50000}
|
||||
assert ev["pills"]["n2"]["금액"] == 50000 and ev["pillsIn"]["n2"]["수량"] == 0.5
|
||||
assert ev["errors"] == []
|
||||
@@ -132,7 +132,7 @@ def test_계산_두_항목_더함_값바뀜() -> None:
|
||||
# 석공 0.2×1.3×200000 = 52000 · 보통인부 0.2×100000 = 20000
|
||||
assert a == {"노무비": 72000, "재료비": None, "경비": None, "계": 72000}
|
||||
assert c == {"결과": 0.26}
|
||||
assert cell == {"식": "석공!B5+보통인부!B5"}
|
||||
assert cell == {"식": "석공!B7+보통인부!B7"}
|
||||
assert b["계"] == 0.2 * 1.5 * 200000 + 20000
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ def test_선_끊기_다시_잇기() -> None:
|
||||
" E.evalLogic(c).result, a.edges.length]);"
|
||||
)
|
||||
cell_a, ra, cell_b, check_b, rc, n_edges = got
|
||||
assert cell_a == {"식": "보통인부!B5"} and ra["계"] == 20000
|
||||
assert cell_a == {"식": "보통인부!B7"} and ra["계"] == 20000
|
||||
assert cell_b is None
|
||||
assert "결과 상자에 이어진 선이 없음" in check_b
|
||||
assert not any("입력 알약" in x for x in check_b)
|
||||
@@ -202,7 +202,7 @@ def test_바깥값_칸목록과_표_덩어리() -> None:
|
||||
)
|
||||
cells, e1, f2, e3, f3, lg, check = got
|
||||
assert cells == [
|
||||
{"id": "n2!B3", "node": "n2", "cell": "B3", "kind": "price", "ref": "LB000002"},
|
||||
{"id": "n2!B2", "node": "n2", "cell": "B2", "kind": "price", "ref": "LB000002"},
|
||||
{
|
||||
"id": "n4!E1",
|
||||
"node": "n4",
|
||||
@@ -230,16 +230,16 @@ def test_알약_정하기_이름바꾸면_선이_따라감() -> None:
|
||||
ONE + "let s2 = M.setPill(sl, pum.id, 'B2', 'out', '품셈 품');\n"
|
||||
"const e = s2.edges[0].from;\n"
|
||||
"s2 = M.setPill(s2, pum.id, 'D2', 'out', '품셈 품');\n"
|
||||
"const moved = sheet(s2, man.id).칸.B2;\n"
|
||||
"const moved = sheet(s2, man.id).칸.B1;\n"
|
||||
"const s3 = M.removePill(s2, pum.id, 'out', '품셈 품');\n"
|
||||
"const s4 = M.setPill(sl, man.id, 'C9', 'in', '할증');\n"
|
||||
"out([e, moved, s3.edges.length, sheet(s3, man.id).칸.B2 ?? null, s4.nodes[1].pillsIn.map(p => [p.name, p.cell])]);"
|
||||
"out([e, moved, s3.edges.length, sheet(s3, man.id).칸.B1 ?? null, s4.nodes[1].pillsIn.map(p => [p.name, p.cell])]);"
|
||||
)
|
||||
e, moved, n_edges, cleared, ins = got
|
||||
assert e == {"node": "n1", "pill": "품셈 품"}
|
||||
assert moved == {"식": "품!D2"}
|
||||
assert n_edges == 1 and cleared is None
|
||||
assert ins == [["수량", "B2"], ["단가", "B3"], ["할증", "C9"]]
|
||||
assert ins == [["수량", "B1"], ["노무 단가", "B2"], ["할증", "C9"]]
|
||||
|
||||
|
||||
def test_상자_지우기() -> None:
|
||||
@@ -250,7 +250,7 @@ def test_상자_지우기() -> None:
|
||||
)
|
||||
names, sheets, n_edges, cell, total, before = got
|
||||
assert names == ["품", "할증률", "할증 품", "보통인부", "결과"] and sheets == names
|
||||
assert n_edges == 4 and cell == {"식": "보통인부!B5"} and total == 20000
|
||||
assert n_edges == 4 and cell == {"식": "보통인부!B7"} and total == 20000
|
||||
assert before == 6 # 받은 것은 안 바뀜
|
||||
|
||||
|
||||
@@ -288,11 +288,11 @@ def test_상자_이름_바꾸기_수식이_따라감() -> None:
|
||||
)
|
||||
why, name, sheet_name, cell, total, tries, before = got
|
||||
assert why is None and name == sheet_name == "석공 (돌쌓기)"
|
||||
assert cell == {"식": "'석공 (돌쌓기)'!B5+보통인부!B5"} and total == 72000
|
||||
assert cell == {"식": "'석공 (돌쌓기)'!B7+보통인부!B7"} and total == 72000
|
||||
assert tries == [
|
||||
"같은 이름 상자가 있음",
|
||||
"이름은 1~31 글자",
|
||||
"이름에 못 쓰는 글자 — [ ] : * ? / \ 와 앞뒤 '",
|
||||
"이름에 못 쓰는 글자 — [ ] : * ? / \\ 와 앞뒤 '",
|
||||
"상자가 없음",
|
||||
"이름은 1~31 글자",
|
||||
]
|
||||
@@ -316,7 +316,7 @@ def test_바깥값_답을_넣을값으로_범위머리() -> None:
|
||||
"const cells = E.outsideCells(sl);\n"
|
||||
"const head = M.rangeHead('높이');\n"
|
||||
"const got = E.answersToValues(cells, [\n"
|
||||
" { id: 'n2!B3', ok: true, value: 172068 },\n"
|
||||
" { id: 'n2!B2', ok: true, value: 172068 },\n"
|
||||
" { id: 'n4!E1', ok: true, rows: [[...head, '증가율'], [3, 4, 30]] },\n"
|
||||
"]);\n"
|
||||
"const s2 = E.applyOutside(sl, got.values);\n"
|
||||
@@ -325,10 +325,39 @@ def test_바깥값_답을_넣을값으로_범위머리() -> None:
|
||||
head, got_values, f1, g2, total = got
|
||||
assert head == ["높이·아래", "높이·위"]
|
||||
assert got_values["errors"] == []
|
||||
assert got_values["values"][0] == {"node": "n2", "cell": "B3", "value": 172068}
|
||||
assert got_values["values"][0] == {"node": "n2", "cell": "B2", "value": 172068}
|
||||
assert (f1, g2, total) == ({"값": "높이·위"}, {"값": 30}, 86034)
|
||||
missing = _run(
|
||||
ONE
|
||||
+ "out(E.answersToValues(E.outsideCells(sl), [{ id: 'n2!B3', ok: false, reason: '없는 직종' }]));"
|
||||
+ "out(E.answersToValues(E.outsideCells(sl), [{ id: 'n2!B2', ok: false, reason: '없는 직종' }]));"
|
||||
)
|
||||
assert missing == {"values": [], "errors": ["B3 칸 바깥 값을 못 받음 없는 직종"]}
|
||||
assert missing == {"values": [], "errors": ["B2 칸 바깥 값을 못 받음 없는 직종"]}
|
||||
|
||||
|
||||
def test_항목_틀은_Item_노무_틀과_같은_칸() -> None:
|
||||
got = _run(
|
||||
"let sl = M.newLogic(); const r = M.addNode(sl, 'item', '석공');"
|
||||
" out([sheet(r.sl, r.node.id).칸, r.node.pillsIn, r.node.pillsOut.map(p => [p.name, p.cell]), r.node.ref]);"
|
||||
)
|
||||
cells, ins, outs, ref = got
|
||||
assert {k: v for k, v in cells.items() if k.startswith("B")} == {
|
||||
"B3": {"값": 0},
|
||||
"B4": {"값": 0},
|
||||
"B5": {"식": "B2+B3+B4"},
|
||||
"B6": {"식": "ROUND(B1*B5,1)"},
|
||||
"B7": {"식": "ROUND(B1*B2,1)"},
|
||||
"B8": {"식": "ROUND(B1*B3,1)"},
|
||||
"B9": {"식": "ROUND(B1*B4,1)"},
|
||||
"B10": {"식": "B1"},
|
||||
}
|
||||
assert [cells[f"A{i}"]["값"] for i in range(1, 11)] == [
|
||||
"수량", "노무 단가", "재료 단가", "경비 단가", "단가", "금액", "노무비", "재료비", "경비", "사용 수량",
|
||||
] # fmt: skip
|
||||
assert ins == [
|
||||
{"name": "수량", "cell": "B1", "unit": ""},
|
||||
{"name": "노무 단가", "cell": "B2", "unit": "원", "outside": True},
|
||||
]
|
||||
assert outs == [
|
||||
["단가", "B5"], ["금액", "B6"], ["노무비", "B7"], ["재료비", "B8"], ["경비", "B9"], ["사용 수량", "B10"],
|
||||
] # fmt: skip
|
||||
assert ref == '{"what":"노무"}'
|
||||
|
||||
Reference in New Issue
Block a user