Files
Aislo/M02_MasterTemplete/M02_Doc_Take.py
T
eomsangdonandClaude Opus 5.5 20300af829 feat(816-1): 기계표 덤프트럭(4륜구동) 1ton 줄 · GF000145 가 그 가격을 읽음 · 시간당 경비 한 길
- 기계.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>
2026-11-11 19:17:21 +09:00

260 lines
13 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.
"""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}