Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
# Conflicts: # B09_Estimation/B09_Estimation_UI_Page.css # B09_Estimation/B09_Estimation_UI_Shell.ts # B09_Estimation/B09_Estimation_UI_Tabs_Bill_Edit.ts
This commit is contained in:
@@ -14,7 +14,7 @@ SPOTS = {
|
||||
"B09_Estimation/B09_Estimation_Calc_Bill.ts": ['"수기 줄 — 연결 없음(우클릭 [연결])"'],
|
||||
"B09_Estimation/B09_Estimation_Calc_Bill.py": ['"수기 줄 — 연결 없음(우클릭 [연결])"'],
|
||||
"B09_Estimation/B09_Estimation_Sangeun.py": ['f"수기 줄 {'],
|
||||
"B09_Estimation/B09_Estimation_UI_Tabs_Base.ts": ["견적 단가 · 면 = 수기 입력"],
|
||||
"B09_Estimation/B09_Estimation_UI_Tabs_Base.ts": ["견적 단가 = 수기 입력"],
|
||||
"B09_Estimation/B09_Estimation_UI_Tabs_Total.ts": ['"수기 입력"', '"수기 입력 = 안전관리비'],
|
||||
"ui_template/ui_template_locale_b2.ts": [
|
||||
"수량 원천: 수기 입력(직접비)",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"""183-2 자재단가대비표 머리 — 견적 「면」 뺌 · 다른 출처 「면」 → 「출처」(값 그대로) · 견적서 = 견적 아래.
|
||||
|
||||
소스 시험(열 정의 · 고칠 칸) — 셈 · 저장(`quote_page` 남음)은 안 바뀜(test_166_4).
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SRC = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Base.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _compare() -> str:
|
||||
start = SRC.index("export const buildCompare")
|
||||
return SRC[start : SRC.index("export const buildMaterial")]
|
||||
|
||||
|
||||
def test_견적은_단가_견적서_그_밖은_단가_출처():
|
||||
body = _compare()
|
||||
assert (
|
||||
's === "견적" ? text("files", "견적서", 72, [s]) : text(`g:${s}`, "출처", 64, [s])' in body
|
||||
)
|
||||
assert '"면"' not in body # 면 머리 없음
|
||||
assert body.count('text("files", "견적서"') == 1 # 맨 끝 견적서 열 지움
|
||||
|
||||
|
||||
def test_견적_면_수기_칸_없앰():
|
||||
body = _compare()
|
||||
assert 'key === "g:견적"' not in body and "quote_page" not in body
|
||||
assert "견적 단가 = 수기 입력" in body and "견적 단가 · 면" not in body
|
||||
@@ -0,0 +1,113 @@
|
||||
"""183-3 자재단가대비표 적용 단가 — 칸 누름 고르기 메뉴 · 고른 출처 칸 강조 · 기본 최저.
|
||||
|
||||
- 공용 표 `cellClass` 옵션(칸 class · 안 주면 같은 글) — Node 로 `renderDataTableHtml`
|
||||
- 고르기 항목 `appliedChoices`(값 있는 출처 · 견적 · 최저(자동) · ✓ 표시) — Node 로 함수만 떼어 돌림
|
||||
- 배선: 수기 적용 칸 · 출처 칸 돌림 없앰 · Shell 이 cellClass · choices 를 넘김 · B09 메뉴 한 벌
|
||||
셈 · 저장(pick › applied › 최저)은 그대로(test_144 · test_166_4).
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TS = ROOT / "config" / "node_modules" / "typescript"
|
||||
|
||||
|
||||
def _src(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _node(script: str):
|
||||
out = subprocess.run(
|
||||
["node", "-e", script], capture_output=True, text=True, encoding="utf-8", timeout=60
|
||||
)
|
||||
assert out.returncode == 0, out.stderr
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
PRELUDE = f"""
|
||||
const fs = require("fs");
|
||||
const ts = require({json.dumps(str(TS))});
|
||||
const js = (code) => ts.transpileModule(code, {{
|
||||
compilerOptions: {{ module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }},
|
||||
}}).outputText;
|
||||
"""
|
||||
|
||||
|
||||
def test_공용_표_cellClass_칸_class_안_주면_같은_글():
|
||||
table = json.dumps(str(ROOT / "ui_template" / "ui_template_data_table.ts"))
|
||||
got = _node(
|
||||
PRELUDE
|
||||
+ f"""
|
||||
const box = {{ exports: {{}} }};
|
||||
new Function("exports", "module", "require", js(fs.readFileSync({table}, "utf8")))(
|
||||
box.exports, box, (p) => (p.endsWith(".css") ? {{}} : require(p)));
|
||||
const m = box.exports;
|
||||
const o = {{ columns: [{{ key: "a", label: "A" }}, {{ key: "b", label: "B" }}],
|
||||
rows: [{{ a: 1, b: 2 }}, {{ a: 3, b: 4 }}] }};
|
||||
const marked = m.renderDataTableHtml({{ ...o,
|
||||
cellClass: (i, k) => (i === 0 && k === "b" ? "is-picked" : null) }});
|
||||
console.log(JSON.stringify({{
|
||||
plain: m.renderDataTableHtml(o),
|
||||
none: m.renderDataTableHtml({{ ...o, cellClass: () => null }}),
|
||||
count: marked.split("is-picked").length - 1,
|
||||
row0: marked.split("<tbody>")[1].split("</tr>")[0],
|
||||
}}));
|
||||
"""
|
||||
)
|
||||
assert got["plain"] == got["none"]
|
||||
assert got["count"] == 1 and '<td class="is-picked">2</td>' in got["row0"]
|
||||
|
||||
|
||||
def test_고르기_항목_값_있는_출처_견적_최저_자동():
|
||||
base = _src("B09_Estimation/B09_Estimation_UI_Tabs_Base.ts")
|
||||
start = base.index("function appliedChoices")
|
||||
body = base[start : base.index("\n}\n", start) + 3]
|
||||
sources = _src("B09_Estimation/B09_Estimation_Calc_Base.ts")
|
||||
at = sources.index("export const SOURCES")
|
||||
line = sources[at : sources.index(";", at) + 1] + "\n"
|
||||
got = _node(
|
||||
PRELUDE
|
||||
+ f"""
|
||||
const appliedChoices = new Function(js({json.dumps(line.replace("export ", "") + body)}) + "; return appliedChoices;")();
|
||||
const r = {{ prices: {{ 관급: null, 물가정보: 1200, 물가자료: 0, 유통물가: 1000, 견적: 1500 }},
|
||||
lowest_from: "유통물가", applied_from: "유통물가", pick: null }};
|
||||
const seen = [];
|
||||
const items = appliedChoices(r, (s) => seen.push(s));
|
||||
items.forEach((x) => Array.isArray(x) && x[1]());
|
||||
const pick = {{ ...r, applied_from: "견적", pick: "견적" }};
|
||||
console.log(JSON.stringify({{
|
||||
labels: items.map((x) => (Array.isArray(x) ? x[0] : x)),
|
||||
seen,
|
||||
picked: appliedChoices(pick, () => 0).map((x) => (Array.isArray(x) ? x[0] : x)),
|
||||
}}));
|
||||
"""
|
||||
)
|
||||
assert got["labels"] == [
|
||||
"물가정보 1,200",
|
||||
"✓ 유통물가 1,000 · 최저",
|
||||
"견적 1,500",
|
||||
"-",
|
||||
"✓ 최저(자동)",
|
||||
]
|
||||
assert got["seen"] == ["물가정보", "유통물가", "견적", None]
|
||||
assert got["picked"][2] == "✓ 견적 1,500" and got["picked"][-1] == "최저(자동)"
|
||||
|
||||
|
||||
def test_배선_수기_적용_칸_출처_돌림_없앰():
|
||||
base = _src("B09_Estimation/B09_Estimation_UI_Tabs_Base.ts")
|
||||
compare = base[base.index("export const buildCompare") : base.index("function usesOf")]
|
||||
assert 'editable: (_i, key) => key === "p:견적",' in compare
|
||||
assert "nextPick" not in base and 'key === "applied_from"' not in compare
|
||||
assert 'key === `p:${rows[i]?.applied_from}` ? "is-picked"' in compare
|
||||
assert 'if (key !== "applied" || !r) return null;' in compare
|
||||
assert "delete cell.applied" in compare
|
||||
shell = _src("B09_Estimation/B09_Estimation_UI_Shell.ts")
|
||||
assert (
|
||||
"cellClass: spec.cellClass," in shell and "openMenuAt(box.left, box.bottom, items)" in shell
|
||||
)
|
||||
edit = _src("B09_Estimation/B09_Estimation_UI_Tabs_Bill_Edit.ts")
|
||||
assert "export function openMenuAt(" in edit and "const ctxMenu = sharedMenu();" in edit
|
||||
css = _src("B09_Estimation/B09_Estimation_UI_Page.css")
|
||||
assert "td.is-picked" in css and "var(--color-accent)" in css
|
||||
@@ -0,0 +1,88 @@
|
||||
"""183-1 재료비목록표 = 연결된 일위대가 · 산근 · 중기가 쓰는 자재만 · 수량 · 단가 한 줄 · 쓰는 곳.
|
||||
|
||||
- 수요: 사본 일위대가 자재 · 굳힌 사전조합 자재 코드도 든다(`Linked.used_keys`)
|
||||
- 거름: 대가 · 산근 · 중기 블록 재료 줄 · B08 줄 · 관급 · 별도 줄만 남김(`Summary.used_materials`)
|
||||
- 수량 = 입력 `material_qty`(1차 역전개 총수량) — `Calc_Base` 짝 거울 · 두 번 셈(`Router.compute_twice`)
|
||||
- 화면 비고 = 쓰는 곳(`UI_Tabs_Base.usesOf`)
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from B09_Estimation import B09_Estimation_Router as router_mod
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import compute_base
|
||||
from B09_Estimation.B09_Estimation_Linked import used_keys
|
||||
from B09_Estimation.B09_Estimation_Summary import used_materials
|
||||
from resources.tester.test_144_b09_base_calc import _run_ts
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
MATERIALS = [
|
||||
{"key": "MT1", "name": "쓰는 자재", "prices": {"물가자료": 1000}},
|
||||
{"key": "MT2", "name": "고르기 후보", "prices": {"물가자료": 500}},
|
||||
{"key": "MT3", "name": "관급 표시", "prices": {"물가자료": 300}},
|
||||
{"key": "MO1", "name": "경유", "prices": {"유가": 1800}},
|
||||
{"key": "b08:pipe_800", "name": "파형강관", "prices": {}, "qty": 8, "supply": "관급"},
|
||||
]
|
||||
USER = {"materials": {"MT3": {"supply": "관급"}}}
|
||||
SG = [{"key": "SG1", "no": 1, "lines": [{"kind": "material", "ref": {"key": "MT1"}, "qty": 2}]}]
|
||||
MACHINE = [{"key": "EQ1", "lines": [{"kind": "material", "ref": {"key": "MO1"}, "qty": None}]}]
|
||||
|
||||
|
||||
def test_사본_일위대가_자재_사전조합_자재_코드도_수요(tmp_path):
|
||||
labor = {
|
||||
"줄": {"담은로직": [], "자재": [{"코드": "MT000053"}, {"코드": "PM000001"}]},
|
||||
"참조사본": {"참조|system||SC-000001": {"담은로직": [], "자재": [{"코드": "MT000008"}]}},
|
||||
}
|
||||
(tmp_path / "일위대가.json").write_text(json.dumps(labor, ensure_ascii=False), "utf-8")
|
||||
assert used_keys(tmp_path) == ["MT000053", "MT000008"] # 개인 자재(PM) = 원가 표 밖
|
||||
|
||||
|
||||
def test_쓰는_자재만_남김():
|
||||
sheets = {"sg": SG, "daega_blocks": [], "machine_blocks": MACHINE}
|
||||
inputs = {"materials": MATERIALS}
|
||||
assert used_materials(sheets, inputs, USER) == {"MT1", "MO1", "MT3", "b08:pipe_800"}
|
||||
|
||||
|
||||
def test_수량_원천_material_qty_거울():
|
||||
inputs = {"labor": [], "machines": [], "materials": MATERIALS}
|
||||
old = {r["key"]: r["qty"] for r in compute_base(inputs, USER)["material"]}
|
||||
assert old["b08:pipe_800"] == 8 and old["MT1"] is None # 없으면 입력 줄 수량(지금 길)
|
||||
got = compute_base({**inputs, "material_qty": {"MT1": 6, "b08:pipe_800": 8}}, USER)
|
||||
rows = {r["key"]: r for r in got["material"]}
|
||||
assert (rows["MT1"]["qty"], rows["MT1"]["amount"]) == (6, 6000)
|
||||
assert rows["MT2"]["qty"] is None and rows["MT2"]["amount"] is None # 없는 키 = 빈칸
|
||||
for case in (inputs, {**inputs, "material_qty": {"MT1": 6}}):
|
||||
assert _run_ts(case, USER) == compute_base(case, USER)
|
||||
|
||||
|
||||
def test_두_번_셈_후보_줄_빠지고_수량_역전개(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake(root, inputs, user):
|
||||
calls.append([m["key"] for m in inputs["materials"]])
|
||||
base = compute_base(inputs, user)
|
||||
bill = {"rows": [{"kind": "item", "qty": 3, "ref": {"tab": "sg", "key": "SG1"}}]}
|
||||
return {**base, "sg": SG, "daega_blocks": [], "machine_blocks": MACHINE, "bill": bill}
|
||||
|
||||
monkeypatch.setattr(router_mod, "compute_sheets", fake)
|
||||
inputs = {"labor": [], "machines": [], "materials": list(MATERIALS)}
|
||||
sheets, summary = router_mod.compute_twice(Path("."), inputs, USER)
|
||||
assert calls[0] == [m["key"] for m in MATERIALS]
|
||||
assert calls[1] == ["MT1", "MT3", "MO1", "b08:pipe_800"] # 후보 MT2 뺌
|
||||
assert inputs["material_qty"] == {"MT1": 6.0} # 설계내역서 3 × 호표 2
|
||||
rows = {r["key"]: r for r in sheets["material"]}
|
||||
assert (rows["MT1"]["qty"], rows["MT1"]["amount"]) == (6.0, 6000)
|
||||
assert [r["qty"] for r in summary["sum_material"]] == [rows["MT1"]["qty"]] # 집계표와 같은 값
|
||||
|
||||
|
||||
def test_화면_비고_쓰는_곳():
|
||||
src = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Base.ts").read_text(encoding="utf-8")
|
||||
for label in (
|
||||
"`대가 ${b.no}호표`",
|
||||
"`산근 ${b.no}호표`",
|
||||
"`중기 ${n + 1}호표`",
|
||||
'"내역 자재대"',
|
||||
):
|
||||
assert label in src
|
||||
assert 'text("uses", "비고(쓰는 곳)"' in src and 'text("origin", "비고"' not in src
|
||||
Reference in New Issue
Block a user