- 끝수 한 곳 `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>
164 lines
8.5 KiB
Python
164 lines
8.5 KiB
Python
"""704-3 일위대가 엑셀 꼴 격자 — 공용 부품 `ui_template_hopyo_grid` 줄 꼴 · M02 문서 + calc → 격자 줄 값.
|
|
|
|
TS 소스를 이어 붙여 Node(타입 지움)로 돌림 — 값은 받은 그대로(단가 = 기준 ÷ 배 · 금액 = 반영 · 합계 = 반영합).
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
|
GRID = "ui_template/ui_template_hopyo_grid.ts"
|
|
|
|
|
|
def _node(files, body, prelude=""):
|
|
src = "".join(IMPORTS.sub("", (ROOT / f).read_text("utf-8")) for f in files)
|
|
result = subprocess.run(
|
|
["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript"],
|
|
input=prelude + src + body, capture_output=True, text=True, encoding="utf-8", timeout=60,
|
|
) # fmt: skip
|
|
assert result.returncode == 0, result.stderr
|
|
return json.loads(result.stdout)
|
|
|
|
|
|
LINE = {"name": "돌쌓기", "spec": "뒷길이 35cm", "qty": 2.09, "unit": "㎡", "tag": "품셈",
|
|
"prices": {"sum": 52995, "labor": 41250, "expense": 11745},
|
|
"amounts": {"sum": 110759.6, "labor": 86212.5, "expense": 24547.1}} # fmt: skip
|
|
|
|
|
|
def test_머리_구성_합계_줄_꼬리표_빈_줄():
|
|
blocks = [
|
|
{"key": "A", "no": 3, "name": "돌기슭막이", "unit": "m", "cite": "산림품셈 13-2",
|
|
"total": {"sum": 110759.6, "labor": 86212.5}, "lines": [LINE, {**LINE, "name": "잡재료",
|
|
"qty": None, "prices": {}, "amounts": {}}]},
|
|
{"key": "B", "name": "번호 없음", "unit": "식", "total": {}, "lines": [], "blank": 2},
|
|
] # fmt: skip
|
|
out = _node([GRID], f"\nconst g = hopyoGrid({json.dumps(blocks, ensure_ascii=False)});"
|
|
"\nconst cls = g.rows.map((_, i) => hopyoCellClass(g.rows, i, 'tag') ?? hopyoCellClass(g.rows, i, 'note'));"
|
|
"\nconsole.log(JSON.stringify({ rows: g.rows, titles: [...g.titles], at: [...g.at], cls,"
|
|
" cols: g.columns.map((c) => c.key) }));") # fmt: skip
|
|
rows = out["rows"]
|
|
assert [r["name"] for r in rows] == [
|
|
"제 3 호표 돌기슭막이", "ㄴ 산림품셈 13-2", "ㄴ 돌쌓기", "ㄴ 잡재료", "합 계", "번호 없음", "합 계",
|
|
] # fmt: skip
|
|
assert rows[0]["sum"] == 110759.6 # 머리 = 1 단위 합계
|
|
assert (
|
|
rows[2]["labor_price"] == 41250 and rows[2]["labor"] == 86212.5 and rows[2]["qty"] == 2.09
|
|
)
|
|
assert rows[2]["material_price"] is None and rows[2]["tag"] == "품셈"
|
|
assert rows[4]["note"] == "빈 줄 1" and rows[4]["blank_names"] == [
|
|
"잡재료"
|
|
] # 금액 모르는 줄에서 셈
|
|
assert rows[6]["note"] == "빈 줄 2" # 준 빈 줄 수 그대로
|
|
assert out["titles"] == [0, 4, 5, 6] and out["at"] == [[2, [0, 0]], [3, [0, 1]]]
|
|
assert out["cls"][2] == "ui-hopyo-grid__tag is-pum" and out["cls"][4] == "ui-hopyo-grid__blank"
|
|
assert out["cols"][-1] == "tag" and out["cols"][:4] == ["name", "spec", "qty", "unit"]
|
|
|
|
|
|
STUBS = r"""
|
|
const tc = (k) => ({ Grid_NoQty: "수량 없음", Grid_Apart: "별도계상 · 합계 밖", Labor_Layer: "켜쌓기 {n}%", Grid_Head: "일위대가", Grid_Mine: "내 자료집", Lib_Company: "회사", Lib_System: "마스터" })[k] ?? k;
|
|
const valueLine = (inputs, values) => ({ text: Object.values(values ?? {}).join(" · "), empty: 0 });
|
|
// 751-3 `Labor_Calc.ts` 가 가져오는 B09 `rounddown`(이어 붙이기라 import 가 빠짐 · 같은 식)
|
|
const rounddown = (v, p = 0) => { const s = 10 ** p; const a = Math.floor(Math.abs(v) * s + 1e-7) / s; return v < 0 ? -a : a; };
|
|
"""
|
|
|
|
DOC = {
|
|
"종류": "일위대가",
|
|
"열": "C1",
|
|
"일위대가번": "SU-000006",
|
|
"단위": "m",
|
|
"담은로직": [
|
|
{"로직": "GF1", "별칭": None, "입력값": {"뒷길이": "35cm"}, "수량": None, "켜쌓기율": 90},
|
|
{"로직": "GF2", "별칭": "고임돌", "입력값": {}, "수량": None},
|
|
],
|
|
"자재": [
|
|
{"코드": "MT1", "이름": "큰돌", "규격": "30cm", "단위": "개", "수량": None},
|
|
{
|
|
"코드": "MT2",
|
|
"이름": "철선",
|
|
"규격": "",
|
|
"단위": "kg",
|
|
"수량": None,
|
|
"별도계상": "직접계산",
|
|
},
|
|
],
|
|
}
|
|
CALC = {"줄": [{"로직": "GF1", "분모": "㎡", "배": 10, "수량": 2.09,
|
|
"기준": {"노무비": 412500, "재료비": 0, "경비": 117450, "계": 529950},
|
|
"반영": {"노무비": 86212.5, "재료비": 0, "경비": 24547.05, "계": 110759.55}},
|
|
{"로직": "GF2", "분모": "㎥", "배": 1, "수량": None,
|
|
"기준": {"노무비": 24816, "재료비": 0, "경비": 0, "계": 24816}, "반영": None}],
|
|
"자재": [{"코드": "MT1", "기준단가": 1200, "수량": 5, "반영": 6000},
|
|
{"코드": "MT2", "기준단가": 900, "수량": 2, "반영": 1800}],
|
|
"반영합": {"노무비": 86212.5, "재료비": 1800, "경비": 24547.05, "계": 112559.55}} # fmt: skip
|
|
|
|
|
|
def test_M02_문서_calc_를_격자_줄로():
|
|
files = [GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts",
|
|
"M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts",
|
|
"M02_MasterTemplete/logic/M02_Born_Tag.ts",
|
|
"M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip
|
|
logics = [
|
|
["GF1", {"이름": "돌쌓기 찰쌓기", "입력": [], "결과단위": "㎡"}],
|
|
["GF2", {"이름": "고임돌 채집"}],
|
|
]
|
|
out = _node(files, (
|
|
f"\nconst b = laborBlock({json.dumps(DOC, ensure_ascii=False)}, {json.dumps(CALC, ensure_ascii=False)},"
|
|
f" new Map({json.dumps(logics, ensure_ascii=False)}));"
|
|
"\nconst wait = laborBlock(" + json.dumps(DOC, ensure_ascii=False) + ", null, new Map());"
|
|
"\nconsole.log(JSON.stringify({ b, foot: hopyoGrid([b]).rows.at(-1), wait: hopyoGrid([wait]).rows.at(-1) }));"
|
|
), STUBS) # fmt: skip
|
|
b = out["b"]
|
|
assert b["name"] == "일위대가" and b["unit"] == "m" and b["total"]["sum"] == 112559.55
|
|
one, two, mt1, mt2 = b["lines"]
|
|
assert (
|
|
one["name"] == "돌쌓기 찰쌓기"
|
|
and one["spec"] == "35cm · 켜쌓기 90%"
|
|
and one["unit"] == "㎡"
|
|
)
|
|
assert one["prices"] == {
|
|
"sum": 52995,
|
|
"labor": 41250,
|
|
"material": 0,
|
|
"expense": 11745,
|
|
} # 기준 ÷ 배
|
|
assert one["amounts"]["sum"] == 110759.55 and one["tag"] == "품셈"
|
|
assert two["name"] == "고임돌" and two["qty"] is None and two["note"] == "수량 없음"
|
|
assert mt1["prices"] == {"sum": 1200, "material": 1200} and mt1["note"] == "별도계상 · 합계 밖"
|
|
assert (
|
|
mt2["amounts"] == {"sum": 1800, "material": 1800}
|
|
and mt2["note"] == "MT2"
|
|
and "tag" not in mt2
|
|
)
|
|
assert out["foot"]["note"] == "빈 줄 1" and out["foot"]["blank_names"] == ["고임돌"]
|
|
assert out["wait"]["note"] == "" # 계산 중 = 빈 줄 알림 없음
|
|
|
|
|
|
def test_세_곳_같은_부품():
|
|
detail = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts").read_text("utf-8")
|
|
assert "laborGrid(cur, calc ?? last, logics, " in detail # 카드 · 상세 = 같은 Labor_Detail
|
|
m02 = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts").read_text("utf-8")
|
|
b09 = (ROOT / "B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts").read_text("utf-8")
|
|
assert '"@ui/ui_template_hopyo_grid"' in m02 and '"@ui/ui_template_hopyo_grid"' in b09
|
|
kit = (ROOT / "ui_template/ui_template_ui_kit_parts.ts").read_text("utf-8")
|
|
assert 'key: "hopyo-grid"' in kit # 부품 모음 견본
|
|
|
|
|
|
def test_번호는_글에_안_냄_사람_글은_그대로():
|
|
files = [GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts",
|
|
"M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts",
|
|
"M02_MasterTemplete/logic/M02_Born_Tag.ts",
|
|
"M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip
|
|
ref = {"로직": "", "별칭": None, "입력값": {}, "수량": {"손": 1},
|
|
"참조": {"층": "personal", "주인": "1/3", "키": "SC-000002"}} # fmt: skip
|
|
named = {**DOC, "일위대가번": "제5호표", "담은로직": [ref]}
|
|
out = _node(files, (
|
|
f"\nconst a = laborBlock({json.dumps(named, ensure_ascii=False)}, null, new Map());"
|
|
f"\nconst c = laborBlock({json.dumps({**named, '일위대가번': 'SP3-000015'}, ensure_ascii=False)}, null, new Map());"
|
|
"\nconsole.log(JSON.stringify({ a, c }));"
|
|
), STUBS) # fmt: skip
|
|
assert out["a"]["name"] == "제5호표" and out["c"]["name"] == "일위대가"
|
|
assert out["a"]["lines"][0]["note"] == "내 자료집"
|