Merge branch 'sub_laptop_3' of C:/Aislo_wt/sub_laptop_3 into main_laptop_1
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
"""경쟁사 원파일(.estx · .stc) → 가져온 식(PLAN 810-2 · 요구 705 · 706 · 서버 단독 · 원파일 우선).
|
||||
|
||||
읽기 = 810-1 파서(`M02_San_Estx` · `M02_San_Stc`) → 둘 꼴(`pack`)을 실무자료 산근 자리
|
||||
(`M02_Sangeun_FromHopyo` · `실무자료/산근/<HP>.json` 칸 `원파일`)에 둠(원본 파일은 안 둠) →
|
||||
블록 하나 = `{로직, 원본}`(`emit` · 793-2a 꼴 → 가져온 식 SK → `M02_Doc_Import.derive` 수식 문서):
|
||||
EST = `M02_Doc_NativeEst`(구조 줄 표지로 정함) ·
|
||||
STmate = 원문 그대로 `M02_San_Emit.stmate`(원파일 원문 = 엑셀 숨은 G 열 · 단가 = COSTN ·
|
||||
끝전 = 옵션 SAN_TYP 1 비목버림 / 0 합버림_큰나머지 · 810-1 저장값 대조로 확인).
|
||||
블록 번 = 엑셀 길과 같음(EST = 산근 표 차례 1부터 = 「단산N」 · STmate = 코드 숫자).
|
||||
같은 공사 = 엑셀 산근 블록이 모두 원파일 블록과 금액 · 이름이 같음(`same`) — 둘 다 있으면 원파일.
|
||||
일위대가(EST HeadMainDat) → 실무자료 호표 꼴(`hopyo`) · STmate 일위대가 구성은 못 읽음(BDQTY 난독화) →
|
||||
호표 없음(엑셀을 같이 올리면 엑셀 호표 + 원파일 산근).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_NativeEst as ne
|
||||
from M02_MasterTemplete import M02_San_Emit as em
|
||||
|
||||
EXT = (".estx", ".stc")
|
||||
RULE = {"0": "합버림_큰나머지"} # STmate SAN_TYP — 그 밖 = 비목버림
|
||||
_EST_KEYS = ("행", "글", "보임", "품명", "단", "색인", "연결", "비목", "자원", "연결단가", "변수")
|
||||
_EST_KEYS += ("줄참조", "결과", "합계", "저장")
|
||||
_RES_KEYS = ("코드", "종류", "이름", "규격", "단위")
|
||||
|
||||
|
||||
class NativeError(ValueError):
|
||||
"""못 읽는 원파일."""
|
||||
|
||||
|
||||
def is_native(name: str) -> bool:
|
||||
return str(name or "").lower().endswith(EXT)
|
||||
|
||||
|
||||
def read(name: str, data: bytes) -> dict[str, Any]:
|
||||
"""원파일 → 둘 꼴(`pack`) · 못 읽음 = NativeError."""
|
||||
from M02_MasterTemplete import M02_San_Estx as estx
|
||||
from M02_MasterTemplete import M02_San_Stc as stc
|
||||
|
||||
try:
|
||||
got = estx.read(data) if name.lower().endswith(".estx") else stc.read(data)
|
||||
except Exception as e: # noqa: BLE001 — 파서 오류 = 사유 글
|
||||
raise NativeError(f"원파일을 못 읽음 — {type(e).__name__}: {str(e)[:80]}") from e
|
||||
return pack(got, name)
|
||||
|
||||
|
||||
def pack(got: dict, name: str) -> dict[str, Any]:
|
||||
"""파서 답 → 둘 꼴(셈 · 변환에 쓰는 칸만 · JSON)."""
|
||||
out: dict[str, Any] = {"꼴": got["꼴"], "파일": name, "표지": got.get("표지") or []}
|
||||
head = ("코드", "이름", "규격", "단위", "저장")
|
||||
if got["꼴"] == "EST":
|
||||
out["산근"] = [
|
||||
{**{k: b.get(k) for k in head}, "줄": [_est_line(ln) for ln in b["줄"]]}
|
||||
for b in got["산근"]
|
||||
]
|
||||
out["일위"] = [{**{k: b.get(k) for k in head}, "줄": b["줄"]} for b in got["일위"]]
|
||||
return out
|
||||
out["산근"] = [
|
||||
{**{k: b.get(k) for k in head}, "줄": [{"행": ln["행"], "글": ln["글"]} for ln in b["줄"]]}
|
||||
for b in got["산근"]
|
||||
]
|
||||
out["일위"] = [{k: b.get(k) for k in head} for b in got["일위"]]
|
||||
res = got.get("자원") or {}
|
||||
out["단가"] = {
|
||||
code: {**p, **{k: (res.get(code) or {}).get(k) for k in ("이름", "규격", "단위")}}
|
||||
for code, p in (got.get("단가") or {}).items()
|
||||
}
|
||||
out["끝전"] = RULE.get(str((got.get("옵션") or {}).get("SAN_TYP")), "비목버림")
|
||||
return out
|
||||
|
||||
|
||||
def _est_line(ln: dict) -> dict:
|
||||
one = {k: ln.get(k) for k in _EST_KEYS if ln.get(k) not in (None, [], False, "")}
|
||||
if one.get("자원"):
|
||||
one["자원"] = {k: ln["자원"].get(k) for k in _RES_KEYS}
|
||||
return one
|
||||
|
||||
|
||||
# ── 블록 ──────────────────────────────────────────────────────────────
|
||||
def numbers(saved: dict) -> dict[int, dict]:
|
||||
"""블록 번 → 산근 블록(엑셀 길 번과 같음)."""
|
||||
if saved["꼴"] == "EST":
|
||||
return {i: b for i, b in enumerate(saved["산근"], 1)}
|
||||
return {int(str(b["코드"])[1:]): b for b in saved["산근"] if str(b["코드"])[1:].isdigit()}
|
||||
|
||||
|
||||
def emit(saved: dict, no: int, keys: bool = True) -> dict[str, Any]:
|
||||
"""원파일 산근 블록 하나 → `{로직, 원본}`(원본 `원파일` True · 코드 · 확정 수) · 없는 번 = KeyError."""
|
||||
blk = numbers(saved)[int(no)]
|
||||
if saved["꼴"] == "EST":
|
||||
names = {b["코드"]: i for i, b in enumerate(saved["산근"], 1)}
|
||||
return ne.block(blk, int(no), keys, saved["파일"], names)
|
||||
from M02_MasterTemplete import M02_San_Stmate as stm
|
||||
|
||||
nodes = [{"행": ln["행"], "글": ln["글"], **stm.parse_line(ln["글"])} for ln in blk["줄"]]
|
||||
saved_amt = blk.get("저장") or {}
|
||||
head = {"C": saved_amt.get("T"), "D": saved_amt.get("L"), "E": saved_amt.get("M")}
|
||||
head["F"] = saved_amt.get("S")
|
||||
block = {"코드": blk["코드"], "이름": blk.get("이름"), "몸": [], "꼬리": [], "머리": head}
|
||||
out = em.stmate(block, nodes, saved["단가"], saved["끝전"], keys, saved["파일"])
|
||||
marks = sum(1 for one in out["원본"]["글줄"] if one.get("표지"))
|
||||
sure = len(out["로직"]["호표"]) + len(out["로직"].get("덧줄") or []) + len(out["로직"]["중간"])
|
||||
out["원본"] |= {"원파일": True, "갈래": "STmate", "확정": {"확정": sure, "표지": marks}}
|
||||
return out
|
||||
|
||||
|
||||
def inner(saved: dict, logic: dict) -> list[tuple[dict, int | None, str | None]]:
|
||||
"""안쪽 산근 줄 `[(호표 줄, 같은 원파일 블록 번, 못 잇는 까닭)]`(`FromHopyo._inner` 짝)."""
|
||||
have = numbers(saved)
|
||||
by_code = {str(b.get("코드")): n for n, b in have.items()}
|
||||
out = []
|
||||
for line in logic["호표"]:
|
||||
res = line.get("자원") or {}
|
||||
if res.get("갈래") != "단가산출":
|
||||
continue
|
||||
n = by_code.get(str(res.get("코드")))
|
||||
if n is None:
|
||||
out.append((line, None, "같은 원파일에 안쪽 블록 없음"))
|
||||
elif res.get("옮김"):
|
||||
out.append((line, n, "비목이 바뀐 부름"))
|
||||
else:
|
||||
out.append((line, n, None))
|
||||
return out
|
||||
|
||||
|
||||
# ── 같은 공사 · 일위대가 ───────────────────────────────────────────────
|
||||
def same(saved: dict, sheets: dict) -> bool:
|
||||
"""엑셀 산근 블록이 모두 원파일 같은 번 블록과 계 금액(1 원 안)이 같음 = 같은 공사."""
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
|
||||
have = numbers(saved)
|
||||
kind = book.kind_of(sheets)
|
||||
if not kind or (kind == "STmate") != (saved["꼴"] == "STmate"):
|
||||
return False
|
||||
if kind == "STmate":
|
||||
excel = {int(b["코드"][1:]): b["머리"].get("C") for b in book.stmate_blocks(sheets)}
|
||||
else:
|
||||
excel = {int(n): t.get("T") for n, t in book.est_prices(sheets)[1].items()}
|
||||
if not excel:
|
||||
return False
|
||||
for n, total in excel.items():
|
||||
blk = have.get(int(n))
|
||||
if blk is None or abs(float(total or 0) - float((blk.get("저장") or {}).get("T") or 0)) > 1:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
GALLAE = {6: "노임", 5: "자재", 105: "기계", 104: "단가산출", 103: "호표", 21: "기타", 101: "기타"}
|
||||
|
||||
|
||||
def hopyo(saved: dict) -> dict[str, Any]:
|
||||
"""EST 일위대가(HeadMainDat) → 실무자료 읽기 꼴(`M02_Hopyo_Excel.read_book` 짝 · 호표 · 단산) ·
|
||||
STmate = 호표 없음(구성 못 읽음)."""
|
||||
blocks = numbers(saved)
|
||||
names = {str(b.get("코드")): n for n, b in blocks.items()}
|
||||
heads = {str(h.get("코드")): n for n, h in enumerate(saved.get("일위") or [], 1)}
|
||||
out: dict[str, Any] = {"꼴": "원파일", "시트": saved["꼴"], "호표": [], "검산": {"어긋남": []}}
|
||||
out["단산"] = [
|
||||
{"번": n, "이름": b.get("이름"), "단위": b.get("단위"), "금액": _cost(b.get("저장"))}
|
||||
for n, b in blocks.items()
|
||||
]
|
||||
if saved["꼴"] != "EST":
|
||||
return out
|
||||
for n, h in enumerate(saved.get("일위") or [], 1):
|
||||
lines = []
|
||||
for k, ln in enumerate(h.get("줄") or []):
|
||||
link = ln.get("연결")
|
||||
code = str((ln.get("자원") or {}).get("코드") or "")
|
||||
ref = None
|
||||
if link == 104 and code in names:
|
||||
ref = {"종류": "단산", "번": names[code]}
|
||||
elif link == 103 and code in heads:
|
||||
ref = {"종류": "일위", "번": heads[code]}
|
||||
kind = "%" if ln.get("연결") in (52, 58, 62) else GALLAE.get(link or 0, "기타")
|
||||
if kind == "자재" and str(ln.get("단위") or "") == "인":
|
||||
kind = "노임"
|
||||
if not any((ln.get("단가") or {}).values()) and kind not in ("%",):
|
||||
kind = "별도계상" if not ref else kind
|
||||
lines.append(
|
||||
{
|
||||
"차례": k,
|
||||
"이름": ln.get("품명") or "",
|
||||
"규격": ln.get("규격") or "",
|
||||
"수량": _dec(ln.get("수량")),
|
||||
"단위": ln.get("단위") or "",
|
||||
"단가": _cost(ln.get("단가")),
|
||||
"금액": _cost(ln.get("저장")),
|
||||
"비고": "",
|
||||
"참조": ref,
|
||||
"갈래": kind,
|
||||
"행": ln.get("행"),
|
||||
"코드": code or None,
|
||||
}
|
||||
)
|
||||
out["호표"].append(
|
||||
{
|
||||
"번": n,
|
||||
"이름": h.get("이름"),
|
||||
"규격": h.get("규격") or "",
|
||||
"단위": h.get("단위") or "",
|
||||
"금액": _cost(h.get("저장")),
|
||||
"행": [n, n],
|
||||
"줄": lines,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _dec(v: Any) -> int | float | None:
|
||||
"""수 → 엑셀 읽기와 같은 꼴(정수 = int · 아니면 float · 없음 = None)."""
|
||||
if v is None:
|
||||
return None
|
||||
d = Decimal(str(v))
|
||||
return int(d) if d == d.to_integral_value() else float(d)
|
||||
|
||||
|
||||
def _cost(d: dict | None) -> dict[str, Any]:
|
||||
d = d or {}
|
||||
out = {"계": _dec(d.get("T"))}
|
||||
return out | {name: _dec(d.get(c)) for c, name in em.COMP.items()}
|
||||
@@ -26,7 +26,8 @@ from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
|
||||
XL = "엑셀" # 연결 칸 — 엑셀 값 기억
|
||||
HEAD = ("파일", "시트", "행", "갈래", "코드")
|
||||
HEAD = ("파일", "시트", "행", "갈래", "코드", "원파일")
|
||||
SUB = "원파일 소계" # 원파일 소계 덧줄(810-2 `M02_Doc_NativeEst`) — 합 밖 · 비목 칸 비움
|
||||
PICK = "소요량" # 고른 표 칸 기호 이름 머리
|
||||
_SCALE = {"eq": (1,), "pct": (100, 0.01), "kgt": (1000, 0.001)}
|
||||
|
||||
@@ -292,6 +293,8 @@ def derive(sk: dict, ctx: tuple | None = None, whole: Any = None) -> dict:
|
||||
doc, notes = dc.cv.convert(_Row(master, key, row), row, given)
|
||||
_attach(doc, row, ids, src)
|
||||
_numbers(doc, keep)
|
||||
if (meta.get("원본") or {}).get("원파일"): # 원파일(810-2) — 소계 비목 칸 비움 · 값 변수 출처
|
||||
_native(doc)
|
||||
head = {k: (meta.get("원본") or {}).get(k) for k in HEAD if (meta.get("원본") or {}).get(k)}
|
||||
marks = list((meta.get("원본") or {}).get("표지") or [])
|
||||
if not (doc.get("금액") or doc.get("결과") or row["호표"] or row.get("덧줄")):
|
||||
@@ -302,6 +305,16 @@ def derive(sk: dict, ctx: tuple | None = None, whole: Any = None) -> dict:
|
||||
return {**doc, "키": str(sk.get("키") or key), "이름": str(sk.get("이름") or doc.get("이름"))}
|
||||
|
||||
|
||||
def _native(doc: dict) -> None:
|
||||
"""원파일 문서 — 소계 덧줄(합 밖) 비목 칸 비움(금액만) · 수 하나 변수(옵션 칸) 출처 「원파일」."""
|
||||
for line in doc.get("금액") or []:
|
||||
if line.get("출처") == SUB and line.get("합밖"):
|
||||
line.pop("비목", None)
|
||||
for var in doc.get("변수") or []:
|
||||
if (var.get("연결") or {}).get("갈래") == "숫자" and XL not in var["연결"]:
|
||||
var["출처"] = "원파일"
|
||||
|
||||
|
||||
EMPTY = "참고 · 빈 산근 — 엑셀 줄 없음 · 금액 0 그대로"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,570 @@
|
||||
"""EST 원파일 산근 블록 → 가져온 로직 줄(PLAN 810-2 · 요구 705 · 706 · 서버 단독 · 읽기만).
|
||||
|
||||
바탕 = `M02_San_Estx.read` 구조 줄(칸 뜻 `resources/master_data/_원파일파서_틀.md`) — 엑셀 글 추정 대신
|
||||
원파일 표지로 정함(`M02_San_Est` 값 역색인 안 씀):
|
||||
단가 = `\\x03` 수 + 자원 코드(LinkIndex) · 변수 = `\\x11` 수(앞 변수 줄 값) · 줄참조 = `\\x0c` 수 ·
|
||||
결과 = `\\x12` 수 · 비목 = SCheck(음수 = 그 줄이 냄 · 율 · 소계 양수 = 대상).
|
||||
꼴 = 793-2a 내보내기(`M02_San_Emit.Out`) 그대로 → `M02_Sangeun_Imported.create` · `M02_Doc_Import.derive`
|
||||
가 수식 문서로(엑셀 길과 같은 길 · 같은 연결 규칙 `M02_San_Emit._m01` · 797-1 기계 시간당 이음).
|
||||
변수 대입 줄 = 중간(변수 그대로 · 공식 글 · 분수 그림 ─ 은 위아래 줄 칸 · 결과와 맞을 때만 식 ·
|
||||
아니면 결과 값 + 표지) · 자원 줄 = 호표(단가 = 원파일 값 · 수량 = 식 · 변수 = 이름) ·
|
||||
기계 · 산근 · 견적 세 줄(같은 코드 · 노 · 재 · 경 · 같은 수량 식) = 로직 값묶음 한 줄(소계 줄 = 글) ·
|
||||
소계(61) = 합 밖 덧줄(출처 `SUB` · 문서에서 비목 칸 비움) + 아래 줄이 그 비목으로 합에 듦 ·
|
||||
율(58 · 62) = 덧줄(줄참조 = 앞 줄 부름 · 구간 `\\x07X\\x07` = `\\x06X\\x06` 줄 합) ·
|
||||
합계(\\x1a) = 글(블록 합 = 문서 비목 합 · 끝수 버림 0).
|
||||
못 읽은 줄 = 보인 결과 값 얼림 + 「참고 · …」 표지(`NOTE`) — 지어 넣는 값 없음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from M02_MasterTemplete import M02_San_Emit as em
|
||||
from M02_MasterTemplete import M02_San_Est as est
|
||||
from M02_MasterTemplete import M02_San_Expr as ex
|
||||
|
||||
COMP = em.COMP
|
||||
NOTE = em.NOTE
|
||||
SUB = "원파일 소계" # 덧줄 출처 — 합 밖 소계(문서 비목 칸 비움 · `M02_Doc_Import`)
|
||||
TRIPLE = (101, 104, 105) # 견적 · 산근 · 기계 — 노 · 재 · 경 세 줄
|
||||
GALLAE = {"노임": "인력", "자재": "재료", "기계": "기계", "견적": "고정", "산근": "단가산출"}
|
||||
GALLAE["표준"] = "고정"
|
||||
NATURAL = {"노임": "L", "자재": "M"}
|
||||
_SENT = 7_770_000.0 # 변수 이름 자리 표 수(토큰 읽기용 · 셈 전에 값으로 바꿈)
|
||||
_ASSIGN = re.compile(r"(?<![A-Za-z0-9_])([A-Za-z][A-Za-z0-9]*)\s*(?:\([^)=]*\))?\s*=(?!=)")
|
||||
_DROP = re.compile(r"\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\x06[A-Z]\x06")
|
||||
_GROUP = re.compile(r"\x07([A-Z])\x07\s*\x1a[^\x1a]*\x1a")
|
||||
_CTRL = re.compile(r"[\x00-\x1f]")
|
||||
GUESS = "추정"
|
||||
_COND = re.compile(r"품셈|기준|적용|이상|이하|미만|초과")
|
||||
|
||||
|
||||
def _clean(raw: str) -> str:
|
||||
return _CTRL.sub("", _DROP.sub(" ", raw or ""))
|
||||
|
||||
|
||||
def _close(a: float, b: float) -> bool:
|
||||
return abs(a - b) < 1e-6 * max(1.0, abs(b))
|
||||
|
||||
|
||||
def _fit(got: float, want: float) -> str | None:
|
||||
"""보인 값이 되는 자리 규칙 — `M02_San_Expr.fit`(3 자리까지) + 버림 · 반올림 4~6 자리(EST Diget)."""
|
||||
if rule := ex.fit(got, want):
|
||||
return rule
|
||||
for n in (4, 5, 6):
|
||||
q = 10**n
|
||||
if abs(ex.math.floor(round(got * q, 6)) / q - want) < 1e-9:
|
||||
return f"버림{n}"
|
||||
if abs(ex.math.floor(got * q + 0.5 + 1e-9) / q - want) < 1e-9:
|
||||
return f"반올림{n}"
|
||||
return None
|
||||
|
||||
|
||||
class Block:
|
||||
"""EST 산근 블록 한 벌 → `{로직, 원본}`."""
|
||||
|
||||
def __init__(self, blk: dict, no: int, keys: bool, file: str, names: dict[str, int]) -> None:
|
||||
name = str(blk.get("이름") or blk.get("코드") or "")
|
||||
if blk.get("규격"):
|
||||
name = f"{name}({blk['규격']})"
|
||||
self.o = em.Out(name, blk.get("단위"), "EST", no)
|
||||
self.o.head |= {"원파일": blk.get("코드")}
|
||||
self.blk, self.keys, self.file, self.nos = blk, keys, file, names
|
||||
self.lines: list[dict] = blk.get("줄") or []
|
||||
self.vars: dict[str, tuple[str, float]] = {} # 원 이름 → (엔진 이름, 값)
|
||||
self.done: list[tuple[float, str, dict]] = [] # (결과, 부름 글, 줄) — 줄참조 찾기
|
||||
self.groups: dict[str, list[tuple[str, float]]] = {} # 구간 글자 → (부름 글, 결과)
|
||||
self.extra: dict[int, dict] = {} # 행 → 소계가 정한 비목(아래 줄)
|
||||
self.got = {"확정": 0, "표지": 0, "추정": 0}
|
||||
|
||||
# ── 작은 도구 ──────────────────────────────────────────────────
|
||||
def mark(self, ln: dict, why: str) -> None:
|
||||
self.o.glyph(ln["행"], ln.get("보임") or "", mark=f"{NOTE}{why}")
|
||||
self.got["표지"] += 1
|
||||
|
||||
def _sub(self, seg: str) -> tuple[str, dict[float, str]]:
|
||||
"""식 조각 속 아는 변수 이름 → 표 수(뒤에서 이름으로 바꿈)."""
|
||||
out, k = {}, 0
|
||||
for name in sorted(self.vars, key=len, reverse=True):
|
||||
pat = re.compile(rf"(?<![A-Za-z0-9_]){re.escape(name)}(?![A-Za-z0-9_])")
|
||||
if pat.search(seg):
|
||||
k += 1
|
||||
mark = _SENT + k
|
||||
out[mark] = name
|
||||
seg = pat.sub(em.num(mark), seg)
|
||||
return seg, out
|
||||
|
||||
def formula(
|
||||
self, seg: str, want: float | None, extra: dict[float, float] | None = None
|
||||
) -> tuple[tuple, dict, str | None] | None:
|
||||
"""식 조각 → (나무, {토큰 차례: 변수 이름}, 자리 규칙) · 결과와 맞지 않으면 None ·
|
||||
extra = {표 수: 값}(구간 합 자리)."""
|
||||
seg, names = self._sub(_power(seg.replace("·", "×")))
|
||||
toks = est.tokens(seg)
|
||||
if not toks:
|
||||
return None
|
||||
try:
|
||||
tree = ex.parse(toks)
|
||||
except ex.ExprError:
|
||||
return None
|
||||
vars_ = {n[2]: names[n[1]] for n in ex.numbers(tree) if n[1] in names}
|
||||
sub = {i: self.vars[v][1] for i, v in vars_.items()}
|
||||
extra = extra or {}
|
||||
sub |= {n[2]: extra[n[1]] for n in ex.numbers(tree) if n[1] in extra}
|
||||
try:
|
||||
got = ex.value(tree, {"V": {}, "sub": sub})
|
||||
except ex.ExprError:
|
||||
return None
|
||||
same = want is None or abs(got - want) <= 1e-9 * max(1.0, abs(want))
|
||||
rule = "그대로" if same else _fit(got, want)
|
||||
return (tree, vars_, rule) if rule else None
|
||||
|
||||
def ref_of(self, value: float, before: int) -> str | None:
|
||||
"""줄참조 수 → 앞 줄 부름(가장 가까운 같은 결과)."""
|
||||
for v, call, ln in reversed(self.done):
|
||||
if ln["행"] < before and _close(v, value):
|
||||
return call
|
||||
return None
|
||||
|
||||
def var_of(self, value: float) -> str | None:
|
||||
for name, (_, v) in reversed(list(self.vars.items())):
|
||||
if _close(v, value):
|
||||
return name
|
||||
return None
|
||||
|
||||
# ── 변수 줄 ────────────────────────────────────────────────────
|
||||
def assigns(self, i: int, ln: dict) -> bool:
|
||||
"""「q = 0.7 K = 0.55 f = 1 / 1.325 = 0.75」 · 분수 그림 Q = ── = ── = 23.39 → 중간."""
|
||||
text = _clean(ln.get("글") or "")
|
||||
hits: list[re.Match] = []
|
||||
for m in _ASSIGN.finditer(text): # 앞 대입 값이 끝난 뒤(연산 · = 뒤 아님)만 새 대입
|
||||
gap = text[hits[-1].end() : m.start()] if hits else ""
|
||||
if not hits or (re.search(r"\d", gap) and not re.search(r"[×÷+\-*/(=]\s*$", gap)):
|
||||
hits.append(m)
|
||||
lead = text[: hits[0].start()].strip(" ○●·-") if hits else ""
|
||||
if not hits or (lead and not re.search(r"[::]$", lead)):
|
||||
return False
|
||||
for n, m in enumerate(hits):
|
||||
end = hits[n + 1].start() if n + 1 < len(hits) else len(text)
|
||||
self.assign(i, ln, m.group(1), text[m.end() : end])
|
||||
return True
|
||||
|
||||
def assign(self, i: int, ln: dict, name: str, rhs: str) -> None:
|
||||
parts = rhs.split("=")
|
||||
lead = re.match(r"\s*(" + est.NUMRE + r")", parts[-1])
|
||||
if not lead:
|
||||
return
|
||||
value = est.num(lead.group(1))
|
||||
body = None
|
||||
for seg in parts[:-1]:
|
||||
if "─" in seg:
|
||||
seg = self.fraction(i, seg)
|
||||
got = self.formula(seg, value) if seg else None
|
||||
if got:
|
||||
body = self.text(got)
|
||||
break
|
||||
eng = self.o.names.line(name)
|
||||
if body is None:
|
||||
body = em.num(value)
|
||||
if len(parts) > 1:
|
||||
self.mark(ln, f"변수 {name} 공식 글 못 읽음 — 결과 값 {em.num(value)}")
|
||||
self.vars[name] = (eng, value)
|
||||
self.o.middle(eng, body, None, ln["행"], ln.get("보임") or "")
|
||||
self.got["확정"] += 1
|
||||
|
||||
def fraction(self, i: int, seg: str) -> str:
|
||||
"""분수 그림 ─ = 위 줄 · 아래 줄 같은 차례 칸(node `native-text` 와 같은 읽기)."""
|
||||
|
||||
def cols(k: int) -> list[str]:
|
||||
if not 0 <= k < len(self.lines):
|
||||
return []
|
||||
t = _clean(self.lines[k].get("글") or "")
|
||||
return re.sub(r"\s*([×÷·+*/−-])\s*", r"\1", t).strip().split()
|
||||
|
||||
tops, bottoms, at = cols(i - 1), cols(i + 1), [0]
|
||||
|
||||
def one(_: re.Match) -> str:
|
||||
k = at[0]
|
||||
at[0] += 1
|
||||
top = tops[k] if k < len(tops) else ""
|
||||
low = bottoms[k] if k < len(bottoms) else ""
|
||||
return f"({top})/({low})" if top and low else "?"
|
||||
|
||||
return re.sub(r"─+", one, seg)
|
||||
|
||||
def text(self, got: tuple, swap: dict[int, str] | None = None) -> str:
|
||||
"""식 글 — 자리 규칙(그대로 빼고)을 식 안 끝수로(금액 줄은 `money` 가 버림1 을 넘김 = 줄 끝수)."""
|
||||
tree, vars_, rule = got
|
||||
swap = {**{i: self.vars[v][0] for i, v in vars_.items()}, **(swap or {})}
|
||||
body = em.text(tree, {}, swap)
|
||||
m = re.fullmatch(r"(버림|반올림|올림)(\d)", rule or "")
|
||||
if m:
|
||||
body = f"{m.group(1)}({body}, {m.group(2)})"
|
||||
return body
|
||||
|
||||
# ── 금액 줄 ────────────────────────────────────────────────────
|
||||
def money(self, ln: dict) -> tuple[str, dict | None] | None:
|
||||
"""금액 줄 식 → (식 글(단가 자리 = 1 · 변수 = 이름 · 줄참조 = 부름), 자리) · 못 읽음 None."""
|
||||
raw = ln.get("글") or ""
|
||||
group: dict[float, str] = {}
|
||||
sums: dict[float, float] = {}
|
||||
for k, m in enumerate(_GROUP.finditer(raw)):
|
||||
calls = self.groups.get(m.group(1)) or []
|
||||
if not calls:
|
||||
return None
|
||||
mark = _SENT + 500 + k
|
||||
group[mark] = "(" + " + ".join(c for c, _ in calls) + ")"
|
||||
sums[mark] = sum(v for _, v in calls)
|
||||
raw = raw.replace(m.group(0), f" {em.num(mark)} ", 1)
|
||||
text = _clean(raw)
|
||||
head = text.split("=", 1)[0]
|
||||
if ":" in head:
|
||||
text = text.split(":", 1)[1]
|
||||
parts = text.split("=")
|
||||
want = ln["결과"][-1] if ln.get("결과") else None
|
||||
if want is None or len(parts) < 2:
|
||||
return None
|
||||
price = (ln.get("연결단가") or [None])[0]
|
||||
for seg in parts[:-1]:
|
||||
bare = re.fullmatch(r"\s*(" + est.NUMRE + r")\s*", seg)
|
||||
if bare and price is not None and _close(est.num(bare.group(1)), want):
|
||||
return "1", None # 단가 그대로(× 1)
|
||||
got = self.formula(seg, want, sums)
|
||||
if not got:
|
||||
continue
|
||||
swap: dict[int, str] = {}
|
||||
used_price = False
|
||||
for _, v, idx in ex.numbers(got[0]):
|
||||
if idx in got[1]:
|
||||
continue
|
||||
if v in group:
|
||||
swap[idx] = group[v]
|
||||
elif price is not None and not used_price and _close(v, price):
|
||||
swap[idx], used_price = "1", True
|
||||
elif v in (ln.get("변수") or []) and self.var_of(v):
|
||||
swap[idx] = self.vars[self.var_of(v)][0]
|
||||
elif v in (ln.get("줄참조") or []) and (call := self.ref_of(v, ln["행"])):
|
||||
swap[idx] = call
|
||||
if price is not None and not used_price:
|
||||
continue
|
||||
place = em._place(got[2]) if got[2] != "그대로" else None
|
||||
return self.text((got[0], got[1], None), swap), place
|
||||
return None
|
||||
|
||||
def frozen(self, ln: dict, why: str) -> tuple[str, dict | None]:
|
||||
self.mark(ln, f"{why} — 보인 값 {em.num(ln['결과'][-1])} 얼림")
|
||||
return em.num(ln["결과"][-1]), None
|
||||
|
||||
def res(self, ln: dict, part: str | None, price: float | None) -> dict:
|
||||
r = ln.get("자원") or {}
|
||||
kind = r.get("종류")
|
||||
out = {"꼴": "EST", "갈래": GALLAE.get(kind, "고정"), "종류": kind, "코드": r.get("코드")}
|
||||
out |= {"이름": r.get("이름"), "규격": r.get("규격") or "", "단위": r.get("단위")}
|
||||
if kind == "산근" and r.get("코드") in self.nos:
|
||||
out["이름"] = f"단산{self.nos[r['코드']]}" # 같은 파일 안쪽 블록(797-1 형제 SK)
|
||||
out["원이름"] = r.get("이름")
|
||||
if part:
|
||||
nat = NATURAL.get(kind, part)
|
||||
out |= {"단가": {nat: price}, "비목": nat, "열쇠비목": nat}
|
||||
out["옮김"] = part if part != nat else None
|
||||
return out
|
||||
|
||||
def register(self, ln: dict, call: str) -> None:
|
||||
if ln.get("결과"):
|
||||
self.done.append((ln["결과"][-1], call, ln))
|
||||
g = re.search(r"\x06([A-Z])\x06", ln.get("글") or "")
|
||||
if g:
|
||||
self.groups.setdefault(g.group(1), []).append((call, (ln.get("결과") or [0.0])[-1]))
|
||||
|
||||
def item(self, ln: dict, part: str | None, inside: bool) -> None:
|
||||
"""자원 줄(노임 · 자재 · 표준 · 견적) → 호표 단가 줄."""
|
||||
price = (ln.get("연결단가") or [None])[0]
|
||||
got = self.money(ln) if price is not None else None
|
||||
qty, place = got if got else ("1", self.frozen(ln, "수량 식 못 읽음")[1])
|
||||
comp = part or NATURAL.get((ln.get("자원") or {}).get("종류")) or "S"
|
||||
r = self.res(ln, comp, price if got else ln["결과"][-1])
|
||||
name = self.o.names.item(ln.get("품명") or r.get("이름") or "줄")
|
||||
link, many = em._m01(r["갈래"], r) if self.keys else (None, None)
|
||||
one = em.resource(name, r.get("단위"), qty, r, link)
|
||||
one |= {} if inside else {"합밖": True}
|
||||
one |= {"자리": place} if place else {}
|
||||
self.o.add_line(one, True, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||||
if many:
|
||||
self.mark(ln, many.removeprefix(NOTE))
|
||||
self.got["확정"] += bool(got)
|
||||
self.register(ln, f"줄.'{name}'")
|
||||
|
||||
def triple(self, rows: list[dict], total: dict | None) -> None:
|
||||
"""기계 · 산근 · 견적 세 줄 → 로직 값묶음 한 줄(수량 식 · 자리 같을 때) · 아니면 줄마다."""
|
||||
got = [self.money(ln) for ln in rows]
|
||||
parts = [ln["비목"] for ln in rows]
|
||||
same = all(got) and len({g for g in got}) == 1 and sorted(parts) == ["L", "M", "S"]
|
||||
if not same:
|
||||
for ln, g in zip(rows, got):
|
||||
self.machine_line(ln, g)
|
||||
if total:
|
||||
self.subtotal(total, [call for _, call, _ in self.done[-len(rows) :]])
|
||||
return
|
||||
qty, place = got[0]
|
||||
prices = {ln["비목"]: (ln.get("연결단가") or [0])[0] for ln in rows}
|
||||
r = self.res(rows[0], None, None)
|
||||
r |= {"단가": prices}
|
||||
head = next(
|
||||
(b for b in reversed(self.lines[: self.lines.index(rows[0])])
|
||||
if (b.get("자원") or {}).get("코드") == r.get("코드") and not b.get("비목")),
|
||||
{},
|
||||
) # fmt: skip
|
||||
title = head.get("품명") or r.get("원이름") or r.get("이름") or "줄"
|
||||
name = self.o.names.item(title)
|
||||
one = {"이름": name, "규격": r.get("규격") or "", "단위": r.get("단위") or "", "수량": qty}
|
||||
one |= {"종류": "로직", "값묶음": em.bundle(r), "이음": self.logic_link(r), "자원": r}
|
||||
one |= {"자리": place} if place else {}
|
||||
value = total["결과"][-1] if total and total.get("결과") else None
|
||||
for ln in rows: # 노 · 재 · 경 글 = 펼침(맞댈 값은 한 줄 계 — 소계 글 · 없으면 첫 줄)
|
||||
self.o.glyph(ln["행"], ln.get("보임") or "")
|
||||
at = total or rows[0]
|
||||
value = value if value is not None else sum(ln["결과"][-1] for ln in rows)
|
||||
self.o.add_line(one, True, at["행"], at.get("보임") or "", value)
|
||||
if total:
|
||||
self.register(total, f"줄.'{name}'")
|
||||
self.got["확정"] += 3
|
||||
|
||||
def machine_line(self, ln: dict, got: tuple | None) -> None:
|
||||
qty, place = got if got else ("1", self.frozen(ln, "수량 식 못 읽음")[1])
|
||||
price = (ln.get("연결단가") or [ln["결과"][-1]])[0]
|
||||
r = self.res(ln, ln["비목"], price if got else ln["결과"][-1])
|
||||
r["갈래"] = "기계" if r["갈래"] == "기계" else r["갈래"]
|
||||
name = self.o.names.item(f"{ln.get('품명') or r.get('이름') or '줄'} {COMP[ln['비목']]}")
|
||||
link, _ = em._m01(r["갈래"], r) if self.keys else (None, None)
|
||||
one = em.resource(name, r.get("단위"), qty, r, link)
|
||||
one |= {"자리": place} if place else {}
|
||||
self.o.add_line(one, True, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||||
self.got["확정"] += bool(got)
|
||||
self.register(ln, f"줄.'{name}'")
|
||||
|
||||
def logic_link(self, r: dict) -> str | None:
|
||||
"""기계 = 시간당 기계경비 로직 부름 글(797-1 짝 고름) · 그 밖 = 없음(산근 = 형제 SK 참조)."""
|
||||
if not self.keys or r.get("갈래") != "기계":
|
||||
return None
|
||||
key, _ = em._m01("기계", r)
|
||||
if not key:
|
||||
return None
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
|
||||
got = dim.hourly_link(key, "노무비")
|
||||
if not got:
|
||||
return None
|
||||
args = "".join(f", {k}={v}" for k, v in got["입력"].items())
|
||||
return f"로직({got['키']}{args})"
|
||||
|
||||
def subtotal(
|
||||
self, ln: dict, kids: list[str], part: str | None = None, glyph: bool = True
|
||||
) -> str:
|
||||
"""합 밖 소계 덧줄 · glyph 거짓 = 글줄에 안 붙임(같은 줄 식이 붙음 · 61 「합 ÷ N = 결과」)."""
|
||||
name = self.o.names.line(self.label(ln) or "소계")
|
||||
one = {"이름": name, "식": " + ".join(kids) or "0", "비목": COMP.get(part or "", "경비")}
|
||||
one |= {"합밖": True, "출처": SUB}
|
||||
value = ln["결과"][-1] if ln.get("결과") else (ln.get("저장") or {}).get("T")
|
||||
if glyph:
|
||||
self.o.add_line(one, False, ln["행"], ln.get("보임") or "", value)
|
||||
else:
|
||||
self.o.adds.append(one)
|
||||
self.done.append((value or 0.0, name, ln))
|
||||
return name
|
||||
|
||||
def sub61(self, i: int, ln: dict) -> None:
|
||||
"""소계(61) — 앞 줄 결과 합(단 > 0 머리까지) · 아래 줄 비목 0 이면 그 줄이 소계 비목으로 합에 듦."""
|
||||
want = (ln.get("줄참조") or [None])[0]
|
||||
part = ln.get("비목")
|
||||
start, skip = 0, set()
|
||||
for k in range(i - 1, -1, -1): # node `native-text` 61 — 머리 · -20 줄까지(그 줄 포함)
|
||||
b = self.lines[k]
|
||||
if b.get("색인") == -1:
|
||||
skip.add(b["행"])
|
||||
if (b.get("단") or 0) > 0 or (b.get("색인") or 0) > 0 or b.get("색인") == -20:
|
||||
start = b["행"]
|
||||
break
|
||||
kids = [d for d in self.done if start <= d[2]["행"] < ln["행"] and d[2]["행"] not in skip]
|
||||
ok = want is not None and kids and _close(round(sum(k[0] for k in kids), 1), want)
|
||||
if ok and ln.get("결과"): # 「인부소계 : ‹합› ÷ N = 결과」 — 합(합 밖) 뒤 식 줄이 합에 듦
|
||||
self.subtotal({**ln, "결과": [want]}, [k[1] for k in kids], part, glyph=False)
|
||||
got = self.money(ln)
|
||||
body, place = got if got else self.frozen(ln, "소계 식 못 읽음")
|
||||
name = self.o.names.line("소계식")
|
||||
one = {"이름": name, "식": body, "비목": COMP.get(part or "", "경비")}
|
||||
one |= ({} if part else {"합밖": True}) | ({"자리": place} if place else {})
|
||||
self.o.add_line(one, False, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||||
self.done.append((ln["결과"][-1], name, ln))
|
||||
self.got["확정"] += bool(got)
|
||||
return
|
||||
if ok and part and all(k[2].get("비목") is None and self._is_line(k[1]) for k in kids):
|
||||
self.subtotal(ln, [k[1] for k in kids], part)
|
||||
for _, call, kid in kids: # 아래 줄 = 소계 비목으로 합에 듦
|
||||
self.joins(call, part)
|
||||
self.got["확정"] += 1
|
||||
return
|
||||
if ok: # 그대로 — 소계 줄이 합에 듦
|
||||
name = self.o.names.line("소계")
|
||||
one = {
|
||||
"이름": name,
|
||||
"식": " + ".join(k[1] for k in kids),
|
||||
"비목": COMP.get(part, "경비"),
|
||||
}
|
||||
one |= {} if part else {"합밖": True}
|
||||
self.o.add_line(one, False, ln["행"], ln.get("보임") or "", want)
|
||||
self.done.append((want, name, ln))
|
||||
self.got["확정"] += 1
|
||||
return
|
||||
value = want if want is not None else 0.0
|
||||
name = self.o.names.line("소계")
|
||||
one = {"이름": name, "식": em.num(value), "비목": COMP.get(part, "경비")}
|
||||
one |= {} if part else {"합밖": True}
|
||||
self.mark(ln, "소계 아래 줄 못 가림 — 보인 값 얼림")
|
||||
self.o.add_line(one, False, ln["행"], ln.get("보임") or "", value)
|
||||
self.done.append((value, name, ln))
|
||||
|
||||
@staticmethod
|
||||
def label(ln: dict) -> str:
|
||||
"""「인부소계 : …」 → 「인부소계」(= 앞 쌍점만 · 낱말 아니면 빈 글)."""
|
||||
head = _clean(ln.get("글") or "").split("=", 1)[0]
|
||||
if ":" not in head:
|
||||
return ""
|
||||
got = re.sub(r"[^\w가-힣]", "", head.split(":", 1)[0])[:20]
|
||||
return "" if not got or got[0].isdigit() else got
|
||||
|
||||
def _is_line(self, call: str) -> bool:
|
||||
name = call[3:-1] if call.startswith("줄.'") else call
|
||||
return any(one["이름"] == name for one in [*self.o.items, *self.o.adds])
|
||||
|
||||
def joins(self, call: str, part: str) -> None:
|
||||
"""합 밖 호표 · 덧줄(부름 글) → 그 비목으로 합에 듦."""
|
||||
name = call[3:-1] if call.startswith("줄.'") else call
|
||||
for one in [*self.o.items, *self.o.adds]:
|
||||
if one["이름"] != name:
|
||||
continue
|
||||
one.pop("합밖", None)
|
||||
if one.get("종류") in ("인력", "재료", "기계") and "단가" in one:
|
||||
one["비목"] = COMP[part]
|
||||
r = one.get("자원") or {}
|
||||
if r.get("비목"):
|
||||
r["옮김"] = part if part != r["비목"] else None
|
||||
elif "비목" in one and not one.get("종류"):
|
||||
one["비목"] = COMP[part]
|
||||
|
||||
def rate(self, ln: dict) -> None:
|
||||
got = self.money(ln)
|
||||
body, place = got if got else self.frozen(ln, "율 식 못 읽음")
|
||||
part = ln.get("비목")
|
||||
name = self.o.names.line(self.label(ln) or "율")
|
||||
one = {"이름": name, "식": body, "비목": COMP.get(part or "", "경비")}
|
||||
one |= {} if part in COMP else {"합밖": True}
|
||||
one |= {"자리": place} if place else {}
|
||||
self.o.add_line(one, False, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||||
self.got["확정"] += bool(got)
|
||||
self.register(ln, name)
|
||||
|
||||
def plain(self, ln: dict) -> None:
|
||||
"""연결 없는 결과 줄 — 비목 있으면 덧줄(식) · 없으면 글 + 값(줄참조 대상이면 중간)."""
|
||||
got = self.money(ln)
|
||||
part = ln.get("비목")
|
||||
if part in COMP:
|
||||
body, place = got if got else self.frozen(ln, "식 못 읽음")
|
||||
name = self.o.names.line("줄")
|
||||
one = {"이름": name, "식": body, "비목": COMP[part]}
|
||||
one |= {"자리": place} if place else {}
|
||||
self.o.add_line(one, False, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||||
self.register(ln, name)
|
||||
self.got["확정"] += bool(got)
|
||||
return
|
||||
eng = self.o.names.line("값")
|
||||
body = got[0] if got else em.num(ln["결과"][-1])
|
||||
if got and got[1]:
|
||||
body = f"{got[1]['방법']}({body}, {got[1]['자리']})"
|
||||
self.o.middle(eng, body, None, ln["행"], ln.get("보임") or "")
|
||||
self.register(ln, eng)
|
||||
|
||||
def text_line(self, ln: dict) -> None:
|
||||
"""글 줄 — 품셈 · 조건 이름만 있는 글(값 없음) = 「추정」 표지(연결 안 함 · 810-3 화면)."""
|
||||
if _COND.search(ln["보임"]):
|
||||
self.o.glyph(
|
||||
ln["행"], ln["보임"], mark=f"{NOTE}{GUESS} — 품셈 조건 글(값 없음 · 연결 안 함)"
|
||||
)
|
||||
self.got["추정"] += 1
|
||||
return
|
||||
self.o.glyph(ln["행"], ln["보임"])
|
||||
|
||||
# ── 한 벌 ─────────────────────────────────────────────────────
|
||||
def run(self) -> dict:
|
||||
for k, ln in enumerate(self.lines): # 「노무비 : ‹단가›」(식 · 결과 없음) = 단가 × 1
|
||||
text = _clean(ln.get("글") or "")
|
||||
if ln.get("연결단가") and not ln.get("결과") and "=" not in text and ln.get("비목"):
|
||||
price = ln["연결단가"][0]
|
||||
self.lines[k] = {**ln, "결과": [price], "글": f"{ln['글']} = {em.num(price)}"}
|
||||
lines, i = self.lines, 0
|
||||
while i < len(lines):
|
||||
ln = lines[i]
|
||||
link, part = ln.get("연결"), ln.get("비목")
|
||||
if ln.get("합계") and link != 62:
|
||||
if (ln.get("보임") or "").strip():
|
||||
self.o.glyph(ln["행"], ln["보임"])
|
||||
i += 1
|
||||
continue
|
||||
if link in TRIPLE and part in ("L", "M", "S"):
|
||||
rows = [ln]
|
||||
code = (ln.get("자원") or {}).get("코드")
|
||||
while (
|
||||
i + len(rows) < len(lines)
|
||||
and lines[i + len(rows)].get("연결") == link
|
||||
and lines[i + len(rows)].get("비목") in ("L", "M", "S")
|
||||
and (lines[i + len(rows)].get("자원") or {}).get("코드") == code
|
||||
):
|
||||
rows.append(lines[i + len(rows)])
|
||||
nxt = lines[i + len(rows)] if i + len(rows) < len(lines) else None
|
||||
total = nxt if nxt and nxt.get("연결") == link and nxt.get("비목") == "T" else None
|
||||
if all(r.get("결과") for r in rows):
|
||||
self.triple(rows, total)
|
||||
else:
|
||||
for r in rows:
|
||||
self.mark(r, "결과 없는 세 줄")
|
||||
i += len(rows) + bool(total)
|
||||
continue
|
||||
if link in (61, 51, 68):
|
||||
self.sub61(i, ln)
|
||||
elif link in (58, 62, 52) and ln.get("결과"):
|
||||
self.rate(ln)
|
||||
elif ln.get("결과") and (ln.get("자원") or {}).get("코드") and ln.get("연결단가"):
|
||||
self.item(ln, part if part in COMP else None, part in COMP)
|
||||
elif ln.get("결과") and link in TRIPLE and part == "T":
|
||||
self.o.glyph(ln["행"], ln.get("보임") or "", value=ln["결과"][-1])
|
||||
self.register(ln, em.num(ln["결과"][-1]))
|
||||
elif not ln.get("자원") and self.assigns(i, ln):
|
||||
pass
|
||||
elif ln.get("결과"):
|
||||
self.plain(ln)
|
||||
elif (ln.get("보임") or "").strip():
|
||||
self.text_line(ln)
|
||||
i += 1
|
||||
saved = self.blk.get("저장") or {}
|
||||
check = {COMP[c]: saved.get(c) or 0 for c in COMP} | {"계": saved.get("T") or 0}
|
||||
out = self.o.done("비목버림", None, check, "산근", self.file)
|
||||
out["원본"] |= {"원파일": True, "확정": dict(self.got)}
|
||||
return out
|
||||
|
||||
|
||||
def block(blk: dict, no: int, keys: bool = True, file: str = "", names: dict | None = None) -> dict:
|
||||
"""EST 산근 블록(`M02_San_Estx.read` 산근 한 개 · 번 = 파일 안 차례 1부터) → {로직, 원본}.
|
||||
names = 같은 파일 산근 코드 → 번(안쪽 산근 「단산N」)."""
|
||||
return Block(blk, no, keys, file, names or {}).run()
|
||||
|
||||
|
||||
_POW = re.compile(r"(\([^()]*\)|" + est.NUMRE + r"|[A-Za-z][A-Za-z0-9]*)\s*\^\s*(\d)(?![\d.])")
|
||||
|
||||
|
||||
def _power(seg: str) -> str:
|
||||
"""「x^2」 · 「(a+b)^2」 → 곱 되풀이(EST 토큰에 ^ 없음 · 정수 지수만)."""
|
||||
for _ in range(8):
|
||||
new = _POW.sub(lambda m: "(" + "×".join([m.group(1)] * int(m.group(2))) + ")", seg)
|
||||
if new == seg:
|
||||
break
|
||||
seg = new
|
||||
return seg
|
||||
@@ -19,6 +19,7 @@ from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete import M02_Doc_FromNative as from_native
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel
|
||||
from M02_MasterTemplete import M02_Hopyo_Forget as hopyo_forget
|
||||
@@ -63,6 +64,9 @@ def _who(session: Session) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]:
|
||||
# 경쟁사 원파일(810-2) — 같은 공사 엑셀 HP 에 붙음 · 원파일 우선
|
||||
if from_native.is_native(name):
|
||||
return from_hopyo.upload_native(store.root(), name, data, who)
|
||||
mark = hopyo_store.fingerprint(data)
|
||||
hopyo_forget.sweep(store.root()) # 하루 지난 작업 중 HP 끝냄(743-1)
|
||||
if same := next((r for r in hopyo_store.rows(store.root()) if r.get("지문") == mark), None):
|
||||
@@ -103,7 +107,7 @@ def hopyo_upload(
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""엑셀(xls · xlsx) 올리기 — 「일위대가」 시트를 읽어 결과 · 격자 저장 · 20 MB ·
|
||||
엉터리 · 시트 없음 400."""
|
||||
엉터리 · 시트 없음 400 · 원파일(.estx · .stc) = 산근 원파일 우선(810-2 `upload_native`)."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
data = file.file.read(HOPYO_MAX_BYTES + 1)
|
||||
if len(data) > HOPYO_MAX_BYTES:
|
||||
|
||||
@@ -304,6 +304,7 @@ def cost_line(at: int, r: dict[str, Any], res: dict, tags: list[str]) -> dict[st
|
||||
"규격": ld.get("규격"),
|
||||
"단위": ld.get("단위"),
|
||||
"단": r.get("Level"),
|
||||
"색인": r.get("Index"),
|
||||
"연결": link,
|
||||
"연결뜻": LINK.get(link or 0),
|
||||
"비목": _sc(r),
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
블록 번 = 판정 블록 번(STmate = 코드 「D00102」 숫자 · EST = 단가산출 블록 번).
|
||||
STmate 블록 끝전(비목버림 | 합버림_큰나머지)은 파일 설정 — 참조 평가기로 둘 다 셈해
|
||||
엑셀 계와 맞는 것.
|
||||
원파일(.estx · .stc · 810-2 `M02_Doc_FromNative`) = 같은 자리 칸 `원파일`(둘 꼴) — 있으면 원파일 우선
|
||||
(그 블록 번이 원파일에 있을 때) · 엑셀 올림 때 같은 공사 원파일이 다른 HP 에 있으면 붙임 · 원파일 올림 =
|
||||
같은 공사 엑셀 HP 에 붙임(없으면 원파일로 새 HP · `upload_native`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -20,6 +23,7 @@ from typing import Any
|
||||
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_time import utc_now_text
|
||||
from M02_MasterTemplete import M02_Doc_FromNative as fn
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_San_Emit as em
|
||||
@@ -58,10 +62,54 @@ def keep(folder: str | Path, key: str, name: str, data: bytes) -> str | None:
|
||||
return None
|
||||
path = _path(folder, key)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
atomic_write_json(path, {"파일": name, "시트": sheets})
|
||||
got = {"파일": name, "시트": sheets}
|
||||
native = next((n for n in _natives(folder) if fn.same(n, sheets)), None)
|
||||
atomic_write_json(path, got | ({"원파일": native} if native else {}))
|
||||
return None
|
||||
|
||||
|
||||
# ── 원파일(810-2) ──────────────────────────────────────────────────────
|
||||
def _saved(folder: str | Path) -> list[tuple[Path, dict]]:
|
||||
out = []
|
||||
for path in sorted((hopyo_store._dir(folder) / DIRNAME).glob("HP-*.json")):
|
||||
try:
|
||||
out.append((path, json.loads(path.read_bytes())))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def _natives(folder: str | Path) -> list[dict]:
|
||||
return [d["원파일"] for _, d in _saved(folder) if isinstance(d.get("원파일"), dict)]
|
||||
|
||||
|
||||
def upload_native(folder: str | Path, name: str, data: bytes, who: Any) -> dict[str, Any]:
|
||||
"""원파일 올림 — 같은 공사 엑셀 HP 에 원파일을 붙임(`붙음` 키들 · 첫 HP 답) · 없으면 원파일로
|
||||
새 HP(EST = 일위대가 호표 · STmate = 호표 없음 · 산근만). 같은 파일 다시 = 그 HP(`이어서`)."""
|
||||
mark = hopyo_store.fingerprint(data)
|
||||
if same := next((r for r in hopyo_store.rows(folder) if r.get("지문") == mark), None):
|
||||
return {**same, "이어서": True, "원파일": True}
|
||||
if why := unreadable(data):
|
||||
raise store.StoreError(400, why)
|
||||
try:
|
||||
native = fn.read(name, data)
|
||||
except fn.NativeError as e:
|
||||
raise store.StoreError(400, str(e)) from e
|
||||
joined = []
|
||||
for path, got in _saved(folder):
|
||||
if got.get("시트") and fn.same(native, got["시트"]):
|
||||
atomic_write_json(path, {**got, "원파일": native})
|
||||
joined.append(path.stem)
|
||||
rows = {r["키"]: r for r in hopyo_store.rows(folder)}
|
||||
if joined and joined[0] in rows:
|
||||
return {**rows[joined[0]], "원파일": True, "붙음": joined}
|
||||
row = hopyo_store.save(folder, name, fn.hopyo(native), who, None, mark)
|
||||
path = _path(folder, row["키"])
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
atomic_write_json(path, {"파일": name, "원파일": native})
|
||||
return {**row, "원파일": True}
|
||||
|
||||
|
||||
def has(folder: str | Path, key: str) -> bool:
|
||||
return _path(folder, key).is_file()
|
||||
|
||||
@@ -94,6 +142,11 @@ def emit(folder: str | Path, key: str, no: int) -> dict[str, Any]:
|
||||
saved = json.loads(_path(folder, key).read_bytes())
|
||||
except (OSError, ValueError) as e:
|
||||
raise store.StoreError(409, RELOAD) from e
|
||||
native = saved.get("원파일")
|
||||
if isinstance(native, dict) and int(no) in fn.numbers(native): # 원파일 우선(810-2)
|
||||
return fn.emit(native, int(no))
|
||||
if not saved.get("시트"):
|
||||
raise store.StoreError(404, f"없는 단산{no}")
|
||||
sheets, name = saved["시트"], str(saved.get("파일") or "")
|
||||
if book.kind_of(sheets) == "STmate":
|
||||
blk = next((b for b in book.stmate_blocks(sheets) if int(b["코드"][1:]) == int(no)), None)
|
||||
@@ -109,12 +162,17 @@ def emit(folder: str | Path, key: str, no: int) -> dict[str, Any]:
|
||||
return em.est(blk, est.parse_block(blk["글"], prices), total[blk["번"]], True, name)
|
||||
|
||||
|
||||
def _inner(source: Path, key: str, logic: dict) -> list[tuple[dict, int | None, str | None]]:
|
||||
def _inner(
|
||||
source: Path, key: str, logic: dict, native: bool = False
|
||||
) -> list[tuple[dict, int | None, str | None]]:
|
||||
"""안쪽 산근 줄 `[(호표 줄, 같은 엑셀 안쪽 블록 번, 못 잇는 까닭)]` — STmate = 줄 코드가 블록 코드 ·
|
||||
EST = 줄 이름 「단산N」(N 은 총괄표에 있는 블록). 비목 하나만 부른 줄은 그 비목이 그대로(옮김 없음)일 때만."""
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
|
||||
sheets = json.loads(_path(source, key).read_bytes())["시트"]
|
||||
saved = json.loads(_path(source, key).read_bytes())
|
||||
if native and isinstance(saved.get("원파일"), dict):
|
||||
return fn.inner(saved["원파일"], logic)
|
||||
sheets = saved["시트"]
|
||||
stmate = book.kind_of(sheets) == "STmate"
|
||||
have = {b["코드"]: int(b["코드"][1:]) for b in book.stmate_blocks(sheets)} if stmate else {}
|
||||
total = set() if stmate else set(book.est_prices(sheets)[1])
|
||||
@@ -143,7 +201,8 @@ def _link_inner(source: Path, key: str, no: int, out: dict, trail: tuple) -> Non
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
made: dict[int, dict | None] = {}
|
||||
for line, n, why in _inner(source, key, out["로직"]):
|
||||
native = bool(out["원본"].get("원파일"))
|
||||
for line, n, why in _inner(source, key, out["로직"], native):
|
||||
if n is not None and not why and (n == no or n in trail or len(trail) >= MAX_INNER):
|
||||
why = "안쪽 블록이 자기 자신을 부름"
|
||||
if n is not None and not why and n not in made:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* M02_Sangeun_Origin.ts
|
||||
* 가져온 식 단가산출 「어디서 왔나」 카드 자료(PLAN 807-1 · 요구 699) — 원문 패널 첫 화면.
|
||||
* SK `가져온.원본`(파일 · 시트 · 행 · 갈래 EST|STmate · 코드 · 표지 · 가져온때 · 글줄)에서 보이기 자료만 뽑음 ·
|
||||
* SK `가져온.원본`(파일 · 시트 · 행 · 갈래 EST|STmate · 코드 · 표지 · 가져온때 · 원파일 · 글줄)에서 보이기 자료만 뽑음 ·
|
||||
* 품셈 절 글 = 엑셀 글줄에 적힌 「…품셈 N-N-N」 글(책 · 절 번호 · 그 줄 글) — 찾기 · 잇기 없음(보이기만)
|
||||
* 화면 · DOM · fetch 없음
|
||||
* ========================================================================== */
|
||||
@@ -16,6 +16,7 @@ export interface OriginMeta {
|
||||
코드?: string | number;
|
||||
표지?: string[];
|
||||
가져온때?: string;
|
||||
원파일?: boolean; // 경쟁사 원파일(.estx · .stc)에서 옮김(810-2) — 엑셀 아님
|
||||
글줄?: { 행?: number; 글?: string }[];
|
||||
};
|
||||
}
|
||||
@@ -81,7 +82,7 @@ export function originOf(meta: OriginMeta | null | undefined): Origin | null {
|
||||
const block = src.갈래 === "EST" && code !== "" ? `단산${code}` : String(code);
|
||||
const rows = (src.행 ?? []).filter((x) => Number.isFinite(x));
|
||||
return {
|
||||
프로그램: src.갈래 ?? "",
|
||||
프로그램: `${src.갈래 ?? ""}${src.원파일 ? " · 원파일" : ""}`,
|
||||
파일: src.파일 ?? "",
|
||||
블록: block,
|
||||
행: rows.length ? `${src.시트 ? `${src.시트} ` : ""}${rows.join("~")}행` : (src.시트 ?? ""),
|
||||
|
||||
Reference in New Issue
Block a user