- 끝수 한 곳 `master_cut.py`(B09 `rounddown` 재사용) — 엔진 답에 입힘: 줄 비목마다 0.1 원 버림 · 하위 로직 줄 = 하위 끝수 계 × 수량 · 덧줄 = 버린 줄 합으로 다시 셈 · 비목 합 1 원 버림 · 중간 칸도 버린 값 - 쓰는 곳: M01 시험 계산(`check_master.calc`) · 읽는 식(`master_text.lines`) · 옛 조합 미리보기(`master_combo`) · M02 `run_line`(단가산출 · 일위대가 · 펼침) · 별도계상 뺀 기준 · 켜쌓기율 곱 뒤 · 일위대가 반영 · 반영합 · 비율 줄 · 사전조합 안 줄 - 일위대가 반영 TS 짝(`Labor_Calc.ts`) 같은 끝수 · 거울 헬퍼가 상대 경로 .ts 가져옴 - B09 = `run_line(raw=True)` 엔진 날 값(제 단가로 다시 버림 · % 덧줄 율 비 그대로) — 검증_sub3 B09 247 칸 전후 같음 - `master_text` 꼴 셋 = `common_util_money` 로 갈음(줄 금액 `fmt_line_money` · 식 안 금액 = 버린 0.1 원) - 화면: 상세 줄 금액 · 시험 계산 줄 · 중간 돈 = `formatLineMoney` · 일위대가 반영 칸 0.1 원 · 상세 바닥 · 박스 · 인력재료경비 비목 소계 = 줄 합 1 원 버림 - 테스트1 네 보기 · 서고 목록 = 124,419(옛 124,420 / 124,419.85) · 재료 줄 11,694.6 + 23,425.6 + 1,404.8 = 소계 36,525 - 새 지킴 시험 test_751_3_money_cut - 옛 날 금액(끝수 없음)을 대조하던 시험 24 파일 고침 — 까닭 = 금액 규칙이 줄 0.1 원 · 소계 1 원 버림으로 바뀜(고정 값 → 새 값 · 줄 합 = 계 → 줄 합 아래 비목마다 1 원 안 · 곱 비 = 끝수 폭 안) · 실무 xlsx 영월 엑셀 같음 22 → 23 · 이어 붙이는 TS 시험(704_3 · 717_2b)에 `rounddown` 같은 식 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
256 lines
12 KiB
Python
256 lines
12 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)
|
|
# 머리 = 수량 × 기초 끝수 계(751-3 · 비목 1 원 · 줄 0.1 원 버림) — 펼침 합과 비목마다 수량 × 1 원 안
|
|
q = add["수량"]
|
|
assert sum(x["금액"] for x in add["펼침"]) == pytest.approx(add["금액"], abs=0.3 + 3 * q)
|
|
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")
|
|
cut = labor.mcut.down # 751-3 — 기초 줄 0.1 원 · 비목 합 1 원 → × 수량 0.1 원
|
|
want = cut(2 * cut(cut(0.036 * 233250, 1) + cut(0.012 * 172068, 1), 0), 1)
|
|
assert add["금액"] == pytest.approx(float(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_Born_Tag.ts",
|
|
"M02_MasterTemplete/logic/M02_Sangeun_Undo.ts",
|
|
"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.072
|
|
) # 화면 품 그대로(M01 원문 몫 · 748-0 — 수량 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"] == [] # 기초 하나면 꼬리표 없음
|