diff --git a/M01_MasterData/M01_MasterData_Router.py b/M01_MasterData/M01_MasterData_Router.py index 5b0aaa349..cd241f9b7 100644 --- a/M01_MasterData/M01_MasterData_Router.py +++ b/M01_MasterData/M01_MasterData_Router.py @@ -234,7 +234,23 @@ def post_calc(body: CalcBody) -> dict: if got is not None: return got with store.cm.mf.mp.choosing(body.고름): - return _call(store.calc, body.key, body.inputs, body.row, body.file) + got = _call(store.calc, body.key, body.inputs, body.row, body.file) + return _pct(got, body) + + +def _pct(got: dict, body: CalcBody) -> dict: + """% 덧줄 표기(149-4) — 줄에 율 · 규격 덧칸(M02 일위대가 하위 행과 같은 도우미 · 금액 그대로).""" + if not got.get("lines"): + return got + from M02_MasterTemplete import M02_Labor_Doc as labor_doc # M02 가 M01 을 부름 — 늦게 + + whole = store.loaded()[1] + try: + logic = body.row or whole.logic(body.key) + except store.cm.mf.FormulaError: + return got + lines = labor_doc.pct_extras(whole, logic, got["lines"], got.get("middle") or {}) + return {**got, "lines": lines} @router.post("/text") diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts index c61432a3f..0dcc8e932 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts @@ -92,7 +92,18 @@ export interface CalcLine { 단위경고?: string; /** 재료 줄이 잡은 품목 — 키 · 이름 · 규격 · 단위 · 값 칸 */ 품목?: { 키: string; 이름?: string; 규격?: string; 단위?: string; 값칸?: string }; + /** % 덧줄(149-4 · 서버 `M02_Labor_Doc.pct_extras`) — 율 · 밑수 글(「노무비의 %」 · 복합 = 빈 글) */ + 율?: number; + 규격?: string; } + +/** % 덧줄 표기(149-4) — 율 있으면 수량 = 율 · 단위 % · 이름 뒤 규격 괄호 · 금액은 그대로(M01 · M02 한 벌) */ +export const pctView = ( + x: Pick, +): { name: string; qty: number; unit: string } => + x.율 == null + ? { name: x.이름, qty: x.수량, unit: x.단위 } + : { name: x.규격 ? `${x.이름} (${x.규격})` : x.이름, qty: x.율, unit: "%" }; export type CalcAnswer = | { ok: true; diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts index 98b79eb28..b5b2da676 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts @@ -7,6 +7,7 @@ import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements"; import { fetchAuto, + pctView, runCalc, runText, searchElements, @@ -226,8 +227,8 @@ export function answerView(answer: CalcAnswer, draft: boolean, money: boolean): children: [ el("span", { text: line.출처 - ? `${line.이름} (${line.출처.replace(/[A-Z]{2}\d{6}\./g, "")})` - : line.이름, + ? `${pctView(line).name} (${line.출처.replace(/[A-Z]{2}\d{6}\./g, "")})` + : pctView(line).name, }), ...(line.단위경고 ? [ @@ -239,7 +240,7 @@ export function answerView(answer: CalcAnswer, draft: boolean, money: boolean): : []), ], }), - el("td", { text: `${formatNumber(line.수량)} ${line.단위}` }), + el("td", { text: `${formatNumber(pctView(line).qty)} ${pctView(line).unit}` }), el("td", { className: "m01-logic__money", text: formatMoney(line.금액) }), ], }), diff --git a/M02_MasterTemplete/M02_Labor_Doc.py b/M02_MasterTemplete/M02_Labor_Doc.py index eb96ae447..faa87a732 100644 --- a/M02_MasterTemplete/M02_Labor_Doc.py +++ b/M02_MasterTemplete/M02_Labor_Doc.py @@ -18,6 +18,8 @@ 고름 = `{호표 차례: MT키}`(엔진 `choosing` 그대로) · 금액은 로직 재료비 안이라 합 · 알약에 안 더함(보기만) · 사용자 자재와 같은 품목 두 번은 [저장] 때 400. 승격(134-1) = 로직 줄 「단가승격」 표시(`true` 만 · 없음 = 단가산출) · 금액 무관 · B09 호표 몫. +% 덧줄(149-4) = 식 「X * Y / 100」 덧줄에 덧칸 `율`(Y) · `규격`(「노무비의 %」 등 · `pct_extras`) — 표기만 · +단위 · 수량 · 금액 그대로(「식 · 1」) · 화면이 율 · % 로 그림 · M01 시험계산 표도 이 함수. """ from __future__ import annotations @@ -442,7 +444,54 @@ def run_line( # 「줄」 = 그 로직 재료 · 노무 · 경비 줄(줄 누름 상세 · 50-4) money = {k: got[k] for k in (*COSTS, "계")} mats = logic_mats(whole, logic, got["줄"], picks) - return {"기준": money, "줄": got["줄"], "재료": mats, "까닭": None} + lines = pct_extras(whole, logic, got["줄"], got.get("중간") or {}) + return {"기준": money, "줄": lines, "재료": mats, "까닭": None} + + +def pct_extras(whole: Any, logic: dict, lines: list[dict], env: dict) -> list[dict]: + """% 덧줄(149-4) — 덧줄 식이 「X * Y / 100」 이면 그 엔진 줄(호표 수 뒤 같은 차례)에 덧칸 + `율` = Y(엔진 마지막 env 로 셈) · `규격` = X 이름(「노무비의 %」 · 인력 줄 합 「인건비의 %」 · + `줄.'이름'` · 중간값 「이름의 %」 · 그 밖 복합 = 빈 글). 다른 덧줄 · 흐름 로직 · 못 셈 = 그대로.""" + if isinstance(logic.get("흐름"), dict): + return lines + out, head = list(lines), len(logic.get("호표") or []) + for n, add in enumerate(logic.get("덧줄") or []): + if head + n >= len(out): + break + try: + node = mf.parse(add.get("식") or "") + except mf.FormulaError: + continue + if not ( + node[0] == "bin" + and node[1] == "/" + and node[3] == ("num", Decimal(100)) + and node[2][0] == "bin" + and node[2][1] == "*" + ): + continue # 금액 식(기계손료 등) · 조건부 % = 「식」 그대로 + base, part = node[2][2], node[2][3] + try: + rate = mf.evaluate(part, dict(env), whole) + except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError): + continue + if isinstance(rate, Decimal): + out[head + n] = {**out[head + n], "율": rate, "규격": _pct_base(base)} + return out + + +def _pct_base(node: tuple) -> str: + """% 덧줄 밑수 이름 — B09 대가(`B09_Estimation_Labor_Source._base_of`)와 같은 말.""" + if node[0] == "name": + return f"{node[1]}의 %" # 노무비 · 재료비 · 경비 · 중간값 이름 + if node[0] == "field" and node[1] == ("name", "줄"): + return f"{node[2]}의 %" + parts = list(mf.walk(node)) + if any(x[0] == "ref" and str(x[1]).startswith("LB") for x in parts) and not any( + x[0] == "name" and x[1] in COSTS for x in parts + ): + return "인건비의 %" + return "" def logic_mats(whole: Any, logic: dict, lines: list[dict], picks: Any) -> list[dict[str, Any]]: diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index adb93289c..ba1edf466 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -13,6 +13,7 @@ * ========================================================================== */ import { createButton, createSelectField, el } from "@ui/ui_template_elements"; +import { pctView } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; import { formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money"; import type { ItemLogic } from "./M02_MasterTemplete_Api_Fetch"; import type { @@ -278,12 +279,15 @@ function subRows(line: LaborCalcLine | undefined, wait: boolean, tools: boolean) cost: x.기준 ? Object.fromEntries(COSTS.map((c) => [c, x.기준![c] * x.수량])) : null, }; }) - : (line?.줄 ?? []).map((x) => ({ - name: x.출처 ? `${x.이름} (${x.출처.replace(/[A-Z]{2}\d{6}\./g, "")})` : x.이름, - qty: x.수량, - unit: x.단위, - cost: { ...x.비목, 계: x.금액 } as Record, - })); + : (line?.줄 ?? []).map((x) => { + const shown = pctView(x); // % 덧줄 = 율 · % · 규격(149-4) + return { + name: x.출처 ? `${shown.name} (${x.출처.replace(/[A-Z]{2}\d{6}\./g, "")})` : shown.name, + qty: shown.qty, + unit: shown.unit, + cost: { ...x.비목, 계: x.금액 } as Record, + }; + }); const blank = (className: string): HTMLElement => el("td", { className, text: "" }); const none = wait ? "…" : (line?.까닭 ?? "—"); return (items.length ? items : [null]).map((x) => diff --git a/resources/tester/test_149_4_pct_extra.py b/resources/tester/test_149_4_pct_extra.py new file mode 100644 index 000000000..d794487a0 --- /dev/null +++ b/resources/tester/test_149_4_pct_extra.py @@ -0,0 +1,59 @@ +"""% 덧줄 표기(PLAN 149-4) — 식 「X * Y / 100」 덧줄 = 덧칸 율 · 규격 · 금액 · 단위 · 수량 그대로. + +- M02 `run_line` → 큰돌쌓기 찰쌓기 제잡비 설치 9 · 안 함 3 · 「노무비의 %」 +- 인력 줄 합 밑수 = 「인건비의 %」 · 호표 줄 밑수 = 「그 줄 이름의 %」 · 금액 식(기계손료) = 덧칸 없음 +- M01 시험계산(`post_calc`) 도 같은 도우미 · 화면 두 곳은 `pctView` 한 벌 +""" + +from decimal import Decimal +from pathlib import Path + +from M01_MasterData import M01_MasterData_Router as m01_router +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Labor_Doc as labor + +ROOT = Path(__file__).resolve().parents[2] + + +def _extra(code: str, given: dict | None = None, name: str | None = None) -> dict: + got = labor.run_line(m01.loaded()[1], code, given or {}) + assert got["까닭"] is None + lines = [x for x in got["줄"] if x.get("단위") == "식" and (name is None or x["이름"] == name)] + assert lines, code + return {**lines[-1], "_기준": got["기준"]} + + +def test_큰돌쌓기_찰쌓기_제잡비_율_설치_9_안함_3_금액_그대로(): + for pipe, rate in (("설치", 9), ("설치 안 함", 3)): + x = _extra("GF000227", {"물빼기파이프": pipe}, "제잡비") + assert x["율"] == Decimal(rate) and x["규격"] == "노무비의 %" + assert x["단위"] == "식" and x["수량"] == 1 # 엔진 줄은 그대로 · 표기는 화면 + labor_sum = x["_기준"]["노무비"] + assert abs(x["금액"] - labor_sum * rate / 100) < Decimal("0.01") + + +def test_인건비_호표줄_밑수_금액식은_덧칸_없음(): + x = _extra("GC000198") + assert x["율"] == 2 and x["규격"] == "인건비의 %" + x = _extra("GF000002", name="잡품") + assert x["규격"] == "휘발유(체인톱)의 %" and x["율"] > 0 + mach = _extra("GC000267", name="기계손료") + assert "율" not in mach and "규격" not in mach + mat = _extra("GC000267", name="잡재료비") + assert mat["규격"] == "재료비의 %" + + +def test_M01_시험계산도_율_규격(): + given = {"물빼기파이프": "설치", "직경": "60", "지역": "전국평균", "보정작업": "해당 없음"} + got = m01_router.post_calc(m01_router.CalcBody(key="GF000227", inputs=given)) + extra = [x for x in got["lines"] if x["이름"] == "제잡비"][0] + assert extra["율"] == 9 and extra["규격"] == "노무비의 %" + + +def test_화면_두_곳_pctView_한_벌(): + api = (ROOT / "M01_MasterData/M01_MasterData_UI_Logic_Api.ts").read_text("utf-8") + assert "x.율 == null" in api and 'qty: x.율, unit: "%"' in api + rows = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts").read_text("utf-8") + assert "const shown = pctView(x);" in rows and "qty: shown.qty," in rows + calc = (ROOT / "M01_MasterData/M01_MasterData_UI_Logic_Calc.ts").read_text("utf-8") + assert "formatNumber(pctView(line).qty)" in calc