Files
Aislo/B09_Estimation/B09_Estimation_Calc_Bill.py
T
eomsangdonandClaude Opus 5.5 3bd20530bf fix(B09): 내역 구조물 줄 ref = 대가 블록 — 집계표 역전개 · 이동 이음 · 한 바퀴 시험(145-2)
- daega_price / daegaPrice 에 블록 key · split_by_block(파이썬 · TS) = 대가 단가가 있으면 ref {tab: dg, key}
- 한 바퀴 시험 = 설계내역서 → 대가 → 산근(145-1) → 중기 → 집계표(145-5) · 연결 없음 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
2026-10-09 15:33:44 +09:00

249 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""B09 설계내역서 셈(PLAN 145-3) — 화면 없음 · 저장 안 함 · 파일 · M01 안 읽음.
⚠ 짝 = `B09_Estimation_Calc_Bill.ts`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 ·
거울 시험 `resources/tester/test_145_3_b09_bill.py`.
실무 엑셀 설계내역서 꼴 · 공종 나무 = 엑셀 꼴 고정(`SECTIONS` · 공구 안 나눔):
1 토공사(1.1~1.6) · 2 구조물공(2.1~2.3) · 3 운반공 · 4 자재대 · 관급자재대(제외금액)
항목(토공 · 구조물 · 운반) = 서버가 B08 수량으로 짠 줄(`B09_Estimation_Bill_Items`)
4 자재대 = 자재단가대비표에서 「별도」 표시한 B08 자재 · 관급자재대 = 「관급」 표시 + 조달수수료
단가 = 호표(대가 · 산근 — 145-1 · 2 가 `inputs.hopyo` 로 줌) · 자재 = 대비표 적용 단가
금액 = ROUNDDOWN(단가 × 수량, 0) 비목마다 · 합계 = 세 비목 합 ·
조달수수료 = ROUNDDOWN(관급 순자재대 × 율 ÷ 100, 0)(율 = 사용자 입력 · 빈칸 시작)
제목 줄 = 아래 항목 금액 합(엑셀 SUMIF — 빈 줄은 0 이 아니라 「빈 줄 n」 으로 셈)
빈칸 = 수량이나 단가를 낸 원천이 없으면 그 줄 금액 None + 「연결 없음」 목록.
원가계산서로 넘길 합 = `sums`(직접재료비 · 직접노무비 · 산출경비 · 관급 순자재대 — 145-4 이음 꼴 ·
빈 줄이 있으면 None) · 화면 표시용 = `totals`(아는 금액 합 · 빈 줄 수 · 조달수수료).
"""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from B09_Estimation.B09_Estimation_Calc_Base import compute_base, gap, num, round_half, rounddown
#: 공종 나무 — (번호, 이름, 부모) · 엑셀 기번3 공통 1공구 꼴(공구 안 나눔)
SECTIONS: list[tuple[str, str, str | None]] = [
("1", "토공사", None),
("1.1", "준비공", "1"),
("1.2", "절토", "1"),
("1.3", "구조물터파기 및 되메우기", "1"),
("1.4", "측구파기", "1"),
("1.5", "유용성토", "1"),
("1.6", "사토 및 다짐공", "1"),
("2", "구조물공", None),
("2.1", "돌기슭막이공", "2"),
("2.2", "배수공", "2"),
("2.3", "기타구조물", "2"),
("3", "운반공", None),
("4", "자재대", None),
("G", "관급자재대", None),
]
#: 순공사비에 드는 맨 위 공종(관급자재대 = 제외금액)
DIRECT = ("1", "2", "3", "4")
PARTS = ("labor", "material", "expense")
def _amounts(qty: float | None, prices: dict[str, float | None] | None) -> dict[str, Any]:
if prices is None:
unit = {p: None for p in PARTS}
else:
unit = {p: num(prices.get(p)) for p in PARTS}
out: dict[str, Any] = {f"price_{p}": unit[p] for p in PARTS}
known = all(unit[p] is not None for p in PARTS)
out["price_sum"] = unit["labor"] + unit["material"] + unit["expense"] if known else None
for p in PARTS:
out[p] = rounddown(qty * unit[p], 0) if qty is not None and unit[p] is not None else None
filled = all(out[p] is not None for p in PARTS)
out["sum"] = out["labor"] + out["material"] + out["expense"] if filled else None
return out
def _item_row(item: dict[str, Any], prices: dict[str, Any] | None, gaps: list) -> dict[str, Any]:
qty = num(item.get("qty"))
row = {
"kind": "item",
"key": item["key"],
"section": item["section"],
"name": item.get("name") or "",
"spec": item.get("spec") or "",
"unit": item.get("unit") or "",
"qty": qty,
"note": item.get("note") or "",
"ref": item.get("ref"),
**_amounts(qty, prices),
}
if qty is None:
gaps.append(gap("bill", item["key"], row["name"], "수량 연결 없음"))
if prices is None:
gaps.append(gap("bill", item["key"], row["name"], item.get("why") or "단가 연결 없음"))
return row
def _material_items(inputs: dict[str, Any], compare: list[dict[str, Any]]) -> list[dict]:
"""대비표에서 「별도」 · 「관급」 표시한 수량 있는 자재 → 4 자재대 · 관급자재대 항목."""
qty_of = {m["key"]: m.get("qty") for m in inputs.get("materials") or [] if "qty" in m}
out = []
for row in compare:
if row["supply"] is None or row["key"] not in qty_of:
continue
out.append(
{
"key": f"mat:{row['key']}",
"section": "G" if row["supply"] == "관급" else "4",
"name": row["name"],
"spec": row["spec"],
"unit": row["unit"],
"qty": qty_of[row["key"]],
"note": f"자재 {row['no']}",
"ref": {"tab": "material", "key": row["key"]},
"_price": row["applied"],
}
)
return out
PriceOf = Callable[[str], dict[str, Any] | None]
def split_by_block(item: dict[str, Any], price_of: PriceOf) -> list[tuple[dict, dict | None]]:
"""구조물 열 → 대가 블록마다 한 줄 — 수량 = 그 블록 사본 주인 값 합(ROUND 3) ·
대가 없는 주인은 한 줄로 모음(단가 빈칸 「연결 없음」) · 차례 = 주인이 처음 나온 차례."""
groups: dict[str, dict[str, Any]] = {}
for owner, value in (item.get("owners") or {}).items():
price = price_of(owner)
tag = str(price.get("no") or "") if price else ""
group = groups.setdefault(tag, {"qty": 0.0, "price": price, "owners": []})
group["qty"] += num(value) or 0
group["owners"].append(owner)
out = []
for tag, group in groups.items():
part = {
**item,
"key": f"{item['key']}#{tag}" if tag else item["key"],
"qty": round_half(group["qty"], 3),
"note": " · ".join(t for t in (tag, item.get("note") or "") if t),
"owners": {o: item["owners"][o] for o in group["owners"]},
}
if group["price"] and group["price"].get("key"): # 대가 블록(145-2) — 이동 · 집계표 역전개
part["ref"] = {"tab": "dg", "key": group["price"]["key"]}
out.append((part, group["price"]))
return out
def compute_bill(
inputs: dict[str, Any],
user: dict[str, Any],
base: dict[str, Any],
price_of: PriceOf | None = None,
) -> dict[str, Any]:
"""`price_of(사본 주인 id)` = 대가 1 단위 단가(145-2 `daega_price`) —
주면 구조물 열을 블록마다 나눔."""
gaps: list[dict[str, str]] = []
hopyo = inputs.get("hopyo") or {}
items: list[dict[str, Any]] = []
for item in (inputs.get("bill") or {}).get("items") or []:
if price_of is not None and item.get("owners"):
for part, prices in split_by_block(item, price_of):
items.append(_item_row(part, prices, gaps))
continue
ref = item.get("ref") or {}
prices = hopyo.get(ref.get("key")) if ref.get("key") else None
items.append(_item_row(item, prices, gaps))
for item in _material_items(inputs, base["compare"]):
price = item["_price"]
prices = None if price is None else {"labor": 0, "material": price, "expense": 0}
items.append(_item_row(item, prices, gaps))
owner = [row for row in items if row["section"] == "G"]
net = 0.0
for row in owner:
net += row["material"] or 0
pct = num(((user.get("estimate") or {}).get("procurement_fee_pct")))
fee = rounddown(net * pct / 100, 0) if pct is not None else None
fee_row = {
"kind": "item",
"key": "fee",
"section": "G",
"name": "조달수수료",
"spec": "관급 재료비 × 율",
"unit": "%",
"qty": pct,
"note": "관급 순자재대 밑수",
"ref": None,
"price_labor": 0,
"price_material": net,
"price_expense": 0,
"price_sum": net,
"labor": 0 if pct is not None else None,
"material": fee,
"expense": 0 if pct is not None else None,
"sum": fee,
}
if owner:
items.append(fee_row)
if pct is None:
gaps.append(gap("bill", "fee", "조달수수료", "조달수수료율 입력 없음"))
rows: list[dict[str, Any]] = []
sums: dict[str, dict[str, Any]] = {}
for code, name, parent in SECTIONS:
sums[code] = {"sum": 0.0, "labor": 0.0, "material": 0.0, "expense": 0.0, "blank": 0}
for row in items:
code = row["section"]
chain = [code]
parent = next(p for c, _n, p in SECTIONS if c == code)
if parent:
chain.append(parent)
for c in chain:
for p in ("sum", *PARTS):
sums[c][p] += row[p] or 0
if row["sum"] is None:
sums[c]["blank"] += 1
for code, name, parent in SECTIONS:
s = sums[code]
rows.append(
{
"kind": "title",
"key": f"sec:{code}",
"section": code,
"name": f"{code} {name}" if code != "G" else name,
"sum": s["sum"],
"labor": s["labor"],
"material": s["material"],
"expense": s["expense"],
"blank": s["blank"],
"note": ("(제외금액)" if code == "G" else "")
+ (f" 빈 줄 {s['blank']}" if s["blank"] else ""),
}
)
rows.extend(row for row in items if row["section"] == code)
direct = {p: 0.0 for p in ("sum", *PARTS)}
blank = 0
for code in DIRECT:
for p in direct:
direct[p] += sums[code][p]
blank += sums[code]["blank"]
totals = {
"direct": {**direct, "blank": blank},
"owner": {"net_material": net, "fee": fee, "total": net + (fee or 0)},
}
# 원가계산서로 넘김(145-4 이음 꼴) — 빈 줄이 있으면 모르는 값 = None
known = blank == 0
owner_known = all(row["sum"] is not None for row in owner)
sums_out = {
"material": direct["material"] if known else None,
"labor": direct["labor"] if known else None,
"expense": direct["expense"] if known else None,
"supply_net": net if owner_known else None,
}
return {"rows": rows, "totals": totals, "sums": sums_out, "gaps": gaps}
def compute_estimation(
inputs: dict[str, Any], user: dict[str, Any], price_of: PriceOf | None = None
) -> dict[str, Any]:
"""144 바탕 + 145-3 설계내역서 — 「연결 없음」 목록은 둘을 이음."""
base = compute_base(inputs, user)
bill = compute_bill(inputs, user, base, price_of)
return {**base, "bill": bill, "gaps": base["gaps"] + bill["gaps"]}