feat(797-1a): 엑셀 불러오기 빠진 정보 표지 다섯 + 줄 단위 합격 시험 · 표본 밖 고정
- 금액 줄 못 읽음: 결과 뒤 꼬리 글은 떼고 금액 줄로 읽음(2016 틀린 줄 2 → 0) · 못 읽는 글끝 · 식못읽음 = 표지 + 보인 값 보존 - 단가 후보 여럿 · 사람 확인 / 추정(문맥 · 기계 세 줄) 표지 — 임의 선택 대신 표지 - 당초 / 적용 칸 표지(Book.applied_how) · 조정 비율 · 낙찰률 미지원 표지 - FromHopyo.keep 이 읽기 불가 사유(빈 파일 · DRM 암호 · 형식 오류)를 돌려줌 - 사람 확인 표지는 「참고 · 」 머리 — 못 옮김 표지와 가름 · test_793_2 는 못 옮김 표지만 셈(낡은 기대 수정) - test_797_1a 신설: 작은 견본 · 표지 없는 틀린 줄 0 · 표본 밖 수목 142 · 대흥공통 46 · 2016 374 고정 · 버림1 일치율 감시 - 금액 결과 그대로(엔진 · 수식 · Copy 손대지 않음) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -162,17 +162,33 @@ def _n(v: Any) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def applied(r: list[Any]) -> dict[str, float]:
|
||||
"""총괄표 줄 → {T, L, M, S} — 적용 칸(5~8) 합이 맞으면 적용 · 아니면 당초(25~28)."""
|
||||
def applied_how(r: list[Any]) -> tuple[dict[str, float], str | None]:
|
||||
"""총괄표 줄 → ({T, L, M, S}, 쓴 칸 표지) — 적용 칸(5~8) 합이 맞으면 적용 · 아니면 당초(25~28).
|
||||
표지 = 당초를 쓴 경우(까닭) · 적용 ≠ 당초인 경우(차이) · 그 밖 None(PLAN 797-1a)."""
|
||||
a = {k: _n(r[j]) for k, j in (("T", 5), ("L", 6), ("M", 7), ("S", 8))}
|
||||
first = {k: _n(r[j]) for k, j in (("T", 25), ("L", 26), ("M", 27), ("S", 28))}
|
||||
names = {"T": "계", "L": "노무비", "M": "재료비", "S": "경비"}
|
||||
gap = " · ".join(
|
||||
f"{names[k]} 적용 {a[k] or 0:g} · 당초 {first[k]:g}"
|
||||
for k in "TLMS"
|
||||
if a["T"] and first[k] is not None and abs((a[k] or 0) - first[k]) >= 0.5
|
||||
)
|
||||
if (
|
||||
a["T"]
|
||||
and all(a[k] is not None for k in "LMS")
|
||||
and abs(a["L"] + a["M"] + a["S"] - a["T"]) < 0.5
|
||||
and (a["L"] or a["M"] or a["S"])
|
||||
):
|
||||
return a
|
||||
return {k: (_n(r[j]) or 0.0) for k, j in (("T", 25), ("L", 26), ("M", 27), ("S", 28))}
|
||||
return a, (f"적용 칸 사용 — 당초와 다름 ({gap})" if gap else None)
|
||||
got = {k: (v or 0.0) for k, v in first.items()}
|
||||
if not got["T"]:
|
||||
return got, None # 쓸 값이 없음 — 표지할 것 없음
|
||||
why = "적용 칸 합이 안 맞음" if a["T"] else "적용 칸 비어 있음"
|
||||
return got, f"당초 칸 사용 — {why}" + (f" ({gap})" if gap else "")
|
||||
|
||||
|
||||
def applied(r: list[Any]) -> dict[str, float]:
|
||||
return applied_how(r)[0]
|
||||
|
||||
|
||||
def est_prices(book: dict[str, list]) -> tuple[list[dict[str, Any]], dict[int, dict[str, Any]]]:
|
||||
@@ -209,8 +225,9 @@ def est_prices(book: dict[str, list]) -> tuple[list[dict[str, Any]], dict[int, d
|
||||
for r in book.get("단가산출총괄표", [])[3:]:
|
||||
if len(r) > 28 and str(r[1]).strip().isdigit():
|
||||
n = int(str(r[1]).strip())
|
||||
san[n] = {k: float(v or 0) for k, v in applied(r).items()}
|
||||
san[n] |= {"이름": flat(r[2]), "규격": flat(r[3])}
|
||||
got, note = applied_how(r)
|
||||
san[n] = {k: float(v or 0) for k, v in got.items()}
|
||||
san[n] |= {"이름": flat(r[2]), "규격": flat(r[3]), "칸": note}
|
||||
for k in "TLMS":
|
||||
if san[n][k] or k != "T":
|
||||
one = {"종류": "산근", "이름": f"단산{n}", "규격": ""}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
계약 전), 글줄 [{행, 글, 자리?, 자리들?, 값?, 표지?}], 코드, 표지 [..], 계약? {낙찰률, 금액}}
|
||||
· 글줄 자리 = 셈 줄 이름 | `mid:이름` | `단계:이름` · 값 = 그 줄 엑셀 금액(계)
|
||||
표지 = 못 옮긴 까닭(0 이 목표) · STmate 계약단가(낙찰률)는 B09 몫.
|
||||
「참고 · …」 표지(`NOTE`) = 엑셀에 없는 정보를 채우거나 건너뛴 자리(797-1a · 요구 658) —
|
||||
금액 줄 못 읽음 · 단가 후보 여럿 · 단가 추정 · 당초 칸 사용 · 조정 비율 미지원 · 낙찰률 미적용.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -36,6 +38,7 @@ RESERVED = {"노무비", "재료비", "경비", "계", "번지", "단계", "줄"
|
||||
RESERVED |= {"또는", "만약", "올림", "버림", "반올림", "최소", "최대"}
|
||||
_KEY = re.compile(r"[A-Z]{2}\d{6}")
|
||||
_LABEL = re.compile(r"'([^']*)'")
|
||||
NOTE = "참고 · " # 사람 확인 표지 머리(못 옮김 표지와 가름)
|
||||
|
||||
|
||||
class EmitError(Exception):
|
||||
@@ -212,7 +215,7 @@ class Out:
|
||||
if value is not None:
|
||||
one["값"] = value
|
||||
if mark:
|
||||
one["표지"] = mark
|
||||
one["표지"] = f"{one['표지']} / {mark}" if one.get("표지") else mark
|
||||
self.marks.append(f"{at}: {mark}")
|
||||
|
||||
def middle(self, name: str, formula: str, show: int | None, at: Any, glyph: str) -> None:
|
||||
@@ -435,6 +438,19 @@ def _cols(row: dict) -> dict[str, Any]:
|
||||
return _money({"L": row["D"], "M": row["E"], "S": row["F"]}) | {"계": row["C"] or 0}
|
||||
|
||||
|
||||
def _num(v: Any) -> float:
|
||||
try:
|
||||
return float(str(v).strip())
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
|
||||
def _ratios(meta: dict) -> str:
|
||||
"""결과금액 조정 비율 ADJL · ADJM · ADJS 중 1 아닌 것 → 「노무비 0.9 · …」 · 모두 1 = 빈 글."""
|
||||
pairs = [(COMP[c], meta.get(f"ADJ{c}")) for c in COMP]
|
||||
return " · ".join(f"{n} {v}" for n, v in pairs if v is not None and abs(_num(v) - 1) > 1e-9)
|
||||
|
||||
|
||||
def stmate(
|
||||
block: dict, nodes: list[dict], prices: dict, rule: str, keys: bool = False, file: str = ""
|
||||
) -> dict:
|
||||
@@ -473,6 +489,12 @@ def stmate(
|
||||
adjust, sbm = None, (meta.get("SBM") or "").strip()
|
||||
if meta.get("ADJ") == "T" and sbm in COMP:
|
||||
adjust = {"옮김": COMP[sbm]}
|
||||
elif meta.get("ADJ") == "T" and _ratios(meta): # 비율 조정 — 엔진에 없음(몰래 건너뛰지 않음)
|
||||
o.marks.append(f"{NOTE}조정 비율 미지원 · 사람 확인 ({_ratios(meta)})")
|
||||
if meta.get("UC") != "T" and _num(meta.get("NAG")):
|
||||
o.marks.append(
|
||||
f"{NOTE}낙찰률 {meta['NAG']} 있으나 계약단가 아님(UC) — 낙찰률 미적용 · 사람 확인"
|
||||
)
|
||||
more = {}
|
||||
if meta.get("UC") == "T" and meta.get("NAG"): # 계약단가 = B09 몫(엔진 밖)
|
||||
more["계약"] = {"낙찰률": float(meta["NAG"]), "금액": _cols(block["머리"])}
|
||||
@@ -502,6 +524,8 @@ def est(block: dict, lines: list[dict], want: dict, keys: bool = False, file: st
|
||||
mine = {k: c for c, k in des.items() if isinstance(k, int)}
|
||||
o = Out(block.get("이름"), block.get("단위"), "EST", block.get("번"))
|
||||
rows = block.get("글행") or []
|
||||
if want.get("칸"): # 당초 칸 사용 · 적용 ≠ 당초(797-1a)
|
||||
o.marks.append(f"{NOTE}{want['칸']}")
|
||||
for c, k in des.items():
|
||||
if k is None:
|
||||
o.marks.append(f"성분 {COMP[c]} {want[c]} 을 낸 줄 없음")
|
||||
@@ -519,7 +543,11 @@ def est(block: dict, lines: list[dict], want: dict, keys: bool = False, file: st
|
||||
o.middle(eng, num(L["값"]), None, at, glyph) # 얼린 값(금액엔 보인 값이 박힘)
|
||||
continue
|
||||
if form not in ("돈", "값"):
|
||||
o.glyph(at, glyph)
|
||||
shown = L.get("보인") if form == "글끝" else L.get("값") if form == "식못읽음" else None
|
||||
if shown is None:
|
||||
o.glyph(at, glyph)
|
||||
else: # 금액 줄 못 읽음 — 보인 값 얼림(글줄 값으로 보존)
|
||||
o.glyph(at, glyph, value=shown, mark=f"{NOTE}금액 줄 못 읽음 · 보인 값 얼림")
|
||||
continue
|
||||
head = {} if comp else {"합밖": True}
|
||||
try:
|
||||
@@ -535,6 +563,10 @@ def est(block: dict, lines: list[dict], want: dict, keys: bool = False, file: st
|
||||
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["값"])
|
||||
if role.get("모호"): # 임의 선택 아님 — 후보를 사람이 고름
|
||||
o.glyph(at, glyph, mark=f"{NOTE}단가 후보 여럿({role['남은']}) · 사람 확인")
|
||||
elif role.get("갈림") in ("문맥", "기계 세 줄"):
|
||||
o.glyph(at, glyph, mark=f"{NOTE}추정 — 단가 정체 근거 {role['갈림']}")
|
||||
ref[k] = f"줄.'{one['이름']}'"
|
||||
continue
|
||||
if kind == "값줄":
|
||||
|
||||
@@ -35,6 +35,7 @@ RES = re.compile(
|
||||
)
|
||||
_PLAIN = re.compile(r"[\s\d.,×÷+\-()A-Za-z가-힣㎡㎥]*")
|
||||
_LABEL = re.compile(r"^(?P<l>[^::=]{0,40}?)\s*[::]\s*(?P<r>.*)$")
|
||||
TAIL = re.compile(r"^\s*(" + NUMRE + r")\s*\.?\s*[::][^=]*$") # 결과 뒤 꼬리 글 「3,094.2 : 채집」
|
||||
_HEADING = re.compile(r"^\s*(\d+|[가-하])\s*[.)]")
|
||||
|
||||
|
||||
@@ -144,8 +145,13 @@ def parse_line(text: str) -> dict[str, Any]:
|
||||
return {"꼴": "값", "라벨": label, "비목": label_comp(label), "값": num(m.group(1))}
|
||||
return {"꼴": "글"}
|
||||
segs = rhs.split("=")
|
||||
mres = RES.match(segs[-1])
|
||||
mres = RES.match(segs[-1]) or TAIL.match(segs[-1]) # 꼬리 글은 떼고 읽음(797-1a)
|
||||
if not mres:
|
||||
lead = re.match(r"\s*(" + NUMRE + r")", segs[-1])
|
||||
if (
|
||||
label and lead and re.search(r"\d", "=".join(segs[:-1]))
|
||||
): # 금액 줄 못 읽음 — 보인 값 보존
|
||||
return {"꼴": "글끝", "라벨": label, "보인": num(lead.group(1))}
|
||||
return {"꼴": "글끝"}
|
||||
shown = num(mres.group(1))
|
||||
toks = None
|
||||
@@ -313,6 +319,7 @@ def identify(lines: list[dict[str, Any]]) -> None:
|
||||
ids, how = ({x for x in hit if not x[2] or x[2] in ctx} or hit or ids), "문맥"
|
||||
role["모호"] = len(ids) != 1
|
||||
role["갈림"] = how
|
||||
role["남은"] = len(ids)
|
||||
pick = sorted(ids)[0]
|
||||
p = next(p for p in cands if _ident(p) == pick)
|
||||
role["정체"] = {"종류": p["종류"], "이름": p["이름"], "규격": p["규격"], "비목": p["비목"]}
|
||||
|
||||
@@ -30,19 +30,32 @@ def _path(folder: str | Path, key: str) -> Path:
|
||||
return hopyo_store._dir(folder) / DIRNAME / f"{key}.json"
|
||||
|
||||
|
||||
def keep(folder: str | Path, key: str, name: str, data: bytes) -> None:
|
||||
"""올린 엑셀 → 산근 시트 둠(STmate · EST 꼴만 · 못 읽으면 안 둠 — 올리기는 됨)."""
|
||||
def unreadable(data: bytes) -> str | None:
|
||||
"""읽기 불가 사유(797-1a) — 빈 파일 · DRM 암호(파일 머리 `DRMONE`) · 아니면 None."""
|
||||
if not data:
|
||||
return "빈 파일"
|
||||
if b"DRMONE" in data[:16]:
|
||||
return "DRM 암호 파일 — 암호 풀린 사본을 올림"
|
||||
return None
|
||||
|
||||
|
||||
def keep(folder: str | Path, key: str, name: str, data: bytes) -> str | None:
|
||||
"""올린 엑셀 → 산근 시트 둠(STmate · EST 꼴만 · 못 읽으면 안 둠 — 올리기는 됨).
|
||||
돌려줌 = 못 읽은 사유(빈 파일 · DRM 암호 · 형식 오류) · 둠 · 산근 꼴 아님 = None."""
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
|
||||
if why := unreadable(data):
|
||||
return why
|
||||
try:
|
||||
sheets = book.sheets(name, data)
|
||||
if not book.kind_of(sheets):
|
||||
return
|
||||
except Exception: # noqa: BLE001 — 산근은 덤(가져온 식 열기만 막힘)
|
||||
return
|
||||
except Exception as e: # noqa: BLE001 — 산근은 덤(가져온 식 열기만 막힘) · 사유는 돌려줌
|
||||
return f"엑셀 형식 오류 — {type(e).__name__}: {str(e)[:80]}"
|
||||
if not book.kind_of(sheets):
|
||||
return None
|
||||
path = _path(folder, key)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
atomic_write_json(path, {"파일": name, "시트": sheets})
|
||||
return None
|
||||
|
||||
|
||||
def has(folder: str | Path, key: str) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user