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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user