feat(802-4b-1): 엑셀 불러오기 가져온 식 SK = 수식 문서 — 숫자 값 · 이음 = 연결 + 엑셀 값 기억 · 산근 속 산근 = 형제 단가산출 연결 · 고른 소요량 = 표 연결 · 표지
- 새 `M02_Doc_Import` — 엑셀 단가 기준 엔진 줄(가져온 줄 + SK 고침)을 `master_doc_convert` 로 문서로 옮김(손 값 = 숫자 · 중간 = 식 줄 · EST 얼린 변수 = 숫자 · 덧줄 · 단계 · 끝전 · 자리 · 합밖 그대로)
· 인력 · 재료 · 유가 · 환율 이음 = 요소 · 기계 EQ = 기계경비 단가산출(칸 = 그 줄 비목 · 시간당 자료 없음 = 숫자 + 설명) · 로직 이음 = 단가산출 · 참조 = 형제 SK 단가산출(칸 = 참조비목)
· 연결 칸 `엑셀` = 엑셀 값 기억 · 머리 `가져온` {파일, 시트, 행, 갈래, 코드, 표지(797-1a · 못 읽음 까닭 그대로)}
· 소요량 · 계수 고른 칸(797-2) = 그 수 자리에 표 연결 기호(꼴 pct · kgt = ÷ 배)
- 셈 = 수식 문서 계산기 — [엑셀 단가 그대로] = 엑셀 값 기억 연결을 숫자로 · [우리 단가로] = 연결 값 · 형제 SK = 부르는 쪽 단가기준(안쪽이 바뀌면 따라감)
- SK 금액 = 문서 셈(`M02_Doc_Sk.on_run` · 가져온 식도 수식 사본 · 파생 = 셈마다 지음 · 따로 = 수식 길 저장본 · 가져온 줄 · 이음 · 고름 고침 409)
- 수식 길(…/sangeun/{키}/doc · preview · save) 가져온 식도 · 사본 표지 단가기준 · 단가갱신 = 문서 그대로
- TS 짝 `common_util_formula_doc_import.ts` `importView`(브라우저 계산기 엑셀 보기 · 거울 시험)
- 옮기기 `M02_Doc_Migrate` 가 가져온 식 SK 도 대조 · 씀(--apply --backup) — 실제 storage 보기만: 개인 가져온 식 1 같음
- 시험 test_802_4b1(9) · 불러오기 시험 793_1 · 793_2 · 793_3a · 793_3b · 797_1a · 797_1 · 797_2 · 802_4a 147 통과
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
"""M02 가져온 식(엑셀 불러오기) 단가산출 = 수식 문서(PLAN 802-4b-1 · 요구 664 · 665) — 서버 단독.
|
||||
|
||||
경쟁사 산근 블록도 같은 꼴(식 + 값)로 들어옴 — 엑셀 단가 그대로 엔진 줄(`가져온.로직` + SK 고침)을
|
||||
`master_doc_convert` 로 문서로 옮기고(손 값 = 숫자 연결 · 중간 = 식 줄 · 덧줄 = 금액 줄 식 · 끝수 ·
|
||||
단계 · 조정 · 합밖 · 자리 그대로) 이은 것을 연결로 얹음 · 연결마다 엑셀 값을 기억(`엑셀` 칸):
|
||||
인력 · 재료 · 유가 · 전력 · 환율 이음 = 요소 {키} · 기계 이음(EQ) = 기계경비 단가산출
|
||||
{키 GC, 입력 기계 …, 칸 = 그 줄 비목}(시간당 자료 없음 = 숫자 그대로 · 설명) ·
|
||||
로직 이음 「로직(GC…)」 = 단가산출 {키, 입력} · 산근 속 산근(797-1 참조) = 단가산출
|
||||
{키 형제 SK, 참조 {층, 주인}, 칸?} ·
|
||||
소요량 · 계수 고른 것(797-2 `가져온.연결.고름`) = 그 수 자리에 표 연결 기호(pct · kgt = ÷ 배).
|
||||
문서 머리 `가져온` = {파일, 시트, 행, 갈래, 코드?, 표지(797-1a · 못 읽음 까닭 그대로), 알림?}.
|
||||
셈(`run`) — 단가기준 엑셀(기본) = 엑셀 값 기억 연결을 숫자로 보고 셈 · 우리 = 연결 값으로 셈 ·
|
||||
형제 SK 는 늘 연결(부르는 쪽 단가기준으로 · 안쪽이 바뀌면 따라감).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy as cp
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
|
||||
XL = "엑셀" # 연결 칸 — 엑셀 값 기억
|
||||
HEAD = ("파일", "시트", "행", "갈래", "코드")
|
||||
PICK = "소요량" # 고른 표 칸 기호 이름 머리
|
||||
_SCALE = {"eq": (1,), "pct": (100, 0.01), "kgt": (1000, 0.001)}
|
||||
|
||||
|
||||
def is_imported(sk: Any) -> bool:
|
||||
return imp.is_imported(sk)
|
||||
|
||||
|
||||
def basis_of(sk: dict) -> str:
|
||||
return "우리" if (sk.get(imp.KEY) or {}).get("단가기준") == "우리" else "엑셀"
|
||||
|
||||
|
||||
# ── 엑셀 단가 그대로 엔진 줄(옛 셈 길 · SK 고침 얹은 것) ───────────────────
|
||||
def _ctx(sk: dict, whole: Any) -> tuple:
|
||||
"""엑셀 단가 기준 고친 줄 — 옛 셈 길(`OLD`)로 돌려 엔진에 넣은 (색인, 키, 입력, 답, 재료)."""
|
||||
from M02_MasterTemplete import M02_Doc_Sk as dsk
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
raw = {**sk, imp.KEY: {**sk[imp.KEY], "단가기준": "엑셀"}}
|
||||
box: list = []
|
||||
token = dsk.OLD.set(True)
|
||||
try:
|
||||
calc.run(sd.clean(raw, None, whole), whole, catch=box)
|
||||
finally:
|
||||
dsk.OLD.reset(token)
|
||||
return box[-1]
|
||||
|
||||
|
||||
# ── 소요량 · 계수 고른 칸 → 수 자리에 기호 ──────────────────────────────
|
||||
def _swap_nth(node: Any, n: int, new: tuple, seen: list) -> Any:
|
||||
"""`M02_Sangeun_QtyLinks._literals` 차례의 n 번째 수 → `new` 마디(같은 걸음 규칙)."""
|
||||
mf = dc.cv.mf
|
||||
if not isinstance(node, tuple):
|
||||
return node
|
||||
kind = node[0]
|
||||
if kind == "num":
|
||||
seen[0] += 1
|
||||
return new if seen[0] - 1 == n else node
|
||||
if kind == "bin":
|
||||
cmp_ = node[1] in mf._OPS
|
||||
sides = [
|
||||
side
|
||||
if cmp_ and isinstance(side, tuple) and side[0] == "num"
|
||||
else _swap_nth(side, n, new, seen)
|
||||
for side in node[2:]
|
||||
]
|
||||
return (kind, node[1], *sides)
|
||||
if kind in ("neg", "field"):
|
||||
return (kind, _swap_nth(node[1], n, new, seen), *node[2:])
|
||||
if kind == "call":
|
||||
return ("call", node[1], [_swap_nth(a, n, new, seen) for a in node[2]])
|
||||
return node
|
||||
|
||||
|
||||
def _conds(table: dict, at: int) -> dict | None:
|
||||
"""표 줄 차례 → 그 줄 하나만 맞는 조건 {조건: 값}(범위 = 위 끝 › 아래 끝) · 못 가림 = None."""
|
||||
rows = table.get("줄") or []
|
||||
if not (0 <= at < len(rows)):
|
||||
return None
|
||||
row, out = rows[at], {}
|
||||
for cond, how in (table.get("조건") or {}).items():
|
||||
if cond not in row:
|
||||
continue
|
||||
have = row[cond]
|
||||
if how == "범위":
|
||||
low, high = have
|
||||
have = high if high is not None else low
|
||||
if have is None:
|
||||
return None
|
||||
out[cond] = have
|
||||
try:
|
||||
hit = dc.ml.find(table, out)
|
||||
except Exception: # noqa: BLE001 — 못 가림 = 수 그대로
|
||||
return None
|
||||
return out if hit is row else None
|
||||
|
||||
|
||||
def _picks(row: dict, ids: list[str], base: list[str], meta: dict, master: Any) -> dict:
|
||||
"""고른 칸(`가져온.연결.고름`) → 중간 줄(표 찾기 하나 · 연결 기호) · 수 자리에 그 기호 ÷ 배 ·
|
||||
돌려줌 = {중간 이름: 엑셀 칸 값}. 자리 줄이 SK 고침으로 바뀌었으면 건너뜀."""
|
||||
mf, mee = dc.cv.mf, dc.cv.mee
|
||||
src = meta["로직"]
|
||||
picked = ((meta.get("연결") or {}).get("고름")) or []
|
||||
out: dict[str, Any] = {}
|
||||
mids = row.setdefault("중간", [])
|
||||
taken = {str(x.get("이름")) for x in [*(row.get("입력") or []), *mids]}
|
||||
for one in picked:
|
||||
part, i, n = str(one.get("자리") or "::").split(":")[:3]
|
||||
field = "수량" if part == "호표" else "식"
|
||||
lines = src.get(part) or []
|
||||
if not (i.isdigit() and n.isdigit() and int(i) < len(lines)):
|
||||
continue
|
||||
cid = base[int(i) + (len(src.get("호표") or []) if part == "덧줄" else 0)]
|
||||
if cid not in ids:
|
||||
continue
|
||||
at = ids.index(cid)
|
||||
mine = (row["호표"] if at < len(row["호표"]) else row.get("덧줄") or [])[
|
||||
at if at < len(row["호표"]) else at - len(row["호표"])
|
||||
]
|
||||
if str(mine.get(field)) != str(lines[int(i)].get(field)):
|
||||
continue # SK 덮개가 그 식을 바꿈 — 수 자리 어긋남
|
||||
try:
|
||||
table = master.get(str(one.get("표")))
|
||||
conds = _conds(table, int(one.get("줄")))
|
||||
node = mf.parse(str(mine[field]))
|
||||
except Exception: # noqa: BLE001 — 표 · 식 못 읽음 = 수 그대로
|
||||
continue
|
||||
if conds is None:
|
||||
continue
|
||||
name = next(f"{PICK}{k}" for k in range(1, 999) if f"{PICK}{k}" not in taken)
|
||||
taken.add(name)
|
||||
scale = next(
|
||||
(m for m in _SCALE.get(str(one.get("꼴")), (1,)) if _near(one, m)), None
|
||||
) # 엑셀 수 = 칸 값 ÷ 배
|
||||
if scale is None:
|
||||
continue
|
||||
ref = (
|
||||
("name", name)
|
||||
if scale == 1
|
||||
else ("bin", "/", ("name", name), ("num", Decimal(str(scale))))
|
||||
)
|
||||
mine[field] = mee.raw_text(_swap_nth(node, int(n), ref, [0]))
|
||||
args = ", ".join(f"{k}={_lit(v)}" for k, v in conds.items())
|
||||
mids.append(
|
||||
{"이름": name, "식": f"찾기({one['표']}{', ' if args else ''}{args}).{_q(one['칸'])}"}
|
||||
)
|
||||
out[name] = Decimal(str(one.get("값")))
|
||||
return out
|
||||
|
||||
|
||||
def _near(one: dict, m: float) -> bool:
|
||||
cell, v = float(one.get("값") or 0), float(one.get("수") or 0)
|
||||
return abs(cell - v * m) <= 1e-9 * max(1.0, abs(cell))
|
||||
|
||||
|
||||
def _lit(v: Any) -> str:
|
||||
if isinstance(v, (int, float, Decimal)) and not isinstance(v, bool):
|
||||
return dc.cv.mf._plain(Decimal(str(v)))
|
||||
return "'" + str(v).replace("'", "") + "'"
|
||||
|
||||
|
||||
def _q(name: Any) -> str:
|
||||
return dc.cv.mee._q(str(name))
|
||||
|
||||
|
||||
# ── 이음 → 연결 ────────────────────────────────────────────────────────
|
||||
def hourly_link(eq: str, part: str) -> dict | None:
|
||||
"""기계(EQ) 시간당 그 비목 = 기계경비 단가산출 부름(`M02_Machine_Hourly` 와 같은 짝 고름) ·
|
||||
자료 없음 = None."""
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as machine
|
||||
|
||||
if part not in imp.COSTS or not imp.hourly_of(eq).get("ok"):
|
||||
return None
|
||||
code = str(imp._whole().get(eq).get("원문번호") or "")
|
||||
fit = [r for r in machine._family() if code in [str(c) for c in machine._codes(r)]]
|
||||
if not fit:
|
||||
return None
|
||||
logic = min(fit, key=machine._rank)
|
||||
given = {"기계": _lit(code)}
|
||||
for spec in logic.get("입력") or []:
|
||||
if spec.get("이름") != "기계":
|
||||
given[spec["이름"]] = _lit((spec.get("고르기") or [""])[0])
|
||||
return {"갈래": "단가산출", "키": str(logic["키"]), "입력": given, "칸": part}
|
||||
|
||||
|
||||
def _logic_link(text: str) -> dict | None:
|
||||
"""「로직(GC…, 이름='값')」 → 단가산출 연결 · 인자가 수 · 글 아니면 None."""
|
||||
try:
|
||||
node = dc.cv.mf.parse(text)
|
||||
except Exception: # noqa: BLE001
|
||||
return None
|
||||
if node[0] != "logic" or any(v[0] not in ("num", "str") for _, v in node[2]):
|
||||
return None
|
||||
return {"갈래": "단가산출", "키": node[1], "입력": {k: _lit(v[1]) for k, v in node[2]}}
|
||||
|
||||
|
||||
def _link_of(item: dict) -> tuple[dict | None, str]:
|
||||
"""가져온 줄 하나의 연결(엑셀 값 기억 없이) · 설명(못 이은 까닭)."""
|
||||
if item.get("참조"):
|
||||
ref = item["참조"]
|
||||
link = {
|
||||
"갈래": "단가산출",
|
||||
"키": ref["키"],
|
||||
"참조": {"층": ref["층"], "주인": ref.get("주인")},
|
||||
}
|
||||
return link | ({"칸": item["참조비목"]} if item.get("참조비목") else {}), ""
|
||||
key = str(item.get("이음") or "")
|
||||
if not key:
|
||||
return None, ""
|
||||
if item.get("종류") == "로직":
|
||||
return _logic_link(key), ""
|
||||
if key.startswith("EQ"):
|
||||
got = hourly_link(key, str(item.get("비목") or ""))
|
||||
return got, "" if got else f"기계 {key} 시간당 경비 자료 없음 — 엑셀 단가 그대로"
|
||||
link = {"갈래": "요소", "키": key}
|
||||
if key[:2] == dc.cv.mm.LINKED: # 품셈재료 = 줄 단위로 값이 갈림(M01 변환과 같음)
|
||||
link["단위"] = str(item.get("단위") or "")
|
||||
return link, ""
|
||||
|
||||
|
||||
def _excel(item: dict) -> Any:
|
||||
if item.get("값묶음"):
|
||||
return {k: Decimal(str(item["값묶음"].get(k) or 0)) for k in imp.COSTS}
|
||||
return Decimal(str(item["단가"])) if item.get("단가") is not None else None
|
||||
|
||||
|
||||
def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None:
|
||||
"""금액 줄 단가 숫자(엑셀 값) → 이은 연결 + 엑셀 값 기억(SK 고침으로 바뀐 값 = 숫자 그대로)."""
|
||||
by_id = {dc.md.var_id(v): v for v in doc.get("변수") or []}
|
||||
for at, line in enumerate((doc.get("금액") or [])[: len(row["호표"])]):
|
||||
item = src.get(ids[at]) if at < len(ids) else None
|
||||
price = str(line.get("단가") or "")
|
||||
var = by_id.get(price.removesuffix(".계"))
|
||||
if item is None or var is None or (var.get("연결") or {}).get("갈래") != "숫자":
|
||||
continue
|
||||
excel = _excel(item)
|
||||
if not item.get("참조") and var["연결"].get("값") != excel:
|
||||
continue # SK 덮개 단가 = 사용자 값
|
||||
link, note = _link_of(item)
|
||||
if note:
|
||||
var["설명"] = note
|
||||
if link is None:
|
||||
continue
|
||||
if item.get("참조비목") and isinstance(excel, dict):
|
||||
excel = excel[item["참조비목"]]
|
||||
line["단가"], line["비목"] = dc.md.var_id(var), item["참조비목"]
|
||||
var["연결"] = {**link, XL: excel}
|
||||
|
||||
|
||||
def _numbers(doc: dict, keep: dict) -> None:
|
||||
"""수 하나뿐인 식 줄(EST 얼린 변수) → 숫자 연결 · 고른 칸 중간 → 엑셀 칸 값 기억."""
|
||||
by_id = {dc.md.var_id(v): v for v in doc.get("변수") or []}
|
||||
lines = []
|
||||
for one in doc.get("식") or []:
|
||||
var = by_id.get(str(one.get("기호")))
|
||||
try:
|
||||
node = dc.md.parse(one.get("식"))
|
||||
except dc.md.DocError:
|
||||
node = None
|
||||
if var is not None and node is not None and node[0] == "num":
|
||||
var["연결"] = {"갈래": "숫자", "값": node[1]}
|
||||
continue
|
||||
lines.append(one)
|
||||
doc["식"] = lines
|
||||
for var in doc.get("변수") or []:
|
||||
if var.get("열쇠") in keep and (var.get("연결") or {}).get("갈래") == "표":
|
||||
var["연결"][XL] = keep[var["열쇠"]]
|
||||
|
||||
|
||||
def derive(sk: dict, ctx: tuple | None = None, whole: Any = None) -> dict:
|
||||
"""가져온 식 SK → 수식 문서(키 = SK 키) — `ctx` = 엑셀 단가 기준 옛 셈 길이 엔진에 넣은 것."""
|
||||
meta = sk[imp.KEY]
|
||||
if ctx is None:
|
||||
ctx = _ctx(sk, whole or imp._whole())
|
||||
master, key, values, got, _ = ctx
|
||||
row = cp.deepcopy(master.logic(key))
|
||||
ids = list(got.get("정체") or [])
|
||||
bare = imp.row_of({**sk, imp.KEY: {**meta, "단가기준": "엑셀"}}) or {}
|
||||
base = sd.line_ids(bare)
|
||||
src = dict(zip(base, meta["로직"].get("호표") or []))
|
||||
keep = _picks(row, ids, base, meta, master)
|
||||
given = dc.given_of(master, key, values)
|
||||
doc, notes = dc.cv.convert(_Row(master, key, row), row, given)
|
||||
_attach(doc, row, ids, src)
|
||||
_numbers(doc, keep)
|
||||
head = {k: (meta.get("원본") or {}).get(k) for k in HEAD if (meta.get("원본") or {}).get(k)}
|
||||
marks = list((meta.get("원본") or {}).get("표지") or [])
|
||||
doc["가져온"] = head | {"표지": marks} | ({"알림": notes} if notes else {})
|
||||
return {**doc, "키": str(sk.get("키") or key), "이름": str(sk.get("이름") or doc.get("이름"))}
|
||||
|
||||
|
||||
class _Row:
|
||||
"""변환 색인 — 이 SK 줄만 고친 줄(소요량 중간 더함)로 · 그 밖 그대로."""
|
||||
|
||||
def __init__(self, master: Any, key: str, row: dict) -> None:
|
||||
self._m, self._k, self._r = master, key, row
|
||||
|
||||
def logic(self, ref: str) -> dict:
|
||||
return self._r if ref == self._k else self._m.logic(ref)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self._m, name)
|
||||
|
||||
|
||||
# ── 셈 ────────────────────────────────────────────────────────────────
|
||||
def view(doc: dict, basis: str) -> dict:
|
||||
"""단가기준 엑셀 = 엑셀 값 기억 연결(형제 SK 빼고)을 숫자로 · 우리 = 그대로 ·
|
||||
TS 짝 = `common_util/common_util_formula_doc_import.ts` `importView`(거울 시험 test_802_4b1)."""
|
||||
if basis == "우리":
|
||||
return doc
|
||||
out = cp.deepcopy(doc)
|
||||
for var in out.get("변수") or []:
|
||||
link = var.get("연결") or {}
|
||||
if XL in link and not str(link.get("키") or "").startswith("SK-"):
|
||||
var["연결"] = {"갈래": "숫자", "값": link[XL]}
|
||||
return out
|
||||
|
||||
|
||||
class Links(dc.ml.Links):
|
||||
"""M01 연결(지금 M01 · 수식 문서 폴더) + 형제 가져온 식 SK(부르는 쪽 단가기준)."""
|
||||
|
||||
def __init__(self, master: Any, basis: str, trail: tuple = ()) -> None:
|
||||
super().__init__(master, dc.folder())
|
||||
self.basis, self.trail, self.mine = basis, trail, {}
|
||||
|
||||
def doc(self, key: str) -> dict | None:
|
||||
if not str(key).startswith("SK-"):
|
||||
return super().doc(key)
|
||||
return self.mine.get(key)
|
||||
|
||||
def learn(self, doc: dict) -> None:
|
||||
"""문서의 형제 SK 연결 → 그 SK 문서를 읽어 둠(깊이 · 순환 = 못 읽음 · 표지는 계산기)."""
|
||||
for var in doc.get("변수") or []:
|
||||
link = var.get("연결") or {}
|
||||
key, ref = str(link.get("키") or ""), link.get("참조")
|
||||
if not key.startswith("SK-") or key in self.mine or not isinstance(ref, dict):
|
||||
continue
|
||||
full = {**ref, "키": key}
|
||||
rid = refs.ref_id(full)
|
||||
if rid in self.trail or len(self.trail) >= refs.DEPTH:
|
||||
continue
|
||||
sib = refs.read_ref(full)
|
||||
if not is_imported(sib):
|
||||
continue
|
||||
self.mine[key] = None # 순환 막음
|
||||
try:
|
||||
sub = view(of(sib), self.basis)
|
||||
except Exception: # noqa: BLE001 — 못 지음 = 없는 단가산출(연결 끊김 표지)
|
||||
continue
|
||||
self.mine[key] = sub
|
||||
self.trail = (*self.trail, rid)
|
||||
self.learn(sub)
|
||||
|
||||
|
||||
def of(sk: dict) -> dict:
|
||||
"""SK 의 문서 — 수식 길로 고친 것(따로) = 저장본 · 아니면 지금 줄에서 지음."""
|
||||
got = sk.get(dc.KEY) if dc.has(sk) else None
|
||||
if got and got.get("따로") and isinstance(got.get("문서"), dict):
|
||||
return got["문서"]
|
||||
return derive(sk)
|
||||
|
||||
|
||||
def run(doc: dict, sk: dict, master: Any = None) -> dict:
|
||||
"""가져온 식 문서 셈(SK 단가기준) — 계산기 = M01 과 같은 `master_doc_calc`."""
|
||||
basis = basis_of(sk)
|
||||
links = Links(master or imp._whole(), basis)
|
||||
mine = view(doc, basis)
|
||||
try:
|
||||
links.learn(mine)
|
||||
return dc.mc.calc(mine, links)
|
||||
except Exception as e: # noqa: BLE001 — 계산기 예외 없음이 원칙 · 남은 틈도 「—」
|
||||
return {
|
||||
"셈됨": False,
|
||||
"표지": [{"갈래": "셈 틀림", "글": str(e)}],
|
||||
**dict.fromkeys(dc.COSTS),
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
층 문서 — 전 = 옛 셈 길(`M02_Doc_Sk.OLD` · 로직 사본 엔진) · 후 = 수식 사본 문서 셈:
|
||||
SK = 기준(노 · 재 · 경 · 계) · SC · 일위대가 = `Labor_Doc.calc` 기준합 · 반영합(원 단위 · 반올림 6 자리).
|
||||
같음 = 파일에 `수식` 칸만 더해 씀(그 밖 칸 그대로) · 다름 · 못 셈 = 안 씀(목록 · 까닭) ·
|
||||
가져온 식 SK = 802-4b(그대로) · 바탕 참조 SK · 로직 사본 없는 문서 = 필요없음 · 이미 있음 = 있음.
|
||||
가져온 식 SK(802-4b-1) = 가져온 줄 문서(`M02_Doc_Import`) · 전 = 옛 엔진 · 후 = 문서 셈 ·
|
||||
바탕 참조 SK · 로직 사본 없는 문서 = 필요없음 · 이미 있음 = 있음.
|
||||
B09 사본 — `수식.json`(`M02_Doc_B09.build` · 만든 때 `단가.json` 줄 기준) 더함 · 견적 셈은 그대로
|
||||
(`prices.calc` 전후 같음) · 문서 대조 = 일위대가 로직 줄마다 옛 엔진(`run_line`) 대 문서 셈
|
||||
(M01 규칙 · 같은 사본 값) — 다르면 목록.
|
||||
@@ -75,15 +76,13 @@ def formula_of(kind: str, doc: dict) -> dict | None:
|
||||
|
||||
|
||||
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)
|
||||
@@ -97,7 +96,7 @@ def check(kind: str, path: Path) -> dict[str, Any]:
|
||||
if before == after:
|
||||
return {**row, "상태": "같음", "수식": copy}
|
||||
why = (mark or {}).get("까닭") or "전후 금액 다름 — 안 옮김"
|
||||
if kind == "sangeun" and not (doc.get("바탕") or doc.get("더한줄")):
|
||||
if kind == "sangeun" and not (doc.get("바탕") or doc.get("더한줄") or doc.get("가져온")):
|
||||
why = "빈 SK(바탕 · 줄 없음) — 옛 0 · 새 빈 문서 「—」(요구서_664 0 장 · 브레인 802-4a ③)"
|
||||
return {**row, "상태": "다름(빈 문서)", "까닭": why, "수식": copy}
|
||||
return {**row, "상태": "다름", "까닭": why}
|
||||
|
||||
@@ -9,7 +9,8 @@ SK `수식`(꼴 = `M02_Doc_Copy`) — `문서` = 이 SK 문서(키 = SK 키 ·
|
||||
따로 — 수식 길(`M02_Doc_Router`)로 문서를 고침: 저장한 문서가 정본 · 옛 칸 고침 저장 = 409 ·
|
||||
[재생성] = 지금 M01 로 다시 찍어 파생으로 돌아감(고친 문서 버림).
|
||||
SK 금액(기준 · 1 단위 = 기준 ÷ 배) = 문서 셈 — 일위대가 · 사전조합 · 구조물 조합이 이 값을 씀.
|
||||
가져온 식 SK(엑셀 · 바탕 null) = 옛 길 그대로(802-4b) · 바탕 참조 SK = 옛 길(값 사본 없음).
|
||||
가져온 식 SK(802-4b-1) = `M02_Doc_Import` 문서(파생 = 가져온 줄 + SK 고침에서 지음 · 따로 =
|
||||
저장본) · 셈 = 단가기준(엑셀 = 엑셀 값 · 우리 = 연결 값) · 바탕 참조 SK = 옛 길(값 사본 없음).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -21,6 +22,7 @@ from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
|
||||
@@ -35,8 +37,8 @@ SAVING: ContextVar[dict | None] = ContextVar("수식길저장", default=None) #
|
||||
|
||||
|
||||
def own(doc: Any) -> bool:
|
||||
"""수식 사본을 가질 SK — 가져온 식 · 바탕 참조 SK 빼고."""
|
||||
if not isinstance(doc, dict) or doc.get("가져온"):
|
||||
"""수식 사본을 가질 SK — 바탕 참조 SK 빼고(가져온 식 = 가져온 줄 문서 · 802-4b-1)."""
|
||||
if not isinstance(doc, dict) or (doc.get("가져온") and not dim.is_imported(doc)):
|
||||
return False
|
||||
return "참조" not in (doc.get("바탕") or {})
|
||||
|
||||
@@ -50,8 +52,11 @@ def _strip(v: Any) -> Any:
|
||||
|
||||
|
||||
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, {}, [])}))
|
||||
"""옛 칸 지문 — 따로 문서 위 옛 칸 고침 막음(가져온 식 = 가져온 줄 · 고른 칸 · 이음도)."""
|
||||
got = {k: doc.get(k) for k in OLD_FIELDS if doc.get(k) not in (None, {}, [])}
|
||||
if dim.is_imported(doc):
|
||||
got["가져온"] = {k: doc["가져온"].get(k) for k in ("로직", "연결")}
|
||||
return dc.fp(_strip(got))
|
||||
|
||||
|
||||
# ── 파생 — 엔진에 넣는 고친 줄 → 문서 ─────────────────────────────────
|
||||
@@ -90,7 +95,10 @@ def derive(sk: dict, ctx: tuple) -> dict:
|
||||
|
||||
|
||||
def seed(sk: dict) -> dict | None:
|
||||
"""수식 사본 없는 SK(옮기기 전 · 새로)의 씨 — 로직 사본 값 + 빈 부름 · 빈 SK = 빈 문서."""
|
||||
"""수식 사본 없는 SK(옮기기 전 · 새로)의 씨 — 로직 사본 값 + 빈 부름 · 빈 SK = 빈 문서 ·
|
||||
가져온 식 = 빈 부름(문서는 셈마다 지음)."""
|
||||
if dim.is_imported(sk):
|
||||
return {"부름": {}}
|
||||
if skc.has(sk):
|
||||
return {**dc.values_of(sk[skc.KEY]), "부름": {}}
|
||||
if sk.get("바탕") or sk.get("더한줄"):
|
||||
@@ -109,6 +117,13 @@ def copy_of(sk: dict, ctx: tuple | None) -> dict | None:
|
||||
return None
|
||||
if got.get("따로") or ctx is None:
|
||||
return got
|
||||
if dim.is_imported(sk): # 가져온 식 — 엑셀 단가 기준 줄에서 지음(우리 = 한 번 더 돎)
|
||||
try:
|
||||
same = dim.basis_of(sk) == "엑셀"
|
||||
root = dim.derive(sk, ctx if same else None, dc.base(ctx[0]))
|
||||
except dc._ERR as e:
|
||||
root = {**dc.md.blank(str(sk.get("키"))), "_틀림": str(e)}
|
||||
return {**got, "문서": root, "부름": {}}
|
||||
try:
|
||||
root = derive(sk, ctx)
|
||||
except dc._ERR as e:
|
||||
@@ -127,7 +142,7 @@ def on_run(sk: dict, ans: dict, ctx: tuple | None) -> dict:
|
||||
copy = copy_of(sk, ctx)
|
||||
if copy is None:
|
||||
return ans
|
||||
got = dc.run(copy["문서"], copy)
|
||||
got = run(sk, copy, ctx[0] if ctx else None)
|
||||
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))
|
||||
@@ -138,6 +153,13 @@ def on_run(sk: dict, ans: dict, ctx: tuple | None) -> dict:
|
||||
return {**ans, "기준": base, "1단위": unit, "수식셈": mark}
|
||||
|
||||
|
||||
def run(sk: dict, copy: dict, master: Any = None) -> dict:
|
||||
"""문서 셈 — 가져온 식 = 단가기준 · 형제 SK(`dim.run`) · 그 밖 = 값 사본(`dc.run`)."""
|
||||
if dim.is_imported(sk):
|
||||
return dim.run(copy["문서"], sk, dc.base(master) if master is not None else None)
|
||||
return dc.run(copy["문서"], copy)
|
||||
|
||||
|
||||
# ── 저장 첫머리(옛 길 `clean_doc` 끝) ──────────────────────────────────
|
||||
def stored(key: Any) -> dict | None:
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
@@ -162,7 +184,9 @@ def settle(out: dict, whole: Any, job: Any, raw: Any) -> dict:
|
||||
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 장)
|
||||
if not skc.has(out) and not dim.is_imported(
|
||||
out
|
||||
): # 빈 SK = 빈 문서 · 금액 「—」(요구서_664 0 장)
|
||||
empty = seed(out)
|
||||
return {**out, KEY: norm(empty)} if empty else out
|
||||
raw = raw if isinstance(raw, dict) else {}
|
||||
@@ -177,11 +201,11 @@ def settle(out: dict, whole: Any, job: Any, raw: Any) -> dict:
|
||||
"수식 문서로 고친 단가산출 — 옛 칸(덮개 · 줄) 고침은 수식 탭에서 · 또는 [재생성]",
|
||||
409,
|
||||
)
|
||||
copy = dc.refresh(have)[0] if job == skc.PRICE else have
|
||||
copy = dc.refresh(have)[0] if job == skc.PRICE and not dim.is_imported(out) 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}
|
||||
first = {**(dc.values_of(out[skc.KEY]) if skc.has(out) else {}), "부름": 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 != "따로"})}
|
||||
|
||||
@@ -7,7 +7,8 @@ M01 수식 문서 길과 같은 꼴: 읽기 {문서, 셈} · 미리 셈 {문서}
|
||||
저장 = 그 SK 저장(`store.write` — 층 쓰기 권한 · 판 409 · 옛 칸 · 로직 사본 그대로) ·
|
||||
문서 고침 = 따로(이 문서가 정본) · 재생성 = 지금 M01 로 다시 찍어 파생으로(고친 문서 · 끊음 버림) ·
|
||||
단가갱신 = 값 줄만 지금 M01(문서 그대로).
|
||||
가져온 식 SK(엑셀) = 802-4b 몫(400) · 바탕 참조 SK = 값 사본 없음(400).
|
||||
가져온 식 SK(802-4b-1) = 가져온 줄 문서 · 셈 = 단가기준(엑셀 값 · 연결 값) · 부름 · 끊음 없음
|
||||
(형제 SK 는 셈 때 읽음) · 단가갱신 = 문서 그대로 · 바탕 참조 SK = 값 사본 없음(400).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -16,6 +17,7 @@ import json
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
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
|
||||
@@ -53,8 +55,6 @@ def current(doc: dict, job: Any = None) -> dict:
|
||||
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 — 저장 때 지을 사본을 미리
|
||||
@@ -71,11 +71,12 @@ def current(doc: dict, job: Any = None) -> dict:
|
||||
return got if got is not None else clean[dsk.KEY]
|
||||
|
||||
|
||||
def answer(copy: dict) -> dict[str, Any]:
|
||||
"""{문서, 부름, 끊음, 셈, 사본 표지} — 글 · 수는 JSON 꼴."""
|
||||
got = dc.run(copy["문서"], copy)
|
||||
def answer(copy: dict, sk: dict) -> dict[str, Any]:
|
||||
"""{문서, 부름, 끊음, 셈, 사본 표지} — 글 · 수는 JSON 꼴 · 가져온 식 표지 = 단가기준."""
|
||||
got = dsk.run(sk, copy)
|
||||
head = {k: copy.get(k) for k in ("시점", "단가시점")}
|
||||
mark = {**head, "따로": bool(copy.get("따로")), **dc.state(copy)}
|
||||
mark = {**head, "따로": bool(copy.get("따로"))}
|
||||
mark |= {"단가기준": dim.basis_of(sk)} if dim.is_imported(sk) else dc.state(copy)
|
||||
body = {
|
||||
"문서": copy["문서"],
|
||||
"부름": copy.get("부름") or {},
|
||||
@@ -89,7 +90,7 @@ def answer(copy: dict) -> dict[str, Any]:
|
||||
def read(key: str) -> dict[str, Any]:
|
||||
"""읽기 — {문서, 부름, 끊음, 셈, 사본, 판}."""
|
||||
got = _file(key)
|
||||
return {**answer(current(got["문서"])), "판": got["판"]}
|
||||
return {**answer(current(got["문서"]), got["문서"]), "판": got["판"]}
|
||||
|
||||
|
||||
# ── 몸 검사 ───────────────────────────────────────────────────────────
|
||||
@@ -104,8 +105,8 @@ def _doc(raw: Any, key: str, where: str) -> dict:
|
||||
return {**raw, "키": key}
|
||||
|
||||
|
||||
def edited(copy: dict, body: dict, key: str) -> dict:
|
||||
"""화면 고침 얹은 사본 — 문서 · 끊음 글만 받음(값 · 부름은 서버 것)."""
|
||||
def edited(copy: dict, body: dict, key: str, sk: dict | None = None) -> dict:
|
||||
"""화면 고침 얹은 사본 — 문서 · 끊음 글만 받음(값 · 부름은 서버 것 · 가져온 식 = 부름 없음)."""
|
||||
if body.get("할일") not in (None, "", *JOBS):
|
||||
raise _bad("할일은 단가갱신 · 재생성")
|
||||
if body.get("할일") == skc.REGEN:
|
||||
@@ -125,8 +126,8 @@ def edited(copy: dict, body: dict, key: str) -> dict:
|
||||
*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("부름"))
|
||||
if not dim.is_imported(sk):
|
||||
out["부름"] = dc.closure(dc.over(out), calls, copy.get("부름"))
|
||||
out["따로"] = True
|
||||
return out
|
||||
|
||||
@@ -136,10 +137,10 @@ 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:
|
||||
after = edited(current(doc, job) if job else before, body, key, doc)
|
||||
out = answer(after, doc)
|
||||
out |= {"전": dc.money(dsk.run(doc, before)), "후": dc.money(out["셈"])}
|
||||
if job == skc.PRICE and not dim.is_imported(doc):
|
||||
out["바뀐자원"] = dc.refresh(before)[1]
|
||||
if job == skc.REGEN:
|
||||
out["버림"] = {
|
||||
@@ -158,7 +159,7 @@ def save(key: str, body: dict) -> dict[str, Any]:
|
||||
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)
|
||||
mine = edited(current(got["문서"], job), body, key, got["문서"])
|
||||
token = dsk.SAVING.set({"키": key, "사본": mine})
|
||||
try:
|
||||
store.write(sd.KIND, key, got["판"], raw)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/* =============================================================================
|
||||
* common_util_formula_doc_import.ts
|
||||
* 가져온 식(엑셀 불러오기) 수식 문서 단가기준 보기(PLAN 802-4b-1 · 요구 664)
|
||||
* [엑셀 단가 그대로](기본) = 연결 칸 `엑셀`(엑셀 값 기억)이 있는 변수를 숫자 연결로 보고 셈 ·
|
||||
* [우리 단가로] = 문서 그대로(연결 값) · 형제 단가산출(SK-…) 연결은 늘 연결(부르는 쪽 단가기준)
|
||||
* 파이썬 · TS 짝 — 짝 = M02_MasterTemplete/M02_Doc_Import.py `view` · 거울 시험
|
||||
* test_802_4b1_import_doc.py(고칠 때 둘을 같이 고침) · 화면 · DOM · fetch 없음
|
||||
* ========================================================================== */
|
||||
|
||||
import type { DocVar, FormulaDoc, Link } from "./common_util_formula_doc";
|
||||
|
||||
export const EXCEL = "엑셀";
|
||||
export type Basis = "엑셀" | "우리";
|
||||
|
||||
/** 단가기준 보기 — 엑셀 = 엑셀 값 기억 연결(형제 SK 빼고)을 숫자로 · 우리 = 그대로(새 문서) */
|
||||
export function importView(doc: FormulaDoc, basis: Basis): FormulaDoc {
|
||||
if (basis === "우리") return doc;
|
||||
const vars = (doc.변수 ?? []).map((v: DocVar): DocVar => {
|
||||
const link = (v.연결 ?? {}) as Link & Record<string, unknown>;
|
||||
if (!(EXCEL in link) || String(link.키 ?? "").startsWith("SK-")) return v;
|
||||
return { ...v, 연결: { 갈래: "숫자", 값: link[EXCEL] } };
|
||||
});
|
||||
return { ...doc, 변수: vars };
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""802-4b-1 엑셀 불러오기 → 수식 문서(요구 664 · 665 · 요구서_664 0 장) — 임시 storage · M01 그대로.
|
||||
|
||||
① 가져온 식 SK = 수식 문서(손 값 = 숫자 · 이음 = 연결 + 엑셀 값 기억 · 중간 = 식 줄 ·
|
||||
얼린 변수 = 숫자 · 금액 줄 = 노무비 · 재료비 · 경비 · 단계 · 끝전 · 머리 `가져온` 표지)
|
||||
② [엑셀 단가 그대로] = 숫자 값 셈 = 엑셀 · [우리 단가로] = 연결 값 셈(기계 = 시간당 그 비목)
|
||||
③ 산근 속 산근 = 형제 SK 단가산출 연결(안쪽이 바뀌면 따라감 · 부르는 쪽 단가기준)
|
||||
④ 소요량 · 계수 고른 칸 = 그 수 자리에 표 연결 기호(금액 그대로)
|
||||
⑤ SK 금액 = 문서 셈 = 옛 엔진 셈 · 수식 길 읽기 · 고침(따로) · 옛 칸 고침 409 · 옮기기 대조
|
||||
실무 엑셀 없으면 그 시험만 skip.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import subprocess
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import helper_m02_hopyo_golden as golden
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
from M02_MasterTemplete import M02_Doc_Migrate as mig
|
||||
from M02_MasterTemplete import M02_Doc_Sk as dsk
|
||||
from M02_MasterTemplete import M02_Doc_Sk_Path as path
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as machine
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
from M02_MasterTemplete import M02_Sangeun_QtyLinks as qty
|
||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||
|
||||
KEY = "HP-000001"
|
||||
|
||||
|
||||
def _doc(items: list[dict], extra: dict | None = None, src: dict | None = None) -> dict:
|
||||
row = {"이름": "견본 sub2임시", "결과단위": "원/식", "호표": items, **(extra or {})}
|
||||
meta = {"로직": row, "단가기준": "엑셀", "원본": {"엑셀금액": {}, **(src or {})}}
|
||||
return {"키": "SK-000001", "이름": "견본", "단위": "식", "바탕": None, imp.KEY: meta}
|
||||
|
||||
|
||||
def _item(kind: str, name: str, price: float, part: str, link: str | None = None, qty_="1"):
|
||||
out = {"종류": kind, "이름": name, "단위": "식", "수량": qty_, "비목": part, "단가": price}
|
||||
return out | ({"이음": link} if link else {})
|
||||
|
||||
|
||||
def _ours(doc: dict) -> dict:
|
||||
return {**doc, imp.KEY: {**doc[imp.KEY], "단가기준": "우리"}}
|
||||
|
||||
|
||||
def _var(doc: dict, part: str) -> dict:
|
||||
"""금액 줄 `이름` 의 단가 기호 변수."""
|
||||
line = next(x for x in doc["금액"] if x["이름"] == part)
|
||||
sym = str(line["단가"]).removesuffix(".계")
|
||||
return next((v for v in doc["변수"] if dim.dc.md.var_id(v) == sym), {})
|
||||
|
||||
|
||||
def _eq(good: bool) -> str:
|
||||
for row in m01.loaded()[1].numbers["EQ"].values():
|
||||
if bool(machine.hourly(row["키"]).get("ok")) is good:
|
||||
return row["키"]
|
||||
pytest.skip("EQ 견본 없음")
|
||||
|
||||
|
||||
# ── ① ② 꼴 · 두 단가기준 ──────────────────────────────────────────────────
|
||||
def test_doc_form_numbers_links_and_both_bases() -> None:
|
||||
eq = _eq(True)
|
||||
items = [
|
||||
_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5"),
|
||||
_item("인력", "작업반장", 150000, "노무비", None, "0.1"),
|
||||
_item("기계", "굴착기", 30000, "경비", eq, "2"),
|
||||
]
|
||||
mids = {"중간": [{"이름": "Qd", "식": "600"}, {"이름": "Q", "식": "Qd / 8"}]}
|
||||
marks = ["참고 · 금액 줄 못 읽음 · 보인 값 얼림", "추정 · 단가 후보 여럿"]
|
||||
doc = _doc(items, mids, {"표지": marks, "갈래": "EST", "파일": "견본.xls"})
|
||||
root = dim.derive(doc)
|
||||
assert root["꼴"] == "수식문서" and root["키"] == "SK-000001"
|
||||
assert root["가져온"]["표지"] == marks and root["가져온"]["갈래"] == "EST" # 까닭 그대로
|
||||
labor = _var(root, "보통인부")["연결"]
|
||||
assert labor == {"갈래": "요소", "키": "LB000002", "엑셀": Decimal(100000)}
|
||||
assert _var(root, "작업반장")["연결"] == {"갈래": "숫자", "값": Decimal(150000)} # 이음 없음
|
||||
mach = _var(root, "굴착기")["연결"]
|
||||
assert mach["갈래"] == "단가산출" and mach["칸"] == "경비" and mach["엑셀"] == Decimal(30000)
|
||||
qd = next(v for v in root["변수"] if v["열쇠"] == "Qd") # 얼린 변수 = 숫자 · 식 = 식 줄
|
||||
assert qd["연결"] == {"갈래": "숫자", "값": Decimal(600)}
|
||||
assert [x["식"] for x in root["식"]] == [f"{dim.dc.md.var_id(qd)} ÷ 8"]
|
||||
excel = dim.run(root, doc)
|
||||
assert excel["계"] == Decimal(50000 + 15000 + 60000) # 엑셀 단가 그대로
|
||||
ours = dim.run(root, _ours(doc))
|
||||
lb = Decimal(str(m01.loaded()[1].get("LB000002")["값"]))
|
||||
hour = machine.hourly(eq)["경비"]
|
||||
assert ours["노무비"] == int(lb * Decimal("0.5") * 10) / Decimal(10) + 15000
|
||||
assert float(ours["경비"]) == pytest.approx(hour * 2, abs=1) # 기계 = 시간당 그 비목
|
||||
|
||||
|
||||
def test_machine_without_hourly_keeps_number_with_note() -> None:
|
||||
eq = _eq(False)
|
||||
doc = _doc([_item("기계", "리퍼", 5000, "경비", eq)])
|
||||
var = _var(dim.derive(doc), "리퍼")
|
||||
assert var["연결"] == {"갈래": "숫자", "값": Decimal(5000)} and eq in var["설명"]
|
||||
assert dim.run(dim.derive(doc), _ours(doc))["계"] == 5000
|
||||
|
||||
|
||||
def test_sk_money_is_doc_calc_and_equals_old_engine(world) -> None:
|
||||
"""⑤ SK 금액 = 문서 셈(수식셈 표지) = 옛 엔진 셈 — 엑셀 · 우리 둘 다 · 덧줄 율 · 합밖."""
|
||||
items = [
|
||||
_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.33"),
|
||||
_item("재료", "경유", 1700, "재료비", "MO000002", "3") | {"합밖": True},
|
||||
]
|
||||
extra = {"덧줄": [{"이름": "공구손료", "식": "노무비 * 0.03", "비목": "경비"}]}
|
||||
doc = _doc(items, extra)
|
||||
for basis in ("엑셀", "우리"):
|
||||
mine = imp._with(doc, 단가기준=basis)
|
||||
new = imp._money(imp._run(mine, imp._whole()))
|
||||
token = dsk.OLD.set(True)
|
||||
try:
|
||||
old = imp._money(imp._run(mine, imp._whole()))
|
||||
finally:
|
||||
dsk.OLD.reset(token)
|
||||
assert new == old, basis
|
||||
got = imp.to_price(doc, "우리")
|
||||
assert got["전"]["계"] == 33000 + 990 and got["후"]["계"] != got["전"]["계"]
|
||||
|
||||
|
||||
# ── ③ 산근 속 산근 · 실무 STmate · EST ──────────────────────────────────────
|
||||
def _hp(world, tag: str):
|
||||
path_ = golden.FILES.get(tag)
|
||||
if path_ is None or not path_.exists():
|
||||
pytest.skip(f"실무 엑셀 없음 {tag}")
|
||||
folder = world["personal"]
|
||||
hs._dir(folder).mkdir(parents=True, exist_ok=True)
|
||||
(hs._dir(folder) / f"{KEY}.json").write_text(f'{{"키": "{KEY}", "판": {hs.BOOK}}}', "utf-8")
|
||||
fh.keep(folder, KEY, path_.name, path_.read_bytes())
|
||||
return folder
|
||||
|
||||
|
||||
def _read(key: str) -> dict:
|
||||
return store.read("sangeun", key)["문서"]
|
||||
|
||||
|
||||
def test_stmate_nested_is_sibling_link(world) -> None:
|
||||
folder = _hp(world, "울진대흥")
|
||||
with store.at(folder):
|
||||
made = fh.make(folder, KEY, 2200) # D02200 안에 D00075 콘크리트믹서사용
|
||||
doc = _read(made["이름"])
|
||||
root = dim.derive(doc)
|
||||
assert root["가져온"]["갈래"] == "STmate" and root["가져온"]["코드"] == "D02200"
|
||||
inner = _var(root, "콘크리트믹서사용")["연결"]
|
||||
assert inner["갈래"] == "단가산출" and inner["키"].startswith("SK-")
|
||||
assert inner["엑셀"]["노무비"] == 6216 # 엑셀 얼린 값 기억
|
||||
assert dim.run(root, doc)["계"] == 67383 == made["검산"]["엑셀"]["계"]
|
||||
assert dim.run(root, _ours(doc))["셈됨"] is True
|
||||
sub = _read(inner["키"]) # 안쪽 단가 줄 하나 두 배 → 바깥이 따라감(엑셀 기준도)
|
||||
one = next(x for x in sub[imp.KEY]["로직"]["호표"] if "단가" in x)
|
||||
one["단가"] = one["단가"] * 2
|
||||
store.write("sangeun", inner["키"], store.read("sangeun", inner["키"])["판"], sub)
|
||||
now = dim.run(dim.derive(_read(inner["키"])), _read(inner["키"]))["계"]
|
||||
assert now != 7528
|
||||
assert dim.run(root, doc)["계"] == pytest.approx(67383 - 7528 + now, abs=1)
|
||||
|
||||
|
||||
def test_est_by_part_and_frozen_vars(world) -> None:
|
||||
folder = _hp(world, "북부청임도")
|
||||
with store.at(folder):
|
||||
made = fh.make(folder, KEY, 67) # 단산68 을 노무비 · 재료비 · 경비 세 줄로 부름
|
||||
doc = _read(made["이름"])
|
||||
root = dim.derive(doc)
|
||||
parts = [
|
||||
(x["비목"], _var(root, x["이름"])["연결"].get("칸"))
|
||||
for x in root["금액"]
|
||||
if (_var(root, x["이름"]).get("연결") or {}).get("키", "").startswith("SK-")
|
||||
]
|
||||
assert parts == [("노무비", "노무비"), ("재료비", "재료비"), ("경비", "경비")]
|
||||
assert not any(x["식"].strip().replace(".", "").isdigit() for x in root["식"])
|
||||
excel = dim.run(root, doc)
|
||||
assert float(excel["계"]) == pytest.approx(float(made["검산"]["엑셀"]["계"]), abs=1)
|
||||
assert dim.run(root, _ours(doc))["셈됨"] is True
|
||||
|
||||
|
||||
# ── ④ 소요량 · 계수 고른 칸 ─────────────────────────────────────────────────
|
||||
def test_qty_pick_becomes_table_link(world) -> None:
|
||||
folder = _hp(world, "북부청임도")
|
||||
found = None
|
||||
for no in range(1, 200):
|
||||
try:
|
||||
out = fh.emit(folder, KEY, no)
|
||||
except store.StoreError:
|
||||
continue
|
||||
if "야면석채집및운반(계곡내)" in out["로직"]["이름"]:
|
||||
found = out
|
||||
break
|
||||
if found is None:
|
||||
pytest.skip("야면석채집및운반(계곡내) 블록 없음")
|
||||
row = found["로직"]
|
||||
doc = _doc(row["호표"], {k: row[k] for k in ("중간", "덧줄", "단계", "끝전") if row.get(k)})
|
||||
doc[imp.KEY]["로직"] |= {k: row[k] for k in ("이름", "결과단위")}
|
||||
at = next(i for i, x in enumerate(row["호표"]) if x["이름"] == "보통인부")
|
||||
cell = next(c for c in qty.view(doc)["줄"] if c["자리"] == f"호표:{at}:0")["후보"][0]
|
||||
kept = qty.pick(doc, f"호표:{at}:0", {k: cell[k] for k in ("표", "줄", "칸")})["문서"]
|
||||
root = dim.derive(kept)
|
||||
pick = next(v for v in root["변수"] if v["열쇠"].startswith(dim.PICK))
|
||||
assert pick["연결"]["갈래"] == "표" and pick["연결"]["표"] == cell["표"]
|
||||
assert pick["연결"]["칸"] == cell["칸"] and pick["연결"][dim.XL] == Decimal(str(cell["값"]))
|
||||
line = next(x for x in root["금액"] if x["이름"] == "보통인부")
|
||||
assert dim.dc.md.var_id(pick) in line["수량"]
|
||||
before = dim.run(dim.derive(doc), doc)["계"]
|
||||
assert dim.run(root, kept)["계"] == before # 엑셀 그대로 금액 같음
|
||||
assert dim.run(root, _ours(kept))["셈됨"] is True # 표 칸 = 같은 값
|
||||
|
||||
|
||||
# ── ⑤ 수식 길 · 옮기기 ───────────────────────────────────────────────────
|
||||
def test_formula_route_edit_then_old_edit_409_and_regen(world) -> None:
|
||||
folder = world["personal"]
|
||||
doc = _doc([_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5")])
|
||||
with store.at(folder):
|
||||
key = imp.create(doc[imp.KEY]["로직"], {"엑셀금액": {"계": 50000}}, "sub2임시 견본")["이름"]
|
||||
read = path.read(key)
|
||||
assert read["셈"]["계"] == 50000 and read["사본"]["단가기준"] == "엑셀"
|
||||
edit = copy.deepcopy(read["문서"])
|
||||
edit["금액"][0]["수량"] = "1"
|
||||
pre = path.preview(key, {"문서": edit})
|
||||
assert pre["전"]["계"] == 50000 and pre["후"]["계"] == 100000
|
||||
path.save(key, {"문서": edit, "판": read["판"]})
|
||||
again = path.read(key)
|
||||
assert again["사본"]["따로"] is True and again["셈"]["계"] == 100000
|
||||
sk = _read(key)
|
||||
moved = imp.link(sk, sd_id(sk), None)["문서"] # 이음 풂 = 옛 칸 고침 → 409
|
||||
with pytest.raises(store.StoreError) as why:
|
||||
store.write("sangeun", key, store.read("sangeun", key)["판"], moved)
|
||||
assert why.value.status == 409
|
||||
ours = imp.to_price(sk, "우리")["문서"] # 단가기준만 바꿈 = 됨(문서 그대로 · 연결 값)
|
||||
store.write("sangeun", key, store.read("sangeun", key)["판"], ours)
|
||||
lb = Decimal(str(m01.loaded()[1].get("LB000002")["값"]))
|
||||
assert path.read(key)["셈"]["노무비"] == int(lb * 10) / Decimal(10)
|
||||
|
||||
|
||||
def sd_id(sk: dict) -> str:
|
||||
return imp.sd.line_ids(imp.row_of(sk))[0]
|
||||
|
||||
|
||||
def test_migrate_imported_same(world) -> None:
|
||||
folder = world["personal"]
|
||||
doc = _doc([_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5")])
|
||||
with store.at(folder):
|
||||
key = imp.create(doc[imp.KEY]["로직"], {"엑셀금액": {"계": 50000}}, "sub2임시 옮김")["이름"]
|
||||
file = folder / "sangeun" / f"{key}.json"
|
||||
got = mig.check("sangeun", file)
|
||||
assert got["상태"] in ("같음", "있음")
|
||||
if got["상태"] == "같음":
|
||||
assert got["수식"]["문서"]["키"] == key and got["전"] == got["후"]
|
||||
|
||||
|
||||
# ── TS 짝(importView) 거울 ─────────────────────────────────────────────
|
||||
_NODE = """
|
||||
const fs = require("fs"), path = require("path");
|
||||
const ts = require(path.join(process.cwd(), "config", "node_modules", "typescript"));
|
||||
const at = path.join(process.cwd(), "common_util", "common_util_formula_doc_import.ts");
|
||||
const js = ts.transpileModule(fs.readFileSync(at, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, () => ({}));
|
||||
const got = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
process.stdout.write(JSON.stringify(got.map(([d, b]) => box.exports.importView(d, b))));
|
||||
"""
|
||||
|
||||
|
||||
def test_ts_view_mirror() -> None:
|
||||
eq = _eq(True)
|
||||
items = [
|
||||
_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5"),
|
||||
_item("인력", "작업반장", 150000, "노무비", None, "0.1"),
|
||||
_item("기계", "굴착기", 30000, "경비", eq, "2"),
|
||||
]
|
||||
root = dim.derive(_doc(items))
|
||||
root["변수"].append(
|
||||
{"열쇠": "S", "기호": "S", "연결": {"갈래": "단가산출", "키": "SK-000009", "엑셀": 5}}
|
||||
)
|
||||
plain = json.loads(json.dumps(root, ensure_ascii=False, default=float))
|
||||
cases = [[plain, b] for b in ("엑셀", "우리")]
|
||||
out = subprocess.run(
|
||||
["node", "-e", _NODE],
|
||||
input=json.dumps(cases, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
cwd=Path(__file__).resolve().parents[2],
|
||||
check=True,
|
||||
)
|
||||
ts_ = json.loads(out.stdout)
|
||||
py = [json.loads(json.dumps(dim.view(plain, b), ensure_ascii=False)) for b in ("엑셀", "우리")]
|
||||
assert ts_ == py
|
||||
assert _var(ts_[0], "보통인부")["연결"] == {"갈래": "숫자", "값": 100000.0}
|
||||
assert ts_[0]["변수"][-1]["연결"]["갈래"] == "단가산출" # 형제 SK = 늘 연결
|
||||
Reference in New Issue
Block a user