feat(M02): 프로젝트 복사본 일위대가 문서 — 스냅샷 · 입력값 첫 값 · 자재 단가 · 수량 반영 계산 (39-1c)

- Item_Source: 층 labor/<열>.json 이 있으면 일위대가.json = {줄: 문서, 로직, 빠진로직, 알약} · 없으면 옛 UA 조합 줄 · 단가 묶음에 자재 MT
- Item_Snapshot: 입력값.json 첫 값 = 문서 입력값 {로직: {입력: {값, 확인: false}}}
- Item_Prices: collect 가 자재 MT 도 · calc 가 줄마다 수량 · 반영 + 자재 · 기준합 · 반영합(M02_Labor_Doc.settle 한 벌 · 면적 알약 칸 값)
- Item_Copy · Project_Items_Router: 일위대가 종류 읽기만(쓰면 400) · 재적용은 새 첫 값 위에 프로젝트 값 · calc 에 면적
- 시험 test_m02_item_labor.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
This commit is contained in:
2026-10-04 17:55:58 +09:00
co-authored by Claude Opus 5.5
parent 9242cd15e5
commit e1ad3d3625
6 changed files with 321 additions and 27 deletions
+9 -2
View File
@@ -55,6 +55,7 @@ KINDS = {
"입력연결": LINK_FILE,
"입력값": VALUES_FILE,
"고른자재": PICKS_FILE,
"일위대가": LABOR_FILE, # 읽기만 — 일위대가는 원본 층에서 고침(39-1c)
}
#: 뼈대 검사에 쓰는 층 양식 종류(도면 · 면적산출 · 재료산출)
SKELETON = {"도면": "structure", "면적": "area", "재료": "material"}
@@ -171,6 +172,8 @@ def _check_links(content: Any) -> None:
def check(kind: str, content: Any) -> None:
if kind == "일위대가":
raise ValueError("일위대가는 원본(마스터 · 회사 · 개인) 층에서 고침 — 프로젝트는 넣을 값만")
if kind == "입력값":
_check_values(content)
elif kind == "고른자재":
@@ -253,7 +256,10 @@ def reapply(project_root: str | Path, folder: str) -> dict[str, Any]:
before = {name: read_json(target / file) for name, file in KINDS.items()}
files = build_snapshot(manifest["원본"])
labor = files.get(LABOR_FILE) or {}
files[VALUES_FILE] = _kept_values(values, labor)
seeded = files[VALUES_FILE] # 새 원본의 첫 값 — 프로젝트에서 넣은 값이 같은 키면 이김
kept_values = _kept_values(values, labor)
for logic, inputs in kept_values.items():
seeded[logic] = {**seeded.get(logic, {}), **inputs}
files[PICKS_FILE] = _kept_picks(picks, labor)
for old in target.glob("*.json"): # 원본에서 빠진 파일(도면 없앰 등)은 지움
if old.name != MANIFEST_NAME and old.name not in files:
@@ -268,7 +274,7 @@ def reapply(project_root: str | Path, folder: str) -> dict[str, Any]:
source = version_of(source_files(files))
stamp = now_text()
# 원본을 다시 가져왔으니 시트 고침은 없음 — 살린 입력값 · 고른 자재는 `값있음` 으로 따로 알림
kept = bool(files[VALUES_FILE] or files[PICKS_FILE])
kept = bool(kept_values or files[PICKS_FILE])
fresh = {**manifest, "판": source, "원본판": source, "가져온날": stamp, "고침": False}
fresh["값있음"] = kept
fresh.pop("고친날", None)
@@ -343,4 +349,5 @@ def calc(project_root: str | Path, folder: str) -> dict[str, Any]:
read_json(target / PRICE_FILE) or {},
read_json(target / VALUES_FILE) or {},
read_json(target / PICKS_FILE) or {},
read_json(target / KINDS["면적"]),
)
+48 -6
View File
@@ -8,9 +8,12 @@
원문번호 틀의 `{입력}` 은 그 입력의 고르기 값으로 모두 끼워 모음(고르기가 없으면 그 테이블 전부 ·
자재품목만은 너무 커서 뺌). 줄은 마스터 파일 차례 그대로 담음 — 고르기의 「첫 줄」 이 같게.
일위대가 자재(MT · 39-1c)도 그 줄(+ 준용 · 연결)을 담음.
`단가.json` = `{파일 이름: {그룹 · 머리 칸 …, 줄|표: [줄]}}` — `master_formula.Master` 가
그대로 읽는 꼴.
계산(`calc`)은 이 묶음과 `입력값.json` · `고른자재.json` 만 씀 — 마스터 현재값을 읽지 않음.
계산(`calc`)은 이 묶음과 `입력값.json` · `고른자재.json` · `면적.json`(알약 칸 값) 만 씀 — 마스터
현재값을 읽지 않음. 수량 · 반영 · 합은 일위대가 문서와 같은 함수(`M02_Labor_Doc.settle`).
"""
from __future__ import annotations
@@ -200,12 +203,14 @@ class _Collector:
self.pick(row, item["요소"], item.get("단위"))
def collect(logic_keys: list[str]) -> dict[str, Any]:
"""담은 로직의 닫힌 단가 묶음 — `{파일 이름: 파일(줄은 모은 것만 · 차례 그대로)}`."""
def collect(logic_keys: list[str], materials: list[str] | None = None) -> dict[str, Any]:
"""담은 로직 + 자재(MT)의 닫힌 단가 묶음 — `{파일 이름: 파일(줄은 모은 것만 · 차례 그대로)}`."""
files, whole = master()
collector = _Collector(whole)
for key in logic_keys:
collector.logic(key)
for code in materials or []:
collector.add(code)
out: dict[str, Any] = {}
for name, data in files.items():
slot = m01.items_key(data)
@@ -256,9 +261,17 @@ def missing_inputs(snapshot: Any, labor: dict[str, Any], values: dict[str, Any])
def calc(
labor: dict[str, Any], prices: dict[str, Any], values: dict[str, Any], picks: dict[str, Any]
labor: dict[str, Any],
prices: dict[str, Any],
values: dict[str, Any],
picks: dict[str, Any],
area: Any = None,
) -> dict[str, Any]:
"""복사본만으로 계산 — 입력이 빠진 로직은 안 돌리고 `멈춤` · 세 비목 · 계는 돈 로직 합."""
"""복사본만으로 계산 — 입력이 빠진 로직은 안 돌리고 `멈춤` · 세 비목 · 계는 돈 로직 합
(수량 1 기준).
일위대가 문서(39-1c)면 줄마다 수량 · 반영 · `자재` · `기준합` · `반영합` 을 덧붙임 —
수량 = 손 · 면적 알약 칸(`area`) · 반영 = `M02_Labor_Doc.settle`(문서 계산과 한 벌)."""
snapshot = load_prices(prices)
gaps = missing_inputs(snapshot, labor, values)
blocked = {gap["로직"] for gap in gaps}
@@ -293,4 +306,33 @@ def calc(
"계": got["계"],
}
)
return to_json({"줄": lines, **sums, "계": sum(sums.values()), "빠진입력": gaps, "멈춤": stuck})
out = {"줄": lines, **sums, "계": sum(sums.values()), "빠진입력": gaps, "멈춤": stuck}
return to_json({**out, **_reflected(snapshot, labor, lines, area)})
def _reflected(snapshot: Any, labor: dict[str, Any], lines: list[dict], area: Any) -> dict:
"""줄마다 수량 · 반영 + 자재 + 기준합 · 반영합 — 기준은 위 줄 그대로 · 알약은 가져온 때 것."""
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
doc = labor.get("줄") or {}
pill_of = {code: pill for pill in labor.get("알약") or [] for code in pill.get("로직") or []}
base = []
for line in lines:
pill = pill_of.get(line["로직"]) or {}
money = {k: line[k] for k in (*mf.COST_ITEMS, "계")} if line.get("갈래") == "돈" else None
head = {"로직": line["로직"], "알약": pill.get("키"), "배": pill.get("배") or 1}
base.append({**head, "분모": pill.get("단위"), "기준": money})
mats = []
for one in doc.get("자재") or []:
code = str(one.get("코드") or "")
try:
price, _ = mm.element(snapshot, code, {}, one.get("단위") or None)
except mf.FormulaError:
price = None
mats.append({"코드": code, "알약": labor_doc.material_key(code), "기준단가": price})
settled = labor_doc.settle(
to_json({"줄": base, "자재": mats}), doc, labor_doc.area_values(area)
)
for line, done in zip(lines, settled["줄"]):
line.update({k: done[k] for k in ("알약", "분모", "배", "수량", "수량출처", "반영")})
return {"자재": settled["자재"], "기준합": settled["기준합"], "반영합": settled["반영합"]}
+18 -3
View File
@@ -6,7 +6,8 @@
[저장](`PUT /route/structures` · `PUT /drainage/pipe-points`) 뒤 서버가 맞춤:
- 항목을 지정한 구조물마다 원본 층에서 내용을 복사(`M02_Item_Source.build_source`) + 프로젝트 값
`입력값.json`(처음엔 빈 값 · 자동값은 안 적음) · `고른자재.json` · `manifest.json`
`입력값.json`(첫 값 = 일위대가 문서 입력값 · 확인 거짓 · 자동값은 안 적음) · `고른자재.json` ·
`manifest.json`
- 이미 있는 복사본은 다시 쓰지 않음 — 원본(마스터 · 회사 · 개인)이 바뀌거나 지워져도 그대로
([재적용]은 사용자가)
- 지정을 다른 항목으로 바꾸면 새로 가져옴 · 구조물을 지우거나 지정을 풀면 복사본을 지움 —
@@ -30,6 +31,7 @@ from common_util.common_util_json import atomic_write_json
from common_util.common_util_summary_item import snapshot_folder_name
from M02_MasterTemplete import M02_Template_Layers as layers
from M02_MasterTemplete.M02_Item_Source import (
LABOR_FILE,
LINK_FILE,
PRICE_FILE,
build_source,
@@ -85,9 +87,22 @@ def origin_of(item: dict[str, Any]) -> dict[str, Any]:
return {**item, "경로": path}
def seed_values(labor: Any) -> dict[str, Any]:
"""일위대가 문서 입력값 → 프로젝트 입력값 첫 값 `{로직: {입력: {값, 확인: false}}}`(39-1c)."""
doc = labor.get("줄") if isinstance(labor, dict) else None
out: dict[str, Any] = {}
for line in (doc or {}).get("담은로직") or []:
given = line.get("입력값") if isinstance(line, dict) else None
if isinstance(given, dict) and given and line.get("로직"):
out[str(line["로직"])] = {k: {"값": v, "확인": False} for k, v in given.items()}
return out
def build_snapshot(item: dict[str, Any]) -> dict[str, Any]:
"""원본 내용 + 빈 프로젝트 값 — `{파일 이름: 내용}` · SS 줄이 없으면 LookupError."""
return {**build_source(item), VALUES_FILE: {}, PICKS_FILE: {}}
"""원본 내용 + 프로젝트 값(입력값 첫 값 · 빈 고른자재) — `{파일 이름: 내용}` · SS 줄이 없으면
LookupError."""
files = build_source(item)
return {**files, VALUES_FILE: seed_values(files.get(LABOR_FILE)), PICKS_FILE: {}}
def source_files(files: dict[str, Any]) -> dict[str, Any]:
+35 -14
View File
@@ -2,9 +2,11 @@
모으는 것 = `{파일 이름: 내용}`:
`집계.json` SS 줄 + 열 정의 · `도면.json` SD · `면적.json` SA · `재료.json` SM ·
`자재조합.json` UM 줄 · `일위대가.json` UA 줄 + 담은 로직 줄(M01 시스템) ·
`자재조합.json` UM 줄 ·
`일위대가.json` = 층 `labor/<열id>.json` 문서(39-1c · 없으면 옛 UA 조합 줄)
+ 담은 로직 줄(M01 시스템) + 알약(가져온 때 그대로) ·
`입력연결.json` 층의 `inputs/<열id>.json`(없으면 빈 연결) ·
`단가.json` 계산에 닿는 마스터 줄 전부
`단가.json` 계산에 닿는 마스터 줄 전부(담은 로직 + 일위대가 자재 MT)
`원본판` = 모은 내용의 해시 — 지금 층 · 마스터에서 다시 모은 판과 대어 「마스터가 바뀜」 을 앎.
프로젝트가 고치는 값(`입력값.json` · `고른자재.json`)은 원본이 아니라 여기서 안 모음.
원본 층 자리 = `M02_Template_Layers` 경로 · 일위대가 · 단가는 M01 시스템 것.
@@ -23,6 +25,7 @@ TABLE_NAME = "구조물집계표"
UM_FILE = "자재조합.json"
#: 열 id → 스냅샷 파일(구조물 도면 · 면적산출 · 재료산출)
PART_FILES = (("structure", "도면.json"), ("area", "면적.json"), ("material", "재료.json"))
LABOR_KIND = "labor" # 열 id → 일위대가 문서(39-1c) — 스냅샷은 `LABOR_FILE` 에 감싸서
LINK_FILE = "입력연결.json"
PRICE_FILE = "단가.json"
LABOR_FILE = "일위대가.json"
@@ -80,15 +83,27 @@ def version_of(content: Any) -> str:
def labor_rows(ua_key: str) -> dict[str, Any] | None:
"""조합 줄(마스터 UA · 회사 UC · 개인 UP) + 담은 로직 줄(M01 시스템 · 없는 로직은 빼고 키만
`빠진로직`)."""
from M01_MasterData import M01_MasterData_Store as m01
"""옛 조합 줄(마스터 UA · 회사 UC · 개인 UP) + 담은 로직 줄 — labor 문서가 없는 열
(옮기기 전)."""
from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos
ua = combos.find(ua_key)
if ua is None:
return None
wanted = [str(entry.get("로직")) for entry in ua.get("담은로직") or [] if entry.get("로직")]
return None if ua is None else logic_rows(ua)
def labor_doc_rows(doc: dict[str, Any]) -> dict[str, Any]:
"""일위대가 문서(39-1a) + 담은 로직 줄 + 알약(`M02_Labor_Doc.pills` · 가져온 때 그대로 —
복사본 계산이 마스터를 안 읽게)."""
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
return {**logic_rows(doc), "알약": labor_doc.pills(doc)}
def logic_rows(row: dict[str, Any]) -> dict[str, Any]:
"""`{줄: row, 로직: 담은 로직 줄(M01 시스템), 빠진로직: 없는 로직 키}`."""
from M01_MasterData import M01_MasterData_Store as m01
wanted = [str(entry.get("로직")) for entry in row.get("담은로직") or [] if entry.get("로직")]
found: dict[str, dict[str, Any]] = {}
raw: dict[str, Any] = {} # M01 적재본은 수가 Decimal — 줄은 원본 파일 JSON 그대로 담음
for key in wanted:
@@ -98,11 +113,11 @@ def labor_rows(ua_key: str) -> dict[str, Any] | None:
continue
if file not in raw:
raw[file] = read_json(Path(m01.FOLDER) / file)
row = row_of(raw[file], key)
if row is not None:
found[key] = row
got = row_of(raw[file], key)
if got is not None:
found[key] = got
return {
"줄": ua,
"줄": row,
"로직": [found[key] for key in wanted if key in found],
"빠진로직": [key for key in wanted if key not in found],
}
@@ -142,10 +157,16 @@ def build_source(item: dict[str, Any]) -> dict[str, Any]:
um = row_of(read_json(root / UM_FILE), str(row["자재조합"]))
if um is not None:
files[UM_FILE] = um
labor = labor_rows(str(row["일위대가"])) if row.get("일위대가") else None
doc = read_json(root / LABOR_KIND / f"{column_id}.json") if column_id else None
if isinstance(doc, dict):
labor = labor_doc_rows(doc)
else:
labor = labor_rows(str(row["일위대가"])) if row.get("일위대가") else None
if labor is not None:
files[LABOR_FILE] = labor
files[LINK_FILE] = input_links(root, column_id)
logic_keys = [str(one.get("키")) for one in (labor or {}).get("로직", [])]
files[PRICE_FILE] = prices.to_json(prices.collect(logic_keys)) if logic_keys else {}
materials = [str(m.get("코드")) for m in ((labor or {}).get("줄") or {}).get("자재") or []]
wanted = logic_keys + [code for code in materials if code]
files[PRICE_FILE] = prices.to_json(prices.collect(logic_keys, materials)) if wanted else {}
return files
@@ -9,7 +9,9 @@
POST …/items/{폴더}/reapply 재적용(입력값 · 고른자재는 같은 키면 살림 · `덮임` 알림)
POST …/items/{폴더}/save-as {to, 경로, 이름} 개인 층에 새 항목으로(단가 묶음은 안 가져감)
POST …/items/{폴더}/calc 복사본만으로 계산(세 비목 · 계 · 빠진 입력)
종류 = 도면 · 면적 · 재료 · 자재조합 · 입력연결 · 입력값 · 고른자재 · `refresh` = 옛 이름(재적용).
일위대가 문서면 줄마다 수량 · 반영 + 자재 · 기준합 · 반영합
종류 = 도면 · 면적 · 재료 · 자재조합 · 입력연결 · 입력값 · 고른자재 · 일위대가(읽기만 · 쓰면 400) ·
`refresh` = 옛 이름(재적용).
⚠ 등록은 `main.py`(로그인만) — 프로젝트 권한(같은 회사)은 여기서 `_project` 로 봄(403 · 404).
"""
@@ -31,7 +33,7 @@ from M02_MasterTemplete.M02_Item_Source import item_name
router = APIRouter(prefix="/api/projects", tags=["M02 Project Items"])
Kind = Literal["도면", "면적", "재료", "자재조합", "입력연결", "입력값", "고른자재"]
Kind = Literal["도면", "면적", "재료", "자재조합", "입력연결", "입력값", "고른자재", "일위대가"]
class WriteBody(BaseModel):
+207
View File
@@ -0,0 +1,207 @@
"""프로젝트 복사본 · 일위대가 문서(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, "자재조합.json"):
shutil.copy2(SAMPLE / name, system / name)
for kind in ("table", "structure", "area", "material"):
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}],
}
)
_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 got["멈춤"] == [], 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):
expect = Decimal(str(line["계"])) * 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)
total = hand["반영"]["계"] + area["반영"]["계"] + mat["반영"]
assert got["반영합"]["계"] == pytest.approx(total)
# 원본 층 문서 계산(마스터 같은 사본)과 같음 — 입력값은 프로젝트에 채운 것
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["내용"])