요구 651 · 653 — 계산 공식 참고용 흐름도(① M01 로직 상세 오른쪽 접는 패널)를 걷음. ② M02 일위대가 다이어그램은 그대로.
지움(64 파일 · 18,666 줄 · 되돌릴 길 = git 이력)
- M01 6 파일 1,064 줄 — Store_Flow.py · UI_LogicLab_Flow.ts/.css · FlowRows.ts · FlowView.ts/.css
- M02_MasterTemplete/DataFlow 43 파일 12,799 줄 — 흐름도 화면 · 셈(Compute · Convert · Lookup · Model · Eval …)
- 시험 15 파일 4,803 줄 — test_m01_logic_flow{view,_engine,_project,_save} · test_m02_df_*(값 찾기 values_old 뺌) — 지운 흐름 코드만 지키던 것
남김(바닥) — DataFlow M02_DF_UnitPick(일위대가 셋) · M02_DF_Values(M02_Item_Prices) · M02_DF_Round · _standard 표 ·
M02_DF_Router = GET /api/m02/df/standards 하나 · M02_DF_Api = cfStandards 하나 · CfRound 꼴은 Round 안으로
고침
- M01 Router — /save = Store_Make.save(개정 확인 · 자체 파일 새 줄 GX · added 그대로) · /calc /text /logic/auto = 옛 셈만 · /logic/flow 둘 지움
- master_formula — 흐름 훅 지움 · 줄에 「흐름」 이 남으면 「흐름도 로직은 걷음」 까닭 오류(저장된 흐름 로직 0 — 마스터 · storage 다시 확인)
- Sangeun_Calc._defaults = 옛 자동값 · M02_Logic_Lib_Read /logic/flow 지움 · main.py 주석
- 화면 — LogicLab(startFlow · scheduleFlow · 예시값 잇기 지움 · [신규 로직] 줄 = 입력 · 중간 · 호표 빈 목록 + 끝수 null) ·
Side(원문 패널 하나) · Source(머리 칸 이름 m01lab-src__top/__title · DF_Panel.css 안 씀) · Detail · View(nowInputs 지움) · Sheet(흐름 갈래 지움) · 글
- 시험 — 지운 파일을 가리키던 목록 넷(spreadsheet_size · 208_1 · 588 · input_prefs) · 717_1 흐름 길 두 줄 · values_old 흐름 셈 부분 지움
- test_713_3_list 기대 — 로직 1,461 → 1,515 · 묶음 307 → 335 · 작업량 21 → 65 · 부르는 506 → 512(782-1 · 2 · 3 건설 기초 로직 54 더함 ·
옛 키만 세면 307 · 21 · 506 그대로)
- 새 지킴 test_794_1b_flow_gone — 흐름 길 404 · 표준 표 남음 · 신규 로직 자체 GX · 흐름 남은 줄 까닭 오류
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
614 lines
30 KiB
Python
614 lines
30 KiB
Python
"""M02 단가산출(SK) 셈 — 바탕 사슬 → 고친 줄 → M01 엔진 한 벌(PLAN 715-1b · 715-2).
|
|
|
|
셈 = ① 바탕(M01 로직 · 바탕 SK 의 고친 줄 · 없음) ② `M02_Sangeun_Doc.patched` ③ 더한줄 참조 ·
|
|
(SK · SC) · 고정 줄 = 먼저 셈한 1 단위 노 · 재 · 경 → `값묶음`
|
|
④ M01 색인 위에 고친 줄을 SK 키로 얹어
|
|
(M01 시험 계산 `_swap` 과 같은 꼴 · 메모리만 · 정본 · 캐시 안 고침) `labor.run_line` — 자동값 ·
|
|
켜쌓기율 · 고름 · % 덧줄을 엔진이 다시 셈. 1 단위 = 기준 ÷ 배(바탕 로직 결과단위 앞 수).
|
|
계산 자리(CLAUDE.md 5장 ③) = 서버 단독 · 화면은 셈 길(저장 안 함 · 최신 응답만).
|
|
저장(`clean_doc`) = 꼴 검사 + 참조 위 층 · 있는 문서 · 순환 422 + 서버가 `원값` · `바탕판` 적음.
|
|
둘째 기초(727-2 · 더한줄 로직) = 세부 덮개 · 뺀줄 있으면 고친 기초 줄을 엔진으로 셈한 `값묶음` 줄
|
|
(B09 굳힘도 그 값) · 화면 셈 길(`calc_doc`) = 그 줄 덧칸 `펼침`(세부 줄 × 수량) · `뺀세부` ·
|
|
`바탕판` ·
|
|
`꼬리표`(원문번호) · 답 `꼬리표` = 바탕 원문번호.
|
|
SK 단위 환산(728-2) = 배 = 바탕 기준단위(「100본」) → SK 단위(`master_unit.swap` ·
|
|
「식」 · 빈 단위 = 앞 수 뗀 바탕 단위 = 옛 셈) ·
|
|
다른 성격 = 문서 `환산` {계수 | 단위중량 {종별, 형상}} · 없으면 셈 막음(까닭) ·
|
|
화면 셈 길 줄 · 계 = SK 1 단위 몫(`per_unit`).
|
|
바탕 재료(747-1) = 별도계상 빈 칸 = 직접계산(M01 기초처럼 재료비에 듦 · 「별도계상」 고른 줄만 밖) ·
|
|
일위대가 덮개 줄(`line` · `M02_Labor_Patch.run`)은 일위대가 기본(222-1 · 162-1) 그대로.
|
|
보기 넷 한 답(750-1) = 화면 셈 길 답에 `글`(읽는 식) · `중간`(`M02_Sangeun_Views`) · 일위대가 줄
|
|
모달 = 문서 `줄모드` true → `line`(일위대가 그 줄 셈과 같은 값).
|
|
율 더한줄(778-4 · `_rated`) = 엔진 한 번 돈 줄로 밑(`M02_Labor_Fixed.rate_base` · 짝 화면
|
|
`M02_MasterTemplete_Labor_Calc.ts` `rateBase` · 거울 시험 `test_778_4_rate_line.py`) → 율 줄 `값묶음` =
|
|
{비목: 밑 ÷ 100} → 엔진 다시(율 줄 없는 문서 = 한 번만 · 값 그대로) · 줄 덧칸 `율` · `규격`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import re
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
|
|
from M02_MasterTemplete import M02_Labor_Doc as labor
|
|
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
|
from M02_MasterTemplete import M02_Labor_Ref as refs
|
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
|
from M02_MasterTemplete import M02_Sangeun_Const as const, M02_Sangeun_Doc as sd
|
|
from M02_MasterTemplete import M02_Sangeun_Views as views
|
|
|
|
COSTS = labor.COSTS
|
|
Ref = dict[str, Any]
|
|
_SHARED = ("_market", "_by_class", "_found") # 마스터에만 따르는 자재 색인 · 찾기 답
|
|
|
|
|
|
class _Over:
|
|
"""M01 색인 위에 고친 줄 하나 — `logic(키)` 만 가로챔(그 밖 그대로 · 색인 안 고침)."""
|
|
|
|
def __init__(self, whole: Any, key: str, row: dict) -> None:
|
|
self._whole, self._key, self._row = whole, key, row
|
|
|
|
def logic(self, ref: str) -> dict:
|
|
return self._row if ref == self._key else self._whole.logic(ref)
|
|
|
|
def __getattr__(self, name: str) -> Any:
|
|
return getattr(self._whole, name)
|
|
|
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
"""자재 색인(`master_material`)은 속 M01 색인에 둠 — 셈마다 다시 안 지음(792-2) ·
|
|
M01 파일 자국이 바뀌면 새 색인 = 새로 지음 · 그 밖은 이 덮개에만."""
|
|
if name in _SHARED:
|
|
setattr(self._whole, name, value)
|
|
else:
|
|
super().__setattr__(name, value)
|
|
|
|
|
|
def _fail(e: sd.DocError) -> store.StoreError:
|
|
return store.StoreError(e.status, str(e))
|
|
|
|
|
|
def times_of(row: dict) -> Decimal:
|
|
"""결과단위 분모 앞 수(원/10㎡ → 10) — 없으면 1."""
|
|
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
|
|
|
per = combo_store.mu.per(row.get("결과단위")) # 712-2 단위 한 벌
|
|
return per[0][0] if len(per) == 1 else Decimal(1)
|
|
|
|
|
|
def base_unit(row: dict) -> tuple[str, str] | None:
|
|
"""바탕 기준단위 — 「원/100본」 → (「100본」, 「본」) · 분모 없거나 여럿 = None."""
|
|
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
|
|
|
per = combo_store.mu.per(row.get("결과단위"))
|
|
if len(per) != 1:
|
|
return None
|
|
n, u = per[0]
|
|
return (u if n == 1 else f"{format(n, 'f')}{u}", u)
|
|
|
|
|
|
def ratio_of(doc: dict, row: dict, whole: Any) -> Decimal:
|
|
"""SK 배(728-2) — 바탕 기준단위 1 개 = SK 단위 몇 개 · 1 단위 = 기준 ÷ 배 ·
|
|
못 바꿈 = `DocError`."""
|
|
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
|
|
|
mu = combo_store.mu
|
|
got = base_unit(row)
|
|
if got is None:
|
|
return Decimal(1)
|
|
want = str(doc.get("단위") or "").strip()
|
|
want = (
|
|
got[1] if want in ("", "식") else want
|
|
) # 일위대가 줄 · 「식」 = 옛 셈(앞 수 뗀 바탕 단위)
|
|
how = doc.get("환산") or {}
|
|
pick = how.get("단위중량") or {}
|
|
rho = mu.unit_weight(whole, pick.get("종별"), pick.get("형상")) if pick else None
|
|
swap = mu.swap(got[0], want, rho, how.get("계수"))
|
|
if swap["배"] is None:
|
|
raise sd.DocError(f"단가산출 단위 · {swap['근거']}")
|
|
return swap["배"]
|
|
|
|
|
|
def fingerprint(row: dict) -> str:
|
|
raw = json.dumps(row, ensure_ascii=False, sort_keys=True, default=str)
|
|
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
|
|
|
|
|
|
# ── 바탕 사슬 → 고친 줄 ─────────────────────────────────────────────────
|
|
def _base_of(doc: dict, whole: Any, read: Any, trail: tuple[str, ...]) -> dict[str, Any]:
|
|
"""이 SK 가 얹힐 바탕 — {줄, 고름, 자재표시, 입력값, 켜쌓기율, 짝잃음}."""
|
|
base = doc.get("바탕")
|
|
empty = {"고름": {}, "자재표시": {}, "입력값": {}, "켜쌓기율": None, "짝잃음": []}
|
|
if not base:
|
|
return {**empty, "줄": sd.empty_row(doc)}
|
|
if "로직" in base:
|
|
try:
|
|
return {**empty, "줄": whole.logic(base["로직"])}
|
|
except Exception as e: # noqa: BLE001 — 엔진 FormulaError
|
|
raise sd.DocError(f"바탕 로직 · {e}") from e
|
|
ref = base["참조"]
|
|
rid = refs.ref_id(ref)
|
|
if rid in trail:
|
|
keys = [t.rsplit("|", 1)[-1] for t in (*trail, rid)]
|
|
raise sd.DocError("순환 — " + " → ".join(keys), 422)
|
|
if len(trail) >= refs.DEPTH:
|
|
raise sd.DocError(f"바탕 사슬이 {refs.DEPTH}단을 넘음", 422)
|
|
parent = read(ref)
|
|
if not isinstance(parent, dict):
|
|
raise sd.DocError(f"없는 바탕 「{ref.get('키')}」")
|
|
up = resolve(parent, whole, read, (*trail, rid))
|
|
lost = [{**x, "바탕": ref.get("키")} for x in up["짝잃음"]]
|
|
return {**up, "짝잃음": lost}
|
|
|
|
|
|
def resolve(
|
|
doc: dict,
|
|
whole: Any,
|
|
read: Any = refs.read_ref,
|
|
trail: tuple = (),
|
|
given: dict | None = None,
|
|
line: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""SK 문서 → 고친 줄 `{줄, 고름, 자재표시, 정체, 짝잃음, 입력값, 켜쌓기율, 배, 바탕판}` ·
|
|
`trail` = 지나온 참조 이름표(순환 · 깊이 422) · `given` = 공통 옵션(더한줄 로직 입력)."""
|
|
base = _base_of(doc, whole, read, trail)
|
|
common = {**base["입력값"], **(doc.get("입력값") or {}), **(given or {})}
|
|
doc, lack = _fill(doc, whole, common)
|
|
got = sd.patched(base["줄"], doc, base["고름"], base["자재표시"])
|
|
items = got["줄"]["호표"]
|
|
if not line:
|
|
_direct(got["자재표시"], items)
|
|
for at, cid in enumerate(got["정체"][: len(items)]):
|
|
hit = re.match(r"^add:#(\d+)$", cid)
|
|
if hit and int(hit[1]) in lack:
|
|
items[at] = _gap(items[at], lack[int(hit[1])])
|
|
elif hit and sd.sub_patched(doc["더한줄"][int(hit[1])]): # 고친 둘째 기초 = 셈한 값묶음
|
|
sub = sub_run(doc["더한줄"][int(hit[1])], whole)
|
|
got["짝잃음"] += [{**x, "cid": f"{cid}/{x['cid']}"} for x in sub["짝잃음"]]
|
|
items[at] = _bundled(items[at], sub)
|
|
rate = doc.get("켜쌓기율", base["켜쌓기율"])
|
|
return {
|
|
**got,
|
|
"채운더한줄": doc["더한줄"],
|
|
"짝잃음": base["짝잃음"] + got["짝잃음"],
|
|
"입력값": {**base["입력값"], **(doc.get("입력값") or {})},
|
|
"켜쌓기율": rate,
|
|
"배": ratio_of(doc, base["줄"], whole),
|
|
"바탕판": fingerprint(base["줄"]),
|
|
}
|
|
|
|
|
|
def _direct(marks: dict, items: list) -> None:
|
|
"""SK 재료 줄 별도계상 빈 칸 = 직접계산(747-1 · 제자리) — 다른 두 칸은 그대로."""
|
|
for n, item in enumerate(items):
|
|
if item.get("종류") == "재료":
|
|
marks[str(n)] = {"별도계상": "직접계산", **labor.marks(marks.get(str(n)))}
|
|
|
|
|
|
# ── 못 셈 줄 = 그 줄만 빠짐(725-1 · 725-2) ─────────────────────────────────
|
|
NO_QTY = "수량 없음" # `M02_Labor_Expand.NO_QTY` 와 같은 글(가져오기 둘레 피함)
|
|
NO_QTY_WHY = "수량 없음 — 쓰는 자리에서 연결"
|
|
|
|
|
|
def _defaults(whole: Any, code: str) -> dict:
|
|
"""로직 기본값 = M01 견본 입력(`/m01/logic/auto` 와 같은 길)."""
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
|
|
try:
|
|
got = {"값": m01.mau.values(whole, whole.logic(code))}
|
|
except Exception: # noqa: BLE001 — 없는 로직 · 자동값 못 셈 = 기본값 없음
|
|
return {}
|
|
return got.get("값") or {}
|
|
|
|
|
|
def _fill(doc: dict, whole: Any, common: dict) -> tuple[dict, dict[int, str]]:
|
|
"""더한줄 로직 입력 = 줄 값 › 공통 옵션 › 로직 기본값 — (채운 사본, {더한줄 차례: 까닭})."""
|
|
adds, lack = [], {}
|
|
for k, one in enumerate(doc.get("더한줄") or []):
|
|
if one.get("종류") != "로직":
|
|
adds.append(one)
|
|
continue
|
|
try:
|
|
names = [str(s.get("이름")) for s in whole.logic(one["로직"]).get("입력") or []]
|
|
except Exception: # noqa: BLE001 — 없는 로직 = 검사(`sd.clean`)가 먼저 막음
|
|
names = []
|
|
mine = dict(one.get("입력값") or {})
|
|
blank = [n for n in names if mine.get(n) is None or not str(mine[n]).strip()]
|
|
auto = _defaults(whole, one["로직"]) if blank else {}
|
|
for n in blank:
|
|
mine.pop(n, None)
|
|
for v in (common.get(n), auto.get(n)):
|
|
if v is not None and str(v).strip():
|
|
mine[n] = v
|
|
break
|
|
miss = [n for n in names if n not in mine]
|
|
if miss:
|
|
lack[k] = f"입력 「{miss[0]}」 없음"
|
|
adds.append({**one, "입력값": mine})
|
|
return {**doc, "더한줄": adds}, lack
|
|
|
|
|
|
def _gap(item: dict, why: str) -> dict:
|
|
"""못 셈 줄 → 0 값묶음 줄(차례 그대로 · 계에 안 듦) + `빠짐` 까닭."""
|
|
zero = dict.fromkeys(COSTS, Decimal(0))
|
|
head = {k: item.get(k) or "" for k in ("이름", "단위")}
|
|
return {**head, "종류": "로직", "요소": None, "수량": item["수량"], "값묶음": zero, "빠짐": why}
|
|
|
|
|
|
# ── 둘째 기초 세부(727-2) ─────────────────────────────────────────────────
|
|
def sub_run(one: dict, whole: Any, patched: bool = True) -> dict[str, Any]:
|
|
"""더한줄 로직 하나 = 그 기초 줄(+ 세부 덮개 · 뺀줄)을 엔진으로 — `run_line` 답 + 정체 ·
|
|
짝잃음 ·
|
|
바탕판(덮기 전 기초 지문) · 입력 = 채운 줄 값(`_fill`). `patched` False = 덮기 전."""
|
|
code = str(one["로직"])
|
|
try:
|
|
row = whole.logic(code)
|
|
except Exception as e: # noqa: BLE001 — 엔진 FormulaError
|
|
return {"기준": None, "까닭": str(e), "정체": [], "짝잃음": [], "바탕판": ""}
|
|
if any(one.get(k) not in (None, {}, []) for k in sd.BODY): # 넣은 SK 몸(740-1) = 이름 없는 SK
|
|
keep = ("입력값", *sd.BODY, *(("덮개", "뺀줄") if patched else ()))
|
|
doc = {"키": "SK-000000", "이름": "줄", "단위": "식", "바탕": {"로직": code}}
|
|
doc |= {"덮개": {}, "뺀줄": [], "더한줄": [], **{k: one[k] for k in keep if k in one}}
|
|
try:
|
|
return {**run(doc, whole), "바탕판": fingerprint(row)}
|
|
except sd.DocError as e:
|
|
return {"기준": None, "까닭": str(e), "정체": [], "짝잃음": [], "바탕판": ""}
|
|
got = sd.patched(row, one if patched else {})
|
|
over = _Over(whole, code, {**got["줄"], "키": code})
|
|
out = labor.run_line(over, code, one.get("입력값"), None, got["고름"] or None)
|
|
return {**out, "정체": got["정체"], "짝잃음": got["짝잃음"], "바탕판": fingerprint(row)}
|
|
|
|
|
|
def _bundled(item: dict, sub: dict) -> dict:
|
|
"""고친 둘째 기초 줄 → 셈한 1 기준단위 노 · 재 · 경 `값묶음` 줄(수량 그대로) ·
|
|
못 셈 = `_gap`."""
|
|
if not sub.get("기준"):
|
|
return _gap(item, str(sub.get("까닭") or "돈 아닌 로직"))
|
|
head = {k: item.get(k) or "" for k in ("이름", "단위")}
|
|
bundle = {k: Decimal(str(sub["기준"][k])) for k in COSTS}
|
|
return {**head, "종류": "로직", "요소": None, "수량": item["수량"], "값묶음": bundle}
|
|
|
|
|
|
def tag_of(whole: Any, code: str) -> str:
|
|
"""꼬리표 = 기초 원문번호(「9-12-1」) · 없으면 키."""
|
|
try:
|
|
return str(whole.logic(code).get("원문번호") or code)
|
|
except Exception: # noqa: BLE001
|
|
return code
|
|
|
|
|
|
def _detail(one: dict, at: int, whole: Any) -> dict[str, Any]:
|
|
"""둘째 기초 세부 줄(화면) — 수량 · 금액 · 비목 = 세부 × 더한줄 수량(상자 합 몫) · `원문` =
|
|
그 기초 1 기준단위 몫(M01 원문 품 · 744-1 화면 품 칸) · cid `add:#k/세부 cid` ·
|
|
`바탕값` = 덮기 전 1 기준단위 품 · 단가 · `뺀세부` = 뺀 줄 이름."""
|
|
sub = sub_run(one, whole)
|
|
plain = sub_run(one, whole, False) if sd.sub_patched(one) else sub
|
|
was = dict(zip(plain["정체"], plain.get("줄") or []))
|
|
covers, times = one.get("덮개") or {}, Decimal(str(one.get("수량") or 0))
|
|
rows = []
|
|
for cid, line in zip(sub["정체"], sub.get("줄") or []):
|
|
mine = covers.get(cid) or {}
|
|
scaled = {
|
|
k: None if line.get(k) is None else Decimal(str(line[k])) * times
|
|
for k in ("수량", "금액")
|
|
}
|
|
parts = {
|
|
k: None if v is None else Decimal(str(v)) * times
|
|
for k, v in (line.get("비목") or {}).items()
|
|
}
|
|
rows.append(
|
|
{
|
|
**line,
|
|
**scaled,
|
|
"비목": parts,
|
|
"cid": f"add:#{at}/{cid}",
|
|
"원문": {k: line.get(k) for k in ("수량", "금액", "비목")},
|
|
"바탕값": _was(was[cid]) if cid in was else None,
|
|
**({"덮개": [k for k in sd.COVER_KEYS if k in mine]} if mine else {}),
|
|
**({"원값": mine["원값"]} if "원값" in mine else {}),
|
|
}
|
|
)
|
|
gone = [
|
|
{"cid": f"add:#{at}/{c}", "이름": was[c].get("이름") or ""}
|
|
for c in one.get("뺀줄") or []
|
|
if c in was
|
|
]
|
|
tag = tag_of(whole, str(one["로직"]))
|
|
return {"펼침": rows, "뺀세부": gone, "바탕판": sub["바탕판"], "꼬리표": tag}
|
|
|
|
|
|
def _bundles(row: dict, whole: Any, expand: Any, trail: tuple) -> None:
|
|
"""더한줄 참조 · 고정 줄 `값묶음` = 1 단위 노 · 재 · 경 — 못 셈 줄 = `_gap`(나머지 셈 계속)."""
|
|
for at, item in enumerate(row["호표"]):
|
|
if item.get("값묶음") is not None or not ("참조" in item or "고정" in item):
|
|
continue
|
|
sub = expand(item["참조"], trail) if "참조" in item else fixed.line(whole, item["고정"])
|
|
item["이름"] = item.get("이름") or sub.get("이름") or ""
|
|
item["단위"] = item.get("단위") or sub.get("단위") or ""
|
|
if sub.get("기준") is None:
|
|
why = str(sub.get("까닭") or "")
|
|
row["호표"][at] = _gap(item, NO_QTY_WHY if NO_QTY in why else why)
|
|
continue
|
|
item["값묶음"] = {k: Decimal(str(sub["기준"][k])) for k in COSTS}
|
|
|
|
|
|
def run(
|
|
doc: dict,
|
|
whole: Any,
|
|
given: dict | None = None,
|
|
read: Any = refs.read_ref,
|
|
expand: Any = None,
|
|
trail: tuple = (),
|
|
detail: bool = False,
|
|
line: bool = False,
|
|
seen: bool = True,
|
|
) -> dict[str, Any]:
|
|
"""SK 셈(저장 안 함) — `run_line` 답 + `{1단위, 배, 정체, 짝잃음, 바탕판}` · 줄마다 `cid` ·
|
|
고친 칸 `덮개`(칸 이름) · `원값`. 틀린 문서 · 순환 = `sd.DocError` ·
|
|
`detail` = 둘째 기초 세부 펼침(`_detail` · 화면 셈 길만) · 그때 `seen` = 보기 덧칸(750-1) ·
|
|
`line` = 일위대가 덮개 줄."""
|
|
from M02_MasterTemplete.M02_Labor_Expand import Expand
|
|
|
|
got = resolve(doc, whole, read, trail, given, line)
|
|
row = {**got["줄"], "호표": [dict(x) for x in got["줄"]["호표"]]}
|
|
_bundles(row, whole, expand or Expand(whole, read), trail)
|
|
head = {k: got[k] for k in ("정체", "짝잃음", "바탕판")} | {"배": got["배"]}
|
|
gaps = {at: x for at, x in enumerate(row["호표"]) if x.get("빠짐")}
|
|
key = str(doc.get("키") or "SK-000000")
|
|
over = _Over(whole, key, {**row, "키": key})
|
|
# 이 SK 줄의 입력만 — 더한줄 로직 입력은 `_fill` 이 따로 채움
|
|
names = {str(i.get("이름")) for i in row.get("입력") or []}
|
|
values = {k: v for k, v in {**got["입력값"], **(given or {})}.items() if k in names}
|
|
out = labor.run_line(over, key, values, got["켜쌓기율"], got["고름"] or None)
|
|
rates = _rated(row, out.get("줄") or [], got["정체"], got["자재표시"], got["켜쌓기율"])
|
|
if rates is not None: # 율 더한줄(778-4) — 밑을 넣고 다시
|
|
head["짝잃음"] = head["짝잃음"] + rates
|
|
out = labor.run_line(over, key, values, got["켜쌓기율"], got["고름"] or None)
|
|
eng = out.get("줄") or []
|
|
seen = (
|
|
views.text_of(over, key, values, got["고름"], got["정체"], eng) if detail and seen else {}
|
|
)
|
|
covers = doc.get("덮개") or {}
|
|
lines = []
|
|
for at, (cid, line) in enumerate(zip(got["정체"], out.get("줄") or [])):
|
|
mine = covers.get(cid) or {}
|
|
mark = {"덮개": [k for k in sd.COVER_KEYS if k in mine]} if mine else {}
|
|
if at in gaps: # 못 셈 줄 = 금액 — · 까닭(725)
|
|
mark |= {"단가": None, "금액": None, "비목": {}, "까닭": gaps[at]["빠짐"]}
|
|
hit = re.match(r"^add:#(\d+)$", cid)
|
|
one = got["채운더한줄"][int(hit[1])] if hit else {}
|
|
if detail and one.get("종류") == "로직" and at not in gaps:
|
|
mark |= _detail(one, int(hit[1]), whole)
|
|
if sd.is_rate(one): # 율 줄 = 율 · 「기준의 %」(화면 % 줄 · `per_unit` 수량 그대로)
|
|
mark |= {"율": Decimal(str(one["수량"])), "규격": _rate_text(one["고정"]["비율"])}
|
|
lines.append(
|
|
{**line, "cid": cid, **mark, **({"원값": mine["원값"]} if "원값" in mine else {})}
|
|
)
|
|
out = labor.drop_apart({**out, "줄": lines}, got["자재표시"])
|
|
base = out.get("기준")
|
|
unit = {k: Decimal(str(v)) / got["배"] for k, v in base.items()} if base else None
|
|
gone = [
|
|
{"cid": got["정체"][at], "이름": x.get("이름") or "", "까닭": x["빠짐"]}
|
|
for at, x in gaps.items()
|
|
]
|
|
base = doc.get("바탕") or {}
|
|
tag = tag_of(whole, base["로직"]) if "로직" in base else (base.get("참조") or {}).get("키")
|
|
return {**out, **head, **seen, "1단위": unit, "빠진줄": gone, "꼬리표": tag}
|
|
|
|
|
|
def _group(item: dict, line: dict) -> str | None:
|
|
"""율 밑 묶음(778-4) — 인력 · 재료 · 장비(기계경비 로직 · 기계 이음 고정 · 기계 줄) · 그 밖 None."""
|
|
if isinstance(item.get("고정"), dict):
|
|
return fixed.group_of(item["고정"])
|
|
if line.get("장비"):
|
|
return "장비"
|
|
return {"인력": "인력", "재료": "재료", "기계": "장비"}.get(str(item.get("종류")))
|
|
|
|
|
|
def _rated(row: dict, eng: list, ids: list, marks: dict, stack: Any = None) -> list[dict] | None:
|
|
"""율 더한줄(778-4) `값묶음` = {비목: 밑 ÷ 100} — 한 번 돈 엔진 줄(`eng` · 정체 `ids`)로
|
|
`fixed.rate_base`(별도계상 재료 뺌 · 앞 율 줄 차례로 · 켜쌓기율 몫 뗌) · 제자리 · 율 줄 없음 = None ·
|
|
돌려줌 = 짝잃음(없는 기준 줄)."""
|
|
items = row["호표"]
|
|
at = [i for i, x in enumerate(items) if (x.get("고정") or {}).get("비율")]
|
|
if not at:
|
|
return None
|
|
rows = [
|
|
{"키": cid, "묶음": _group(items[i] if i < len(items) else {}, x), "비목": x.get("비목")}
|
|
for i, (cid, x) in enumerate(zip(ids, eng))
|
|
if i not in at
|
|
and not (
|
|
i < len(items) and items[i].get("종류") == "재료" and labor.apart(marks.get(str(i)))
|
|
)
|
|
]
|
|
times = Decimal(str(stack)) / 100 if stack not in (None, 100) else Decimal(1)
|
|
lost = []
|
|
for i in at:
|
|
rate, qty = items[i]["고정"]["비율"], Decimal(str(items[i]["수량"]))
|
|
if isinstance(rate["기준"], list):
|
|
lost += [
|
|
{"cid": ids[i], "까닭": f"율 기준 줄 없음 「{k}」"}
|
|
for k in rate["기준"] or [""]
|
|
if fixed.split_key(k)[0] not in ids
|
|
]
|
|
base = fixed.rate_base(rate["기준"], rows) / times
|
|
items[i] = {
|
|
**items[i],
|
|
"값묶음": {**dict.fromkeys(COSTS, Decimal(0)), rate["비목"]: base / 100},
|
|
}
|
|
rows.append(
|
|
{"키": ids[i], "묶음": None, "비목": {rate["비목"]: base * qty / 100}, "율": True}
|
|
)
|
|
return lost
|
|
|
|
|
|
def _rate_text(rate: dict) -> str:
|
|
"""율 줄 규격 — 「인력의 %」 · 목록 = 「줄 n 개의 %」."""
|
|
basis = rate["기준"]
|
|
return f"줄 {len(basis)} 개의 %" if isinstance(basis, list) else f"{basis}의 %"
|
|
|
|
|
|
def unit(doc: dict, whole: Any, expand: Any, trail: tuple) -> dict[str, Any]:
|
|
"""펼침(`Expand`) 한 칸 — `{이름, 단위, 기준(1 단위), 까닭, 줄, 짝잃음}` · 못 셈 = 까닭."""
|
|
head = {"이름": doc.get("이름"), "단위": doc.get("단위")}
|
|
try:
|
|
got = run(doc, whole, None, expand.read, expand, trail)
|
|
except sd.DocError as e:
|
|
return {**head, "기준": None, "까닭": str(e)}
|
|
gone = got.get("빠진줄") or [] # 쓰는 자리 = 빠진 줄 있으면 통째 기준 없음(부분 값 안 넘김)
|
|
why = got.get("까닭") or next((f"「{x['이름']}」 {x['까닭']}" for x in gone), None)
|
|
why = why or (None if got.get("1단위") else "돈 아닌 로직")
|
|
times = got["배"]
|
|
lines = [
|
|
{
|
|
**x,
|
|
"수량": None if x.get("수량") is None else Decimal(str(x["수량"])) / times,
|
|
"금액": None if x.get("금액") is None else Decimal(str(x["금액"])) / times,
|
|
}
|
|
for x in got.get("줄") or []
|
|
]
|
|
return {
|
|
**head,
|
|
"기준": None if gone else got["1단위"],
|
|
"까닭": why,
|
|
"줄": lines,
|
|
"짝잃음": got["짝잃음"],
|
|
}
|
|
|
|
|
|
def head_of(doc: dict) -> str:
|
|
"""머리 띠 — 「1 본 당」 · 앞 수 있는 단위 = 「100본 당」."""
|
|
unit = str(doc.get("단위") or "")
|
|
return f"{unit} 당" if unit[:1].isdigit() else f"1 {unit} 당"
|
|
|
|
|
|
def per_unit(got: dict, doc: dict) -> dict:
|
|
"""화면 셈 답 줄 · 계 = SK 1 단위 몫(728-2) — 수량 · 금액 · 비목 ÷배(율 · 단가 그대로) ·
|
|
「식」 · 빈 단위(일위대가 줄 모드) = 옛 바탕 몫 그대로."""
|
|
times = got.get("배") or Decimal(1)
|
|
if times == 1 or str(doc.get("단위") or "").strip() in ("", "식"):
|
|
return got
|
|
|
|
def cut(v: Any) -> Any:
|
|
return None if v is None else Decimal(str(v)) / times
|
|
|
|
def one(x: dict) -> dict:
|
|
out = {**x, "금액": cut(x.get("금액"))}
|
|
if "율" not in x:
|
|
out["수량"] = cut(x.get("수량"))
|
|
if x.get("비목"):
|
|
out["비목"] = {k: cut(v) for k, v in x["비목"].items()}
|
|
if x.get("펼침"):
|
|
out["펼침"] = [one(y) for y in x["펼침"]]
|
|
return out
|
|
|
|
lines = [one(x) for x in got.get("줄") or []]
|
|
goods = [one(x) for x in got.get("재료") or []]
|
|
return {**got, "줄": lines, "재료": goods, "기준": got.get("1단위"), "화면배": times}
|
|
|
|
|
|
# ── 저장 ──────────────────────────────────────────────────────────────
|
|
def _check_refs(doc: dict, here_: refs.Here) -> None:
|
|
me = {"층": here_[0], "주인": here_[1], "키": doc["키"]}
|
|
base = doc.get("바탕") or {}
|
|
if "참조" in base:
|
|
base["참조"] = refs.check_ref(base["참조"], "바탕", here_)
|
|
if refs.kind_of(base["참조"]["키"]) != sd.KIND:
|
|
raise store.StoreError(400, "바탕 참조는 단가산출(SK)만")
|
|
for at, one in enumerate(doc.get("더한줄") or [], 1):
|
|
if "참조" in one:
|
|
one["참조"] = refs.check_ref(one["참조"], f"더한줄 {at}째", here_)
|
|
for ref in refs.refs_in(doc):
|
|
if refs.ref_id(ref) == refs.ref_id(me):
|
|
raise store.StoreError(422, f"순환 — 자기 자신을 담음 「{doc['키']}」")
|
|
refs._no_loop(ref, [me, ref])
|
|
|
|
|
|
def clean_doc(doc: Any, here_: refs.Here) -> dict[str, Any]:
|
|
"""저장 꼴(`Labor_Ref.clean_doc` 가 부름) — 검사 · 참조 · 순환 · 서버가 `원값` · `바탕판`."""
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
|
|
whole = m01.loaded()[1]
|
|
try:
|
|
rough = sd.clean(doc, None, whole)
|
|
_check_refs(rough, here_)
|
|
base = _base_of(rough, whole, refs.read_ref, ())
|
|
out = sd.clean(doc, base["줄"], whole)
|
|
out["바탕"], out["더한줄"] = rough["바탕"], rough["더한줄"]
|
|
for one in out["더한줄"]: # 처음수량(744-1) = 넣을 때 수량 · 없으면 지금 수량
|
|
one.setdefault("처음수량", one["수량"])
|
|
if out["덮개"]:
|
|
bare = run({**out, "덮개": {}}, whole) # 덮기 전 값 = 원값(그때 M01)
|
|
was = {x["cid"]: x for x in bare.get("줄") or []}
|
|
for cid, one in out["덮개"].items():
|
|
if cid in was:
|
|
one["원값"] = _was(was[cid])
|
|
out |= const.stamp(base["줄"], out) # 바탕판 · M01판(790-2) · 식 칸 엔진 식(792-1)
|
|
ratio_of(out, base["줄"], whole) # 단위 못 바꿈(계수 없음) = 저장 막음(728-2)
|
|
_stamp_adds(out, base, whole)
|
|
except sd.DocError as e:
|
|
raise _fail(e) from e
|
|
return out
|
|
|
|
|
|
def _stamp_adds(out: dict, base: dict, whole: Any) -> None:
|
|
"""둘째 기초 저장 칸(727-2) — `바탕판` = 그 기초 지문 · 세부 덮개 `원값` = 덮기 전
|
|
1 기준단위 값."""
|
|
filled, _ = _fill(out, whole, {**base["입력값"], **out["입력값"]})
|
|
for one, mine in zip(filled["더한줄"], out["더한줄"]):
|
|
if one.get("종류") != "로직":
|
|
continue
|
|
plain = sub_run(one, whole, False)
|
|
if plain["바탕판"]:
|
|
mine["바탕판"] = plain["바탕판"]
|
|
was = dict(zip(plain["정체"], plain.get("줄") or []))
|
|
for cid, cover in (mine.get("덮개") or {}).items():
|
|
if cid in was:
|
|
cover["원값"] = _was(was[cid])
|
|
|
|
|
|
def _was(line: dict) -> dict[str, Any]:
|
|
keep = {"qty": line.get("율", line.get("수량")), "단가": line.get("단가")}
|
|
return {k: sd.plain(Decimal(str(v))) for k, v in keep.items() if v is not None}
|
|
|
|
|
|
def calc_doc(doc: Any, given: dict | None = None, seen: bool = True) -> dict[str, Any]:
|
|
"""셈 길(`M02_Sangeun_Router`) — 지금 층 문서 · 저장 전 화면 문서 · 틀림 400 · 순환 422 ·
|
|
`seen` = 보기 덧칸(`글` · `중간`) 지음."""
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
|
|
with m01.cache.steady(): # 한 셈 동안 M01 파일 자국 한 번(792-2)
|
|
whole = m01.loaded()[1]
|
|
try:
|
|
clean = sd.clean(doc, None, whole)
|
|
_check_refs(clean, refs.here()) # 저장 전 문서도 위 층만 · 있는 문서 · 순환 422
|
|
me = refs.ref_id({"층": refs.here()[0], "주인": refs.here()[1], "키": clean["키"]})
|
|
line = isinstance(doc, dict) and doc.get("줄모드") is True # 일위대가 줄 모달(750-1)
|
|
got = run(clean, whole, given, trail=(me,), detail=True, line=line, seen=seen)
|
|
except sd.DocError as e:
|
|
raise _fail(e) from e
|
|
return const.answer(got, clean, doc, given, (me,)) # 옛 답 + 원래 · 시트(790-2)
|
|
|
|
|
|
def from_line(
|
|
line: Any, name: str, unit: str, new: bool = False, use: str | None = None
|
|
) -> dict[str, Any]:
|
|
"""[단가산출로 저장](715-3) — 일위대가 줄(바탕 · 입력값 · 고름 · 줄 덮개) → 지금 층 새 SK ·
|
|
답 = `{이름: 새 키, 줄: 그 SK 참조 줄}`(일위대가 문서는 화면 [저장] 때 바뀜) ·
|
|
같은 층 같은 내용 SK = 409(733-2) → `new` 그래도 새로 · `use` 그 SK(새로 안 만듦)."""
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
from M02_MasterTemplete import M02_Labor_Patch as patch
|
|
from M02_MasterTemplete import M02_Library_Ask as ask
|
|
|
|
if not isinstance(line, dict):
|
|
raise store.StoreError(400, "줄이 묶음 아님")
|
|
doc = patch.to_sangeun(line, name, unit, m01.loaded()[1])
|
|
key = ask.ask(sd.KIND, doc, new, use) or refs.create(sd.KIND, doc)["이름"]
|
|
at = refs.here()
|
|
keep = {k: line[k] for k in ("별칭", "메모", "수량") if k in line}
|
|
return {
|
|
"이름": key,
|
|
"줄": {"참조": {"층": at[0], "주인": at[1], "키": key}, **keep},
|
|
}
|