Files
Aislo/resources/tester/test_727_sk_base_add.py
T
eomsangdonandClaude Opus 5.5 08ad680188 feat(728-2): SK 단위 환산 — 기초 기준단위 제안 · 바꾸면 배 · 계수 칸 · 둘째 기초 수량 처음값 · 돈 아닌 기초 뺌
- 서버 Sangeun_Calc: 배 = 바탕 기준단위 → SK 단위(master_unit.swap) · 성격 다름 = 문서 환산 {계수 | 단위중량} · 없으면 셈 · 저장 막음(까닭 글) · 「식」 · 빈 단위 = 옛 셈 그대로(일위대가 줄 모드)
- 화면 셈 답 줄 · 계 = SK 1 단위 몫(per_unit · 화면배) · 머리 띠 「100본 당」 · 「1 본 당」
- 서버 Sangeun_Doc: 환산 칸 검사(계수 양수 · 단위중량 종별 · 형상)
- B09 Labor_Source: SK 참조 배 = 같은 ratio_of(옛 바탕 앞 수 대신 · 기존 문서 같은 값)
- 화면 Sangeun_Detail: SK 단위 처음 값 = 기초 기준단위(「100본」) · 둘째 기초 수량 처음값 = 바탕 기준단위 ÷ 그 기초 기준단위(swapUnit) · 계 칸 원/SK 단위
- 새 Sangeun_Unit: 근거 글 · 환산 계수 칸 · 단위중량 표(CC000337) 고르기
- Cover_Model: 화면 품 × 화면배 = 문서 품(덮개 · 더한줄 · 세부) · 율 그대로
- Find_Pick: 돈 아닌 기초(결과단위 「원」 아님 21 개) 목록에서 뺌
- 낡은 시험 test_715_sangeun_engine(바탕 원/주 + SK ㎡) · test_727(바탕 원/㎡ + SK ㎥) = 성격 다른 단위가 이제 계수 없으면 막혀 SK 단위를 바탕 단위로 고침(금액 같음)
- 새 시험 test_728_2_sk_unit(100본 → 본 1,538.89 · ㎡ 44,791.32 · 계수 · 단위중량 · 식 · 둘째 기초 · 돈 아닌 기초 · 고침 꼴)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 10:52:05 +09:00

244 lines
11 KiB
Python

"""727 M02 단가산출 기초 넣기(요구서_내역19 · 내역 20-1) — 임시 storage · 실제 M01 읽기만.
- 727-1 고르기 창 기초 = 로직마다 한 줄(`logic/M02_Find_Heads.ts` · 옛 창도) · 규격 칸 = 「옵션 N」
- 727-2 첫 기초 = 바탕 · 둘째부터 = 더한줄 세부 펼침(덮개 · 뺀줄 · M01 개정 지문 · 꼬리표) ·
B09 굳힘 같은 값
- 727-3 넣은 뒤 고른 옵션 = SK `입력값` → 일위대가(펼침) · 굳힘도 그 옵션
"""
from __future__ import annotations
import json
import re
import subprocess
from pathlib import Path
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Item_Prices as prices
from M02_MasterTemplete import M02_Item_Source as source
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Logic_Standard as standard
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from resources.tester.test_m02_labor_ref import MINE, world # noqa: F401 — 같은 임시 층
ROOT = Path(__file__).resolve().parents[2]
M02 = ROOT / "M02_MasterTemplete"
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
GF = "GF000099" # 측구터파기 토사 · 원/㎥ · 입력 지역 · 보정작업
def _node(files: list[str], body: str):
code = "".join(IMPORTS.sub("", (ROOT / f).read_text("utf-8")) for f in files)
got = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-"],
input=code + body, capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert got.returncode == 0, got.stderr
return json.loads(got.stdout)
def _heads(rows):
body = (
f"\nconst rows = {json.dumps(rows, ensure_ascii=False, default=str)};"
"\nconsole.log(JSON.stringify(logicHeads(rows, (n) => `옵션 ${n}`)));"
)
return _node(["M02_MasterTemplete/logic/M02_Find_Heads.ts"], body)
def test_727_1_기초마다_한_줄():
whole = m01.loaded()[1]
rows = []
for key in (GF, "GC000269", "GC001109"):
rows += standard._rows_of(whole, whole.logic(key), False)
assert sum(r["키"] == GF for r in rows) == 3 # 옵션 축 보정작업 두 값 + 바탕
got = _heads(rows)
assert [r["키"] for r in got] == [GF, "GC000269", "GC001109"]
assert all(r["바꿈"] == {} for r in got)
one = got[0]
assert one["규격"] == "옵션 1" and one["금액"] == next(r for r in rows if r["키"] == GF)["금액"]
assert got[2]["규격"] == "" # 옵션 없는 기초
listed = standard.listing()
if listed["준비"]: # 파생 저장본이 있을 때 — 1,461 안팎(로직 수)
heads = _heads([{k: r[k] for k in ("키", "규격", "바꿈")} for r in listed["줄"]])
assert len(heads) == len({r["키"] for r in listed["줄"]})
assert 1400 <= len(heads) <= 1600
def test_727_1_두_창_모두_기초_한_줄():
find = (M02 / "M02_MasterTemplete_Find_Pick.ts").read_text("utf-8")
old = (M02 / "M02_MasterTemplete_Labor_Pick.ts").read_text("utf-8")
assert "std = s?.줄.length ? logicHeads(s.줄) : null;" in find
assert "std = got.줄.length ? logicHeads(got.줄) : null;" in old
assert "logicOnly" not in find
# ── 727-2 · 727-3 서버(임시 storage) ───────────────────────────────────────
# 금속기와 해체 3-2-1 · 원/㎡ · 지붕잇기공 0.018 · 보통인부 0.012 · 덧줄 공구손료 3 %
PCT = "GC001109"
SECOND = {"종류": "로직", "로직": PCT, "입력값": {}, "수량": 1}
COVER = {"labor:LB000036#0": {"qty": 0.036}}
DROP = ["pct:공구손료 및 경장비#0"]
def _sk(folder, **doc):
with store.at(folder):
made = refs.create("sangeun", {"이름": "sub7임시", "단위": "㎥", "더한줄": [], **doc})
return store.read("sangeun", made["이름"])["문서"]
def _run(folder, doc, given=None):
with store.at(folder):
return calc.calc_doc(doc, given)
def _line(got, cid):
return next(x for x in got["줄"] if x["cid"] == cid)
def _m01(code, values=None):
return float(labor.run_line(m01.loaded()[1], code, values or {}, None, None)["기준"]["계"])
def test_727_2_첫_기초_바탕_세부_줄(world): # noqa: F811
doc = _sk(world["personal"], 바탕={"로직": GF})
got = _run(world["personal"], doc)
assert _line(got, "labor:LB000002#0")["수량"] == pytest.approx(0.023)
assert _line(got, "logic:GC000267#0")["수량"] == pytest.approx(0.01546, abs=1e-5)
assert got["꼬리표"] == "9-12-1"
def test_727_2_기초_둘_세부_펼침_계(world): # noqa: F811
doc = _sk(world["personal"], 바탕={"로직": GF}, 더한줄=[SECOND])
bare = _run(world["personal"], _sk(world["personal"], 바탕={"로직": GF}))
got = _run(world["personal"], doc)
add = _line(got, "add:#0")
assert add["꼬리표"] == "3-2-1" and add["바탕판"] and add["뺀세부"] == []
sub = {x["cid"]: x for x in add["펼침"]}
assert list(sub) == [
"add:#0/labor:LB000036#0",
"add:#0/labor:LB000002#0",
"add:#0/pct:공구손료 및 경장비#0",
]
assert sub["add:#0/labor:LB000036#0"]["수량"] == pytest.approx(0.018)
assert sum(x["금액"] for x in add["펼침"]) == pytest.approx(add["금액"], rel=1e-9)
assert add["금액"] == pytest.approx(_m01(PCT), rel=1e-9)
assert got["기준"]["계"] == pytest.approx(bare["기준"]["계"] + _m01(PCT), rel=1e-9)
with store.at(world["personal"]): # 펼침 없는 셈(쓰는 자리) = 같은 계
plain = calc.run(doc, m01.loaded()[1])
assert float(plain["기준"]["계"]) == pytest.approx(got["기준"]["계"], rel=1e-9)
def test_727_2_둘째_기초_덮개_뺀줄_저장(world): # noqa: F811
one = {**SECOND, "수량": 2, "덮개": COVER, "뺀줄": DROP}
doc = _sk(world["personal"], 바탕={"로직": GF}, 더한줄=[one])
saved = doc["더한줄"][0]
assert saved["덮개"]["labor:LB000036#0"] == {
"qty": 0.036,
"원값": {"qty": 0.018, "단가": 233250},
}
assert saved["뺀줄"] == DROP and saved["바탕판"]
got = _run(world["personal"], doc)
add = _line(got, "add:#0")
want = 2 * (0.036 * 233250 + 0.012 * 172068)
assert add["금액"] == pytest.approx(want, rel=1e-9)
sub = {x["cid"]: x for x in add["펼침"]}
assert sub["add:#0/labor:LB000036#0"]["덮개"] == ["qty"]
assert sub["add:#0/labor:LB000036#0"]["수량"] == pytest.approx(0.072)
assert add["뺀세부"] == [{"cid": "add:#0/" + DROP[0], "이름": "공구손료 및 경장비"}]
lost = {**one, "덮개": {"labor:LB999999#0": {"qty": 1}}}
got = _run(world["personal"], {**doc, "더한줄": [lost]})
assert any(x["cid"] == "add:#0/labor:LB999999#0" for x in got["짝잃음"])
def test_727_2_굳힘_같은_값(world, monkeypatch): # noqa: F811
one = {**SECOND, "덮개": COVER, "뺀줄": DROP}
key = _sk(
world["personal"],
바탕={"로직": GF},
더한줄=[one],
입력값={"보정작업": "암석작업(연암・보통암・경암)"},
)
ref = {**MINE, "키": key["키"]}
with store.at(world["personal"]):
top = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 2}}]})
live = labor.calc(top, {})["줄"][0]
rows = source.labor_doc_rows(top)
bundle = source.price_bundle(rows, world["personal"])
frozen = rows["참조사본"][refs.ref_id(ref)]
assert frozen["굳힌줄"]["호표"][-1]["값묶음"] # 고친 둘째 기초 = 셈한 값묶음 줄
got = prices.calc(rows, json.loads(json.dumps(bundle, ensure_ascii=False)), {}, {}, None)
line = got["줄"][0]
assert not line["까닭"] and not live["까닭"]
for part in ("노무비", "재료비", "경비", "계"):
assert line["기준"][part] == pytest.approx(live["기준"][part], rel=1e-9)
def test_727_3_옵션_저장_일위대가_그_옵션(world): # noqa: F811
rock = {"지역": "전국평균", "보정작업": "암석작업(연암・보통암・경암)"}
doc = _sk(
world["personal"], 바탕={"로직": GF}, 입력값={"보정작업": "암석작업(연암・보통암・경암)"}
)
assert doc["입력값"] == {"보정작업": "암석작업(연암・보통암・경암)"} # 다시 열면 그대로
with store.at(world["personal"]):
up = refs.Expand(m01.loaded()[1])({**MINE, "키": doc["키"]})
assert float(up["기준"]["계"]) == pytest.approx(_m01(GF, rock), rel=1e-9)
assert _m01(GF, rock) > _m01(GF, {"지역": "전국평균", "보정작업": "해당 없음"})
# 둘째 기초 입력(바탕에 없는 이름)도 SK 입력값에 남고 그 옵션으로 셈
two = _sk( # 단위 = 바탕 단위(728-2 — 성격 다르면 계수 칸)
world["personal"],
바탕={"로직": PCT},
단위="㎡",
더한줄=[{**SECOND, "로직": GF}],
입력값=rock,
)
assert two["입력값"] == rock
got = _run(world["personal"], two)
assert _line(got, "add:#0")["금액"] == pytest.approx(_m01(GF, rock), rel=1e-9)
def _model(ans, doc, script):
body = (
f"\nconst ans = {json.dumps(ans, ensure_ascii=False)};"
f"\nconst doc = {json.dumps(doc, ensure_ascii=False)};\n{script}"
)
return _node(["M02_MasterTemplete/logic/M02_Sangeun_Cover_Model.ts"], body)
def test_727_2_화면_꼴_세부_줄_고침(world): # noqa: F811
doc = _sk(world["personal"], 바탕={"로직": GF}, 더한줄=[{**SECOND, "수량": 2}])
ans = _run(world["personal"], doc)
got = _model(
ans,
doc,
"""
const rows = rowsOf(ans, null, doc);
const view = hoView(ans, null, null, doc);
const sub = rows.find((r) => r.cid === "add:#0/labor:LB000036#0");
setLineCover(doc, sub, { qty: 0.072 }, false);
const covered = JSON.parse(JSON.stringify(doc.더한줄[0]));
dropLine(doc, rows.find((r) => r.cid === "add:#0/pct:공구손료 및 경장비#0"));
const dropped = [...doc.더한줄[0].뺀줄];
restoreLine(doc, "add:#0/pct:공구손료 및 경장비#0");
const one = { ...doc, 더한줄: [] };
console.log(JSON.stringify({
cids: rows.map((r) => r.cid), boxes: rows.map((r) => r.묶음), sub, covered, dropped,
back: doc.더한줄[0].뺀줄 ?? null, tags: view.호표.map((x) => x.꼬리표 ?? ""),
extraTags: view.덧줄.map((x) => x.꼬리표 ?? ""), viewCids: view.cids,
single: [...tagsOf(ans, one).values()],
}));
""",
)
assert "add:#0" not in got["cids"] and "add:#0/labor:LB000036#0" in got["cids"]
assert got["sub"]["sub"] == {"k": 0, "cid": "labor:LB000036#0", "scale": 2}
assert got["sub"]["수량"] == pytest.approx(0.036) and got["sub"]["묶음"] == "인력"
assert got["covered"]["덮개"]["labor:LB000036#0"]["qty"] == pytest.approx(0.036) # ÷ 수량 2
assert got["covered"]["덮개"]["labor:LB000036#0"]["원값"] == {"qty": 0.018, "단가": 233250}
assert got["dropped"] == DROP and got["back"] is None
assert got["tags"] == ["9-12-1", "9-12-1", "3-2-1", "3-2-1"] # 바탕 둘 · 둘째 기초 인력 둘
assert got["extraTags"] == ["3-2-1"] and got["single"] == [] # 기초 하나면 꼬리표 없음