Files
Aislo/resources/tester/test_184_2_b09_bill_free.py
T

364 lines
18 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.
"""184-2 B09 설계내역서 자유 편집 — 셈 · 저장(`user.bill` v2 덮개 · 계약 = `Calc_Bill.py` 머리).
- 덮개 없음 · 굳힘(보이는 줄 키 · 부모 · 차례만) = 기본 표 그대로 ·
번호 자동(1 · 1.1 · 관급 없음)
- 자동 줄 칸 덮기(이름 · 규격 · 단위 · 수량 · 연결) = 그 값 + auto_{칸} · null = 빈칸 ·
안 고친 칸 = B08 따라감
- gone = 안 살아남(「연결 없음」에도 없음) · 저장에 없는 자동 줄 = 기본 부모 끝 「새 자동 줄」 ·
원천이 안 낸 키 = 안 보임
- 새 1단 공종 = 직접비(sums) · 관급 자재 줄 = 관급자재대 밖 못 감 · 조달수수료 = 관급자재대 끝 ·
깊이 5 · 돎 · 옛 꼴 = 같은 표의 v2 · 구조물 블록 키 · 자동 줄 로직 연결 → sg_hand · 거울 · 다듬기
"""
import copy
import json
import re
import subprocess
import sys
from pathlib import Path
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_Sangeun as sg # noqa: E402
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
from B09_Estimation.B09_Estimation_Calc_Bill import compute_estimation # noqa: E402
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
ITEMS = [
{"key": "earth:1", "section": "1.1", "name": "규준틀", "unit": "개소", "qty": 14,
"ref": {"tab": "dg_list", "key": "B00194"}},
{"key": "earth:4", "section": "1.2", "name": "절토(토사)", "spec": "기계", "unit": "㎥",
"qty": 10, "ref": {"tab": "sg_list", "key": "D01341"}},
{"key": "earth:5", "section": "1.2", "name": "절토(연암)", "unit": "㎥", "qty": 9, "ref": None,
"why": "조합 밖 토공 산근 — 연결 없음"},
{"key": "haul:1", "section": "3", "name": "운반", "unit": "㎥", "qty": 2,
"ref": {"tab": "sg_list", "key": "D01341"}},
] # fmt: skip
HOPYO = {
"B00194": {"labor": 21936, "material": 3885, "expense": 0},
"D01341": {"labor": 1116, "material": 360, "expense": 463},
}
MATERIALS = [
{"key": "b08:pipe_800", "name": "파형강관", "spec": "Φ800", "unit": "M", "origin": "B08",
"prices": {"관급": 157000}, "qty": 212},
{"key": "b08:sand", "name": "골재", "spec": "모래", "unit": "㎥", "origin": "B08",
"prices": {"물가정보": 30000}, "qty": 16},
] # fmt: skip
INPUTS = {"materials": MATERIALS, "bill": {"items": ITEMS}, "hopyo": HOPYO}
BASE = {
"materials": {"b08:pipe_800": {"supply": "관급"}, "b08:sand": {"supply": "별도"}},
"estimate": {"procurement_fee_pct": 0.54},
}
BOOKS = {
"daega": [{"no": 3, "key": "DG:a", "labor": 1000, "material": 200, "expense": 30,
"sum": 1230}],
"sg_list": [{"no": 2, "key": "SG:new", "labor": 10, "material": 1, "expense": 0, "sum": 11}],
"sg_hand": {"earth:5": "SG:new"},
} # fmt: skip
DG = {"tab": "dg", "key": "DG:a"}
MAT = "mat:b08:pipe_800"
def _calc(bill=None, inputs=INPUTS, price_of=None):
user = {**BASE, **({"bill": bill} if bill is not None else {})}
return compute_estimation(inputs, user, price_of, BOOKS)
def _rows(sheets):
return {row["key"]: row for row in sheets["bill"]["rows"]}
def _keys(sheets):
return [row["key"] for row in sheets["bill"]["rows"]]
def _freeze(sheets, **more):
"""화면 첫 손질 굳힘 — 보이는 줄의 키 · 부모 · 차례(새 줄은 모든 칸) · fee 뺌."""
rows = []
for r in sheets["bill"]["rows"]:
if r["key"] == "fee":
continue
row = {"key": r["key"], "parent": r["parent"]}
if r.get("hand"):
row.update({k: r[k] for k in ("name", "spec", "unit", "qty", "ref")})
row.update({"kind": "group"} if r["kind"] == "group" else {})
rows.append(row)
return {"v": 2, "rows": rows, "gone": [], **more}
def _put(bill, key, **cover):
"""굳힌 덮개의 한 줄 칸 고침 · 부모 바꿈."""
out = copy.deepcopy(bill)
next(r for r in out["rows"] if r["key"] == key).update(cover)
return out
def test_덮개_없음_빈_굳힘_기본_표_번호():
plain = _calc()
assert _calc({})["bill"] == plain["bill"] # [초기화] = bill 지움(빈 v2 는 다듬기가 버림)
assert _calc(_freeze(plain))["bill"] == plain["bill"] # 굳혀도 표 · 합 · 연결 없음 그대로
rows = _rows(plain)
got = [(r["key"], r["no"], r["name"], r["level"], r["parent"]) for r in plain["bill"]["rows"]
if r["kind"] == "title"][:3] # fmt: skip
assert got == [("sec:1", "1", "토공사", 1, None), ("sec:1.1", "1.1", "준비공", 2, "sec:1"),
("sec:1.2", "1.2", "절토", 2, "sec:1")] # fmt: skip
assert (rows["sec:4"]["no"], rows["sec:G"]["no"], rows["fee"]["no"]) == ("4", "", "")
assert (rows["earth:4"]["auto"], rows["earth:4"]["parent"], rows["earth:4"]["cost"]) == (
True, "sec:1.2", "direct") # fmt: skip
assert (rows[MAT]["cost"], rows["fee"]["cost"], _keys(plain)[-1]) == ("supply", "supply", "fee")
assert not any(k.startswith("auto_") for r in rows.values() for k in r)
def test_자동_줄_칸_덮기_자동값_남김():
bill = _put(
_freeze(_calc()), "earth:4", name="절토 고침", spec=None, unit="m3", qty=100, ref=DG
)
row = _rows(_calc(bill))["earth:4"]
assert (row["name"], row["spec"], row["unit"], row["qty"]) == ("절토 고침", "", "m3", 100)
assert (row["auto_name"], row["auto_spec"], row["auto_unit"], row["auto_qty"]) == (
"절토(토사)", "기계", "㎥", 10) # fmt: skip
assert (row["ref"], row["auto_ref"], row["note"]) == (DG, {"tab": "sg_list", "key": "D01341"},
"대가3") # fmt: skip
assert (row["labor"], row["sum"]) == (100000, 123000) # 대가3 1,230 × 100
# 제목 이름 덮개 · 제목 = 이름만(수량 덮개 안 받음)
title = _rows(_calc(_put(bill, "sec:1.2", name="절토공", qty=5)))["sec:1.2"]
assert (title["name"], title["auto_name"], title["no"], "auto_qty" in title) == (
"절토공", "절토", "1.2", False) # fmt: skip
def test_B08_바뀜_안_고친_칸_따라감_고친_칸_지킴_null_빈칸():
bill = _put(_freeze(_calc()), "earth:4", qty=100)
bill = _put(bill, "earth:1", qty=None)
changed = copy.deepcopy(INPUTS)
for item in changed["bill"]["items"]:
item["qty"] = item["qty"] * 2
item["name"] = item["name"] + "*"
sheets = _calc(bill, changed)
rows = _rows(sheets)
assert (rows["earth:4"]["qty"], rows["earth:4"]["auto_qty"], rows["earth:4"]["name"]) == (
100, 20, "절토(토사)*") # fmt: skip
assert (rows["earth:5"]["qty"], rows["haul:1"]["qty"]) == (18, 4) # 안 고친 줄 = 따라감
assert rows["earth:1"]["qty"] is None and rows["earth:1"]["sum"] is None # 일부러 빈칸
assert ("earth:1", "수량 연결 없음") in {(g["key"], g["why"]) for g in sheets["gaps"]}
def test_gone_안_살아남_새_자동_줄_기본_부모_끝_원천_사라짐_안_보임():
bill = _freeze(_calc(), gone=["earth:5", "sec:G", MAT]) # sec:G · 자재 줄 = 못 지움
bill["rows"].append({"key": "earth:99", "parent": "sec:1.2", "qty": 3}) # 원천 없음
more = copy.deepcopy(INPUTS)
more["bill"]["items"] += [
{"key": "earth:5", "section": "1.2", "name": "되살아남?", "unit": "㎥", "qty": 1},
{"key": "earth:6", "section": "1.2", "name": "암발파", "unit": "㎥", "qty": 4, "ref": None},
]
sheets = _calc(bill, more)
keys = _keys(sheets)
assert "earth:5" not in keys and "earth:99" not in keys
assert {"sec:G", MAT} <= set(keys)
assert "earth:5" not in {g["key"] for g in sheets["gaps"]} # 지운 줄 = 연결 없음에도 없음
at = keys.index("earth:6")
assert keys[at - 1] == "earth:4" and keys[at + 1] == "sec:1.3" # 절토 끝
assert _rows(sheets)["earth:6"]["note"] == "새 자동 줄"
# 제목을 지웠으면 새 자동 줄 = 표 끝 맨 위
gone = _calc(_freeze(_calc(), gone=["sec:1.2", "earth:4", "earth:5"]), more)
assert (_keys(gone)[-1], _rows(gone)["earth:6"]["parent"]) == ("earth:6", None)
def test_새_1단_공종_직접비_번호_옮김_들여_내어():
plain = _calc()
bill = _freeze(plain)
keys = [r["key"] for r in bill["rows"]]
at = keys.index("sec:G") # 4 자재대 뒤 · 관급자재대 앞에 새 1단 공종
bill["rows"][at:at] = [
{"key": "hand:g", "kind": "group", "cost": "direct", "name": "부대공", "spec": "",
"unit": "", "qty": None, "ref": None},
{"key": "hand:a", "parent": "hand:g", "name": "a", "spec": "", "unit": "㎡", "qty": 10,
"ref": DG},
{"key": "hand:s", "parent": "hand:g", "kind": "group", "name": "소공종", "spec": "",
"unit": "", "qty": None, "ref": None},
] # fmt: skip
sheets = _calc(bill)
rows = _rows(sheets)
g = rows["hand:g"]
assert (g["no"], g["level"], g["cost"], g["sum"], g["section"]) == ("5", 1, "direct", 12300,
None) # fmt: skip
assert (rows["hand:s"]["no"], rows["hand:a"]["no"]) == ("5.1", "") # 레벨 2 묶음만 번호
totals = sheets["bill"]["totals"]["direct"]
assert totals["labor"] - plain["bill"]["totals"]["direct"]["labor"] == 10000
# 형제 차례 바꿈(위로 ↑) = 번호 따라감 · 들여(다른 묶음 아래) · 공종 제목도 옮김
up = copy.deepcopy(bill)
keys = [r["key"] for r in up["rows"]]
up["rows"].insert(keys.index("sec:1.1"), up["rows"].pop(keys.index("sec:1.2")))
rows = _rows(_calc(up))
assert (rows["sec:1.2"]["no"], rows["sec:1.1"]["no"]) == ("1.1", "1.2")
moved = _put(_put(bill, "earth:4", parent="sec:1.1"), "sec:3", parent="hand:g")
rows = _rows(_calc(moved))
assert (rows["earth:4"]["parent"], rows["earth:4"]["level"], rows["earth:4"]["section"]) == (
"sec:1.1", 3, "1.1") # fmt: skip
assert (rows["sec:3"]["level"], rows["sec:3"]["no"], rows["hand:s"]["no"]) == (2, "4.1", "4.2")
assert (rows["sec:4"]["no"], rows["hand:g"]["no"]) == ("3", "4")
assert rows["sec:1.1"]["sum"] == _rows(plain)["sec:1.1"]["sum"] + _rows(plain)["earth:4"]["sum"]
assert _calc(moved)["bill"]["sums"] == sheets["bill"]["sums"] # 옮겨도 직접비 합 같음
def test_관급_줄_관급자재대_밖_못_감_수수료_끝():
plain = _calc()
bill = _put(_freeze(plain), MAT, parent="sec:1")
bill["rows"].append({"key": "hand:k", "parent": "sec:G", "name": "관급 손", "spec": "",
"unit": "개", "qty": 10, "ref": DG}) # fmt: skip
sheets = _calc(bill)
rows = _rows(sheets)
assert (rows[MAT]["parent"], rows[MAT]["cost"]) == ("sec:G", "supply")
assert _keys(sheets)[-3:] == [MAT, "hand:k", "fee"] # 조달수수료 = 관급자재대 끝
net = 212 * 157000 + 2000 # + 손 줄 재료비 200 × 10
assert sheets["bill"]["sums"]["supply_net"] == net
assert sheets["bill"]["sums"]["labor"] == plain["bill"]["sums"]["labor"] # 관급 = 직접비 밖
assert rows["fee"]["material"] == int(net * 0.54 / 100)
def test_깊이_5_돎_부모_바로잡기():
bill = _freeze(_calc())
chain = [("hand:g1", "sec:3"), ("hand:g2", "hand:g1"), ("hand:g3", "hand:g2"),
("hand:g4", "hand:g3"), ("hand:g5", "hand:g4"), ("hand:g6", "hand:g5")] # fmt: skip
for key, parent in chain:
bill["rows"].append({"key": key, "parent": parent, "kind": "group", "name": key})
bill["rows"].append({"key": "hand:i", "parent": "hand:g6", "name": "i", "qty": 1, "ref": DG})
bill["rows"] += [
{"key": "hand:p", "parent": "hand:q", "kind": "group", "name": "p"},
{"key": "hand:q", "parent": "hand:p", "kind": "group", "name": "q"},
]
bill = _put(_put(bill, "sec:1", parent="earth:4"), "sec:G", parent="sec:1")
rows = _rows(_calc(bill))
got = [(k, rows[k]["level"], rows[k]["parent"]) for k in ("hand:g4", "hand:g5", "hand:g6",
"hand:i")] # fmt: skip
assert got == [("hand:g4", 5, "hand:g3"), ("hand:g5", 5, "hand:g3"),
("hand:g6", 5, "hand:g3"), ("hand:i", 5, "hand:g3")] # fmt: skip
assert rows["hand:g1"]["sum"] == 1230 and rows["sec:3"]["sum"] == rows["haul:1"]["sum"] + 1230
assert (rows["hand:p"]["parent"], rows["hand:q"]["parent"]) == (None, "hand:p") # 돎 = 맨 위
assert (rows["sec:1"]["parent"], rows["sec:G"]["parent"]) == (None, None) # 항목 부모 · G 고정
#: 옛 꼴(166-2 수량 · 182 손 줄 after · parent)
OLD = {
"qty": {"earth:4": 100},
"rows": [
{"key": "hand:g1", "section": "1.2", "after": "earth:4", "kind": "group", "name": "묶음"},
{"key": "hand:a", "section": "1.2", "after": "hand:g1", "parent": "hand:g1", "name": "a",
"unit": "㎡", "qty": 10, "ref": DG},
{"key": "hand:b", "section": "3", "after": "sec:3", "name": "b", "qty": 2, "ref": None},
],
} # fmt: skip
def test_옛_꼴_그대로_읽음_굳히면_같은_표():
old = _calc(OLD)
rows = _rows(old)
keys = _keys(old)
assert keys[keys.index("sec:1.2") :][:5] == ["sec:1.2", "earth:4", "hand:g1", "hand:a",
"earth:5"] # fmt: skip
assert (rows["earth:4"]["qty"], rows["earth:4"]["auto_qty"]) == (100, 10)
assert (rows["hand:a"]["parent"], rows["hand:b"]["parent"]) == ("hand:g1", "sec:3")
v2 = _put(_freeze(old), "earth:4", qty=100) # 화면 첫 손질 = v2 로 굳힘
assert _calc(v2)["bill"] == old["bill"]
REVET = {"key": "st:r", "section": "2.1", "name": "흙막이", "unit": "m", "qty": 30, "note": "",
"ref": {"tab": "dg_list", "key": "r"}, "owners": {"o1": 10, "o2": 20}} # fmt: skip
def test_구조물_블록_키_대가_번호_밀려도_덮개_같은_줄():
inputs = {**INPUTS, "bill": {"items": [REVET]}}
first = {"no": "대가17", "key": "blk:a", "labor": 100, "material": 0, "expense": 0}
shifted = {**first, "no": "대가18"} # 앞에 블록이 끼어 번호만 밀림
bill = _put(_freeze(_calc({}, inputs, lambda o: first)), "st:r#blk:a", qty=7)
row = _rows(_calc(bill, inputs, lambda o: shifted))["st:r#blk:a"]
assert (row["qty"], row["auto_qty"], row["note"], row["labor"]) == (7, 30, "대가18", 700)
assert row["ref"] == {"tab": "dg", "key": "blk:a"}
def test_자동_줄_로직_연결_새_산근_쓰는_곳():
logic = {"tab": "logic", "code": "GF000219", "name": "돌쌓기", "given": {}}
bill = _put(_freeze(_calc()), "earth:5", ref=logic)
user = {**BASE, "bill": store.clean_bill(bill)}
assert [(k, u) for k, u, _s in sg.hand_sources(user)] == [("earth:5", "내역 줄 돌쌓기")]
row = _rows(compute_estimation(INPUTS, user, None, BOOKS))["earth:5"]
assert (row["note"], row["ref"], row["sum"]) == (
"산근 2호표",
{"tab": "sg", "key": "SG:new"},
99,
)
assert row["auto_ref"] is None # 원래 연결 없음
def test_저장_다듬기_v2_왕복(tmp_path):
raw = {
"v": 2,
"rows": [
{"key": "sec:1", "name": None, "qty": 3, "junk": 1},
{"key": "earth:4", "parent": "sec:1.1", "qty": "x", "unit": None, "ref": {"tab": "zz"}},
{"key": "earth:5", "qty": None, "ref": None, "name": 12},
{"key": "earth:4", "qty": 1}, # 겹친 키
{"key": "fee", "qty": 9}, # 조달수수료 = 덮개 없음
{"key": 3},
{"key": "hand:g", "kind": "group", "cost": "direct", "qty": 5, "ref": DG},
{"key": "hand:a", "parent": "hand:g", "kind": "zz", "cost": "direct", "qty": "1"},
],
"gone": ["earth:1", "earth:1", "hand:a", 3, ""],
}
clean = store.write_user(tmp_path, {"bill": raw})["bill"]
assert store.read_user(tmp_path)["bill"] == clean
assert clean["gone"] == ["earth:1"]
assert clean["rows"] == [
{"key": "sec:1", "name": None, "qty": 3},
{"key": "earth:4", "parent": "sec:1.1", "unit": None},
{"key": "earth:5", "name": "12", "qty": None, "ref": None},
{"key": "hand:g", "kind": "group", "cost": "direct", "name": "", "spec": "", "unit": "",
"qty": None, "ref": None},
{"key": "hand:a", "parent": "hand:g", "name": "", "spec": "", "unit": "", "qty": None,
"ref": None},
] # fmt: skip
assert "bill" not in store.clean_user({"bill": {"v": 2, "rows": [], "gone": []}})
back = store.read_user(tmp_path)
rows = _rows(compute_estimation(INPUTS, {**BASE, **back}, None, BOOKS))
assert "earth:1" not in rows and rows["earth:4"]["parent"] == "sec:1.1"
assert (rows["sec:1"]["name"], rows["earth:5"]["qty"]) == ("", None)
def _node(source: str) -> str:
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript"],
input=source, capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
return result.stdout
def test_거울_TS_가_같은_값():
calc = "".join(
IMPORTS.sub("", (ROOT / "B09_Estimation" / f"B09_Estimation_{n}.ts").read_text("utf-8"))
for n in ("Calc_Base", "Calc_Bill")
)
frozen = _freeze(_calc())
bills = [
None,
OLD,
frozen,
_put(_put(frozen, "earth:4", name=None, qty=3, ref=DG), MAT, parent="sec:1"),
{**frozen, "gone": ["earth:5", "sec:1.1"], "rows": frozen["rows"][3:]},
{"v": 2, "rows": [{"key": "hand:g", "kind": "group", "name": "g"},
{"key": "hand:a", "parent": "hand:g", "qty": 1, "ref": DG},
{"key": "sec:1", "parent": "hand:a"}], "gone": []},
] # fmt: skip
users = [{**BASE, **({"bill": b} if b else {})} for b in bills]
body = (
f"\nconst i = {json.dumps(INPUTS, ensure_ascii=False)};"
f"\nconst us = {json.dumps(users, ensure_ascii=False)};"
f"\nconst b = {json.dumps(BOOKS, ensure_ascii=False)};"
"\nconsole.log(JSON.stringify(us.map((u) => computeEstimation(i, u, null, b))));"
)
got = json.loads(_node(calc + body))
assert got == [compute_estimation(INPUTS, u, None, BOOKS) for u in users]