refactor(841-1): B09 대가 · 산근 단가 원천 = M02 엔진 — A 걸음
- 산근 Solver 단가 = M02 엔진 줄 금액 ÷ 수량(labor_doc.run_line 날 값) · B09 단가 책 · 바탕 표 안 읽음 - 대가 블록 줄 노무 · 자재 · 기계 = 엔진 단가(own) · 자재 줄 = 엔진 기준단가 · 고정 줄 단가 책 덮어쓰기 걷음 - 대가 단가 고친 칸(daega_cover 노 · 재 · 경) 걷음 · 품(수량) 고친 칸 둠 - 기계 시간당 = M02 엔진 기계경비 값(운전사 계수 16/12 · 25/20 따로 곱 0) — 견본 노체포설 4,522 → 3,548 = M02 - 운반 단가 미리보기(logic-preview) = 엔진 단가 - 낡은 시험(B09 단가 책 입력 견본)은 C 걸음에서 정리 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,7 +47,7 @@ from B09_Estimation.B09_Estimation_Calc_Base import (
|
||||
)
|
||||
|
||||
PARTS = ("labor", "material", "expense")
|
||||
COVER_KEYS = ("qty", *PARTS)
|
||||
COVER_KEYS = ("qty",) # 841-1 단가 고친 칸 걷음 — 품(수량)만
|
||||
TAB = "dg"
|
||||
NO_LINES = "구성 줄 없음" # 줄 0 개 일위대가의 빈 줄 이름
|
||||
COST = {"노무비": "labor", "재료비": "material", "경비": "expense"}
|
||||
@@ -55,9 +55,9 @@ WAYS = {"버림": rounddown, "올림": roundup, "반올림": round_half}
|
||||
COPY = "@copy"
|
||||
ADD = "@add"
|
||||
WHY = {
|
||||
"labor": "노임 단가 없음",
|
||||
"material": "자재 적용 단가 없음",
|
||||
"machine": "중기 시간 단가 없음",
|
||||
"labor": "노임 단가 없음(M02 엔진)",
|
||||
"material": "자재 단가 없음(M02 엔진)",
|
||||
"machine": "기계 시간당 없음(M02 엔진)",
|
||||
"daega": "부른 대가 합계 없음",
|
||||
"sangeun": "산근 단가 없음(145-1)",
|
||||
}
|
||||
@@ -96,16 +96,18 @@ def _prices(line: dict[str, Any], books: dict[str, Any], sums: dict) -> dict[str
|
||||
"""줄 비목 단가 — 그 비목이 없는 줄은 0 · 원천이 없으면 None."""
|
||||
kind, key = line["kind"], line.get("key")
|
||||
out: dict[str, float | None] = dict.fromkeys(PARTS, 0.0)
|
||||
if kind == "labor":
|
||||
out["labor"] = num((books["labor"].get(key) or {}).get("price"))
|
||||
elif kind == "material":
|
||||
row = books["material"].get(key) or {}
|
||||
# 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락에서 셈 — 두 번 안 셈)
|
||||
out["material"] = 0.0 if row.get("supply") in SUPPLY else num(row.get("price"))
|
||||
elif kind in ("machine", "daega", "sangeun"):
|
||||
row = {"machine": books["machine"], "daega": sums, "sangeun": books["sangeun"]}[kind].get(
|
||||
key
|
||||
) or {}
|
||||
if kind in (
|
||||
"labor",
|
||||
"material",
|
||||
"machine",
|
||||
): # 841-1 = M02 엔진 단가(`own`) · B09 단가 책 안 읽음
|
||||
own = line.get("own")
|
||||
out = {p: num(own.get(p)) if p in own else 0.0 for p in PARTS} if own else _none()
|
||||
row = (books["material"].get(key) or {}) if kind == "material" else {}
|
||||
if row.get("supply") in SUPPLY: # 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락)
|
||||
out["material"] = 0.0
|
||||
elif kind in ("daega", "sangeun"):
|
||||
row = {"daega": sums, "sangeun": books["sangeun"]}[kind].get(key) or {}
|
||||
for part in PARTS:
|
||||
out[part] = None if row.get("blank") else num(row.get(part)) # 빈 줄 대가 = 빈칸
|
||||
elif kind == "fixed":
|
||||
@@ -115,21 +117,18 @@ def _prices(line: dict[str, Any], books: dict[str, Any], sums: dict) -> dict[str
|
||||
return out
|
||||
|
||||
|
||||
def _none() -> dict[str, float | None]:
|
||||
return dict.fromkeys(PARTS, None)
|
||||
|
||||
|
||||
def _fixed(line: dict[str, Any], books: dict[str, Any]) -> dict[str, float | None]:
|
||||
"""고정 줄 단가(705-1) — 줄 단가 위에 이은 비목만 단가 책 값."""
|
||||
"""고정 줄 단가(705-1) — 줄 단가 그대로 · 이은 자재가 관급 · 별도계상이면 재료 0(841-1 단가 책
|
||||
값으로 다시 안 덮음)."""
|
||||
own, link = line.get("own") or {}, line.get("link") or {}
|
||||
out = {part: num(own.get(part)) or 0.0 for part in PARTS}
|
||||
labor = books["labor"].get(link.get("labor")) if link.get("labor") else None
|
||||
if labor and num(labor.get("price")) is not None:
|
||||
out["labor"] = num(labor["price"])
|
||||
row = books["material"].get(link.get("material")) if link.get("material") else None
|
||||
if row and (row.get("supply") in SUPPLY or num(row.get("price")) is not None):
|
||||
out["material"] = 0.0 if row.get("supply") in SUPPLY else num(row["price"])
|
||||
machine = books["machine"].get(link.get("machine")) if link.get("machine") else None
|
||||
if machine and not machine.get("blank"):
|
||||
for part in PARTS:
|
||||
if num(machine.get(part)) is not None:
|
||||
out[part] = num(machine[part])
|
||||
if row and row.get("supply") in SUPPLY:
|
||||
out["material"] = 0.0
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -39,16 +39,16 @@ import {
|
||||
} from "./B09_Estimation_Calc_Base";
|
||||
|
||||
const PARTS = ["labor", "material", "expense"] as const;
|
||||
const COVER_KEYS = ["qty", ...PARTS] as const;
|
||||
const COVER_KEYS = ["qty"] as const; // 841-1 단가 고친 칸 걷음 — 품(수량)만
|
||||
const TAB = "dg";
|
||||
const NO_LINES = "구성 줄 없음"; // 줄 0 개 일위대가의 빈 줄 이름(파이썬 짝)
|
||||
export const COPY = "@copy";
|
||||
/** 붙인 줄 덮개 열쇠(705-2) */
|
||||
export const ADD = "@add";
|
||||
const WHY: Record<string, string> = {
|
||||
labor: "노임 단가 없음",
|
||||
material: "자재 적용 단가 없음",
|
||||
machine: "중기 시간 단가 없음",
|
||||
labor: "노임 단가 없음(M02 엔진)",
|
||||
material: "자재 단가 없음(M02 엔진)",
|
||||
machine: "기계 시간당 없음(M02 엔진)",
|
||||
daega: "부른 대가 합계 없음",
|
||||
sangeun: "산근 단가 없음(145-1)",
|
||||
};
|
||||
@@ -103,14 +103,17 @@ const blanks = (): Parts => ({ labor: null, material: null, expense: null });
|
||||
|
||||
function linePrices(line: Row, books: Row, sums: Record<string, Row>): Parts {
|
||||
const { kind, key } = line;
|
||||
const out = zeros();
|
||||
if (kind === "labor") out.labor = num((books.labor[key] || {}).price);
|
||||
else if (kind === "material") {
|
||||
const row: Row = books.material[key] || {};
|
||||
// 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락에서 셈 — 두 번 안 셈)
|
||||
out.material = (SUPPLY as readonly string[]).includes(row.supply) ? 0 : num(row.price);
|
||||
} else if (kind === "machine" || kind === "daega" || kind === "sangeun") {
|
||||
const src: Row = ({ machine: books.machine, daega: sums, sangeun: books.sangeun } as Row)[kind];
|
||||
let out = zeros();
|
||||
if (kind === "labor" || kind === "material" || kind === "machine") {
|
||||
// 841-1 = M02 엔진 단가(`own`) · B09 단가 책 안 읽음
|
||||
const own: Row | undefined = line.own;
|
||||
out = blanks();
|
||||
if (own) for (const part of PARTS) out[part] = part in own ? num(own[part]) : 0;
|
||||
const row: Row = kind === "material" ? books.material[key] || {} : {};
|
||||
// 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락)
|
||||
if ((SUPPLY as readonly string[]).includes(row.supply)) out.material = 0;
|
||||
} else if (kind === "daega" || kind === "sangeun") {
|
||||
const src: Row = ({ daega: sums, sangeun: books.sangeun } as Row)[kind];
|
||||
const row: Row = src[key] || {};
|
||||
for (const part of PARTS) out[part] = row.blank ? null : num(row[part]); // 빈 줄 대가 = 빈칸
|
||||
} else if (kind === "fixed") return fixedPrices(line, books);
|
||||
@@ -118,20 +121,14 @@ function linePrices(line: Row, books: Row, sums: Record<string, Row>): Parts {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 고정 줄 단가(705-1) — 줄 단가 위에 이은 비목만 단가 책 값(짝 `_fixed`) */
|
||||
/** 고정 줄 단가(705-1) — 줄 단가 그대로 · 이은 자재가 관급 · 별도계상이면 재료 0(짝 `_fixed`) */
|
||||
function fixedPrices(line: Row, books: Row): Parts {
|
||||
const own: Row = line.own || {};
|
||||
const link: Row = line.link || {};
|
||||
const out = zeros();
|
||||
for (const part of PARTS) out[part] = num(own[part]) ?? 0;
|
||||
const labor: Row | undefined = link.labor ? books.labor[link.labor] : undefined;
|
||||
if (labor && num(labor.price) !== null) out.labor = num(labor.price);
|
||||
const row: Row | undefined = link.material ? books.material[link.material] : undefined;
|
||||
const supplied = !!row && (SUPPLY as readonly string[]).includes(row.supply);
|
||||
if (row && (supplied || num(row.price) !== null)) out.material = supplied ? 0 : num(row.price);
|
||||
const machine: Row | undefined = link.machine ? books.machine[link.machine] : undefined;
|
||||
if (machine && !machine.blank)
|
||||
for (const part of PARTS) if (num(machine[part]) !== null) out[part] = num(machine[part]);
|
||||
if (row && (SUPPLY as readonly string[]).includes(row.supply)) out.material = 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -243,18 +243,18 @@ class Builder:
|
||||
part = PART.get(str(add.get("비목")), "expense")
|
||||
return {**head, "kind": "pct", "part": part, "base": base, "spec": f"{label}의 %",
|
||||
"unit": "%", "qty": round(value / under * 100, 6)} # fmt: skip
|
||||
kind = item.get("종류")
|
||||
kind, own = item.get("종류"), _own(x, qty)
|
||||
if kind == "인력":
|
||||
key = goods.get("키") or str(item.get("요소") or "")
|
||||
return {**head, "kind": "labor", "key": key, "qty": qty}
|
||||
return {**head, "kind": "labor", "key": key, "qty": qty, "own": own}
|
||||
if kind == "재료":
|
||||
if not goods.get("키"):
|
||||
return {**head, "kind": "blank", "qty": qty, "why": x.get("까닭") or "품목 없음"}
|
||||
return {**head, "kind": "material", "key": goods["키"], "spec": goods.get("규격") or "",
|
||||
"qty": qty} # fmt: skip
|
||||
"qty": qty, "own": own} # fmt: skip
|
||||
# 로직 줄 — `Sangeun.call_of` 판정: 기계경비 로직 = 중기 · 그 밖 = 하위 산근
|
||||
if call and call.get("machine"):
|
||||
return {**head, "kind": "machine", "key": call["machine"], "qty": qty}
|
||||
if call and call.get("machine"): # 기계 시간당 = M02 엔진 값(841-1)
|
||||
return {**head, "kind": "machine", "key": call["machine"], "qty": qty, "own": own}
|
||||
if call and call.get("code"):
|
||||
key = sg.sangeun_key(call["code"], call["given"])
|
||||
return {**head, "kind": "sangeun", "key": key, "qty": qty}
|
||||
@@ -559,11 +559,24 @@ def _row_of(key: str, where: dict) -> str:
|
||||
return f"{where[line]}@{PART[cost]}" if cost else str(where[line])
|
||||
|
||||
|
||||
def _own(x: dict, qty: float | None) -> dict[str, float] | None:
|
||||
"""엔진 줄 비목 단가(841-1) = 엔진 금액 ÷ 엔진 수량 · 수량 0 = 0 · 못 셈 = None(빈칸)."""
|
||||
money = x.get("비목") or {}
|
||||
if not qty:
|
||||
return dict.fromkeys(PART.values(), 0.0) if qty == 0 else None
|
||||
return {PART[k]: (_f(v) or 0.0) / qty for k, v in money.items() if k in PART}
|
||||
|
||||
|
||||
def _material(whole: Any, mat: dict, qty: Any) -> dict[str, Any]:
|
||||
"""자재 줄 — 조합 사용자 자재 · 사전조합 자재(175-2) 한 꼴 · 이름 · 규격 · 단위 = 사본 단가."""
|
||||
"""자재 줄 — 조합 사용자 자재 · 사전조합 자재(175-2) 한 꼴 · 이름 · 규격 · 단위 = 사본 단가 ·
|
||||
단가 = M02 엔진 자재 기준단가(841-1 · 조합 셈 답 `기준단가` · 없으면 사본 단가 묶음으로)."""
|
||||
row = (whole.get(mat.get("코드")) if mat.get("코드") else None) or {}
|
||||
names = {k: row.get(v) or "" for k, v in (("name", "이름"), ("spec", "규격"), ("unit", "단위"))}
|
||||
return {"kind": "material", "key": mat.get("코드"), **names, "qty": _f(qty)}
|
||||
price = mat["기준단가"] if "기준단가" in mat else None
|
||||
if price is None and mat.get("코드"):
|
||||
price = labor_doc.material_price(whole, mat["코드"], mat.get("단위"))["기준단가"]
|
||||
own = None if _f(price) is None else {"material": _f(price)}
|
||||
return {"kind": "material", "key": mat.get("코드"), **names, "qty": _f(qty), "own": own}
|
||||
|
||||
|
||||
def _engine_bases(engine: list[dict], items: list[dict]) -> dict[str, float]:
|
||||
|
||||
@@ -160,7 +160,7 @@ def compute_sheets(
|
||||
화면 짝 = `B09_Estimation_UI_Shell.computeSheets`(산근만 서버 답 그대로) ·
|
||||
memo = 두 번 셈의 앞 산근 풀이(`Sangeun.collect`)."""
|
||||
base = compute_base(inputs, user)
|
||||
sg = sangeun.build(root, base, user, memo) # 145-1 — 굳힌 단가로 풀이(+ 166-2 손 줄 로직)
|
||||
sg = sangeun.build(root, user, memo) # 145-1 — M02 엔진 값으로 풀이(841-1 · + 166-2 손 줄 로직)
|
||||
sg = sg_cover.cover_sangeun(sg, user.get("sg_cover")) # 719-3 이 공사 산근 고친 칸
|
||||
by_key = {row["key"]: row for row in sg["sg_list"]}
|
||||
labor = compute_labor(
|
||||
@@ -423,14 +423,11 @@ class PreviewBody(BaseModel):
|
||||
|
||||
|
||||
def logic_preview(root: Path, body: PreviewBody) -> dict[str, Any]:
|
||||
"""로직 한 산근 미리보기 — 단가 = 굳힌 책(없는 줄 = 지금 M01) · 사용자 대비표 고름."""
|
||||
"""로직 한 산근 미리보기 — 단가 = M02 엔진(841-1 · 그 로직 닫힌 묶음 · B09 단가 책 안 읽음)."""
|
||||
src = {"code": body.code, "given": sangeun._clean_given(body.given), "rate": body.rate,
|
||||
"picks": body.picks} # fmt: skip
|
||||
hands = [("preview", "미리보기", src)]
|
||||
keys = sangeun.hand_resource_keys({"haul_refs": {"preview": {"tab": "logic", **src}}})
|
||||
user = store.read_user(root)
|
||||
base = compute_base(prices.build_inputs(store.read_book(root), keys), user)
|
||||
solver = sangeun.Solver(base)
|
||||
solver = sangeun.Solver()
|
||||
block = solver.add(sangeun.hand_whole(hands), src, None)
|
||||
text = [
|
||||
*block["head"],
|
||||
|
||||
@@ -62,6 +62,7 @@ import master_sheet_line as msl # noqa: E402 — 계산서 줄 덧칸(714-1) ·
|
||||
|
||||
PARTS = ("labor", "material", "expense")
|
||||
COST_PART = {"노무비": "labor", "재료비": "material", "경비": "expense"}
|
||||
PART_COST = {v: k for k, v in COST_PART.items()}
|
||||
PCT_OF = {**COST_PART, "인건비": "people"} # % 덧줄 밑수 → 덮개 다시 셈(719-3)
|
||||
DEPTH = 4 # 하위 산근 깊이 상한
|
||||
NO_SOURCE = "조합 밖 토공 · 운반 · 사토 · 준비공은 산근 없음"
|
||||
@@ -165,49 +166,15 @@ def rows_of(folder: Path, whole: Any = None) -> list[dict[str, Any]]:
|
||||
|
||||
|
||||
# ── 풀이 ──────────────────────────────────────────────────────────────
|
||||
class _Seen(dict):
|
||||
"""읽은 키를 적는 단가 표 — 두 번 셈(183-1)에서 첫 풀이가 본 값만 견줌."""
|
||||
|
||||
def __init__(self, *args: Any) -> None:
|
||||
super().__init__(*args)
|
||||
self.seen: set[Any] = set()
|
||||
|
||||
def __contains__(self, key: object) -> bool:
|
||||
self.seen.add(key)
|
||||
return super().__contains__(key)
|
||||
|
||||
def __getitem__(self, key: Any) -> Any:
|
||||
self.seen.add(key)
|
||||
return super().__getitem__(key)
|
||||
|
||||
def get(self, key: Any, default: Any = None) -> Any:
|
||||
self.seen.add(key)
|
||||
return super().get(key, default)
|
||||
|
||||
def same(self, other: dict) -> bool:
|
||||
"""본 키마다 있음 · 값이 `other` 와 같음."""
|
||||
return all(
|
||||
dict.__contains__(self, k) == dict.__contains__(other, k)
|
||||
and dict.get(self, k) == dict.get(other, k)
|
||||
for k in self.seen
|
||||
)
|
||||
|
||||
|
||||
class _Prices:
|
||||
"""B09 표 단가 — 노무 · 중기(노 · 재 · 경) · 자재 적용 단가(키 → 값)."""
|
||||
"""엔진 단가(841-1) — M02 엔진이 사본 단가 묶음으로 낸 줄 금액 ÷ 수량 · 텍스트 수식 글이 읽음
|
||||
(노무 · 자재 = 수 · 중기 = 노 · 재 · 경). B09 단가 책 · 바탕 표는 안 읽음."""
|
||||
|
||||
def __init__(self, sheets: dict[str, Any]) -> None:
|
||||
self.labor = _Seen({r["key"]: num(r.get("price")) for r in sheets.get("labor") or []})
|
||||
self.machine = _Seen({r["key"]: r for r in sheets.get("machine") or []})
|
||||
self.material = _Seen(
|
||||
{r["key"]: num(r.get("applied")) for r in sheets.get("compare") or []}
|
||||
)
|
||||
|
||||
def same(self, other: _Prices) -> bool:
|
||||
"""첫 풀이가 본 단가가 `other` 에서도 같음 — 같으면 풀이도 같음."""
|
||||
return all(
|
||||
getattr(self, k).same(getattr(other, k)) for k in ("labor", "machine", "material")
|
||||
)
|
||||
def __init__(self) -> None:
|
||||
# ponytail: 키 하나 = 단가 하나(사본마다 기준 시점이 달라도 마지막 본 값) — 글 짓기 전용
|
||||
self.labor: dict[str, float | None] = {}
|
||||
self.material: dict[str, float | None] = {}
|
||||
self.machine: dict[str, dict[str, float | None]] = {}
|
||||
|
||||
|
||||
def _unit(row: dict) -> tuple[str, Decimal]:
|
||||
@@ -323,6 +290,10 @@ def spec_text(whole: Any, given: dict[str, Any]) -> tuple[str, str]:
|
||||
return ", ".join(f"{k}={shown(k, v)}" for k, v in given.items()), raw
|
||||
|
||||
|
||||
def _float(value: Any) -> Any:
|
||||
return float(value) if isinstance(value, Decimal) else value
|
||||
|
||||
|
||||
def _money(price: float | None, qty: float | None) -> float | None:
|
||||
"""줄 금액 = ROUNDDOWN(단가 × 수량, 1)(엑셀 자리)."""
|
||||
return None if price is None or qty is None else rounddown(price * qty, 1)
|
||||
@@ -340,8 +311,9 @@ def block_totals(lines: list[dict[str, Any]]) -> dict[str, float | None]:
|
||||
class Solver:
|
||||
"""한 번 셈 — 산근 블록을 첫 등장 차례로 모음(하위 산근 포함)."""
|
||||
|
||||
def __init__(self, sheets: dict[str, Any]) -> None:
|
||||
self.prices = _Prices(sheets)
|
||||
def __init__(self) -> None:
|
||||
self.prices = _Prices()
|
||||
self.eng: list[dict[str, Any]] = [] # 지금 푸는 로직의 엔진 줄(호표 차례 · 841-1)
|
||||
self.blocks: dict[str, dict[str, Any]] = {}
|
||||
|
||||
def add(
|
||||
@@ -367,10 +339,13 @@ class Solver:
|
||||
unit, times = _unit(row)
|
||||
rate = Decimal(str(src["rate"])) / 100 if src.get("rate") not in (None, 100) else None
|
||||
lines: list[dict[str, Any]] = []
|
||||
# 841-1 단가 = M02 엔진 줄 금액 ÷ 수량(켜쌓기율 없이 · 날 값 · B09 단가 책 안 읽음)
|
||||
eng = labor_doc.run_line(whole, code, src["given"], None, src.get("picks"), raw=True)
|
||||
try:
|
||||
env = _env(whole, row, src["given"])
|
||||
with mf.mp.choosing(src.get("picks")):
|
||||
for n, item in enumerate(row.get("호표", [])):
|
||||
self.eng = eng.get("줄") or [] # 하위 산근 풀이가 바꿔 놓아도 되돌림
|
||||
lines.append(self._line(whole, item, n, env, times, rate, depth))
|
||||
ss.places(row, lines) # 줄 자리(792-3)
|
||||
self._extras(whole, row, env, lines)
|
||||
@@ -434,6 +409,14 @@ class Solver:
|
||||
at += n
|
||||
return text[:start], text[-1]
|
||||
|
||||
def _each(self, n: int, part: str, q: float) -> float | None:
|
||||
"""엔진 줄 n 비목 단가 = 엔진 금액 ÷ 엔진 수량 · 안 쓰는 줄(수량 0) = 0 · 못 셈 = None."""
|
||||
x = self.eng[n] if n < len(self.eng) else {}
|
||||
qty, money = num(_float(x.get("수량"))), (x.get("비목") or {}).get(PART_COST[part])
|
||||
if not qty:
|
||||
return 0.0 if q == 0 else None
|
||||
return None if money is None else float(money) / qty
|
||||
|
||||
def _blank(self, item: dict, why: str) -> dict[str, Any]:
|
||||
part = COST_PART.get(item.get("비목"), "material")
|
||||
return {"kind": "blank", "name": item.get("이름", ""), "spec": "", "qty": None,
|
||||
@@ -451,11 +434,12 @@ class Solver:
|
||||
zero = dict.fromkeys(PARTS, 0.0)
|
||||
kind = item.get("종류")
|
||||
if kind == "로직":
|
||||
return self._logic_line(whole, item, env, q, base, zero, depth)
|
||||
return self._logic_line(whole, item, n, env, q, base, zero, depth)
|
||||
if kind == "인력":
|
||||
key = mf.fill(item["요소"], env) if isinstance(item["요소"], str) else ""
|
||||
price = self.prices.labor.get(key)
|
||||
why = None if price is not None else f"노무비목록표에 없음 「{key}」"
|
||||
price = self._each(n, "labor", q)
|
||||
self.prices.labor[key] = price
|
||||
why = None if price is not None else f"노임 단가 없음(M02 엔진) 「{key}」"
|
||||
return {**base, **zero, "kind": "labor", "ref": {"tab": "labor", "key": key},
|
||||
"price": {"labor": price}, "labor": _money(price, q), "why": why} # fmt: skip
|
||||
ref = (
|
||||
@@ -468,29 +452,33 @@ class Solver:
|
||||
key = str(
|
||||
goods.get("키") or (source or "").split(".")[0] or (ref if isinstance(ref, str) else "")
|
||||
)
|
||||
if key not in self.prices.material: # 자리표시 글(「MO:경유:전국평균」) = 대비표 키로
|
||||
try:
|
||||
key = str((whole.get(key) or {}).get("키") or key)
|
||||
except (mf.FormulaError, KeyError, TypeError, ValueError):
|
||||
pass
|
||||
try: # 자리표시 글(「MO:경유:전국평균」) = 그 줄 키로
|
||||
key = str((whole.get(key) or {}).get("키") or key)
|
||||
except (mf.FormulaError, KeyError, TypeError, ValueError):
|
||||
pass
|
||||
part = COST_PART.get(item.get("비목"), "material")
|
||||
price = self.prices.material.get(key)
|
||||
if q == 0:
|
||||
price = price if price is not None else 0.0 # 안 쓰는 재료 줄 — 금액 0
|
||||
why = None if price is not None else f"자재단가대비표에 없음 「{key}」"
|
||||
price = self._each(n, part, q)
|
||||
self.prices.material[key] = price
|
||||
why = None if price is not None else f"자재 단가 없음(M02 엔진) 「{key}」"
|
||||
spec = goods.get("규격") or ""
|
||||
ref = {"tab": "compare", "key": key}
|
||||
return {**base, **zero, "spec": spec, "kind": "material", "ref": ref,
|
||||
"price": {part: price}, part: _money(price, q), "why": why,
|
||||
"_goods": goods} # fmt: skip
|
||||
|
||||
def _logic_line(self, whole, item, env, q, base, zero, depth) -> dict[str, Any]:
|
||||
def _logic_line(self, whole, item, n, env, q, base, zero, depth) -> dict[str, Any]:
|
||||
call = call_of(whole, item, env)
|
||||
if call and "machine" in call:
|
||||
keys = [call["machine"]] if call["machine"] else []
|
||||
m = self.prices.machine.get(keys[0]) if keys else None
|
||||
price = {p: num((m or {}).get(p)) for p in PARTS}
|
||||
why = None if m else f"중기목록표에 없음 「{keys[0] if keys else item.get('이름')}」"
|
||||
price = {p: self._each(n, p, q) for p in PARTS} # 기계 시간당 = M02 엔진 값(841-1)
|
||||
m = None not in price.values()
|
||||
if keys:
|
||||
self.prices.machine[keys[0]] = price
|
||||
why = (
|
||||
None
|
||||
if m
|
||||
else f"기계 시간당 없음(M02 엔진) 「{keys[0] if keys else item.get('이름')}」"
|
||||
)
|
||||
amounts = {p: _money(price[p], q) if m else None for p in PARTS}
|
||||
ref = {"tab": "machine", "key": keys[0] if keys else ""}
|
||||
return {**base, "kind": "machine", "ref": ref,
|
||||
@@ -618,16 +606,15 @@ def hand_resource_keys(user: dict[str, Any] | None) -> list[str]:
|
||||
|
||||
def collect(
|
||||
entries: list[tuple[str, Path]],
|
||||
sheets: dict[str, Any],
|
||||
hands: list[tuple[str, str, dict[str, Any]]] | None = None,
|
||||
memo: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""`entries` = [(구조물 id, 사본 폴더)] → 블록 · 목록 · 「연결 없음」 ·
|
||||
`hands` = 손 줄 로직(166-2 · 사본 뒤 번호 · 마스터 로직) ·
|
||||
memo = 같은 사본 · 손 줄의 앞 풀이(두 번 셈) — 앞 풀이가 본 단가가 같으면 그 풀이 사본."""
|
||||
solver = Solver(sheets)
|
||||
if memo and memo["prices"].same(solver.prices):
|
||||
if memo and "sg" in memo: # 841-1 풀이 = 사본 · 엔진만(바탕 표 안 읽음) → 앞 풀이 그대로
|
||||
return copy.deepcopy(memo["sg"])
|
||||
solver = Solver()
|
||||
for owner, folder in entries:
|
||||
whole = item_prices.load_prices(read_json(folder / PRICE_FILE) or {})
|
||||
for src in rows_of(folder, whole):
|
||||
@@ -652,13 +639,12 @@ def collect(
|
||||
"sg_hand": hand_keys,
|
||||
}
|
||||
if memo is not None:
|
||||
memo.update(prices=solver.prices, sg=copy.deepcopy(out))
|
||||
memo.update(sg=copy.deepcopy(out))
|
||||
return out
|
||||
|
||||
|
||||
def build(
|
||||
project_root: str | Path,
|
||||
sheets: dict[str, Any],
|
||||
user: dict[str, Any] | None = None,
|
||||
memo: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
@@ -676,4 +662,4 @@ def build(
|
||||
entries += [
|
||||
(f"서고 {(m.get('원본') or {}).get('name') or f.name}", f) for f, m in copies(project_root)
|
||||
]
|
||||
return collect(entries, sheets, hand_sources(user), memo)
|
||||
return collect(entries, hand_sources(user), memo)
|
||||
|
||||
Reference in New Issue
Block a user