코드 - 고정 줄 단가 = M02_Labor_Fixed.line(사본 묶음 · 이은 노임 · 자재 = 묶음 값) — B09 단가 책으로 다시 안 덮음 - 대가 · 산근 고친 칸 = 품만 실제로(단가 칸 덮기 줄 남던 흠 · 품 고친 뒤 줄 금액 다시 셈 흠 py · ts 짝) - 산근 글 = 엔진 단가 셈 그대로 · 별도계상 재료 줄 끝 「· 별도계상 = 0.0」 · 계 줄 = 블록 합 - 노무비 · 중기목록 이름에서 규격 겹침 뗌 · TS 바탕 셈 이름 PARTS → MACHINE_PARTS(짝 시험이 TS 를 이어 붙일 때 겹침) 시험(고친 까닭 = 단가 원천이 B09 책 → M02 엔진 · 노임 · 기계 덮개 · 단가 고친 칸 · 중기사용료 걷음) - 144 바탕 · 뼈대: 운전사 · 중기사용료 · 경비 · 덮개 시험 걷음 → 노무 · 중기 나열 · 사급 표시 · 자재만 책 - 145-1 · 145-2 · 155 · 157-1 · 166-2 · 166-6 · 719-3 · 719-4 · 723-4 · 778-4 · 792-3: 줄 단가 = own(엔진) · 산근 = Solver() · collect 새 부름 꼴 - 702 · 705-1 · 719-2: 단가 칸 덮개 = 안 읽음 · 고정 줄 = M02 셈(노임 새 판 따라가기 시험 걷음) - 203-1 · 296 · 703-3 · 795-4: 단가 책 = 자재 줄로 - 707 · 707-6 · 707-7: 단가표 불러오기 = 자재만 · 거창 이은 안내판 줄 = M02 사본 셈(엑셀 −50,000) - 201-1: 환율 · 중기사용료 표 걷음 · b09 저장 속도: 산근 앞 풀이 그대로(단가 표 같음 견줌 걷음) - 대상 113 파일 764 통과 · 25 실패 = 원래 실패(formatMoney · formatPrice 없음 · 단위 글 ㎜ · sec · 719-1 · 796-2e · 717-4 · 721 · 311 · 243 · 246) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
404 lines
18 KiB
Python
404 lines
18 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 · 율 더한줄(778-4) 밑수 = `bundle:` 묶음 줄 합 ·
|
||
`rows:` 블록 몇째 줄 합(`_base`)
|
||
고정 줄(705-1) = 줄 단가(`own`) · 이음(`link`) 있으면 그 비목만 단가 책 값(노임 = 노무 · 자재 =
|
||
재료(관급 · 별도 0원) · 기계 = 중기 세 비목) · 책에 없으면 줄 단가 그대로 · 빈칸 안 됨
|
||
끝수 = 엑셀 자리 그대로 — 줄 금액 = ROUNDDOWN(단가 × 수량, 1) ·
|
||
% 줄 = ROUNDDOWN(밑수 × % / 100, 1) · 관급 · 별도계상 자재 = 대가 안 0원 ·
|
||
블록 비목 합 = ROUNDDOWN(Σ, 0) · 합계 = 세 비목 합.
|
||
새 의미 로직 블록(792-4 · `B09_Estimation_Labor_Rule`) = 줄 `place` {방법, 자리}(그 끝수) ·
|
||
줄 `out`(비목 합 · `part:` 밑에서 뺌) · 블록 `rule` {끝전 큰나머지, 끝수, 조정}(`settle` ·
|
||
엔진 `master_cut.settle` 과 같은 차례).
|
||
빈칸 = 단가 · 수량을 낸 원천이 없으면 None · 「연결 없음」 목록(gaps).
|
||
빈 줄(166-6) = 블록 합 = 아는 줄 합 + `blank` n · `blank_names` — 내역 단가 · 부른 대가로는 안 넘김 ·
|
||
줄 0 개 블록 = 빈 줄 하나(`NO_LINES`) · 줄 `ask`(723-4 SC 안 줄 수량 없음) = 수량이 비면 `why`.
|
||
고친 칸 덮개(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}.
|
||
붙인 줄(705-2 · 엑셀 붙여넣기) = 덮개 `@add` [블록 줄 `fixed` · `pct` · cid `@add|…`] — 그 cover 블록 끝에 더함
|
||
(복사본 앞 · 뒤 두 번 · 같은 cid 는 한 번 · 복사본이 원본 붙인 줄을 이어받음) · 고친 칸 덮개는 다른 줄과 같음.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from B09_Estimation.B09_Estimation_Calc_Base import (
|
||
SUPPLY,
|
||
gap,
|
||
num,
|
||
round_half,
|
||
rounddown,
|
||
roundup,
|
||
total,
|
||
)
|
||
|
||
PARTS = ("labor", "material", "expense")
|
||
COVER_KEYS = ("qty",) # 841-1 단가 고친 칸 걷음 — 품(수량)만
|
||
TAB = "dg"
|
||
NO_LINES = "구성 줄 없음" # 줄 0 개 일위대가의 빈 줄 이름
|
||
COST = {"노무비": "labor", "재료비": "material", "경비": "expense"}
|
||
WAYS = {"버림": rounddown, "올림": roundup, "반올림": round_half}
|
||
COPY = "@copy"
|
||
ADD = "@add"
|
||
WHY = {
|
||
"labor": "노임 단가 없음(M02 엔진)",
|
||
"material": "자재 단가 없음(M02 엔진)",
|
||
"machine": "기계 시간당 없음(M02 엔진)",
|
||
"daega": "부른 대가 합계 없음",
|
||
"sangeun": "산근 단가 없음(145-1)",
|
||
}
|
||
|
||
|
||
def cut(value: float, place: dict | None) -> float:
|
||
"""줄 금액 끝수 — `place` {방법, 자리}(792-4) · 없으면 ROUNDDOWN 1."""
|
||
if not place:
|
||
return rounddown(value, 1)
|
||
return WAYS[place.get("방법")](value, int(place.get("자리") or 0))
|
||
|
||
|
||
def settle(raw: dict[str, float], rule: dict | None) -> dict[str, float]:
|
||
"""블록 비목 합 — 비목마다 ROUNDDOWN 0 · `rule`(792-4) = 큰나머지(계 원 버림 뒤 모자란 원을
|
||
소수 큰 비목부터 · 같으면 경 › 재 › 노) → `끝수`(대상 계) → `조정`(배율 · 옮김) → 원 버림."""
|
||
parts = {p: rounddown(v, 0) for p, v in raw.items()}
|
||
if not rule:
|
||
return parts
|
||
if rule.get("끝전") == "큰나머지":
|
||
rest = int(rounddown(sum(raw.values()), 0) - sum(parts.values()))
|
||
for p in sorted(PARTS, key=lambda p: (parts[p] - raw[p], -PARTS.index(p)))[:rest]:
|
||
parts[p] += 1
|
||
end = rule.get("끝수")
|
||
if isinstance(end, dict) and end.get("대상") == "계":
|
||
parts = {p: cut(v, end) for p, v in parts.items()}
|
||
adjust = rule.get("조정") or {}
|
||
for name, rate in (adjust.get("배율") or {}).items():
|
||
parts[COST[name]] *= float(rate)
|
||
if adjust.get("옮김"):
|
||
whole = sum(parts.values())
|
||
parts = {p: whole if p == COST[adjust["옮김"]] else 0.0 for p in PARTS}
|
||
return {p: rounddown(v, 0) for p, v in parts.items()}
|
||
|
||
|
||
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 in (
|
||
"labor",
|
||
"material",
|
||
"machine",
|
||
): # 841-1 = M02 엔진 단가(`own`) · B09 단가 책 안 읽음
|
||
own = line.get("own")
|
||
out = {p: num(own.get(p)) if p in own else 0.0 for p in PARTS} if own else _none()
|
||
elif kind in ("daega", "sangeun"):
|
||
row = {"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)) # 빈 줄 대가 = 빈칸
|
||
elif kind == "fixed":
|
||
out = _fixed(line, books)
|
||
else: # blank — 못 펼친 줄
|
||
out = dict.fromkeys(PARTS, None)
|
||
return out
|
||
|
||
|
||
def _apart(line: dict[str, Any], books: dict[str, Any]) -> bool:
|
||
"""대가 밖 재료(841-1) — 대비표 표시(사용자 › M02 자재표시 기본) 관급 · 별도 = 자재대 · 관급 단락."""
|
||
return (books["material"].get(line.get("key")) or {}).get("supply") in SUPPLY
|
||
|
||
|
||
def _as_m02(block: dict, lines: list[dict], books: dict, own: dict) -> bool:
|
||
"""SK 블록 합을 M02 셈으로 — 고친 칸 · 붙인 줄 없음 · 재료 표시가 M02 자재표시 그대로일 때만."""
|
||
return (
|
||
bool(block.get("m02"))
|
||
and not own
|
||
and all(
|
||
_apart(ln, books) == (ln.get("mark") in SUPPLY)
|
||
for ln in lines
|
||
if ln["kind"] == "material"
|
||
)
|
||
)
|
||
|
||
|
||
def _none() -> dict[str, float | None]:
|
||
return dict.fromkeys(PARTS, None)
|
||
|
||
|
||
def _fixed(line: dict[str, Any], books: dict[str, Any]) -> dict[str, float | None]:
|
||
"""고정 줄 단가(705-1) — 줄 단가 그대로 · 이은 자재가 관급 · 별도계상이면 재료 0(841-1 단가 책
|
||
값으로 다시 안 덮음)."""
|
||
own, link = line.get("own") or {}, line.get("link") or {}
|
||
out = {part: num(own.get(part)) or 0.0 for part in PARTS}
|
||
row = books["material"].get(link.get("material")) if link.get("material") else None
|
||
if row and row.get("supply") in SUPPLY:
|
||
out["material"] = 0.0
|
||
return out
|
||
|
||
|
||
def _base(spec: str, lines: list[dict[str, Any]], src: list) -> float | None:
|
||
"""% 줄 밑수 — `part:비목` 그 비목 줄 금액 합 · `lines:labor` 노무 줄 금액 합 ·
|
||
`line:이름` 그 줄 · `bundle:labor|material|machine` 그 종류 줄(+ 그 이음 고정 줄) 합 ·
|
||
`rows:0,2@labor` 블록 그 차례 줄(`src` cid · `@비목` = 그 몫) 합(778-4 · 짝 `.ts` `pctBase`)."""
|
||
kind, _, name = spec.partition(":")
|
||
if kind == "part": # 대가 밖 재료(841-1 `aside`)도 밑수에 — M02 엔진 「재료비의 %」 꼴
|
||
aside = "aside" if name == "material" else ""
|
||
return total([ln.get(aside) or ln["amounts"][name] for ln in lines if not ln.get("out")])
|
||
if kind == "rows": # `3@labor` = 그 줄 그 비목 몫
|
||
out: list[float | None] = []
|
||
for one in dict.fromkeys(name.split(",")):
|
||
at, _, part = one.partition("@")
|
||
if not at.isdigit() or int(at) >= len(src):
|
||
continue
|
||
cid = src[int(at)].get("cid")
|
||
out += [
|
||
ln["amounts"].get(part) if part else total(list(ln["amounts"].values()))
|
||
for ln in lines
|
||
if ln.get("cid") == cid
|
||
]
|
||
return total(out)
|
||
if kind == "bundle":
|
||
return total(
|
||
[
|
||
total(list(ln["amounts"].values()))
|
||
for ln in lines
|
||
if ln["kind"] == name
|
||
or (ln["kind"] == "fixed" and (ln.get("link") or {}).get(name))
|
||
]
|
||
)
|
||
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}
|
||
return num(mine["qty"]), dict(prices), extra # 841-1 품만(단가 칸 = 안 읽음)
|
||
|
||
|
||
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 cut(p * qty, line.get("place"))
|
||
for part, p in prices.items()
|
||
}
|
||
if line["kind"] == "material" and _apart(line, books): # 관급 · 별도계상 = 대가 안 0원
|
||
extra["aside"], prices["material"], amounts["material"] = amounts["material"], 0.0, 0.0
|
||
ref = {"ref": {"key": line["key"]}} if line.get("key") else {} # 집계표(145-5) 이음
|
||
ask = {"why": line["ask"]} if qty is None and line.get("ask") else {} # 723-4
|
||
lines.append(
|
||
{**line, **ref, **ask, "qty": qty, "prices": prices, "amounts": amounts, **extra}
|
||
)
|
||
if qty is None or None in prices.values():
|
||
why = (
|
||
line.get("why")
|
||
or ask.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, block["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 cut(base * pct / 100, line.get("place"))
|
||
)
|
||
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 and not ln.get("out")]
|
||
parts = settle({part: sum(ln["amounts"][part] for ln in known) for part in PARTS},
|
||
block.get("rule")) # fmt: skip
|
||
if _as_m02(
|
||
block, lines, books, own
|
||
): # 841-1 단가산출 블록 = M02 SK 셈 그대로(덮음 · 수식 고침)
|
||
parts = {p: rounddown(block["m02"][p], 0) for p in PARTS}
|
||
blank_names = [ln.get("name") or "" for ln in lines if ln["amount"] is None]
|
||
if not lines: # 줄 0 개 일위대가 = 금액 0 이 아니라 모름(빈 줄)
|
||
blank_names = [NO_LINES]
|
||
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 with_added(blocks: list[dict[str, Any]], cover: dict[str, Any]) -> list[dict[str, Any]]:
|
||
"""붙인 줄(705-2) — 블록 cover 의 `@add` 줄을 끝에 더함(이미 있는 cid 는 뺌)."""
|
||
out = []
|
||
for b in blocks:
|
||
add = (cover.get(b.get("cover")) or {}).get(ADD)
|
||
have = {ln.get("cid") for ln in b["lines"]}
|
||
more = [ln for ln in add if ln.get("cid") not in have] if isinstance(add, list) else []
|
||
out.append({**b, "lines": [*b["lines"], *more]} if more else b)
|
||
return out
|
||
|
||
|
||
def added_keys(cover: dict[str, Any] | None) -> list[str]:
|
||
"""붙인 줄이 잇는 단가 책 키(705-2 · 수요에 더함)."""
|
||
out: list[str] = []
|
||
for lines in (cover or {}).values():
|
||
for ln in (lines or {}).get(ADD) or []:
|
||
out += [v for v in (ln.get("link") or {}).values() if v and v not in out]
|
||
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_added(with_copies(with_added(blocks, cover), cover), 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)
|
||
and cid != ADD
|
||
]
|
||
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")
|
||
},
|
||
}
|