Files
Aislo/B09_Estimation/B09_Estimation_Calc_Labor.py
T
eomsangdonandClaude Opus 5.5 82a3cb5e7d feat(b09): 호표 갈래 뒤집기 — 일위대가화 줄 = 대가 · 단가산출 줄 = 산근(155-3 · 155-4)
- 일위대가목록 = 구조물 조합 + 사전조합 + 일위대가화(승격 · 옛 SP) 줄 · 단가산출 줄 = 산근 줄
- 산근 줄 수량 = 수량 그대로(배 ≠ 1 로직 금액 1/배 흠) · 대가 줄만 ÷ 배
- 품셈 블록 로직 줄 판정 = 산근 풀이와 같은 call_of(기계경비 = 중기 · 그 밖 = 하위 산근)
- 비고 「산근 n호표」(Calc_Labor 파이썬 · TS 짝) · 산근 줄 누름 = 단가산출근거 탭
- 산근목록 원천 = 단가산출 줄 + 사전조합 안 줄 + 일위대가화 줄 하위 로직(노무 · 자재 · 중기 줄 제외)
- 산근 자재 자리표시 키(MO:경유:…) = 자재단가대비표 키로
- 시험: 145-1 다섯 · 145-2 셋 새 갈래로(머리에 근거 한 줄) · 새 test_155_b09_sources

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
2026-10-09 19:20:39 +09:00

203 lines
8.3 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-2) — 화면 없음 · 저장 안 함 · 파일 · M01 안 읽음.
⚠ 짝 = `B09_Estimation_Calc_Labor.ts`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 ·
거울 시험 `resources/tester/test_145_2_b09_labor_calc.py`.
조작 중은 화면 짝이 바로 셈 · [저장] 때 서버가 이 파일로 정본을 다시 셈.
입력 = 대가 블록(`B09_Estimation_Labor_Source.daega_inputs` — 줄 꼴 · 수량만)
+ 바탕 표(144 굳힌 단가):
노무 줄 = 노무비목록표 단가 · 재료 줄 = 재료비목록표 단가 ·
중기 줄 = 중기목록표(노무 · 재료 · 경비)
대가 줄 = 부른 블록 합계(노무 · 재료 · 경비) · 산근 줄 = 145-1 산근 단가(`sangeun` {키: 값})
비고 = 대가 줄 「대가n」 · 산근 줄 「산근 n호표」(산근 목록 번호)
% 줄 = 밑수(그 블록 노무 줄 금액 합) × % / 100
끝수 = 엑셀 자리 그대로 — 줄 금액 = ROUNDDOWN(단가 × 수량, 1) ·
% 줄 = ROUNDDOWN(밑수 × % / 100, 1) · 관급 · 별도계상 자재 = 대가 안 0원 ·
블록 비목 합 = ROUNDDOWN(Σ, 0) · 합계 = 세 비목 합.
빈칸 = 단가 · 수량을 낸 원천이 없으면 None · 그 칸을 쓰는 합도 None · 「연결 없음」 목록(gaps).
"""
from __future__ import annotations
from typing import Any
from B09_Estimation.B09_Estimation_Calc_Base import SUPPLY, gap, num, rounddown, total
PARTS = ("labor", "material", "expense")
TAB = "dg"
WHY = {
"labor": "노임 단가 없음",
"material": "자재 적용 단가 없음",
"machine": "중기 시간 단가 없음",
"daega": "부른 대가 합계 없음",
"sangeun": "산근 단가 없음(145-1)",
}
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")
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 {}
for part in PARTS:
out[part] = num(row.get(part))
else: # blank — 못 펼친 줄
out = dict.fromkeys(PARTS, None)
return out
def _base(spec: str, lines: list[dict[str, Any]]) -> float | None:
"""% 줄 밑수 — `part:비목` 그 비목 줄 금액 합 · `lines:labor` 노무 줄 금액 합 ·
`line:이름` 그 줄."""
kind, _, name = spec.partition(":")
if kind == "part":
return total([ln["amounts"][name] for ln in lines])
if kind == "line":
return next(
(total(list(ln["amounts"].values())) for ln in lines if ln.get("name") == name), None
)
return total([ln["amounts"]["labor"] for ln in lines if ln["kind"] == "labor"])
def _block(
block: dict[str, Any],
books: dict[str, Any],
sums: dict[str, dict],
gaps: list,
) -> dict[str, Any]:
lines = []
for line in block["lines"]:
if line["kind"] == "pct":
continue
qty = num(line.get("qty"))
prices = _prices(line, books, sums)
amounts = {
part: None if p is None or qty is None else rounddown(p * qty, 1)
for part, p in prices.items()
}
ref = {"ref": {"key": line["key"]}} if line.get("key") else {} # 집계표(145-5) 이음
lines.append({**line, **ref, "qty": qty, "prices": prices, "amounts": amounts})
if qty is None or None in prices.values():
why = line.get("why") or (
"수량 없음" if qty is None else WHY.get(line["kind"], "단가 없음")
)
gaps.append(gap(TAB, block["id"], f"{block['name']} · {line.get('name') or ''}", why))
for line in block["lines"]:
if line["kind"] != "pct":
continue
base = _base(line.get("base") or "lines:labor", lines)
pct = num(line.get("qty"))
prices = dict.fromkeys(PARTS, 0.0)
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)
lines.append({**line, "qty": pct, "prices": prices, "amounts": amounts})
if amounts[part] is None:
gaps.append(
gap(TAB, block["id"], f"{block['name']} · {line.get('name')}", "% 밑수 없음")
)
for line in lines:
line["price"] = total(list(line["prices"].values()))
line["amount"] = total(list(line["amounts"].values()))
parts: dict[str, float | None] = {}
for part in PARTS:
s = total([ln["amounts"][part] for ln in lines])
parts[part] = None if s is None else rounddown(s, 0)
return {
**{k: block.get(k) for k in ("id", "kind", "name", "spec", "unit", "cite")},
"key": block["id"],
"lines": lines,
**parts,
"sum": total([parts[p] for p in PARTS]),
}
def compute_labor(
daega: dict[str, Any] | None,
sheets: dict[str, Any],
sangeun: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""대가 탭 둘 — `daega_blocks`(일위대가표) · `daega`(목록표) · `daega_owners`
+ 바탕 gaps 이어 붙임.
블록은 부른 블록을 먼저 셈(겹 대가) · 순환은 빈칸 · 번호 = 입력 차례(대가1 …)."""
daega = daega or {}
blocks = daega.get("blocks") or []
books = {
"labor": {r["key"]: r for r in sheets.get("labor") or []},
"material": {r["key"]: r for r in sheets.get("material") or []},
"machine": {r["key"]: r for r in sheets.get("machine") or []},
"sangeun": sangeun or {},
}
by_id = {b["id"]: b for b in blocks}
no = {b["id"]: n for n, b in enumerate(blocks, 1)}
done: dict[str, dict[str, Any]] = {}
gaps: list[dict[str, str]] = []
def run(bid: str, trail: tuple[str, ...]) -> None:
if bid in done or bid in trail or bid not in by_id:
return
for line in by_id[bid]["lines"]:
if line["kind"] == "daega":
run(line.get("key"), (*trail, bid))
done[bid] = _block(by_id[bid], books, done, gaps)
for b in blocks:
run(b["id"], ())
out_blocks = []
for b in blocks:
got = {**done[b["id"]], "no": no[b["id"]]}
for line in got["lines"]:
if line["kind"] == "daega" and line.get("key") in no:
line["ref_no"] = f"대가{no[line['key']]}"
sg_no = (books["sangeun"].get(line.get("key")) or {}).get("no")
if line["kind"] == "sangeun" and sg_no is not None:
line["ref_no"] = f"산근 {sg_no}호표" # 155-3 비고(엑셀 꼴)
out_blocks.append(got)
listing = [
{
"no": b["no"],
"key": b["id"],
"kind": b["kind"],
"name": b["name"],
"spec": b["spec"],
"unit": b["unit"],
"sum": b["sum"],
"labor": b["labor"],
"material": b["material"],
"expense": b["expense"],
}
for b in out_blocks
]
return {
"daega_blocks": out_blocks,
"daega": listing,
"daega_owners": dict(daega.get("owners") or {}),
"gaps": [*(sheets.get("gaps") or []), *gaps],
}
def daega_price(sheets: dict[str, Any], owner: str) -> dict[str, Any] | None:
"""내역서(145-3)가 읽는 대가 단가 — 구조물 id(연결된 사본 주인) → 호표 · 비목 단가.
사본이 없으면 None."""
bid = (sheets.get("daega_owners") or {}).get(owner)
row = next((r for r in sheets.get("daega") or [] if r["key"] == bid), None)
if row is None:
return None
return {
"no": f"대가{row['no']}",
"key": bid,
**{k: row[k] for k in ("labor", "material", "expense", "sum")},
}