feat(750-1): SK 셈 답 하나에 보기 넷 — 텍스트 수식 글 · 시험 계산 중간 · 일위대가 줄 모달 줄모드
- 새 `M02_Sangeun_Views` — SK 고친 줄(`_Over`) · 같은 입력 · 같은 고름 위에서 `master_text.lines` + `annotate` = `글` · 줄마다 cid · 금액 · 소계 · 계 = SK 줄 · 기준(별도계상 뺌 · 단위 환산 ÷배 · 못 셈 까닭) · `중간` = 시험 계산 중간값 - `calc_doc` — 문서 `줄모드` true = 일위대가 그 줄 셈(`line` · 별도계상 기본) · 몸 `보기` false = 덧칸 안 지음(바탕 원값 셈) - `master_text.lines` · `master_sheet_line.annotate` — `files` 자리에 Master 도 받음(파일 묶음 길 그대로 · M01 `/text` 답 같음) - `lineDoc` — `줄모드: true`(일위대가 줄 모달 89,299.60 = 그 줄 금액) - 시험 `test_717_3` — lineDoc = as_doc 대조에서 `줄모드` 칸 따로 확인(서버 as_doc 은 `line` 인자로 줄 셈 · 문서 칸 없음) - 새 지킴 `test_750_1_sk_views` — 변형 일곱 · 참조 바탕 · 줄 모달 · 보기 false Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,8 @@ SK 단위 환산(728-2) = 배 = 바탕 기준단위(「100본」) → SK 단위(
|
||||
화면 셈 길 줄 · 계 = SK 1 단위 몫(`per_unit`).
|
||||
바탕 재료(747-1) = 별도계상 빈 칸 = 직접계산(M01 기초처럼 재료비에 듦 · 「별도계상」 고른 줄만 밖) ·
|
||||
일위대가 덮개 줄(`line` · `M02_Labor_Patch.run`)은 일위대가 기본(222-1 · 162-1) 그대로.
|
||||
보기 넷 한 답(750-1) = 화면 셈 길 답에 `글`(읽는 식) · `중간`(`M02_Sangeun_Views`) · 일위대가 줄
|
||||
모달 = 문서 `줄모드` true → `line`(일위대가 그 줄 셈과 같은 값).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -32,6 +34,7 @@ from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_Views as views
|
||||
|
||||
COSTS = labor.COSTS
|
||||
Ref = dict[str, Any]
|
||||
@@ -328,10 +331,12 @@ def run(
|
||||
trail: tuple = (),
|
||||
detail: bool = False,
|
||||
line: bool = False,
|
||||
seen: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""SK 셈(저장 안 함) — `run_line` 답 + `{1단위, 배, 정체, 짝잃음, 바탕판}` · 줄마다 `cid` ·
|
||||
고친 칸 `덮개`(칸 이름) · `원값`. 틀린 문서 · 순환 = `sd.DocError` ·
|
||||
`detail` = 둘째 기초 세부 펼침(`_detail` · 화면 셈 길만) · `line` = 일위대가 덮개 줄."""
|
||||
`detail` = 둘째 기초 세부 펼침(`_detail` · 화면 셈 길만) · 그때 `seen` = 보기 덧칸(750-1) ·
|
||||
`line` = 일위대가 덮개 줄."""
|
||||
from M02_MasterTemplete.M02_Labor_Expand import Expand
|
||||
|
||||
got = resolve(doc, whole, read, trail, given, line)
|
||||
@@ -345,6 +350,10 @@ def run(
|
||||
names = {str(i.get("이름")) for i in row.get("입력") or []}
|
||||
values = {k: v for k, v in {**got["입력값"], **(given or {})}.items() if k in names}
|
||||
out = labor.run_line(over, key, values, got["켜쌓기율"], got["고름"] or None)
|
||||
eng = out.get("줄") or []
|
||||
seen = (
|
||||
views.text_of(over, key, values, got["고름"], got["정체"], eng) if detail and seen else {}
|
||||
)
|
||||
covers = doc.get("덮개") or {}
|
||||
lines = []
|
||||
for at, (cid, line) in enumerate(zip(got["정체"], out.get("줄") or [])):
|
||||
@@ -368,7 +377,7 @@ def run(
|
||||
]
|
||||
base = doc.get("바탕") or {}
|
||||
tag = tag_of(whole, base["로직"]) if "로직" in base else (base.get("참조") or {}).get("키")
|
||||
return {**out, **head, "1단위": unit, "빠진줄": gone, "꼬리표": tag}
|
||||
return {**out, **head, **seen, "1단위": unit, "빠진줄": gone, "꼬리표": tag}
|
||||
|
||||
|
||||
def unit(doc: dict, whole: Any, expand: Any, trail: tuple) -> dict[str, Any]:
|
||||
@@ -495,8 +504,9 @@ def _was(line: dict) -> dict[str, Any]:
|
||||
return {k: sd.plain(Decimal(str(v))) for k, v in keep.items() if v is not None}
|
||||
|
||||
|
||||
def calc_doc(doc: Any, given: dict | None = None) -> dict[str, Any]:
|
||||
"""셈 길(`M02_Sangeun_Router`) — 지금 층 문서 · 저장 전 화면 문서 · 틀림 400 · 순환 422."""
|
||||
def calc_doc(doc: Any, given: dict | None = None, seen: bool = True) -> dict[str, Any]:
|
||||
"""셈 길(`M02_Sangeun_Router`) — 지금 층 문서 · 저장 전 화면 문서 · 틀림 400 · 순환 422 ·
|
||||
`seen` = 보기 덧칸(`글` · `중간`) 지음."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
whole = m01.loaded()[1]
|
||||
@@ -504,10 +514,11 @@ def calc_doc(doc: Any, given: dict | None = None) -> dict[str, Any]:
|
||||
clean = sd.clean(doc, None, whole)
|
||||
_check_refs(clean, refs.here()) # 저장 전 문서도 위 층만 · 있는 문서 · 순환 422
|
||||
me = refs.ref_id({"층": refs.here()[0], "주인": refs.here()[1], "키": clean["키"]})
|
||||
got = run(clean, whole, given, trail=(me,), detail=True)
|
||||
line = isinstance(doc, dict) and doc.get("줄모드") is True # 일위대가 줄 모달(750-1)
|
||||
got = run(clean, whole, given, trail=(me,), detail=True, line=line, seen=seen)
|
||||
except sd.DocError as e:
|
||||
raise _fail(e) from e
|
||||
return labor._json({**per_unit(got, clean), "머리": head_of(clean)})
|
||||
return labor._json({**views.finish(per_unit(got, clean)), "머리": head_of(clean)})
|
||||
|
||||
|
||||
def from_line(
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
[단가산출로 저장](715-3) `POST /api/m02/layers/{층}/sangeun/from-line`(층 쓰기 권한).
|
||||
|
||||
저장 안 함 · 로그인 사용자 · 층 읽기 권한(`Router_Items._root`) · 화면은 최신 응답만.
|
||||
몸 = `문서`(저장 전 화면 문서) 또는 `키`(그 층 저장본) + `입력값`(열린입력 · 시험 값).
|
||||
몸 = `문서`(저장 전 화면 문서) 또는 `키`(그 층 저장본) + `입력값`(열린입력 · 시험 값) ·
|
||||
`보기` false = 보기 덧칸(`글` · `중간` · 750-1) 안 지음(바탕 원값 셈처럼 금액만 쓸 때).
|
||||
문서 읽기 · 쓰기 · 새로 · 복사 · 지우기는 양식 길 · 서고 길 그대로(종류 `sangeun`).
|
||||
⚠ 등록은 `main.py`(로그인만).
|
||||
"""
|
||||
@@ -27,6 +28,7 @@ class SangeunCalcBody(BaseModel):
|
||||
문서: dict[str, Any] | None = None
|
||||
키: str | None = None
|
||||
입력값: dict[str, Any] | None = None
|
||||
보기: bool = True
|
||||
|
||||
|
||||
def _calc(body: SangeunCalcBody) -> dict[str, Any]:
|
||||
@@ -35,7 +37,7 @@ def _calc(body: SangeunCalcBody) -> dict[str, Any]:
|
||||
if not sd.KEY.match(str(body.키 or "")):
|
||||
raise store.StoreError(400, "문서 또는 키(SK-######)")
|
||||
doc = store.read(sd.KIND, str(body.키))["문서"]
|
||||
return calc.calc_doc(doc, body.입력값)
|
||||
return calc.calc_doc(doc, body.입력값, body.보기)
|
||||
|
||||
|
||||
class FromLineBody(BaseModel):
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"""M02 단가산출(SK) 보기 넷 = 셈 답 하나(PLAN 750-1 · 요구 588-1) — 화면 셈 길 답 덧칸.
|
||||
|
||||
- `글` = 읽는 식(M01 `/text` 와 같은 꼴 · `master_text.lines` + `master_sheet_line.annotate`) ·
|
||||
SK 고친 줄(`_Over`) · 같은 입력(자동값 + 입력값) · 같은 고름(`choosing`) 위에서 지음.
|
||||
줄마다 `cid` = SK 줄 cid(`annotate` 와 같은 차례 — 호표 줄 × `_shares` 비목 · 뒤 덧줄).
|
||||
금액 · 소계 · 계 = SK `줄` · `기준` 그대로(`finish` — 별도계상 뺌 · 켜쌓기율 · 단위 환산 ÷배 ·
|
||||
못 셈 줄 까닭) · 식 글(`글` · `이름글`)은 바탕 단위 셈 그대로 + 머리 `나눔`(배 ≠ 1).
|
||||
- `중간` = 시험 계산 중간값(수 · 글만) — 시험 계산 = {lines: `줄`, sums: `기준`, middle: `중간`}.
|
||||
계산 자리 = 서버 단독(CLAUDE.md 5장 ③) · 화면은 그리기만.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
mf = labor.m01.cm.mf
|
||||
import master_sheet_line as msl # noqa: E402 — scripts 길은 `m01` 이 둠
|
||||
import master_text as mtx # noqa: E402
|
||||
|
||||
_ERRORS = (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError)
|
||||
|
||||
|
||||
def text_of(over: Any, key: str, values: dict, picks: Any, ids: list, eng: list) -> dict:
|
||||
"""`{글, 중간}` — `eng` = `run_line` 줄(별도계상 빼기 전 · 차례 맞춤용) · 못 지음 = 까닭."""
|
||||
row = over.logic(key)
|
||||
auto = {k: v for k, v in labor.m01.mau.values(over, row).items() if v is not None}
|
||||
given = {**auto, **labor.m01._dec(values or {})}
|
||||
try:
|
||||
with mf.mp.choosing(picks or None):
|
||||
text = msl.annotate(over, key, given, mtx.lines(over, key, given))
|
||||
mid = mf.run(over, key, given).get("중간") or {}
|
||||
except _ERRORS as e:
|
||||
return {"글": {"ok": False, "reason": str(e) or type(e).__name__}, "중간": {}}
|
||||
if text.get("ok") and "groups" in text and "결과" not in row:
|
||||
_tag(row, ids, eng, text)
|
||||
keep = (Decimal, int, float, str, bool)
|
||||
return {"글": text, "중간": {k: v for k, v in mid.items() if isinstance(v, keep)}}
|
||||
|
||||
|
||||
def _tag(row: dict, ids: list, eng: list, text: dict) -> None:
|
||||
"""글 줄마다 `cid` — `annotate` 와 같은 차례(호표 줄 × 몫 비목 → 덧줄)."""
|
||||
slots = {g["비목"]: iter(g["줄"]) for g in text["groups"]}
|
||||
items, adds = row.get("호표") or [], row.get("덧줄") or []
|
||||
for n, line in enumerate(eng):
|
||||
if n < len(items):
|
||||
parts = [p for p, _ in mtx._shares(line)] if line.get("비목") else []
|
||||
else:
|
||||
parts = [adds[n - len(items)]["비목"]] if n - len(items) < len(adds) else []
|
||||
for part in parts:
|
||||
one = next(slots.get(part, iter(())), None)
|
||||
if one is not None and n < len(ids):
|
||||
one["cid"] = ids[n]
|
||||
|
||||
|
||||
def finish(ans: dict) -> dict:
|
||||
"""화면 답(`per_unit` 뒤) — 글 금액 · 소계 · 계 = SK 줄 · 기준 · SK 에 없는 줄(별도계상) 뺌."""
|
||||
text = ans.get("글") or {}
|
||||
base = ans.get("기준")
|
||||
if not text.get("ok") or "groups" not in text or not base:
|
||||
return ans
|
||||
sk = {x["cid"]: x for x in ans.get("줄") or [] if x.get("cid")}
|
||||
groups = []
|
||||
for g in text["groups"]:
|
||||
kept = []
|
||||
for one in g["줄"]:
|
||||
line = sk.get(one.get("cid"))
|
||||
if line is None and one.get("cid"):
|
||||
continue # SK 에서 빠진 줄(별도계상)
|
||||
if line is not None:
|
||||
parts = line.get("비목") or {}
|
||||
money = parts.get(g["비목"]) if len(parts) > 1 else line.get("금액")
|
||||
one = {**one, "금액": money}
|
||||
if line.get("까닭"):
|
||||
one["까닭"] = line["까닭"]
|
||||
kept.append(one)
|
||||
if kept:
|
||||
groups.append({**g, "줄": kept, "소계": base.get(g["비목"])})
|
||||
times = ans.get("화면배")
|
||||
head = {"나눔": times} if times not in (None, 1) else {}
|
||||
return {**ans, "글": {**text, "groups": groups, "계": base.get("계"), **head}}
|
||||
@@ -632,6 +632,7 @@ export function lineDoc(
|
||||
덮개: patch?.덮개 ?? {},
|
||||
뺀줄: patch?.뺀줄 ?? [],
|
||||
더한줄: patch?.더한줄 ?? [],
|
||||
줄모드: true, // 셈 = 일위대가 그 줄 셈(별도계상 기본 · 750-1 · 서버 `calc_doc`)
|
||||
};
|
||||
if (row.참조) return { ...head, 바탕: { 참조: row.참조 } };
|
||||
return {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
- `환산근거` = 표 기준배 ↔ 결과단위 배(「10㎡당 → 1㎡당 ÷10」) · 재료 품목 단위 ↔ 줄 단위
|
||||
(712-2 `master_unit.note`) · 없으면 「」.
|
||||
줄 차례 맞춤 = `master_text.lines` 와 같은 차례(호표 줄마다 `_shares` 비목 · 뒤 덧줄).
|
||||
`files` 자리 = 파일 묶음 또는 Master(M02 SK 고친 줄 `_Over` · 750-1 · `_spread` 도 그대로 넘김).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -151,7 +152,7 @@ def annotate(files: dict, key: str, given: dict, answer: dict) -> dict:
|
||||
"""`master_text.lines` 답에 줄마다 `KEYS` 덧칸 · 머리 `기준단위` — 못 셈이면 답 그대로."""
|
||||
if not answer.get("ok") or "groups" not in answer:
|
||||
return answer
|
||||
master = mf.Master(files)
|
||||
master = files if hasattr(files, "logic") else mf.Master(files)
|
||||
row = master.logic(key)
|
||||
per = mbs.result_per(row.get("결과단위"))
|
||||
answer["기준단위"] = _per_text(per) or None
|
||||
|
||||
@@ -494,7 +494,7 @@ def lines(files: dict, key: str, given: dict) -> dict:
|
||||
"""로직 한 줄의 읽는 식 — 비목 묶음마다 줄과 소계 · 돈 아닌 로직은 「결과」 묶음 하나.
|
||||
호표 줄 계산이 막히면(재료 단가 없음 따위) `row` 구조만으로 이름 식·조각을 짓고
|
||||
`글`·`금액`은 비움(입력 자체가 없는 경우는 지금처럼 그대로 막힘 — `_env_of` 에서 걸림)."""
|
||||
master = mf.Master(files)
|
||||
master = files if hasattr(files, "logic") else mf.Master(files) # Master 째(750-1)
|
||||
row = master.logic(key)
|
||||
env = _env_of(master, row, given)
|
||||
marks = mc.aliases(row, master)
|
||||
|
||||
@@ -93,6 +93,7 @@ def test_화면_문서_서버_as_doc_꼴():
|
||||
"needsRef": ["needsIds", ref],
|
||||
}
|
||||
)
|
||||
assert got["logic"].pop("줄모드") is True and got["ref"].pop("줄모드") is True # 750-1
|
||||
assert got["logic"] == patch.as_doc({**one, "패치": drop}, whole)
|
||||
assert got["ref"] == patch.as_doc({**ref, "패치": COVER}, whole)
|
||||
assert got["bare"]["고름"] == {} and got["bare"]["덮개"] == {}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""SK 보기 넷 = 셈 답 하나(PLAN 750-1 · 요구 588-1) — 화면 셈 답 `글`(텍스트 수식) ·
|
||||
시험 계산(`줄` · `기준` · `중간`) = SK 줄 · 계 · 변형 일곱(기본 · 줄 빼기 · 더하기 · 덧줄 ·
|
||||
별도계상 · 단위 환산 · 재료 고름) + 참조 바탕 · 일위대가 줄 모달(`줄모드`) = 일위대가 그 줄 셈 ·
|
||||
`보기` false = 덧칸 없음 · M01 엔진 · storage 안 씀."""
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_Views as views
|
||||
|
||||
FORM = "GC000211" # 6-3-1 합판거푸집 — 재료 일곱 · 소모자재 = 재료비 % 덧줄
|
||||
PLY = "material:합판(내수 12㎜)#0"
|
||||
DOC = {"키": "SK-000002", "이름": "테스트1", "단위": "㎡", "바탕": {"로직": FORM}}
|
||||
ADD = {"종류": "인력", "요소": "LB000002", "이름": "보통인부(더함)", "단위": "인", "수량": 0.05}
|
||||
CASES = {
|
||||
"기본": {},
|
||||
"줄 빼기": {"뺀줄": ["material:각재#0"]},
|
||||
"더하기": {"더한줄": [ADD]},
|
||||
"덧줄": {"덮개": {"pct:소모자재#0": {"qty": 5}}},
|
||||
"별도계상": {"자재표시": {PLY: {"별도계상": "별도계상"}}},
|
||||
"단위 환산": {"단위": "10㎡"},
|
||||
"재료 고름": {"고름": {PLY: "MT000020"}},
|
||||
}
|
||||
|
||||
|
||||
def _whole():
|
||||
return m01.loaded()[1]
|
||||
|
||||
|
||||
def _answer(doc: dict, read=None, line: bool = False) -> dict:
|
||||
"""`calc_doc` 와 같은 길(층 검사만 뺌)."""
|
||||
whole = _whole()
|
||||
clean = sd.clean(doc, None, whole)
|
||||
got = calc.run(clean, whole, None, read or calc.refs.read_ref, detail=True, line=line)
|
||||
return labor._json(views.finish(calc.per_unit(got, clean)))
|
||||
|
||||
|
||||
def _same(a: dict) -> None:
|
||||
text, base = a["글"], a["기준"]
|
||||
assert text["ok"] is True
|
||||
money: dict[str, float] = {}
|
||||
for g in text["groups"]:
|
||||
assert g["소계"] == pytest.approx(base[g["비목"]])
|
||||
for one in g["줄"]:
|
||||
money[one["cid"]] = money.get(one["cid"], 0) + (one["금액"] or 0)
|
||||
assert set(money) == {x["cid"] for x in a["줄"]} # 글 줄 = SK 줄(cid)
|
||||
for x in a["줄"]:
|
||||
assert money[x["cid"]] == pytest.approx(x["금액"] or 0)
|
||||
assert text["계"] == pytest.approx(base["계"])
|
||||
assert sum(x["금액"] or 0 for x in a["줄"]) == pytest.approx(base["계"]) # 시험 계산 표 합
|
||||
assert a["중간"] and all(not isinstance(v, (dict, list)) for v in a["중간"].values())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(CASES))
|
||||
def test_four_views_one_answer(name):
|
||||
a = _answer({**DOC, **CASES[name]})
|
||||
_same(a)
|
||||
if name == "별도계상":
|
||||
assert all(one.get("cid") != PLY for g in a["글"]["groups"] for one in g["줄"])
|
||||
if name == "단위 환산":
|
||||
assert a["글"]["나눔"] == pytest.approx(0.1)
|
||||
plain = _answer(DOC)
|
||||
assert a["기준"]["계"] == pytest.approx(plain["기준"]["계"] * 10)
|
||||
if name == "재료 고름":
|
||||
assert a["기준"]["재료비"] != pytest.approx(_answer(DOC)["기준"]["재료비"])
|
||||
|
||||
|
||||
def test_ref_base_same():
|
||||
"""참조 바탕(SK 위 SK) — 글도 바탕 SK 고친 줄 위에서."""
|
||||
parent = {**DOC, **CASES["덧줄"], "뺀줄": ["material:각재#0"]}
|
||||
ref = {"층": "personal", "주인": None, "키": "SK-000002"}
|
||||
_same(_answer({**DOC, "키": "SK-000003", "바탕": {"참조": ref}}, read=lambda r: parent))
|
||||
|
||||
|
||||
def test_row_modal_is_hopyo_line():
|
||||
"""일위대가 줄 모달(`줄모드`) = 일위대가 그 줄 셈(별도계상 기본 · 747 한쪽 흠)."""
|
||||
whole = _whole()
|
||||
line = labor._json(labor.drop_apart(labor.run_line(whole, FORM, None), {}))
|
||||
got = _answer({**DOC, "단위": "식", "줄모드": True}, line=True)
|
||||
assert got["기준"]["계"] == pytest.approx(line["기준"]["계"])
|
||||
assert [x["이름"] for x in got["줄"]] == [x["이름"] for x in line["줄"]]
|
||||
_same(got)
|
||||
|
||||
|
||||
def test_no_views_flag():
|
||||
"""`보기` false(바탕 원값 셈) = 금액만 · 덧칸 없음 · 금액 같음."""
|
||||
whole = _whole()
|
||||
clean = sd.clean(DOC, None, whole)
|
||||
got = labor._json(calc.run(clean, whole, None, detail=True, seen=False))
|
||||
assert "글" not in got and "중간" not in got
|
||||
assert got["기준"]["계"] == pytest.approx(_answer(DOC)["기준"]["계"])
|
||||
Reference in New Issue
Block a user