Files
Aislo/B09_Estimation/B09_Estimation_Labor_Source.py
T
eomsangdonandClaude Opus 5.5 9633e97c3c feat(723-4): 723-1 뒤 B09 일위대가 수량 연결 마무리 — 알약 값 쓴 SC 만 덮개 나눔 · SK 속 SC · 공사 불러오기 수량 · 비고 경고
- Labor_Source = 덮개 열쇠 `C:참조|S:사본` 은 알약 값 쓴 SC 블록만(빈 수량 · 옛 손 = `C:참조` 그대로 = 703-1 지난 공사 가져옴 꼴) ·
  SK 속 SC 안 줄 수량 없음 = 엔진엔 0 값묶음(`_Loose`) · 그 줄 = 사전조합 블록(이 공사 입력) — SK 통째 빈칸 안 됨
- Bill_Hopyo(707-7 공사 통째 불러오기) = 이 공사 엑셀 수량을 굳힌 손으로 남김(723-1 `with_pills` 수량 걷기는 M02 저장용)
- Daega_Personal 원천 = 블록 origin 참조(덮개 열쇠 꼬리 무관) · 내 자료집 저장 알림 글 = 줄 수량 저장 안 함
- 비고(펼침 · 일위대가표) = 수량 없는 SC 안 줄은 「산근 n호표 · 수량 없음 — 이 공사에서 입력」(`noteOf`)
- 낡은 시험 고침(사유 = 723-1 SC 안 줄 수량 저장 안 함 · 이 공사 입력 · 알약으로 이음):
  test_204_2(서고 SC 수량 없음 · ask) · test_703(자료집 저장 손 걷힘) · test_719_1(서고 사본 = 알약 값으로 같은 값 · 없으면 수량 없음) ·
  test_719_2(SK 속 SC = 빈 줄 → 이 공사 입력 1 = M02 알약 1 금액) · test_702_3(서고 SC 빈 줄 3 → 입력 뒤 금액)
- test_723_4 = 구조물 사본 면적 알약 굳힘 · 설계 연동 · 알약 안 이음 = 빈 줄 시험 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 22:25:30 +09:00

582 lines
30 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) — 서버 단독 · 읽기만 · 값(단가)은 안 씀.
연결된 사본(`M02_Item_Snapshot.linked_snapshots`)마다 `일위대가.json` 을 사본 셈 한 벌
(`M02_Item_Prices.calc` = 142-1 층 셈 꼴)로 돌려 줄 수량을 받고, 블록 꼴로 펼침:
조합 구조물 조합 사본 하나 = 블록(엑셀 「조합」 꼴) · 줄 = 담은 로직 줄 · 자재 줄
사전조합 참조 SC = 겹 대가(굳힌 `참조사본`) · 줄 = 그 안 로직 · 참조 줄 · 자재 줄(175-2) ·
안 줄 수량(723-4) = 굳힌 옛 손 → 쓰는 구조물 사본 면적 알약 값(723-1
「참조 id#차례」 · 「참조 id#자재|코드」) → 없음 = 빈 줄(`ask` · 이 공사에서 입력 =
702 덮개 qty) · 알약 값 쓴 블록 덮개 = `C:참조|S:사본`(사본마다 값 다름) ·
그 밖 = `C:참조`(공사 안 한 덮개)
품셈 일위대가화(승격) 줄 · 옛 참조 SP = 로직마다 따로(노무 · 재료 · 중기 · 산근 · % 줄)
— 조합이 수량만큼 부름 · 기계 = 기계경비 로직 줄 · 다른 돈 로직 줄 = 하위 산근
산근 단가산출 줄(일위대가화 아닌 로직 줄) = 산근 호표 참조(키 = 145-1 `sangeun_key`)
(155-3 · 사용자 뜻 = 일위대가화 줄 = 대가 · 단가산출 줄 = 산근 · 145 정함 뒤집음)
고정 고정 줄(705-1) = `fixed` 줄 하나 · `own` = 줄 단가 · `link` = 이음 키(셈 짝이 단가 책 값)
같은 블록 = 같은 꼴 → 한 호표(id = 내용 해시 · 같은 원본 + 같은 셈 단가 = 하나) ·
덮개 열쇠(702-2 · 내용 해시 밖 · 수량 · 단가가 바뀌어도 그대로) = 블록 `cover` + 줄 `cid` —
cover = 조합 `S:사본 폴더` · 서고 `L:폴더` · 사전조합 `C:참조` · 품셈 `P:로직 + 입력 해시`
cid = 줄 정체(비목 : 노임 · 자재 · 중기 코드 · 산근 · 부른 대가 로직 · 참조) + `#n번째`
줄 수량 = 사본 셈 수량(손 · 면적 알약 칸) — 대가 줄만 ÷ 배(부르는 호표 1 단위 몫 ·
산근 단가는 이미 1 단위).
단가 · 금액 · 끝수는 셈 짝(`B09_Estimation_Calc_Labor`)이 B09 프로젝트 한 벌 단가로 냄.
"""
from __future__ import annotations
import hashlib
import json
import re
from decimal import Decimal
from pathlib import Path
from typing import Any
from B09_Estimation import B09_Estimation_Bill_Library as library
from B09_Estimation import B09_Estimation_Sangeun as sg
from B09_Estimation import B09_Estimation_Sangeun_Text as st
from B09_Estimation.B09_Estimation_Bill_Items import LAYER_WORD
from M02_MasterTemplete import M02_Item_Prices as item_prices
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
from M02_MasterTemplete import M02_Labor_Fixed as fixed
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete.M02_Item_Snapshot import (
MANIFEST_NAME,
PICKS_FILE,
VALUES_FILE,
linked_snapshots,
structures_dir,
)
from M02_MasterTemplete.M02_Item_Source import LABOR_FILE, PRICE_FILE, read_json
mf = item_prices.mf
#: 엔진 비목 → 블록 비목
PART = {"노무비": "labor", "재료비": "material", "경비": "expense"}
#: % 덧줄 밑수 — `노무비 * …` · `줄.'이름' * …` · `(석공 * LB… + …) * …`(인력 줄)
BASE_PART = re.compile(r"^\s*(노무비|재료비|경비)\s*\*")
BASE_LINE = re.compile(r"^\s*줄\.'([^']+)'\s*\*")
BASE_LABOR = re.compile(r"^\s*\(.*\*\s*LB\d+")
AREA_FILE = "면적.json"
ASK = "수량 없음 — 이 공사에서 입력" # 723-4 SC 안 줄 수량 빈칸(펼침 품 · 수량 칸)
def _f(value: Any) -> float | None:
if isinstance(value, bool) or value is None:
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def _digest(value: Any) -> str:
text = json.dumps(value, ensure_ascii=False, sort_keys=True, default=str)
return hashlib.sha1(text.encode("utf-8")).hexdigest()[:10]
def sangeun_ref(code: str, given: Any, rate: Any, picks: Any) -> str:
"""산근 호표 키 — 145-1 `B09_Estimation_Sangeun.sangeun_key`."""
return sg.sangeun_key(code, given or {}, rate, picks or {})
class Builder:
"""한 프로젝트 블록 모음 — 같은 id 는 한 번만(처음 본 차례)."""
def __init__(self) -> None:
self.blocks: dict[str, dict[str, Any]] = {}
self.gaps: list[dict[str, str]] = []
def put(self, block: dict[str, Any], cover: str | None = None) -> str:
bid = f"DG:{_digest([block['kind'], block['origin'], block['lines']])}"
if bid not in self.blocks:
mark = {"품셈": "P", "사전조합": "C"}.get(block["kind"], "S")
if cover is None:
origin = block["origin"]
cover = f"C:{origin['참조']}" if mark == "C" else f"{mark}:{_digest(origin)}"
lines = [{**ln, "cid": cid} for ln, cid in zip(block["lines"], self._cids(block))]
self.blocks[bid] = {"id": bid, "cover": cover, **block, "lines": lines}
return bid
def _cids(self, block: dict[str, Any]) -> list[str]:
"""줄 정체(702-2) — 같은 정체 n번째 = `#n` · 재적용으로 줄이 늘거나 바뀌어도 맞는 줄에."""
seen: dict[str, int] = {}
out = []
for line in block["lines"]:
kind, key = line["kind"], str(line.get("key") or "")
if kind == "daega" and key in self.blocks:
origin = self.blocks[key]["origin"]
key = str(origin.get("로직") or origin.get("참조") or key)
elif kind == "sangeun":
key = key.rsplit(":", 1)[0] # SG:로직:입력 해시 → 로직만
elif kind == "pct":
key = str(line.get("base") or "")
head = f"{kind}:{key or line.get('name') or ''}"
seen[head] = seen.get(head, -1) + 1
out.append(f"{head}#{seen[head]}")
return out
# ── 품셈(로직 하나) ─────────────────────────────────────────────────
def poomsem(self, whole: Any, one: dict[str, Any], title: str = "") -> str | None:
got = self._poomsem(whole, one, title)
if got is None:
return None
got.pop("_engine"), got.pop("_items")
return self.put(got)
def _poomsem(self, whole: Any, one: dict[str, Any], title: str = "") -> dict | None:
"""품셈 블록 꼴(넣기 전) — 엔진 줄 · 줄마다 `_n`(엔진 차례 · 단가산출 블록이 씀)."""
code = str(one.get("로직") or "")
given, rate, picks = one.get("입력값") or {}, one.get("켜쌓기율"), one.get("고름")
try:
logic = whole.logic(code)
except mf.FormulaError:
return None
got = labor_doc.run_line(whole, code, given, rate, picks)
if got.get("기준") is None:
return None # 돈 아닌 로직 · 못 돈 로직 — 조합 줄이 까닭을 냄
items = logic.get("호표") or []
lines = []
engine = got.get("줄") or []
bases = _engine_bases(engine[: len(items)], items)
calls = sg.line_calls(whole, code, given, picks)
for n, x in enumerate(engine):
item = items[n] if n < len(items) else None
lines.append(self._engine_line(item, x, logic, n - len(items), bases, calls.get(n)))
eq = {str(r.get("키")): r for r in (whole.numbers.get("EQ") or {}).values()}
for line in lines:
if line["kind"] == "machine":
line["spec"] = (eq.get(line["key"]) or {}).get("규격") or ""
unit = str(logic.get("결과단위") or "").replace("원/", "")
spec = " · ".join(f"{k} {v}" for k, v in sorted(given.items()))
shown = " · ".join( # 157-4 기계 · 직종 원문번호 = 「이름 규격」(번호 = 화면 풍선 spec_raw)
f"{k} {st._word(whole, k, v) if isinstance(v, str) else v}"
for k, v in sorted(given.items())
)
return {
"kind": "품셈",
"name": title or logic.get("이름") or code,
"spec": shown,
"spec_raw": spec,
"unit": unit,
"cite": logic.get("출처") or "",
"origin": {"로직": code, "입력값": given, "켜쌓기율": rate, "고름": picks},
"lines": lines,
"_engine": engine,
"_items": items,
}
# ── 단가산출(SK · 719-2) ────────────────────────────────────────────
def sangeun(self, whole: Any, ref: dict, doc: dict, copies: dict, depth: int = 0) -> str | None:
"""굳힌 SK(사본 `단가.json` 로직 줄 · 719-1) = 품셈 블록 꼴 대가 — 셈 = 굳힌 줄 그대로 ·
단가산출 속 일위대가 · SK 줄(참조) = 그 대가 블록을 부르는 줄(펼침 · B09 차례 그대로) ·
고정 줄 = `fixed` · 단가 고친 줄(덮개 · 더한줄 단가 · 값묶음) = `fixed`(줄 단가 = M02 셈) ·
cover = `P:` + SK 이름표(SK 를 고쳐 [업데이트] 해도 B09 덮개 그대로) ·
속 일위대가 안 줄 수량 없음(723-4) = 엔진엔 0 값묶음 · 그 줄 = 사전조합 블록(이 공사 입력) ·
줄 `sk` · `poom`(M02 에서 고친 줄의 품셈 값 = 원본 덮개 `원값`)."""
from M02_MasterTemplete import M02_Item_Sangeun as item_sk
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
rid = refs.ref_id(ref)
key = str((doc.get("바탕") or {}).get("로직") or "")
if item_sk.ROW not in doc or not key:
return None # 못 푼 SK(없는 바탕 · 순환) — 부르는 줄이 까닭
def read(r: dict) -> dict | None:
return copies.get(refs.ref_id(r))
try:
got = calc.resolve(doc, whole, read, (rid,))
row = {**got["줄"], "키": key, "호표": [dict(x) for x in got["줄"]["호표"]]}
if calc._bundles(row, whole, _Loose(whole, read), (rid,)):
return None
except (sd.DocError, mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError):
return None
one = {"로직": key, "입력값": got["입력값"], "켜쌓기율": got["켜쌓기율"]}
block = self._poomsem(calc._Over(whole, key, row), {**one, "고름": got["고름"]})
if block is None:
return None
ids = list(doc.get("정체") or [])
was = {c: v.get("원값") for c, v in ((doc.get("원본") or {}).get("덮개") or {}).items()}
lines = []
for n, (line, x) in enumerate(zip(block["lines"], block.pop("_engine"))):
item = block["_items"][n] if n < len(block["_items"]) else {}
qty = _f(x.get("수량"))
head = {"name": line.get("name") or "", "spec": "", "unit": line.get("unit") or ""}
if isinstance(item.get("참조"), dict) and depth < refs.DEPTH:
line = {**self.line_of(whole, item, qty, 1, head["name"], copies, depth), **head}
elif isinstance(item.get("고정"), dict):
line = {**fixed_line(item["고정"], qty), **head}
elif qty and ("단가" in item or item.get("값묶음")):
own = {PART[k]: _f(v) / qty for k, v in (x.get("비목") or {}).items() if k in PART}
line = {**head, "kind": "fixed", "qty": qty, "own": own, "link": {}}
poom = was.get(ids[n]) if n < len(ids) else None
lines.append({**line, "sk": True, **({"poom": poom} if poom else {})})
block.pop("_items")
name = doc.get("이름") or block["name"]
origin = {**block["origin"], "단가산출": rid}
cover = f"P:{_digest({'단가산출': rid})}"
return self.put({**block, "name": name,
"cite": f"단가산출 {ref.get('키')}", "origin": origin,
"lines": lines}, cover) # fmt: skip
def _engine_line(
self, item: dict | None, x: dict, logic: dict, extra: int, bases: dict, call: dict | None
) -> dict[str, Any]:
head = {"name": x.get("이름") or "", "spec": "", "unit": x.get("단위") or ""}
qty = _f(x.get("수량"))
goods = x.get("품목") or {}
if item is None: # 덧줄 — 밑수 × % / 100(% = 엔진 값 ÷ 엔진 밑수)
add = (logic.get("덧줄") or [])[extra]
base, label = _base_of(str(add.get("식") or ""))
value, under = _f(x.get("금액")), bases.get(base) if base else None
if value is None or not under:
# ponytail: 밑수 없는 덧줄(손료 식 등)은 B09 단가로 못 셈 — 줄만 두고 빈칸
return {**head, "kind": "blank", "qty": qty, "why": "덧줄 — 대가 단가로 못 셈"}
part = PART.get(str(add.get("비목")), "expense")
return {**head, "kind": "pct", "part": part, "base": base, "spec": f"{label}의 %",
"unit": "%", "qty": round(value / under * 100, 6)} # fmt: skip
kind = item.get("종류")
if kind == "인력":
key = goods.get("키") or str(item.get("요소") or "")
return {**head, "kind": "labor", "key": key, "qty": qty}
if kind == "재료":
if not goods.get("키"):
return {**head, "kind": "blank", "qty": qty, "why": x.get("까닭") or "품목 없음"}
return {**head, "kind": "material", "key": goods["키"], "spec": goods.get("규격") or "",
"qty": qty} # fmt: skip
# 로직 줄 — `Sangeun.call_of` 판정: 기계경비 로직 = 중기 · 그 밖 = 하위 산근
if call and call.get("machine"):
return {**head, "kind": "machine", "key": call["machine"], "qty": qty}
if call and call.get("code"):
key = sg.sangeun_key(call["code"], call["given"])
return {**head, "kind": "sangeun", "key": key, "qty": qty}
return {**head, "kind": "blank", "qty": qty, "why": "하위 로직 줄 — 못 펼침"}
# ── 조합 · 사전조합 줄 ──────────────────────────────────────────────
def line_of(
self,
whole: Any,
one: dict,
qty: Any,
times: Any,
name: str,
copies: dict,
depth: int = 0,
use: tuple[dict, str] = ({}, ""),
) -> dict[str, Any]:
"""담은 로직 줄 하나 → 블록 줄 — 대가 줄 수량 = 수량 ÷ 배(부르는 호표 1 단위 몫) ·
산근 줄 수량 = 수량 그대로(산근 단가 = 결과단위 1 단위) · use = 쓰는 곳(알약 값 · 자리)."""
q = _f(qty)
t = _f(times) or 1.0
each = None if q is None else q / t
if fixed.is_fixed(one):
return fixed_line(one[fixed.FIXED], q)
ref = one.get("참조")
if isinstance(ref, dict):
doc = copies.get(refs.ref_id(ref))
if not isinstance(doc, dict):
return {
"kind": "blank",
"name": name,
"qty": each,
"why": f"없는 참조 「{ref.get('키')}」",
}
if refs.kind_of(ref.get("키")) == "sangeun": # 단가산출(719-2) = 굳힌 줄 대가
bid = self.sangeun(whole, ref, doc, copies, depth + 1)
if bid is None:
why = "단가산출을 못 셈(바탕 없음 · 순환)"
return {"kind": "blank", "name": name, "qty": each, "why": why}
from M02_MasterTemplete.M02_Sangeun_Calc import times_of
block = self.blocks[bid] # 블록 = 굳힌 줄 결과단위(배) 몫 · M02 참조 줄 = 1 단위
per = float(times_of(whole.logic(doc["바탕"]["로직"])))
return {"kind": "daega", "key": bid, "name": block["name"], "unit": block["unit"],
"qty": None if each is None else each / per} # fmt: skip
if refs.kind_of(ref.get("키")) == "promote": # 옛 단가승격 참조 = 일위대가화 = 대가
first = (doc.get("담은로직") or [{}])[0]
bid = self.poomsem(whole, first, doc.get("이름") or name)
if bid is None:
return {"kind": "blank", "name": name, "qty": each, "why": "돈 아닌 로직"}
return {"kind": "daega", "key": bid, "name": self.blocks[bid]["name"],
"unit": doc.get("단위") or "", "qty": each} # fmt: skip
bid = self.precombo(whole, ref, doc, copies, depth + 1, use)
return {"kind": "daega", "key": bid, "name": doc.get("이름") or name,
"unit": doc.get("단위") or "", "qty": each} # fmt: skip
code = str(one.get("로직") or "")
if one.get("승격") is not True: # 단가산출 줄 = 산근
if not sg.is_money(whole, code):
return {"kind": "blank", "name": name or code, "qty": q, "why": "돈 아닌 로직"}
key = sangeun_ref(code, one.get("입력값"), one.get("켜쌓기율"), one.get("고름"))
return {"kind": "sangeun", "key": key, "name": name or code,
"unit": sg.unit_of(whole, code), "qty": q} # fmt: skip
bid = self.poomsem(whole, one, str(one.get("별칭") or "") or name)
if bid is None:
return {
"kind": "blank",
"name": name or code,
"qty": each,
"why": "돈 아닌 로직 · 못 돈 로직",
}
block = self.blocks[bid]
return {"kind": "daega", "key": bid, "name": block["name"], "spec": block["spec"],
"spec_raw": block.get("spec_raw"), "unit": block["unit"], "qty": each} # fmt: skip
def precombo(
self,
whole: Any,
ref: dict,
doc: dict,
copies: dict,
depth: int = 0,
use: tuple[dict, str] = ({}, ""),
) -> str:
"""사전조합(SC) = 겹 대가 — 로직 줄 = 품셈 호표 · 자재 줄(175-2) · 줄 수량(723-4) =
굳힌 손 → use 알약 값 → 없음(`ask`) · use = (쓰는 구조물 사본 면적 알약 값, 덮개 자리)."""
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
rid, (given, where) = refs.ref_id(ref), use
pilled: list[bool] = [] # 줄마다 알약 값을 썼나
def qty_of(key: str, hand: Any) -> float | None: # 짝 = `M02_Labor_Expand._qty` 차례
got = _f(hand)
pilled.append(got is None and key in given)
return _f(given.get(key)) if got is None else got
inner = [one for one in doc.get("담은로직") or [] if isinstance(one, dict)]
kept = doc.get("알약") # 굳힌 알약(142-2 · 지금 M01 안 읽음 · 719-2) · 없으면 셈
pills = {
code: pill
for pill in (kept if isinstance(kept, list) else None)
or combo_store.combo_pills({"담은로직": inner})["물량"]
for code in pill["로직"]
}
lines = []
for n, one in enumerate(inner):
qty = qty_of(f"{rid}#{n}", (one.get("수량") or {}).get("손"))
pill = pills.get(one.get("로직")) or {}
times = pill.get("배") if "로직" in one else 1
name = pill.get("이름") or str(one.get("로직") or "")
if depth >= refs.DEPTH and isinstance(one.get("참조"), dict):
lines.append({"kind": "blank", "name": name, "qty": qty, "why": "참조 깊이 넘음"})
continue
lines.append(self.line_of(whole, one, qty, times, name, copies, depth, use))
for mat in doc.get("자재") or []:
key = f"{rid}#{labor_doc.material_key(mat.get('코드'))}"
lines.append(_material(whole, mat, qty_of(key, (mat.get("수량") or {}).get("손"))))
lines = [ln if ln["kind"] == "blank" or ln.get("qty") is not None else {**ln, "ask": ASK}
for ln in lines] # fmt: skip
return self.put(
{
"kind": "사전조합",
"name": doc.get("이름") or ref.get("키"),
"spec": LAYER_WORD.get(str(ref.get("층")), ""), # 번호(키)는 풍선 글로만
"spec_raw": str(ref.get("키") or ""),
"unit": doc.get("단위") or "",
"cite": "",
"origin": {"참조": rid},
"lines": lines,
},
f"C:{rid}|{where}" if where and any(pilled) else None,
)
# ── 조합(구조물 사본) ───────────────────────────────────────────────
def combo(self, folder: Path, cover: str | None = None) -> str | None:
labor = read_json(folder / LABOR_FILE)
if not isinstance(labor, dict):
return None
prices = read_json(folder / PRICE_FILE) or {}
values = read_json(folder / VALUES_FILE) or {}
picks = read_json(folder / PICKS_FILE) or {}
whole = item_prices.load_prices(prices)
area = read_json(folder / AREA_FILE)
result = item_prices.calc(labor, prices, values, picks, area)
use = (labor_doc.area_values(area), cover or f"S:{folder.name}") # SC 안 줄 알약(723-4)
doc = item_prices.with_values(labor.get("줄") or {}, values, picks)
copies = labor.get("참조사본") or {}
lines = []
for one, done in zip(doc.get("담은로직") or [], result.get("줄") or []):
name = done.get("이름") or str(one.get("로직") or "")
pill = next(
(p for p in result.get("알약") or [] if p.get("키") == done.get("알약")), {}
)
line = self.line_of(
whole,
one,
done.get("수량"),
done.get("배"),
pill.get("이름") or name,
copies,
0,
use,
)
if line.get("qty") is None and line["kind"] != "blank":
line = {**line, "why": "수량 없음 — 면적산출 알약 칸 · 손 수량을 넣음"}
lines.append(line)
lines += [_material(whole, mat, mat.get("수량")) for mat in result.get("자재") or []]
manifest = read_json(folder / MANIFEST_NAME) or {}
origin = manifest.get("원본") or {}
return self.put(
{
"kind": "조합",
"name": origin.get("name") or " ".join(origin.get("경로") or []) or folder.name,
"spec": " › ".join(origin.get("경로") or []),
"unit": (labor.get("줄") or {}).get("단위") or "",
"cite": "",
"origin": {k: origin.get(k) for k in ("layer", "owner", "key")},
"lines": lines,
},
cover or f"S:{folder.name}",
)
def library(self, folder: Path, manifest: dict[str, Any]) -> str | None:
"""설계내역서 서고 사본(204-2) — 구조물조합 = 조합 블록 · 사전조합 = 겹 대가 블록 그대로."""
if manifest.get("종류") not in ("precombo", "sangeun"):
return self.combo(folder, f"L:{folder.name}")
labor = read_json(folder / LABOR_FILE)
ref = (manifest.get("원본") or {}).get("참조")
copies = (labor or {}).get("참조사본") or {}
doc = copies.get(refs.ref_id(ref)) if isinstance(ref, dict) else None
if not isinstance(doc, dict):
return None
whole = item_prices.load_prices(read_json(folder / PRICE_FILE) or {})
if manifest["종류"] == "sangeun": # 내 자료집 단가산출(719-2)
return self.sangeun(whole, ref, doc, copies, 1)
return self.precombo(whole, ref, doc, copies, 1)
def _Loose(whole: Any, read: Any) -> Any:
"""B09 SK 속 펼침(723-4) — 사전조합 안 줄 수량 없음 = 0 값묶음(엔진 차례 · 수량만 씀 ·
그 줄 값 = B09 사전조합 블록 · 이 공사에서 입력) · 그 밖 까닭은 그대로."""
from M02_MasterTemplete.M02_Labor_Expand import NO_QTY, Expand
class Loose(Expand):
def __call__(self, ref: dict, trail: tuple[str, ...] = ()) -> dict[str, Any]:
got = super().__call__(ref, trail)
if got.get("기준") is None and str(got.get("까닭") or "").endswith(NO_QTY):
return {**got, "기준": dict.fromkeys(labor_doc.COSTS, Decimal(0))}
return got
return Loose(whole, read)
def fixed_line(fx: dict, qty: Any) -> dict[str, Any]:
"""고정 줄(705-1) → 블록 줄 `fixed` — 수량 그대로(배 1) · 이음 없는 비목 = 줄 단가 ·
비율 줄(705-3) = `pct`(밑 = 블록 그 비목 합 · 수량 = 율)."""
if rate := fx.get("비율"):
name = {"name": fx.get("이름") or "", "spec": fx.get("규격") or "", "unit": "%"}
return {**name, "kind": "pct", "part": PART[rate["비목"]],
"base": f"part:{PART[rate['기준']]}", "qty": _f(qty)} # fmt: skip
link = fx.get("이음") or {}
return {
"kind": "fixed",
**{k: fx.get(v) or "" for k, v in (("name", "이름"), ("spec", "규격"), ("unit", "단위"))},
"qty": _f(qty),
"own": {PART[k]: _f(v) for k, v in (fx.get("단가") or {}).items() if k in PART},
"link": {
"labor": link.get("노임"),
"material": link.get("자재"),
"machine": link.get("기계"),
},
}
def _material(whole: Any, mat: dict, qty: Any) -> dict[str, Any]:
"""자재 줄 — 조합 사용자 자재 · 사전조합 자재(175-2) 한 꼴 · 이름 · 규격 · 단위 = 사본 단가."""
row = (whole.get(mat.get("코드")) if mat.get("코드") else None) or {}
names = {k: row.get(v) or "" for k, v in (("name", "이름"), ("spec", "규격"), ("unit", "단위"))}
return {"kind": "material", "key": mat.get("코드"), **names, "qty": _f(qty)}
def _engine_bases(engine: list[dict], items: list[dict]) -> dict[str, float]:
"""엔진 호표 줄 → 덧줄 밑수 후보 — `part:비목` 합 · `lines:labor` 인력 줄 합 · `line:이름`."""
out: dict[str, float] = {f"part:{p}": 0.0 for p in PART.values()}
out["lines:labor"] = 0.0
for x, item in zip(engine, items):
for name, value in (x.get("비목") or {}).items():
if name in PART:
out[f"part:{PART[name]}"] += _f(value) or 0
if item.get("종류") == "인력":
out["lines:labor"] += _f(x.get("금액")) or 0
if x.get("이름"):
out[f"line:{x['이름']}"] = _f(x.get("금액")) or 0
return out
def _base_of(formula: str) -> tuple[str | None, str]:
"""덧줄 식 → (밑수 갈래, 이름) — 모르는 꼴이면 (None, "")."""
if hit := BASE_PART.match(formula):
return f"part:{PART[hit.group(1)]}", hit.group(1)
if hit := BASE_LINE.match(formula):
return f"line:{hit.group(1)}", hit.group(1)
if BASE_LABOR.match(formula):
return "lines:labor", "인건비"
return None, ""
def _plain(value: Any) -> Any:
if isinstance(value, Decimal):
return float(value)
if isinstance(value, dict):
return {k: _plain(v) for k, v in value.items()}
if isinstance(value, list):
return [_plain(v) for v in value]
return value
def cover_count(project_root: str | Path, folder: str) -> int:
"""702-4 B08 구조물도 카드 — 그 사본 블록 + 부른 블록에 붙은 B09 고친 칸 수(카드 = 사본 셈)."""
from B09_Estimation.B09_Estimation_Store import read_user
cover = read_user(project_root).get("daega_cover") or {}
if not cover:
return 0
got = daega_inputs(
project_root
) # ponytail: 프로젝트 한 벌 셈 — 카드 열 때만 · 느리면 사본 하나로
by_id = {b["id"]: b for b in got["blocks"]}
owners = {linked["폴더"]: o for o, linked in linked_snapshots(project_root).items()}
todo, seen = [got["owners"].get(owners.get(folder, ""))], set()
while todo:
block = by_id.get(todo.pop())
if block is None or block["id"] in seen:
continue
seen.add(block["id"])
todo += [ln.get("key") for ln in block["lines"] if ln["kind"] == "daega"]
return sum(len(cover.get(by_id[bid]["cover"]) or {}) for bid in seen)
def daega_inputs(project_root: str | Path) -> dict[str, Any]:
"""대가 셈 입력 — `{blocks: [...], owners: {구조물 id: 블록 id}}` · 사본 없으면 빈 것 ·
설계내역서 서고 사본(204-2)이 있으면 `libs` {폴더: 블록 id}.
차례 = 조합(구조물 id 차례 → 서고 사본) → 사전조합 → 일위대가화(품셈 · 처음 본 차례) —
호표 번호 = 이 차례.
"""
base = structures_dir(project_root)
build = Builder()
owners: dict[str, str] = {}
for owner, linked in sorted(linked_snapshots(project_root).items()):
if "일위대가" not in linked.get("부분", []):
continue
try:
bid = build.combo(base / linked["폴더"])
except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError):
bid = None # 한 사본이 막혀도 나머지 블록은 셈 — 그 구조물은 없음
if bid:
owners[owner] = bid
libs: dict[str, str] = {}
for folder, manifest in library.copies(project_root):
try:
bid = build.library(folder, manifest)
except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError):
bid = None
if bid:
libs[folder.name] = bid
order = {"조합": 0, "사전조합": 1, "품셈": 2}
blocks = sorted(build.blocks.values(), key=lambda b: order[b["kind"]])
out = {"blocks": blocks, "owners": owners}
return _plain({**out, "libs": libs} if libs else out)