- 기계.json EQ000655(산림품셈 10-9 · 가격 28,000 천원 = 조달청 등재가격 기본값) · 키대장 EQ 656 - GF000145 손료 = EQ000655 가격 × 2.5 ton 계수(로직 · 수식 문서 한 줄씩 · 빌더 다시 변환) - M02_Machine_Hourly 의 기계 줄만의 기계경비식(기계 입력 없음) 고름 · 기계='…' 는 받는 식에만(Hourly · Doc_Import · Doc_Take) - 가져온 「덤프트럭 1.0Ton · 1톤 · 1ton」 → EQ000655(이름 · 규격 정규화로 이어짐 · 별칭 안 씀) - test_812_9 못 찾음 견본 1.0Ton → 3.0Ton(1 톤이 우리 표에 생겨 낡음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
260 lines
13 KiB
Python
260 lines
13 KiB
Python
"""M02 줄 쌓기 — 다른 단가산출 · 일위대가 복사해 펼침(PLAN 804-4 · 요구 680-1 · 681) — 서버 단독.
|
||
|
||
M02 단가산출(SK) 줄 쌓기에서 M01 단가산출 · M02 단가산출 · M02 일위대가(SC)를 고름 = 그 문서 전체를 복사해
|
||
이 단가산출의 항목 · 줄로 펼침(참조 줄 「단산 N 참조」 없음 · 원본과 연결 없음) — 이 길은 복사할 조각만 줌,
|
||
문서에 넣는 것은 화면(`common_util_line_stack_copy.ts` · 옵션 = 고른 값으로 굳힘 · 기호 겹침 = 새 이름).
|
||
조각(`take`) — {갈래: 문서, 문서, 입력(옵션 값), 곱(수량 식 · 없으면 그대로), 이름} |
|
||
{갈래: 인력 · 재료 · 기계, 고른, 수량} | {갈래: 율 · 손, 줄} | {갈래: 글, 글}(못 옮김 = 「⚠」)
|
||
M01 단가산출 = 지금 M01 문서 · SK = 그 SK 수식 사본 문서(`M02_Doc_Sk_Path.current`) ·
|
||
SC = 담은로직 줄마다(로직 = M01 문서 × 수량 ÷ 배 · 참조 SK = 그 SK 문서 × 수량 ÷ 그 배 ·
|
||
참조 SC = 그 안 조각 × 수량 · 고정 = 인력(노임) · 재료(자재 · 유가) · 기계(시간당 경비 로직) · 손 단가 ·
|
||
율(비목 기준 · 끝에 — 밑 = 앞 줄 누계)) + 자재 = 재료 줄.
|
||
M01 단가산출은 원천 기준값 — M01 줄 쌓기는 이 길이 없음(M01 찾기 창 = M01 전체만).
|
||
[단가 갱신] = 사본 값 줄(자원 단가)만 지금 M01(`M02_Doc_Copy.refresh` 그대로 · 구조 · 품 · 식 그대로).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||
|
||
COSTS = ("노무비", "재료비", "경비")
|
||
_LAYERS = ("system", "company", "personal")
|
||
DEPTH = refs.DEPTH
|
||
|
||
|
||
def _bad(text: str, status: int = 400) -> store.StoreError:
|
||
return store.StoreError(status, text)
|
||
|
||
|
||
def parse(src: Any) -> dict | None:
|
||
"""`참조|층|주인|키`(`refs.ref_id` 꼴 · SK · SC) → {층, 주인, 키} · 아니면 None(M01 키)."""
|
||
parts = str(src or "").split("|")
|
||
if len(parts) != 4 or parts[0] != "참조" or parts[1] not in _LAYERS:
|
||
return None
|
||
_, layer, owner, code = parts
|
||
if not refs._KEY.match(code) or code[:2] not in ("SK", "SC"):
|
||
return None
|
||
if layer != "system" and not refs._OWNER[layer].match(owner):
|
||
return None
|
||
return {"층": layer, "주인": owner or None, "키": code}
|
||
|
||
|
||
def _text(v: Any) -> str:
|
||
return format(Decimal(str(v)).normalize(), "f")
|
||
|
||
|
||
def _times(q: str | None, more: str | None) -> str | None:
|
||
"""수량 식 곱 — 없음 · 1 = 그대로."""
|
||
q, more = (None if x in (None, "1") else x for x in (q, more))
|
||
if q is None or more is None:
|
||
return q if more is None else more
|
||
return f"({q}) × ({more})"
|
||
|
||
|
||
def _warn(text: str) -> dict:
|
||
return {"갈래": "글", "글": f"⚠ {text}"}
|
||
|
||
|
||
def _plain(given: dict) -> dict:
|
||
out = {}
|
||
for k, v in (given or {}).items():
|
||
if isinstance(v, Decimal):
|
||
v = int(v) if v == v.to_integral_value() else float(v)
|
||
out[str(k)] = v
|
||
return out
|
||
|
||
|
||
# ── 문서 하나 ─────────────────────────────────────────────────────────
|
||
def _m01(code: str, given: Any = None, factor: str | None = None) -> dict:
|
||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||
|
||
whole = dc.skc.whole_()
|
||
doc = dc.doc_of(whole, code)
|
||
if doc is None:
|
||
return _warn(f"수식 문서 없는 단가산출 「{code}」")
|
||
values = dc.given_of(whole, code, given) if given is not None else {}
|
||
piece = {"갈래": "문서", "문서": doc, "입력": _plain(values), "이름": doc.get("이름") or code}
|
||
factor = _times(factor, None)
|
||
return {**piece, "곱": factor} if factor else piece
|
||
|
||
|
||
def _sk(ref: dict, factor: str | None = None, per_unit: bool = False) -> list[dict]:
|
||
from M02_MasterTemplete import M02_Doc_Sk_Path as sp
|
||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||
|
||
raw = refs.read_ref(ref)
|
||
if not isinstance(raw, dict):
|
||
return [_warn(f"없는 단가산출 「{ref['키']}」")]
|
||
try:
|
||
with store.at(refs.folder_of(ref["층"], ref.get("주인"))):
|
||
copy = sp.current(raw)
|
||
if per_unit: # 일위대가 안 SK = 1 단위(기준 ÷ 배)
|
||
clean, whole = sp._settled(raw)
|
||
times = Decimal(str(calc.run(clean, whole).get("배") or 1))
|
||
if times != 1:
|
||
factor = _times(factor, f"1 ÷ {_text(times)}")
|
||
except Exception as e: # noqa: BLE001 — 가져온 식 · 바탕 참조 SK 등 = 못 복사(까닭 글)
|
||
why = getattr(e, "detail", None) or str(e)
|
||
return [_warn(f"단가산출 「{raw.get('이름') or ref['키']}」 못 복사 — {why}")]
|
||
doc = copy["문서"]
|
||
out = [{"갈래": "문서", "문서": doc, "입력": {}, "이름": raw.get("이름") or ref["키"]}]
|
||
factor = _times(factor, None)
|
||
if factor:
|
||
out[0]["곱"] = factor
|
||
if copy.get("끊음"):
|
||
out.append(_warn(f"「{raw.get('이름')}」 안에서 끊어 고친 부름 문서는 지금 M01 판으로 셈"))
|
||
return out
|
||
|
||
|
||
# ── 일위대가(SC) ──────────────────────────────────────────────────────
|
||
def _pick(kind: str, qty: str | None, **picked: Any) -> list[dict]:
|
||
one = {"갈래": kind, "고른": {k: v for k, v in picked.items() if v not in (None, "", {})}}
|
||
if qty is None:
|
||
return [{**one, "수량": "0"}, _warn(f"{picked.get('이름')} — 수량 없음(0 으로 넣음)")]
|
||
return [{**one, "수량": qty}]
|
||
|
||
|
||
def _fixed(fx: dict, qty: str | None, rates: list[dict]) -> list[dict]:
|
||
from M02_MasterTemplete import M02_Machine_Hourly as mh
|
||
|
||
name, spec, unit = fx.get("이름") or "", fx.get("규격") or "", fx.get("단위") or ""
|
||
rate, link = fx.get("비율"), fx.get("이음") or {}
|
||
if rate:
|
||
if rate.get("기준") not in COSTS:
|
||
return [
|
||
_warn(f"율 줄 「{name}」 — 기준 「{rate.get('기준')}」 는 줄 쌓기 율로 못 옮김")
|
||
]
|
||
line = {"이름": name, "단위": "식", "비목": rate.get("비목") or "경비"}
|
||
rates.append({"갈래": "율", "줄": {**line, "단가": f"{rate['기준']} × {qty or 0} %"}})
|
||
return []
|
||
if link.get("노임"):
|
||
return _pick("인력", qty, 키=link["노임"], 이름=name, 규격=spec, 단위="인")
|
||
if link.get("자재") or link.get("유가"):
|
||
code = link.get("자재") or link.get("유가")
|
||
return _pick("재료", qty, 키=code, 이름=name, 규격=spec, 단위=unit)
|
||
if link.get("기계"):
|
||
got = mh.hourly(str(link["기계"]))
|
||
if not got.get("ok") or not got.get("로직"):
|
||
return [_warn(f"기계 「{name}」 — {got.get('reason') or '시간당 경비 못 셈'}")]
|
||
machine = {"기계": f"'{got['원문번호']}'"} if mh.takes_machine(mh.logic_of(got["원문번호"]) or {}) else {} # fmt: skip
|
||
return _pick("기계", qty, 키=got["로직"], 이름=name, 규격=spec, 입력=machine)
|
||
price = fx.get("단가") or {}
|
||
costs = [c for c in COSTS if Decimal(str(price.get(c) or 0))] or ["재료비"]
|
||
return [
|
||
{
|
||
"갈래": "손",
|
||
"줄": {
|
||
"이름": name,
|
||
**({"규격": spec} if spec else {}),
|
||
"단위": unit,
|
||
"비목": c,
|
||
"수량": qty or "0",
|
||
"단가": _text(price.get(c) or 0),
|
||
},
|
||
} # fmt: skip
|
||
for c in costs
|
||
]
|
||
|
||
|
||
def _scaled(pieces: list[dict], factor: str | None) -> list[dict]:
|
||
"""안 SC 조각 × 수량 — 문서 = 곱 · 줄 = 수량 식 · 율 = 그대로(밑이 이미 곱해짐)."""
|
||
if factor is None:
|
||
return pieces
|
||
out = []
|
||
for p in pieces:
|
||
if p["갈래"] == "문서":
|
||
out.append({**p, "곱": _times(p.get("곱"), factor)})
|
||
elif p["갈래"] in ("인력", "재료", "기계"):
|
||
out.append({**p, "수량": _times(p["수량"], factor)})
|
||
elif p["갈래"] == "손":
|
||
out.append({**p, "줄": {**p["줄"], "수량": _times(p["줄"]["수량"], factor)}})
|
||
else:
|
||
out.append(p)
|
||
return out
|
||
|
||
|
||
def _combo(ref: dict, trail: tuple[str, ...] = ()) -> list[dict]:
|
||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||
|
||
rid = refs.ref_id(ref)
|
||
if rid in trail or len(trail) >= DEPTH:
|
||
return [_warn(f"일위대가 참조 돌고 돎 · 너무 깊음 「{ref['키']}」")]
|
||
doc = refs.read_ref(ref)
|
||
if not isinstance(doc, dict):
|
||
return [_warn(f"없는 일위대가 「{ref['키']}」")]
|
||
with store.at(refs.folder_of(ref["층"], ref.get("주인"))):
|
||
ans = labor.calc(doc, {})
|
||
out, rates = [], []
|
||
for one, got in zip(doc.get("담은로직") or [], ans.get("줄") or []):
|
||
qty = None if got.get("수량") is None else _text(got["수량"])
|
||
if isinstance(one.get("고정"), dict):
|
||
out += _fixed(one["고정"], qty, rates)
|
||
elif isinstance(one.get("참조"), dict):
|
||
sub = one["참조"]
|
||
if qty is None:
|
||
out.append(_warn(f"「{got.get('이름') or sub.get('키')}」 — 수량 없음(안 옮김)"))
|
||
elif str(sub.get("키", "")).startswith("SK"):
|
||
out += _sk(sub, qty, per_unit=True)
|
||
elif str(sub.get("키", "")).startswith("SC"):
|
||
out += _scaled(_combo(sub, (*trail, rid)), qty)
|
||
else:
|
||
out.append(_warn(f"옛 단가승격 「{sub.get('키')}」 — 줄 쌓기로 못 옮김"))
|
||
elif one.get("패치"):
|
||
out.append(_warn(f"줄 덮개로 고친 로직 「{one.get('로직')}」 — 줄 쌓기로 못 옮김"))
|
||
else:
|
||
times, r = Decimal(str(got.get("배") or 1)), one.get("켜쌓기율")
|
||
q = qty if qty is None or times == 1 else f"{qty} ÷ {_text(times)}"
|
||
if q is not None and r not in (None, 100):
|
||
q = f"({q}) × {r} ÷ 100"
|
||
if q is None:
|
||
out.append(_warn(f"로직 「{one.get('로직')}」 — 수량 없음(안 옮김)"))
|
||
continue
|
||
out.append(_m01(str(one.get("로직") or ""), one.get("입력값") or {}, q))
|
||
shown = (one.get("자재표시") or {}).values()
|
||
if one.get("고름") or any(labor.apart(x) for x in shown):
|
||
out.append(_warn(f"「{one.get('로직')}」 고른 자재 · 별도계상 표시는 안 옮김"))
|
||
whole = labor.m01.loaded()[1]
|
||
for one, got in zip(doc.get("자재") or [], ans.get("자재") or []):
|
||
code = str(one.get("코드") or "")
|
||
try:
|
||
row = whole.get(code) or {}
|
||
except Exception: # noqa: BLE001 — 지워진 자재 = 코드만
|
||
row = {}
|
||
name = row.get("이름") or code
|
||
if labor.apart(one):
|
||
out.append(_warn(f"별도계상 자재 「{name}」 — 합에서 빠지는 줄이라 안 옮김"))
|
||
continue
|
||
qty = None if got.get("수량") is None else _text(got["수량"])
|
||
unit = one.get("단위") or row.get("단위")
|
||
out += _pick("재료", qty, 키=code, 이름=name, 규격=row.get("규격"), 단위=unit)
|
||
return out + rates
|
||
|
||
|
||
# ── 길 ────────────────────────────────────────────────────────────────
|
||
def take(src: str) -> dict[str, Any]:
|
||
"""복사할 조각 — {이름, 출처, 조각, 계?(일위대가 반영합 · 대조)}."""
|
||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||
|
||
ref = parse(src)
|
||
if ref is None: # M01 단가산출
|
||
piece = _m01(str(src or ""))
|
||
if piece["갈래"] != "문서":
|
||
raise _bad(piece["글"][2:], 404)
|
||
return {"이름": piece["이름"], "출처": f"M01 {src}", "조각": [piece]}
|
||
if not refs.readable(refs.here(), ref):
|
||
raise _bad("위 층(마스터 · 내 회사 · 나) 문서만 복사", 403)
|
||
doc = refs.read_ref(ref)
|
||
if not isinstance(doc, dict):
|
||
raise _bad(f"없는 문서 「{ref['키']}」", 404)
|
||
layer = {"system": "마스터", "company": "회사", "personal": "개인"}[ref["층"]]
|
||
head = {"이름": str(doc.get("이름") or ref["키"]), "출처": f"{layer} {ref['키']}"}
|
||
if ref["키"].startswith("SK"):
|
||
return {**head, "조각": _sk(ref)}
|
||
with store.at(refs.folder_of(ref["층"], ref.get("주인"))):
|
||
total = (labor.calc(doc, {}).get("반영합") or {}).get("계")
|
||
return {**head, "단위": doc.get("단위"), "조각": _combo(ref), "계": total}
|