Merge remote-tracking branch 'origin/sub_laptop_1' into sub_laptop_2

This commit is contained in:
2026-11-05 18:26:15 +09:00
7 changed files with 338 additions and 25 deletions
+51 -6
View File
@@ -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]
+54 -10
View File
@@ -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 이 아니라 모름(빈 줄)
+135
View File
@@ -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()))}"
+3 -1
View File
@@ -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 # 읽기만
+84 -3
View File
@@ -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_단위_로직_새_의미가_금액을_바꿈():