diff --git a/M02_MasterTemplete/M02_San_Emit.py b/M02_MasterTemplete/M02_San_Emit.py index 302dc9bea..1177dd9a0 100644 --- a/M02_MasterTemplete/M02_San_Emit.py +++ b/M02_MasterTemplete/M02_San_Emit.py @@ -1,21 +1,21 @@ """경쟁사 산근 → 우리 엔진 로직 줄 꼴 내보내기(PLAN 793-2a · 요구서_652 · eval 없음). -793-1 줄 나무(`M02_San_Stmate` · `M02_San_Est`) → 로직 JSON 한 개(`_틀.md` 7 장 GX 꼴 · -셈 = `master_formula.run` → `master_cut.engine`). - -내보낸 로직 꼴: -- 키 · 이름 · 결과단위 · 입력 [] · 끝전 · 조정 · 단계 [{이름, 단, 줄, 제외?, 원문, 검산}] -- 중간 [{이름, 식, 표시?, 원문}] — STmate = 식(같은 이름 다시 대입 = `이름_2`) · EST = 얼린 값 -- 호표 [{종류 로직, 요소 None, 이름, 단위, 수량, 값묶음, 자원, 번지?, 합밖?, 자리?, 원문, 검산}] - · 값묶음 = 「엑셀 단가 그대로」 · 자원 = 이음 열쇠 {꼴, 갈래, 코드?, 이름, 규격, 단위, - 단가 {L, M, S}, 비목?, 옮김?, M01?} - (M01 = 1차 줄 판정 `M02_Hopyo_Row.m01_picks` 유일 짝 · 나중 [우리 단가로]) -- 덧줄 [{이름, 식, 비목, 번지?, 합밖?, 자리?, 원문, 검산}] — 율 · 특수 % · 앞줄 참조 · 값 줄 -- 글줄 [{자리, 글, 행?, 표지?}] — 셈 없는 원본 글(자리 = 그 앞 줄 이름 · `mid:이름` · "" = 맨 위) -- 가져옴 {꼴, 코드, 검산 {노무비, 재료비, 경비, 계}, 계약?, 표지 [..]} -검산 = 엑셀 값(줄마다 · 블록) · 표지 = 못 옮긴 까닭(0 이 목표). -EST 는 블록 성분을 낸 줄만 합에 듦(나머지 줄 = 합밖) · -STmate 계약단가(낙찰률)는 B09 몫 — 가져옴.계약. +793-1 줄 나무(`M02_San_Stmate` · `M02_San_Est`) → `{로직, 원본}` — 꼴 = 793-2 가져온 식 SK +만들기 몸(`M02_Sangeun_Imported.py` 머리) · 로직 = `_틀.md` 7 장 꼴(셈 = `master_formula.run` → +`master_cut.engine` · `요소` 는 SK 서버가 채움). +- 로직 = {이름, 결과단위, 입력 [], 중간 [{이름, 식, 표시?}], 호표, + 덧줄 [{이름, 식, 비목, 번지?, 합밖?, 자리?}], 단계? [{이름, 단, 줄, 제외?}], 끝전, 조정?} · + STmate 변수 = 식(다시 대입 = `이름_2`) · EST 변수 = 얼린 값 · EST 는 블록 성분을 낸 줄만 합 + (나머지 = 합밖) +- 호표 줄 = {종류 인력|재료|기계, 이름, 규격, 단위, 수량, 비목, 단가(엑셀), 이음(M01 키 | null)} + (비목 하나 자원) | {종류 로직, 이름, 단위, 수량, 값묶음(엑셀), 이음 null}(기계 셋 · 산근 · 고정) · + 번지? · 합밖? · 자리? · 덧칸 `자원` {꼴, 갈래, 코드?, 종류?, 이름, 규격, 단위, 단가 {L, M, S}, + 비목?, 옮김?}(경쟁사 코드 · 엑셀 단가 원본 · `reprice`) · 이음 = 1차 줄 판정 + `M02_Hopyo_Row.m01_picks` 유일 짝(keys=True 일 때) +- 원본 = {파일, 시트, 행 [처음, 끝], 갈래 STmate|EST, 엑셀금액 {노무비, 재료비, 경비, 계}(1 단위 · + 계약 전), 글줄 [{행, 글, 자리?, 자리들?, 값?, 표지?}], 코드, 표지 [..], 계약? {낙찰률, 금액}} + · 글줄 자리 = 셈 줄 이름 | `mid:이름` | `단계:이름` · 값 = 그 줄 엑셀 금액(계) +표지 = 못 옮긴 까닭(0 이 목표) · STmate 계약단가(낙찰률)는 B09 몫. """ from __future__ import annotations @@ -148,14 +148,27 @@ def bundle(src: dict) -> dict[str, Decimal]: return out +def resource(name: str, unit: Any, qty: str, res: dict, link: str | None) -> dict: + """자원 → 호표 줄 — 비목 하나인 인력 · 재료 · 기계 = 단가 줄 · 그 밖 = 로직 값묶음 줄.""" + one = {"이름": name, "규격": res.get("규격") or "", "단위": unit or "", "수량": qty} + if res.get("비목") and res["갈래"] in ("인력", "재료", "기계"): + price = Decimal(str(res["단가"].get(res["비목"]) or 0)) + one |= {"종류": res["갈래"], "비목": COMP[res.get("옮김") or res["비목"]], "단가": price} + return {**one, "이음": link, "자원": res} + return {**one, "종류": "로직", "값묶음": bundle(res), "이음": None, "자원": res} + + def reprice(logic: dict, where: dict, price: dict[str, float]) -> int: - """단가 바꿈 — 자원이 where 칸과 같은 호표 줄 단가 {L, M, S} 고침 · 값묶음 다시 · 고친 줄 수.""" + """단가 바꿈 — where 칸이 같은 자원 단가 {L, M, S} 고침 · 줄 단가 · 값묶음 다시 · 고친 줄 수.""" n = 0 for item in logic.get("호표", []): src = item.get("자원") or {} if all(src.get(k) == v for k, v in where.items()): src["단가"] = {**src["단가"], **price} - item["값묶음"] = bundle(src) + if "단가" in item: + item["단가"] = Decimal(str(src["단가"].get(src["비목"]) or 0)) + else: + item["값묶음"] = bundle(src) n += 1 return n @@ -173,46 +186,52 @@ def _m01(kind: str, src: dict) -> str | None: # ── 내보낼 로직 틀 ───────────────────────────────────────────────────── class Out: - def __init__(self, key: str, name: str, unit: str, form: str, code: Any) -> None: + def __init__(self, name: str, unit: str, form: str, code: Any) -> None: self.names = Names() self.mid: list[dict] = [] self.items: list[dict] = [] self.adds: list[dict] = [] self.steps: list[dict] = [] - self.texts: list[dict] = [] + self.texts: list[dict] = [] # 원본 글줄(원문 차례) + self.at: dict[Any, dict] = {} # 행 → 글줄 self.marks: list[str] = [] - self.step_src: dict[str, dict] = {} # 단계 이름 → {원문, 검산}(엔진 단계 칸 밖) - self.last = "" # 글줄 자리 - self.row = {"키": key, "이름": name or key, "결과단위": f"원/{unit or '식'}", "입력": []} - self.head = {"꼴": form, "코드": code} + self.row = {"이름": name or str(code), "결과단위": f"원/{unit or '식'}", "입력": []} + self.head = {"갈래": form, "코드": code} - def text_line(self, glyph: str, at: Any = None, mark: str | None = None) -> None: - where = "행" if self.head["꼴"] == "STmate" else "차례" - one = {"자리": self.last, "글": glyph} | ({where: at} if at is not None else {}) - self.texts.append(one | ({"표지": mark} if mark else {})) + def glyph(self, at: Any, glyph: str, where: str | None = None, value: Any = None, mark=None): + """원본 글줄 한 줄(같은 행이면 자리만 더함).""" + one = self.at.get(at) + if one is None: + one = {"행": at, "글": glyph} + self.texts.append(one) + self.at[at] = one + if where and "자리" in one: + one.setdefault("자리들", [one["자리"]]).append(where) + elif where: + one["자리"] = where + if value is not None: + one["값"] = value if mark: + one["표지"] = mark self.marks.append(f"{at}: {mark}") - def middle(self, name: str, formula: str, show: int | None, source: dict) -> None: + def middle(self, name: str, formula: str, show: int | None, at: Any, glyph: str) -> None: self.mid.append({"이름": name, "식": formula, **({"표시": show} if show else {})}) - self.mid[-1]["원문"] = source - self.last = f"mid:{name}" + self.glyph(at, glyph, f"mid:{name}") - def add_line(self, one: dict, item: bool) -> dict: + def add_line(self, one: dict, item: bool, at: Any, glyph: str, value: Any) -> dict: (self.items if item else self.adds).append(one) - self.last = one["이름"] + self.glyph(at, glyph, one["이름"], value) return one - def done(self, end: str, adjust: dict | None, check: dict, **more: Any) -> dict: - out = {**self.row, "중간": self.mid, "호표": self.items, "덧줄": self.adds} - if self.steps: - out["단계"] = self.steps - out |= {"끝전": end, **({"조정": adjust} if adjust else {})} - out |= {"글줄": self.texts, "가져옴": {**self.head, "검산": check, "표지": self.marks}} - if self.step_src: - out["가져옴"]["단계"] = self.step_src - out["가져옴"] |= more - return out + def done(self, end: str, adjust: dict | None, check: dict, sheet: str, file: str, **more): + logic = {**self.row, "중간": self.mid, "호표": self.items, "덧줄": self.adds} + logic |= {"단계": self.steps} if self.steps else {} + logic |= {"끝전": end, **({"조정": adjust} if adjust else {})} + rows = [x["행"] for x in self.texts if isinstance(x["행"], int)] or [0] + source = {"파일": file, "시트": sheet, "행": [min(rows), max(rows)], **self.head} + source |= {"엑셀금액": check, "글줄": self.texts, "표지": self.marks, **more} + return {"로직": logic, "원본": source} def _money(d: dict | None) -> dict[str, Any]: @@ -245,10 +264,6 @@ class _Stm: self.out: dict[str, int] = {} # 제외 단계 줄 → 그 계 줄까지 줄 수(합에서 빠지는 때) self.fix: list[tuple[dict, int]] = [] # 누계 % 덧줄 · 그때 줄 수 - def xl(self, at: Any) -> dict: - ln = self.rows.get(at) or {} - return {c: ln.get(k) or 0 for c, k in (("L", "D"), ("M", "E"), ("S", "F"))} - def brackets(self, node: tuple, src: dict, kept: dict) -> None: """끼움 `[Q=0.208]` → 중간(왼쪽부터 · 원문 차례).""" kind = node[0] @@ -259,7 +274,7 @@ class _Stm: eng = self.o.names.line(node[1].lower()) self.names[node[1].lower()] = eng kept[id(node)] = eng - self.o.middle(eng, body, None, src) + self.o.middle(eng, body, None, src["행"], src["글"]) elif kind == "셈": self.brackets(node[2], src, kept) self.brackets(node[3], src, kept) @@ -288,14 +303,14 @@ class _Stm: except (ex.ExprError, EmitError) as error: if part["이름"] is None: continue - self.o.text_line(node["글"], src["행"], f"셈 못 함 — {error}") + self.o.glyph(src["행"], src["글"], mark=f"셈 못 함 — {error}") return if part["이름"] is None: continue self.V[part["이름"]] = ex.rnd(v, 2) if part["찍기"] else v eng = self.o.names.line(part["이름"]) self.names[part["이름"]] = eng - self.o.middle(eng, body, 2 if part["찍기"] else None, src) + self.o.middle(eng, body, 2 if part["찍기"] else None, src["행"], src["글"]) def total(self, node: dict, src: dict) -> None: o = self.o @@ -308,27 +323,26 @@ class _Stm: self.open = keep if node["제외"] else [*keep, (node["단"], mine)] for n in mine if node["제외"] else []: self.out[n] = len(self.order) - o.step_src[name] = {"원문": src, "검산": _money(self.xl(src["행"]))} + o.glyph(src["행"], src["글"], f"단계:{name}", self.rows.get(src["행"], {}).get("C")) o.steps.append(step) self.pending = [] if node["누계번지"]: self.tsum[node["누계번지"]] = name self.last_step = name - o.last = f"단계:{name}" - def line(self, one: dict, node: dict, item: bool) -> None: + def line(self, one: dict, node: dict, item: bool, src: dict) -> None: if node.get("번지") and node["번지"].startswith("A"): one["번지"] = node["번지"] self.addr.add(node["번지"]) money = one.get("값묶음") or {} - comps = {c for c in COMP if money.get(COMP[c])} if item else {_CODE[one["비목"]]} + comps = {c for c in COMP if money.get(COMP[c])} if "값묶음" in one else {_CODE[one["비목"]]} self.calls[one["이름"]] = (f"줄.'{one['이름']}'" if item else one["이름"], comps) if node.get("빼기"): one["합밖"] = True else: self.pending.append(one["이름"]) self.order.append(one["이름"]) - self.o.add_line(one, item) + self.o.add_line(one, item, src["행"], src["글"], self.rows.get(src["행"], {}).get("C")) def ref(self, node: dict, src: dict, cf: str) -> None: o, code = self.o, node["자원"]["코드"] @@ -336,7 +350,6 @@ class _Stm: unt = (pr.get("meta") or {}).get("UNT") one_, to = node["자원"]["비목"], node["자원"]["옮김"] name = _label(node) or str(pr.get("이름") or code) - xl = self.xl(src["행"]) if unt in ev.SPECIAL: # 특수 % — 처음부터 비목 누계 · 앞 계표시 dst = to or {"L": "L", "M": "M", "S": "S"}.get(code[0]) if unt == "T%": @@ -346,18 +359,18 @@ class _Stm: else: formula = f"{_SUM.format(unt[0])} * ({cf}) / 100" one = {"이름": o.names.line(name), "식": formula, "비목": COMP[dst]} - one |= {"원문": src, "검산": {COMP[dst]: xl[dst]}} self.fix.append((one, len(self.order))) - return self.line(one, node, False) + return self.line(one, node, False, src) res = {"꼴": "STmate", "갈래": KIND.get(code[0], "고정"), "코드": code} res |= {k: pr.get(k) for k in ("이름", "규격", "단위")} res |= {"단가": {c: pr.get(c) or 0 for c in COMP}} + sole = [c for c in COMP if res["단가"][c]] + if not one_ and len(sole) == 1: # 비목 꼬리 없어도 값 든 비목 하나면 단가 줄(셈 같음) + one_, to = sole[0], None res |= {"비목": one_, "옮김": to} if one_ else {} - if self.keys and (m01 := _m01(res["갈래"], res)): - res["M01"] = m01 - one = {"종류": "로직", "요소": None, "이름": o.names.item(name)} - one |= {"단위": pr.get("단위") or "", "수량": cf, "값묶음": bundle(res), "자원": res} - self.line(one | {"원문": src, "검산": _money(xl)}, node, True) + link = _m01(res["갈래"], res) if self.keys else None + one = resource(o.names.item(name), pr.get("단위"), cf, res, link) + self.line(one, node, True, src) def rate(self, node: dict, src: dict, cf: str) -> None: rate, o = node["율"], self.o @@ -389,14 +402,12 @@ class _Stm: else: parts = [(c, base(c)) for c in COMP] label = _label(node) or "율" - xl = self.xl(src["행"]) for c, b in parts: nm = o.names.line(label if len(parts) == 1 else f"{label}_{COMP[c]}") one = {"이름": nm, "식": f"({b}) * {pct}", "비목": COMP[c]} - one |= {"원문": src, "검산": {COMP[c]: xl[c]}} if "@합" in b: self.fix.append((one, len(self.order))) - self.line(one, node, False) + self.line(one, node, False, src) def finish(self) -> None: """누계 % 밑 = 그 줄 앞까지 합 — 엔진 비목 합(호표 다 · 앞 덧줄 · 제외 단계 뺌)에서 @@ -424,18 +435,20 @@ def _cols(row: dict) -> dict[str, Any]: return _money({"L": row["D"], "M": row["E"], "S": row["F"]}) | {"계": row["C"] or 0} -def stmate(block: dict, nodes: list[dict], prices: dict, rule: str, key: str, keys: bool = False): - """STmate 블록(`M02_San_Book.stmate_blocks` 한 개) + 줄 나무 + 같은 책 단가 → 로직 JSON. - rule = 블록 끝전(비목버림 | 합버림_큰나머지 · 파일 설정) · keys = 자원 M01 키 찾기.""" +def stmate( + block: dict, nodes: list[dict], prices: dict, rule: str, keys: bool = False, file: str = "" +) -> dict: + """STmate 블록(`M02_San_Book.stmate_blocks` 한 개) + 줄 나무 + 같은 책 단가 → {로직, 원본}. + rule = 블록 끝전(비목버림 | 합버림_큰나머지 · 파일 설정) · keys = 자원 M01 키(이음) 찾기.""" me = prices.get(block["코드"]) or {} meta = me.get("meta") or {} - o = Out(key, block.get("이름"), me.get("단위"), "STmate", block["코드"]) + o = Out(block.get("이름"), me.get("단위"), "STmate", block["코드"]) st = _Stm(o, block, prices, keys) for node in nodes: kind, src = node["종류"], {"행": node.get("행"), "글": node.get("글") or ""} if kind in ("빈", "글"): if src["글"].strip(): - o.text_line(src["글"], src["행"]) + o.glyph(src["행"], src["글"]) elif kind == "대입": st.assign(node, src) elif kind == "계": @@ -450,7 +463,7 @@ def stmate(block: dict, nodes: list[dict], prices: dict, rule: str, key: str, ke cf = st.coef(node["식"], src) (st.rate if kind == "율" else st.ref)(node, src, cf) except (ex.ExprError, EmitError) as error: - o.text_line(src["글"], src["행"], f"셈 못 함 — {error}") + o.glyph(src["행"], src["글"], mark=f"셈 못 함 — {error}") try: st.finish() except EmitError as error: @@ -464,14 +477,14 @@ def stmate(block: dict, nodes: list[dict], prices: dict, rule: str, key: str, ke if meta.get("UC") == "T" and meta.get("NAG"): # 계약단가 = B09 몫(엔진 밖) more["계약"] = {"낙찰률": float(meta["NAG"]), "금액": _cols(block["머리"])} end = "큰나머지" if rule.startswith("합버림") else "비목버림" - return o.done(end, adjust, _cols(kye), **more) + return o.done(end, adjust, _cols(kye), "단가산출근거", file, **more) # ── EST ──────────────────────────────────────────────────────────────── _VAR = re.compile(r"([A-Za-z][A-Za-z0-9_]*)\s*(?:\([^)]*\))?\s*=") -def _est_res(role: dict, value: float, comp: str | None, keys: bool) -> dict: +def _est_res(role: dict, value: float, comp: str | None) -> dict: """EST 단가 토큰 정체 → 자원 이음 열쇠(옮김 = 성분 비목이 단가 비목과 다를 때).""" p = role["정체"] pc = p["비목"] if p["비목"] in COMP else (comp or "S") @@ -479,19 +492,16 @@ def _est_res(role: dict, value: float, comp: str | None, keys: bool) -> dict: res |= {"이름": p["이름"], "규격": p["규격"], "단위": None} res |= {"단가": {pc: value}} res |= {"비목": pc, "옮김": comp if comp and comp != pc else None, "열쇠비목": p["비목"]} - if role.get("모호"): - res["모호"] = True - if keys and (m01 := _m01(res["갈래"], res)): - res["M01"] = m01 - return res + return res | ({"모호": True} if role.get("모호") else {}) -def est(block: dict, lines: list[dict], want: dict, key: str, keys: bool = False) -> dict: +def est(block: dict, lines: list[dict], want: dict, keys: bool = False, file: str = "") -> dict: """EST 블록(`M02_San_Book.est_blocks` 한 개) + 복원 줄(`M02_San_Est.parse_block`) + - 총괄표 성분 want {T, L, M, S} → 로직 JSON · 성분을 낸 줄만 합(나머지 = 합밖).""" + 총괄표 성분 want {T, L, M, S} → {로직, 원본} · 성분을 낸 줄만 합(나머지 = 합밖).""" des = ev.designate(lines, want) mine = {k: c for c, k in des.items() if isinstance(k, int)} - o = Out(key, block.get("이름"), block.get("단위"), "EST", block.get("번")) + o = Out(block.get("이름"), block.get("단위"), "EST", block.get("번")) + rows = block.get("글행") or [] for c, k in des.items(): if k is None: o.marks.append(f"성분 {COMP[c]} {want[c]} 을 낸 줄 없음") @@ -499,32 +509,32 @@ def est(block: dict, lines: list[dict], want: dict, key: str, keys: bool = False ref: dict[int, str] = {} # 차례 → 엔진 부름 글(앞줄 · 숨은 합) for L in lines: k, form, kind = L["차례"], L["꼴"], L.get("종류") - src = {"차례": k, "글": L["원글"]} + at, glyph = rows[k] if k < len(rows) else k, L["원글"] comp = mine.get(k) if form == "빈": continue if form == "변수" and L.get("값") is not None: m = _VAR.search(L["원글"]) eng = o.names.line(m.group(1) if m else "변수") - o.middle(eng, num(L["값"]), None, src) # 얼린 값(금액엔 보인 값이 박힘) + o.middle(eng, num(L["값"]), None, at, glyph) # 얼린 값(금액엔 보인 값이 박힘) continue if form not in ("돈", "값"): - o.text_line(L["원글"], k) + o.glyph(at, glyph) continue - head = {"원문": src, "검산": {COMP[comp or L.get("비목") or "S"]: L["값"]}} - head |= {} if comp else {"합밖": True} + head = {} if comp else {"합밖": True} try: place = _place(L.get("규칙")) if k not in val: raise EmitError("보인 값이 되는 식 · 자리 못 찾음 — 보인 값으로 얼림") if kind == "단가줄": - at, role = next((i, r) for i, r in L["역할"].items() if r["갈래"] == "단가") - price = next(n[1] for n in ex.numbers(L["식"]) if n[2] == at) - res = _est_res(role, price, comp, keys) - one = {"종류": "로직", "요소": None, "이름": o.names.item(L.get("라벨") or "")} - one |= {"단위": "", "수량": text(L["식"], {}, {at: "1"}), "값묶음": bundle(res)} - one |= {"자원": res, **head, **({"자리": place} if place else {})} - o.add_line(one, True) + i, role = next((i, r) for i, r in L["역할"].items() if r["갈래"] == "단가") + price = next(n[1] for n in ex.numbers(L["식"]) if n[2] == i) + res = _est_res(role, price, comp) + link = _m01(res["갈래"], res) if keys else None + qty = text(L["식"], {}, {i: "1"}) + one = resource(o.names.item(L.get("라벨") or ""), None, qty, res, link) + one |= {**head, **({"자리": place} if place else {})} + o.add_line(one, True, at, glyph, L["값"]) ref[k] = f"줄.'{one['이름']}'" continue if kind == "값줄": @@ -534,12 +544,12 @@ def est(block: dict, lines: list[dict], want: dict, key: str, keys: bool = False swap = {i: ref[r["줄"]] for i, r in L["역할"].items() if r.get("줄") in ref} formula = text(L["식"], {}, swap) except EmitError as error: - o.marks.append(f"{k}: {error}") + o.glyph(at, glyph, mark=str(error)) formula, place = num(L["값"]), None name = o.names.line(L.get("라벨") or "줄") one = {"이름": name, "식": formula, "비목": COMP[comp or L.get("비목") or "S"]} - o.add_line(one | head | ({"자리": place} if place else {}), False) + o.add_line(one | head | ({"자리": place} if place else {}), False, at, glyph, L["값"]) if k in val: ref[k] = name check = _money(want) | {"계": want["T"]} - return o.done("비목버림", None, check) + return o.done("비목버림", None, check, "단가산출", file) diff --git a/resources/tester/test_793_2_san_emit.py b/resources/tester/test_793_2_san_emit.py index a4b46ec45..98462c758 100644 --- a/resources/tester/test_793_2_san_emit.py +++ b/resources/tester/test_793_2_san_emit.py @@ -27,21 +27,32 @@ mf = importlib.import_module("master_formula") mcut = importlib.import_module("master_cut") ms = importlib.import_module("master_formula_sum") -STMATE = { - "봉화": (62, "비목버림"), - "동부청영월": (69, "합버림_큰나머지"), - "울진대흥": (47, "비목버림"), - "영덕계류": (15, "비목버림"), - "울진소광": (40, "비목버림"), +STMATE = { # 파일: (블록, 금액 줄, 계표시 줄, 끝전) + "봉화": (62, 349, 184, "비목버림"), + "동부청영월": (69, 411, 230, "합버림_큰나머지"), + "울진대흥": (47, 261, 128, "비목버림"), + "영덕계류": (15, 86, 41, "비목버림"), + "울진소광": (40, 208, 112, "비목버림"), } -EST = {"북부청임도": 94, "북부청사방": 67, "거창": 61} +EST = {"북부청임도": (94, 1252), "북부청사방": (67, 849), "거창": (61, 802)} # (블록, 돈 · 값 줄) # 엑셀 글 식이 보인 값과 안 맞는 줄(793-1 틀린 줄 2) — 보인 값으로 얼림 · 블록 금액은 같음 -EST_FROZEN = {"북부청임도": {27: ["37"], 33: ["51"]}} +EST_FROZEN = {"북부청임도": {27: 1, 33: 1}} K = ("노무비", "재료비", "경비", "계") LABOR = {"종류": "노임", "이름": "보통인부", "규격": "", "열쇠비목": "L"} -def engine(logic: dict) -> dict: +def engine(out: dict) -> dict: + """내보낸 로직 → 엔진 셈 — 단가 줄은 SK 서버처럼 값을 얹음(요소 대신 값묶음 {비목: 단가}).""" + logic = {**out["로직"], "키": "GX990001"} + items = [] + for item in logic["호표"]: + one = {k: v for k, v in item.items() if k not in ("이음", "자원", "단가", "규격")} + if "단가" in item: + money = dict.fromkeys(K[:3], 0) | {item["비목"]: item["단가"]} + one |= {"종류": "로직", "값묶음": money} + del one["비목"] + items.append(one | {"요소": None}) + logic["호표"] = items master = mf.Master({"로직.json": {"그룹": "로직", "줄": [logic]}}) return mcut.engine(master, logic["키"], mf.run(master, logic["키"], {})) @@ -60,69 +71,79 @@ def _load(tag: str) -> dict: def _stmate(tag: str, blk: dict) -> tuple[list, dict]: nodes = stm.parse_block(blk["몸"]) - return nodes, em.stmate(blk, nodes, _load(tag)["단가"], STMATE[tag][1], "GX990001") + return nodes, em.stmate(blk, nodes, _load(tag)["단가"], STMATE[tag][3]) -def _lines_equal(logic: dict, got: dict) -> list: - """줄마다 엔진 비목 금액 = 엑셀 · 단계 = 계표시 줄.""" - bad = [] - for item, line in zip([*logic["호표"], *logic["덧줄"]], got["줄"], strict=True): - for k, want in item["검산"].items(): - if abs(float(line["비목"].get(k) or 0) - want) >= 0.05: - bad.append((item["이름"], k, line["비목"].get(k), want)) - for step in got.get("단계", []): - want = logic["가져옴"]["단계"][step["이름"]]["검산"] - if any(abs(float(step[k]) - want[k]) >= 0.15 for k in want): - bad.append((step["이름"], want)) - return bad +def _lines_equal(out: dict, got: dict) -> tuple[list, int, int]: + """원본 글줄마다 엔진 줄 금액(계) = 엑셀 · 단계 = 계표시 줄 → (틀린 것, 줄 수, 단계 수).""" + lines = {x["이름"]: x for x in got["줄"]} + steps = {x["이름"]: x for x in got.get("단계", [])} + bad, n_line, n_step = [], 0, 0 + for g in out["원본"]["글줄"]: + if g.get("값") is None or not g.get("자리") or g["자리"].startswith("mid:"): + continue + if g["자리"].startswith("단계:"): + n_step += 1 + have = float(steps[g["자리"][3:]]["계"]) + tol = 0.15 + else: + n_line += 1 + have = sum(float(lines[n]["금액"] or 0) for n in g.get("자리들") or [g["자리"]]) + tol = 0.05 + if abs(have - g["값"]) >= tol: + bad.append((g["행"], g["글"].strip(), have, g["값"])) + return bad, n_line, n_step @pytest.mark.parametrize("tag", list(STMATE)) def test_stmate_engine_equals_excel(tag: str) -> None: data = _load(tag) + blocks, n_lines, n_steps, rule = STMATE[tag] n, wrong, lines, steps = 0, [], 0, 0 for blk in data["블록"]: if not any(ln["G"].strip() for ln in blk["몸"]): continue n += 1 - nodes, logic = _stmate(tag, blk) - got = engine(logic) - ref = ev.stmate_block(blk, nodes, data["단가"], STMATE[tag][1])["계"] - want = logic["가져옴"]["검산"] + nodes, out = _stmate(tag, blk) + got = engine(out) + ref = ev.stmate_block(blk, nodes, data["단가"], rule)["계"] + want = out["원본"]["엑셀금액"] same = money(got) == tuple(round(want[k]) for k in K) same &= money(got) == (ref["L"], ref["M"], ref["S"], ref["T"]) - bad = _lines_equal(logic, got) + ms.check(logic, logic["키"]) + logic["가져옴"]["표지"] - if deal := logic["가져옴"].get("계약"): # B09 몫 — 엔진 계 × 낙찰률 원 버림 = 엑셀 머리 + bad, a, b = _lines_equal(out, got) + bad += ms.check(out["로직"], blk["코드"]) + out["원본"]["표지"] + lines, steps = lines + a, steps + b + if deal := out["원본"].get("계약"): # B09 몫 — 엔진 계 × 낙찰률 원 버림 = 엑셀 머리 cut = [math.floor(round(int(got[k]) * deal["낙찰률"] / 100, 6)) for k in K[:3]] same &= cut == [deal["금액"][k] for k in K[:3]] if not same or bad: wrong.append((blk["코드"], money(got), bad[:3])) - lines += len(logic["호표"]) + len(logic["덧줄"]) - steps += len(got.get("단계", [])) - assert (n, wrong) == (STMATE[tag][0], []) - assert lines >= 1 and steps >= 1 + assert (n, wrong) == (blocks, []) + assert (lines, steps) == (n_lines, n_steps) @pytest.mark.parametrize("tag", list(EST)) def test_est_engine_equals_excel(tag: str) -> None: data = _load(tag) - n, wrong = 0, [] + n, wrong, lines = 0, [], 0 for blk in data["블록"]: want = data["총괄"].get(blk["번"]) if not want or not (want["T"] or any(want[c] for c in "LMS")): continue n += 1 parsed = est.parse_block(blk["글"], data["단가"]) - logic = em.est(blk, parsed, want, "GX990002") - got = engine(logic) + out = em.est(blk, parsed, want) + got = engine(out) ref = ev.est_block(parsed, ev.designate(parsed, want))["금액"] same = money(got) == (want["L"], want["M"], want["S"], want["T"]) same &= money(got) == (ref["L"], ref["M"], ref["S"], ref["T"]) - frozen = [m.split(":")[0] for m in logic["가져옴"]["표지"]] - bad = _lines_equal(logic, got) + ms.check(logic, logic["키"]) - if not same or bad or frozen != EST_FROZEN.get(tag, {}).get(blk["번"], []): - wrong.append((blk["번"], money(got), bad[:3], frozen)) - assert (n, wrong) == (EST[tag], []) + bad, a, _ = _lines_equal(out, got) + bad += ms.check(out["로직"], str(blk["번"])) + lines += a + frozen = len(out["원본"]["표지"]) + if not same or bad or frozen != EST_FROZEN.get(tag, {}).get(blk["번"], 0): + wrong.append((blk["번"], money(got), bad[:3], out["원본"]["표지"])) + assert (n, wrong, lines) == (EST[tag][0], [], EST[tag][1]) @pytest.mark.parametrize( @@ -135,10 +156,10 @@ def test_est_engine_equals_excel(tag: str) -> None: ) def test_stmate_price_propagates(tag, code, labor, before, after) -> None: blk = next(b for b in _load(tag)["블록"] if b["코드"] == code) - _, logic = _stmate(tag, blk) - assert money(engine(logic))[:3] == before - assert em.reprice(logic, {"코드": labor}, {"L": 180000}) >= 1 - assert money(engine(logic))[:3] == after + _, out = _stmate(tag, blk) + assert money(engine(out))[:3] == before + assert em.reprice(out["로직"], {"코드": labor}, {"L": 180000}) >= 1 + assert money(engine(out))[:3] == after @pytest.mark.parametrize( @@ -153,20 +174,29 @@ def test_stmate_price_propagates(tag, code, labor, before, after) -> None: def test_est_price_propagates(tag, no, after) -> None: data = _load(tag) blk = next(b for b in data["블록"] if b["번"] == no) - logic = em.est(blk, est.parse_block(blk["글"], data["단가"]), data["총괄"][no], "GX990002") - assert em.reprice(logic, LABOR, {"L": 180000.0}) >= 1 - assert money(engine(logic))[:3] == after + out = em.est(blk, est.parse_block(blk["글"], data["단가"]), data["총괄"][no]) + assert em.reprice(out["로직"], LABOR, {"L": 180000.0}) >= 1 + assert money(engine(out))[:3] == after def test_link_keys_m01() -> None: - """자원 이음 열쇠 — 엑셀 품명 · 규격 · 단위 · 경쟁사 코드 · 1차 줄 판정 M01 키.""" + """호표 줄 = 793-2 꼴(종류 · 단가 · 이음 = 1차 줄 판정 M01 키) · 덧칸 자원 · 원본.""" tag = "봉화" blk = next(b for b in _load(tag)["블록"] if b["코드"] == "D00102") - logic = em.stmate( - blk, stm.parse_block(blk["몸"]), _load(tag)["단가"], "비목버림", "GX990001", True - ) - labor = next(x for x in logic["호표"] if x["이름"] == "보통인부") - assert labor["자원"] | {"M01": labor["자원"].get("M01")} == { + out = em.stmate(blk, stm.parse_block(blk["몸"]), _load(tag)["단가"], "비목버림", True) + labor = next(x for x in out["로직"]["호표"] if x["이름"] == "보통인부") + assert {k: v for k, v in labor.items() if k != "자원"} == { + "이름": "보통인부", + "규격": "", + "단위": "인", + "수량": "(2 / 50)", + "종류": "인력", + "비목": "노무비", + "단가": 165545, + "이음": "LB000002", + "번지": "A02", + } + assert labor["자원"] == { "꼴": "STmate", "갈래": "인력", "코드": "L00007", @@ -174,12 +204,21 @@ def test_link_keys_m01() -> None: "규격": None, "단위": "인", "단가": {"L": 165545, "M": 0, "S": 0}, - "M01": "LB000002", + "비목": "L", + "옮김": None, } - assert labor["값묶음"] == {"노무비": 165545, "재료비": 0, "경비": 0} + src = out["원본"] + assert {k: src[k] for k in ("시트", "갈래", "코드", "행")} == { + "시트": "단가산출근거", + "갈래": "STmate", + "코드": "D00102", + "행": [2912, 2937], + } + assert src["엑셀금액"] == {"노무비": 34119, "재료비": 11192, "경비": 341, "계": 45652} + assert next(g for g in src["글줄"] if g.get("자리") == "보통인부")["값"] == 6621.8 -# ── 엑셀 없이 도는 문법 견본(793-1 `test_stmate_grammar_sample` 그대로) ── +# ── 엑셀 없이 도는 문법 견본(793-1 `test_stmate_grammar_sample` + 다시 대입 · 뒤 줄) ── def test_stmate_grammar_sample_engine() -> None: prices = { "M00081": {"T": 11468, "L": 0, "M": 11468, "S": 0, "meta": {}, "이름": "합판(내수)"}, @@ -202,18 +241,16 @@ def test_stmate_grammar_sample_engine() -> None: ] rows = [{"행": i, "G": g} for i, g in enumerate(text)] nodes = stm.parse_block(rows) - block = {"코드": "D00001", "이름": "견본", "머리": {}, "꼬리": [], "몸": rows} + block = {"코드": "D00001", "이름": "견본", "꼬리": [], "몸": rows} block["머리"] = {"A": "", "B": "", "C": 0, "D": 0, "E": 0, "F": 0} - logic = em.stmate(block, nodes, prices, "비목버림", "GX990003") - assert [x["이름"] for x in logic["중간"]] == ["q", "q_2"] # 다시 대입 = 새 이름 - assert logic["중간"][0] == { - "이름": "q", - "식": "(2.5 / 3)", - "표시": 2, - "원문": {"행": 1, "글": text[1]}, - } - assert ms.check(logic, "견본") == [] and logic["가져옴"]["표지"] == [] - got = engine(logic) + out = em.stmate(block, nodes, prices, "비목버림") + logic = out["로직"] + assert logic["중간"] == [ + {"이름": "q", "식": "(2.5 / 3)", "표시": 2}, + {"이름": "q_2", "식": "(1 / 3)", "표시": 2}, # 다시 대입 = 새 이름 + ] + assert ms.check(logic, "견본") == [] and out["원본"]["표지"] == [] + got = engine(out) lines, tot = ev.run_stmate(nodes, prices) want = ev.block_total(tot, "비목버림") assert money(got) == (want["L"], want["M"], want["S"], want["T"]) @@ -236,14 +273,16 @@ def test_est_grammar_sample_engine() -> None: ] lines = est.parse_block(text, prices) want = {"T": 15950.0, "L": 15950.0, "M": 0.0, "S": 0.0} - logic = em.est({"번": 1, "이름": "견본", "단위": "㎡"}, lines, want, "GX990004") + out = em.est({"번": 1, "이름": "견본", "단위": "㎡"}, lines, want) + logic = out["로직"] assert [x["식"] for x in logic["덧줄"]] == [ "((줄.'보통인부' * 3) / 100)", "줄.'보통인부' + 공구손료", "소계", ] + assert logic["호표"][0]["종류"] == "인력" and logic["호표"][0]["단가"] == 172068 assert logic["덧줄"][2]["자리"] == {"방법": "버림", "자리": 0} assert [x.get("합밖") for x in (*logic["호표"], *logic["덧줄"])] == [True, True, True, None] - assert money(engine(logic)) == (15950, 0, 0, 15950) + assert money(engine(out)) == (15950, 0, 0, 15950) em.reprice(logic, LABOR, {"L": 180000.0}) - assert money(engine(logic)) == (16686, 0, 0, 16686) # 793-1 run_est 전파와 같음 + assert money(engine(out)) == (16686, 0, 0, 16686) # 793-1 run_est 전파와 같음