- 복사 = 덮개 K:… 의 @copy {from · name · spec} + 원본 덮개째(B09 사용자 값 · 브레인 정함) — 셈 짝 Calc_Labor py·ts 가 원본 블록을 본뜬 새 호표(번호 끝 · 기본값 = 원본) · 이 줄 단가연결을 새 호표로 바꿀지 물음 · 복사본 지우기(잇는 줄 있으면 막음)
- 원본 없음 = 짝 잃음 알림 · 저장 다듬기 @copy 글만 · 서고 사본 원본 = prune 에서 지킴
- 내 자료집 = POST …/estimation/daega/personal → 개인 층 사전조합(B09_Estimation_Daega_Personal) — 넣을 값 · 고른 자재 · 줄 수량 덮개(손) 담음 · 품 · 단가 덮개 = 안 담은 수 알림 · 같은 이름 409 → 덮어쓰기 · 새 이름
- 어디 쓰였나 = 누른 줄 노임 · 자재 · 중기(그 밖 = 호표)를 쓰는 내역 줄 목록 → 누르면 그 줄로 · 펼침
- 새 파일 B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts · 시험 test_703_b09_daega_copy(거울 · 임시 storage 길)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
259 lines
12 KiB
Python
259 lines
12 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` {키: 값})
|
||
비고 = 대가 줄 「대가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}] — 지우지 않고 알림만.
|
||
복사본(703-2) = 덮개 `K:…` 의 `@copy` {from · name · spec} — 원본 cover 블록을 본뜬 새 호표
|
||
(id `DG:K:…` · 번호 = 끝 · 줄 · 기본값 = 원본 · 덮개 = 제 것) · 원본 없음 = 짝 잃음 {cover, cid: @copy}.
|
||
"""
|
||
|
||
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"
|
||
COPY = "@copy"
|
||
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 with_copies(blocks: list[dict[str, Any]], cover: dict[str, Any]) -> list[dict[str, Any]]:
|
||
"""복사본(703-2) — 덮개 차례대로 `@copy` 마다 원본 블록을 본떠 끝에 더함(복사본의 복사본 됨)."""
|
||
out = list(blocks)
|
||
for key, lines in cover.items():
|
||
meta = (lines or {}).get(COPY)
|
||
if not isinstance(meta, dict):
|
||
continue
|
||
src = next((b for b in out if b.get("cover") == meta.get("from")), None)
|
||
if src is not None:
|
||
spec = meta["spec"] if "spec" in meta else src["spec"]
|
||
name = meta.get("name") or src["name"]
|
||
out.append({**src, "id": f"DG:{key}", "cover": key, "name": name, "spec": spec})
|
||
return out
|
||
|
||
|
||
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 {}
|
||
blocks = with_copies(blocks, cover)
|
||
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, ()) and not (cid == COPY and f"DG:{key}" in by_id)
|
||
]
|
||
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")
|
||
},
|
||
}
|