- 원천(서버 단독) = 연결된 사본 → 사본 셈 한 벌(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
198 lines
8.0 KiB
Python
198 lines
8.0 KiB
Python
"""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")},
|
||
}
|