feat(b09): 설계내역서 자유 편집 셈 · 저장 — user.bill v2 덮개 · 차례 · 부모 · gone · 새 1단 공종 · 블록 키 · 옛 꼴 읽음(184-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
"""B09 설계내역서 셈(PLAN 145-3) — 화면 없음 · 저장 안 함 · 파일 · M01 안 읽음.
|
||||
|
||||
⚠ 짝 = `B09_Estimation_Calc_Bill.ts`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 ·
|
||||
거울 시험 `resources/tester/test_145_3_b09_bill.py` · `test_182_b09_bill_level.py`(레벨) · `test_184_2_b09_bill_free.py`(자유 편집).
|
||||
거울 시험 `resources/tester/test_145_3_b09_bill.py` · `test_182_b09_bill_level.py`(레벨) ·
|
||||
`test_184_2_b09_bill_free.py`(자유 편집).
|
||||
|
||||
실무 엑셀 설계내역서 꼴 · 공종 나무 = 엑셀 꼴 고정(`SECTIONS` · 공구 안 나눔):
|
||||
실무 엑셀 설계내역서 꼴 · 기본 공종 나무(초기값) = 엑셀 꼴(`SECTIONS` · 공구 안 나눔):
|
||||
1 토공사(1.1~1.6) · 2 구조물공(2.1~2.3) · 3 운반공 · 4 자재대 · 관급자재대(제외금액)
|
||||
항목(토공 · 구조물 · 운반) = 서버가 B08 수량으로 짠 줄(`B09_Estimation_Bill_Items`)
|
||||
4 자재대 = 자재단가대비표에서 「별도」 표시한 B08 자재 · 관급자재대 = 「관급」 표시 + 조달수수료
|
||||
@@ -15,31 +16,38 @@
|
||||
단가 = `ref_price(ref)` 하나 — dg = 일위대가목록표 · sg = 단가산출근거목록표 ·
|
||||
logic = M01 로직 새 산근(서버가 `sg_hand{줄 키: 산근 키}` 로 줌 · 그전엔 빈칸 「[저장] 뒤」) ·
|
||||
그 밖 = `inputs.hopyo` · 비고 = 「대가n」 · 「산근 n호표」(155-3 꼴).
|
||||
자유 편집(184) = 기본 로직 표 = 초기값(저장 안 함) · 사용자 값 `user.bill` v2 = 덮개(계약 — 184-3 화면):
|
||||
자유 편집(184) = 기본 로직 표 = 초기값(저장 안 함) ·
|
||||
사용자 값 `user.bill` v2 = 덮개(계약 — 184-3 화면):
|
||||
{"v": 2,
|
||||
"rows": [표 차례 전체(order · 부모 먼저 · 자손 이어서) {
|
||||
"key": 줄 키, "parent": 묶음 줄 키(`sec:*` · `hand:*`) | 없음 = 맨 위,
|
||||
자동 줄(`sec:*` · `earth:` · `st:` · `haul:` · `mat:`) = 고친 칸만 — 칸 없음 = 자동(B08 따라감) ·
|
||||
값 = 지킴 · null = 일부러 빈칸 · 칸 = name · spec · unit · qty · ref
|
||||
(제목 `sec:*` = name 만 · `mat:` = ref 없음 · `fee` = 덮개 없음 · 차례에 안 넣음 — `sec:G` 끝 고정)
|
||||
자동 줄(`sec:*` · `earth:` · `st:` · `haul:` · `mat:`) = 고친 칸만 —
|
||||
칸 없음 = 자동(B08 따라감) · 값 = 지킴 · null = 일부러 빈칸 ·
|
||||
칸 = name · spec · unit · qty · ref
|
||||
(제목 `sec:*` = name 만 · `mat:` = ref 없음 ·
|
||||
`fee` = 덮개 없음 · 차례에 안 넣음 — `sec:G` 끝 고정)
|
||||
새 줄(`hand:*`) = 모든 칸 + "kind": "group"(묶음 · 수량 · 연결 없음) | 없음 = 항목 ·
|
||||
"cost": "direct"(맨 위 새 묶음 · 없음도 direct)}],
|
||||
"gone": [지운 자동 줄 키 — `sec:G` · `fee` · `mat:` 는 안 받음(자재 줄 주인 = 대비표 표시)]}
|
||||
덮개 없음 = 기본 표 그대로 · [초기화] = `bill` 지움 ·
|
||||
`rows` 에 없고 `gone` 에도 없는 자동 줄(B08 새 열 · 대비표 새 표시) = 기본 부모 끝 + 비고 「새 자동 줄」
|
||||
(기본 부모가 없으면 표 끝 맨 위) · 원천이 안 낸 키 = 안 보임 ·
|
||||
`rows` 에 없고 `gone` 에도 없는 자동 줄(B08 새 열 · 대비표 새 표시) =
|
||||
기본 부모 끝 + 비고 「새 자동 줄」(기본 부모가 없으면 표 끝 맨 위) ·
|
||||
원천이 안 낸 키 = 안 보임 ·
|
||||
부모 못 찾음 · 묶음 아님 · 돎 = 자동 줄 기본 부모 / 새 줄 맨 위 · `sec:G` = 맨 위 고정 ·
|
||||
관급 자재 줄 = `sec:G` 밖 못 감(밖이면 `sec:G` 아래) · 깊이 5 넘음 = 레벨 4 조상 아래.
|
||||
옛 꼴(v 없음 · 166-2 · 182) = `qty{줄 키: 수}` 수량 덮개 + `rows[]` 손 줄(`section` · `after` 앞 줄
|
||||
뒤에 끼움 · `parent` = 같은 공종 손 묶음) — 그대로 읽음(화면 첫 손질 때 v2 로 굳힘).
|
||||
셈 출력 줄 칸(계약) = kind("title" 자동 묶음 · "group" 새 묶음 · "item") · key · name(번호 뺀 이름) ·
|
||||
parent(실제 부모 키 · None = 맨 위) · level(1~5 = 깊이) · no(레벨 1 · 2 묶음 차례 번호 「1」 · 「1.1」 ·
|
||||
관급 · 항목 = "") · cost("direct" | "supply" = 맨 위 조상 갈래 · `sec:G` 만 supply) ·
|
||||
옛 꼴(v 없음 · 166-2 · 182) = `qty{줄 키: 수}` 수량 덮개 + `rows[]` 손 줄(`section` ·
|
||||
`after` 앞 줄 뒤에 끼움 · `parent` = 같은 공종 손 묶음) — 그대로 읽음(화면 첫 손질 때 v2).
|
||||
셈 출력 줄 칸(계약) = kind("title" 자동 묶음 · "group" 새 묶음 · "item") · key ·
|
||||
name(번호 뺀 이름) · parent(실제 부모 키 · None = 맨 위) · level(1~5 = 깊이) ·
|
||||
no(레벨 1 · 2 묶음 차례 번호 「1」 · 「1.1」 · 관급 · 항목 = "") ·
|
||||
cost("direct" | "supply" = 맨 위 조상 갈래 · `sec:G` 만 supply) ·
|
||||
auto(True = 자동 줄) | hand(True = 새 줄) · auto_{name|spec|unit|qty|ref}(고친 칸만 = 자동값) ·
|
||||
section(가장 가까운 제목 공종 · 없으면 None).
|
||||
묶음 금액 = 자손 항목 합 + 「빈 줄 n」 · 구조물 블록 줄 키 꼬리 = 대가 블록 키(대가 번호 밀림에 안 흔들림).
|
||||
원가계산서로 넘길 합 = `sums`(cost direct 항목의 직접재료비 · 직접노무비 · 산출경비 · supply 항목의 관급
|
||||
순자재대 — 145-4 이음 꼴 · 빈 줄이 있으면 None) · 화면 표시용 = `totals`(아는 금액 합 · 빈 줄 수 · 조달수수료).
|
||||
묶음 금액 = 자손 항목 합 + 「빈 줄 n」 ·
|
||||
구조물 블록 줄 키 꼬리 = 대가 블록 키(대가 번호 밀림에 안 흔들림).
|
||||
원가계산서로 넘길 합 = `sums`(cost direct 항목의 직접재료비 · 직접노무비 · 산출경비 ·
|
||||
supply 항목의 관급 순자재대 — 145-4 이음 꼴 · 빈 줄이 있으면 None) ·
|
||||
화면 표시용 = `totals`(아는 금액 합 · 빈 줄 수 · 조달수수료).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -66,9 +74,11 @@ SECTIONS: list[tuple[str, str, str | None]] = [
|
||||
("4", "자재대", None),
|
||||
("G", "관급자재대", None),
|
||||
]
|
||||
#: 순공사비에 드는 맨 위 공종(관급자재대 = 제외금액)
|
||||
DIRECT = ("1", "2", "3", "4")
|
||||
PARTS = ("labor", "material", "expense")
|
||||
#: 184 덮개로 고칠 칸 · 묶음 깊이 한도
|
||||
FIELDS = ("name", "spec", "unit", "qty", "ref")
|
||||
DEPTH = 5
|
||||
NEW_AUTO = "새 자동 줄"
|
||||
|
||||
|
||||
def _amounts(qty: float | None, prices: dict[str, float | None] | None) -> dict[str, Any]:
|
||||
@@ -86,32 +96,29 @@ def _amounts(qty: float | None, prices: dict[str, float | None] | None) -> dict[
|
||||
return out
|
||||
|
||||
|
||||
def _item_row(
|
||||
item: dict[str, Any], prices: dict[str, Any] | None, gaps: list, qty_of: dict | None = None
|
||||
) -> dict[str, Any]:
|
||||
def _item_row(item: dict[str, Any], prices: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""항목 줄 — 「연결 없음」은 `_gaps` 에 담아 둠(보이는 줄만 표 차례로 모음)."""
|
||||
qty = num(item.get("qty"))
|
||||
over = {}
|
||||
if qty_of and item["key"] in qty_of and not item.get("hand"): # 166-2 수량 덮어쓰기
|
||||
over = {"qty_auto": qty}
|
||||
qty = num(qty_of[item["key"]])
|
||||
row = {
|
||||
"kind": "item",
|
||||
"key": item["key"],
|
||||
"section": item["section"],
|
||||
"section": item.get("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"),
|
||||
**over,
|
||||
**({"hand": True} if item.get("hand") else {}),
|
||||
**{k: v for k, v in item.items() if k.startswith("auto_")},
|
||||
**({"hand": True} if item.get("hand") else {"auto": True}),
|
||||
**_amounts(qty, prices),
|
||||
}
|
||||
row["_gaps"] = []
|
||||
if qty is None:
|
||||
gaps.append(gap("bill", item["key"], row["name"], "수량 연결 없음"))
|
||||
row["_gaps"].append(gap("bill", item["key"], row["name"], "수량 연결 없음"))
|
||||
if prices is None:
|
||||
gaps.append(gap("bill", item["key"], row["name"], item.get("why") or "단가 연결 없음"))
|
||||
why = item.get("why") or "단가 연결 없음"
|
||||
row["_gaps"].append(gap("bill", item["key"], row["name"], why))
|
||||
return row
|
||||
|
||||
|
||||
@@ -162,9 +169,11 @@ def ref_price(
|
||||
|
||||
|
||||
def hand_items(user: dict[str, Any], books: dict[str, Any], hopyo: dict) -> list[tuple]:
|
||||
"""손 줄 → (항목, 단가) — 연결 = 고른 호표(로직 = 새 산근 키로 풀어 둠)."""
|
||||
"""새 줄(`hand:`) → (항목, 단가) — 연결 = 고른 호표(로직 = 새 산근 키로 풀어 둠)."""
|
||||
out = []
|
||||
for hand in (user.get("bill") or {}).get("rows") or []:
|
||||
if not str(hand.get("key") or "").startswith("hand:"):
|
||||
continue
|
||||
price = ref_price(hand.get("ref"), books, hopyo, hand["key"])
|
||||
ref = {"tab": price["tab"], "key": price["key"]} if price and price.get("tab") else None
|
||||
why = WHY_LOGIC if (hand.get("ref") or {}).get("tab") == "logic" else WHY_HAND
|
||||
@@ -173,12 +182,51 @@ def hand_items(user: dict[str, Any], books: dict[str, Any], hopyo: dict) -> list
|
||||
return out
|
||||
|
||||
|
||||
def _covered(
|
||||
item: dict[str, Any], prices: Any, cover: dict | None, books: dict, hopyo: dict
|
||||
) -> tuple[dict[str, Any], Any]:
|
||||
"""자동 줄 덮개(184) — 고친 칸 = 그 값(null = 빈칸) · `auto_{칸}` = 자동값 ·
|
||||
연결 덮개 = 그 단가(자재 줄 = 단가 주인 대비표라 연결 안 받음)."""
|
||||
if not cover:
|
||||
return item, prices
|
||||
out = dict(item)
|
||||
for field in FIELDS:
|
||||
if field not in cover or (field == "ref" and item["key"].startswith("mat:")):
|
||||
continue
|
||||
if field == "qty":
|
||||
out["auto_qty"] = num(item.get("qty"))
|
||||
elif field == "ref":
|
||||
out["auto_ref"] = item.get("ref")
|
||||
else:
|
||||
out[f"auto_{field}"] = item.get(field) or ""
|
||||
out[field] = cover[field]
|
||||
if "auto_ref" in out:
|
||||
prices = ref_price(cover["ref"], books, hopyo, item["key"])
|
||||
tab = (prices or {}).get("tab")
|
||||
out["ref"] = {"tab": tab, "key": prices["key"]} if tab else None
|
||||
out["note"] = (prices or {}).get("no") or ""
|
||||
if (cover["ref"] or {}).get("tab") == "logic":
|
||||
out["why"] = WHY_LOGIC
|
||||
return out, prices
|
||||
|
||||
|
||||
def _title_row(code: str, name: str, parent: str | None, cover: dict | None) -> dict[str, Any]:
|
||||
"""기본 공종 제목(자동 묶음) — 이름 덮개만 · 번호는 `no`(차례 자동)."""
|
||||
row = {"kind": "title", "key": f"sec:{code}", "section": code, "name": name, "auto": True}
|
||||
if cover and "name" in cover:
|
||||
row["auto_name"] = name
|
||||
row["name"] = cover["name"] or ""
|
||||
row["parent"] = f"sec:{parent}" if parent else None
|
||||
row["_home"] = row["parent"]
|
||||
return row
|
||||
|
||||
|
||||
def _group_row(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"""묶음 손 줄(182) — 이름만 · 금액 = 자손 항목 합(`_nest`)."""
|
||||
"""새 묶음 줄(182 · 184) — 이름만 · 금액 = 자손 항목 합."""
|
||||
return {
|
||||
"kind": "group",
|
||||
"key": item["key"],
|
||||
"section": item["section"],
|
||||
"section": item.get("section"),
|
||||
"name": item.get("name") or "",
|
||||
"spec": item.get("spec") or "",
|
||||
"unit": item.get("unit") or "",
|
||||
@@ -186,51 +234,17 @@ def _group_row(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"note": "",
|
||||
"ref": None,
|
||||
"hand": True,
|
||||
"sum": 0.0,
|
||||
"labor": 0.0,
|
||||
"material": 0.0,
|
||||
"expense": 0.0,
|
||||
"blank": 0,
|
||||
}
|
||||
|
||||
|
||||
def _nest(rows: list[dict[str, Any]]) -> None:
|
||||
"""182 레벨 — 부모 = 같은 공종 묶음 손 줄(못 찾거나 돌면 공종 바로 아래) · level · 묶음 금액."""
|
||||
groups = {row["key"]: row for row in rows if row["kind"] == "group"}
|
||||
for row in rows:
|
||||
if not row.get("hand"):
|
||||
continue
|
||||
up = groups.get(row.get("parent") or "")
|
||||
if up is None or up["section"] != row["section"]:
|
||||
row["parent"] = None
|
||||
continue
|
||||
at, n = up, 0
|
||||
while at is not None and n <= len(groups):
|
||||
if at["key"] == row["key"]:
|
||||
row["parent"] = None
|
||||
break
|
||||
at, n = groups.get(at.get("parent") or ""), n + 1
|
||||
for row in rows:
|
||||
chain = []
|
||||
at = groups.get(row.get("parent") or "")
|
||||
while at is not None:
|
||||
chain.append(at)
|
||||
at = groups.get(at.get("parent") or "")
|
||||
depth = 2 if next(p for c, _n, p in SECTIONS if c == row["section"]) else 1
|
||||
row["level"] = depth if row["kind"] == "title" else depth + 1 + len(chain)
|
||||
if row["kind"] != "item":
|
||||
continue
|
||||
for g in chain:
|
||||
for p in ("sum", *PARTS):
|
||||
g[p] += row[p] or 0
|
||||
if row["sum"] is None:
|
||||
g["blank"] += 1
|
||||
for g in groups.values():
|
||||
g["note"] = f"빈 줄 {g['blank']}" if g["blank"] else ""
|
||||
def _hand_row(item: dict[str, Any], prices: Any, why: str) -> dict[str, Any]:
|
||||
if item.get("kind") == "group":
|
||||
return _group_row(item)
|
||||
return _item_row({**item, "why": why}, prices)
|
||||
|
||||
|
||||
def _place(items: list[dict[str, Any]], hand: dict[str, Any], after: str) -> None:
|
||||
"""손 줄 끼우기 — `after` 줄 바로 뒤 · 제목(`sec:`) = 그 공종 첫 자리 · 못 찾으면 끝."""
|
||||
"""옛 꼴 손 줄 끼우기 — `after` 줄 바로 뒤 · 제목(`sec:`) = 그 공종 첫 자리 · 못 찾으면 끝."""
|
||||
at = next((n + 1 for n, row in enumerate(items) if row["key"] == after), None)
|
||||
if at is None and after == f"sec:{hand['section']}":
|
||||
at = next((n for n, row in enumerate(items) if row["section"] == hand["section"]), None)
|
||||
@@ -239,21 +253,23 @@ def _place(items: list[dict[str, Any]], hand: dict[str, Any], after: str) -> Non
|
||||
|
||||
def split_by_block(item: dict[str, Any], price_of: PriceOf) -> list[tuple[dict, dict | None]]:
|
||||
"""구조물 열 → 대가 블록마다 한 줄 — 수량 = 그 블록 사본 주인 값 합(ROUND 3) ·
|
||||
대가 없는 주인은 한 줄로 모음(단가 빈칸 「연결 없음」) · 차례 = 주인이 처음 나온 차례."""
|
||||
대가 없는 주인은 한 줄로 모음(단가 빈칸 「연결 없음」) · 차례 = 주인이 처음 나온 차례 ·
|
||||
줄 키 꼬리 = 대가 블록 키(184 — 대가 번호가 밀려도 덮개가 같은 줄)."""
|
||||
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 ""
|
||||
tag = str(price.get("key") or 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():
|
||||
no = str((group["price"] or {}).get("no") or "")
|
||||
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),
|
||||
"note": " · ".join(t for t in (no, 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) — 이동 · 집계표 역전개
|
||||
@@ -262,6 +278,114 @@ def split_by_block(item: dict[str, Any], price_of: PriceOf) -> list[tuple[dict,
|
||||
return out
|
||||
|
||||
|
||||
def _order_old(bill: dict, titles: list[dict], items: list[dict], hands: list[tuple]) -> list:
|
||||
"""옛 꼴(166-2 · 182) — 손 줄 = `after` 뒤 · 부모 = 같은 공종 손 묶음(아니면 공종 제목)."""
|
||||
groups = {i["key"]: i for i, _p, _w in hands if i.get("kind") == "group"}
|
||||
for item, prices, why in hands:
|
||||
row = _hand_row(item, prices, why)
|
||||
up = groups.get(item.get("parent") or "")
|
||||
row["parent"] = up["key"] if up and up.get("section") == item.get("section") else None
|
||||
_place(items, row, item.get("after") or "")
|
||||
out = []
|
||||
for title in titles:
|
||||
out.append(title)
|
||||
for row in items:
|
||||
if row["section"] == title["section"]:
|
||||
row["_home"] = title["key"]
|
||||
row["parent"] = row.get("parent") or title["key"]
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
def _order_v2(bill: dict, titles: list[dict], items: list[dict], hands: list[tuple]) -> list:
|
||||
"""184 v2 — 저장 차례 · 부모 · `gone` 뺌 · 저장에 없는 자동 줄 = 기본 부모 끝 「새 자동 줄」."""
|
||||
auto = {row["key"]: row for row in titles}
|
||||
for row in items:
|
||||
row["_home"] = f"sec:{row['section']}"
|
||||
auto[row["key"]] = row
|
||||
gone = {k for k in bill.get("gone") or [] if k != "sec:G" and not k.startswith("mat:")}
|
||||
new = {item["key"]: (item, prices, why) for item, prices, why in hands}
|
||||
out, seen = [], set()
|
||||
for saved in bill.get("rows") or []:
|
||||
key = saved["key"]
|
||||
if key in seen:
|
||||
continue
|
||||
if key in new:
|
||||
row = _hand_row(*new[key])
|
||||
row["_home"] = None
|
||||
elif key in auto and key not in gone:
|
||||
row = auto[key]
|
||||
else:
|
||||
continue
|
||||
seen.add(key)
|
||||
row["parent"] = saved.get("parent")
|
||||
out.append(row)
|
||||
for row in [*titles, *items]:
|
||||
if row["key"] in seen or row["key"] in gone:
|
||||
continue
|
||||
row["parent"] = row["_home"]
|
||||
if row["kind"] == "item":
|
||||
row["note"] = " · ".join(t for t in (row["note"], NEW_AUTO) if t)
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
def _tree(order: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""부모 바로잡기(없음 · 항목 · 돎 = 기본 부모 / 맨 위 · `sec:G` 맨 위 ·
|
||||
관급 자재 줄 = `sec:G` 안) → 표 차례(부모 먼저 · 형제 = 저장 차례) · level · cost · section
|
||||
(깊이 5 넘음 = 레벨 4 조상 아래)."""
|
||||
by_key = {row["key"]: row for row in order}
|
||||
|
||||
def loops(row: dict[str, Any]) -> bool:
|
||||
at, n = by_key.get(row["parent"] or ""), 0
|
||||
while at is not None and n <= len(order):
|
||||
if at is row:
|
||||
return True
|
||||
at, n = by_key.get(at["parent"] or ""), n + 1
|
||||
return False
|
||||
|
||||
def root(row: dict[str, Any]) -> dict[str, Any]:
|
||||
while row["parent"]:
|
||||
row = by_key[row["parent"]]
|
||||
return row
|
||||
|
||||
for row in order:
|
||||
up = by_key.get(row.get("parent") or "")
|
||||
if row["key"] == "sec:G":
|
||||
row["parent"] = None
|
||||
elif up is None or up["kind"] == "item" or up is row:
|
||||
row["parent"] = row["_home"] if row["_home"] in by_key else None
|
||||
for row in order:
|
||||
if loops(row):
|
||||
row["parent"] = row["_home"] if row["_home"] in by_key else None
|
||||
if loops(row):
|
||||
row["parent"] = None
|
||||
for row in order:
|
||||
if (
|
||||
row["key"].startswith("mat:")
|
||||
and row["_home"] == "sec:G"
|
||||
and root(row)["key"] != "sec:G"
|
||||
):
|
||||
row["parent"] = "sec:G"
|
||||
kids: dict[Any, list[dict[str, Any]]] = {}
|
||||
for row in order:
|
||||
kids.setdefault(row["parent"], []).append(row)
|
||||
rows: list[dict[str, Any]] = []
|
||||
stack: list[tuple[dict, list]] = [(row, []) for row in reversed(kids.get(None, []))]
|
||||
while stack:
|
||||
row, chain = stack.pop()
|
||||
if len(chain) >= DEPTH:
|
||||
chain = chain[: DEPTH - 1]
|
||||
row["parent"] = chain[-1]["key"]
|
||||
row["level"] = len(chain) + 1
|
||||
row["cost"] = "supply" if (chain[0] if chain else row)["key"] == "sec:G" else "direct"
|
||||
if row["kind"] != "title":
|
||||
row["section"] = chain[-1]["section"] if chain else None
|
||||
rows.append(row)
|
||||
stack.extend((kid, [*chain, row]) for kid in reversed(kids.get(row["key"], [])))
|
||||
return rows
|
||||
|
||||
|
||||
def compute_bill(
|
||||
inputs: dict[str, Any],
|
||||
user: dict[str, Any],
|
||||
@@ -272,30 +396,30 @@ def compute_bill(
|
||||
"""`price_of(사본 주인 id)` = 대가 1 단위 단가(145-2 `daega_price`) —
|
||||
주면 구조물 열을 블록마다 나눔 ·
|
||||
`books` = 호표 목록(`daega` · `sg_list` · `sg_hand` · 166-2)."""
|
||||
gaps: list[dict[str, str]] = []
|
||||
hopyo = inputs.get("hopyo") or {}
|
||||
books = books or {}
|
||||
qty_of = (user.get("bill") or {}).get("qty") or {}
|
||||
bill = user.get("bill") or {}
|
||||
v2 = bill.get("v") == 2
|
||||
if v2:
|
||||
cover = {r["key"]: r for r in bill.get("rows") or [] if not r["key"].startswith("hand:")}
|
||||
else:
|
||||
cover = {k: {"qty": v} for k, v in (bill.get("qty") or {}).items()}
|
||||
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, qty_of))
|
||||
continue
|
||||
prices = ref_price(item.get("ref"), books, hopyo)
|
||||
items.append(_item_row(item, prices, gaps, qty_of))
|
||||
parts = split_by_block(item, price_of)
|
||||
else:
|
||||
parts = [(item, ref_price(item.get("ref"), books, hopyo))]
|
||||
for part, prices in parts:
|
||||
items.append(_item_row(*_covered(part, prices, cover.get(part["key"]), books, hopyo)))
|
||||
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, qty_of))
|
||||
for item, prices, why in hand_items(user, books, hopyo):
|
||||
if item.get("kind") == "group":
|
||||
row = _group_row(item)
|
||||
else:
|
||||
row = _item_row({**item, "why": why}, prices, gaps)
|
||||
row["parent"] = item.get("parent")
|
||||
_place(items, row, item.get("after") or "")
|
||||
owner = [row for row in items if row["section"] == "G" and row["kind"] == "item"]
|
||||
items.append(_item_row(*_covered(item, prices, cover.get(item["key"]), books, hopyo)))
|
||||
titles = [_title_row(c, n, p, cover.get(f"sec:{c}")) for c, n, p in SECTIONS]
|
||||
hands = hand_items(user, books, hopyo)
|
||||
rows = _tree((_order_v2 if v2 else _order_old)(bill, titles, items, hands))
|
||||
owner = [row for row in rows if row["kind"] == "item" and row["cost"] == "supply"]
|
||||
net = 0.0
|
||||
for row in owner:
|
||||
net += row["material"] or 0
|
||||
@@ -311,6 +435,7 @@ def compute_bill(
|
||||
"qty": pct,
|
||||
"note": "관급 순자재대 밑수",
|
||||
"ref": None,
|
||||
"auto": True,
|
||||
"price_labor": 0,
|
||||
"price_material": net,
|
||||
"price_expense": 0,
|
||||
@@ -319,53 +444,54 @@ def compute_bill(
|
||||
"material": fee,
|
||||
"expense": 0 if pct is not None else None,
|
||||
"sum": fee,
|
||||
"parent": "sec:G",
|
||||
"level": 2,
|
||||
"cost": "supply",
|
||||
"_gaps": [gap("bill", "fee", "조달수수료", "조달수수료율 입력 없음")]
|
||||
if pct is None
|
||||
else [],
|
||||
}
|
||||
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:
|
||||
if owner: # 조달수수료 = `sec:G` 끝 고정
|
||||
at = next(n for n, row in enumerate(rows) if row["key"] == "sec:G") + 1
|
||||
while at < len(rows) and rows[at]["level"] > 1:
|
||||
at += 1
|
||||
rows.insert(at, fee_row)
|
||||
by_key = {row["key"]: row for row in rows}
|
||||
for row in rows:
|
||||
if row["kind"] != "item":
|
||||
continue
|
||||
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)
|
||||
_nest(rows)
|
||||
row.update({"sum": 0.0, "labor": 0.0, "material": 0.0, "expense": 0.0, "blank": 0})
|
||||
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"]
|
||||
for row in rows:
|
||||
if row["kind"] != "item":
|
||||
continue
|
||||
at = by_key.get(row["parent"] or "")
|
||||
while at is not None:
|
||||
for p in ("sum", *PARTS):
|
||||
at[p] += row[p] or 0
|
||||
if row["sum"] is None:
|
||||
at["blank"] += 1
|
||||
at = by_key.get(at["parent"] or "")
|
||||
if row["cost"] == "direct":
|
||||
for p in direct:
|
||||
direct[p] += row[p] or 0
|
||||
if row["sum"] is None:
|
||||
blank += 1
|
||||
gaps: list[dict[str, str]] = []
|
||||
seq: dict[Any, int] = {}
|
||||
for row in rows:
|
||||
gaps.extend(row.pop("_gaps", []))
|
||||
row.pop("_home", None)
|
||||
row["no"] = ""
|
||||
if row["kind"] == "title":
|
||||
empty = f" 빈 줄 {row['blank']}" if row["blank"] else ""
|
||||
row["note"] = ("(제외금액)" if row["key"] == "sec:G" else "") + empty
|
||||
elif row["kind"] == "group":
|
||||
row["note"] = f"빈 줄 {row['blank']}" if row["blank"] else ""
|
||||
if row["kind"] != "item" and row["cost"] == "direct" and row["level"] <= 2:
|
||||
up = row["parent"] # 번호 = 레벨 1 · 2 묶음 차례(1 · 1.1 · 관급 없음)
|
||||
seq[up] = seq.get(up, 0) + 1
|
||||
row["no"] = f"{by_key[up]['no']}.{seq[up]}" if up else str(seq[up])
|
||||
totals = {
|
||||
"direct": {**direct, "blank": blank},
|
||||
"owner": {"net_material": net, "fee": fee, "total": net + (fee or 0)},
|
||||
|
||||
Reference in New Issue
Block a user