코드 - 고정 줄 단가 = 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>
195 lines
8.9 KiB
Python
195 lines
8.9 KiB
Python
"""B09 산근 덮개(PLAN 719-3) — 이 공사 단가산출근거 고친 칸(품 · 단가)을 서버 풀이 뒤에 덧씌움.
|
|
|
|
⚠ 짝 = `B09_Estimation_Sangeun_Cover.ts`(같은 식 · 차례 · 끝수) — 한쪽을 고치면 다른 쪽도 ·
|
|
거울 시험 `resources/tester/test_719_3_sangeun_cover.py`.
|
|
조작 중 = 화면이 TS 짝으로 바로 셈 · [저장] = 서버가 다시 풀이(`Sangeun.build`) 뒤 이 파일로 정본.
|
|
|
|
덮개 = 사용자 값 `sg_cover` {산근 키: {줄 열쇠 「n:이름」: {qty?}}} —
|
|
qty = 1 단위 품(841-1 단가 칸 걷음 · 옛 단가 칸은 안 읽음) ·
|
|
있는 칸만 셈 값 대신 · 줄이 없거나 이름이 다르면 짝 잃음
|
|
(`sg_orphans` · 알림만 · 덮개는 남음) · 산근 키 = 로직 + 입력값 → [업데이트] 뒤에도 같은 블록.
|
|
고칠 칸(`fields`) = 노무 · 자재 · 중기 · 빈 줄 · 하위 산근 = 품 ·
|
|
% 덧줄(`of` 있는 것) = 율.
|
|
다시 셈 = 덮개가 닿은 블록과 그 블록을 부르는 블록만(안 닿은 블록 = 서버 값 그대로) —
|
|
줄 금액 ROUNDDOWN(단가 × 품, 1) · 하위 산근 줄 단가 = 다시 센 그 블록 비목 합 ·
|
|
% 덧줄 = 서버 값 × (지금 밑수 ÷ 처음 밑수) × (지금 율 ÷ 처음 율) · 처음 밑수 · 율이 0 · 빈칸이면
|
|
밑수 × 율 ÷ 100(밑수 = 앞 줄 그 비목 합 · `people` = 노무 줄 노무비 합) ·
|
|
총계 = 비목마다 ROUNDDOWN(Σ, 0) · 합계 = 셋 합 · 목록표(`sg_list`) · 빈칸 목록(`sg_gaps`) 같이.
|
|
율 식 밖 덧줄(`of` 없음 · 기계손료 · 조건식) = 서버 값 그대로.
|
|
ponytail: 인건비 % 덧줄 = 노무 줄 합 비례(식이 일부 직종만 부르면 어긋남 · 저장 뒤도 같은 값)
|
|
고친 줄 = `base`(덮기 전 {qty, 고칠 비목 단가}) · `covered`(고친 칸) ·
|
|
목록표 줄 `covered`(고친 줄 수) · 다시 센 블록 · 목록 줄 `raw` = 덮기 전 값(두 번 덮어도 같음 —
|
|
서버 답이 덮인 채 오면 화면 짝이 벗기고 다시 덮음).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
from typing import Any
|
|
|
|
from B09_Estimation.B09_Estimation_Calc_Base import gap, num, rounddown, total
|
|
|
|
PARTS = ("labor", "material", "expense")
|
|
COVER_KEYS = ("qty",) # 841-1 품만
|
|
DEPTH = 8 # 하위 산근 깊이 상한(서버 풀이 4 보다 넉넉히)
|
|
|
|
|
|
def line_id(n: int, line: dict[str, Any]) -> str:
|
|
"""덮개 줄 열쇠 — 블록 안 차례 + 이름(로직 개정으로 줄이 밀리면 짝 잃음)."""
|
|
return f"{n}:{line.get('name') or ''}"
|
|
|
|
|
|
def fields(line: dict[str, Any]) -> list[str]:
|
|
"""고칠 수 있는 칸 — 품(qty)만(841-1 단가 칸 걷음 · 단가 = M02 사본 값) · % 덧줄 = 율."""
|
|
kind = line.get("kind")
|
|
if kind == "extra":
|
|
return ["qty"] if line.get("of") else []
|
|
return ["qty"] if kind in ("sangeun", "machine", "labor", "material", "blank") else []
|
|
|
|
|
|
def _money(price: float | None, qty: float | None) -> float | None:
|
|
return None if price is None or qty is None else rounddown(price * qty, 1)
|
|
|
|
|
|
def _base(lines: list[dict[str, Any]], n: int, of: str) -> float | None:
|
|
"""% 덧줄 밑수 — 앞 줄(0 … n-1) 비목 합 · people = 노무 줄 노무비 합."""
|
|
if of == "people":
|
|
return total([ln.get("labor") for ln in lines[:n] if ln.get("kind") == "labor"])
|
|
return total([ln.get(of) for ln in lines[:n]])
|
|
|
|
|
|
def _settle(block: dict[str, Any], mine: dict[str, Any], by: dict[str, dict[str, Any]]) -> None:
|
|
"""블록 한 장 다시 셈 — 덮개 칸 · 하위 산근 새 합 · % 덧줄 · 총계."""
|
|
first = copy.deepcopy(block["lines"])
|
|
for n, ln in enumerate(block["lines"]):
|
|
cell = mine.get(line_id(n, ln)) or {}
|
|
can = fields(ln)
|
|
keys = [k for k in can if num(cell.get(k)) is not None]
|
|
qty0, price0 = ln.get("qty"), dict(ln.get("price") or {})
|
|
if keys:
|
|
ln["base"] = {"qty": qty0, **{p: price0.get(p) for p in can if p != "qty"}}
|
|
ln["covered"] = keys
|
|
qty = num(cell["qty"]) if "qty" in keys else qty0
|
|
kind = ln.get("kind")
|
|
if kind == "sangeun":
|
|
sub = by.get((ln.get("ref") or {}).get("key")) or {}
|
|
price = {p: sub.get(p) for p in PARTS}
|
|
ln.update(qty=qty, price=price, **{p: _money(price[p], qty) for p in PARTS})
|
|
ln["why"] = (
|
|
None if sub.get("sum") is not None else f"하위 산근 {sub.get('no', '')} 빈칸"
|
|
)
|
|
elif kind == "extra":
|
|
of = ln.get("of")
|
|
part = next(iter(price0), None) or next((p for p in PARTS if ln.get(p) is None), None)
|
|
if not of or part is None:
|
|
continue
|
|
now, then, value0 = _base(block["lines"], n, of), _base(first, n, of), price0.get(part)
|
|
if now is None or qty is None:
|
|
value = None
|
|
elif value0 is not None and then and qty0:
|
|
value = value0 * (now / then) * (qty / qty0)
|
|
else:
|
|
value = now * qty / 100
|
|
ln.update(qty=qty, price={part: value}, **{part: _money(value, 1)})
|
|
ln["why"] = None if value is not None else "밑수 빈칸"
|
|
else:
|
|
price = dict(price0) # 841-1 단가 = M02 사본 값 그대로 · 품만 고침
|
|
for p in PARTS:
|
|
if p in price:
|
|
ln[p] = _money(price[p], qty)
|
|
ln.update(qty=qty, price=price)
|
|
if all(ln.get(p) is not None for p in PARTS):
|
|
ln["why"] = None
|
|
for part in PARTS:
|
|
s = total([ln.get(part) for ln in block["lines"]])
|
|
block[part] = None if s is None else rounddown(s, 0)
|
|
block["sum"] = total([block[p] for p in PARTS])
|
|
block["why"] = next((ln["why"] for ln in block["lines"] if ln.get("why")), None)
|
|
|
|
|
|
def _orphans(by: dict[str, dict[str, Any]], cover: dict[str, Any]) -> list[dict[str, str]]:
|
|
out = []
|
|
for key, lines in cover.items():
|
|
block = by.get(key)
|
|
for lid in lines if isinstance(lines, dict) else {}:
|
|
n, _, name = str(lid).partition(":")
|
|
ln = (
|
|
block["lines"][int(n)]
|
|
if block and n.isdigit() and int(n) < len(block["lines"])
|
|
else None
|
|
)
|
|
if not ln or (ln.get("name") or "") != name or not fields(ln):
|
|
out.append({"key": key, "line": str(lid)})
|
|
return out
|
|
|
|
|
|
def _bare(row: dict[str, Any]) -> dict[str, Any]:
|
|
"""덮인 목록 줄 → 덮기 전 값(`raw`)."""
|
|
if "raw" not in row:
|
|
return row
|
|
return {**{k: v for k, v in row.items() if k not in ("raw", "covered")}, **row["raw"]}
|
|
|
|
|
|
def _gaps(blocks: list[dict[str, Any]]) -> list[dict[str, str]]:
|
|
return [
|
|
gap("sg", b["key"], f"산근 {b['no']} {b['name']}", b["why"] or "단가 빈칸")
|
|
for b in blocks
|
|
if b["sum"] is None
|
|
]
|
|
|
|
|
|
def cover_sangeun(sg: dict[str, Any], cover: Any) -> dict[str, Any]:
|
|
"""산근 답(`Sangeun.collect` 꼴) + 덮개 → 새 답(받은 답 안 고침) · 덮개 없으면 그대로 ·
|
|
이미 덮인 답(서버 셈 답) = 블록 · 목록 줄 `raw`(덮기 전 값)로 벗긴 뒤 다시 덮음."""
|
|
cover = cover if isinstance(cover, dict) else {}
|
|
worn = sg.get("sg") or []
|
|
blocks = [b["raw"] if "raw" in b else b for b in worn]
|
|
orphans = _orphans({b["key"]: b for b in blocks}, cover)
|
|
if not any(cover.get(b["key"]) for b in blocks):
|
|
if not any("raw" in b for b in worn):
|
|
return {**sg, "sg_orphans": orphans}
|
|
rows = [_bare(r) for r in sg.get("sg_list") or []]
|
|
return {
|
|
**sg,
|
|
"sg": blocks,
|
|
"sg_list": rows,
|
|
"sg_gaps": _gaps(blocks),
|
|
"sg_orphans": orphans,
|
|
}
|
|
out = copy.deepcopy(blocks)
|
|
by = {b["key"]: b for b in out}
|
|
hit: dict[str, bool] = {}
|
|
|
|
def touch(key: str, depth: int) -> bool:
|
|
if key in hit:
|
|
return hit[key]
|
|
block = by.get(key)
|
|
if block is None or depth > DEPTH:
|
|
return False
|
|
hit[key] = False # 돎 막음
|
|
subs = [
|
|
touch(ln["ref"]["key"], depth + 1)
|
|
for ln in block["lines"]
|
|
if ln.get("kind") == "sangeun" and (ln.get("ref") or {}).get("key")
|
|
]
|
|
hit[key] = bool(cover.get(key)) or any(subs)
|
|
if hit[key]:
|
|
raw = copy.deepcopy(block)
|
|
_settle(block, cover.get(key) or {}, by)
|
|
block["raw"] = raw
|
|
return hit[key]
|
|
|
|
for b in out:
|
|
touch(b["key"], 0)
|
|
listing = []
|
|
for row in map(_bare, sg.get("sg_list") or []):
|
|
b = by.get(row["key"])
|
|
if b is None or not hit.get(row["key"]):
|
|
listing.append(row)
|
|
continue
|
|
n = sum(1 for ln in b["lines"] if ln.get("covered"))
|
|
sums = {k: b[k] for k in (*PARTS, "sum")}
|
|
raw = {k: row.get(k) for k in (*PARTS, "sum")}
|
|
listing.append({**row, **sums, **({"covered": n} if n else {}), "raw": raw})
|
|
return {**sg, "sg": out, "sg_list": listing, "sg_gaps": _gaps(out), "sg_orphans": orphans}
|