feat(751-3): M01 · M02 셈 끝수 = 품셈 1-2-2 버림 — 줄 0.1 원 · 비목 소계 1 원 · 계 = 셋 합 · B09 와 같은 값
- 끝수 한 곳 `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>
This commit is contained in:
@@ -133,7 +133,7 @@ class Builder:
|
||||
logic = whole.logic(code)
|
||||
except mf.FormulaError:
|
||||
return None
|
||||
got = labor_doc.run_line(whole, code, given, rate, picks)
|
||||
got = labor_doc.run_line(whole, code, given, rate, picks, raw=True) # B09 끝수는 제 단가로
|
||||
if got.get("기준") is None:
|
||||
return None # 돈 아닌 로직 · 못 돈 로직 — 조합 줄이 까닭을 냄
|
||||
items = logic.get("호표") or []
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
* 어느 박스도 안 쓰는 입력(공통 옵션)도 모든 박스에 같이(737-3 · 한 값 · 어디서 바꿔도 모두 따라감)
|
||||
* 「계산식」 열 = 값 든 식(「268,908 × 0.12」 · 서버 `글`) + 표 · 요소 · 로직 알약 + 그 줄 입력 칩 · 변수 이름 식(`이름글`)은 마우스 올리면
|
||||
* 맨 아래 = 한 줄 4 칸 — 비목 소계 셋(노무비 · 재료비 · 경비 — 장비 노무는 여기서 인력과 더해짐) + 계 · 화면(모달) 바닥에 붙음(737-1)
|
||||
* 줄 누름 = 그 줄 자료 모달(`_LogicLab_Modal.ts`) · 고침 고리(`edit`) 있으면 모달 안 「그 자리 고침」(`_LogicLab_Cover.ts`)
|
||||
* 값 = 서버 셈 그대로(박스 소계 · 비목 소계만 줄 금액 합) · 화면에서 다시 셈 안 함
|
||||
* 줄 누름 = 그 줄 자료 모달(`_LogicLab_Modal.ts`) · 고침 고리(`edit`) 있으면 모달 안 「그 자리 고침」(`_LogicLab_Cover.ts`) · 값 = 서버 셈 그대로(박스 소계 · 비목 소계만 줄 금액 합 1 원 버림 751-3)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
@@ -30,7 +29,7 @@ import type {
|
||||
} from "./M01_MasterData_UI_Logic_Api";
|
||||
import { pctView } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { formatMoney, formatPrice } from "./M01_MasterData_UI_Logic_Money";
|
||||
import { formatLineMoney, formatMoney, formatPrice } from "./M01_MasterData_UI_Logic_Money";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { pickMaterial } from "./M01_MasterData_UI_Logic_CalcPick";
|
||||
import { coverForm, type CoverPart, type LineCover } from "./M01_MasterData_UI_LogicLab_Cover";
|
||||
@@ -44,6 +43,7 @@ import {
|
||||
type PickCond,
|
||||
} from "./M01_MasterData_UI_LogicLab_Modal";
|
||||
import { buildFoot, FOOT_KEYS, type FootKey } from "./M01_MasterData_UI_LogicLab_Foot";
|
||||
import { rounddown } from "../B09_Estimation/B09_Estimation_Calc_Base";
|
||||
import { fragmentRow, type Fragment } from "./M01_MasterData_UI_LogicLab_Pill";
|
||||
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
|
||||
@@ -428,7 +428,7 @@ const COLS: Column[] = [
|
||||
{ label: tl("Col_Amount"), width: "13%", align: "end" },
|
||||
];
|
||||
|
||||
const money = (v: number | null): string => (v === null ? "" : formatMoney(v));
|
||||
const money = (v: number | null): string => (v === null ? "" : formatLineMoney(v)); // 줄 0.1 원(751-3)
|
||||
|
||||
/** 계산식 칸 — 값 든 식(계산 뒤) · 계산 전 = 수량 식 · 알약 · 이름 식은 풍선(748-3 옵션 값 꼬리표 없음) */
|
||||
function formulaCell(e: Entry, text = ""): HTMLElement {
|
||||
@@ -508,7 +508,7 @@ type Unfoldable = TextLine & { 펼침?: SheetUnfold | null };
|
||||
function mergedRows(m: HoMerged<Entry>, ctx: HoContext): HTMLTableRowElement[] {
|
||||
const first = m.몫[0];
|
||||
const share = (cost: string): string =>
|
||||
formatMoney(
|
||||
formatLineMoney(
|
||||
m.몫
|
||||
.filter((x) => (x.share ?? COST_OF[x.group]) === cost)
|
||||
.reduce((s, x) => s + (x.amount ?? 0), 0),
|
||||
@@ -622,7 +622,7 @@ function boxCard(
|
||||
);
|
||||
const sub = tableRow(COLS, [
|
||||
{ content: `${name} ${tl("Subtotal")}`, colSpan: 5 },
|
||||
calculated ? formatMoney(b.소계) : "—",
|
||||
calculated ? formatMoney(rounddown(b.소계, 0)) : "—", // 751-3 소계 1 원 버림
|
||||
]);
|
||||
sub.classList.add("m01lab__subtotal");
|
||||
sub.dataset.box = b.묶음;
|
||||
@@ -668,10 +668,10 @@ export function buildHo(ctx: HoContext): HTMLElement[] {
|
||||
const sums = Object.fromEntries(
|
||||
FOOT_KEYS.map((g) => [
|
||||
g,
|
||||
all.filter((e) => e.group === g).reduce((s, e) => s + (e.amount ?? 0), 0),
|
||||
rounddown(all.filter((e) => e.group === g).reduce((s, e) => s + (e.amount ?? 0), 0)),
|
||||
]),
|
||||
) as Record<FootKey, number>;
|
||||
const total = all.reduce((s, e) => s + (e.amount ?? 0), 0);
|
||||
) as Record<FootKey, number>; // 751-3 비목 소계 = 줄 합 1 원 버림 · 계 = 셋 합(서버와 같은 값)
|
||||
const total = FOOT_KEYS.reduce((s, g) => s + sums[g], 0);
|
||||
return [
|
||||
...(boxes.length || !common.length ? [] : [commonCard(optionRows(common, ctx))]),
|
||||
...boxes.map((b) => boxCard(b, ctx, calculated, common)),
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
showItems,
|
||||
} from "./M01_MasterData_UI_Logic_CalcPick";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { formatMoney } from "./M01_MasterData_UI_Logic_Money";
|
||||
import { formatLineMoney, formatMoney } from "./M01_MasterData_UI_Logic_Money";
|
||||
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createDivider } from "@ui/ui_template_divider";
|
||||
@@ -278,7 +278,7 @@ export function answerView(answer: CalcAnswer, draft: boolean, money: boolean):
|
||||
],
|
||||
}),
|
||||
el("td", { text: `${formatNumber(pctView(line).qty)} ${pctView(line).unit}` }),
|
||||
el("td", { className: "m01-logic__money", text: formatMoney(line.금액) }),
|
||||
el("td", { className: "m01-logic__money", text: formatLineMoney(line.금액) }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -334,7 +334,7 @@ export function answerView(answer: CalcAnswer, draft: boolean, money: boolean):
|
||||
el("dd", {
|
||||
text:
|
||||
money && typeof v === "number" && (Math.abs(v) >= 1000 || MONEY_NAME.test(k))
|
||||
? formatMoney(v)
|
||||
? formatLineMoney(v) // 751-3 중간 돈 = 버린 줄 합(0.1 원)
|
||||
: formatNumber(v),
|
||||
}),
|
||||
]),
|
||||
|
||||
@@ -50,6 +50,8 @@ from M02_MasterTemplete import M02_Material_Own as own
|
||||
|
||||
mf = m01.cm.mf
|
||||
mm = mf.mp.mm
|
||||
import master_cut as mcut # noqa: E402 — 끝수(751-3 · 품셈 1-2-2) · scripts 길은 `m01` 이 둠
|
||||
|
||||
COSTS = mf.COST_ITEMS # 노무비 · 재료비 · 경비
|
||||
MATERIAL_HEAD = "자재|"
|
||||
_MAT_COLS = ("코드", "이름", "규격", "단위")
|
||||
@@ -265,14 +267,9 @@ def drop_apart(got: dict[str, Any], shown: Any) -> dict[str, Any]:
|
||||
}
|
||||
if not out or not got.get("기준"):
|
||||
return got
|
||||
base = {k: _number(v) or Decimal(0) for k, v in got["기준"].items()}
|
||||
for n in out:
|
||||
for k, v in (lines[n].get("비목") or {}).items():
|
||||
cut = _number(v) or Decimal(0)
|
||||
base[k] = base.get(k, Decimal(0)) - cut
|
||||
base["계"] -= cut
|
||||
kept = [x for i, x in enumerate(lines) if i not in out]
|
||||
return {**got, "기준": _json(base), "줄": kept}
|
||||
kept = [x for i, x in enumerate(lines) if i not in out] # 기준 = 남은 줄 합(751-3 끝수)
|
||||
got = mcut.plain({**got, "줄": kept})
|
||||
return {**got, "기준": _json(got["기준"])}
|
||||
|
||||
|
||||
def _mat_marks(value: Any, logic: dict) -> dict[str, dict[str, str]] | None:
|
||||
@@ -403,10 +400,8 @@ def _money(value: Any) -> dict[str, Decimal] | None:
|
||||
|
||||
|
||||
def reflect(base: dict[str, Decimal], qty: Decimal | None, times: Decimal) -> dict | None:
|
||||
"""반영 = 기준 × 수량 ÷ 배 — 수량이 없으면 None."""
|
||||
if qty is None:
|
||||
return None
|
||||
return {k: base[k] * qty / times for k in (*COSTS, "계")}
|
||||
"""반영 = 기준 × 수량 ÷ 배 — 비목마다 0.1 원 버림 · 계 = 셋 합(751-3) · 수량이 없으면 None."""
|
||||
return None if qty is None else mcut.row({k: base[k] * qty / times for k in COSTS})
|
||||
|
||||
|
||||
def _sum(rows: list[dict | None]) -> dict[str, Decimal]:
|
||||
@@ -453,14 +448,14 @@ def settle(result: dict, doc: dict, given: dict[str, Any], col: Any = None) -> d
|
||||
for one in result.get("자재") or []:
|
||||
qty, source = _pick(mhands.get(one.get("코드")), one.get("알약"), have)
|
||||
price = _number(one.get("기준단가"))
|
||||
cost = price * qty if price is not None and qty is not None else None
|
||||
cost = mcut.down(price * qty, 1) if price is not None and qty is not None else None
|
||||
mats.append({**one, "수량": qty, "수량출처": source, "반영": cost})
|
||||
if one.get("코드") in aside:
|
||||
continue
|
||||
bases.append({**zero, "재료비": price, "계": price} if price is not None else None)
|
||||
reflects.append({**zero, "재료비": cost, "계": cost} if cost is not None else None)
|
||||
fixed.ratios(lines, reflects, bases) # 비율 고정 줄(705-3) — 다른 줄 반영합 × 율
|
||||
return _json({"줄": lines, "자재": mats, "기준합": _sum(bases), "반영합": _sum(reflects)})
|
||||
return _json({"줄": lines, "자재": mats, "기준합": _sum(bases), "반영합": mcut.sums(reflects)})
|
||||
|
||||
|
||||
def material_price(master: Any, code: str, unit: str | None) -> dict[str, Any]:
|
||||
@@ -545,15 +540,17 @@ def calc(
|
||||
|
||||
|
||||
def run_line(
|
||||
whole: Any, code: str, given: Any, rate: Any = None, picks: Any = None
|
||||
whole: Any, code: str, given: Any, rate: Any = None, picks: Any = None, raw: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""로직 한 줄 기준(서버 단독 · M01 엔진 · 빈 입력 = 자동값 · × 켜쌓기율 · 고른 재료 = 엔진
|
||||
`choosing`) — `{기준, 줄, 재료, 까닭}` · 돈 아닌 로직은 `{결과, 기준: None}`(반영 없음)."""
|
||||
`choosing`) — `{기준, 줄, 재료, 까닭}` · 돈 아닌 로직은 `{결과, 기준: None}`(반영 없음) ·
|
||||
끝수 = 품셈 1-2-2(`master_cut` · 751-3) · `raw` = 엔진 날 값(B09 = 제 단가로 버림)."""
|
||||
try:
|
||||
logic = whole.logic(code)
|
||||
auto = {k: v for k, v in m01.mau.values(whole, logic).items() if v is not None}
|
||||
with mf.mp.choosing(picks):
|
||||
got = layered(mf.run(whole, code, {**auto, **m01._dec(given or {})}), rate)
|
||||
got = mf.run(whole, code, {**auto, **m01._dec(given or {})})
|
||||
got = layered(got if raw else mcut.engine(whole, code, got), rate)
|
||||
except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) as e:
|
||||
return {"기준": None, "까닭": str(e) or type(e).__name__}
|
||||
if "결과" in got:
|
||||
@@ -562,7 +559,8 @@ def run_line(
|
||||
money = {k: got[k] for k in (*COSTS, "계")}
|
||||
mats = logic_mats(whole, logic, got["줄"], picks)
|
||||
lines = pct_extras(whole, logic, got["줄"], got.get("중간") or {})
|
||||
return {"기준": money, "줄": lines, "재료": mats, "까닭": None}
|
||||
out = {"기준": money, "줄": lines, "재료": mats, "까닭": None}
|
||||
return out if raw or rate in (None, 100) else mcut.plain(out) # 켜쌓기율 곱 뒤 다시 버림
|
||||
|
||||
|
||||
def pct_extras(whole: Any, logic: dict, lines: list[dict], env: dict) -> list[dict]:
|
||||
|
||||
@@ -165,7 +165,7 @@ class Expand:
|
||||
inner[-1]["까닭"] = NO_QTY
|
||||
why = why or f"「{sub.get('이름')}」 {NO_QTY}"
|
||||
continue
|
||||
rows.append({k: v * qty for k, v in sub["기준"].items()})
|
||||
rows.append(labor.mcut.row({k: v * qty for k, v in sub["기준"].items()}))
|
||||
for mat in doc.get("자재") or []: # 사용자 자재(175-2) = 기준단가 × 수량(이은 값)
|
||||
if labor.apart(mat): # 별도계상 = 안 셈 · 안 줄에도 없음(222-1)
|
||||
continue
|
||||
@@ -189,7 +189,7 @@ class Expand:
|
||||
inner[-1]["까닭"] = got["까닭"] or NO_QTY # 단가 까닭 먼저
|
||||
why = why or f"「{name}」 {inner[-1]['까닭']}"
|
||||
continue
|
||||
rows.append({k: v * qty for k, v in base.items()})
|
||||
rows.append(labor.mcut.row({k: v * qty for k, v in base.items()}))
|
||||
under = labor._sum(rows)
|
||||
for one in inner: # 비율 고정 줄(705-3) — 밑 = 다른 줄 합
|
||||
if (one.get("고정") or {}).get("비율"):
|
||||
@@ -199,7 +199,7 @@ class Expand:
|
||||
"이름": doc.get("이름"),
|
||||
"단위": doc.get("단위"),
|
||||
"머리": head_of(doc),
|
||||
"기준": None if why else labor._sum(rows),
|
||||
"기준": None if why else labor.mcut.sums(rows), # 줄 0.1 원 · 합 1 원 버림(751-3)
|
||||
"까닭": why,
|
||||
"안": inner,
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ from typing import Any
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
mf = m01.cm.mf
|
||||
import master_cut as mcut # noqa: E402 — 끝수(751-3) · scripts 길은 `m01` 이 둠
|
||||
|
||||
mm = mf.mp.mm
|
||||
COSTS = mf.COST_ITEMS # 노무비 · 재료비 · 경비
|
||||
FIXED = "고정"
|
||||
@@ -135,10 +137,11 @@ def line(whole: Any, fx: dict) -> dict[str, Any]:
|
||||
|
||||
|
||||
def ratio_row(rate: dict, qty: Decimal | None, under: dict) -> tuple[dict, dict | None]:
|
||||
"""비율 줄 (기준, 반영) — 기준[비목] = 밑[기준] · 반영 = 기준 × 율 ÷ 100(수량 없음 = None)."""
|
||||
"""비율 줄 (기준, 반영) — 기준[비목] = 밑[기준] · 반영 = 기준 × 율 ÷ 100 ·
|
||||
0.1 원 버림(751-3) · 수량 없음 = None."""
|
||||
base = dict.fromkeys((*COSTS, "계"), Decimal(0))
|
||||
base[rate["비목"]] = base["계"] = under[rate["기준"]]
|
||||
return base, None if qty is None else {k: v * qty / 100 for k, v in base.items()}
|
||||
return base, None if qty is None else mcut.row({k: base[k] * qty / 100 for k in COSTS})
|
||||
|
||||
|
||||
def ratios(lines: list[dict], reflects: list[dict | None], bases: list[dict | None]) -> None:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* 거울 시험 `resources/tester/test_m02_labor_calc_mirror.py`.
|
||||
* 기준(결과단위 그대로)은 서버 calc 길이 셈 · 여기는 받은 기준에
|
||||
* 수량(손이 칸보다 앞섬 · 칸 = 알약 키 값) · 반영 = 기준 × 수량 ÷ 배 · 자재 = 재료비만.
|
||||
* 끝수(751-3 · 품셈 1-2-2) = B09 `rounddown` — 반영 · 자재 금액 비목마다 0.1 원 · 반영합 비목마다 1 원 버림.
|
||||
* 별도계상 자재(222-1 · 빈 칸 = 기본 별도계상) = 반영은 보이되 합에 안 더함(짝 `apart`).
|
||||
* 참조 줄(단가승격 · 사전조합 · 50-3)은 `lineId` = 「참조|층|주인|키」(짝 `M02_Labor_Ref.ref_id`) ·
|
||||
* 고정 줄(705-1)은 「고정|이름|규격|단위」(짝 `M02_Labor_Fixed.fixed_id`) · 기준 = 서버가 낸 줄 단가.
|
||||
@@ -11,8 +12,10 @@
|
||||
|
||||
import type { CalcLine as LogicLine } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import type { LogicMat } from "./M02_Labor_LogicMats_Model";
|
||||
import { rounddown } from "../B09_Estimation/B09_Estimation_Calc_Base";
|
||||
|
||||
export const MONEY_KEYS = ["노무비", "재료비", "경비", "계"] as const;
|
||||
const COSTS = ["노무비", "재료비", "경비"] as const;
|
||||
export type Money = Record<(typeof MONEY_KEYS)[number], number>;
|
||||
export type QtySource = "손" | "칸" | "없음";
|
||||
/** 문서의 수량 칸 — null = 면적산출 알약 · {손} = 손 입력 */
|
||||
@@ -175,14 +178,22 @@ function pick(
|
||||
return [null, "없음"];
|
||||
}
|
||||
|
||||
/** 반영 = 기준 × 수량 ÷ 배 — 수량이 없으면 null */
|
||||
/** 반영 = 기준 × 수량 ÷ 배 — 비목마다 0.1 원 버림 · 계 = 셋 합 · 수량이 없으면 null */
|
||||
export function reflect(base: Money, qty: number | null, times: number): Money | null {
|
||||
if (qty === null) return null;
|
||||
const out = zero();
|
||||
for (const k of MONEY_KEYS) out[k] = (base[k] * qty) / times;
|
||||
for (const k of COSTS) out[k] = rounddown((base[k] * qty) / times, 1);
|
||||
out.계 = out.노무비 + out.재료비 + out.경비;
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 반영합 끝수 — 비목마다 1 원 버림 · 계 = 셋 합(짝 `master_cut.sums`) */
|
||||
function cutSums(sum: Money): Money {
|
||||
for (const k of COSTS) sum[k] = rounddown(sum[k], 0);
|
||||
sum.계 = sum.노무비 + sum.재료비 + sum.경비;
|
||||
return sum;
|
||||
}
|
||||
|
||||
/** 기준이 선 계산 답 + 문서 손 값 + 알약 값(`{키: 값}`) → 수량 · 반영 · 합을 새로 채운 답 ·
|
||||
* `col` = 규격 열(없으면 기본 · 704-4) */
|
||||
export function settle(
|
||||
@@ -218,12 +229,13 @@ export function settle(
|
||||
const 자재 = result.자재.map((one) => {
|
||||
const [qty, source] = pick(mhands.get(one.코드), one.알약, have);
|
||||
const price = num(one.기준단가);
|
||||
const cost = price !== null && qty !== null ? price * qty : null;
|
||||
const cost = price !== null && qty !== null ? rounddown(price * qty, 1) : null;
|
||||
if (price !== null && !aside.has(one.코드)) add(base, { ...zero(), 재료비: price, 계: price });
|
||||
if (cost !== null && !aside.has(one.코드)) add(done, { ...zero(), 재료비: cost, 계: cost });
|
||||
return { ...one, 수량: qty, 수량출처: source, 반영: cost };
|
||||
});
|
||||
return { 줄: ratioLines(줄, done, base), 자재, 기준합: base, 반영합: done };
|
||||
const lines = ratioLines(줄, done, base);
|
||||
return { 줄: lines, 자재, 기준합: base, 반영합: cutSums(done) };
|
||||
}
|
||||
|
||||
/** 비율 고정 줄(705-3 · 짝 `M02_Labor_Fixed.ratios`) — 밑 = 비율 줄 뺀 반영합 · 기준[비목] = 밑[기준] ·
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { createButton, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { HOPYO_TAG } from "@ui/ui_template_hopyo_grid";
|
||||
import { pctView } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import { formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money";
|
||||
import { formatLineMoney, formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money";
|
||||
import type { ItemLogic } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import type {
|
||||
LaborCalc,
|
||||
@@ -157,8 +157,8 @@ export function moneyCell(
|
||||
wait: boolean,
|
||||
label: string,
|
||||
): HTMLElement {
|
||||
const show = (v: number | null | undefined): string =>
|
||||
wait ? "…" : v === null || v === undefined ? "—" : formatMoney(v);
|
||||
const show = (v: number | null | undefined, f = formatMoney): string =>
|
||||
wait ? "…" : v === null || v === undefined ? "—" : f(v);
|
||||
return el("td", {
|
||||
className: "m02-labrow__money",
|
||||
attrs: { "data-label": label },
|
||||
@@ -170,7 +170,7 @@ export function moneyCell(
|
||||
}),
|
||||
el("span", {
|
||||
className: "m02-labrow__applied",
|
||||
text: show(applied),
|
||||
text: show(applied, formatLineMoney), // 반영 = 줄 0.1 원(751-3)
|
||||
attrs: { title: tc("Labor_Applied") },
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -31,7 +31,7 @@ def text_of(over: Any, key: str, values: dict, picks: Any, ids: list, eng: list)
|
||||
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 {}
|
||||
mid = labor.mcut.engine(over, key, mf.run(over, key, given)).get("중간") or {} # 751-3
|
||||
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:
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import type { CalcLine, TextAnswer } from "../../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import { bornOfCid } from "./M02_Born_Tag";
|
||||
import type { SheetGroup } from "../../A00_Common/calc_sheet";
|
||||
import { rounddown } from "../../B09_Estimation/B09_Estimation_Calc_Base";
|
||||
import { forgetAdd, goneName, reviveAdd, setQty, resetQty } from "./M02_Sangeun_Undo";
|
||||
|
||||
export type CoverPart = "labor" | "material" | "expense";
|
||||
@@ -425,7 +426,9 @@ export function sheetGroups(rows: CoverRow[]): SheetGroup[] {
|
||||
}
|
||||
}
|
||||
const order = Object.keys(PART_OF);
|
||||
return [...groups.values()].sort((a, b) => order.indexOf(a.비목) - order.indexOf(b.비목));
|
||||
return [...groups.values()] // 751-3 소계 = 줄 합 1 원 버림(품셈 1-2-2 · 서버 `master_cut` 와 같은 값)
|
||||
.map((g) => ({ ...g, 소계: rounddown(g.소계 ?? 0, 0) }))
|
||||
.sort((a, b) => order.indexOf(a.비목) - order.indexOf(b.비목));
|
||||
}
|
||||
|
||||
/* ── 공용 상세(Ho) 줄 ── */
|
||||
|
||||
@@ -173,8 +173,12 @@ def master(folder: Path = MASTER) -> mf.Master:
|
||||
|
||||
|
||||
def calc(files: dict[str, dict], ref: str, given: dict) -> dict:
|
||||
"""시험 계산(M01) — `ref` = 로직 키 · 멈추면 FormulaError."""
|
||||
return mf.run(mf.Master(files), ref, given)
|
||||
"""시험 계산(M01) — `ref` = 로직 키 · 멈추면 FormulaError ·
|
||||
금액 끝수 = 품셈 1-2-2(`master_cut` · 751-3)."""
|
||||
import master_cut as mcut # B09 끝수 함수를 부름 — 검사만 돌 때는 안 읽음
|
||||
|
||||
master = mf.Master(files)
|
||||
return mcut.engine(master, ref, mf.run(master, ref, given))
|
||||
|
||||
|
||||
# ── (1) 틀 ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -125,7 +125,9 @@ def run(master: mf.Master, row: dict, given: dict[str, dict] | None = None) -> d
|
||||
{k: target.get(k) for k in ("이름", "원문번호", "결과단위", "구분", "상세구분", "출처")}
|
||||
)
|
||||
try:
|
||||
got = mf.run(master, key, dict(given.get(key) or {}))
|
||||
import master_cut as mcut # 끝수(751-3) = 시험 계산(`check_master.calc`)과 같음
|
||||
|
||||
got = mcut.engine(master, key, mf.run(master, key, dict(given.get(key) or {})))
|
||||
except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) as e:
|
||||
reason = str(e) or type(e).__name__
|
||||
lines.append({**head, "ok": False, "까닭": reason})
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""M01 · M02 셈 끝수(PLAN 751-3) — 품셈 1-2-2 금액의 단위표준
|
||||
(일위대가 금액란 0.1 원 · 계금 1 원 · 미만버림).
|
||||
|
||||
규칙 = B09 일위대가 · 산근과 같은 함수 `rounddown`(`B09_Estimation_Calc_Base` · 엑셀 ROUNDDOWN):
|
||||
줄 금액 = 비목마다 ROUNDDOWN(·, 1) · 금액 = 그 합
|
||||
하위 로직 줄(값묶음 없음) = 하위 로직도 같은 끝수로 다시 셈한 노 · 재 · 경 × 수량
|
||||
덧줄 = 버린 줄 합(노무비 · 재료비 · 경비 · `줄.'이름'`)으로 식을 다시 셈 → ROUNDDOWN 1
|
||||
비목 합 = ROUNDDOWN(Σ 줄, 0)(+ 로직 `끝수` 묶음) · 계 = 셋 합
|
||||
엔진 줄 덧칸(단위 · 수량 · 단가 · 출처 · 까닭 · 묶음 · 옵션꺼짐 …) · 수량 · 품 자리는 그대로.
|
||||
쓰는 곳 = M01 시험 계산(`check_master.calc`) · 읽는 식(`master_text.lines`) · M02 로직 줄
|
||||
(`M02_Labor_Doc.run_line` — 단가산출 · 일위대가 · 펼침이 모두 거침) · 일위대가 반영(`row` · `sums` ·
|
||||
TS 짝 `M02_MasterTemplete_Labor_Calc.ts` · 거울 시험 `test_m02_labor_calc_mirror.py`).
|
||||
B09 는 제 단가로 다시 버리므로 엔진 날 값(`run_line(raw=True)`)을 받음.
|
||||
ponytail: 흐름 로직 · 돈 결과 로직(결과 하나)은 끝수 없음 — 그 로직이 돈을 내면 여기에 더함.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import master_formula as mf
|
||||
|
||||
_ROOT = str(Path(__file__).resolve().parents[3])
|
||||
if _ROOT not in sys.path: # 스크립트 · 시험 길 — 서버는 이미 뿌리에서 돎
|
||||
sys.path.insert(0, _ROOT)
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import rounddown # noqa: E402
|
||||
|
||||
COSTS = mf.COST_ITEMS
|
||||
_ERRORS = (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError)
|
||||
|
||||
|
||||
def down(value, places: int):
|
||||
"""ROUNDDOWN(value, places) — Decimal 로 · 빈 값 = None."""
|
||||
if value is None:
|
||||
return None
|
||||
return Decimal(str(rounddown(float(value), places)))
|
||||
|
||||
|
||||
def _parts(split: dict) -> tuple[dict, Decimal | None]:
|
||||
"""줄 비목 → (비목마다 0.1 원 버림, 금액 = 그 합 · 모두 빈 값 = None)."""
|
||||
cut = {k: down(v, 1) for k, v in split.items()}
|
||||
kept = [v for v in cut.values() if v is not None]
|
||||
return cut, (sum(kept, Decimal(0)) if kept else None)
|
||||
|
||||
|
||||
def row(money: dict | None) -> dict | None:
|
||||
"""반영 한 줄 `{노무비, 재료비, 경비, 계}` — 비목마다 0.1 원 버림 · 계 = 셋 합."""
|
||||
if money is None:
|
||||
return None
|
||||
cut = {k: down(money.get(k) or 0, 1) for k in COSTS}
|
||||
return {**cut, "계": sum(cut.values(), Decimal(0))}
|
||||
|
||||
|
||||
def sums(rows: list) -> dict:
|
||||
"""비목 합 — 비목마다 ROUNDDOWN(Σ 줄, 0) · 계 = 셋 합(빈 줄 = 뺌)."""
|
||||
cut = {k: down(sum((Decimal(str(r[k])) for r in rows if r), Decimal(0)), 0) for k in COSTS}
|
||||
return {**cut, "계": sum(cut.values(), Decimal(0))}
|
||||
|
||||
|
||||
def _sub(master, item: dict, env: dict, depth: int) -> dict | None:
|
||||
"""하위 로직 줄 노 · 재 · 경 — 같은 끝수로 다시 셈 · 로직 부르기가 아니거나 못 셈 = None."""
|
||||
if item.get("값묶음") is not None or item.get("종류") != "로직":
|
||||
return None
|
||||
try:
|
||||
node = mf.parse(item["요소"])
|
||||
if node[0] != "logic":
|
||||
return None
|
||||
given = {k: mf.evaluate(v, env, master, depth) for k, v in node[2]}
|
||||
ref = mf.fill(node[1], env)
|
||||
got = engine(master, ref, mf.run(master, ref, given, depth + 1), depth + 1)
|
||||
except _ERRORS:
|
||||
return None
|
||||
return None if "결과" in got else got
|
||||
|
||||
|
||||
def engine(master, ref: str, got: dict, depth: int = 0) -> dict:
|
||||
"""엔진 답(`mf.run` 꼴 `{줄, 노무비, 재료비, 경비, 계, 중간}`) → 끝수 입힌 답(새 묶음)."""
|
||||
if "결과" in got or "줄" not in got:
|
||||
return got
|
||||
logic = master.logic(ref)
|
||||
if isinstance(logic.get("흐름"), dict):
|
||||
return got
|
||||
items, adds = logic.get("호표") or [], logic.get("덧줄") or []
|
||||
env = dict(got.get("중간") or {})
|
||||
total = dict.fromkeys(COSTS, Decimal(0))
|
||||
lines = []
|
||||
for n, line in enumerate(got["줄"]):
|
||||
split, more = dict(line.get("비목") or {}), {}
|
||||
if n == len(items): # 덧줄이 부르는 `줄.'이름'` = 버린 호표 줄 금액(엔진과 같이 한 번)
|
||||
env["줄"] = {x["이름"]: x["금액"] for x in lines if x["이름"] and x["금액"] is not None}
|
||||
if n < len(items):
|
||||
sub = _sub(master, items[n], env, depth) if line.get("수량") is not None else None
|
||||
if sub is not None:
|
||||
qty, one = line["수량"], items[n].get("비목")
|
||||
split = {one: qty * sub["계"]} if one else {k: qty * sub[k] for k in COSTS}
|
||||
more = {"단가": sub["계"]} # 하위 로직 계(1 원 버림) = 이 줄 단가
|
||||
elif n - len(items) < len(adds) and line.get("금액") is not None:
|
||||
add = adds[n - len(items)]
|
||||
env.update(total)
|
||||
try:
|
||||
value = mf.evaluate(mf.parse(add["식"]), env, master, depth)
|
||||
except _ERRORS:
|
||||
value = line["금액"]
|
||||
env[add["이름"]] = down(value, 1) # 뒤 덧줄 · 중간 칸 = 버린 값
|
||||
split = {add["비목"]: value}
|
||||
cut, money = _parts(split)
|
||||
for k, v in cut.items():
|
||||
total[k] += v or 0
|
||||
if n >= len(items) and money is not None:
|
||||
more = {"단가": money}
|
||||
lines.append({**line, "비목": cut, "금액": money, **more})
|
||||
costs = {k: mf.cut(logic, down(v, 0), "계") for k, v in total.items()}
|
||||
# 중간 = 덧줄이 본 값(버린 줄 합 · 덧줄 값 · `줄`) — 시험 계산 중간 칸
|
||||
return {**got, "줄": lines, **costs, "계": sum(costs.values()), "중간": env}
|
||||
|
||||
|
||||
def plain(got: dict) -> dict:
|
||||
"""로직 줄 답(`run_line` 꼴 `{기준, 줄, 재료}`)을 줄만 다시 버림 —
|
||||
켜쌓기율 곱 · 별도계상 뺀 뒤 · 기준 = 남은 줄 비목 합 ROUNDDOWN 0."""
|
||||
if not got.get("기준"):
|
||||
return got
|
||||
lines, total = [], dict.fromkeys(COSTS, Decimal(0))
|
||||
for line in got.get("줄") or []:
|
||||
cut, money = _parts(dict(line.get("비목") or {}))
|
||||
for k, v in cut.items():
|
||||
total[k] += Decimal(str(v or 0))
|
||||
lines.append({**line, "비목": cut, "금액": money})
|
||||
mats = [{**m, "금액": down(m.get("금액"), 1)} for m in got.get("재료") or []]
|
||||
base = {k: down(v, 0) for k, v in total.items()}
|
||||
return {**got, "줄": lines, "재료": mats, "기준": {**base, "계": sum(base.values())}}
|
||||
@@ -12,18 +12,22 @@
|
||||
계산이 안 되면(FormulaError 따위) 갈래 양쪽을 다 내보내고 깊이를 적어 접을 수 있게 함.
|
||||
호표 줄 계산 자체가 막혀도(재료 단가 없음 따위) 이름 식·조각은 `row` 구조만으로 그리고
|
||||
`글`·`금액`만 비움 — `만약() 과 무관` 인 그 60개 로직도 이름 식은 보이게.
|
||||
끝수는 줄이 아니라 소계에 녹여 적음. 계산 자체는 `master_formula.run` 이 한 벌만 함.
|
||||
금액 = 품셈 1-2-2 끝수 입힌 엔진 답(`master_cut.engine` · 줄 0.1 원 · 소계 1 원 버림) · 수 꼴 =
|
||||
`common_util_money`(줄 금액 `fmt_line_money` · 단가 `fmt_price` · 수량 `fmt`).
|
||||
계산은 `master_formula.run` 한 벌.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from decimal import Decimal
|
||||
|
||||
import master_copy as mc
|
||||
import master_cut as mcut # 끝수(751-3) — 뿌리 길도 둠(아래 common_util)
|
||||
import master_formula as mf
|
||||
import master_text_item as mti
|
||||
|
||||
PLACES = Decimal("0.0001") # 화면 `formatNumber` 와 같음(소수 넷째 자리까지)
|
||||
from common_util.common_util_money import fmt, fmt_line_money, fmt_money, fmt_price # noqa: F401
|
||||
|
||||
PRICE_TAIL = {"인력": "노임", "기계": "손료"} # 단가 자리의 이름 꼬리(그 밖은 「단가」)
|
||||
ELEM_KEYS = ("LB", "EQ", "MT", "MP", "MO") # 요소 테이블 — 그 밖의 열쇠(QC · QF …)는 별칭 이름
|
||||
ELEM_TAIL = {"LB": "노임", "EQ": "손료"} # 식 안 요소 키의 이름 꼬리(그 밖은 「단가」)
|
||||
@@ -33,34 +37,6 @@ _RIGHT_TIGHT = ("-", "/") # 오른쪽 짝은 같은 세기라도 괄호가 필
|
||||
_CALC_ERRORS = (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) # 계산 못 함
|
||||
|
||||
|
||||
def fmt(value) -> str:
|
||||
"""보이는 자릿수 — 소수 넷째 자리·유효숫자 4 자리 중 많은 쪽 · 뒤 0 뗌 · 천 단위 쉼표."""
|
||||
if not isinstance(value, Decimal):
|
||||
return str(value)
|
||||
places = max(4, 3 - value.adjusted()) if value else 4 # 0.00167 → 0.001667
|
||||
cut = value
|
||||
if value.as_tuple().exponent < -places:
|
||||
cut = value.quantize(Decimal(1).scaleb(-places))
|
||||
sign, digits = ("-", -cut) if cut < 0 else ("", cut)
|
||||
whole, _, rest = f"{digits.normalize():f}".partition(".")
|
||||
if "E" in whole or "e" in whole: # 아주 큰 수 — 지수 꼴을 펴서 씀
|
||||
whole, _, rest = f"{digits:f}".rstrip("0").rstrip(".").partition(".")
|
||||
return sign + f"{int(whole):,}" + (f".{rest}" if rest else "")
|
||||
|
||||
|
||||
def fmt_money(value) -> str:
|
||||
"""돈(단가 · 금액)은 원 단위 정수로 보임 — 글만 그렇고 계산 값은 소수 그대로."""
|
||||
if isinstance(value, Decimal):
|
||||
value = value.quantize(Decimal(1), rounding=ROUND_HALF_UP)
|
||||
return fmt(value)
|
||||
|
||||
|
||||
def fmt_price(value) -> str:
|
||||
"""단가 — 정수로 보이되 200 원 미만은 반올림 오차가 커서 유효숫자로."""
|
||||
small = isinstance(value, Decimal) and abs(value) < 200
|
||||
return fmt(value) if small else fmt_money(value)
|
||||
|
||||
|
||||
# ── 식을 글로 — 값 꼴과 이름 꼴 ────────────────────────────────────────
|
||||
def render(node, env: dict, master, depth: int = 0) -> str:
|
||||
"""식 한 마디 → 값 꼴 글. 값이 정해지는 마디(요소 · 찾기 · 로직 · 이름)는 그 수로 바뀜."""
|
||||
@@ -358,7 +334,7 @@ def _pieces(show, real, env: dict, master, alias_by_name: dict, ifdepth: int = 0
|
||||
piece["글"] = show[2]
|
||||
try:
|
||||
value = _value(real, env, master, 0)
|
||||
if value == "None": # 값이 빈 줄 — 「None」 을 글로 안 찍음
|
||||
if value in ("None", ""): # 값이 빈 줄 — 빈 글 · 「None」 을 글로 안 찍음
|
||||
raise mf.FormulaError("값 없음")
|
||||
piece.update({"값": value} if kind == "name" else {"글": value})
|
||||
except _CALC_ERRORS:
|
||||
@@ -444,7 +420,7 @@ def _line_text(item: dict, line: dict, env: dict, master, part: str | None) -> s
|
||||
qty = _times(render(mf.parse(item["수량"]), env, master), item, env, master)
|
||||
money = line["비목"][part] if part else line["금액"]
|
||||
price = line["단가"] if part is None or line["수량"] == 0 else money / line["수량"]
|
||||
return f"{fmt_price(price)} * {qty} = {fmt_money(money)}"
|
||||
return f"{fmt_price(price)} * {qty} = {fmt_line_money(money)}"
|
||||
|
||||
|
||||
def _item_names(items: list) -> list[tuple[str, str]]:
|
||||
@@ -472,7 +448,7 @@ def _line_names(
|
||||
def _extra_text(extra: dict, line: dict, env: dict, master) -> str:
|
||||
if line["금액"] is None: # 빈 덧줄 — 식을 다시 셈하지 않음
|
||||
return ""
|
||||
return f"{render(mf.parse(extra['식']), env, master)} = {fmt_money(line['금액'])}"
|
||||
return f"{render(mf.parse(extra['식']), env, master)} = {fmt_line_money(line['금액'])}"
|
||||
|
||||
|
||||
def _extra_names(extra: dict, env: dict, master, name_of: dict) -> str:
|
||||
@@ -501,7 +477,7 @@ def lines(files: dict, key: str, given: dict) -> dict:
|
||||
name_of = mc.names(marks)
|
||||
alias_by_name = _alias_index(marks)
|
||||
try:
|
||||
result = mf.run(master, key, given)
|
||||
result = mcut.engine(master, key, mf.run(master, key, given))
|
||||
except _CALC_ERRORS:
|
||||
result = None
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* M02 일위대가 수량 · 반영 · 합(`M02_MasterTemplete_Labor_Calc.ts` `settle`) 거울 헬퍼 —
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌림 · stdin = {답, 문서, 값, 열?} · stdout = settle 결과 JSON. */
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌림(상대 경로 .ts 가져옴도 · B09 `rounddown` 751-3) ·
|
||||
* stdin = {답, 문서, 값, 열?} · stdout = settle 결과 JSON. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
@@ -11,11 +12,17 @@ const file = path.join(
|
||||
"M02_MasterTemplete",
|
||||
"M02_MasterTemplete_Labor_Calc.ts",
|
||||
);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, require);
|
||||
function load(at) {
|
||||
const js = ts.transpileModule(fs.readFileSync(at, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
const need = (p) =>
|
||||
p.startsWith(".") ? load(path.resolve(path.dirname(at), `${p}.ts`)) : require(p);
|
||||
new Function("exports", "module", "require", js)(box.exports, box, need);
|
||||
return box.exports;
|
||||
}
|
||||
const box = { exports: load(file) };
|
||||
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
process.stdout.write(JSON.stringify(box.exports.settle(input.답, input.문서, input.값, input.열)));
|
||||
|
||||
@@ -29,7 +29,8 @@ def test_큰돌쌓기_찰쌓기_제잡비_율_설치_9_안함_3_금액_그대로
|
||||
assert x["율"] == Decimal(rate) and x["규격"] == "노무비의 %"
|
||||
assert x["단위"] == "식" and x["수량"] == 1 # 엔진 줄은 그대로 · 표기는 화면
|
||||
labor_sum = x["_기준"]["노무비"]
|
||||
assert abs(x["금액"] - labor_sum * rate / 100) < Decimal("0.01")
|
||||
# 751-3 — % 밑 = 버린 줄 합(소계 1 원 버림 전) · 금액 0.1 원 버림
|
||||
assert abs(x["금액"] - labor_sum * rate / 100) <= Decimal(rate) / 100 + Decimal("0.1")
|
||||
|
||||
|
||||
def test_인건비_호표줄_밑수_금액식은_덧칸_없음():
|
||||
|
||||
@@ -60,6 +60,8 @@ def test_머리_구성_합계_줄_꼬리표_빈_줄():
|
||||
STUBS = r"""
|
||||
const tc = (k) => ({ Grid_NoQty: "수량 없음", Grid_Apart: "별도계상 · 합계 밖", Labor_Layer: "켜쌓기 {n}%", Grid_Head: "일위대가", Grid_Mine: "내 자료집", Lib_Company: "회사", Lib_System: "마스터" })[k] ?? k;
|
||||
const valueLine = (inputs, values) => ({ text: Object.values(values ?? {}).join(" · "), empty: 0 });
|
||||
// 751-3 `Labor_Calc.ts` 가 가져오는 B09 `rounddown`(이어 붙이기라 import 가 빠짐 · 같은 식)
|
||||
const rounddown = (v, p = 0) => { const s = 10 ** p; const a = Math.floor(Math.abs(v) * s + 1e-7) / s; return v < 0 ? -a : a; };
|
||||
"""
|
||||
|
||||
DOC = {
|
||||
|
||||
@@ -194,8 +194,8 @@ def test_격자_규격_열_수량_칸_열마다():
|
||||
mt1 = next(r for r in out["rows"] if r["name"] == "ㄴ 큰돌")
|
||||
assert mt1["qty_0"] == 4 and mt1["qty_1"] == 5 # 2.0 빈 열 = 공통 수량
|
||||
t15, t20 = out["specs"]["totals"]
|
||||
assert t20 == pytest.approx(112559.55) # 기본 열 = 금액 칸 합
|
||||
assert t15 == pytest.approx(529950 * 1.5 / 10 + 1800) # 열 수량 · 별도계상 MT1 = 합 밖
|
||||
assert t20 == 112559 # 기본 열 = 금액 칸 합 비목마다 1 원 버림(751-3 · 옛 112,559.55)
|
||||
assert t15 == 81292 # 529,950 × 1.5 ÷ 10 + 1,800 = 81,292.5 → 1 원 버림 · 별도계상 MT1 = 합 밖
|
||||
|
||||
|
||||
def test_고르기_창_spec_돌려줌():
|
||||
|
||||
@@ -50,7 +50,7 @@ FILES = { # 실무 xlsx 넷 — EST 꼴 거창 · STmate 꼴 영월 · 울진
|
||||
# (호표, 줄, 고정, 하위 참조, 이음, 엑셀 = 우리) — 영월 제22호표 = STmate 하위 호표 비목 반올림 1 원 끝수
|
||||
EXPECT = {
|
||||
"거창": (14, 68, 56, 12, 11, 14),
|
||||
"영월": (23, 138, 135, 3, 0, 22),
|
||||
"영월": (23, 138, 135, 3, 0, 23), # 751-3 끝수 뒤 엑셀 금액과 같음 하나 늚(23 개 전부)
|
||||
"대흥": (34, 171, 143, 28, 0, 34),
|
||||
"소광": (25, 117, 98, 19, 0, 25),
|
||||
}
|
||||
@@ -116,12 +116,11 @@ def test_비율_고정_줄_파이썬과_TS_가_같은_답(values):
|
||||
misc, tool = [ln for ln in py["줄"] if (ln.get("고정") or {}).get("비율")]
|
||||
others = [ln for ln in py["줄"] if ln not in (misc, tool)]
|
||||
under = sum((ln["반영"] or {}).get("재료비", 0) for ln in others) # 자재 = 별도계상(합 밖)
|
||||
assert misc["반영"]["재료비"] == pytest.approx(under * 3 / 100) and misc["배"] == 100
|
||||
assert misc["반영"]["재료비"] == pytest.approx(under * 3 / 100, abs=0.1) and misc["배"] == 100
|
||||
assert misc["반영"]["노무비"] == misc["반영"]["경비"] == 0
|
||||
assert tool["반영"]["경비"] == 0 # 인부 수량 없음 = 노무비 밑 0
|
||||
assert py["반영합"]["계"] == pytest.approx(
|
||||
sum((ln["반영"] or {}).get("계", 0) for ln in py["줄"])
|
||||
)
|
||||
gap = sum((ln["반영"] or {}).get("계", 0) for ln in py["줄"]) - py["반영합"]["계"]
|
||||
assert -1e-6 <= gap < 3 # 751-3 반영합 = 비목마다 1 원 버림
|
||||
|
||||
|
||||
def test_비율_정리_막힘():
|
||||
|
||||
@@ -48,10 +48,11 @@ def test_logic_line_cover_in_precombo(world):
|
||||
kept = store.read("precombo", sc)["문서"]["담은로직"][0]
|
||||
assert kept["패치"] == COVER
|
||||
got = _value(world["personal"], sc)["기준"]
|
||||
labor_sum = Decimal("0.5") * Decimal(str(bare["줄"][0]["단가"])) + Decimal(
|
||||
cut = labor.mcut.down # 751-3 — 줄 0.1 원 · 비목 합 1 원 버림
|
||||
labor_sum = cut(Decimal("0.5") * Decimal(str(bare["줄"][0]["단가"])), 1) + Decimal(
|
||||
str(bare["줄"][1]["금액"])
|
||||
)
|
||||
assert got["노무비"] == pytest.approx(labor_sum)
|
||||
assert got["노무비"] == pytest.approx(cut(labor_sum, 0))
|
||||
assert got["경비"] == pytest.approx(labor_sum * 3 / 100, abs=1)
|
||||
|
||||
|
||||
|
||||
@@ -38,8 +38,9 @@ def test_price_cover_is_hand_price_and_pct_follows():
|
||||
got = _run(바탕={"로직": PCT}, 덮개={"labor:LB000036#0": {"labor": 200000}})
|
||||
line = got["줄"][0]
|
||||
assert line["단가"] == 200000 and line["출처"] == "손 단가"
|
||||
want = Decimal(str(first["수량"])) * 200000 + Decimal(str(second["금액"]))
|
||||
assert got["기준"]["노무비"] == pytest.approx(float(want))
|
||||
cut = labor.mcut.down # 751-3 — 줄 0.1 원 · 비목 합 1 원 버림
|
||||
want = cut(Decimal(str(first["수량"])) * 200000, 1) + Decimal(str(second["금액"]))
|
||||
assert got["기준"]["노무비"] == pytest.approx(float(cut(want, 0)))
|
||||
assert got["기준"]["경비"] == pytest.approx(float(want * 3 / 100), abs=1)
|
||||
|
||||
|
||||
|
||||
@@ -57,8 +57,9 @@ def test_qty_cover_hand_and_pct_recomputed(world):
|
||||
assert doc["덮개"]["labor:LB000036#0"]["원값"]["qty"] == pytest.approx(float(first["수량"]))
|
||||
assert len(doc["바탕판"]) == 16
|
||||
got = _calc(world["personal"], key)
|
||||
labor_sum = Decimal("0.5") * Decimal(str(first["단가"])) + Decimal(str(second["금액"]))
|
||||
assert got["기준"]["노무비"] == pytest.approx(float(labor_sum))
|
||||
cut = calc.labor.mcut.down # 751-3 — 줄 0.1 원 · 비목 합 1 원 버림
|
||||
labor_sum = cut(Decimal("0.5") * Decimal(str(first["단가"])), 1) + Decimal(str(second["금액"]))
|
||||
assert got["기준"]["노무비"] == pytest.approx(float(cut(labor_sum, 0)))
|
||||
assert got["기준"]["경비"] == pytest.approx(float(labor_sum * 3 / 100), abs=1)
|
||||
assert got["줄"][0]["덮개"] == ["qty"] and got["줄"][0]["cid"] == "labor:LB000036#0"
|
||||
assert got["짝잃음"] == []
|
||||
@@ -116,7 +117,9 @@ def test_chain_base_flattened_delete_free(world):
|
||||
assert _doc(world["personal"], b)["바탕"] == {"로직": PCT}
|
||||
got = _calc(world["personal"], b)
|
||||
bare = _bare(PCT)
|
||||
assert got["기준"]["노무비"] == pytest.approx(float(bare["줄"][0]["금액"]))
|
||||
assert got["기준"]["노무비"] == pytest.approx(
|
||||
float(calc.labor.mcut.down(bare["줄"][0]["금액"], 0))
|
||||
)
|
||||
with store.at(world["personal"]):
|
||||
store.delete("sangeun", a) # 쓰는 곳 없음(서로 독립)
|
||||
assert _calc(world["personal"], b)["기준"] == got["기준"]
|
||||
|
||||
@@ -87,7 +87,8 @@ def test_저장_안_한_줄_셈_쓰기_없음(user: TestClient) -> None:
|
||||
double = user.post(f"{BASE}/calc", json={"key": KEY, "inputs": {}, "row": row2}).json()
|
||||
text = user.post(f"{BASE}/text", json={"key": KEY, "inputs": {}, "row": row2}).json()
|
||||
assert saved["ok"] and same["sums"] == saved["sums"]
|
||||
assert double["sums"]["노무비"] == pytest.approx(saved["sums"]["노무비"] * 2)
|
||||
# 751-3 — 줄 0.1 원 · 소계 1 원 버림이라 두 배는 1 원 안
|
||||
assert double["sums"]["노무비"] == pytest.approx(saved["sums"]["노무비"] * 2, abs=2)
|
||||
assert text["ok"] and text["계"] == pytest.approx(double["sums"]["계"], rel=1e-6)
|
||||
assert store.logic(KEY)["logic"]["호표"][0]["수량"] == line["수량"] # 정본 그대로
|
||||
assert _prints() == before
|
||||
|
||||
@@ -36,7 +36,11 @@ def _node(body: str) -> object:
|
||||
born = _text(M02 / "logic" / "M02_Born_Tag.ts") # 740-2 — 모델이 쓰는 꼬리표 글(import 줄은 뺌)
|
||||
model = re.sub(r"^import .*$", "", _text(MODEL), flags=re.M)
|
||||
undo = re.sub(r"^import .*$", "", _text(M02 / "logic" / "M02_Sangeun_Undo.ts"), flags=re.M)
|
||||
src = born + "\n" + undo + "\n" + model + "\n" + body
|
||||
# 751-3 모델이 가져오는 B09 `rounddown`(import 줄을 빼므로 같은 식을 둠)
|
||||
cut = "const rounddown = (v, p = 0) => { const s = 10 ** p; " + (
|
||||
"const a = Math.floor(Math.abs(v) * s + 1e-7) / s; return v < 0 ? -a : a; };"
|
||||
)
|
||||
src = "\n".join((born, undo, cut, model, body))
|
||||
out = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", src],
|
||||
@@ -230,11 +234,13 @@ def test_화면_문서_꼴_서버_저장_셈(world): # noqa: F811 — 가져온
|
||||
by = {x["cid"]: x for x in got["줄"]}
|
||||
assert "labor:LB000002#1" not in by
|
||||
assert by["labor:LB000018#0"]["금액"] == pytest.approx(0.02 * a["단가"])
|
||||
assert by["logic:GC000267#0"]["금액"] == pytest.approx(g["수량"] * 90000, rel=1e-6)
|
||||
# 751-3 — 비목마다 0.1 원 버림(셋)
|
||||
assert by["logic:GC000267#0"]["금액"] == pytest.approx(g["수량"] * 90000, abs=0.3)
|
||||
added = by["add:#0"] # 더한 보통인부 = 바탕 보통인부 단가 × 0.01
|
||||
assert added["수량"] == pytest.approx(0.01)
|
||||
assert added["단가"] == pytest.approx(lines["labor:LB000002#0"]["단가"])
|
||||
assert got["기준"]["계"] == pytest.approx(sum(x["금액"] for x in got["줄"]))
|
||||
gap = sum(x["금액"] for x in got["줄"]) - got["기준"]["계"] # 751-3 비목마다 1 원 버림
|
||||
assert -1e-6 <= gap < 3
|
||||
|
||||
|
||||
def test_725_바탕_없으면_개정_안_뜸_새_SK_나무_다시_읽음_더한줄_단가_찾음():
|
||||
|
||||
@@ -104,7 +104,7 @@ def test_no_base_common_option_seoul(world): # noqa: F811
|
||||
assert got["빠진줄"] == [], got
|
||||
assert got["기준"], {k: v for k, v in got.items() if k != "줄"}
|
||||
assert got["기준"]["계"] == pytest.approx(_m01(GF, {"지역": "서울", "보정작업": "해당 없음"}))
|
||||
assert round(float(got["기준"]["계"]), 2) == 5297.37
|
||||
assert float(got["기준"]["계"]) == 5295 # 751-3 끝수(옛 날 값 5,297.37)
|
||||
|
||||
|
||||
def test_screen_no_base_inputs_and_gap_dash():
|
||||
|
||||
@@ -125,7 +125,9 @@ def test_727_2_기초_둘_세부_펼침_계(world): # noqa: F811
|
||||
"add:#0/pct:공구손료 및 경장비#0",
|
||||
]
|
||||
assert sub["add:#0/labor:LB000036#0"]["수량"] == pytest.approx(0.018)
|
||||
assert sum(x["금액"] for x in add["펼침"]) == pytest.approx(add["금액"], rel=1e-9)
|
||||
# 머리 = 수량 × 기초 끝수 계(751-3 · 비목 1 원 · 줄 0.1 원 버림) — 펼침 합과 비목마다 수량 × 1 원 안
|
||||
q = add["수량"]
|
||||
assert sum(x["금액"] for x in add["펼침"]) == pytest.approx(add["금액"], abs=0.3 + 3 * q)
|
||||
assert add["금액"] == pytest.approx(_m01(PCT), rel=1e-9)
|
||||
assert got["기준"]["계"] == pytest.approx(bare["기준"]["계"] + _m01(PCT), rel=1e-9)
|
||||
with store.at(world["personal"]): # 펼침 없는 셈(쓰는 자리) = 같은 계
|
||||
@@ -144,8 +146,9 @@ def test_727_2_둘째_기초_덮개_뺀줄_저장(world): # noqa: F811
|
||||
assert saved["뺀줄"] == DROP and saved["바탕판"]
|
||||
got = _run(world["personal"], doc)
|
||||
add = _line(got, "add:#0")
|
||||
want = 2 * (0.036 * 233250 + 0.012 * 172068)
|
||||
assert add["금액"] == pytest.approx(want, rel=1e-9)
|
||||
cut = labor.mcut.down # 751-3 — 기초 줄 0.1 원 · 비목 합 1 원 → × 수량 0.1 원
|
||||
want = cut(2 * cut(cut(0.036 * 233250, 1) + cut(0.012 * 172068, 1), 0), 1)
|
||||
assert add["금액"] == pytest.approx(float(want), rel=1e-9)
|
||||
sub = {x["cid"]: x for x in add["펼침"]}
|
||||
assert sub["add:#0/labor:LB000036#0"]["덮개"] == ["qty"]
|
||||
assert sub["add:#0/labor:LB000036#0"]["수량"] == pytest.approx(0.072)
|
||||
|
||||
@@ -40,7 +40,7 @@ def _shown(whole, doc):
|
||||
|
||||
def test_단목베기_100본_제안_본으로_바꾸면_1본_계(whole):
|
||||
full = _shown(whole, _doc("GF000287", "100본"))
|
||||
assert full["배"] == 1 and round(full["기준"]["계"], 2) == Decimal("153889.40")
|
||||
assert full["배"] == 1 and full["기준"]["계"] == Decimal("153889") # 751-3 1 원 버림
|
||||
assert sc.head_of({"단위": "100본"}) == "100본 당"
|
||||
one = _shown(whole, _doc("GF000287", "본"))
|
||||
assert one["배"] == 100 and round(one["기준"]["계"], 2) == Decimal("1538.89")
|
||||
@@ -51,7 +51,7 @@ def test_단목베기_100본_제안_본으로_바꾸면_1본_계(whole):
|
||||
|
||||
def test_사석_수상고르기_10제곱미터_에서_1제곱미터(whole):
|
||||
one = _shown(whole, _doc("GC000421", "㎡"))
|
||||
assert round(one["기준"]["계"], 2) == Decimal("44791.32")
|
||||
assert one["기준"]["계"] == Decimal("44791.1") # 10㎡ 447,911(1 원 버림) ÷10
|
||||
|
||||
|
||||
def test_성격_다르면_계수_칸_없으면_셈_막음(whole):
|
||||
@@ -73,7 +73,7 @@ def test_성격_다르면_계수_칸_없으면_셈_막음(whole):
|
||||
def test_식_빈_단위는_옛_셈(whole):
|
||||
old = _shown(whole, _doc("GF000287", "식"))
|
||||
assert old["배"] == 100 and round(old["1단위"]["계"], 2) == Decimal("1538.89")
|
||||
assert "화면배" not in old and old["기준"]["계"] == Decimal("153889.4") # 화면 = 바탕 몫 그대로
|
||||
assert "화면배" not in old and old["기준"]["계"] == Decimal("153889") # 화면 = 바탕 몫 그대로
|
||||
|
||||
|
||||
def test_둘째_기초_수량_처음값_배(whole):
|
||||
@@ -85,7 +85,8 @@ def test_둘째_기초_수량_처음값_배(whole):
|
||||
got = sc.run(sd.clean(_doc("GC000421", "㎡", 더한줄=[one]), None, whole), whole)
|
||||
bare = sc.run(sd.clean(_doc("GC000421", "㎡"), None, whole), whole)
|
||||
added = got["1단위"]["계"] - bare["1단위"]["계"]
|
||||
assert abs(added - alone * qty / 10) < Decimal("0.01") # ㎡ 몫 = 100㎡ 값 ÷100
|
||||
# ㎡ 몫 = 100㎡ 값 ÷100 · 비목 합 1 원 버림(751-3)이라 바탕 몫 ÷10 = 0.1 원 안
|
||||
assert abs(added - alone * qty / 10) <= Decimal("0.1")
|
||||
src = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
@@ -46,8 +46,12 @@ def test_둘째_기초_세부_원문_품_머리_수량(world):
|
||||
for row, line in zip(head["펼침"], m01_lines):
|
||||
assert row["원문"]["수량"] == pytest.approx(float(line["수량"]), rel=1e-9) # 원문 그대로
|
||||
assert row["수량"] == pytest.approx(float(line["수량"]) * 0.01, rel=1e-9) # 상자 몫
|
||||
assert sum(x["금액"] for x in head["펼침"]) == pytest.approx(head["금액"], rel=1e-9)
|
||||
assert sum(x["원문"]["금액"] for x in head["펼침"]) * 0.01 == pytest.approx(head["금액"])
|
||||
# 머리 = 수량 × 기초 끝수 계(751-3 · 비목 1 원 · 줄 0.1 원 버림) — 펼침 합과 0.3 + 수량 × 3 원 안
|
||||
near = 0.3 + 3 * 0.01
|
||||
assert sum(x["금액"] for x in head["펼침"]) == pytest.approx(head["금액"], abs=near)
|
||||
assert sum(x["원문"]["금액"] for x in head["펼침"]) * 0.01 == pytest.approx(
|
||||
head["금액"], abs=near
|
||||
)
|
||||
|
||||
|
||||
def test_처음수량_넣을때_값_셈_지문_안_듦(world):
|
||||
|
||||
@@ -50,7 +50,9 @@ def _same(a: dict) -> None:
|
||||
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["계"]) # 시험 계산 표 합
|
||||
# 시험 계산 표 합 — 줄 0.1 원 · 비목 합 1 원 버림(751-3) = 줄 합 아래 비목마다 1 원(× 화면 배) 안
|
||||
gap = sum(x["금액"] or 0 for x in a["줄"]) - base["계"]
|
||||
assert -1e-6 <= gap < 3 / (a.get("화면배") or 1)
|
||||
assert a["중간"] and all(not isinstance(v, (dict, list)) for v in a["중간"].values())
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
"""751-3 셈 끝수 = 품셈 1-2-2(줄 0.1 원 · 소계 1 원 미만버림) — `master_cut` 지킴 시험.
|
||||
|
||||
M01 시험 계산 · 읽는 식 · M02 단가산출(SK) · 일위대가 반영이 B09 끝수(`rounddown`)와 같은 값 ·
|
||||
줄 합 = 소계(비목마다 ROUNDDOWN(Σ 줄, 0)) · B09 는 엔진 날 값(`raw`)을 받아 제 단가로 다시 버림.
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from B09_Estimation import B09_Estimation_Sangeun as sg
|
||||
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
|
||||
|
||||
mcut, mf = labor.mcut, labor.mf # scripts 길은 `m01` 이 둠
|
||||
KEY = "GC000211" # 합판거푸집(테스트1 바탕) — 인력 · 재료 · % 덧줄 둘
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
|
||||
|
||||
def _whole():
|
||||
return m01.loaded()[1]
|
||||
|
||||
|
||||
def _tenth(v) -> bool:
|
||||
return v is None or Decimal(str(v)) == Decimal(str(v)).quantize(Decimal("0.1"))
|
||||
|
||||
|
||||
def _sums_of_lines(lines) -> dict:
|
||||
"""비목마다 ROUNDDOWN(Σ 줄, 0) — B09 `block_totals` 규칙."""
|
||||
total = dict.fromkeys(COSTS, Decimal(0))
|
||||
for x in lines:
|
||||
for k, v in (x.get("비목") or {}).items():
|
||||
total[k] += Decimal(str(v or 0))
|
||||
return {k: mcut.down(v, 0) for k, v in total.items()}
|
||||
|
||||
|
||||
def test_줄은_0점1원_소계는_1원_버림_줄합이_소계():
|
||||
got = labor.run_line(_whole(), KEY, {})
|
||||
base = got["기준"]
|
||||
assert all(_tenth(x.get("금액")) for x in got["줄"])
|
||||
assert all(Decimal(str(base[k])) == int(base[k]) for k in COSTS)
|
||||
assert {k: Decimal(str(base[k])) for k in COSTS} == _sums_of_lines(got["줄"])
|
||||
assert base["계"] == sum(base[k] for k in COSTS)
|
||||
|
||||
|
||||
def test_B09_산근_끝수와_같음():
|
||||
"""엔진 날 줄을 B09 규칙(줄 ROUNDDOWN 1 · % = 버린 밑 × 율 · 비목 ROUNDDOWN 0)으로 = 끝수 답."""
|
||||
whole = _whole()
|
||||
raw = labor.run_line(whole, KEY, {}, raw=True)
|
||||
cut = labor.run_line(whole, KEY, {})
|
||||
lines = []
|
||||
for x in raw["줄"][: len(whole.logic(KEY)["호표"])]:
|
||||
lines.append(
|
||||
{k: sg._money(float(x["단가"] or 0), float(x["수량"] or 0)) for k in x["비목"]}
|
||||
)
|
||||
under = {k: sum(ln.get(k, 0) for ln in lines) for k in COSTS}
|
||||
for x in raw["줄"][len(lines) :]: # % 덧줄 = 버린 밑 × 율 / 100 → ROUNDDOWN 1
|
||||
(part,) = x["비목"]
|
||||
base = under["재료비"] if "재료" in x["이름"] or "소모" in x["이름"] else under["노무비"]
|
||||
lines.append({part: sg.rounddown(base * float(x["율"]) / 100, 1)})
|
||||
b09 = sg.block_totals([{"labor": ln.get("노무비", 0), "material": ln.get("재료비", 0),
|
||||
"expense": ln.get("경비", 0)} for ln in lines]) # fmt: skip
|
||||
assert (b09["labor"], b09["material"], b09["expense"], b09["sum"]) == tuple(
|
||||
float(cut["기준"][k]) for k in (*COSTS, "계")
|
||||
)
|
||||
assert raw["기준"]["계"] != cut["기준"]["계"] # 날 값은 버리기 전(B09 몫)
|
||||
|
||||
|
||||
def test_화면_넷_같은_값_SK_시험계산_읽는식():
|
||||
whole = _whole()
|
||||
doc = sd.clean(
|
||||
{"키": "SK-000000", "이름": "끝수", "단위": "㎡", "바탕": {"로직": KEY}}, None, whole
|
||||
)
|
||||
sk = calc.run(doc, whole)["기준"]
|
||||
auto = {k: v for k, v in (m01.auto(KEY)["값"] or {}).items() if v is not None}
|
||||
m1 = m01.calc(KEY, auto)
|
||||
text = m01.text(KEY, auto)
|
||||
assert float(sk["계"]) == m1["sums"]["계"] == float(text["계"])
|
||||
assert {g["비목"]: float(g["소계"]) for g in text["groups"]} == {k: float(sk[k]) for k in COSTS}
|
||||
line = next(x for g in text["groups"] for x in g["줄"] if x["이름"].startswith("형틀"))
|
||||
assert line["글"].endswith(" = " + _line_text(line["금액"])) and line["글"].endswith(".9")
|
||||
|
||||
|
||||
def _line_text(v) -> str:
|
||||
from common_util.common_util_money import fmt_line_money
|
||||
|
||||
return fmt_line_money(Decimal(str(v)))
|
||||
|
||||
|
||||
def test_하위_로직_줄은_하위_계를_버려_단가로():
|
||||
"""기계 · 하위 로직 줄 단가 = 하위 로직 끝수 계(1 원) ·
|
||||
비목마다 하위 비목 × 수량 0.1 원 버림."""
|
||||
whole = _whole()
|
||||
key = next(
|
||||
str(r["키"])
|
||||
for _, d in m01._logic_files(m01.cm.load(folder=m01.FOLDER))
|
||||
for r in d.get("줄") or []
|
||||
if any(i.get("종류") == "로직" for i in r.get("호표") or [])
|
||||
and "결과" not in r
|
||||
and not isinstance(r.get("흐름"), dict)
|
||||
and labor.run_line(whole, str(r["키"]), {}).get("기준")
|
||||
)
|
||||
got = labor.run_line(whole, key, {})
|
||||
for item, x in zip(whole.logic(key)["호표"], got["줄"]):
|
||||
if item.get("종류") == "로직" and x.get("단가") is not None and x.get("수량") is not None:
|
||||
assert Decimal(str(x["단가"])) == int(x["단가"])
|
||||
parts = [
|
||||
Decimal(str(v)) for v in x["비목"].values()
|
||||
] # 비목마다 0.1 원 버림 · 금액 = 합
|
||||
assert all(_tenth(v) for v in parts) and Decimal(str(x["금액"])) == sum(parts)
|
||||
assert abs(Decimal(str(x["단가"])) * x["수량"] - sum(parts)) < Decimal("0.3")
|
||||
|
||||
|
||||
def test_날_값은_엔진_그대로():
|
||||
whole = _whole()
|
||||
eng = mf.run(whole, KEY, {**m01.mau.values(whole, whole.logic(KEY))})
|
||||
raw = labor.run_line(whole, KEY, {}, raw=True)
|
||||
assert raw["기준"]["계"] == eng["계"]
|
||||
|
||||
|
||||
def test_일위대가_반영_줄_0점1원_합_1원():
|
||||
doc = labor.clean({"담은로직": [{"로직": KEY, "수량": {"손": 3.3}}]})
|
||||
out = labor.calc(doc, {})
|
||||
line = out["줄"][0]["반영"]
|
||||
assert all(_tenth(line[k]) for k in COSTS) and line["계"] == sum(line[k] for k in COSTS)
|
||||
total = out["반영합"]
|
||||
assert all(float(total[k]) == int(total[k]) for k in COSTS)
|
||||
assert {k: Decimal(str(total[k])) for k in COSTS} == {
|
||||
k: mcut.down(Decimal(str(line[k])), 0) for k in COSTS
|
||||
}
|
||||
|
||||
|
||||
def test_별도계상_뺀_기준은_남은_줄_합_버림():
|
||||
whole = _whole()
|
||||
got = labor.run_line(whole, KEY, {})
|
||||
marks = {str(m["차례"]): {"별도계상": "직접계산"} for m in got["재료"]}
|
||||
marks[str(got["재료"][0]["차례"])] = {"별도계상": "별도계상"} # 첫 재료만 뺌
|
||||
out = labor.drop_apart(got, marks)
|
||||
assert len(out["줄"]) == len(got["줄"]) - 1
|
||||
assert {k: Decimal(str(out["기준"][k])) for k in COSTS} == _sums_of_lines(out["줄"])
|
||||
@@ -164,10 +164,11 @@ def test_로직_목록_하나_시험계산(client: TestClient) -> None:
|
||||
got = _calc(client)
|
||||
assert got["ok"] is True
|
||||
lines = {x["이름"]: x for x in got["lines"]}
|
||||
assert Decimal(str(lines["화약취급공"]["금액"])) == Decimal(
|
||||
str(lines["화약취급공"]["수량"])
|
||||
) * Decimal(str(lines["화약취급공"]["단가"]))
|
||||
assert got["sums"]["계"] == pytest.approx(sum(x["금액"] for x in got["lines"]))
|
||||
one = lines["화약취급공"] # 751-3 — 줄 0.1 원 · 비목 합 1 원 버림
|
||||
raw = Decimal(str(one["수량"])) * Decimal(str(one["단가"]))
|
||||
assert Decimal(str(one["금액"])) == Decimal(str(int(raw * 10))) / 10
|
||||
gap = sum(x["금액"] for x in got["lines"]) - got["sums"]["계"]
|
||||
assert -1e-6 <= gap < 3
|
||||
stop = _calc(client, 없는입력=1)
|
||||
assert stop["ok"] is False and "모르는 입력" in stop["reason"]
|
||||
|
||||
|
||||
@@ -59,8 +59,9 @@ def test_없는_로직은_404(client) -> None:
|
||||
|
||||
|
||||
def test_견본_금액은_앞과_같음(client) -> None:
|
||||
assert Decimal(str(_calc(client, "GF000160")["sums"]["계"])) == Decimal("117485.212")
|
||||
assert Decimal(str(_calc(client, "GF000219")["sums"]["계"])) == Decimal("79279.668")
|
||||
# 751-3 끝수(줄 0.1 원 · 비목 합 1 원 버림) — 옛 날 값 117,485.212 · 79,279.668
|
||||
assert Decimal(str(_calc(client, "GF000160")["sums"]["계"])) == Decimal("117484")
|
||||
assert Decimal(str(_calc(client, "GF000219")["sums"]["계"])) == Decimal("79279")
|
||||
|
||||
|
||||
def test_빈_줄을_부르는_덧줄도_비움(client) -> None:
|
||||
@@ -83,7 +84,8 @@ def test_빈_입력을_쓰는_줄만_비움(client) -> None:
|
||||
for one in blank.values():
|
||||
assert one["단가"] is None and one["금액"] is None and "비어 있음" in one["까닭"]
|
||||
rest = sum(Decimal(str(one["금액"])) for one in got["lines"] if one["금액"] is not None)
|
||||
assert Decimal(str(got["sums"]["계"])) == rest > 0 # 빈 줄은 빼고 더함
|
||||
gap = rest - Decimal(str(got["sums"]["계"])) # 빈 줄은 빼고 더함 · 비목마다 1 원 버림(751-3)
|
||||
assert rest > 0 and 0 <= gap < 3
|
||||
assert got["sums"]["경비"] == 0
|
||||
|
||||
|
||||
@@ -107,8 +109,8 @@ def test_체인톱_가격_오일은_자재품목(client, key) -> None:
|
||||
assert lines["체인오일"]["단가"] == 4612
|
||||
gas = Decimal(str(lines["휘발유(체인톱)"]["금액"]))
|
||||
assert abs(Decimal(str(lines["잡품"]["금액"])) - gas * Decimal("0.4")) < Decimal(
|
||||
"0.001"
|
||||
) # 잡품 = 휘발유값 40% 그대로
|
||||
"0.1"
|
||||
) # 잡품 = 휘발유값 40% 그대로(0.1 원 버림 · 751-3)
|
||||
|
||||
|
||||
def test_체인톱_규격은_고를_수_있음(client) -> None:
|
||||
|
||||
@@ -286,7 +286,8 @@ def test_서버_길이_저장까지_하고_그_로직이_돈다(client: TestClie
|
||||
calc = res.json()
|
||||
assert calc["ok"] is True, calc
|
||||
노무 = Decimal(str(calc["sums"]["노무비"]))
|
||||
assert 노무 > 0 and Decimal(str(calc["sums"]["재료비"])) == (노무 * Decimal("0.05"))
|
||||
재료 = Decimal(str(calc["sums"]["재료비"])) # 751-3 — % 밑 = 버린 줄 합 · 소계 1 원 버림
|
||||
assert 노무 > 0 and abs(재료 - 노무 * Decimal("0.05")) <= 1
|
||||
assert Decimal(str(calc["middle"]["넓이"])) == 6
|
||||
|
||||
|
||||
|
||||
@@ -75,14 +75,14 @@ def test_끝수_묶음이_계에_붙고_글자는_안_붙는다(client: TestClie
|
||||
plain = _post(client, "/api/m01/calc", {"key": "", "inputs": {}, "row": _draft()})
|
||||
assert plain["ok"] is True, plain
|
||||
raw = Decimal(str(plain["sums"]["계"]))
|
||||
assert raw != raw.to_integral_value() # 172068 / 7 — 원 미만이 남음
|
||||
assert raw == 24581 # 172068 / 7 = 24,581.14 → 줄 0.1 원 · 소계 1 원 버림(751-3 · 품셈 1-2-2)
|
||||
|
||||
cut = _post(
|
||||
client,
|
||||
"/api/m01/calc",
|
||||
{"key": "", "inputs": {}, "row": _draft({"대상": "계", "자리": 0, "방법": "버림"})},
|
||||
{"key": "", "inputs": {}, "row": _draft({"대상": "계", "자리": -1, "방법": "올림"})},
|
||||
)
|
||||
assert Decimal(str(cut["sums"]["계"])) == raw.to_integral_value(rounding="ROUND_DOWN")
|
||||
assert Decimal(str(cut["sums"]["계"])) == 24590 # 로직 끝수 묶음 = 1-2-2 버림 뒤 한 번 더
|
||||
assert cut["sums"]["노무비"] == cut["sums"]["계"] # 비목 합마다 끊고 계 = 그 합
|
||||
|
||||
text = _post(client, "/api/m01/calc", {"key": "", "inputs": {}, "row": _draft("원 미만 버림")})
|
||||
|
||||
@@ -82,7 +82,10 @@ def test_합판거푸집은_비목마다_묶이고_안_쓰는_줄에_까닭이_
|
||||
assert [g["비목"] for g in got["groups"]] == ["노무비", "재료비"]
|
||||
for group in got["groups"]:
|
||||
assert Decimal(str(group["소계"])) == Decimal(str(calc["sums"][group["비목"]]))
|
||||
assert sum(Decimal(str(one["금액"])) for one in group["줄"]) == Decimal(str(group["소계"]))
|
||||
lines = sum(
|
||||
Decimal(str(one["금액"])) for one in group["줄"]
|
||||
) # 751-3 소계 = 줄 합 1 원 버림
|
||||
assert 0 <= lines - Decimal(str(group["소계"])) < 1
|
||||
why = [one for one in _all_lines(got) if one.get("까닭")]
|
||||
assert why and all("0" in one["까닭"] for one in why)
|
||||
|
||||
@@ -95,7 +98,7 @@ def test_펌프카는_하위로직이_비목마다_한_줄씩이고_덧줄이_
|
||||
assert len(pump) == 3 # 노무비 · 재료비 · 경비 몫이 따로
|
||||
extra = next(one for one in _all_lines(got) if one["이름"].startswith("공구손료"))
|
||||
assert (
|
||||
extra["글"].endswith(f"= {mt.fmt_money(Decimal(str(extra['금액'])))}")
|
||||
extra["글"].endswith(f"= {mt.fmt_line_money(Decimal(str(extra['금액'])))}")
|
||||
and "%" in extra["글"]
|
||||
)
|
||||
|
||||
@@ -398,8 +401,10 @@ def test_글_안의_돈은_정수_계산_값은_소수_그대로(client: TestCli
|
||||
assert mt.fmt_money(Decimal("0.4")) == "0"
|
||||
got, _ = _both(client, FORM)
|
||||
first = _all_lines(got)[0]
|
||||
assert first["글"] == "275,790 * 0.22 * 1.31 = 79,483" # 수량·비율은 소수 그대로
|
||||
assert Decimal(str(first["금액"])) == Decimal("79482.678") # 값은 그대로
|
||||
assert first["글"] == "275,790 * 0.22 * 1.31 = 79,482.6" # 수량·비율은 소수 그대로
|
||||
assert Decimal(str(first["금액"])) == Decimal(
|
||||
"79482.6"
|
||||
) # 줄 0.1 원 버림(751-3 · 옛 79,482.678)
|
||||
|
||||
|
||||
def test_자동값으로_모든_로직을_돌려도_멈추지_않는다() -> None:
|
||||
@@ -482,9 +487,11 @@ def test_값_식_작은_수는_유효숫자로_보인_수끼리_곱해도_금액
|
||||
row = store.cm.master(REAL).logic("GC001064")
|
||||
got = mt.lines(files, "GC001064", dict(store.cm.시험입력(row)))
|
||||
first = _all_lines(got)[0]["글"]
|
||||
assert first == "273,308 * 0.001667 = 456"
|
||||
assert first == "273,308 * 0.001667 = 455.5" # 줄 0.1 원 버림(751-3)
|
||||
assert mt.fmt(Decimal("0.00166667")) == "0.001667" and mt.fmt(Decimal("12.34567")) == "12.3457"
|
||||
assert mt.fmt_price(Decimal("3.5")) == "3.5" and mt.fmt_price(Decimal("3500.4")) == "3,500"
|
||||
assert (
|
||||
mt.fmt_price(Decimal("3.5")) == "3.5" and mt.fmt_price(Decimal("3500.4")) == "3,500.4"
|
||||
) # 751-2 단가 그대로
|
||||
|
||||
|
||||
def _text_of(key: str, **given) -> list[dict]:
|
||||
@@ -500,7 +507,7 @@ def test_빈_줄을_부른_덧줄_값_조각은_None_대신_그_줄_이름() ->
|
||||
|
||||
def test_만약이_고른_갈래가_덧셈이면_곱셈_안에서_괄호로_묶임() -> None:
|
||||
line = next(x for x in _text_of("GC000376", 면="BOX 형상 내면") if x["이름"] == "도장공")
|
||||
assert line["글"] == "263,017 * 0.031 * (1 + 60 %) = 13,046"
|
||||
assert line["글"] == "263,017 * 0.031 * (1 + 60 %) = 13,045.6" # 줄 0.1 원(751-3)
|
||||
outer = next(x for x in _text_of("GC000376", 면="외면") if x["이름"] == "도장공")
|
||||
assert "(" not in outer["글"]
|
||||
|
||||
|
||||
@@ -181,12 +181,12 @@ def test_계산_반영_합은_손셈_원본과_같음(world):
|
||||
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)
|
||||
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 got["반영합"]["계"] == pytest.approx(total)
|
||||
assert 0 <= total - got["반영합"]["계"] < 3 # 반영합 = 비목마다 1 원 버림(751-3)
|
||||
# 원본 층 문서 계산(마스터 같은 사본)과 같음 — 입력값은 프로젝트에 채운 것
|
||||
values = _read(root / "templates/structures" / FOLDER / "입력값.json")
|
||||
doc = {
|
||||
|
||||
@@ -85,16 +85,16 @@ def test_반영은_기준_곱_수량_나누기_배_돈_아닌_로직은_반영_
|
||||
ten, plain = got["줄"]
|
||||
assert (ten["배"], ten["수량"], ten["수량출처"]) == (10, 4, "손")
|
||||
for k in ("노무비", "재료비", "경비", "계"):
|
||||
assert ten["반영"][k] == pytest.approx(ten["기준"][k] * 4 / 10)
|
||||
assert ten["반영"][k] == pytest.approx(ten["기준"][k] * 4 / 10, abs=0.3) # 0.1 원 버림(751-3)
|
||||
assert plain["기준"] is None and plain["반영"] is None and "결과" in plain
|
||||
assert got["반영합"]["계"] == pytest.approx(ten["반영"]["계"])
|
||||
assert 0 <= ten["반영"]["계"] - got["반영합"]["계"] < 3 # 반영합 = 비목마다 1 원 버림(751-3)
|
||||
|
||||
|
||||
def test_돈_로직_줄에_재료_노무_경비_줄_실림_합은_기준():
|
||||
got = labor.calc(labor.clean(_doc(담은로직=[{"로직": TEN}, {"로직": PLAIN}])), {})
|
||||
ten, plain = got["줄"]
|
||||
assert ten["줄"] and all({"이름", "수량", "단위", "금액"} <= set(x) for x in ten["줄"])
|
||||
assert sum(x["금액"] for x in ten["줄"]) == pytest.approx(ten["기준"]["계"])
|
||||
assert 0 <= sum(x["금액"] for x in ten["줄"]) - ten["기준"]["계"] < 3 # 751-3 비목 1 원 버림
|
||||
assert "줄" not in plain
|
||||
|
||||
|
||||
|
||||
@@ -39,13 +39,14 @@ def test_90퍼센트는_100퍼센트_곱하기_0점9() -> None:
|
||||
full = labor.run_line(whole, CODE, {})
|
||||
part = labor.run_line(whole, CODE, {}, 90)
|
||||
for k in (*labor.COSTS, "계"):
|
||||
assert part["기준"][k] == pytest.approx(full["기준"][k] * Decimal("0.9"))
|
||||
# 751-3 — 곱한 뒤 줄 0.1 원 · 비목 합 1 원 다시 버림
|
||||
assert part["기준"][k] == pytest.approx(full["기준"][k] * Decimal("0.9"), abs=5)
|
||||
for a, b in zip(full["줄"], part["줄"]):
|
||||
assert b["단가"] == a["단가"]
|
||||
assert b["수량"] == pytest.approx(a["수량"] * Decimal("0.9"))
|
||||
assert b["금액"] == pytest.approx(a["금액"] * Decimal("0.9"))
|
||||
assert b["금액"] == pytest.approx(a["금액"] * Decimal("0.9"), abs=Decimal("0.3"))
|
||||
for k, v in a["비목"].items():
|
||||
assert b["비목"][k] == pytest.approx(v * Decimal("0.9"))
|
||||
assert b["비목"][k] == pytest.approx(v * Decimal("0.9"), abs=Decimal("0.1"))
|
||||
assert labor.run_line(whole, CODE, {}, 100) == full
|
||||
|
||||
|
||||
@@ -80,7 +81,9 @@ def test_문서_계산과_프로젝트_사본_같음() -> None:
|
||||
(line,) = got["줄"]
|
||||
assert line["기준"]["계"] == pytest.approx(origin["줄"][0]["기준"]["계"]) # 층 셈 꼴(142-1)
|
||||
full = labor.calc({**doc, "담은로직": [{**doc["담은로직"][0], "켜쌓기율": None}]}, {})
|
||||
assert _d(line["기준"]["계"]) == pytest.approx(_d(full["줄"][0]["기준"]["계"]) * Decimal("0.9"))
|
||||
assert _d(line["기준"]["계"]) == pytest.approx(
|
||||
_d(full["줄"][0]["기준"]["계"]) * Decimal("0.9"), abs=5
|
||||
) # 751-3 다시 버림
|
||||
|
||||
|
||||
def test_옛_단가승격도_율() -> None:
|
||||
@@ -90,7 +93,7 @@ def test_옛_단가승격도_율() -> None:
|
||||
got = expand({"층": "system", "주인": None, "키": "SP-900001"})
|
||||
full = labor.run_line(whole, CODE, {})
|
||||
times = _d(doc["배"])
|
||||
assert got["기준"]["계"] == pytest.approx(full["기준"]["계"] * Decimal("0.9") / times)
|
||||
assert got["기준"]["계"] == pytest.approx(full["기준"]["계"] * Decimal("0.9") / times, abs=5)
|
||||
|
||||
|
||||
RATE_SCRIPT = r"""
|
||||
|
||||
@@ -63,7 +63,8 @@ def test_고름이_단가와_로직_재료비를_바꿈_고름_없으면_지금_
|
||||
assert (row["품목"]["키"], row["고름"]) == (WIRE, True)
|
||||
assert row["단가"] == picked["재료"][1]["단가"] != plain["재료"][0]["단가"]
|
||||
gap = (plain["재료"][0]["단가"] - row["단가"]) * row["수량"]
|
||||
assert plain["기준"]["재료비"] - picked["기준"]["재료비"] == pytest.approx(gap)
|
||||
# 751-3 — 줄 0.1 원 · 재료비 합 1 원 버림이라 단가 차 × 수량 과 1 원 안
|
||||
assert plain["기준"]["재료비"] - picked["기준"]["재료비"] == pytest.approx(gap, abs=1.2)
|
||||
assert picked["기준"]["노무비"] == plain["기준"]["노무비"]
|
||||
|
||||
|
||||
|
||||
@@ -232,16 +232,19 @@ def test_calc_는_참조를_펼침_승격은_기준_나누기_배_사전조합
|
||||
got = labor.calc(doc, {f"{refs.ref_id(ref)}#0": 2}) # 안 줄 알약 = 구조물 조합 칸 값
|
||||
fed = labor.calc(doc, {f"{refs.ref_id(ref)}#0": 4})["줄"][0] # 알약이 저장보다 앞섬
|
||||
want = Decimal(str(_base(TEN)["계"])) / 10 * 2
|
||||
assert bare["기준"]["계"] == pytest.approx(float(want)) and bare["안"][0]["수량"] == 2
|
||||
assert fed["기준"]["계"] == pytest.approx(float(want * 2)) and fed["안"][0]["수량"] == 4
|
||||
near = 3 # 751-3 — 안 줄 0.1 원 · 비목 합 1 원 버림
|
||||
assert bare["기준"]["계"] == pytest.approx(float(want), abs=near) and bare["안"][0]["수량"] == 2
|
||||
assert (
|
||||
fed["기준"]["계"] == pytest.approx(float(want * 2), abs=near) and fed["안"][0]["수량"] == 4
|
||||
)
|
||||
one = got["줄"][0]
|
||||
assert one["기준"]["계"] == pytest.approx(float(want))
|
||||
assert one["기준"]["계"] == pytest.approx(float(want), abs=near)
|
||||
assert (one["분모"], one["배"], one["알약"]) == (
|
||||
"㎡",
|
||||
1,
|
||||
refs.ref_id(ref),
|
||||
) # 단위 = 그 사전조합 것
|
||||
assert one["반영"]["계"] == pytest.approx(float(want * 5))
|
||||
assert one["반영"]["계"] == pytest.approx(float(want * 5), abs=20) # 751-3 끝수 × 5
|
||||
assert one["안"][0]["수량"] == 2 and one["안"][0]["참조"]["키"] == sp
|
||||
pills = labor.pills(doc)
|
||||
assert pills[0] == {"키": refs.ref_id(ref), "이름": "둘", "단위": "㎡", "로직": [], "참조": ref}
|
||||
@@ -257,8 +260,8 @@ def test_승격_줄_상세는_원_로직_키_표_사전조합은_안_줄만(worl
|
||||
promote, combo = labor.calc(doc, {})["줄"]
|
||||
assert promote["원로직"] == TEN and promote["줄"]
|
||||
assert sum(x["금액"] for x in promote["줄"]) == pytest.approx(
|
||||
promote["기준"]["계"]
|
||||
) # 1 단위(51-2)
|
||||
promote["기준"]["계"], abs=0.3
|
||||
) # 1 단위(51-2) · 비목 합 1 원 버림 ÷ 배(751-3)
|
||||
assert "줄" not in combo and [x["참조"]["키"] for x in combo["안"]] == [sp]
|
||||
|
||||
|
||||
@@ -455,7 +458,9 @@ def test_층_목록_서고_줄은_이름_단위_기준금액을_실음(world):
|
||||
refs.summaries(world["system"], rows)
|
||||
# 742-1 — 목록 기준금액 = 안 줄 저장 수량(손 3)으로
|
||||
want = Decimal(str(_base(TEN)["계"])) / 10 * 3
|
||||
assert rows[0]["제목"] == "셋" and rows[0]["기준금액"] == pytest.approx(float(want))
|
||||
assert rows[0]["제목"] == "셋" and rows[0]["기준금액"] == pytest.approx(
|
||||
float(want), abs=3
|
||||
) # 751-3
|
||||
assert "기준금액" not in rows[1]
|
||||
|
||||
|
||||
@@ -483,8 +488,8 @@ def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world
|
||||
with store.at(world["system"]):
|
||||
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 5}}]})
|
||||
one = labor.calc(doc, given)["줄"][0]
|
||||
assert one["기준"]["계"] == pytest.approx(float(want))
|
||||
assert one["반영"]["계"] == pytest.approx(float(want * 5))
|
||||
assert one["기준"]["계"] == pytest.approx(float(want), abs=4) # 751-3 안 줄 셋 · 비목 합 버림
|
||||
assert one["반영"]["계"] == pytest.approx(float(want * 5), abs=20) # 751-3 끝수 × 5
|
||||
assert [x.get("로직") or x["참조"]["키"] for x in one["안"]] == [sp, STONE, TEN]
|
||||
|
||||
class Gone: # STONE 을 지운 마스터
|
||||
@@ -500,7 +505,7 @@ def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world
|
||||
for key in (sc, sp): # 원본이 사라져도 굳힌 사본으로 같은 값
|
||||
(world["system"] / refs.kind_of(key) / f"{key}.json").unlink()
|
||||
got = prices.calc(rows, bundle, {}, {}, _area(given))["줄"][0]
|
||||
assert got["기준"]["계"] == pytest.approx(float(want), rel=1e-9)
|
||||
assert got["기준"]["계"] == pytest.approx(float(want), abs=4) # 751-3 안 줄 셋 · 비목 합 버림
|
||||
|
||||
|
||||
def test_사전조합이_자기_자신을_담으면_422_수량_없음(world):
|
||||
@@ -564,4 +569,4 @@ def test_사전조합_굳힘은_안_로직_알약도_사본_값_142_2(world, mon
|
||||
assert again == pytest.approx(before)
|
||||
with store.at(world["system"]):
|
||||
live = labor.calc(doc, given)["줄"][0]["기준"]["계"]
|
||||
assert live == pytest.approx(before * 10) # 층 셈은 지금 M01(배 1)
|
||||
assert live == pytest.approx(before * 10, abs=30) # 층 셈은 지금 M01(배 1) · 751-3 끝수 × 10
|
||||
|
||||
Reference in New Issue
Block a user