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:
@@ -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) 줄 ── */
|
||||
|
||||
Reference in New Issue
Block a user