- 끝수 한 곳 `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>
144 lines
6.7 KiB
Python
144 lines
6.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""일위대가 조합 — 단가산출 로직을 골라 담은 묶음 (`_틀.md` 10장 · 계약 `_화면_계약.md` 9장).
|
|
|
|
조합은 **담은 로직 목록만** 지님 — 수량 · 비율 · 계산은 두지 않음(프로젝트 몫).
|
|
미리 보기(`run`)는 담은 로직마다 그 자리에서 받은 입력값으로 값을 뽑아
|
|
비목별로 갈라 합침 — 저장하지 않음.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
|
|
import master_formula as mf
|
|
import master_keys as mk
|
|
|
|
GROUP = "일위대가조합"
|
|
FILE = "일위대가조합.json"
|
|
TID = "UA"
|
|
OWNERS = ("현장", "공용")
|
|
MAX_이름 = 60 # 조합 이름 최대 글자 수 — 관리자가 짓는 짧은 이름이라 이보다 길면 저장 막음
|
|
# 줄 열 한 벌 — 차례까지 이대로(`_틀.md` 10장) · 본뜬키 = 갈라 낸 원래 조합 키(없으면 null)
|
|
COLS = ("키", "이름", "구분", "상세구분", "단위", "담은로직", "본뜬키", "출처", "소유", "비고")
|
|
ITEM_COLS = ("로직", "메모")
|
|
|
|
|
|
def rows(files: dict[str, dict]):
|
|
"""(파일 이름, 조합 줄) — 조합 파일은 하나지만 그룹으로 찾음."""
|
|
for name, data in files.items():
|
|
if data.get("그룹") == GROUP:
|
|
for row in data.get("줄") or []:
|
|
yield name, row
|
|
|
|
|
|
def items(row: dict) -> list[dict]:
|
|
"""담은 로직 줄 목록 — 차례가 곧 조합 안 차례."""
|
|
got = row.get("담은로직")
|
|
return got if isinstance(got, list) else []
|
|
|
|
|
|
def logic_keys(row: dict) -> list[str]:
|
|
return [str(one.get("로직") or "") for one in items(row) if isinstance(one, dict)]
|
|
|
|
|
|
# ── 검사 ──────────────────────────────────────────────────────────────
|
|
def check_row(whole: mf.Master, row: dict) -> list[str]:
|
|
"""조합 한 줄 — 빈 조합 · 없는 로직 키 · 같은 로직 두 번 · 조합을 담음(1단만) · 메모 모양."""
|
|
where = f"{row.get('키')} {row.get('이름') or ''}".strip()
|
|
got = items(row)
|
|
out = [] if got else [f"{where} · 빈 조합 — 담은 로직이 없음"]
|
|
if row.get("담은로직") is not None and not isinstance(row.get("담은로직"), list):
|
|
out.append(f"{where} · 「담은로직」 이 목록 아님")
|
|
seen: set[str] = set()
|
|
for at, one in enumerate(got, start=1):
|
|
if not isinstance(one, dict):
|
|
out.append(f"{where} 줄{at} · 담은 로직 줄이 묶음 아님 「{one}」")
|
|
continue
|
|
extra = set(one) - set(ITEM_COLS)
|
|
if extra:
|
|
out.append(f"{where} 줄{at} · 모르는 칸 {sorted(extra)}")
|
|
memo = one.get("메모")
|
|
if memo is not None and not isinstance(memo, str):
|
|
out.append(f"{where} 줄{at} · 「메모」 가 글자 아님 「{memo}」 — 수량·계산은 두지 않음")
|
|
key = str(one.get("로직") or "")
|
|
if not mk.KEY.fullmatch(key):
|
|
out.append(f"{where} 줄{at} · 로직 키 모양 아님 「{key}」")
|
|
continue
|
|
if mk.id_of(key) == TID:
|
|
out.append(f"{where} 줄{at} · 조합은 조합을 담지 못함 「{key}」 — 단가산출 로직만")
|
|
continue
|
|
try:
|
|
whole.logic(key)
|
|
except mf.FormulaError as e:
|
|
out.append(f"{where} 줄{at} · {e}")
|
|
continue
|
|
if key in seen:
|
|
out.append(f"{where} 줄{at} · 같은 로직을 두 번 담음 「{key}」")
|
|
seen.add(key)
|
|
return out
|
|
|
|
|
|
def check_all(files: dict[str, dict], whole: mf.Master) -> list[str]:
|
|
"""조합 전부 — 파일 이름을 앞에 붙임."""
|
|
return [f"{name} · {x}" for name, row in rows(files) for x in check_row(whole, row)]
|
|
|
|
|
|
def using(files: dict[str, dict], key: str) -> list[dict]:
|
|
"""「이 로직을 쓰는 조합」 — 소요량 「용도」 와 결은 같되 자리가 다름.
|
|
|
|
조합은 관리자가 그때그때 만드는 것이라 로직 줄에 칸으로 박지 않고 물을 때 훑어 찾음
|
|
(박아 두면 조합을 고칠 때마다 낡음).
|
|
"""
|
|
out = []
|
|
for _, row in rows(files):
|
|
at = [i for i, k in enumerate(logic_keys(row), start=1) if k == key]
|
|
if at:
|
|
out.append(
|
|
{
|
|
**{k: row.get(k) for k in ("키", "이름", "구분", "상세구분", "단위", "소유")},
|
|
"차례": at,
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
# ── 미리 보기 ─────────────────────────────────────────────────────────
|
|
def run(master: mf.Master, row: dict, given: dict[str, dict] | None = None) -> dict:
|
|
"""담은 로직마다 그 자리의 입력값으로 값을 뽑아 비목별로 갈라 합침 — 저장하지 않음.
|
|
|
|
`given` = {로직 키: {입력 이름: 값}} · 못 도는 줄은 까닭을 적고 합계에서 뺌(통째로 멈추지 않음).
|
|
돈이 아닌 로직(결과 수 하나)은 비목이 없어 합계에 얹지 않고 결과만 보임.
|
|
"""
|
|
given = given or {}
|
|
sums = dict.fromkeys(mf.COST_ITEMS, Decimal(0))
|
|
lines, stuck = [], []
|
|
for at, one in enumerate(items(row), start=1):
|
|
key = str(one.get("로직") or "") if isinstance(one, dict) else ""
|
|
head = {"차례": at, "로직": key, "메모": one.get("메모") if isinstance(one, dict) else None}
|
|
try:
|
|
target = master.logic(key)
|
|
except mf.FormulaError as e:
|
|
lines.append({**head, "ok": False, "까닭": str(e)})
|
|
stuck.append(f"{key} · {e}")
|
|
continue
|
|
head.update(
|
|
{k: target.get(k) for k in ("이름", "원문번호", "결과단위", "구분", "상세구분", "출처")}
|
|
)
|
|
try:
|
|
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})
|
|
stuck.append(f"{key} {target.get('이름')} · {reason}")
|
|
continue
|
|
if "결과" in got: # 돈 아닌 로직 — 비목이 없어 합계에 안 얹음
|
|
lines.append({**head, "ok": True, "갈래": "결과", "결과": got["결과"]})
|
|
continue
|
|
part = {k: got[k] for k in mf.COST_ITEMS}
|
|
for k, v in part.items():
|
|
sums[k] += v
|
|
lines.append({**head, "ok": True, "갈래": "돈", **part, "계": got["계"]})
|
|
return {"줄": lines, **sums, "계": sum(sums.values()), "멈춤": stuck}
|