feat(802-4a): M02 단가산출 · 일위대가 · SC · B09 사본 = 수식 문서 사본 — 문서 셈 · 수식 길 · 옮기기 도구 · 대조
- 문서 칸 `수식`(옛 `사본` 은 802-5 까지 옆에) = 값 사본(로직 · 자원 단가 줄) + 부름 닫힌 묶음 + SK 문서 · 끊음 · 따로 — `M02_Doc_Copy`(새)
· 부름 문서 = 값 줄이 지금 M01 줄과 같으면 M01 수식 문서 · 다르면 만든 때 줄 변환
- SK 금액 = 문서 셈(`M02_Doc_Sk.on_run`) — 옛 칸(덮개 · 더한줄 …)은 파생(엔진에 넣던 고친 줄 → 문서 · 덮개 없는 바탕 = M01 문서 사본) · 수식 길 고침 = 따로(옛 칸 고침 저장 409 · 재생성 = 파생으로)
- 일위대가 · SC 로직 줄 = 문서 셈(`M02_Labor_Copy.line`) · 줄 옵션 = 문서 규칙(켜쌓기율 변수 r · 고른 자재 = 요소 키 · 별도계상 = 합밖 + 뒤 「재료비의 %」 밑엔 그 줄 금액 더함)
- [단가 갱신] = 자원 단가 줄만(문서 그대로) · [재생성] = 지금 M01 문서로(고친 문서 · 끊음 버림 알림) · 빈 SK = 빈 문서 「—」
- 수식 길 GET/POST …/sangeun/{키}/doc · /preview · /save(층 쓰기 · 서버 정본 셈) — `M02_Doc_Sk_Path` · `M02_Doc_Router`(새 · main 등록)
- B09 구조물 · 서고 사본 = `수식.json` 함께 굳힘(`M02_Doc_B09` · 원본판 안 듦) · B09 셈 그대로(문서 셈 전환 = 802-6)
- 옮기기 `M02_Doc_Migrate`(보기만 · --apply --backup · --restore) · 목록 `docs/raw/요구_1104/_목록_802-4a_M02대조.xlsx` — 층 SK 15 · 일위대가 21 · SC 4 · B09 서고 사본 3 같음 · 빈 SK 2 = 옛 0 → 「—」 · 가져온 식 1 = 802-4b
- 시험 test_802_4a(12) · test_204_2 서고 사본 파일 목록에 수식.json · test_795_1 `_fake` 가 M01 수식 문서도 같이 바꿈(M01 정본 = 수식 문서 — 옛 줄만 바꾼 흉내는 문서 사본에 안 닿음) · 옛 SK 흉내 = 사본 · 수식 둘 다 뗌
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
"""B09 사본(구조물 · 서고)의 수식 문서 사본 `수식.json`(PLAN 802-4a 2 · 브레인 802-4a ①) — 서버 단독.
|
||||
|
||||
사본을 만들 때(`M02_Item_Source.build_source`) 단가 묶음 `단가.json` 옆에 함께 굳힘 —
|
||||
{그룹: 수식문서, 문서: {키: 문서}} = 그 묶음의 로직 줄(담은 로직 · 하위 로직 · 굳힌 단가산출 GX9)이
|
||||
닿는 닫힌 묶음 · 원본 일위대가 문서의 수식 사본 부름이 먼저(만든 때 것) ·
|
||||
없으면 묶음 줄 = 지금 M01 줄이면 M01 문서 · 다르면 그 줄 변환(`M02_Doc_Copy.doc_of`).
|
||||
[단가 갱신](값만) = 이 파일 안 고침 · [재생성](재적용) = 새로 찍음.
|
||||
B09 셈은 지금 풀이(`단가.json` · 단가 책) 그대로 — 산근을 문서 셈으로 바꾸는 것은 802-6.
|
||||
원본판(`version_of`)에는 안 듦(로직 줄은 `단가.json` 이 이미 잼 · 옛 사본이 거짓 「원본 바뀜」 안 뜸).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Doc_Sk as dsk
|
||||
|
||||
FILE = "수식.json"
|
||||
GROUP = "수식문서"
|
||||
|
||||
|
||||
def logic_keys(bundle: dict[str, Any]) -> list[str]:
|
||||
"""단가 묶음의 로직 줄 키(그룹 「로직」 파일)."""
|
||||
return [
|
||||
str(row.get("키"))
|
||||
for data in bundle.values()
|
||||
if isinstance(data, dict) and data.get("그룹") == "로직"
|
||||
for row in data.get("줄") or []
|
||||
if isinstance(row, dict) and row.get("키")
|
||||
]
|
||||
|
||||
|
||||
def build(bundle: dict[str, Any], labor: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""`단가.json` 묶음 → `수식.json` 내용."""
|
||||
from M02_MasterTemplete import M02_Item_Prices as prices
|
||||
|
||||
keys = logic_keys(bundle)
|
||||
if not keys:
|
||||
return {"그룹": GROUP, "문서": {}}
|
||||
doc = (labor or {}).get("줄") or {}
|
||||
have = (doc.get(dc.KEY) or {}).get("부름") if dc.has(doc) else None
|
||||
whole = prices.load_prices(bundle)
|
||||
return dsk.norm({"그룹": GROUP, "문서": dc.closure(whole, keys, have)})
|
||||
|
||||
|
||||
def attach(files: dict[str, Any]) -> dict[str, Any]:
|
||||
"""사본 파일 묶음에 `수식.json` 을 더함(이미 있으면 그대로 · 단가 묶음 없으면 그대로)."""
|
||||
bundle = files.get("단가.json")
|
||||
if FILE in files or not isinstance(bundle, dict):
|
||||
return files
|
||||
return {**files, FILE: build(bundle, files.get("일위대가.json"))}
|
||||
@@ -0,0 +1,279 @@
|
||||
"""M02 수식 문서 사본(PLAN 802-4a · 요구 664 · 665 · 요구서_664 1 장 4) — 서버 단독.
|
||||
|
||||
단가산출 기초 정본 = 수식 문서(`resources/master_data/_수식문서_틀.md`) — M02 단가산출(SK) ·
|
||||
일위대가 · 사전조합(SC) · B09 사본은 만들 때 M01 문서를 찍어 가짐(M01 이 바뀌어도 그대로).
|
||||
문서 칸 `수식`(옛 로직 사본 `사본` 은 802-5 까지 옆에 둠):
|
||||
{판, 시점, 로직 [M01 줄], 단가판, 단가시점, 단가 [M01 자원 줄], ← 값 사본(`M02_Sangeun_Copy` 꼴)
|
||||
부름 {키: 문서}(닫힌 묶음 — 연결 · [재생성] 때 새로) ·
|
||||
SK 만: 문서(이 SK 문서 · 사용자 고침) · 끊음 {키: 문서}(이 사본 안에 굳힌 부름 · 고칠 수 있음) ·
|
||||
따로(문서를 수식 길로 고침 — 옛 칸에서 다시 짓지 않음)}
|
||||
셈 = `master_doc_calc.calc`(M01 과 같은 계산기) · 연결 = `master_doc_link.Links`(값 사본 색인 위) ·
|
||||
[단가 갱신] = 자원 단가 줄만 지금 M01(`skc.refresh`) · 문서 그대로 · [재생성] = 지금 M01 문서로.
|
||||
부름 문서 = 값 사본 줄이 지금 M01 줄과 같으면 M01 문서(`수식문서/`) · 다르면(만든 때 옛 줄) 그 줄을
|
||||
변환(`master_doc_convert` · 만든 때 그대로).
|
||||
줄 규칙(요구 664 · 브레인 802-4a ④) = 일위대가 줄 옵션을 문서 안에 드러냄(`line_doc`) —
|
||||
켜쌓기율 = 변수 `r_켜쌓기`(입력) · 금액 줄 수량 × r ÷ 100 · 고른 자재 = 그 줄 단가 기호 연결
|
||||
요소 키 · 별도계상 = 그 금액 줄 `합밖`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy as cp
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Item_Prices as prices
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
|
||||
_SCRIPTS = str(Path(__file__).resolve().parents[1] / "resources/master_data/scripts")
|
||||
if _SCRIPTS not in sys.path:
|
||||
sys.path.insert(0, _SCRIPTS)
|
||||
|
||||
import master_auto as mau # noqa: E402
|
||||
import master_doc as md # noqa: E402
|
||||
import master_doc_calc as mc # noqa: E402
|
||||
import master_doc_convert as cv # noqa: E402
|
||||
import master_doc_link as ml # noqa: E402
|
||||
|
||||
KEY = "수식"
|
||||
COSTS = ("노무비", "재료비", "경비", "계")
|
||||
RATE = {"열쇠": "켜쌓기율", "기호": "r", "아래": "켜쌓기", "위": "", "이름": "켜쌓기율"}
|
||||
RATE_ID = md.sym_id(RATE["기호"], RATE["아래"])
|
||||
_ERR = (Exception,) # 변환 · 셈 틀림 = 그 문서 못 셈(까닭) — 셈 길은 멈추지 않음
|
||||
|
||||
|
||||
def has(doc: Any) -> bool:
|
||||
got = doc.get(KEY) if isinstance(doc, dict) else None
|
||||
return isinstance(got, dict) and isinstance(got.get("부름"), dict)
|
||||
|
||||
|
||||
def fp(value: Any) -> str:
|
||||
raw = json.dumps(ml._norm(value), ensure_ascii=False, sort_keys=True, default=str)
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
|
||||
# ── M01 문서(정본 폴더) ────────────────────────────────────────────────
|
||||
_FOLDER: dict[str, Any] = {"자국": None, "문서": {}}
|
||||
|
||||
|
||||
def folder() -> dict[str, dict]:
|
||||
"""M01 수식 문서 {키: 문서} — 폴더 파일 자국이 바뀌면 다시 읽음(M01 관리자 고침)."""
|
||||
files = sorted(ml.FOLDER.glob("수식_*.json"))
|
||||
mark = tuple((p.name, p.stat().st_mtime_ns, p.stat().st_size) for p in files)
|
||||
if _FOLDER["자국"] != mark:
|
||||
_FOLDER["문서"], _FOLDER["자국"] = ml.load_all(), mark
|
||||
return _FOLDER["문서"]
|
||||
|
||||
|
||||
def _live(key: str) -> Any:
|
||||
whole = skc.whole_()
|
||||
return whole.index.get(key[:2], {}).get(key)
|
||||
|
||||
|
||||
def doc_of(whole: Any, key: str) -> dict | None:
|
||||
"""키 하나의 문서 — 값 사본 줄 = 지금 M01 줄이면 M01 문서 · 다르면 그 줄 변환 · 없으면 None."""
|
||||
try:
|
||||
row = whole.logic(key)
|
||||
except _ERR:
|
||||
row = None
|
||||
live = folder().get(key)
|
||||
if row is None:
|
||||
return cp.deepcopy(live) if live else None
|
||||
if live and (_live(key) is None or skc._fp(_live(key)) == skc._fp(prices.to_json(row))):
|
||||
return cp.deepcopy(live)
|
||||
try:
|
||||
return cv.convert(whole, row)[0]
|
||||
except _ERR:
|
||||
return cp.deepcopy(live) if live else None
|
||||
|
||||
|
||||
def calls(doc: dict) -> list[str]:
|
||||
"""문서가 부르는 단가산출 키."""
|
||||
return [
|
||||
str(v["연결"]["키"])
|
||||
for v in doc.get("변수") or []
|
||||
if (v.get("연결") or {}).get("갈래") == "단가산출" and (v.get("연결") or {}).get("키")
|
||||
]
|
||||
|
||||
|
||||
def closure(whole: Any, keys: list[str], have: dict | None = None) -> dict[str, dict]:
|
||||
"""키들과 그 문서가 부르는 단가산출 닫힌 묶음 {키: 문서} — `have`(만든 때 것) 먼저."""
|
||||
have, out, todo = have or {}, {}, list(keys)
|
||||
while todo:
|
||||
key = str(todo.pop())
|
||||
if key in out:
|
||||
continue
|
||||
doc = have.get(key) or doc_of(whole, key)
|
||||
if doc is None:
|
||||
continue
|
||||
out[key] = doc
|
||||
todo += [k for k in calls(doc) if k not in out]
|
||||
return out
|
||||
|
||||
|
||||
# ── 값 사본 색인 · 연결 ────────────────────────────────────────────────
|
||||
def values_of(copy: dict) -> dict:
|
||||
"""값 사본 몫만(`skc` 꼴) — 셈 색인 · [단가 갱신] 에 씀."""
|
||||
return {k: copy[k] for k in ("판", "시점", "로직", "단가판", "단가시점", "단가") if k in copy}
|
||||
|
||||
|
||||
def base(whole: Any) -> Any:
|
||||
while isinstance(whole, skc.Copy):
|
||||
whole = whole._whole
|
||||
return whole
|
||||
|
||||
|
||||
def over(copy: dict, whole: Any = None) -> Any:
|
||||
"""값 사본 색인 — M01 색인 위 사본 줄(`skc.over` 같은 메모 · 같은 줄 = 같은 색인)."""
|
||||
return skc.over({skc.KEY: values_of(copy)}, base(whole or skc.whole_()))
|
||||
|
||||
|
||||
def docs_of(copy: dict) -> dict[str, dict]:
|
||||
return {**(copy.get("부름") or {}), **(copy.get("끊음") or {})}
|
||||
|
||||
|
||||
def run(doc: dict, copy: dict, given: dict | None = None, whole: Any = None) -> dict:
|
||||
"""문서 셈 — 연결 = 값 사본 · 부름 = 사본 문서(끊음 먼저)."""
|
||||
try:
|
||||
return mc.calc(doc, ml.Links(whole or over(copy), docs_of(copy)), given)
|
||||
except _ERR as e: # 계산기는 예외 없음이 원칙 — 남은 틈도 금액 「—」 로
|
||||
return {"셈됨": False, "표지": [{"갈래": "셈 틀림", "글": str(e)}], **dict.fromkeys(COSTS)}
|
||||
|
||||
|
||||
def money(ans: dict) -> dict | None:
|
||||
if not ans.get("셈됨") or "결과" in ans:
|
||||
return None
|
||||
return {k: ans.get(k) for k in COSTS}
|
||||
|
||||
|
||||
def why(ans: dict) -> str:
|
||||
marks = " · ".join(f"{m.get('갈래')} — {m.get('글')}" for m in ans.get("표지") or [])
|
||||
return marks or str(ans.get("까닭") or "") or " · ".join((ans.get("빈값") or {}).values())
|
||||
|
||||
|
||||
# ── 줄 규칙을 문서에(켜쌓기율 · 고른 자재 · 별도계상) ────────────────────
|
||||
def _qty_scaled(line: dict) -> bool:
|
||||
return line.get("수량") is not None or str(line.get("단위") or "") != "식"
|
||||
|
||||
|
||||
def line_doc(doc: dict, rate: Any = None, picks: dict | None = None, apart=()) -> dict:
|
||||
"""일위대가 줄 옵션을 문서 규칙으로 — 새 문서(원본 안 고침).
|
||||
`picks` = {호표 차례: MT 키} · `apart` = 별도계상 금액 줄 차례."""
|
||||
if rate in (None, 100) and not picks and not apart:
|
||||
return doc
|
||||
doc = cp.deepcopy(doc)
|
||||
lines, by_id = doc.get("금액") or [], {md.var_id(v): v for v in doc.get("변수") or []}
|
||||
if rate not in (None, 100):
|
||||
doc["변수"].append({**RATE, "단위": "%", "연결": {"갈래": "입력", "값": rate}})
|
||||
for line in lines:
|
||||
if _qty_scaled(line):
|
||||
line["수량"] = f"({line.get('수량') or 1}) × {RATE_ID} ÷ 100"
|
||||
for at, key in (picks or {}).items():
|
||||
n = int(at) if str(at).isdigit() else -1
|
||||
var = by_id.get(str(lines[n].get("단가"))) if 0 <= n < len(lines) else None
|
||||
link = (var or {}).get("연결") or {}
|
||||
if link.get("갈래") == "요소":
|
||||
keep = {k: link[k] for k in ("단위", "지역", "계약종별") if link.get(k)}
|
||||
var["연결"] = {"갈래": "요소", "키": str(key), **keep}
|
||||
for n in apart:
|
||||
if 0 <= n < len(lines):
|
||||
_apart(doc, lines, n)
|
||||
return doc
|
||||
|
||||
|
||||
def _plus(node, cost: str, sym: str):
|
||||
"""식 나무 안 `cost`(앞 줄 누계) → (cost + sym)."""
|
||||
kind = node[0]
|
||||
if kind == "var":
|
||||
return ("bin", "+", node, ("var", sym)) if node[1] == cost else node
|
||||
if kind == "bin":
|
||||
return ("bin", node[1], _plus(node[2], cost, sym), _plus(node[3], cost, sym))
|
||||
if kind in ("neg", "pct"):
|
||||
return (kind, _plus(node[1], cost, sym))
|
||||
if kind == "field":
|
||||
return ("field", _plus(node[1], cost, sym), node[2])
|
||||
if kind == "call":
|
||||
return ("call", node[1], [_plus(x, cost, sym) for x in node[2]])
|
||||
return node
|
||||
|
||||
|
||||
def _apart(doc: dict, lines: list[dict], n: int) -> None:
|
||||
"""별도계상 = 그 줄 `합밖` · 뒤 줄이 그 비목 누계를 쓰면 그 줄 금액을 더해 씀
|
||||
(옛 엔진 = 뺀 재료 몫도 「재료비의 %」 밑에 듦 — `M02_Labor_Doc.drop_apart`)."""
|
||||
line, cost = lines[n], str(lines[n].get("비목") or "")
|
||||
line["합밖"] = True
|
||||
if cost not in md.COSTS:
|
||||
return
|
||||
taken = {md.var_id(v) for v in doc.get("변수") or []} | {str(x.get("기호")) for x in lines}
|
||||
sym = str(line.get("기호") or "")
|
||||
if not sym:
|
||||
sym = next(
|
||||
s for k in range(n + 1, n + 99) if (s := md.sym_id("S", f"별도{k}")) not in taken
|
||||
)
|
||||
line["기호"] = sym
|
||||
for later in lines[n + 1 :]:
|
||||
for slot in ("수량", "단가"):
|
||||
try:
|
||||
node = md.parse(later.get(slot))
|
||||
except md.DocError:
|
||||
continue
|
||||
if later.get(slot) not in (None, "") and cost in md.names(node):
|
||||
later[slot] = md.text_of(_plus(node, cost, sym))
|
||||
|
||||
|
||||
def given_of(whole: Any, code: str, given: Any) -> dict:
|
||||
"""옛 `run_line` 과 같은 입력 — 빈 입력 = 자동값(M01 줄) › 받은 값."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
try:
|
||||
auto = {k: v for k, v in mau.values(whole, whole.logic(code)).items() if v is not None}
|
||||
except _ERR:
|
||||
auto = {}
|
||||
return {**auto, **m01._dec(given or {})}
|
||||
|
||||
|
||||
def apart_of(marks: Any, mats: Any) -> list[int]:
|
||||
"""별도계상 금액 줄 차례 — 옛 `drop_apart` 와 같은 판정(엔진 답 `재료` 의 차례 · 자재표시)."""
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
marks = marks if isinstance(marks, dict) else {}
|
||||
return [
|
||||
n
|
||||
for m in mats or []
|
||||
if isinstance(n := m.get("차례"), int) and labor.apart(marks.get(str(n)))
|
||||
]
|
||||
|
||||
|
||||
# ── 찍기 · 갱신 ────────────────────────────────────────────────────────
|
||||
def stamp(values: dict | None, keys: list[str], have: dict | None = None) -> dict | None:
|
||||
"""값 사본(`skc.snap` 꼴) + 부름 닫힌 묶음 → `수식` 칸 · 값 사본 없음 = None."""
|
||||
if not values:
|
||||
return None
|
||||
whole = over({**values, "부름": {}})
|
||||
return {**values_of(values), "부름": closure(whole, keys, have)}
|
||||
|
||||
|
||||
def grow(copy: dict, values: dict | None, keys: list[str]) -> dict:
|
||||
"""저장 때 — 새 키(더한 기초 · 줄)만 지금 M01 에서 더함 · 있는 부름 · 값 줄 그대로."""
|
||||
rows = skc.grow(values_of(copy), values)
|
||||
whole = over({**rows, "부름": {}})
|
||||
return {**copy, **rows, "부름": closure(whole, keys, copy.get("부름"))}
|
||||
|
||||
|
||||
def refresh(copy: dict) -> tuple[dict, list[dict]]:
|
||||
"""[단가 갱신] — 자원 단가 줄만 지금 M01 · 문서 그대로."""
|
||||
rows, changed = skc.refresh(values_of(copy), skc.whole_())
|
||||
return {**copy, **rows}, changed
|
||||
|
||||
|
||||
def state(copy: dict) -> dict[str, Any]:
|
||||
"""{M01새판, 지운키, 단가새값, 문서새판} — 값 줄 · 부름 문서를 지금 M01 과 대조."""
|
||||
got = skc.state(values_of(copy), skc.whole_())
|
||||
live, base = folder(), copy.get("원본") or {}
|
||||
new = {k for k, d in (copy.get("부름") or {}).items() if k in live and fp(d) != fp(live[k])}
|
||||
if base.get("키") in live and fp(live[base["키"]]) != base.get("판"):
|
||||
new.add(base["키"]) # 이 SK 문서를 찍은 M01 문서가 바뀜
|
||||
return {**got, "문서새판": sorted(new)}
|
||||
@@ -0,0 +1,233 @@
|
||||
"""옛 SK · SC · 일위대가 · B09 사본 → 수식 문서 사본 찍어 넣기 — 한 번 돌리는 도구(PLAN 802-4a 3).
|
||||
|
||||
대상 = 마스터 · 회사 · 개인 층 문서(`sangeun` · `precombo` · `labor`) · B09 사본(구조물 · 서고).
|
||||
층 문서 — 전 = 옛 셈 길(`M02_Doc_Sk.OLD` · 로직 사본 엔진) · 후 = 수식 사본 문서 셈:
|
||||
SK = 기준(노 · 재 · 경 · 계) · SC · 일위대가 = `Labor_Doc.calc` 기준합 · 반영합(원 단위 · 반올림 6 자리).
|
||||
같음 = 파일에 `수식` 칸만 더해 씀(그 밖 칸 그대로) · 다름 · 못 셈 = 안 씀(목록 · 까닭) ·
|
||||
가져온 식 SK = 802-4b(그대로) · 바탕 참조 SK · 로직 사본 없는 문서 = 필요없음 · 이미 있음 = 있음.
|
||||
B09 사본 — `수식.json`(`M02_Doc_B09.build` · 만든 때 `단가.json` 줄 기준) 더함 · 견적 셈은 그대로
|
||||
(`prices.calc` 전후 같음) · 문서 대조 = 일위대가 로직 줄마다 옛 엔진(`run_line`) 대 문서 셈
|
||||
(M01 규칙 · 같은 사본 값) — 다르면 목록.
|
||||
되돌릴 길 = 쓰기 전 파일 그대로 `--backup` 폴더(층 자리 그대로) · `--restore` = 다시 덮음
|
||||
(B09 = 더한 `수식.json` 을 지움).
|
||||
|
||||
./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Doc_Migrate # 보기만(표 · 목록)
|
||||
./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Doc_Migrate --apply --backup tmp/802_4a
|
||||
./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Doc_Migrate --restore tmp/802_4a
|
||||
--list <xlsx> 목록 파일(기본 `docs/raw/요구_1104/_목록_802-4a_M02대조.xlsx`)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from M02_MasterTemplete import M02_Copy_Migrate as old
|
||||
from M02_MasterTemplete import M02_Doc_B09 as doc_b09
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Doc_Sk as dsk
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
|
||||
LIST = Path(__file__).resolve().parents[1] / "docs/raw/요구_1104/_목록_802-4a_M02대조.xlsx"
|
||||
HEAD = ("어디", "종류", "키", "이름", "상태", "전", "후", "까닭")
|
||||
|
||||
|
||||
def _money(kind: str, doc: dict) -> Any:
|
||||
from M02_MasterTemplete import M02_Labor_Copy as lc
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
if kind == "sangeun":
|
||||
got = calc.calc_doc(doc, None, False)
|
||||
return old._round(got.get("기준")), got.get("수식셈")
|
||||
lc._LAST[0] = (None, None)
|
||||
got = labor.calc(lc.settle(doc), None if kind == "labor" else {})
|
||||
return old._round({k: got.get(k) for k in ("기준합", "반영합")}), None
|
||||
|
||||
|
||||
def _with_old(fn, *args):
|
||||
token = dsk.OLD.set(True)
|
||||
try:
|
||||
return fn(*args)
|
||||
finally:
|
||||
dsk.OLD.reset(token)
|
||||
|
||||
|
||||
def formula_of(kind: str, doc: dict) -> dict | None:
|
||||
"""그 문서에 찍을 수식 사본(저장 첫머리와 같은 길 · 저장 없음)."""
|
||||
from M02_MasterTemplete import M02_Labor_Copy as lc
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
|
||||
if kind != "sangeun":
|
||||
return lc.settle(doc).get(dc.KEY)
|
||||
if not dsk.own(doc):
|
||||
return None
|
||||
clean, whole = None, None
|
||||
settled = skc.settle(doc)
|
||||
whole = skc.over(settled, skc.whole_())
|
||||
clean = sd.clean(settled, None, whole)
|
||||
return dsk.settle(clean, whole, None, doc).get(dc.KEY)
|
||||
|
||||
|
||||
def check(kind: str, path: Path) -> dict[str, Any]:
|
||||
"""층 문서 하나 — {상태: 있음 | 필요없음 | 가져온식 | 같음 | 다름 | 못셈, 전, 후, 수식?, 까닭?}."""
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
doc = json.loads(path.read_bytes())
|
||||
head = {"이름": doc.get("이름") or ""}
|
||||
if dc.has(doc):
|
||||
return {**head, "상태": "있음"}
|
||||
if kind == "sangeun" and doc.get("가져온"):
|
||||
return {**head, "상태": "가져온식", "까닭": "엑셀 가져온 식 = 802-4b 몫(옛 길 그대로)"}
|
||||
try:
|
||||
with store.at(path.parent.parent):
|
||||
copy = formula_of(kind, doc)
|
||||
if copy is None:
|
||||
return {**head, "상태": "필요없음", "까닭": "로직 사본 없음(바탕 참조 · 손 줄만)"}
|
||||
before = _with_old(_money, kind, doc)[0]
|
||||
after, mark = _money(kind, {**doc, dc.KEY: copy})
|
||||
except Exception as e: # noqa: BLE001 — 못 셈 = 목록(통째로 멈추지 않음)
|
||||
return {**head, "상태": "못셈", "까닭": str(e) or type(e).__name__}
|
||||
row = {**head, "전": before, "후": after}
|
||||
if before == after:
|
||||
return {**row, "상태": "같음", "수식": copy}
|
||||
why = (mark or {}).get("까닭") or "전후 금액 다름 — 안 옮김"
|
||||
if kind == "sangeun" and not (doc.get("바탕") or doc.get("더한줄")):
|
||||
why = "빈 SK(바탕 · 줄 없음) — 옛 0 · 새 빈 문서 「—」(요구서_664 0 장 · 브레인 802-4a ③)"
|
||||
return {**row, "상태": "다름(빈 문서)", "까닭": why, "수식": copy}
|
||||
return {**row, "상태": "다름", "까닭": why}
|
||||
|
||||
|
||||
def b09_check(folder: Path) -> dict[str, Any]:
|
||||
"""B09 사본 하나 — {상태: 있음 | 필요없음 | 같음 | 다름 | 못셈, 전, 후, 수식?, 까닭?}."""
|
||||
from M02_MasterTemplete import M02_Item_Prices as prices
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete.M02_Item_Source import read_json
|
||||
|
||||
if (folder / doc_b09.FILE).is_file():
|
||||
return {"상태": "있음"}
|
||||
own = {p.name: read_json(p) for p in folder.glob("*.json")}
|
||||
bundle = own.get("단가.json")
|
||||
if not isinstance(bundle, dict) or not doc_b09.logic_keys(bundle):
|
||||
return {"상태": "필요없음", "까닭": "로직 줄 없음"}
|
||||
try:
|
||||
before = old._copy_money(own)
|
||||
made = doc_b09.build(bundle, own.get("일위대가.json"))
|
||||
after = old._copy_money({**own, doc_b09.FILE: made})
|
||||
whole, copy = prices.load_prices(bundle), {"부름": made["문서"]}
|
||||
lines = ((own.get("일위대가.json") or {}).get("줄") or {}).get("담은로직") or []
|
||||
bad = []
|
||||
for one in [x for x in lines if isinstance(x, dict) and x.get("로직")]:
|
||||
code = str(one["로직"])
|
||||
was = labor.run_line(
|
||||
whole, code, one.get("입력값"), one.get("켜쌓기율"), one.get("고름")
|
||||
)
|
||||
if was.get("기준") is None or code not in made["문서"]:
|
||||
continue
|
||||
doc = dc.line_doc(made["문서"][code], one.get("켜쌓기율"), one.get("고름"))
|
||||
got = dc.money(dc.run(doc, copy, dc.given_of(whole, code, one.get("입력값")), whole))
|
||||
if old._round(got) != old._round(was["기준"]):
|
||||
bad.append(f"{code} 옛 {old._round(was['기준'])} · 문서 {old._round(got)}")
|
||||
except Exception as e: # noqa: BLE001
|
||||
return {"상태": "못셈", "까닭": str(e) or type(e).__name__}
|
||||
row = {"전": before, "후": after, "수식": made}
|
||||
if before != after:
|
||||
return {**row, "상태": "다름", "까닭": "견적 셈이 바뀜 — 안 옮김"}
|
||||
if bad:
|
||||
return {**row, "상태": "다름", "까닭": "문서 대조 다름 — " + " / ".join(bad[:5])}
|
||||
return {**row, "상태": "같음", "까닭": f"문서 {len(made['문서'])} · 견적 셈 그대로"}
|
||||
|
||||
|
||||
def migrate(apply: bool = False, backup: Path | None = None) -> list[dict[str, Any]]:
|
||||
"""모든 층 · B09 사본 — {어디, 종류, 키, 상태, 씀, …} · `apply` = 같음(빈 문서 포함)만 씀."""
|
||||
if apply and backup is None:
|
||||
raise ValueError("--apply 는 --backup 폴더가 있어야 함")
|
||||
out = []
|
||||
for where_, kind, path, project in old._files():
|
||||
if project:
|
||||
continue # 프로젝트 층 일위대가 = B09 몫(옛 길 · 795-4)
|
||||
got = check(kind, path)
|
||||
copy = got.pop("수식", None)
|
||||
row = {"어디": where_, "종류": kind, "키": path.stem, **got, "씀": False}
|
||||
if apply and copy is not None:
|
||||
keep = old._backup_path(path, backup)
|
||||
keep.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(path, keep)
|
||||
atomic_write_json(path, {**json.loads(path.read_bytes()), dc.KEY: copy})
|
||||
row["씀"] = True
|
||||
out.append(row)
|
||||
for where_, kind, folder in old._b09_files():
|
||||
got = b09_check(folder)
|
||||
made = got.pop("수식", None)
|
||||
row = {"어디": where_, "종류": kind, "키": folder.name, **got, "씀": False}
|
||||
if apply and made is not None and got["상태"] == "같음":
|
||||
note = old._backup_path(folder / doc_b09.FILE, backup).with_suffix(".added")
|
||||
note.parent.mkdir(parents=True, exist_ok=True)
|
||||
note.write_text("802-4a 이 더함 — --restore 가 지움", encoding="utf-8")
|
||||
atomic_write_json(folder / doc_b09.FILE, made)
|
||||
row["씀"] = True
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
def restore(backup: Path) -> list[str]:
|
||||
"""`--backup` 폴더 파일을 제자리로 · 더한 `수식.json` 지움 — 손댄 파일 목록."""
|
||||
done = old.restore(backup)
|
||||
for name, anchor in old._anchors().items():
|
||||
for note in sorted((backup / name).rglob("*.added")):
|
||||
target = (anchor / note.relative_to(backup / name)).with_suffix(".json")
|
||||
target.unlink(missing_ok=True)
|
||||
done.append(f"지움 {target}")
|
||||
return done
|
||||
|
||||
|
||||
def table(rows: list[dict[str, Any]]) -> dict[str, dict[str, int]]:
|
||||
count: Counter = Counter((r["어디"].split(" ")[0], r["종류"], r["상태"]) for r in rows)
|
||||
out: dict[str, dict[str, int]] = {}
|
||||
for (layer, kind, state), n in sorted(count.items()):
|
||||
out.setdefault(f"{layer} {kind}", {})[state] = n
|
||||
return out
|
||||
|
||||
|
||||
def write_list(rows: list[dict[str, Any]], path: Path = LIST) -> Path:
|
||||
from openpyxl import Workbook
|
||||
|
||||
book = Workbook()
|
||||
sheet = book.active
|
||||
sheet.title = "요약"
|
||||
sheet.append(("층 · 종류", "상태", "개수"))
|
||||
for group, states in table(rows).items():
|
||||
for state, n in states.items():
|
||||
sheet.append((group, state, n))
|
||||
rows_sheet = book.create_sheet("목록")
|
||||
rows_sheet.append(HEAD)
|
||||
for r in rows:
|
||||
cells = [r.get(k) for k in HEAD]
|
||||
plain = (str, int, float, type(None))
|
||||
rows_sheet.append(
|
||||
[c if isinstance(c, plain) else json.dumps(c, ensure_ascii=False) for c in cells]
|
||||
)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
book.save(path)
|
||||
return path
|
||||
|
||||
|
||||
if __name__ == "__main__": # 브레인 · 사용자 승인 뒤만 --apply
|
||||
args = sys.argv[1:]
|
||||
if "--restore" in args:
|
||||
print("\n".join(restore(Path(args[args.index("--restore") + 1]))))
|
||||
raise SystemExit
|
||||
at = Path(args[args.index("--backup") + 1]) if "--backup" in args else None
|
||||
rows = migrate("--apply" in args, at)
|
||||
for one in rows:
|
||||
if one["상태"] not in ("있음", "필요없음", "같음"):
|
||||
print(json.dumps(one, ensure_ascii=False, default=str))
|
||||
print(json.dumps(table(rows), ensure_ascii=False, indent=1))
|
||||
if "--list" in args or "--apply" not in args:
|
||||
to = Path(args[args.index("--list") + 1]) if "--list" in args else LIST
|
||||
print("목록", write_list(rows, to))
|
||||
@@ -0,0 +1,59 @@
|
||||
"""M02 단가산출(SK) 수식 길(PLAN 802-4a 4) — `M02_Doc_Sk_Path` 를 층 권한 안에서.
|
||||
|
||||
GET /api/m02/layers/{층}/sangeun/{키}/doc 읽기(층 읽기) → {문서, 부름, 끊음, 셈, 사본, 판}
|
||||
POST /api/m02/layers/{층}/sangeun/{키}/doc/preview 미리 셈(층 쓰기) {문서?, 끊음?, 할일?}
|
||||
→ {…, 전, 후, 바뀐자원?, 버림?}
|
||||
POST /api/m02/layers/{층}/sangeun/{키}/doc/save 저장(층 쓰기) {문서?, 끊음?, 할일?, 판} → 읽기 꼴
|
||||
⚠ 등록은 `main.py`(로그인만 · 층 권한은 길 안에서).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete import M02_Doc_Sk_Path as path
|
||||
from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root
|
||||
|
||||
router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete Sangeun Doc"])
|
||||
|
||||
|
||||
class DocBody(BaseModel):
|
||||
문서: dict[str, Any] | None = None
|
||||
끊음: dict[str, Any] | None = None
|
||||
할일: str | None = None
|
||||
판: str | None = None
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/sangeun/{key}/doc")
|
||||
def read_doc(
|
||||
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
return _call(_root(session, layer, owner), path.read, key)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/sangeun/{key}/doc/preview")
|
||||
def preview_doc(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
body: DocBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, path.preview, key, body.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/sangeun/{key}/doc/save")
|
||||
def save_doc(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
body: DocBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, path.save, key, body.model_dump(exclude_none=True))
|
||||
@@ -0,0 +1,187 @@
|
||||
"""M02 단가산출(SK) 수식 문서 사본(PLAN 802-4a · 요구 664 · 665) — 서버 단독.
|
||||
|
||||
SK `수식`(꼴 = `M02_Doc_Copy`) — `문서` = 이 SK 문서(키 = SK 키 · 옵션 값 · 줄 규칙까지 든 정본) ·
|
||||
`부름` = 부르는 단가산출 닫힌 묶음 · `끊음` = 이 사본에 굳힌 부름(고칠 수 있음) · 값 사본.
|
||||
두 갈래:
|
||||
파생(따로 없음) — 옛 칸(바탕 · 덮개 · 뺀줄 · 더한줄 · 수식고침 …)이 정함: 셈마다 옛 칸이 낸 줄
|
||||
(`M02_Sangeun_Calc.run` 이 엔진에 넣는 고친 줄)을 문서로 옮겨(덮개 없는 바탕 = M01 문서 사본 ·
|
||||
고친 줄 = `master_doc_convert`) 문서 셈 · 지금 화면(옛 칸 고침)이 그대로 돎.
|
||||
따로 — 수식 길(`M02_Doc_Router`)로 문서를 고침: 저장한 문서가 정본 · 옛 칸 고침 저장 = 409 ·
|
||||
[재생성] = 지금 M01 로 다시 찍어 파생으로 돌아감(고친 문서 버림).
|
||||
SK 금액(기준 · 1 단위 = 기준 ÷ 배) = 문서 셈 — 일위대가 · 사전조합 · 구조물 조합이 이 값을 씀.
|
||||
가져온 식 SK(엑셀 · 바탕 null) = 옛 길 그대로(802-4b) · 바탕 참조 SK = 옛 길(값 사본 없음).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy as cp
|
||||
import json
|
||||
from contextvars import ContextVar
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
|
||||
KEY = dc.KEY
|
||||
OLD_FIELDS = (
|
||||
"바탕", "입력값", "열린입력", "덮개", "뺀줄", "더한줄", "뺀더한줄", "상수덮개", "표값덮개",
|
||||
"수식고침", "고름", "자재표시", "켜쌓기율",
|
||||
) # fmt: skip
|
||||
_STAMPED = ("원값", "바탕판", "처음수량", "M01판", "끊음") # 서버가 적는 칸 — 옛 칸 대조에서 뺌
|
||||
OLD: ContextVar[bool] = ContextVar("옛길", default=False) # 옛 셈 길(옮기기 전 금액 대조)
|
||||
SAVING: ContextVar[dict | None] = ContextVar("수식길저장", default=None) # {키, 사본} — 수식 길만
|
||||
|
||||
|
||||
def own(doc: Any) -> bool:
|
||||
"""수식 사본을 가질 SK — 가져온 식 · 바탕 참조 SK 빼고."""
|
||||
if not isinstance(doc, dict) or doc.get("가져온"):
|
||||
return False
|
||||
return "참조" not in (doc.get("바탕") or {})
|
||||
|
||||
|
||||
def _strip(v: Any) -> Any:
|
||||
if isinstance(v, dict):
|
||||
return {k: _strip(x) for k, x in v.items() if k not in _STAMPED}
|
||||
if isinstance(v, list):
|
||||
return [_strip(x) for x in v]
|
||||
return v
|
||||
|
||||
|
||||
def old_fp(doc: dict) -> str:
|
||||
"""옛 칸 지문 — 따로 문서 위 옛 칸 고침 막음."""
|
||||
return dc.fp(_strip({k: doc.get(k) for k in OLD_FIELDS if doc.get(k) not in (None, {}, [])}))
|
||||
|
||||
|
||||
# ── 파생 — 엔진에 넣는 고친 줄 → 문서 ─────────────────────────────────
|
||||
def _set_values(doc: dict, given: dict) -> None:
|
||||
for var in doc.get("변수") or []:
|
||||
link = var.get("연결") or {}
|
||||
if link.get("갈래") in ("선택", "입력") and var.get("열쇠") in given:
|
||||
link["값"] = given[var["열쇠"]]
|
||||
|
||||
|
||||
def derive(sk: dict, ctx: tuple) -> dict:
|
||||
"""옛 칸이 낸 고친 줄(`ctx` = run 이 엔진에 넣은 (색인, 키, 입력, 바탕 답)) → 이 SK 문서.
|
||||
덮개 없는 바탕(고친 줄 = 바탕 줄) = 그 M01 문서 사본 · 아니면 고친 줄 변환."""
|
||||
whole, key, values, got, mats = ctx
|
||||
row = whole.logic(key)
|
||||
given = dc.given_of(whole, key, values)
|
||||
base_key = str((sk.get("바탕") or {}).get("로직") or "")
|
||||
plain = None
|
||||
if base_key:
|
||||
try:
|
||||
frozen = whole.logic(base_key)
|
||||
same = {**row, "키": base_key} == frozen
|
||||
except dc._ERR:
|
||||
same = False
|
||||
plain = dc.doc_of(whole, base_key) if same else None
|
||||
if plain is not None:
|
||||
doc = cp.deepcopy(plain)
|
||||
_set_values(doc, given)
|
||||
elif not (row.get("호표") or row.get("덧줄") or row.get("결과")):
|
||||
doc = dc.md.blank(str(sk.get("키") or ""), str(sk.get("이름") or ""))
|
||||
else:
|
||||
doc = dc.cv.convert(whole, row, given)[0]
|
||||
apart = dc.apart_of(got.get("자재표시"), mats)
|
||||
doc = dc.line_doc(doc, got.get("켜쌓기율"), got.get("고름") or None, apart)
|
||||
return {**doc, "키": str(sk.get("키") or key), "이름": str(sk.get("이름") or doc.get("이름"))}
|
||||
|
||||
|
||||
def seed(sk: dict) -> dict | None:
|
||||
"""수식 사본 없는 SK(옮기기 전 · 새로)의 씨 — 로직 사본 값 + 빈 부름 · 빈 SK = 빈 문서."""
|
||||
if skc.has(sk):
|
||||
return {**dc.values_of(sk[skc.KEY]), "부름": {}}
|
||||
if sk.get("바탕") or sk.get("더한줄"):
|
||||
return None
|
||||
empty = {"판": skc._fp([]), "로직": [], "단가판": skc._fp([]), "단가": [], "부름": {}}
|
||||
return {**empty, "문서": dc.md.blank(str(sk.get("키") or ""), str(sk.get("이름") or ""))}
|
||||
|
||||
|
||||
def copy_of(sk: dict, ctx: tuple | None) -> dict | None:
|
||||
"""셈에 쓸 수식 사본 — 따로 = 저장본 · 파생 = 지금 옛 칸으로 다시 지은 문서(부름은 있던 것 먼저) ·
|
||||
`OLD` 켬(옮기기 대조 · 시험) = 옛 길."""
|
||||
if OLD.get() or not own(sk):
|
||||
return None
|
||||
got = sk.get(KEY) if dc.has(sk) else seed(sk)
|
||||
if got is None:
|
||||
return None
|
||||
if got.get("따로") or ctx is None:
|
||||
return got
|
||||
try:
|
||||
root = derive(sk, ctx)
|
||||
except dc._ERR as e:
|
||||
root = {**dc.md.blank(str(sk.get("키"))), "_틀림": str(e)}
|
||||
values = dc.values_of(sk[skc.KEY]) if skc.has(sk) else dc.values_of(got) # 서버가 정한 값
|
||||
whole = dc.over(values)
|
||||
calls = dc.closure(whole, dc.calls(root), got.get("부름"))
|
||||
base = str((sk.get("바탕") or {}).get("로직") or "")
|
||||
seen = dc.doc_of(whole, base) if base else None # 바탕 M01 문서 판(「M01 새 판」 표지)
|
||||
origin = {"원본": {"키": base, "판": dc.fp(seen)}} if seen else {}
|
||||
return {**got, **values, "문서": root, "부름": calls, **origin}
|
||||
|
||||
|
||||
def on_run(sk: dict, ans: dict, ctx: tuple | None) -> dict:
|
||||
"""`M02_Sangeun_Calc.run` 끝 — 수식 사본 있는 SK 금액 = 문서 셈(기준 · 1 단위) + 덧칸 `수식셈`."""
|
||||
copy = copy_of(sk, ctx)
|
||||
if copy is None:
|
||||
return ans
|
||||
got = dc.run(copy["문서"], copy)
|
||||
money = dc.money(got)
|
||||
base = {k: Decimal(str(v)) for k, v in money.items()} if money else None
|
||||
times = Decimal(str(ans.get("배") or 1))
|
||||
unit = {k: v / times for k, v in base.items()} if base else None
|
||||
mark = {"셈됨": bool(base), "따로": bool(copy.get("따로"))}
|
||||
if not base:
|
||||
mark["까닭"] = copy["문서"].get("_틀림") or dc.why(got) or "돈 아닌 문서"
|
||||
return {**ans, "기준": base, "1단위": unit, "수식셈": mark}
|
||||
|
||||
|
||||
# ── 저장 첫머리(옛 길 `clean_doc` 끝) ──────────────────────────────────
|
||||
def stored(key: Any) -> dict | None:
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
if not sd.KEY.match(str(key or "")):
|
||||
return None
|
||||
try:
|
||||
return json.loads((store.root() / sd.KIND / f"{key}.json").read_bytes())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def norm(copy: dict) -> dict:
|
||||
return json.loads(json.dumps(dc.ml._norm(copy), ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
def settle(out: dict, whole: Any, job: Any, raw: Any) -> dict:
|
||||
"""저장 꼴에 `수식` 을 정함 — 수식 길 저장(`NEW`) = 그 사본 · 따로 = 저장본(옛 칸 고침 409 ·
|
||||
단가갱신 = 값만 · 재생성 = 파생으로) · 그 밖 = 옛 칸으로 다시 지음(부름 = 있던 것 · 재생성 = 새로)."""
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
out = {k: v for k, v in out.items() if k != KEY}
|
||||
if not own(out):
|
||||
return out
|
||||
if not skc.has(out): # 빈 SK(바탕 · 줄 없음) = 빈 문서 — 금액 「—」(요구서_664 0 장)
|
||||
empty = seed(out)
|
||||
return {**out, KEY: norm(empty)} if empty else out
|
||||
raw = raw if isinstance(raw, dict) else {}
|
||||
mine = SAVING.get()
|
||||
if mine is not None and mine.get("키") == out.get("키"): # 수식 길 저장 — 서버가 지은 사본
|
||||
return {**out, KEY: norm(mine["사본"])}
|
||||
file = stored(out.get("키")) or {}
|
||||
have = file.get(KEY) if dc.has(file) else (raw.get(KEY) if dc.has(raw) else None)
|
||||
if have and have.get("따로") and job != skc.REGEN:
|
||||
if file and old_fp(file) != old_fp(out):
|
||||
raise sd.DocError(
|
||||
"수식 문서로 고친 단가산출 — 옛 칸(덮개 · 줄) 고침은 수식 탭에서 · 또는 [재생성]",
|
||||
409,
|
||||
)
|
||||
copy = dc.refresh(have)[0] if job == skc.PRICE else have
|
||||
return {**out, KEY: norm(copy)}
|
||||
box: list = []
|
||||
calc.run(out, whole, catch=box)
|
||||
calls = {} if job == skc.REGEN else (have or {}).get("부름") or {}
|
||||
first = {**dc.values_of(out[skc.KEY]), "부름": calls}
|
||||
copy = copy_of({**out, KEY: first}, box[-1] if box else None)
|
||||
return {**out, KEY: norm({k: v for k, v in (copy or first).items() if k != "따로"})}
|
||||
@@ -0,0 +1,168 @@
|
||||
"""M02 단가산출(SK) 수식 길 — 읽기 · 미리 셈 · 저장(PLAN 802-4a 4 · 요구 664) — 서버 단독.
|
||||
|
||||
M01 수식 문서 길과 같은 꼴: 읽기 {문서, 셈} · 미리 셈 {문서} → {셈} · 저장 {문서} → {문서, 셈}.
|
||||
몸 칸 — `문서`(이 SK 수식 문서 · 키 = SK 키) · `끊음` {키: 문서}(이 사본에 굳힌 부름 · 고칠 수 있음 ·
|
||||
없는 키 = 다시 이음) · `할일` 단가갱신 | 재생성 · `판`(저장 · 그 SK 파일 판).
|
||||
셈 = 서버 정본(`M02_Doc_Copy.run`) — 화면이 보낸 값 · 사본은 안 받음(문서 · 끊음 글만).
|
||||
저장 = 그 SK 저장(`store.write` — 층 쓰기 권한 · 판 409 · 옛 칸 · 로직 사본 그대로) ·
|
||||
문서 고침 = 따로(이 문서가 정본) · 재생성 = 지금 M01 로 다시 찍어 파생으로(고친 문서 · 끊음 버림) ·
|
||||
단가갱신 = 값 줄만 지금 M01(문서 그대로).
|
||||
가져온 식 SK(엑셀) = 802-4b 몫(400) · 바탕 참조 SK = 값 사본 없음(400).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Doc_Sk as dsk
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
|
||||
MAX = 2_000_000 # 문서 글 크기(바이트) 상한
|
||||
JOBS = (skc.PRICE, skc.REGEN)
|
||||
|
||||
|
||||
def _bad(why: str, status: int = 400) -> store.StoreError:
|
||||
return store.StoreError(status, why)
|
||||
|
||||
|
||||
def _file(key: str) -> dict[str, Any]:
|
||||
if not sd.KEY.match(str(key or "")):
|
||||
raise _bad("키(SK-######)")
|
||||
return store.read(sd.KIND, key)
|
||||
|
||||
|
||||
def _settled(doc: dict, job: Any = None) -> tuple[dict, Any]:
|
||||
"""옛 셈 길 첫머리(`calc_doc`)와 같게 — (정리한 문서, 사본 색인)."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
raw = {**doc, skc.JOB: job} if job else doc
|
||||
whole = skc.over(doc := skc.settle(raw), m01.loaded()[1])
|
||||
try:
|
||||
return sd.clean(doc, None, whole), whole
|
||||
except sd.DocError as e:
|
||||
raise calc._fail(e) from e
|
||||
|
||||
|
||||
def current(doc: dict, job: Any = None) -> dict:
|
||||
"""그 SK 의 지금 수식 사본(할일 적용 · 저장 없음) — 따로 = 저장한 문서 · 파생 = 옛 칸으로 지음."""
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
if not dsk.own(doc):
|
||||
if doc.get("가져온"):
|
||||
raise _bad("가져온 식 단가산출 — 수식 문서는 엑셀 불러오기 몫(802-4b)")
|
||||
raise _bad("바탕 참조 단가산출 — 수식 사본 없음")
|
||||
clean, whole = _settled(doc, job)
|
||||
if not dc.has(clean): # 옮기기 전 · 새 SK — 저장 때 지을 사본을 미리
|
||||
seed = dsk.settle(clean, whole, job, doc).get(dsk.KEY)
|
||||
if seed is None:
|
||||
raise _bad("수식 사본을 지을 수 없음 — 바탕 로직 · 줄 없음")
|
||||
clean = {**clean, dsk.KEY: seed}
|
||||
box: list = []
|
||||
try:
|
||||
calc.run(clean, whole, catch=box)
|
||||
except sd.DocError as e:
|
||||
raise calc._fail(e) from e
|
||||
got = dsk.copy_of(clean, box[-1] if box else None)
|
||||
return got if got is not None else clean[dsk.KEY]
|
||||
|
||||
|
||||
def answer(copy: dict) -> dict[str, Any]:
|
||||
"""{문서, 부름, 끊음, 셈, 사본 표지} — 글 · 수는 JSON 꼴."""
|
||||
got = dc.run(copy["문서"], copy)
|
||||
head = {k: copy.get(k) for k in ("시점", "단가시점")}
|
||||
mark = {**head, "따로": bool(copy.get("따로")), **dc.state(copy)}
|
||||
body = {
|
||||
"문서": copy["문서"],
|
||||
"부름": copy.get("부름") or {},
|
||||
"끊음": copy.get("끊음") or {},
|
||||
"셈": got,
|
||||
"사본": mark,
|
||||
}
|
||||
return json.loads(json.dumps(dc.ml._norm(body), ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
def read(key: str) -> dict[str, Any]:
|
||||
"""읽기 — {문서, 부름, 끊음, 셈, 사본, 판}."""
|
||||
got = _file(key)
|
||||
return {**answer(current(got["문서"])), "판": got["판"]}
|
||||
|
||||
|
||||
# ── 몸 검사 ───────────────────────────────────────────────────────────
|
||||
def _doc(raw: Any, key: str, where: str) -> dict:
|
||||
if not isinstance(raw, dict):
|
||||
raise _bad(f"{where} · 묶음 아님")
|
||||
if len(json.dumps(raw, ensure_ascii=False, default=str).encode("utf-8")) > MAX:
|
||||
raise _bad(f"{where} · 너무 큼")
|
||||
for name in ("변수", "식", "금액"):
|
||||
if not isinstance(raw.get(name, []), list):
|
||||
raise _bad(f"{where} · 「{name}」 목록 아님")
|
||||
return {**raw, "키": key}
|
||||
|
||||
|
||||
def edited(copy: dict, body: dict, key: str) -> dict:
|
||||
"""화면 고침 얹은 사본 — 문서 · 끊음 글만 받음(값 · 부름은 서버 것)."""
|
||||
if body.get("할일") not in (None, "", *JOBS):
|
||||
raise _bad("할일은 단가갱신 · 재생성")
|
||||
if body.get("할일") == skc.REGEN:
|
||||
return copy # 재생성 = 고친 것 버림(그 사본이 이미 새로 찍힌 것)
|
||||
out = {**copy}
|
||||
if body.get("문서") is not None:
|
||||
out["문서"] = _doc(body["문서"], key, "문서")
|
||||
cut = body.get("끊음")
|
||||
if cut is not None:
|
||||
if not isinstance(cut, dict):
|
||||
raise _bad("끊음 · {키: 문서} 묶음")
|
||||
keys = set(copy.get("부름") or {}) | set(copy.get("끊음") or {})
|
||||
if bad := sorted(k for k in cut if k not in keys):
|
||||
raise _bad(f"끊음 · 이 사본이 부르지 않는 단가산출 {bad}")
|
||||
out["끊음"] = {k: _doc(v, k, f"끊음 「{k}」") for k, v in cut.items()}
|
||||
calls = [
|
||||
*dc.calls(out["문서"]),
|
||||
*(c for d in out.get("끊음", {}).values() for c in dc.calls(d)),
|
||||
]
|
||||
whole = dc.over(out)
|
||||
out["부름"] = dc.closure(whole, calls, copy.get("부름"))
|
||||
out["따로"] = True
|
||||
return out
|
||||
|
||||
|
||||
def preview(key: str, body: dict) -> dict[str, Any]:
|
||||
"""미리 셈(저장 없음) — {셈, 전, 후, 바뀐자원?, 버림?}."""
|
||||
doc = _file(key)["문서"]
|
||||
before = current(doc)
|
||||
job = body.get("할일") or None
|
||||
after = edited(current(doc, job) if job else before, body, key)
|
||||
out = answer(after)
|
||||
out |= {"전": dc.money(dc.run(before["문서"], before)), "후": dc.money(out["셈"])}
|
||||
if job == skc.PRICE:
|
||||
out["바뀐자원"] = dc.refresh(before)[1]
|
||||
if job == skc.REGEN:
|
||||
out["버림"] = {
|
||||
"문서고침": bool(before.get("따로")),
|
||||
"끊음": sorted(before.get("끊음") or {}),
|
||||
}
|
||||
return json.loads(json.dumps(dc.ml._norm(out), ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
def save(key: str, body: dict) -> dict[str, Any]:
|
||||
"""저장(층 쓰기) — 서버가 다시 셈한 사본을 그 SK 에 씀 · {문서, 부름, 끊음, 셈, 사본, 판}."""
|
||||
got = _file(key)
|
||||
if str(body.get("판") or "") != got["판"]:
|
||||
raise _bad({"stale": [key], "판": got["판"]}, 409)
|
||||
job = body.get("할일") or None
|
||||
raw = {**got["문서"], **({skc.JOB: job} if job else {})}
|
||||
token = None
|
||||
if job != skc.REGEN and (body.get("문서") is not None or body.get("끊음") is not None):
|
||||
mine = edited(current(got["문서"], job), body, key)
|
||||
token = dsk.SAVING.set({"키": key, "사본": mine})
|
||||
try:
|
||||
store.write(sd.KIND, key, got["판"], raw)
|
||||
finally:
|
||||
if token is not None:
|
||||
dsk.SAVING.reset(token)
|
||||
return read(key)
|
||||
@@ -126,13 +126,16 @@ def seed_picks(labor: Any) -> dict[str, Any]:
|
||||
def build_snapshot(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"""원본 내용 + 프로젝트 값(입력값 · 고른자재 첫 값 = 일위대가 문서 것) — `{파일 이름: 내용}` ·
|
||||
SS 줄이 없으면 LookupError."""
|
||||
files = build_source(item)
|
||||
from M02_MasterTemplete import M02_Doc_B09 as doc_b09
|
||||
|
||||
files = doc_b09.attach(build_source(item)) # 수식 문서 사본(802-4a)
|
||||
labor = files.get(LABOR_FILE)
|
||||
return {**files, VALUES_FILE: seed_values(labor), PICKS_FILE: seed_picks(labor)}
|
||||
|
||||
|
||||
def source_files(files: dict[str, Any]) -> dict[str, Any]:
|
||||
return {k: v for k, v in files.items() if k not in LOCAL_FILES}
|
||||
"""원본판 몫 — 수식 문서 사본(802-4a · 로직은 `단가.json` 이 잼)은 뺌."""
|
||||
return {k: v for k, v in files.items() if k not in (*LOCAL_FILES, "수식.json")}
|
||||
|
||||
|
||||
def write_snapshot(target: Path, item: dict[str, Any], owner_id: str, extra: dict) -> dict:
|
||||
@@ -400,7 +403,7 @@ def complete_old(folder: Path) -> bool:
|
||||
kept = {
|
||||
name: read_json(folder / name)
|
||||
for name in sorted(p.name for p in folder.glob("*.json"))
|
||||
if name not in (MANIFEST_NAME, *LOCAL_FILES)
|
||||
if name not in (MANIFEST_NAME, *LOCAL_FILES, "수식.json") # 수식 사본(802-4a) 원본판 밖
|
||||
}
|
||||
atomic_write_json(
|
||||
folder / MANIFEST_NAME,
|
||||
|
||||
@@ -21,8 +21,11 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Doc_Sk as dsk
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
|
||||
KEY, JOB, PRICE, REGEN = skc.KEY, skc.JOB, skc.PRICE, skc.REGEN
|
||||
@@ -49,7 +52,7 @@ def stamp(doc: dict) -> dict | None:
|
||||
return skc.snap(list(dict.fromkeys(keys)), list(dict.fromkeys(links)))
|
||||
|
||||
|
||||
def _stored(doc: dict) -> dict | None:
|
||||
def _file(doc: dict) -> dict:
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
key = str(doc.get("키") or doc.get("열") or "")
|
||||
@@ -57,31 +60,57 @@ def _stored(doc: dict) -> dict | None:
|
||||
try:
|
||||
got = json.loads(store._path(kind, key).read_bytes())
|
||||
except (OSError, ValueError, store.StoreError):
|
||||
return None
|
||||
return {}
|
||||
return got if isinstance(got, dict) else {}
|
||||
|
||||
|
||||
def _stored(doc: dict) -> dict | None:
|
||||
got = _file(doc)
|
||||
return got[KEY] if skc.has(got) else None
|
||||
|
||||
|
||||
def _keys(doc: dict) -> list[str]:
|
||||
"""수식 사본 부름 뿌리 — 로직 줄 키(줄 덮개 · 참조 · 고정 줄은 옛 길)."""
|
||||
lines = [x for x in doc.get("담은로직") or [] if isinstance(x, dict)]
|
||||
return list(dict.fromkeys(str(x["로직"]) for x in lines if x.get("로직")))
|
||||
|
||||
|
||||
def formula(doc: dict, raw: Any, job: Any, file: dict) -> dict | None:
|
||||
"""수식 사본(802-4a) — 값 = 로직 사본 줄 · 부름 = 있던 것(받은 것 › 저장본 · 옛 사본과 같은 차례) ·
|
||||
재생성 = 새로."""
|
||||
copy = doc.get(KEY)
|
||||
if not copy:
|
||||
return None
|
||||
have = raw.get(dc.KEY) if dc.has(raw) else (file.get(dc.KEY) if dc.has(file) else None)
|
||||
seed = None if job == REGEN else (have or {}).get("부름")
|
||||
return dsk.norm(dc.stamp(copy, _keys(doc), seed) or {})
|
||||
|
||||
|
||||
def settle(doc: Any, save: bool = False) -> Any:
|
||||
"""셈 · 저장 첫머리 — 서버가 정한 사본으로 갈음(할일 · 저장 때 찍음 · 새 줄 · 할일 칸 뗌)."""
|
||||
if not isinstance(doc, dict) or _project():
|
||||
return doc
|
||||
job = doc.get(JOB) # 모르는 할일 = 없음
|
||||
out = {k: v for k, v in doc.items() if k not in (KEY, JOB)}
|
||||
copy = skc.taken(doc.get(KEY)) or _stored(doc)
|
||||
out = {k: v for k, v in doc.items() if k not in (KEY, JOB, dc.KEY)}
|
||||
file = _file(doc)
|
||||
copy = skc.taken(doc.get(KEY)) or (file[KEY] if skc.has(file) else None)
|
||||
if job == REGEN or (copy is None and save):
|
||||
copy = stamp(out)
|
||||
elif job == PRICE and copy:
|
||||
copy = skc.refresh(copy, skc.whole_())[0]
|
||||
if save and copy:
|
||||
copy = skc.grow(copy, stamp(out))
|
||||
return {**out, KEY: copy} if copy else out
|
||||
if not copy:
|
||||
return out
|
||||
got = {**out, KEY: copy}
|
||||
return {**got, dc.KEY: formula(got, doc, job, file)}
|
||||
|
||||
|
||||
def keep(raw: Any, out: dict) -> dict:
|
||||
"""정리한 문서에 사본 · 할일 칸을 실어 보냄(꼴 검사는 `settle` 몫)."""
|
||||
if not isinstance(raw, dict):
|
||||
return out
|
||||
got = {k: raw[k] for k in (KEY, JOB) if raw.get(k) is not None}
|
||||
got = {k: raw[k] for k in (KEY, JOB, dc.KEY) if raw.get(k) is not None}
|
||||
return {**out, **got}
|
||||
|
||||
|
||||
@@ -93,6 +122,42 @@ def live(doc: dict) -> Any:
|
||||
return whole if _project() else skc.over(settle(doc), whole)
|
||||
|
||||
|
||||
_LAST: list = [(None, None)] # (문서, 수식 사본) — 한 셈 안 줄마다 settle 안 되풀이
|
||||
|
||||
|
||||
def _formula_of(doc: dict) -> dict | None:
|
||||
have, got = _LAST[0]
|
||||
if have is not doc:
|
||||
got = (settle(doc) or {}).get(dc.KEY)
|
||||
_LAST[0] = (doc, got)
|
||||
return got
|
||||
|
||||
|
||||
def line(whole: Any, doc: dict, one: dict) -> dict[str, Any]:
|
||||
"""로직 줄 기준 — 수식 사본 있는 층 셈 = 문서 셈(줄 옵션 = 문서 규칙 · `dc.line_doc`) ·
|
||||
상세 줄 · 재료 = 옛 엔진(화면 그대로 · 802-4b) · 프로젝트 층 · B09(단가 책) · 사본 없음 = 옛 길."""
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
code, rate, picks = one["로직"], one.get("켜쌓기율"), one.get("고름")
|
||||
old = labor.drop_apart(
|
||||
labor.run_line(whole, code, one.get("입력값"), rate, picks), one.get("자재표시")
|
||||
)
|
||||
copy = None if _project() or dsk.OLD.get() else _formula_of(doc)
|
||||
if not copy or getattr(whole, "_tag", None) != (copy.get("판"), copy.get("단가판")):
|
||||
return old
|
||||
base = copy["부름"].get(code)
|
||||
if base is None:
|
||||
return {**old, "기준": None, "까닭": f"수식 문서 없음 「{code}」"}
|
||||
apart = dc.apart_of(one.get("자재표시"), old.get("재료"))
|
||||
given = dc.given_of(whole, code, one.get("입력값"))
|
||||
ans = dc.run(dc.line_doc(base, rate, picks, apart), copy, given, whole)
|
||||
if "결과" in ans:
|
||||
return old
|
||||
money = dc.money(ans)
|
||||
got = {k: Decimal(str(v)) for k, v in money.items()} if money else None
|
||||
return {**old, "기준": got, "까닭": None if got else dc.why(ans)}
|
||||
|
||||
|
||||
def combo(expand: Any, doc: dict, trail: tuple[str, ...]) -> dict[str, Any]:
|
||||
"""사전조합 1 단위 값 — 그 SC 사본 색인으로(층 셈만 · 프로젝트 = 굳힌 단가 그대로)."""
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
@@ -525,8 +525,8 @@ def calc(
|
||||
if one.get(patch.FIELD): # 줄 덮개(715-3) = 이름 없는 단가산출 셈 · 별도계상까지
|
||||
lines.append({**line, **patch.run(whole, one, read)})
|
||||
continue
|
||||
got = run_line(whole, code, one.get("입력값"), one.get("켜쌓기율"), one.get("고름"))
|
||||
lines.append({**line, **drop_apart(got, one.get("자재표시"))})
|
||||
got = lc.line(whole, doc, one) # 수식 사본 = 문서 셈(802-4a) · 별도계상까지
|
||||
lines.append({**line, **got})
|
||||
mats = [
|
||||
{
|
||||
"코드": one["코드"],
|
||||
|
||||
@@ -112,7 +112,7 @@ class Expand:
|
||||
],
|
||||
}
|
||||
|
||||
def _logic(self, one: dict, pill: dict | None) -> dict[str, Any]:
|
||||
def _logic(self, one: dict, pill: dict | None, doc: dict | None = None) -> dict[str, Any]:
|
||||
"""사전조합 안 로직 줄 — 일위대가 로직 줄과 같은 `run_line` · 배는 물량 알약 것."""
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
@@ -124,11 +124,8 @@ class Expand:
|
||||
got = labor.drop_apart(got, fz.get("자재표시"))
|
||||
elif one.get(patch.FIELD): # 줄 덮개(715-3) = 이름 없는 단가산출 · 별도계상까지
|
||||
got = patch.run(self.whole, one, self.read)
|
||||
else:
|
||||
got = labor.run_line(
|
||||
self.whole, one["로직"], one.get("입력값"), one.get("켜쌓기율"), one.get("고름")
|
||||
)
|
||||
got = labor.drop_apart(got, one.get("자재표시")) # 별도계상 로직 자재 뺌(222-1)
|
||||
else: # 별도계상 로직 자재 뺌(222-1) · 수식 사본 SC = 문서 셈(802-4a)
|
||||
got = lc.line(self.whole, doc or {}, one)
|
||||
times = labor._number(pill.get("배")) or Decimal(1)
|
||||
base = got.get("기준")
|
||||
return {
|
||||
@@ -158,7 +155,7 @@ class Expand:
|
||||
head = {"참조": ref}
|
||||
sub = self(ref, trail) if not one.get(patch.FIELD) else patch.ref(one, self)
|
||||
elif one.get("로직"): # 로직 줄(51-11) = 1 단위 값(기준 ÷ 배 · 승격과 같음) · 덮개 있음
|
||||
head, sub = {"로직": one["로직"]}, self._logic(one, by_logic.get(one["로직"]))
|
||||
head, sub = {"로직": one["로직"]}, self._logic(one, by_logic.get(one["로직"]), doc)
|
||||
elif fixed.is_fixed(one): # 고정 줄(705-3) = 줄 단가 · 비율 줄 = 끝에(`ratio_row`)
|
||||
head, sub = {"고정": one["고정"]}, fixed.line(self.whole, one["고정"])
|
||||
else:
|
||||
|
||||
@@ -32,6 +32,7 @@ import re
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Sk as dsk # 수식 사본(802-4a)
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor, M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
@@ -361,6 +362,7 @@ def run(
|
||||
detail: bool = False,
|
||||
line: bool = False,
|
||||
seen: bool = True,
|
||||
catch: list | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""SK 셈(저장 안 함) — `run_line` 답 + `{1단위, 배, 정체, 짝잃음, 바탕판}` · 줄마다 `cid` ·
|
||||
고친 칸 `덮개`(칸 이름) · `원값`. 틀린 문서 · 순환 = `sd.DocError` ·
|
||||
@@ -383,7 +385,8 @@ def run(
|
||||
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 []
|
||||
eng, ctx = out.get("줄") or [], (over, key, values, got, out.get("재료")) # 802-4a 고친 줄
|
||||
catch is None or catch.append(ctx)
|
||||
seen = (
|
||||
views.text_of(over, key, values, got["고름"], got["정체"], eng) if detail and seen else {}
|
||||
)
|
||||
@@ -412,7 +415,8 @@ def run(
|
||||
]
|
||||
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}
|
||||
ans = {**out, **head, **seen, "1단위": unit, "빠진줄": gone, "꼬리표": tag}
|
||||
return dsk.on_run(doc, ans, ctx) # 수식 사본 SK = 문서 셈 금액(802-4a)
|
||||
|
||||
|
||||
def _group(item: dict, line: dict) -> str | None:
|
||||
@@ -547,6 +551,7 @@ def clean_doc(doc: Any, here_: refs.Here) -> dict[str, Any]:
|
||||
"""저장 꼴(`Labor_Ref.clean_doc` 가 부름) — 검사 · 참조 · 순환 · 서버가 `원값` · `바탕판`."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
raw, job = doc, doc.get(skc.JOB) if isinstance(doc, dict) else None # 수식 사본(802-4a)
|
||||
whole = skc.over(doc := skc.settle(doc, True), m01.loaded()[1]) # 사본(795-1)
|
||||
try:
|
||||
rough = sd.clean(doc, None, whole)
|
||||
@@ -566,6 +571,7 @@ def clean_doc(doc: Any, here_: refs.Here) -> dict[str, Any]:
|
||||
out |= const.stamp(base["줄"], out) # 바탕판 · M01판(790-2) · 식 칸 엔진 식(792-1)
|
||||
ratio_of(out, base["줄"], whole) # 단위 못 바꿈(계수 없음) = 저장 막음(728-2)
|
||||
_stamp_adds(out, base, whole)
|
||||
out = dsk.settle(out, whole, job, raw)
|
||||
except sd.DocError as e:
|
||||
raise _fail(e) from e
|
||||
return out
|
||||
|
||||
@@ -234,6 +234,8 @@ def clean_more(raw: Any, out: dict) -> dict:
|
||||
out["까닭"] = why
|
||||
if isinstance(raw.get("사본"), dict): # 로직 사본(795-1) — 셈 · 저장 첫머리 settle 이 정함
|
||||
out["사본"] = raw["사본"]
|
||||
if isinstance(raw.get("수식"), dict): # 수식 사본(802-4a) — 저장 끝 `M02_Doc_Sk.settle` 이 정함
|
||||
out["수식"] = raw["수식"]
|
||||
out = eqm.clean(raw, out) # 수식 고침(796-2b)
|
||||
return imp.clean(raw, fx.clean(raw, out)) # 식 칸(792-1) · 가져온 식(793-2)
|
||||
|
||||
|
||||
@@ -261,9 +261,25 @@ def settle(doc: Any, save: bool = False) -> Any:
|
||||
copy = rejoin(copy, out["더한줄"])
|
||||
if save and copy:
|
||||
copy = grow(copy, stamp(out))
|
||||
out = _formula_job(out, job)
|
||||
return {**out, KEY: copy} if copy else out
|
||||
|
||||
|
||||
def _formula_job(out: dict, job: Any) -> dict:
|
||||
"""수식 사본(802-4a · `M02_Doc_Sk`) 할일 — 재생성 = 파생으로(부름 새로 · 고친 문서 버림) ·
|
||||
단가갱신 = 따로 문서의 값 줄도 지금 M01."""
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
|
||||
got = out.get(dc.KEY) if dc.has(out) else None
|
||||
if got is None or job not in (PRICE, REGEN):
|
||||
return out
|
||||
if job == REGEN:
|
||||
got = {k: v for k, v in got.items() if k not in ("문서", "끊음", "따로")} | {"부름": {}}
|
||||
elif got.get("따로"):
|
||||
got = dc.refresh(got)[0]
|
||||
return {**out, dc.KEY: got}
|
||||
|
||||
|
||||
# ── 표지 ──────────────────────────────────────────────────────────────
|
||||
def state(copy: dict, whole: Any) -> dict[str, Any]:
|
||||
"""{M01새판, 지운키, 단가새값} — 사본 줄과 지금 M01 줄 대조(같은 M01 · 같은 판 = 메모)."""
|
||||
|
||||
Reference in New Issue
Block a user