feat(B09): 설계내역서 구조물 줄 = 대가 블록마다 나눔 — B08 열 owners 잇기(145-3)
- 항목 짜기 = B08 구조물 집계표 열 owners {사본 주인 id: 값}(sub8 덧칸) 를 싣고
- 짝 셈 split_by_block / splitByBlock = 주인마다 대가 단가(price_of = 145-2 daega_price 자리) → 같은 대가 블록끼리 한 줄(수량 합 ROUND 3) · 대가 없는 주인 = 한 줄 「연결 없음」
- price_of 없으면 열 하나 그대로(단가 빈칸)
- 시험 = test_145_3_b09_bill 더함(대가17 × 20 = 2,491,880 · 대가24 × 12.5 = 2,078,274 · 거울)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
This commit is contained in:
@@ -105,6 +105,8 @@ def structure_items(summary: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||
"qty": total.get(cid),
|
||||
"note": " · ".join(combos),
|
||||
"ref": {"tab": "dg_list", "key": cid},
|
||||
# 사본 주인마다 값(B08 145-3 덧칸) — 대가 블록마다 줄을 나눌 때 씀
|
||||
"owners": dict(column.get("owners") or {}),
|
||||
"why": WHY_STRUCTURE,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
|
||||
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, rounddown
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import compute_base, gap, num, round_half, rounddown
|
||||
|
||||
#: 공종 나무 — (번호, 이름, 부모) · 엑셀 기번3 공통 1공구 꼴(공구 안 나눔)
|
||||
SECTIONS: list[tuple[str, str, str | None]] = [
|
||||
@@ -103,13 +104,48 @@ def _material_items(inputs: dict[str, Any], compare: list[dict[str, Any]]) -> li
|
||||
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"]},
|
||||
}
|
||||
out.append((part, group["price"]))
|
||||
return out
|
||||
|
||||
|
||||
def compute_bill(
|
||||
inputs: dict[str, Any], user: dict[str, Any], base: dict[str, Any]
|
||||
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))
|
||||
@@ -201,8 +237,10 @@ def compute_bill(
|
||||
return {"rows": rows, "totals": totals, "sums": sums_out, "gaps": gaps}
|
||||
|
||||
|
||||
def compute_estimation(inputs: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]:
|
||||
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)
|
||||
bill = compute_bill(inputs, user, base, price_of)
|
||||
return {**base, "bill": bill, "gaps": base["gaps"] + bill["gaps"]}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
computeBase,
|
||||
gap,
|
||||
num,
|
||||
roundHalf,
|
||||
rounddown,
|
||||
type Gap,
|
||||
type Num,
|
||||
@@ -95,11 +96,49 @@ function materialItems(inputs: Row, compare: Row[]): Row[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
export function computeBill(inputs: Row, user: Row, base: Row): Row {
|
||||
/** 사본 주인 id → 대가 1 단위 단가(145-2 `daegaPrice`) */
|
||||
export type PriceOf = (owner: string) => Row | null;
|
||||
|
||||
/** 구조물 열 → 대가 블록마다 한 줄(수량 = 그 블록 주인 값 합 ROUND 3 · 대가 없는 주인 = 한 줄) */
|
||||
export function splitByBlock(item: Row, priceOf: PriceOf): Array<[Row, Row | null]> {
|
||||
const groups = new Map<string, Row>();
|
||||
for (const [owner, value] of Object.entries(item.owners || {})) {
|
||||
const price = priceOf(owner);
|
||||
const tag = price ? String(price.no || "") : "";
|
||||
if (!groups.has(tag)) groups.set(tag, { qty: 0, price, owners: [] });
|
||||
const group = groups.get(tag)!;
|
||||
group.qty += num(value) || 0;
|
||||
group.owners.push(owner);
|
||||
}
|
||||
const out: Array<[Row, Row | null]> = [];
|
||||
for (const [tag, group] of groups) {
|
||||
const part: Row = {
|
||||
...item,
|
||||
key: tag ? `${item.key}#${tag}` : item.key,
|
||||
qty: roundHalf(group.qty, 3),
|
||||
note: [tag, item.note || ""].filter((t) => t).join(" · "),
|
||||
owners: Object.fromEntries(group.owners.map((o: string) => [o, item.owners[o]])),
|
||||
};
|
||||
out.push([part, group.price]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function computeBill(
|
||||
inputs: Row,
|
||||
user: Row,
|
||||
base: Row,
|
||||
priceOf: PriceOf | null = null,
|
||||
): Row {
|
||||
const gaps: Gap[] = [];
|
||||
const hopyo: Row = inputs.hopyo || {};
|
||||
const items: Row[] = [];
|
||||
for (const item of (inputs.bill || {}).items || []) {
|
||||
if (priceOf !== null && item.owners && Object.keys(item.owners).length) {
|
||||
for (const [part, prices] of splitByBlock(item, priceOf))
|
||||
items.push(itemRow(part, prices, gaps));
|
||||
continue;
|
||||
}
|
||||
const ref: Row = item.ref || {};
|
||||
const prices = ref.key ? (hopyo[ref.key] ?? null) : null;
|
||||
items.push(itemRow(item, prices, gaps));
|
||||
@@ -190,8 +229,8 @@ export function computeBill(inputs: Row, user: Row, base: Row): Row {
|
||||
}
|
||||
|
||||
/** 144 바탕 + 145-3 설계내역서 */
|
||||
export function computeEstimation(inputs: Row, user: Row): Row {
|
||||
export function computeEstimation(inputs: Row, user: Row, priceOf: PriceOf | null = null): Row {
|
||||
const base = computeBase(inputs, user);
|
||||
const bill = computeBill(inputs, user, base);
|
||||
const bill = computeBill(inputs, user, base, priceOf);
|
||||
return { ...base, bill, gaps: [...base.gaps, ...bill.gaps] };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user