- 끝수 한 곳 `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>
230 lines
9.7 KiB
Python
230 lines
9.7 KiB
Python
"""프로젝트 복사본 · 일위대가 문서(PLAN 39-1c · 임시 저장소 · 임시 마스터 사본).
|
|
|
|
- 층 `labor/<열>.json` 이 있으면 스냅샷 `일위대가.json` = {줄: 문서, 로직, 빠진로직, 알약} ·
|
|
단가 묶음에 자재 MT
|
|
- `입력값.json` 첫 값 = 문서 입력값(확인 거짓) · 재적용은 프로젝트에서 넣은 값이 이김
|
|
- 계산 = 수량(손 · 면적 알약 칸) · 반영 = 기준 × 수량 ÷ 배 · 자재 = 기준 단가 × 수량 ·
|
|
반영합 = 손셈 ·
|
|
기준 · 반영이 원본 층 문서 계산(`M02_Labor_Doc.calc`)과 같음
|
|
- 일위대가 쓰기는 400(읽기만)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from config import config_system
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
from M02_MasterTemplete import M02_Item_Copy as copies
|
|
from M02_MasterTemplete import M02_Item_Snapshot as snap
|
|
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
|
|
from M02_MasterTemplete import M02_Template_Layers as layers
|
|
from resources.tester.helper_m02_master_sample import SAMPLE
|
|
|
|
MASTER = config_system.PROJECT_ROOT / "resources/master_data"
|
|
PIPE = {"layer": "system", "owner": None, "key": "SS000011", "name": "관공 관매설"}
|
|
COLUMN = "pp_len"
|
|
FOLDER = "pipe@40.0"
|
|
LOGICS = ("GF000176", "GC000088", "GF000113")
|
|
MATERIAL = "MT000008"
|
|
HAND = 3 # GC000088 손 수량
|
|
AREA_QTY = 7.5 # 자재 · GF000113 은 면적 알약 칸 값
|
|
MAT_QTY = 4
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def master_copy(tmp_path_factory) -> Path:
|
|
target = tmp_path_factory.mktemp("master_data")
|
|
for path in MASTER.glob("*.json"):
|
|
shutil.copy2(path, target / path.name)
|
|
return target
|
|
|
|
|
|
def _read(path: Path) -> Any:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
def _write(path: Path, content: Any) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(content, ensure_ascii=False), encoding="utf-8")
|
|
|
|
|
|
def _first_input(code: str) -> tuple[str, Any]:
|
|
whole = m01.loaded()[1]
|
|
name, value = next(iter(m01.mau.values(whole, whole.logic(code)).items()))
|
|
return name, int(value) if isinstance(value, Decimal) and value % 1 == 0 else value
|
|
|
|
|
|
@pytest.fixture
|
|
def world(tmp_path: Path, monkeypatch, master_copy: Path) -> dict[str, Any]:
|
|
system = tmp_path / "master"
|
|
system.mkdir()
|
|
for name in (layers.COMBO_FILE,):
|
|
shutil.copy2(SAMPLE / name, system / name)
|
|
for kind in ("table", "structure", "area"):
|
|
shutil.copytree(SAMPLE / kind, system / kind)
|
|
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
|
|
monkeypatch.setattr(m01, "FOLDER", master_copy)
|
|
storage = tmp_path / "storage"
|
|
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage))
|
|
root = storage / "3" / "12" / "proj"
|
|
(root / "B05_Profile/route").mkdir(parents=True)
|
|
(root / "B04_PreProcess/drainage/edits").mkdir(parents=True)
|
|
|
|
name, value = _first_input("GC000088")
|
|
doc = labor_doc.clean(
|
|
{
|
|
"열": COLUMN,
|
|
"단위": "m",
|
|
"담은로직": [
|
|
{"로직": LOGICS[0]},
|
|
{"로직": LOGICS[1], "입력값": {name: value}, "수량": {"손": HAND}},
|
|
{"로직": LOGICS[2]},
|
|
],
|
|
"자재": [{"코드": MATERIAL, "별도계상": "직접계산"}], # 222-1 합에 셈
|
|
}
|
|
)
|
|
_write(system / "labor" / f"{COLUMN}.json", doc)
|
|
pills = {p["키"]: p for p in labor_doc.pills(doc)}
|
|
area_pill = next(k for k, p in pills.items() if LOGICS[2] in p.get("로직", []))
|
|
area = _read(system / "area" / f"{COLUMN}.json")
|
|
sheet = area["시트"][0]
|
|
sheet["칸"] = {"A1": {"값": AREA_QTY}, "A2": {"값": MAT_QTY}}
|
|
sheet["연결"] = [
|
|
{"키": area_pill, "칸": "A1"},
|
|
{"키": labor_doc.material_key(MATERIAL), "칸": "A2"},
|
|
]
|
|
_write(system / "area" / f"{COLUMN}.json", area)
|
|
return {
|
|
"root": root,
|
|
"doc": doc,
|
|
"input": (name, value),
|
|
"pills": pills,
|
|
"area_pill": area_pill,
|
|
}
|
|
|
|
|
|
def _save(root: Path) -> dict[str, list[str]]:
|
|
_write(root / "B05_Profile/route/structures.json", {"revision": 1, "structures": []})
|
|
pipe = {"chainage_m": 40.0, "source": "user", "summary_item": PIPE}
|
|
_write(root / "B04_PreProcess/drainage/edits/pipe_points.json", {"points": [pipe]})
|
|
return snap.sync_snapshots(root)
|
|
|
|
|
|
def _fill_auto(root: Path) -> None:
|
|
"""빠진 입력을 자동값으로(확인 참) — 문서 입력값은 그대로 둠."""
|
|
current = copies.read_file(root, FOLDER, "입력값")
|
|
values = current["내용"]
|
|
whole = m01.loaded()[1]
|
|
for code in LOGICS:
|
|
for key, auto in m01.mau.values(whole, whole.logic(code)).items():
|
|
mine = values.setdefault(code, {})
|
|
if key not in mine and auto is not None:
|
|
mine[key] = {"값": labor_doc._json(auto), "확인": True}
|
|
copies.write_file(root, FOLDER, "입력값", current["판"], values)
|
|
|
|
|
|
def test_스냅샷_일위대가는_문서_알약_자재_단가(world):
|
|
root = world["root"]
|
|
assert _save(root)["만듦"] == [FOLDER]
|
|
folder = root / "templates/structures" / FOLDER
|
|
labor = _read(folder / "일위대가.json")
|
|
assert labor["줄"] == world["doc"]
|
|
assert [row["키"] for row in labor["로직"]] == list(LOGICS) and labor["빠진로직"] == []
|
|
assert {p["키"] for p in labor["알약"]} == set(world["pills"])
|
|
keys = {
|
|
row["키"] for data in _read(folder / "단가.json").values() for row in data.get("줄", [])
|
|
}
|
|
assert MATERIAL in keys and set(LOGICS) <= keys
|
|
|
|
|
|
def test_입력값_첫_값은_문서_입력값(world):
|
|
root = world["root"]
|
|
_save(root)
|
|
name, value = world["input"]
|
|
values = _read(root / "templates/structures" / FOLDER / "입력값.json")
|
|
assert values == {LOGICS[1]: {name: {"값": value, "확인": False}}}
|
|
|
|
|
|
def test_재적용은_프로젝트_값이_이김(world):
|
|
root = world["root"]
|
|
_save(root)
|
|
name, _ = world["input"]
|
|
current = copies.read_file(root, FOLDER, "입력값")
|
|
mine = {LOGICS[1]: {name: {"값": 99, "확인": True}}}
|
|
copies.write_file(root, FOLDER, "입력값", current["판"], mine)
|
|
got = copies.reapply(root, FOLDER)
|
|
assert got["값있음"] is True
|
|
assert _read(root / "templates/structures" / FOLDER / "입력값.json") == mine
|
|
|
|
|
|
def test_계산_반영_합은_손셈_원본과_같음(world):
|
|
root = world["root"]
|
|
_save(root)
|
|
_fill_auto(root)
|
|
got = copies.calc(root, FOLDER)
|
|
assert [line for line in got["줄"] if line.get("까닭")] == [], got["줄"]
|
|
lines = {line["로직"]: line for line in got["줄"]}
|
|
hand, area = lines[LOGICS[1]], lines[LOGICS[2]]
|
|
assert (hand["수량"], hand["수량출처"]) == (HAND, "손")
|
|
assert (area["수량"], area["수량출처"]) == (AREA_QTY, "칸")
|
|
assert lines[LOGICS[0]]["수량출처"] == "없음" and lines[LOGICS[0]]["반영"] is None
|
|
for line in (hand, area):
|
|
base = Decimal(str(line["기준"]["계"]))
|
|
expect = base * Decimal(str(line["수량"])) / Decimal(str(line["배"]))
|
|
assert Decimal(str(line["반영"]["계"])) == pytest.approx(expect)
|
|
(mat,) = got["자재"]
|
|
assert mat["코드"] == MATERIAL and mat["수량"] == MAT_QTY and mat["기준단가"]
|
|
assert mat["반영"] == pytest.approx(mat["기준단가"] * MAT_QTY, abs=0.1) # 0.1 원 버림(751-3)
|
|
assert (
|
|
mat["가격"][mat["기준칸"]] == mat["기준단가"]
|
|
) # 복사본 단가 묶음으로도 가격 다섯 · 기준칸(43-3)
|
|
total = hand["반영"]["계"] + area["반영"]["계"] + mat["반영"]
|
|
assert 0 <= total - got["반영합"]["계"] < 3 # 반영합 = 비목마다 1 원 버림(751-3)
|
|
# 원본 층 문서 계산(마스터 같은 사본)과 같음 — 입력값은 프로젝트에 채운 것
|
|
values = _read(root / "templates/structures" / FOLDER / "입력값.json")
|
|
doc = {
|
|
**world["doc"],
|
|
"담은로직": [
|
|
{**one, "입력값": {k: v["값"] for k, v in values.get(one["로직"], {}).items()}}
|
|
for one in world["doc"]["담은로직"]
|
|
],
|
|
}
|
|
given = {world["area_pill"]: AREA_QTY, labor_doc.material_key(MATERIAL): MAT_QTY}
|
|
origin = labor_doc.calc(doc, given)
|
|
assert got["반영합"] == pytest.approx(origin["반영합"])
|
|
assert got["기준합"] == pytest.approx(origin["기준합"])
|
|
|
|
|
|
def test_일위대가_쓰기는_막힘(world):
|
|
root = world["root"]
|
|
_save(root)
|
|
current = copies.read_file(root, FOLDER, "일위대가")
|
|
assert current["내용"]["줄"]["열"] == COLUMN
|
|
with pytest.raises(ValueError, match="원본"):
|
|
copies.write_file(root, FOLDER, "일위대가", current["판"], current["내용"])
|
|
|
|
|
|
def test_같은_문서_같은_단가면_사본_셈은_층_셈(world):
|
|
"""142-1 — 사본 셈 = 층 셈 한 벌(`M02_Labor_Doc.calc`) · 빈 입력은 둘 다 자동값 · 줄 꼴 같음."""
|
|
root = world["root"]
|
|
_save(root)
|
|
got = copies.calc(root, FOLDER)
|
|
given = {world["area_pill"]: AREA_QTY, labor_doc.material_key(MATERIAL): MAT_QTY}
|
|
origin = labor_doc.calc(world["doc"], given)
|
|
keys = ("로직", "알약", "분모", "배", "기준", "수량", "수량출처", "반영", "까닭")
|
|
assert [{k: line.get(k) for k in keys} for line in got["줄"]] == [
|
|
{k: line.get(k) for k in keys} for line in origin["줄"]
|
|
]
|
|
assert got["자재"] == origin["자재"]
|
|
assert got["기준합"] == origin["기준합"] and got["반영합"] == origin["반영합"]
|
|
assert got["반영합"]["계"] > 0
|
|
assert {p["키"] for p in got["알약"]} == set(world["pills"]) # 사본 알약(가져온 때 것)
|
|
assert got["빠진입력"] # 비운 입력은 목록으로(화면 「빈 값 n」)
|