Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1
This commit is contained in:
@@ -14,7 +14,8 @@
|
||||
끝수 = 엑셀 자리 그대로 — 줄 금액 = ROUNDDOWN(단가 × 수량, 1) ·
|
||||
% 줄 = ROUNDDOWN(밑수 × % / 100, 1) · 관급 · 별도계상 자재 = 대가 안 0원 ·
|
||||
블록 비목 합 = ROUNDDOWN(Σ, 0) · 합계 = 세 비목 합.
|
||||
빈칸 = 단가 · 수량을 낸 원천이 없으면 None · 그 칸을 쓰는 합도 None · 「연결 없음」 목록(gaps).
|
||||
빈칸 = 단가 · 수량을 낸 원천이 없으면 None · 「연결 없음」 목록(gaps).
|
||||
빈 줄(166-6) = 블록 합 = 아는 줄 합 + `blank` n · `blank_names` — 내역 단가 · 부른 대가로는 안 넘김.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -49,7 +50,7 @@ def _prices(line: dict[str, Any], books: dict[str, Any], sums: dict) -> dict[str
|
||||
key
|
||||
) or {}
|
||||
for part in PARTS:
|
||||
out[part] = num(row.get(part))
|
||||
out[part] = None if row.get("blank") else num(row.get(part)) # 빈 줄 대가 = 빈칸
|
||||
else: # blank — 못 펼친 줄
|
||||
out = dict.fromkeys(PARTS, None)
|
||||
return out
|
||||
@@ -109,17 +110,18 @@ def _block(
|
||||
for line in lines:
|
||||
line["price"] = total(list(line["prices"].values()))
|
||||
line["amount"] = total(list(line["amounts"].values()))
|
||||
parts: dict[str, float | None] = {}
|
||||
for part in PARTS:
|
||||
s = total([ln["amounts"][part] for ln in lines])
|
||||
parts[part] = None if s is None else rounddown(s, 0)
|
||||
known = [ln for ln in lines if ln["amount"] is not None]
|
||||
parts = {part: rounddown(sum(ln["amounts"][part] for ln in known), 0) for part in PARTS}
|
||||
blank_names = [ln.get("name") or "" for ln in lines if ln["amount"] is None]
|
||||
return {
|
||||
**{k: block.get(k) for k in ("id", "kind", "name", "spec", "unit", "cite")},
|
||||
**({"spec_raw": block["spec_raw"]} if block.get("spec_raw") else {}), # 157-4 풍선
|
||||
"key": block["id"],
|
||||
"lines": lines,
|
||||
**parts,
|
||||
"sum": total([parts[p] for p in PARTS]),
|
||||
"sum": sum(parts.values()),
|
||||
"blank": len(blank_names),
|
||||
"blank_names": blank_names,
|
||||
}
|
||||
|
||||
|
||||
@@ -178,6 +180,8 @@ def compute_labor(
|
||||
"labor": b["labor"],
|
||||
"material": b["material"],
|
||||
"expense": b["expense"],
|
||||
"blank": b["blank"],
|
||||
"blank_names": b["blank_names"],
|
||||
}
|
||||
for b in out_blocks
|
||||
]
|
||||
@@ -192,7 +196,7 @@ def compute_labor(
|
||||
def daega_price(sheets: dict[str, Any], owner: str) -> dict[str, Any] | None:
|
||||
"""내역서(145-3)가 읽는 대가 단가 — 구조물 id(연결된 사본 주인) → 호표 · 비목 단가.
|
||||
|
||||
사본이 없으면 None."""
|
||||
사본이 없으면 None · 빈 줄 있는 블록 = 비목 단가 빈칸(166-6 · 아는 줄 합은 안 넘김)."""
|
||||
bid = (sheets.get("daega_owners") or {}).get(owner)
|
||||
row = next((r for r in sheets.get("daega") or [] if r["key"] == bid), None)
|
||||
if row is None:
|
||||
@@ -200,5 +204,7 @@ def daega_price(sheets: dict[str, Any], owner: str) -> dict[str, Any] | None:
|
||||
return {
|
||||
"no": f"대가{row['no']}",
|
||||
"key": bid,
|
||||
**{k: row[k] for k in ("labor", "material", "expense", "sum")},
|
||||
**{
|
||||
k: None if row.get("blank") else row[k] for k in ("labor", "material", "expense", "sum")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
* 비고 = 대가 줄 「대가n」 · 산근 줄 「산근 n호표」(산근 목록 번호)
|
||||
* 줄 금액 ROUNDDOWN(단가 × 수량, 1) · 블록 비목 합 ROUNDDOWN(Σ, 0) · 합계 = 세 비목 합
|
||||
* 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락에서 셈)
|
||||
* 빈칸 = 원천이 없으면 null · 그 칸을 쓰는 합도 null · 「연결 없음」 목록(gaps)
|
||||
* 빈칸 = 원천이 없으면 null · 「연결 없음」 목록(gaps)
|
||||
* 빈 줄(166-6) = 블록 합 = 아는 줄 합 + blank n · blank_names — 내역 단가 · 부른 대가로는 안 넘김
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
@@ -51,7 +52,7 @@ function linePrices(line: Row, books: Row, sums: Record<string, Row>): Parts {
|
||||
} else if (kind === "machine" || kind === "daega" || kind === "sangeun") {
|
||||
const src: Row = ({ machine: books.machine, daega: sums, sangeun: books.sangeun } as Row)[kind];
|
||||
const row: Row = src[key] || {};
|
||||
for (const part of PARTS) out[part] = num(row[part]);
|
||||
for (const part of PARTS) out[part] = row.blank ? null : num(row[part]); // 빈 줄 대가 = 빈칸
|
||||
} else return blanks(); // blank — 못 펼친 줄
|
||||
return out;
|
||||
}
|
||||
@@ -104,11 +105,14 @@ function block(b: Row, books: Row, sums: Record<string, Row>, gaps: Gap[]): Row
|
||||
line.price = total(PARTS.map((p) => line.prices[p]));
|
||||
line.amount = total(PARTS.map((p) => line.amounts[p]));
|
||||
}
|
||||
const parts = blanks();
|
||||
for (const part of PARTS) {
|
||||
const s = total(lines.map((ln) => ln.amounts[part]));
|
||||
parts[part] = s === null ? null : rounddown(s, 0);
|
||||
}
|
||||
const known = lines.filter((ln) => ln.amount !== null);
|
||||
const parts = zeros();
|
||||
for (const part of PARTS)
|
||||
parts[part] = rounddown(
|
||||
known.reduce((acc, ln) => acc + ln.amounts[part], 0),
|
||||
0,
|
||||
);
|
||||
const blankNames = lines.filter((ln) => ln.amount === null).map((ln) => ln.name || "");
|
||||
return {
|
||||
id: b.id,
|
||||
key: b.id,
|
||||
@@ -120,7 +124,9 @@ function block(b: Row, books: Row, sums: Record<string, Row>, gaps: Gap[]): Row
|
||||
cite: b.cite,
|
||||
lines,
|
||||
...parts,
|
||||
sum: total(PARTS.map((p) => parts[p])),
|
||||
sum: PARTS.reduce((acc, p) => acc + (parts[p] as number), 0),
|
||||
blank: blankNames.length,
|
||||
blank_names: blankNames,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -167,6 +173,8 @@ export function computeLabor(daega: Row | null | undefined, sheets: Row, sangeun
|
||||
labor: b.labor,
|
||||
material: b.material,
|
||||
expense: b.expense,
|
||||
blank: b.blank,
|
||||
blank_names: b.blank_names,
|
||||
}));
|
||||
return {
|
||||
daega_blocks: outBlocks,
|
||||
@@ -176,7 +184,8 @@ export function computeLabor(daega: Row | null | undefined, sheets: Row, sangeun
|
||||
};
|
||||
}
|
||||
|
||||
/** 내역서(145-3)가 읽는 대가 단가 — 구조물 id → 호표 · 비목 단가 · 없으면 null */
|
||||
/** 내역서(145-3)가 읽는 대가 단가 — 구조물 id → 호표 · 비목 단가 · 없으면 null ·
|
||||
* 빈 줄 있는 블록 = 비목 단가 빈칸(166-6 · 아는 줄 합은 안 넘김) */
|
||||
export function daegaPrice(sheets: Row, owner: string): Row | null {
|
||||
const bid = (sheets.daega_owners || {})[owner];
|
||||
const row = (sheets.daega || []).find((r: Row) => r.key === bid);
|
||||
@@ -184,9 +193,9 @@ export function daegaPrice(sheets: Row, owner: string): Row | null {
|
||||
return {
|
||||
no: `대가${row.no}`,
|
||||
key: bid,
|
||||
labor: row.labor,
|
||||
material: row.material,
|
||||
expense: row.expense,
|
||||
sum: row.sum,
|
||||
labor: row.blank ? null : row.labor,
|
||||
material: row.blank ? null : row.material,
|
||||
expense: row.blank ? null : row.expense,
|
||||
sum: row.blank ? null : row.sum,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* 일위대가표 = 블록(머리 · [품셈 인용] · 줄 · 합계) · 줄 누름 = 단가를 낸 탭 · 줄(대가 줄 = 그 블록)
|
||||
* 줄 꼴(155-2) = 머리 · 합계 굵게(`is-title`) · 인용 · 줄 = 들여쓰기 「ㄴ」(2단 · 겹 대가는 안 펼침)
|
||||
* 연결된 구조물 조합 사본이 없으면 빈 표 「연결 없음」
|
||||
* 빈 줄(166-6) = 합계 = 아는 줄 합 · 옆 「빈 줄 n」 · 풍선 = 빈 줄 이름 · 내역 단가엔 안 넘김
|
||||
* ========================================================================== */
|
||||
|
||||
import type { DataRow } from "@ui/ui_template_data_table";
|
||||
@@ -26,6 +27,11 @@ const EMPTY =
|
||||
/** 하위 줄 앞 — 들여쓰기 + 「ㄴ」(공용 표가 앞 빈칸을 접지 않음) */
|
||||
const CHILD = " ㄴ ";
|
||||
const ROUNDS = "줄 금액 ROUNDDOWN(단가 × 수량, 1) · % 줄 = 밑수 × % / 100 · 합계 ROUNDDOWN 0";
|
||||
const BLANK = "빈 줄 n = 금액 모르는 줄 · 합계 = 아는 줄 합 · 내역 단가엔 안 넘김";
|
||||
/** 「빈 줄 n」 작은 글 · 풍선 = 빈 줄 이름 */
|
||||
const blankNote = (b: Row): string => (b.blank ? `빈 줄 ${b.blank}` : "");
|
||||
const blankTip = (b: Row | undefined): string | null =>
|
||||
b?.blank ? `빈 줄 — ${(b.blank_names as string[]).join(" · ")}` : null;
|
||||
|
||||
/** 블록 줄 → 단가를 낸 탭 */
|
||||
const LINE_TAB: Record<string, TabId> = {
|
||||
@@ -37,7 +43,11 @@ const LINE_TAB: Record<string, TabId> = {
|
||||
};
|
||||
|
||||
export const buildDaegaList: TabBuild = ({ sheets }): TabTable | string => {
|
||||
const rows: DataRow[] = (sheets.daega || []).map((r: Row) => ({ ...r, no: `대가${r.no}` }));
|
||||
const rows: DataRow[] = (sheets.daega || []).map((r: Row) => ({
|
||||
...r,
|
||||
no: `대가${r.no}`,
|
||||
blank_note: blankNote(r),
|
||||
}));
|
||||
if (!rows.length) return EMPTY;
|
||||
return {
|
||||
columns: [
|
||||
@@ -47,15 +57,21 @@ export const buildDaegaList: TabBuild = ({ sheets }): TabTable | string => {
|
||||
text("spec", "규격", 200),
|
||||
text("unit", "단위", 56),
|
||||
money("sum", "합계"),
|
||||
text("blank_note", "", 64),
|
||||
money("labor", "노무비"),
|
||||
money("material", "재료비"),
|
||||
money("expense", "경비"),
|
||||
],
|
||||
rows,
|
||||
frozen: 3,
|
||||
hint: "호표 = 일위대가표 블록 합계 · 줄을 누르면 그 블록으로",
|
||||
hint: `호표 = 일위대가표 블록 합계 · ${BLANK} · 줄을 누르면 그 블록으로`,
|
||||
link: (i: number): Link | null => (rows[i] ? { tab: "dg", key: String(rows[i].key) } : null),
|
||||
tip: (i, key) => (key === "spec" ? raw(rows[i]?.spec, rows[i]?.spec_raw) : null),
|
||||
tip: (i, key) =>
|
||||
key === "spec"
|
||||
? raw(rows[i]?.spec, rows[i]?.spec_raw)
|
||||
: key === "blank_note" || key === "sum"
|
||||
? blankTip(rows[i])
|
||||
: null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -109,6 +125,9 @@ export const buildDaegaBlocks: TabBuild = ({ sheets }): TabTable | string => {
|
||||
labor: b.labor,
|
||||
material: b.material,
|
||||
expense: b.expense,
|
||||
note: blankNote(b),
|
||||
blank: b.blank,
|
||||
blank_names: b.blank_names,
|
||||
});
|
||||
links.push(null);
|
||||
}
|
||||
@@ -130,10 +149,14 @@ export const buildDaegaBlocks: TabBuild = ({ sheets }): TabTable | string => {
|
||||
],
|
||||
rows,
|
||||
frozen: 1,
|
||||
hint: `단가 = B09 프로젝트 한 벌 굳힌 단가 · ${ROUNDS} · 줄 누름 = 단가를 낸 탭(대가 줄 = 그 블록)`,
|
||||
hint: `단가 = B09 프로젝트 한 벌 굳힌 단가 · ${ROUNDS} · ${BLANK} · 줄 누름 = 단가를 낸 탭(대가 줄 = 그 블록)`,
|
||||
link: (i: number) => links[i] ?? null,
|
||||
rowClass: (i: number) => (titles.has(i) ? "is-title" : null),
|
||||
tip: (i: number, key: string) =>
|
||||
key === "spec" ? raw(rows[i]?.spec, rows[i]?.spec_raw) : null,
|
||||
key === "spec"
|
||||
? raw(rows[i]?.spec, rows[i]?.spec_raw)
|
||||
: key === "note" || key === "sum"
|
||||
? blankTip(rows[i])
|
||||
: null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*
|
||||
* 값 = 서버 단독 역전개(`B09_Estimation_Summary` · GET 응답 `summary`) — 조작 중엔 안 바뀜 · [저장] 뒤 정본
|
||||
* 끝수 = 총수량 ROUND 2 · 금액 ROUND(수량 × 단가, 0) · 줄 누름 = 단가를 낸 목록표 줄
|
||||
* 경비 집계표 빈 까닭(166-5) = 산근 · 대가 어느 길도 경비 자원 줄을 안 냄(기계 → 중기 · 구역화물 → 자재)
|
||||
* ========================================================================== */
|
||||
|
||||
import type { DataRow } from "@ui/ui_template_data_table";
|
||||
@@ -26,7 +27,10 @@ const hint = (payload: Row, extra = ""): string => {
|
||||
return `${HINT}${extra}${n ? ` · 연결 없음 ${n} — 전개 안 함(연결 점검 탭)` : ""}`;
|
||||
};
|
||||
|
||||
function resourceTab(id: string, list: TabId, unitPrice: string): TabBuild {
|
||||
/** 줄이 없을 때 표 위 맨 앞 안내 */
|
||||
const EXPENSE_NONE = "경비 갈래 자원 없음 — 중기 · 자재로 셈 · 경비 로직이 들어오면 채워짐";
|
||||
|
||||
function resourceTab(id: string, list: TabId, unitPrice: string, none = ""): TabBuild {
|
||||
return ({ payload }): TabTable => {
|
||||
const rows: DataRow[] = ((payload.summary || {})[id] || []).map((r: Row) => ({ ...r }));
|
||||
const sum = rows.reduce((acc, r) => acc + (typeof r.amount === "number" ? r.amount : 0), 0);
|
||||
@@ -44,7 +48,7 @@ function resourceTab(id: string, list: TabId, unitPrice: string): TabBuild {
|
||||
rows,
|
||||
total: rows.length ? { name: "계", amount: sum } : null,
|
||||
frozen: 2,
|
||||
hint: hint(payload),
|
||||
hint: !rows.length && none ? `${none} · ${hint(payload)}` : hint(payload),
|
||||
link: (i) => (rows[i] ? { tab: list, key: String(rows[i].key) } : null),
|
||||
};
|
||||
};
|
||||
@@ -52,7 +56,7 @@ function resourceTab(id: string, list: TabId, unitPrice: string): TabBuild {
|
||||
|
||||
export const buildSumMaterial = resourceTab("sum_material", "material", "단가");
|
||||
export const buildSumLabor = resourceTab("sum_labor", "labor", "단가");
|
||||
export const buildSumExpense = resourceTab("sum_expense", "expense", "단가");
|
||||
export const buildSumExpense = resourceTab("sum_expense", "expense", "단가", EXPENSE_NONE);
|
||||
|
||||
export const buildSumMachine: TabBuild = ({ payload }): TabTable => {
|
||||
const rows: DataRow[] = ((payload.summary || {}).sum_machine || []).map((r: Row) => ({ ...r }));
|
||||
|
||||
@@ -124,6 +124,7 @@ def test_엑셀_대가_블록_검산():
|
||||
assert out["daega"][0] == {
|
||||
"no": 1, "key": "DG14", "kind": "조합", "name": "돌붙임(채집)", "spec": "", "unit": "",
|
||||
"sum": 101522, "labor": 77755, "material": 9519, "expense": 14248,
|
||||
"blank": 0, "blank_names": [],
|
||||
} # fmt: skip
|
||||
assert daega_price(out, "pipe@100.0") == {
|
||||
"no": "대가1",
|
||||
@@ -147,11 +148,19 @@ def _blank_cases():
|
||||
|
||||
|
||||
def test_빈칸_규칙():
|
||||
"""166-6 — 블록 합 = 아는 줄 합 + 빈 줄 n · 내역 단가는 빈칸."""
|
||||
out = compute_labor(_blank_cases(), SHEETS, {})
|
||||
assert _block(out, "DG2")["sum"] is None and _block(out, "DG2")["labor"] is None
|
||||
assert _block(out, "DG14")["sum"] is None # 산근 단가 없음(145-1 전)
|
||||
assert _block(out, "DGZ")["sum"] is None
|
||||
assert _block(out, "DGX")["sum"] is None or _block(out, "DGY")["sum"] is None # 순환 = 빈칸
|
||||
dg2 = _block(out, "DG2")
|
||||
assert (dg2["sum"], dg2["labor"], dg2["blank"]) == (6013, 5275, 2)
|
||||
assert dg2["blank_names"] == ["L00032", "공구손료"] # 노임 없음 · 그 노무 밑수 % 줄
|
||||
dg14 = _block(out, "DG14") # 산근 단가 없음(145-1 전)
|
||||
assert (dg14["sum"], dg14["blank"]) == (53926, 3)
|
||||
assert _sums(dg14) == (53926, 43539, 3842, 6545)
|
||||
assert (_block(out, "DGZ")["sum"], _block(out, "DGZ")["blank"]) == (0, 1)
|
||||
assert _block(out, "DGX")["blank"] + _block(out, "DGY")["blank"] >= 1 # 순환 = 빈 줄
|
||||
assert daega_price(out, "pipe@100.0") == {
|
||||
"no": "대가1", "key": "DG14", "labor": None, "material": None, "expense": None, "sum": None,
|
||||
} # fmt: skip
|
||||
whys = {g["why"] for g in out["gaps"]}
|
||||
assert {"노임 단가 없음", "산근 단가 없음(145-1)", "수량 없음", "% 밑수 없음"} <= whys
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -54,7 +55,7 @@ LINES = {
|
||||
{"kind": "machine", "key": "X1", "qty": 4.295},
|
||||
{"kind": "hopyo", "key": "B", "qty": 0.5},
|
||||
{"kind": "material", "key": "M4", "qty": 0.354},
|
||||
{"kind": "expense", "key": "E16", "qty": 1.103},
|
||||
{"kind": "expense", "key": "E16", "qty": 1.103}, # 합성 줄 — 산근 · 대가는 경비 줄을 안 냄(166-5)
|
||||
]},
|
||||
"B": {"lines": [{"kind": "labor", "key": "L1", "qty": 1.658},
|
||||
{"kind": "labor", "key": "L9", "qty": 1}]},
|
||||
@@ -155,3 +156,26 @@ def test_산근_블록_역전개_엑셀_반올림():
|
||||
machine = s["sum_machine"][0] # 중기시간금액집계표 10 줄 노무비
|
||||
assert (machine["qty"], machine["labor"], machine["sum"]) == (1436.14, 79992998, None)
|
||||
assert s["unlinked"] == 3 # 토공 · 대가 전 구조물 · 수량 빈칸 = 전개 안 함
|
||||
|
||||
|
||||
# ── 166-5 경비 집계표 — 실제 블록 꼴은 경비 자원 0 → 표 위 안내 ──
|
||||
from test_155_b09_tabs import _node # noqa: E402
|
||||
|
||||
EXPENSE_NONE = "경비 갈래 자원 없음 — 중기 · 자재로 셈 · 경비 로직이 들어오면 채워짐"
|
||||
|
||||
|
||||
def _expense_tab(summary):
|
||||
return _node(
|
||||
f"\nconst t = buildSumExpense({{ payload: {{ summary: {json.dumps(summary)} }} }});"
|
||||
"\nconsole.log(JSON.stringify({ n: t.rows.length, hint: t.hint }));",
|
||||
"UI_Tabs_Summary",
|
||||
)
|
||||
|
||||
|
||||
def test_166_5_실자료_경비_0_이면_안내():
|
||||
s = compute_summary(SG_SHEETS, hopyo_lines(SG_SHEETS))
|
||||
assert s["sum_expense"] == [] # 산근 블록 꼴 = 경비 갈래 줄 없음
|
||||
out = _expense_tab(s)
|
||||
assert out["n"] == 0 and out["hint"].startswith(EXPENSE_NONE)
|
||||
filled = _expense_tab(compute_summary(SHEETS, LINES)) # 합성 경비 줄이 있으면 안내 없음
|
||||
assert filled["n"] == 1 and EXPENSE_NONE not in filled["hint"]
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""166-6 일위대가목록표 블록 합 — 빈 줄 있는 블록 = 아는 줄 합 + 「빈 줄 n」 · 내역 단가엔 안 넘김.
|
||||
|
||||
견본 = 대가 A(노무 아는 줄 · 자재 단가 없음 줄) · 대가 B(A 를 부름 → 그 줄도 빈 줄) · 대가 C(빈 줄 없음).
|
||||
파이썬 `B09_Estimation_Calc_Labor` ↔ TS 짝 거울 + 목록표 화면 「빈 줄 n」 · 풍선(Node 타입 지움).
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor, daega_price # noqa: E402
|
||||
from test_145_2_b09_labor_calc import _same, _ts # noqa: E402
|
||||
from test_155_b09_tabs import _node # noqa: E402
|
||||
|
||||
SHEETS = {
|
||||
"labor": [{"key": "L1", "price": 165545}],
|
||||
"material": [{"key": "M1", "price": None}],
|
||||
"machine": [],
|
||||
"gaps": [],
|
||||
}
|
||||
|
||||
|
||||
def _b(bid, name, lines):
|
||||
return {
|
||||
"id": bid,
|
||||
"kind": "조합",
|
||||
"name": name,
|
||||
"spec": "",
|
||||
"unit": "m",
|
||||
"cite": "",
|
||||
"lines": lines,
|
||||
}
|
||||
|
||||
|
||||
DAEGA = {
|
||||
"blocks": [
|
||||
_b("A", "기슭막이", [
|
||||
{"kind": "labor", "key": "L1", "qty": 0.5, "name": "보통인부"},
|
||||
{"kind": "material", "key": "M1", "qty": 2, "name": "토목용부직포"},
|
||||
]),
|
||||
_b("B", "겹 대가", [
|
||||
{"kind": "labor", "key": "L1", "qty": 0.1, "name": "보통인부"},
|
||||
{"kind": "daega", "key": "A", "qty": 1, "name": "기슭막이"},
|
||||
]),
|
||||
_b("C", "아는 줄만", [{"kind": "labor", "key": "L1", "qty": 1, "name": "보통인부"}]),
|
||||
],
|
||||
"owners": {"pipe@100.0": "A", "pipe@200.0": "C"},
|
||||
} # fmt: skip
|
||||
|
||||
|
||||
def test_아는_줄_합_빈_줄_n():
|
||||
out = compute_labor(DAEGA, SHEETS)
|
||||
rows = {r["key"]: r for r in out["daega"]}
|
||||
# 165,545 × 0.5 = 82,772.5 → 블록 ROUNDDOWN 0 · 자재 줄은 빈 줄
|
||||
assert (rows["A"]["sum"], rows["A"]["labor"], rows["A"]["material"]) == (82772, 82772, 0)
|
||||
assert (rows["A"]["blank"], rows["A"]["blank_names"]) == (1, ["토목용부직포"])
|
||||
# 부른 대가에 빈 줄 → 그 줄도 빈 줄(아는 줄 합을 위로 안 넘김)
|
||||
assert (rows["B"]["sum"], rows["B"]["blank"], rows["B"]["blank_names"]) == (
|
||||
16554,
|
||||
1,
|
||||
["기슭막이"],
|
||||
)
|
||||
assert (rows["C"]["sum"], rows["C"]["blank"]) == (165545, 0)
|
||||
# 목록표 = 블록 합계 그대로(같은 셈 한 벌)
|
||||
for b in out["daega_blocks"]:
|
||||
assert _same(
|
||||
{k: rows[b["key"]][k] for k in ("sum", "blank")}, {"sum": b["sum"], "blank": b["blank"]}
|
||||
)
|
||||
# 내역 단가 = 빈 줄 블록은 빈칸 · 없는 블록은 그대로
|
||||
assert daega_price(out, "pipe@100.0")["sum"] is None
|
||||
assert daega_price(out, "pipe@100.0")["labor"] is None
|
||||
assert daega_price(out, "pipe@200.0")["sum"] == 165545
|
||||
|
||||
|
||||
def test_거울_파이썬_TS():
|
||||
py = json.loads(json.dumps(compute_labor(DAEGA, SHEETS), ensure_ascii=False))
|
||||
ts = _ts(DAEGA, SHEETS, {})
|
||||
assert _same(py, ts["out"])
|
||||
assert _same(daega_price(py, "pipe@100.0"), ts["price"])
|
||||
|
||||
|
||||
def test_목록표_빈_줄_글_풍선():
|
||||
sheets = json.dumps(compute_labor(DAEGA, SHEETS), ensure_ascii=False)
|
||||
out = _node(
|
||||
f"\nconst t = buildDaegaList({{ sheets: {sheets} }});"
|
||||
"\nconsole.log(JSON.stringify({ notes: t.rows.map((r) => r.blank_note),"
|
||||
" sums: t.rows.map((r) => r.sum), cols: t.columns.map((c) => c.key),"
|
||||
" tips: t.rows.map((_, i) => t.tip(i, 'blank_note') ?? null) }));"
|
||||
)
|
||||
assert out["cols"][5:7] == ["sum", "blank_note"] # 합계 바로 옆
|
||||
assert out["notes"] == ["빈 줄 1", "빈 줄 1", ""]
|
||||
assert out["sums"] == [82772, 16554, 165545]
|
||||
assert out["tips"] == ["빈 줄 — 토목용부직포", "빈 줄 — 기슭막이", None]
|
||||
Reference in New Issue
Block a user