Merge remote-tracking branch 'origin/sub_laptop_1' into sub_laptop_2
This commit is contained in:
@@ -17,6 +17,9 @@
|
||||
끝수 = 엑셀 자리 그대로 — 줄 금액 = ROUNDDOWN(단가 × 수량, 1) ·
|
||||
% 줄 = ROUNDDOWN(밑수 × % / 100, 1) · 관급 · 별도계상 자재 = 대가 안 0원 ·
|
||||
블록 비목 합 = ROUNDDOWN(Σ, 0) · 합계 = 세 비목 합.
|
||||
새 의미 로직 블록(792-4 · `B09_Estimation_Labor_Rule`) = 줄 `place` {방법, 자리}(그 끝수) ·
|
||||
줄 `out`(비목 합 · `part:` 밑에서 뺌) · 블록 `rule` {끝전 큰나머지, 끝수, 조정}(`settle` ·
|
||||
엔진 `master_cut.settle` 과 같은 차례).
|
||||
빈칸 = 단가 · 수량을 낸 원천이 없으면 None · 「연결 없음」 목록(gaps).
|
||||
빈 줄(166-6) = 블록 합 = 아는 줄 합 + `blank` n · `blank_names` — 내역 단가 · 부른 대가로는 안 넘김 ·
|
||||
줄 0 개 블록 = 빈 줄 하나(`NO_LINES`) · 줄 `ask`(723-4 SC 안 줄 수량 없음) = 수량이 비면 `why`.
|
||||
@@ -33,12 +36,22 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import SUPPLY, gap, num, rounddown, total
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import (
|
||||
SUPPLY,
|
||||
gap,
|
||||
num,
|
||||
round_half,
|
||||
rounddown,
|
||||
roundup,
|
||||
total,
|
||||
)
|
||||
|
||||
PARTS = ("labor", "material", "expense")
|
||||
COVER_KEYS = ("qty", *PARTS)
|
||||
TAB = "dg"
|
||||
NO_LINES = "구성 줄 없음" # 줄 0 개 일위대가의 빈 줄 이름
|
||||
COST = {"노무비": "labor", "재료비": "material", "경비": "expense"}
|
||||
WAYS = {"버림": rounddown, "올림": roundup, "반올림": round_half}
|
||||
COPY = "@copy"
|
||||
ADD = "@add"
|
||||
WHY = {
|
||||
@@ -50,6 +63,35 @@ WHY = {
|
||||
}
|
||||
|
||||
|
||||
def cut(value: float, place: dict | None) -> float:
|
||||
"""줄 금액 끝수 — `place` {방법, 자리}(792-4) · 없으면 ROUNDDOWN 1."""
|
||||
if not place:
|
||||
return rounddown(value, 1)
|
||||
return WAYS[place.get("방법")](value, int(place.get("자리") or 0))
|
||||
|
||||
|
||||
def settle(raw: dict[str, float], rule: dict | None) -> dict[str, float]:
|
||||
"""블록 비목 합 — 비목마다 ROUNDDOWN 0 · `rule`(792-4) = 큰나머지(계 원 버림 뒤 모자란 원을
|
||||
소수 큰 비목부터 · 같으면 경 › 재 › 노) → `끝수`(대상 계) → `조정`(배율 · 옮김) → 원 버림."""
|
||||
parts = {p: rounddown(v, 0) for p, v in raw.items()}
|
||||
if not rule:
|
||||
return parts
|
||||
if rule.get("끝전") == "큰나머지":
|
||||
rest = int(rounddown(sum(raw.values()), 0) - sum(parts.values()))
|
||||
for p in sorted(PARTS, key=lambda p: (parts[p] - raw[p], -PARTS.index(p)))[:rest]:
|
||||
parts[p] += 1
|
||||
end = rule.get("끝수")
|
||||
if isinstance(end, dict) and end.get("대상") == "계":
|
||||
parts = {p: cut(v, end) for p, v in parts.items()}
|
||||
adjust = rule.get("조정") or {}
|
||||
for name, rate in (adjust.get("배율") or {}).items():
|
||||
parts[COST[name]] *= float(rate)
|
||||
if adjust.get("옮김"):
|
||||
whole = sum(parts.values())
|
||||
parts = {p: whole if p == COST[adjust["옮김"]] else 0.0 for p in PARTS}
|
||||
return {p: rounddown(v, 0) for p, v in parts.items()}
|
||||
|
||||
|
||||
def _prices(line: dict[str, Any], books: dict[str, Any], sums: dict) -> dict[str, float | None]:
|
||||
"""줄 비목 단가 — 그 비목이 없는 줄은 0 · 원천이 없으면 None."""
|
||||
kind, key = line["kind"], line.get("key")
|
||||
@@ -97,7 +139,7 @@ def _base(spec: str, lines: list[dict[str, Any]], src: list) -> float | None:
|
||||
`rows:0,2@labor` 블록 그 차례 줄(`src` cid · `@비목` = 그 몫) 합(778-4 · 짝 `.ts` `pctBase`)."""
|
||||
kind, _, name = spec.partition(":")
|
||||
if kind == "part":
|
||||
return total([ln["amounts"][name] for ln in lines])
|
||||
return total([ln["amounts"][name] for ln in lines if not ln.get("out")])
|
||||
if kind == "rows": # `3@labor` = 그 줄 그 비목 몫
|
||||
out: list[float | None] = []
|
||||
for one in dict.fromkeys(name.split(",")):
|
||||
@@ -151,7 +193,7 @@ def _block(
|
||||
continue
|
||||
qty, prices, extra = _cover(line, own, num(line.get("qty")), _prices(line, books, sums))
|
||||
amounts = {
|
||||
part: None if p is None or qty is None else rounddown(p * qty, 1)
|
||||
part: None if p is None or qty is None else cut(p * qty, line.get("place"))
|
||||
for part, p in prices.items()
|
||||
}
|
||||
ref = {"ref": {"key": line["key"]}} if line.get("key") else {} # 집계표(145-5) 이음
|
||||
@@ -177,7 +219,9 @@ def _block(
|
||||
amounts = dict.fromkeys(PARTS, 0.0)
|
||||
part = line.get("part") or "expense"
|
||||
prices[part] = base
|
||||
amounts[part] = None if base is None or pct is None else rounddown(base * pct / 100, 1)
|
||||
amounts[part] = (
|
||||
None if base is None or pct is None else cut(base * pct / 100, line.get("place"))
|
||||
)
|
||||
lines.append({**line, "qty": pct, "prices": prices, "amounts": amounts, **extra})
|
||||
if amounts[part] is None:
|
||||
gaps.append(
|
||||
@@ -186,8 +230,9 @@ def _block(
|
||||
for line in lines:
|
||||
line["price"] = total(list(line["prices"].values()))
|
||||
line["amount"] = total(list(line["amounts"].values()))
|
||||
known = [ln for ln in lines if ln["amount"] is not None]
|
||||
parts = {part: rounddown(sum(ln["amounts"][part] for ln in known), 0) for part in PARTS}
|
||||
known = [ln for ln in lines if ln["amount"] is not None and not ln.get("out")]
|
||||
parts = settle({part: sum(ln["amounts"][part] for ln in known) for part in PARTS},
|
||||
block.get("rule")) # fmt: skip
|
||||
blank_names = [ln.get("name") or "" for ln in lines if ln["amount"] is None]
|
||||
if not lines: # 줄 0 개 일위대가 = 금액 0 이 아니라 모름(빈 줄)
|
||||
blank_names = [NO_LINES]
|
||||
|
||||
@@ -21,12 +21,16 @@
|
||||
* 복사본(703-2) = 덮개 K:… 의 @copy {from · name · spec} — 원본 cover 블록을 본뜬 새 호표
|
||||
* (id DG:K:… · 번호 = 끝 · 줄 · 기본값 = 원본 · 덮개 = 제 것) · 원본 없음 = 짝 잃음 {cover, cid: @copy}
|
||||
* 붙인 줄(705-2) = 덮개 @add [블록 줄 fixed · pct · cid @add|…] — 그 cover 블록 끝에 더함(복사본 앞 · 뒤 · 같은 cid 한 번)
|
||||
* 새 의미 로직 블록(792-4) = 줄 place {방법, 자리}(그 끝수) · 줄 out(비목 합 · part: 밑에서 뺌) ·
|
||||
* 블록 rule {끝전 큰나머지, 끝수, 조정}(settle · 짝 `.py` settle)
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
gap,
|
||||
num,
|
||||
roundHalf,
|
||||
rounddown,
|
||||
roundup,
|
||||
SUPPLY,
|
||||
total,
|
||||
type Gap,
|
||||
@@ -50,6 +54,49 @@ const WHY: Record<string, string> = {
|
||||
};
|
||||
|
||||
type Parts = Record<(typeof PARTS)[number], Num>;
|
||||
type Part = (typeof PARTS)[number];
|
||||
const COST: Record<string, Part> = { 노무비: "labor", 재료비: "material", 경비: "expense" };
|
||||
const WAYS: Record<string, (v: number, p?: number) => number> = {
|
||||
버림: rounddown,
|
||||
올림: roundup,
|
||||
반올림: roundHalf,
|
||||
};
|
||||
|
||||
/** 줄 금액 끝수 — place {방법, 자리}(792-4) · 없으면 ROUNDDOWN 1(짝 `cut`) */
|
||||
export function cut(value: number, place: Row | null | undefined): number {
|
||||
if (!place) return rounddown(value, 1);
|
||||
return WAYS[place.방법](value, Number(place.자리 || 0));
|
||||
}
|
||||
|
||||
/** 블록 비목 합 — 비목마다 ROUNDDOWN 0 · rule(792-4) = 큰나머지 → 끝수(대상 계) → 조정 → 원 버림
|
||||
* (짝 `settle`) */
|
||||
export function settle(
|
||||
raw: Record<Part, number>,
|
||||
rule: Row | null | undefined,
|
||||
): Record<Part, number> {
|
||||
const parts = { ...raw };
|
||||
for (const p of PARTS) parts[p] = rounddown(raw[p], 0);
|
||||
if (!rule) return parts;
|
||||
const sum = (o: Record<Part, number>): number => PARTS.reduce((acc, p) => acc + o[p], 0);
|
||||
if (rule.끝전 === "큰나머지") {
|
||||
const rest = rounddown(sum(raw), 0) - sum(parts);
|
||||
const order = [...PARTS].sort(
|
||||
(a, b) => parts[a] - raw[a] - (parts[b] - raw[b]) || PARTS.indexOf(b) - PARTS.indexOf(a),
|
||||
);
|
||||
for (const p of order.slice(0, rest)) parts[p] += 1;
|
||||
}
|
||||
const end: Row | undefined = rule.끝수;
|
||||
if (end && typeof end === "object" && end.대상 === "계")
|
||||
for (const p of PARTS) parts[p] = cut(parts[p], end);
|
||||
const adjust: Row = rule.조정 || {};
|
||||
for (const [name, rate] of Object.entries(adjust.배율 || {})) parts[COST[name]] *= Number(rate);
|
||||
if (adjust.옮김) {
|
||||
const whole = sum(parts);
|
||||
for (const p of PARTS) parts[p] = p === COST[adjust.옮김] ? whole : 0;
|
||||
}
|
||||
for (const p of PARTS) parts[p] = rounddown(parts[p], 0);
|
||||
return parts;
|
||||
}
|
||||
|
||||
const zeros = (): Parts => ({ labor: 0, material: 0, expense: 0 });
|
||||
const blanks = (): Parts => ({ labor: null, material: null, expense: null });
|
||||
@@ -96,7 +143,7 @@ function pctBase(spec: string, lines: Row[], src: Row[]): Num {
|
||||
const kind = spec.slice(0, at);
|
||||
const name = spec.slice(at + 1);
|
||||
const whole = (ln: Row): Num => total(PARTS.map((p) => ln.amounts[p]));
|
||||
if (kind === "part") return total(lines.map((ln) => ln.amounts[name]));
|
||||
if (kind === "part") return total(lines.filter((ln) => !ln.out).map((ln) => ln.amounts[name]));
|
||||
if (kind === "rows") {
|
||||
// `3@labor` = 그 줄 그 비목 몫
|
||||
const out: Num[] = [];
|
||||
@@ -139,7 +186,7 @@ function block(b: Row, books: Row, sums: Record<string, Row>, gaps: Gap[], own:
|
||||
const amounts = blanks();
|
||||
for (const part of PARTS) {
|
||||
const p = prices[part];
|
||||
amounts[part] = p === null || qty === null ? null : rounddown(p * qty, 1);
|
||||
amounts[part] = p === null || qty === null ? null : cut(p * qty, line.place);
|
||||
}
|
||||
const ref = line.key ? { ref: { key: line.key } } : {}; // 집계표(145-5) 이음
|
||||
const ask: Row = qty === null && line.ask ? { why: line.ask } : {}; // 723-4
|
||||
@@ -160,7 +207,7 @@ function block(b: Row, books: Row, sums: Record<string, Row>, gaps: Gap[], own:
|
||||
const amounts = zeros();
|
||||
const part = (line.part || "expense") as (typeof PARTS)[number];
|
||||
prices[part] = base;
|
||||
amounts[part] = base === null || pct === null ? null : rounddown((base * pct) / 100, 1);
|
||||
amounts[part] = base === null || pct === null ? null : cut((base * pct) / 100, line.place);
|
||||
lines.push({ ...line, qty: pct, prices, amounts, ...extra });
|
||||
if (amounts[part] === null)
|
||||
gaps.push(gap(TAB, b.id, `${b.name} · ${line.name}`, "% 밑수 없음"));
|
||||
@@ -169,13 +216,10 @@ function block(b: Row, books: Row, sums: Record<string, Row>, gaps: Gap[], own:
|
||||
line.price = total(PARTS.map((p) => line.prices[p]));
|
||||
line.amount = total(PARTS.map((p) => line.amounts[p]));
|
||||
}
|
||||
const known = lines.filter((ln) => ln.amount !== null);
|
||||
const parts = zeros();
|
||||
for (const part of PARTS)
|
||||
parts[part] = rounddown(
|
||||
known.reduce((acc, ln) => acc + ln.amounts[part], 0),
|
||||
0,
|
||||
);
|
||||
const known = lines.filter((ln) => ln.amount !== null && !ln.out);
|
||||
const raw = { labor: 0, material: 0, expense: 0 };
|
||||
for (const part of PARTS) raw[part] = known.reduce((acc, ln) => acc + ln.amounts[part], 0);
|
||||
const parts = settle(raw, b.rule);
|
||||
const blankNames = lines.length
|
||||
? lines.filter((ln) => ln.amount === null).map((ln) => ln.name || "")
|
||||
: [NO_LINES]; // 줄 0 개 일위대가 = 금액 0 이 아니라 모름(빈 줄)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"""B09 대가 블록 셈법(PLAN 792-4) — 791-1 엔진 새 의미(줄 자리 · 합밖 · 번지 · 단계 · 블록 끝전 ·
|
||||
결과 조정)를 쓰는 로직만 블록 줄 · 블록에 표지를 붙임(`B09_Estimation_Labor_Source._poomsem`):
|
||||
|
||||
줄 `place` {방법, 자리} 줄 금액 끝수(없으면 ROUNDDOWN 1)
|
||||
줄 `out` true 비목 합에서 뺌(합밖 줄 · 제외 단계가 담은 줄 · 번지 · 단계엔 쌓임)
|
||||
덧줄 = 수 하나 × 밑 합 꼴이면 % 줄 밑 `rows:`(그 줄 차례 · `@비목` · % = 그 수 × 100) —
|
||||
밑 = `줄.'이름'` · 앞 덧줄 이름 · `번지.X.비목` · `단계.'이름'.비목` · `노무비` 등(앞 줄 합)
|
||||
덧줄 = 이름 없는 수 식 = 고정 줄(수량 1 · 엔진 값)
|
||||
블록 `rule` {끝전?, 조정?, 끝수?} 블록 비목 합 = `B09_Estimation_Calc_Labor.settle`
|
||||
그 밖 꼴(밑마다 다른 수 · 곱 · 중간 이름) = 빈 줄 그대로(대가 단가로 못 셈 · 틀린 값 대신 빈칸).
|
||||
|
||||
셈 = 셈 짝(`B09_Estimation_Calc_Labor` .py · .ts)이 B09 단가로 · 옛 로직(새 칸 없음) = 표지 없음 ·
|
||||
지킴 시험 = `test_792_3_engine_paths.py`(엔진 · SK · SC · B09 산근 · B09 대가 다섯 길 같은 금액).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as _m01 # noqa: F401 — scripts 길을 둠
|
||||
|
||||
mf = importlib.import_module("master_formula")
|
||||
ms = importlib.import_module("master_formula_sum")
|
||||
|
||||
PART = {"노무비": "labor", "재료비": "material", "경비": "expense"}
|
||||
FLAGS = ("place", "out") # 블록 줄 덧칸 — 단가산출 블록이 줄을 바꿔도 이어 받음
|
||||
Refs = list[tuple[int, str | None]] # (줄 차례, 비목 | None = 줄 전체)
|
||||
|
||||
|
||||
def _f(value: Any) -> float | None:
|
||||
try:
|
||||
return None if value is None else float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _cost(rows: list[int], cost: str) -> Refs | None:
|
||||
if cost == "계":
|
||||
return [(i, None) for i in rows]
|
||||
return [(i, PART[cost]) for i in rows] if cost in PART else None
|
||||
|
||||
|
||||
def _ref(row: dict, node: tuple, n: int) -> Refs | None:
|
||||
"""밑 이름 하나 → 줄 차례(n 앞만) · 모름 = None."""
|
||||
lines = ms.items(row)
|
||||
names = {x.get("이름"): i for i, x in reversed(list(enumerate(lines[:n]))) if x.get("이름")}
|
||||
if node[0] == "name":
|
||||
if node[1] in PART: # 비목 합 = 앞 줄 중 합에 드는 줄
|
||||
skip = ms.skipped(row)
|
||||
return [(i, PART[node[1]]) for i in range(n) if i not in skip]
|
||||
return [(names[node[1]], None)] if node[1] in names else None
|
||||
if node[0] != "field":
|
||||
return None
|
||||
head, name = node[1], node[2]
|
||||
if head == ("name", "줄"):
|
||||
return [(names[name], None)] if name in names else None
|
||||
if head[0] != "field" or head[1][0] != "name":
|
||||
return None
|
||||
group, key = head[1][1], head[2]
|
||||
if group == ms.ADDR:
|
||||
return _cost([i for i, x in enumerate(lines[:n]) if x.get(ms.ADDR) == key], name)
|
||||
if group == ms.STEP:
|
||||
got = next((mine for st, mine in ms._cover(row) if st.get("이름") == key), None)
|
||||
return None if got is None else _cost(sorted(i for i in got if i < n), name)
|
||||
return None
|
||||
|
||||
|
||||
def _linear(row: dict, node: tuple, n: int) -> tuple[Refs, Decimal] | None:
|
||||
"""식 = 수 하나 × 밑 합 → (밑, 수) · 다른 꼴 = None."""
|
||||
if node[0] in ("name", "field"):
|
||||
got = _ref(row, node, n)
|
||||
return None if got is None else (got, Decimal(1))
|
||||
if node[0] != "bin":
|
||||
return None
|
||||
op, a, b = node[1], node[2], node[3]
|
||||
if op in ("*", "/") and b[0] == "num" and (left := _linear(row, a, n)):
|
||||
return left[0], left[1] * b[1] if op == "*" else left[1] / b[1]
|
||||
if op == "*" and a[0] == "num" and (right := _linear(row, b, n)):
|
||||
return right[0], a[1] * right[1]
|
||||
if op == "+" and (left := _linear(row, a, n)) and (right := _linear(row, b, n)):
|
||||
return (left[0] + right[0], left[1]) if left[1] == right[1] else None
|
||||
return None
|
||||
|
||||
|
||||
def _pct(row: dict, formula: str, n: int) -> tuple[str, float] | None:
|
||||
"""덧줄 식 → (`rows:` 밑, %) · 못 바꿈 = None(밑 없음 · 겹친 밑 포함)."""
|
||||
try:
|
||||
got = _linear(row, mf.parse(formula), n)
|
||||
except mf.FormulaError:
|
||||
return None
|
||||
if not got or not got[0] or len(set(got[0])) != len(got[0]):
|
||||
return None
|
||||
spec = ",".join(str(i) if part is None else f"{i}@{part}" for i, part in got[0])
|
||||
return f"rows:{spec}", float(got[1] * 100)
|
||||
|
||||
|
||||
def mark(row: dict, lines: list[dict], engine: list[dict]) -> dict | None:
|
||||
"""새 의미 로직 = 블록 줄에 `place` · `out` · 덧줄 % 밑 · 상수 덧줄(제자리) → 블록 `rule` ·
|
||||
옛 로직 = None."""
|
||||
if not ms.uses(row):
|
||||
return None
|
||||
items, skip, heads = ms.items(row), ms.skipped(row), len(row.get("호표") or [])
|
||||
for n, (item, line) in enumerate(zip(items, lines)):
|
||||
if item.get("자리"):
|
||||
line["place"] = dict(item["자리"])
|
||||
if n in skip:
|
||||
line["out"] = True
|
||||
if n < heads:
|
||||
continue
|
||||
formula = str(item.get("식") or "")
|
||||
part = PART.get(str(item.get("비목")), "expense")
|
||||
try:
|
||||
plain = _plain(mf.parse(formula))
|
||||
except mf.FormulaError:
|
||||
continue
|
||||
value = _f(engine[n].get("금액")) if n < len(engine) else None
|
||||
if plain and value is not None: # 이름 없는 수 식 = 고정 줄(수량 1)
|
||||
line.update(kind="fixed", qty=1.0, own={part: value}, link={}, unit="식")
|
||||
elif got := _pct(row, formula, n):
|
||||
line.update(kind="pct", part=part, base=got[0], spec=formula, unit="%", qty=got[1])
|
||||
else:
|
||||
continue
|
||||
line.pop("why", None)
|
||||
return {k: row[k] for k in ("끝전", "조정", "끝수") if k in row}
|
||||
|
||||
|
||||
def _plain(node: tuple) -> bool:
|
||||
"""수 · 사칙 · 부호만 있는 식(이름 없음)."""
|
||||
if node[0] == "num":
|
||||
return True
|
||||
if node[0] == "bin":
|
||||
return _plain(node[2]) and _plain(node[3])
|
||||
return node[0] == "neg" and _plain(node[1])
|
||||
@@ -34,6 +34,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from B09_Estimation import B09_Estimation_Bill_Library as library
|
||||
from B09_Estimation import B09_Estimation_Labor_Rule as rule
|
||||
from B09_Estimation import B09_Estimation_Sangeun as sg
|
||||
from B09_Estimation import B09_Estimation_Sangeun_Text as st
|
||||
from B09_Estimation.B09_Estimation_Bill_Items import LAYER_WORD
|
||||
@@ -149,6 +150,7 @@ class Builder:
|
||||
for line in lines:
|
||||
if line["kind"] == "machine":
|
||||
line["spec"] = (eq.get(line["key"]) or {}).get("규격") or ""
|
||||
law = rule.mark(logic, lines, engine) # 새 의미 로직 = 줄 자리 · 합밖 · 번지 · 끝전(792-4)
|
||||
unit = str(logic.get("결과단위") or "").replace("원/", "")
|
||||
spec = " · ".join(f"{k} {v}" for k, v in sorted(given.items()))
|
||||
shown = " · ".join( # 157-4 기계 · 직종 원문번호 = 「이름 규격」(번호 = 화면 풍선 spec_raw)
|
||||
@@ -164,6 +166,7 @@ class Builder:
|
||||
"cite": logic.get("출처") or "",
|
||||
"origin": {"로직": code, "입력값": given, "켜쌓기율": rate, "고름": picks},
|
||||
"lines": lines,
|
||||
**({"rule": law} if law else {}),
|
||||
"_engine": engine,
|
||||
"_items": items,
|
||||
}
|
||||
@@ -205,7 +208,7 @@ class Builder:
|
||||
lines = []
|
||||
for n, (line, x) in enumerate(zip(block["lines"], block.pop("_engine"))):
|
||||
item = block["_items"][n] if n < len(block["_items"]) else {}
|
||||
qty = _f(x.get("수량"))
|
||||
qty, keep = _f(x.get("수량")), {k: line[k] for k in rule.FLAGS if k in line}
|
||||
head = {"name": line.get("name") or "", "spec": "", "unit": line.get("unit") or ""}
|
||||
if isinstance(item.get("참조"), dict) and depth < refs.DEPTH:
|
||||
line = {**self.line_of(whole, item, qty, 1, head["name"], copies, depth), **head}
|
||||
@@ -215,7 +218,7 @@ class Builder:
|
||||
own = {PART[k]: _f(v) / qty for k, v in (x.get("비목") or {}).items() if k in PART}
|
||||
line = {**head, "kind": "fixed", "qty": qty, "own": own, "link": {}}
|
||||
poom = was.get(ids[n]) if n < len(ids) else None
|
||||
lines.append({**line, "sk": True, **({"poom": poom} if poom else {})})
|
||||
lines.append({**line, **keep, "sk": True, **({"poom": poom} if poom else {})})
|
||||
block.pop("_items")
|
||||
name = doc.get("이름") or block["name"]
|
||||
origin = {**block["origin"], "단가산출": rid}
|
||||
|
||||
@@ -269,7 +269,8 @@ def _material_key(whole: Any, item: dict, ref: Any, env: dict, n: int) -> tuple[
|
||||
|
||||
|
||||
def _logic_lines(whole, item, env, qty_text, q, price_of) -> list[tuple[str, dict]]:
|
||||
"""로직 부르기 줄 — 기계경비 = 중기 노 · 재 · 경 세 줄 · 그 밖 = 하위 산근 한 줄."""
|
||||
"""로직 부르기 줄 — 기계경비 = 중기 노 · 재 · 경 세 줄 · 그 밖 = 하위 산근 한 줄 ·
|
||||
금액 끝수 = 줄 `자리`(792-4 · `Sangeun_Sum.places` 와 같음)."""
|
||||
node = mf.parse(item["요소"])
|
||||
called = node[1] if node and node[0] == "logic" else None
|
||||
try:
|
||||
@@ -284,7 +285,9 @@ def _logic_lines(whole, item, env, qty_text, q, price_of) -> list[tuple[str, dic
|
||||
return [(f"{name} : {NO_PRICE} * {qty_text}", dict.fromkeys(sg.PARTS))]
|
||||
out = []
|
||||
for part in sg.PARTS:
|
||||
text, money = _line(f"{name} {PART_NAME[part]}", got.get(part), qty_text, q)
|
||||
text, money = _line(
|
||||
f"{name} {PART_NAME[part]}", got.get(part), qty_text, q, item.get("자리")
|
||||
)
|
||||
out.append((text, {part: money}))
|
||||
return out
|
||||
if not target:
|
||||
@@ -294,7 +297,7 @@ def _logic_lines(whole, item, env, qty_text, q, price_of) -> list[tuple[str, dic
|
||||
parts = {p: got.get(p) for p in sg.PARTS}
|
||||
if any(v is None for v in parts.values()):
|
||||
return [(f"{name} : {NO_PRICE} * {qty_text}", dict.fromkeys(sg.PARTS))]
|
||||
amounts = {p: sg._money(float(v), q) for p, v in parts.items()}
|
||||
amounts = {p: ss._cut(float(v) * q, item.get("자리")) for p, v in parts.items()}
|
||||
head = f"산근 {got['no']}호표 " if got.get("no") else ""
|
||||
unit = sum(float(v) for v in parts.values())
|
||||
text = f"{name} : {head}{_price(unit)} * {qty_text} = {_money(sum(amounts.values()))}"
|
||||
|
||||
@@ -52,7 +52,9 @@ def test_fill_ratio_and_no_write():
|
||||
]
|
||||
unit = sum(1 for b in called if b["갈래"] == "단위")
|
||||
from_body = sum(1 for b in called if str(b["기준근거"] or "").startswith("원문"))
|
||||
assert unit / len(called) >= 0.78 # 로직이 부르는 표 2,023 중 1,601(79 %)
|
||||
# 로직이 부르는 표 2,117 중 1,626(76.8 %) — 712-3 때 2,023 중 1,601(79 %) · 782 계수 표 94 중
|
||||
# 단위 기준 25(시공능력 · 적용기준 계수 = 단위 기준 없음) · 단위 수가 줄면 잡음
|
||||
assert unit >= 1601 and unit / len(called) >= 0.76
|
||||
assert from_body >= 280 # 빈 기준 579 중 원문 머리로 채움 285
|
||||
assert json.dumps(files, default=str, ensure_ascii=False) == before # 읽기만
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
"""792-3 엔진 새 의미 네 길 지킴 시험 — 같은 로직 JSON 이 ① 엔진 직접(`master_cut.engine`) ·
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""792-3 · 792-4 엔진 새 의미 다섯 길 지킴 시험 — 같은 로직 JSON 이
|
||||
① 엔진 직접(`master_cut.engine`) ·
|
||||
② M02 단가산출 SK 셈(`M02_Sangeun_Calc.run`) · ③ 일위대가 SC 한 줄(수량 1 · `M02_Labor_Doc.calc`) ·
|
||||
④ B09 산근 풀이(`B09_Estimation_Sangeun.Solver` · 단가 = 같은 값의 B09 목록표)에서 같은 금액.
|
||||
④ B09 산근 풀이(`B09_Estimation_Sangeun.Solver` · 단가 = 같은 값의 B09 목록표) ·
|
||||
⑤ B09 일위대가 대가 블록(SC → 굳힌 SK 참조 수량 1 · `Labor_Source.Builder` → `Calc_Labor` 파이썬 ·
|
||||
TS 짝 · 내역 단가 `daega_price`)에서 같은 금액.
|
||||
실무 견본(봉화 D00102 · 북부청임도 18)은 793-2 길(가져온 식 SK · 임시 층)로 만들어 ⑤ = 엑셀.
|
||||
|
||||
견본 = 791-1 실무 손 옮김 넷(봉화 D00102 · 울진소광 D01489 · 동부청영월 D00003 · 북부청임도 18 =
|
||||
엑셀 금액) + 합밖 · 제외 · 조정 단위 로직. 켜쌓기율 · 별도계상 뒤 다시 버림(`master_cut.plain`)도
|
||||
@@ -9,15 +14,21 @@
|
||||
|
||||
import copy
|
||||
import importlib
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
import resources.tester.test_791_1_engine_semantics as t
|
||||
from B09_Estimation import B09_Estimation_Calc_Labor as dcalc
|
||||
from B09_Estimation import B09_Estimation_Labor_Source as dsrc
|
||||
from B09_Estimation import B09_Estimation_Sangeun as sg
|
||||
from M02_MasterTemplete import M02_Item_Sangeun as item_sk
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as sc
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 임시 층
|
||||
|
||||
mcut = importlib.import_module("master_cut")
|
||||
D = Decimal
|
||||
@@ -26,6 +37,10 @@ PART = {"labor": "노무비", "material": "재료비", "expense": "경비", "sum
|
||||
EQ = "EQ990001"
|
||||
|
||||
|
||||
def ms_uses(row) -> bool:
|
||||
return importlib.import_module("master_formula_sum").uses(row)
|
||||
|
||||
|
||||
def _units():
|
||||
"""합밖 · 제외 단계 · 조정 · 큰나머지 · 줄 자리를 한 로직에(791-1 단위 견본 바탕)."""
|
||||
master = t._base(
|
||||
@@ -107,8 +122,46 @@ def _b09(build, row) -> dict:
|
||||
return {PART[p]: D(str(block[p])) for p in PART}
|
||||
|
||||
|
||||
OWNER = "pipe@100.0" # `test_145_2._ts` 가 내역 단가를 읽는 구조물 id
|
||||
|
||||
|
||||
def _daega(whole, sk: dict, sheets: dict) -> dict:
|
||||
"""⑤ SC(굳힌 SK 참조 수량 1) → 대가 블록 → 파이썬 셈 = TS 짝 → 내역 단가."""
|
||||
from resources.tester.test_145_2_b09_labor_calc import _same, _ts
|
||||
|
||||
sk_ref, sc_ref = {"층": "project", "키": "SK-990001"}, {"층": "project", "키": "SC-990001"}
|
||||
sc = {"종류": refs.TITLE["precombo"], "이름": "SC", "단위": "식",
|
||||
"담은로직": [{"참조": sk_ref, "수량": {"손": 1}}]} # fmt: skip
|
||||
build = dsrc.Builder()
|
||||
bid = build.precombo(whole, sc_ref, sc, {refs.ref_id(sk_ref): sk, refs.ref_id(sc_ref): sc})
|
||||
daega = dsrc._plain({"blocks": list(build.blocks.values()), "owners": {OWNER: bid}})
|
||||
py = dcalc.compute_labor(daega, sheets)
|
||||
ts = _ts(daega, sheets, {})
|
||||
assert _same(json.loads(json.dumps(py, ensure_ascii=False)), ts["out"]) # 거울
|
||||
price = dcalc.daega_price(py, OWNER)
|
||||
assert _same(price, ts["price"])
|
||||
return {PART[p]: D(str(price[p])) for p in PART}
|
||||
|
||||
|
||||
def _b09_daega(build) -> dict:
|
||||
master, row, _ = build()
|
||||
sk = {"종류": sd.TITLE, "키": "SK-990001", "이름": "시험", "단위": "식",
|
||||
"바탕": {"로직": _key(row)}, "입력값": {}, "덮개": {}, "더한줄": [], "뺀줄": [],
|
||||
item_sk.ROW: row, "정체": []} # fmt: skip
|
||||
files = master.index
|
||||
sheets = {
|
||||
"labor": [{"key": k, "price": float(r["값"])} for k, r in files.get("LB", {}).items()],
|
||||
"material": [
|
||||
{"key": k, "price": float(r["물가자료"])} for k, r in files.get("MT", {}).items()
|
||||
],
|
||||
"machine": [],
|
||||
"gaps": [],
|
||||
}
|
||||
return _daega(master, sk, sheets)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(CASES))
|
||||
def test_네_길_같은_금액(name):
|
||||
def test_다섯_길_같은_금액(name):
|
||||
build = CASES[name]
|
||||
master, row, want = build()
|
||||
one = _engine(master, row)
|
||||
@@ -117,6 +170,34 @@ def test_네_길_같은_금액(name):
|
||||
assert _sk(*build()[:2]) == one
|
||||
assert _sc(*build()[:2]) == one
|
||||
assert _b09(build, row) == one
|
||||
assert _b09_daega(build) == one # ⑤ B09 일위대가 대가 블록(792-4)
|
||||
|
||||
|
||||
REAL = [("봉화", "D00102"), ("북부청임도", 18)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("tag", "code"), REAL)
|
||||
def test_실무_가져온_식_SK_대가_내역_엑셀(tag, code, world):
|
||||
"""793-2 길 — 경쟁사 블록 → 가져온 식 SK(임시 층) → 굳힘 → SC 줄 → B09 대가 →
|
||||
내역 단가 = 엑셀."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
from resources.tester.test_793_2_sangeun_imported import _emitted
|
||||
|
||||
out = _emitted(tag, code)
|
||||
with store.at(world["personal"]):
|
||||
made = imp.create(out["로직"], out["원본"], f"{tag} {code}")
|
||||
doc = store.read("sangeun", made["이름"])["문서"]
|
||||
whole = m01.loaded()[1]
|
||||
rid = "project||SK-990001"
|
||||
frozen = item_sk.freeze({rid: sd.clean(doc, None, whole)})[rid]
|
||||
key, row = frozen["바탕"]["로직"], json.loads(json.dumps(frozen[item_sk.ROW]))
|
||||
assert ms_uses(row) # 새 의미 로직(줄 자리 · 번지 · 단계)
|
||||
books = {"labor": [], "material": [], "machine": [], "gaps": []} # 엑셀 단가 = 고정 줄
|
||||
got = _daega(sc._Over(whole, key, row), frozen, books)
|
||||
excel = {k: D(str(round(v))) for k, v in out["원본"]["엑셀금액"].items()}
|
||||
assert got == excel
|
||||
|
||||
|
||||
def test_단위_로직_새_의미가_금액을_바꿈():
|
||||
|
||||
Reference in New Issue
Block a user