Merge remote-tracking branch 'origin/sub_laptop_6' into sub_laptop_8
This commit is contained in:
@@ -165,7 +165,7 @@ def test_빈칸_규칙():
|
||||
assert {"노임 단가 없음", "산근 단가 없음(145-1)", "수량 없음", "% 밑수 없음"} <= whys
|
||||
|
||||
|
||||
def _ts(daega, sheets, sangeun):
|
||||
def _ts(daega, sheets, sangeun, cover=None):
|
||||
base = IMPORTS.sub("", (ROOT / "B09_Estimation/B09_Estimation_Calc_Base.ts").read_text("utf-8"))
|
||||
calc = IMPORTS.sub(
|
||||
"", (ROOT / "B09_Estimation/B09_Estimation_Calc_Labor.ts").read_text("utf-8")
|
||||
@@ -174,7 +174,8 @@ def _ts(daega, sheets, sangeun):
|
||||
f"\nconst d = {json.dumps(daega, ensure_ascii=False)};"
|
||||
f"\nconst s = {json.dumps(sheets, ensure_ascii=False)};"
|
||||
f"\nconst g = {json.dumps(sangeun, ensure_ascii=False)};"
|
||||
"\nconst out = computeLabor(d, s, g);"
|
||||
f"\nconst c = {json.dumps(cover or {}, ensure_ascii=False)};"
|
||||
"\nconst out = computeLabor(d, s, g, c);"
|
||||
"\nconsole.log(JSON.stringify({out, price: daegaPrice(out, 'pipe@100.0')}));"
|
||||
)
|
||||
result = subprocess.run(
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
"""702-2 · 702-4 B09 일위대가 고친 칸 덮개 — 사용자 값 `daega_cover`
|
||||
{블록 cover: {줄 cid: {qty · labor · material · expense}}}.
|
||||
|
||||
- 덮개 열쇠 = 블록 `cover` · 줄 `cid`(`B09_Estimation_Labor_Source` · 내용 해시 밖)
|
||||
- 셈 = `B09_Estimation_Calc_Labor` 파이썬 · TS 짝(거울) — 고친 칸만 셈 값 대신 · `base` = 품셈 값
|
||||
- 노임 새 판(바탕 단가 바뀜) · 사본 다시 만듦([재적용]) 뒤에도 덮개 칸 그대로 · 안 고친 칸만 새 값
|
||||
- 짝 잃은 덮개 = `daega_orphans`(지우지 않음) · 저장 다듬기 = `B09_Estimation_Store.clean_cover`
|
||||
견본 = `test_145_2_b09_labor_calc` 엑셀 블록 · 견본 사본.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
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_Store as store # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor, daega_price # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Labor_Source import cover_count, daega_inputs # noqa: E402
|
||||
from M02_MasterTemplete import M02_Item_Snapshot as snapshots # noqa: E402
|
||||
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"b145", Path(__file__).with_name("test_145_2_b09_labor_calc.py")
|
||||
)
|
||||
b145 = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(b145)
|
||||
|
||||
|
||||
def _covered_excel():
|
||||
"""엑셀 블록 + cover · cid(사본 길이 붙이는 꼴)."""
|
||||
daega = copy.deepcopy(b145.EXCEL)
|
||||
for b in daega["blocks"]:
|
||||
b["cover"] = f"T:{b['id']}"
|
||||
for n, ln in enumerate(b["lines"]):
|
||||
ln["cid"] = f"{ln['kind']}:{ln.get('key') or ln['name']}#{n}"
|
||||
return daega
|
||||
|
||||
|
||||
#: DG9 찰붙임 석공 품 0.1 → 0.12 · DG2 % 2 → 3 · DG14 산근 SG10 노무 단가 손 · 짝 잃은 둘
|
||||
COVER = {
|
||||
"T:DG9": {"labor:L00005#0": {"qty": 0.12}},
|
||||
"T:DG2": {"pct:공구손료#3": {"qty": 3}},
|
||||
"T:DG14": {"sangeun:SG10#2": {"labor": 20000}},
|
||||
"T:없는블록": {"labor:L00017#0": {"qty": 1}},
|
||||
"T:DG1": {"labor:없는줄#9": {"qty": 1}},
|
||||
}
|
||||
|
||||
|
||||
def _line(out, bid, n):
|
||||
return next(b for b in out["daega_blocks"] if b["id"] == bid)["lines"][n]
|
||||
|
||||
|
||||
def test_고친_칸만_셈값_대신():
|
||||
daega = _covered_excel()
|
||||
plain = compute_labor(daega, b145.SHEETS, b145.SANGEUN)
|
||||
out = compute_labor(daega, b145.SHEETS, b145.SANGEUN, COVER)
|
||||
stone = _line(out, "DG9", 0)
|
||||
assert stone["qty"] == 0.12 and stone["covered"] == ["qty"]
|
||||
assert stone["base"] == {"qty": 0.1, "labor": 258935, "material": 0, "expense": 0}
|
||||
assert stone["amounts"]["labor"] == 31072.2 # ROUNDDOWN(258,935 × 0.12, 1)
|
||||
assert "covered" not in _line(out, "DG9", 1) # 안 고친 줄 = 덧칸 없음
|
||||
# 부른 블록이 바뀌면 부르는 블록 금액도(DG14 ← DG9) · 내역 단가도
|
||||
nine = next(b for b in out["daega"] if b["key"] == "DG9")
|
||||
assert nine["labor"] == 42556 + 31072 - 25893 # 석공 줄만 바뀜(ROUNDDOWN 0)
|
||||
assert daega_price(out, "pipe@100.0")["sum"] != daega_price(plain, "pipe@100.0")["sum"]
|
||||
pct = _line(out, "DG2", 4) # % 줄 = 끝에 셈(차례 4)
|
||||
assert pct["qty"] == 3 and pct["base"] == {"qty": 2} and pct["covered"] == ["qty"]
|
||||
sg = _line(out, "DG14", 2)
|
||||
assert sg["prices"]["labor"] == 20000 and sg["base"]["labor"] == 18084
|
||||
assert sg["covered"] == ["labor"] and sg["prices"]["material"] == 2498
|
||||
assert out["daega_orphans"] == [
|
||||
{"cover": "T:없는블록", "cid": "labor:L00017#0"},
|
||||
{"cover": "T:DG1", "cid": "labor:없는줄#9"},
|
||||
]
|
||||
assert plain["daega_orphans"] == []
|
||||
|
||||
|
||||
def test_덮개가_빈칸을_채움():
|
||||
daega = _covered_excel()
|
||||
sheets = {**b145.SHEETS, "labor": b145.SHEETS["labor"][:2]} # 석공 노임 없음
|
||||
bare = compute_labor(daega, sheets, b145.SANGEUN)
|
||||
assert _line(bare, "DG9", 0)["amount"] is None
|
||||
out = compute_labor(daega, sheets, b145.SANGEUN, {"T:DG9": {"labor:L00005#0": {"labor": 1e5}}})
|
||||
assert _line(out, "DG9", 0)["amount"] == 10000.0
|
||||
assert not [g for g in out["gaps"] if "찰붙임" in g["name"]]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", ["cover", "none"])
|
||||
def test_거울_파이썬_TS(case):
|
||||
daega = _covered_excel()
|
||||
cover = COVER if case == "cover" else {}
|
||||
py = json.loads(json.dumps(compute_labor(daega, b145.SHEETS, b145.SANGEUN, cover)))
|
||||
ts = b145._ts(daega, b145.SHEETS, b145.SANGEUN, cover)
|
||||
assert b145._same(py, ts["out"])
|
||||
assert b145._same(daega_price(py, "pipe@100.0"), ts["price"])
|
||||
|
||||
|
||||
def test_노임_새_판_덮개_칸_그대로():
|
||||
"""702-4 — 바탕 단가가 새 판으로 바뀌어도 고친 칸 = 그대로 · 안 고친 칸만 새 값."""
|
||||
daega = _covered_excel()
|
||||
cover = {"T:DG9": {"labor:L00005#0": {"qty": 0.12}, "labor:L00017#1": {"labor": 170000}}}
|
||||
old = compute_labor(daega, b145.SHEETS, b145.SANGEUN, cover)
|
||||
new_sheets = {
|
||||
**b145.SHEETS,
|
||||
"labor": [{**r, "price": round(r["price"] * 1.05)} for r in b145.SHEETS["labor"]],
|
||||
}
|
||||
new = compute_labor(daega, new_sheets, b145.SANGEUN, cover)
|
||||
a, b = _line(old, "DG9", 0), _line(new, "DG9", 0)
|
||||
assert a["qty"] == b["qty"] == 0.12 # 고친 품 그대로
|
||||
assert b["prices"]["labor"] == round(258935 * 1.05) != a["prices"]["labor"] # 안 고친 단가 = 새
|
||||
assert b["base"]["labor"] == b["prices"]["labor"] # 품셈 값 곁 = 새 판 값
|
||||
c, d = _line(old, "DG9", 1), _line(new, "DG9", 1)
|
||||
assert c["prices"]["labor"] == d["prices"]["labor"] == 170000 # 고친 단가 그대로
|
||||
assert d["qty"] == 0.03 and d["base"]["labor"] == round(165545 * 1.05)
|
||||
assert new["daega_orphans"] == []
|
||||
|
||||
|
||||
# ── 사본 → 덮개 열쇠 ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def project(tmp_path) -> Path:
|
||||
root = b145._project(
|
||||
tmp_path / "proj", [(100, "SS000901"), (200, "SS000901"), (400, "SS000902")]
|
||||
)
|
||||
b145._snap(root, "pipe@100.0", "SS000901", b145.LINES, b145._values(b145.STONE))
|
||||
b145._snap(root, "pipe@200.0", "SS000901", b145.LINES, b145._values(b145.STONE))
|
||||
promoted = [
|
||||
{"로직": "GF000219", "승격": True, "수량": {"손": 1}},
|
||||
{"로직": "GF000119", "승격": True, "수량": {"손": 1}},
|
||||
{"참조": {"층": "system", "주인": None, "키": "SC-T"}, "수량": {"손": 4}},
|
||||
]
|
||||
b145._snap(root, "pipe@400.0", "SS000902", promoted, b145._values(b145.STONE), b145.SC)
|
||||
return root
|
||||
|
||||
|
||||
def test_사본_덮개_열쇠(project):
|
||||
got = daega_inputs(project)
|
||||
blocks = {b["id"]: b for b in got["blocks"]}
|
||||
one = blocks[got["owners"]["pipe@100.0"]]
|
||||
assert one is blocks[got["owners"]["pipe@200.0"]] # 같은 꼴 = 한 호표 · 처음 본 사본 열쇠
|
||||
assert one["cover"] == f"S:{snapshots.folder_name('pipe@100.0')}"
|
||||
assert [ln["cid"] for ln in one["lines"]] == [
|
||||
"sangeun:SG:GF000219#0",
|
||||
"sangeun:SG:GF000119#0",
|
||||
"sangeun:SG:GF000143#0",
|
||||
]
|
||||
four = blocks[got["owners"]["pipe@400.0"]]
|
||||
assert [ln["cid"] for ln in four["lines"]] == [
|
||||
"daega:GF000219#0",
|
||||
"daega:GF000119#0",
|
||||
"daega:참조|system||SC-T#0",
|
||||
]
|
||||
stone = blocks[four["lines"][0]["key"]]
|
||||
assert stone["cover"].startswith("P:") and [ln["cid"] for ln in stone["lines"]] == [
|
||||
"labor:LB000033#0",
|
||||
"labor:LB000002#0",
|
||||
]
|
||||
assert blocks[four["lines"][2]["key"]]["cover"] == "C:참조|system||SC-T"
|
||||
|
||||
|
||||
def test_재적용_뒤_덮개_남음(project):
|
||||
"""702-4 — 사본을 다시 만들어(재적용 꼴 · 수량 · 줄 차례 바뀜 · 줄 빠짐) 블록 id 가 바뀌어도
|
||||
cover · cid 가 같아 고친 칸이 맞는 줄에 붙음 · 사라진 줄 덮개 = 짝 잃음(남김)."""
|
||||
before = daega_inputs(project)
|
||||
owner = before["owners"]["pipe@100.0"]
|
||||
first = next(b for b in before["blocks"] if b["id"] == owner)
|
||||
cover = {
|
||||
first["cover"]: {
|
||||
"sangeun:SG:GF000119#0": {"qty": 0.7},
|
||||
"sangeun:SG:GF000143#0": {"labor": 1000},
|
||||
}
|
||||
}
|
||||
folder = snapshots.structures_dir(project) / snapshots.folder_name("pipe@100.0")
|
||||
doc = json.loads((folder / "일위대가.json").read_text(encoding="utf-8"))
|
||||
lines = doc["줄"]["담은로직"]
|
||||
lines[0]["수량"] = {"손": 5} # 설계 바뀜(수량)
|
||||
doc["줄"]["담은로직"] = [lines[1], lines[0]] # 차례 바뀜 · GF000143 빠짐
|
||||
(folder / "일위대가.json").write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
|
||||
after = daega_inputs(project)
|
||||
block = next(b for b in after["blocks"] if b["id"] == after["owners"]["pipe@100.0"])
|
||||
assert block["id"] != owner and block["cover"] == first["cover"]
|
||||
cids = [ln["cid"] for ln in block["lines"]]
|
||||
assert cids == ["sangeun:SG:GF000119#0", "sangeun:SG:GF000219#0"]
|
||||
sheets = {"labor": [], "material": [], "machine": [], "gaps": []}
|
||||
out = compute_labor(after, sheets, {}, cover)
|
||||
got = next(b for b in out["daega_blocks"] if b["id"] == block["id"])
|
||||
assert got["lines"][0]["qty"] == 0.7 and got["lines"][0]["covered"] == ["qty"] # 맞는 줄
|
||||
assert got["lines"][1]["qty"] == 5 and "covered" not in got["lines"][1]
|
||||
assert out["daega_orphans"] == [{"cover": first["cover"], "cid": "sangeun:SG:GF000143#0"}]
|
||||
|
||||
|
||||
def test_저장_다듬기():
|
||||
raw = {
|
||||
"daega_cover": {
|
||||
"S:a": {"labor:L1#0": {"qty": 0.5, "labor": "x", "memo": 1}, "b#0": {"qty": None}},
|
||||
"S:빈": {},
|
||||
"S:c": "글",
|
||||
"P:d": {"x#0": {"expense": float("inf"), "material": 3}},
|
||||
}
|
||||
}
|
||||
assert store.clean_user(raw)["daega_cover"] == {
|
||||
"S:a": {"labor:L1#0": {"qty": 0.5}},
|
||||
"P:d": {"x#0": {"material": 3.0}},
|
||||
}
|
||||
assert "daega_cover" not in store.clean_user({"daega_cover": {"S:a": {}}})
|
||||
|
||||
|
||||
def test_저장_뒤_서버_값(tmp_path):
|
||||
"""[저장](write_user → read_user) 뒤 서버 셈 = 화면 짝 셈(덮개 입력값만 받고 다시 셈)."""
|
||||
stored = store.write_user(tmp_path, {"daega_cover": COVER})
|
||||
assert store.read_user(tmp_path)["daega_cover"] == stored["daega_cover"] == COVER
|
||||
daega = _covered_excel()
|
||||
server = compute_labor(daega, b145.SHEETS, b145.SANGEUN, stored["daega_cover"])
|
||||
screen = b145._ts(daega, b145.SHEETS, b145.SANGEUN, COVER)["out"]
|
||||
assert b145._same(json.loads(json.dumps(server)), screen)
|
||||
|
||||
|
||||
def test_B08_카드_고친_칸_수(project):
|
||||
"""702-4 B08 구조물도 카드 — 그 사본 블록 + 부른 블록의 덮개 수(카드 금액 = 사본 셈)."""
|
||||
got = daega_inputs(project)
|
||||
four = next(b for b in got["blocks"] if b["id"] == got["owners"]["pipe@400.0"])
|
||||
stone = next(b for b in got["blocks"] if b["id"] == four["lines"][0]["key"])
|
||||
store.write_user(
|
||||
project,
|
||||
{
|
||||
"daega_cover": {
|
||||
stone["cover"]: {"labor:LB000033#0": {"qty": 0.3}},
|
||||
"S:없음": {"x#0": {"qty": 1}},
|
||||
}
|
||||
},
|
||||
)
|
||||
assert cover_count(project, snapshots.folder_name("pipe@400.0")) == 1 # 부른 품셈 블록
|
||||
assert cover_count(project, snapshots.folder_name("pipe@100.0")) == 0
|
||||
Reference in New Issue
Block a user