Files
Aislo/B09_Estimation/B09_Estimation_Calc_Labor.py
T
eomsangdonandClaude Opus 5.5 969d7f74f3 feat(B09 일위대가): 고친 칸 덮개 셈 — 사용자 값 daega_cover · 블록 cover · 줄 cid · 파이썬·TS 짝 (PLAN 702-2)
- 덮개 자리 = B09 사용자 값(user_values.json · 화면 캐시와 같은 user) {블록 cover: {줄 cid: {qty · 노 · 재 · 경 단가}}} · 원본 층 · 사본 파일 안 건드림(브레인 승인)
- 블록 cover = 조합 S:사본 폴더 · 서고 L:폴더 · 사전조합 C:참조 · 품셈 P:로직+입력 해시 · 줄 cid = 줄 정체(코드 · 산근 로직 · 부른 대가 로직) + n번째 — 내용 해시 밖(호표 id 그대로)
- 셈 짝 Calc_Labor.py · .ts — 고친 칸만 셈 값 대신 · 줄 base(품셈 값) · covered · 짝 잃은 덮개 daega_orphans(지우지 않음) · 데이터 점검 알림
- [저장] = 서버가 덮개 입력값만 받아 다시 셈(Store.clean_cover 수만) · 노임 새 판 · [재적용] 뒤에도 남음
- B08 구조물도 카드용 고친 칸 수 cover_count
- 시험 test_702_b09_daega_cover(거울 · 판 바꿈 · 재적용 꼴 · 다듬기) · test_145_2 _ts 덮개 인자

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:41:22 +09:00

240 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-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 · 「연결 없음」 목록(gaps).
빈 줄(166-6) = 블록 합 = 아는 줄 합 + `blank` n · `blank_names` — 내역 단가 · 부른 대가로는 안 넘김.
고친 칸 덮개(702-2) = 사용자 값 `daega_cover` {블록 cover: {줄 cid: {qty · 노 · 재 · 경 단가}}} —
있는 칸만 셈 값 대신 · 줄 `base`(품셈 값 = 셈 수량 · 단가) · `covered`(고친 칸) ·
짝 잃은 덮개(그 블록 · 줄 없음) = `daega_orphans` [{cover, cid}] — 지우지 않고 알림만.
"""
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")
COVER_KEYS = ("qty", *PARTS)
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] = None if row.get("blank") else 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 _cover(line: dict[str, Any], own: dict[str, Any], qty: Any, prices: dict) -> tuple:
"""덮개 한 줄 — (수량, 단가, 덧칸) · 덧칸 = `base`(품셈 값) · `covered`(고친 칸 차례)."""
mine = own.get(line.get("cid")) or {}
keys = [k for k in COVER_KEYS if k in mine]
if not keys:
return qty, prices, {}
extra = {"base": {"qty": qty, **prices}, "covered": keys}
qty = num(mine["qty"]) if "qty" in mine else qty
return qty, {p: num(mine[p]) if p in mine else v for p, v in prices.items()}, extra
def _block(
block: dict[str, Any],
books: dict[str, Any],
sums: dict[str, dict],
gaps: list,
own: dict[str, Any],
) -> dict[str, Any]:
lines = []
for line in block["lines"]:
if line["kind"] == "pct":
continue
qty, prices, extra = _cover(line, own, num(line.get("qty")), _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, **extra})
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)
mine = own.get(line.get("cid")) or {}
pct = num(mine["qty"]) if "qty" in mine else num(line.get("qty"))
extra = {"base": {"qty": num(line.get("qty"))}, "covered": ["qty"]} if "qty" in mine else {}
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, **extra})
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()))
known = [ln for ln in lines if ln["amount"] is not None]
parts = {part: rounddown(sum(ln["amounts"][part] for ln in known), 0) for part in PARTS}
blank_names = [ln.get("name") or "" for ln in lines if ln["amount"] is None]
return {
**{k: block.get(k) for k in ("id", "kind", "name", "spec", "unit", "cite", "cover")},
**({"spec_raw": block["spec_raw"]} if block.get("spec_raw") else {}), # 157-4 풍선
"key": block["id"],
"lines": lines,
**parts,
"sum": sum(parts.values()),
"blank": len(blank_names),
"blank_names": blank_names,
}
def compute_labor(
daega: dict[str, Any] | None,
sheets: dict[str, Any],
sangeun: dict[str, Any] | None = None,
cover: 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 {},
}
cover = cover 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, cover.get(by_id[bid].get("cover")) or {})
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"],
**({"spec_raw": b["spec_raw"]} if b.get("spec_raw") else {}),
"unit": b["unit"],
"sum": b["sum"],
"labor": b["labor"],
"material": b["material"],
"expense": b["expense"],
"blank": b["blank"],
"blank_names": b["blank_names"],
}
for b in out_blocks
]
cids: dict[str, set] = {}
for b in blocks:
cids.setdefault(b.get("cover"), set()).update(ln.get("cid") for ln in b["lines"])
orphans = [
{"cover": key, "cid": cid}
for key, lines in cover.items()
for cid in lines
if cid not in cids.get(key, ())
]
return {
"daega_blocks": out_blocks,
"daega": listing,
"daega_owners": dict(daega.get("owners") or {}),
"daega_orphans": orphans,
"gaps": [*(sheets.get("gaps") or []), *gaps],
}
def daega_price(sheets: dict[str, Any], owner: str) -> dict[str, Any] | None:
"""내역서(145-3)가 읽는 대가 단가 — 구조물 id(연결된 사본 주인) → 호표 · 비목 단가.
사본이 없으면 None · 빈 줄 있는 블록 = 비목 단가 빈칸(166-6 · 아는 줄 합은 안 넘김)."""
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: None if row.get("blank") else row[k] for k in ("labor", "material", "expense", "sum")
},
}