Files
Aislo/B09_Estimation/B09_Estimation_Calc_Labor.py
T
eomsangdonandClaude Opus 5.5 a3047f5141 feat(B09): 일위대가표 · 일위대가목록표 — 사본 일위대가 → 조합 · 사전조합 · 품셈 블록 · B09 굳힌 단가 · 엑셀 끝수(145-2)
- 원천(서버 단독) = 연결된 사본 → 사본 셈 한 벌(142-1) 수량 ÷ 배 · 같은 원본 + 같은 입력값 = 한 호표(내용 해시)
  승격 아닌 로직 = 품셈 호표(노무 · 재료 · 중기 · % 줄) · 승격 · 옛 SP = 산근 참조 · SC = 겹 대가
- 셈 짝(파이썬 · TS) = 줄 ROUNDDOWN 1 · % 줄 = 밑수 × % / 100 · 합계 ROUNDDOWN 0 · 관급 · 별도계상 = 대가 안 0원 · 빈칸 규칙
- 내역서 이음 daega_price(sheets, owner) / daegaPrice · 집계표 이음 = 블록 key · 줄 ref.key
- 탭 둘(목록표 · 블록표 · 호표 누름 이동 · 사본 없으면 「연결 없음」) · 길 = GET · PUT 셈에 대가
- 단가 수요(used_keys) = 사본 셈과 같은 길(빈 입력 자동값 · 참조사본 안 로직) — 144 시험 옛 단언 고침
- 새 시험 test_145_2_b09_labor_calc(엑셀 대가 1 · 2 · 9 · 14 검산 · 거울 · 견본 사본 · 빈 표 · 이음 꼴)

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

198 lines
8.0 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` {키: 값})
% 줄 = 밑수(그 블록 노무 줄 금액 합) × % / 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']]}"
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']}",
**{k: row[k] for k in ("labor", "material", "expense", "sum")},
}