⚠ 또 하나의 단위 불일치 — 층따기 9-18 은 [주] 공식 `Q1 = 3600×q×K×f×E/㎝ = ㎥/시간` 이라 단가가 ㎥당인데 우리는 성토 비탈면적 ㎡ 2,645.21 을 보내고 있었음. 그대로 곱혀 4,102,708원이 서 있었음(B09 실측). - 매핑에 `basis_unit`·`basis_source`·`mismatch_reason` 을 두고, 우리 단위와 뜻이 다르면 **환산하지 않고** 막음(`input_missing` — 층따기 단 높이·폭은 설계 입력). 막힌 줄에도 집계값을 `spec_detail` 에 남겨 되짚게 함. - ⚠ 절 머리에 「(단위: …)」가 없는 공종은 마스터 `basis_unit` 이 빔(477 중 167만 참). 그래서 밑수 대조가 조용히 통과하고 있었음 — 매핑이 원문에서 읽어 적은 밑수를 `declared_units()` 로 함께 넘겨 대조가 서게 함. - 성토면다짐(9-17-1)은 시공량이 ㎡/시간이라 면적이 맞음 — 좁게 막아 형제 공종은 그대로. 시험 736 통과 (B05 코리도 1건 기존 깨짐, 무관). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
136 lines
5.8 KiB
Python
136 lines
5.8 KiB
Python
"""품셈 밑수 단위 대조 — **보내는 단위가 그 공종의 밑수와 같은가** (2026-09-08).
|
|
|
|
⚠ **왜 있나 — 실제로 금액이 틀렸다.**
|
|
돌쌓기(찰)를 `m · 10.0` 으로 보내는데 품셈 13-4-5 밑수는 **㎡** 였다. 받는 쪽이 그 공종의
|
|
단가(52,938.9 원/㎡)를 그대로 곱해 **529,389원**이 섰다. 면적으로 세면 26.101㎡ ×
|
|
52,938.9 = **1,381,753원** — **2.6배 차이**인데 양쪽 다 오류가 안 났다.
|
|
|
|
⚠ **오늘만 다섯 번째 「보내는 쪽과 받는 쪽 사이」 사고다**
|
|
개소가 미터로 나간 것 · 준비공 표가 통째로 안 간 것 · B군 코드가 빈 것 · 성분 줄이 안
|
|
가는 것 · 이번 단위 불일치. **전부 조용했다.** 한쪽만 보는 시험은 이 자리를 못 잡는다 —
|
|
보내는 값과 **품셈 원문**을 맞대야 잡힌다. 그래서 이 파일은 매핑이 아니라 **마스터**를 본다.
|
|
|
|
⚠ **환산하지 않는다.** m 을 ㎡ 로 바꾸는 길은 없다(두께·기울기를 지어내야 한다).
|
|
표기 차이(㎥/m3 · 개소/개)만 같은 것으로 보고, **뜻이 다른 단위는 드러낸다.**
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any, Iterable
|
|
|
|
MASTER_DIR = Path(__file__).resolve().parents[1] / "resources" / "data_work_item_master"
|
|
MASTER_PREFIX = "work_item_master_"
|
|
|
|
#: 같은 뜻인데 표기만 다른 단위 — **환산이 아니라 표기 흡수**다.
|
|
UNIT_ALIASES = {
|
|
"m3": "㎥",
|
|
"m³": "㎥",
|
|
"m2": "㎡",
|
|
"m²": "㎡",
|
|
"개": "개소",
|
|
"본": "개소",
|
|
"kg": "㎏",
|
|
"ton": "t",
|
|
"톤": "t",
|
|
}
|
|
|
|
|
|
def normalize_unit(unit: str) -> str:
|
|
"""표기만 다른 단위를 한 글자로 모은다. **뜻이 다른 단위는 안 건드린다.**"""
|
|
text = str(unit or "").strip()
|
|
return UNIT_ALIASES.get(text, text)
|
|
|
|
|
|
def load_master(path: Path | None = None) -> dict[str, Any]:
|
|
"""공종 마스터. 파일이 없으면 빈 표 — 대조를 못 할 뿐 값은 그대로 간다."""
|
|
target = path
|
|
if target is None:
|
|
files = sorted(MASTER_DIR.glob(MASTER_PREFIX + "*.json")) if MASTER_DIR.is_dir() else []
|
|
target = files[-1] if files else None
|
|
if target is None or not target.is_file():
|
|
return {}
|
|
return json.loads(target.read_text(encoding="utf-8"))
|
|
|
|
|
|
def basis_units(master: dict[str, Any] | None = None) -> dict[str, set[str]]:
|
|
"""공종코드 → 그 공종 표들이 쓰는 밑수 단위 모음.
|
|
|
|
⚠ 한 공종에 표가 여럿이면 단위도 여럿이다(흄관은 「m」와 「개소」 둘 다 있다).
|
|
**하나로 줄이지 않는다** — 줄이면 맞는 단위를 틀렸다고 말하게 된다.
|
|
"""
|
|
found = master if master is not None else load_master()
|
|
out: dict[str, set[str]] = {}
|
|
for item in found.get("work_items") or []:
|
|
code = str(item.get("work_item_code") or "")
|
|
if not code:
|
|
continue
|
|
units = {
|
|
normalize_unit(table.get("basis_unit"))
|
|
for table in item.get("tables") or []
|
|
if table.get("basis_unit")
|
|
}
|
|
if units:
|
|
out[code] = units
|
|
return out
|
|
|
|
|
|
def merge_units(table: dict[str, set[str]], extra: dict[str, str] | None) -> dict[str, set[str]]:
|
|
"""마스터 밑수에 **매핑이 원문에서 읽어 적은 밑수**를 얹는다.
|
|
|
|
⚠ **왜 얹나** — 마스터 `basis_unit` 은 절 머리의 「(단위: …)」에서만 온다. 층따기 9-18
|
|
처럼 **공식 [주]로만 단위가 밝혀지는 공종**은 그 자리가 비어(477 중 밑수가 선 것은 167)
|
|
대조가 조용히 통과한다. 매핑이 적은 값은 **마스터가 빈 자리를 채우고, 있으면 함께 둔다**
|
|
(하나로 줄이면 맞는 단위를 틀렸다고 말하게 된다).
|
|
"""
|
|
if not extra:
|
|
return table
|
|
merged = {code: set(units) for code, units in table.items()}
|
|
for code, unit in extra.items():
|
|
if unit:
|
|
merged.setdefault(code, set()).add(normalize_unit(unit))
|
|
return merged
|
|
|
|
|
|
def verify_unit_matches_basis(
|
|
rows: Iterable[dict[str, Any]],
|
|
table: dict[str, set[str]] | None = None,
|
|
extra: dict[str, str] | None = None,
|
|
) -> list[str]:
|
|
"""⚠ 인계 줄의 단위가 품셈 밑수와 다르면 알린다.
|
|
|
|
**막지는 않는다** — 여기서 줄을 빼면 「빠진 줄」이 되어 더 안 보인다. 사유를 내고
|
|
사람이 보게 한다. 코드가 없는 줄·수량이 없는 줄·마스터에 없는 코드는 대상이 아니다.
|
|
"""
|
|
known = merge_units(table if table is not None else basis_units(), extra)
|
|
if not known:
|
|
return []
|
|
out: list[str] = []
|
|
for row in rows:
|
|
code = str(row.get("work_item_code") or "")
|
|
unit = normalize_unit(row.get("unit"))
|
|
if not code or not unit or code not in known:
|
|
continue
|
|
if not row.get("in_bill"):
|
|
continue
|
|
if unit in known[code]:
|
|
continue
|
|
expected = " · ".join(sorted(known[code]))
|
|
out.append(
|
|
f"{row.get('name') or code}({code}) — 보내는 단위 「{unit}」가 품셈 밑수 "
|
|
f"「{expected}」와 다릅니다. 그대로 곱하면 금액이 틀립니다"
|
|
)
|
|
return out
|
|
|
|
|
|
def unit_for_code(code: str, table: dict[str, set[str]] | None = None) -> str:
|
|
"""그 공종이 **한 가지 밑수 단위만** 쓰면 그 단위, 아니면 빈 문자열.
|
|
|
|
⚠ 물량이 못 서는 줄에도 **단위는 맞는 것**을 실으려고 쓴다 — 「0 m」로 내면 받는 쪽이
|
|
길이로 읽고, 나중에 값이 채워질 때 축이 어긋난 채로 선다. 여럿이면 고르지 않는다.
|
|
"""
|
|
known = table if table is not None else basis_units()
|
|
units = known.get(str(code or ""), set())
|
|
return next(iter(units)) if len(units) == 1 else ""
|