feat(799-1): 수식 탭 단가산출 소계 · 끝수 · 합계 — 블록마다 노 · 재 · 경 소계 보이기만(금액 그대로)
- 서버 master_equation.subtotals — 호표 줄(하위 로직 블록 한 줄씩 · 자원 · 덧줄)의 버린 줄 비목 + 끝수(합계 − 줄 합) - 부품 ui_template_equation_sum — 줄 이음 아래 소계 표(M01 · M02 수식 탭 같이) - 시험 test_799_1_subtotals — 줄 합 + 끝수 = 합계 · 합계 = 엔진 끝수 입힌 비목 합(전후 같음) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import { createBanner } from "@ui/ui_template_banner";
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createButton, createIconButton } from "@ui/ui_template_button";
|
||||
import { createEqChain } from "@ui/ui_template_equation_line";
|
||||
import { createEqSum } from "@ui/ui_template_equation_sum";
|
||||
import { eqText, type EqLine, type EqTree, type EqVarRow } from "@ui/ui_template_equation";
|
||||
import {
|
||||
el,
|
||||
@@ -457,6 +458,7 @@ export function createLabEquation(opt: LabEquationOptions) {
|
||||
return;
|
||||
}
|
||||
const edit = canEdit();
|
||||
const sum = createEqSum(eq);
|
||||
body.replaceChildren(
|
||||
createEqChain(eq.줄, {
|
||||
path: "",
|
||||
@@ -472,6 +474,7 @@ export function createLabEquation(opt: LabEquationOptions) {
|
||||
onVar: edit ? varDialog : undefined,
|
||||
slot: edit ? editor : undefined,
|
||||
}),
|
||||
...(sum ? [sum] : []), // 799-1 소계 · 끝수 · 합계
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -389,6 +389,39 @@ def _line(ctx: _Ctx, kind: str, key: str, text: str, value, **more) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def subtotals(logic: dict, cut: dict, total: dict) -> dict:
|
||||
"""호표 줄 → 소계(799-1 · 보이기만) — 하위 로직 줄 = 블록마다 · 그 밖 자원 · 덧줄은 한 줄씩 + 끝수.
|
||||
|
||||
값 = `master_cut.engine` 이 버린 줄 비목(0.1 원) 그대로 · 합계 = 엔진 비목 합(1 원 버림) ·
|
||||
끝수 = 합계 − 줄 합(금액 셈은 안 바꿈 · 사용자 확정 뒤에 정함).
|
||||
"""
|
||||
items, adds = logic.get("호표") or [], logic.get("덧줄") or []
|
||||
skip = mcut.ms.skipped(logic) if mcut.ms.uses(logic) else set()
|
||||
rows: list[dict] = []
|
||||
pool: dict[str, dict] = {}
|
||||
for n, line in enumerate(cut.get("줄") or []):
|
||||
if n in skip:
|
||||
continue
|
||||
split = {k: line.get("비목", {}).get(k) or Decimal(0) for k in mf.COST_ITEMS}
|
||||
if n < len(items) and items[n].get("종류") == "로직":
|
||||
rows.append({"갈래": "블록", "이름": str(items[n].get("이름") or ""), **split})
|
||||
continue
|
||||
kind = "덧줄" if n >= len(items) else "자원"
|
||||
one = pool.setdefault(
|
||||
kind, {"갈래": kind, "이름": kind, **dict.fromkeys(mf.COST_ITEMS, Decimal(0))}
|
||||
)
|
||||
for k in mf.COST_ITEMS:
|
||||
one[k] += split[k]
|
||||
rows += [pool[k] for k in ("자원", "덧줄") if k in pool]
|
||||
for one in rows:
|
||||
one["계"] = sum((one[k] for k in mf.COST_ITEMS), Decimal(0))
|
||||
rest = {
|
||||
k: (total.get(k) or Decimal(0)) - sum((one[k] for one in rows), Decimal(0))
|
||||
for k in mf.BUNDLE
|
||||
}
|
||||
return {"줄": rows, "끝수": rest}
|
||||
|
||||
|
||||
def export(master, key: str, given: dict | None = None, depth: int = 0) -> dict:
|
||||
"""로직 키 → 수식 {열쇠, 이름, 결과단위, 줄, 풀이, 변수표, 합계 | 결과} — 셈은 엔진 한 벌."""
|
||||
row = master.logic(key)
|
||||
@@ -402,6 +435,7 @@ def export(master, key: str, given: dict | None = None, depth: int = 0) -> dict:
|
||||
more = {"표시": step["표시"]} if step.get("표시") is not None else {}
|
||||
out_lines.append(_line(ctx, "중간", name, step["식"], env.get(name), **more))
|
||||
total: dict = {}
|
||||
sub: dict = {}
|
||||
if "결과" in row:
|
||||
out_lines.append(_line(ctx, "결과", "결과", row["결과"]["식"], raw["결과"]))
|
||||
total = {"결과": raw["결과"]}
|
||||
@@ -415,6 +449,8 @@ def export(master, key: str, given: dict | None = None, depth: int = 0) -> dict:
|
||||
one = _line(ctx, "덧줄", extra["이름"], extra["식"], got, 차례=at)
|
||||
out_lines.append(one | {"비목": extra.get("비목")})
|
||||
total = {k: cut.get(k) for k in mf.BUNDLE}
|
||||
if depth == 0:
|
||||
sub = {"소계": subtotals(row, cut, total)}
|
||||
return _clean(
|
||||
{
|
||||
"열쇠": row.get("키"),
|
||||
@@ -424,6 +460,7 @@ def export(master, key: str, given: dict | None = None, depth: int = 0) -> dict:
|
||||
"풀이": list(ctx.used.values()),
|
||||
"변수표": table(row, master),
|
||||
"합계": total,
|
||||
**sub,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""799-1 단가산출 소계 · 끝수 · 합계 지킴(보이기만 — 금액 셈은 그대로).
|
||||
|
||||
① 예시 셋 — 굴착기(블록 한 줄) · 거푸집(자원 + 덧줄) · 지게(자원) : 줄 합 + 끝수 = 합계(비목마다).
|
||||
② 표본 — 호표 로직 여럿 : 합계 = 엔진 끝수 입힌 비목 합(전후 같음) · 줄 합 + 끝수 = 합계 · 결과 로직은 소계 없음.
|
||||
③ 부품 짝 — `ui_template_equation_sum.ts` 가 서버 칸 이름을 그대로 읽음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from decimal import Decimal as D
|
||||
from pathlib import Path
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
mf = importlib.import_module("master_formula")
|
||||
mcut = importlib.import_module("master_cut")
|
||||
meq = importlib.import_module("master_equation")
|
||||
|
||||
COSTS = ("노무비", "재료비", "경비", "계")
|
||||
|
||||
|
||||
def _whole():
|
||||
return store.loaded()[1]
|
||||
|
||||
|
||||
def _logics() -> list[dict]:
|
||||
"""M01 로직 줄 전부(키 순)."""
|
||||
w = _whole()
|
||||
return [
|
||||
r
|
||||
for tid, rows in sorted(w.index.items())
|
||||
if mf.mk.GROUP_OF.get(tid) == "로직"
|
||||
for r in rows.values()
|
||||
]
|
||||
|
||||
|
||||
def _export(key: str) -> dict:
|
||||
w = _whole()
|
||||
return meq.export(w, key, meq.given_of(w, w.logic(key), None))
|
||||
|
||||
|
||||
def _closes(eq: dict) -> None:
|
||||
sub = eq["소계"]
|
||||
for k in COSTS:
|
||||
got = sum((r[k] for r in sub["줄"]), D(0)) + sub["끝수"][k]
|
||||
assert got == eq["합계"][k], (eq["열쇠"], k, got, eq["합계"][k])
|
||||
|
||||
|
||||
def test_굴착기_블록_한_줄_끝수():
|
||||
eq = _export("GC001148")
|
||||
rows = eq["소계"]["줄"]
|
||||
assert [r["갈래"] for r in rows] == ["블록"] and rows[0]["이름"] == "굴착기(무한궤도)"
|
||||
assert (rows[0]["노무비"], rows[0]["재료비"], rows[0]["경비"]) == (
|
||||
D("437.1"),
|
||||
D("88.2"),
|
||||
D("116.0"),
|
||||
)
|
||||
assert eq["합계"]["계"] == D("641.0") and eq["소계"]["끝수"]["계"] == D("-0.3")
|
||||
_closes(eq)
|
||||
|
||||
|
||||
def test_거푸집_자원_덧줄_지게_자원():
|
||||
eq = _export("GC000211")
|
||||
assert [r["갈래"] for r in eq["소계"]["줄"]] == ["자원", "덧줄"]
|
||||
_closes(eq)
|
||||
eq = _export("GC001140")
|
||||
assert [r["갈래"] for r in eq["소계"]["줄"]] == ["자원"]
|
||||
_closes(eq)
|
||||
|
||||
|
||||
def test_표본_합계는_엔진_끝수_그대로():
|
||||
w = _whole()
|
||||
keys = [x["키"] for x in _logics() if x.get("호표")][::25]
|
||||
assert keys, "표본 없음"
|
||||
seen = 0
|
||||
for key in keys:
|
||||
try:
|
||||
eq = _export(key)
|
||||
except mf.FormulaError:
|
||||
continue
|
||||
if "소계" not in eq:
|
||||
continue
|
||||
seen += 1
|
||||
_closes(eq)
|
||||
raw = mf.run(w, key, meq.given_of(w, w.logic(key), None))
|
||||
cut = mcut.engine(w, key, raw)
|
||||
assert {k: eq["합계"][k] for k in COSTS} == {k: cut[k] for k in COSTS}, key
|
||||
assert seen >= 10
|
||||
|
||||
|
||||
def test_결과_로직은_소계_없음():
|
||||
key = next(x["키"] for x in _logics() if "결과" in x)
|
||||
assert "소계" not in _export(key)
|
||||
|
||||
|
||||
def test_화면_부품이_서버_칸을_읽음():
|
||||
ts = Path("ui_template/ui_template_equation_sum.ts").read_text(encoding="utf-8")
|
||||
for name in ("노무비", "재료비", "경비", "끝수", "합계", "자원", "덧줄"):
|
||||
assert name in ts
|
||||
line = Path("M01_MasterData/M01_MasterData_UI_LogicLab_Equation.ts").read_text(encoding="utf-8")
|
||||
assert "createEqSum(eq)" in line
|
||||
@@ -322,3 +322,36 @@
|
||||
:is(.ui-eq__block-body, .ui-eq__bad)[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ── 단가산출 소계 · 합계(799-1) ── */
|
||||
.ui-eq__sum {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid var(--color-border);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.ui-eq__sum-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(var(--spacing-96), 2fr) repeat(4, minmax(0, 1fr));
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.ui-eq__sum-head {
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.ui-eq__sum-cell {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ui-eq__sum-row[data-eq-sum-kind="끝수"] {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.ui-eq__sum-row[data-eq-sum-kind="합계"] {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,17 @@ export interface EqNode {
|
||||
사슬?: boolean;
|
||||
}
|
||||
|
||||
/** 로직 하나의 수식(줄 · 변수표 · 합계) */
|
||||
/** 단가산출 소계 줄(799-1) — 갈래 블록 | 자원 | 덧줄 */
|
||||
export interface EqSumRow {
|
||||
갈래: string;
|
||||
이름: string;
|
||||
노무비: unknown;
|
||||
재료비: unknown;
|
||||
경비: unknown;
|
||||
계: unknown;
|
||||
}
|
||||
|
||||
/** 로직 하나의 수식(줄 · 변수표 · 합계 · 소계) */
|
||||
export interface EqTree {
|
||||
열쇠: string;
|
||||
이름: string;
|
||||
@@ -52,6 +62,8 @@ export interface EqTree {
|
||||
줄: EqLine[];
|
||||
변수표: EqVarRow[];
|
||||
합계: Record<string, unknown>;
|
||||
/** 799-1 — 하위 로직 블록마다 노 · 재 · 경 소계 + 끝수(합계 − 줄 합) · 결과 로직은 없음 */
|
||||
소계?: { 줄: EqSumRow[]; 끝수: Record<string, unknown> };
|
||||
}
|
||||
|
||||
export interface EqVarRow {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/* =============================================================================
|
||||
* ui_template_equation_sum.ts
|
||||
* 부품 「수식」 셋째 파일(799-1 · 요구 655-1 · 요구서_655 2 장 4) — 단가산출 소계 · 합계(보이기만 · 금액 셈 그대로).
|
||||
* 표 = 줄(하위 로직 블록마다 한 줄 · 자기 자원 · 덧줄) × 노무비 · 재료비 · 경비 · 계 → 합계(엔진 비목 합) → 끝수(합계 − 줄 합)
|
||||
* 값 = 서버 `소계`(`master_equation.subtotals` · 버린 줄 비목 0.1 원) · 끝수가 0 이면 줄을 안 그림 · 소계가 없으면(결과 로직) 아무것도 안 그림
|
||||
* 쓰는 법 = `createEqSum(tree)` → 요소 | null — 줄 이음(`createEqChain`) 아래에 붙임
|
||||
* ========================================================================== */
|
||||
|
||||
import { eqText, type EqTree } from "./ui_template_equation";
|
||||
|
||||
const COLS = ["노무비", "재료비", "경비", "계"] as const;
|
||||
const KIND: Record<string, string> = { 자원: "자기 자원", 덧줄: "덧줄" };
|
||||
|
||||
const node = (tag: "div" | "span", className: string, text = ""): HTMLElement => {
|
||||
const out = document.createElement(tag);
|
||||
out.className = className;
|
||||
if (text) out.textContent = text;
|
||||
return out;
|
||||
};
|
||||
|
||||
function row(kind: string, name: string, get: (k: (typeof COLS)[number]) => unknown): HTMLElement {
|
||||
const line = node("div", "ui-eq__sum-row");
|
||||
line.dataset.eqSumKind = kind;
|
||||
line.append(node("span", "ui-eq__sum-name", name));
|
||||
for (const k of COLS) {
|
||||
const cell = node("span", "ui-eq__sum-cell", eqText(get(k)) || "0");
|
||||
cell.dataset.eqSumCol = k;
|
||||
line.append(cell);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
export function createEqSum(tree: Pick<EqTree, "합계" | "소계">): HTMLElement | null {
|
||||
const sub = tree.소계;
|
||||
if (!sub?.줄.length) return null;
|
||||
const box = node("div", "ui-eq__sum");
|
||||
box.dataset.eqSum = "";
|
||||
const head = node("div", "ui-eq__sum-row ui-eq__sum-head");
|
||||
head.append(node("span", "ui-eq__sum-name", "소계"));
|
||||
for (const k of COLS) head.append(node("span", "ui-eq__sum-cell", k));
|
||||
box.append(head);
|
||||
for (const r of sub.줄) box.append(row(r.갈래, KIND[r.갈래] ?? r.이름, (k) => r[k]));
|
||||
const rest = sub.끝수;
|
||||
if (COLS.some((k) => Number(rest[k] ?? 0) !== 0)) box.append(row("끝수", "끝수", (k) => rest[k]));
|
||||
box.append(row("합계", "합계", (k) => tree.합계[k]));
|
||||
return box;
|
||||
}
|
||||
Reference in New Issue
Block a user