- 서버 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>
244 lines
11 KiB
Python
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"] == [] # 기초 하나면 꼬리표 없음
|