- 규칙 보강(기존 규칙 뒤에 붙임 · 이미 분류된 줄 변동 0) - build_재료_자재품목.py 끝에서 분류 적용 — 다시 돌려도 두 칸 · 머리 비고 유지 - 이름 빈 24줄은 비고에 까닭만 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
157 lines
7.7 KiB
Python
157 lines
7.7 KiB
Python
"""상용 적산 자료(ESTX 2026-07) 자재 → 재료_자재품목.json — 개발용 · 배포 전 판권 구매 뒤 갱신.
|
|
|
|
ESTX 자재 표 = StuffcDat(칸 뜻: 다산소프트/ESTX_값사전_자재노무기계.md).
|
|
열 한 벌 = 키 · 원문번호 · 구분 · 상세구분 · 이름 · 규격 · 단위 · 물가자료 · 유통물가 · 물가정보 · 거래가격 · 관급 · 출처 · 비고 · 면수.
|
|
Cost2..5 = 물가지 넷 · Page2..5 = 면수. TC(채택) · Cost6(견적가)는 마스터 아님 — 뺌.
|
|
품명에 「관급」 인 줄의 Cost1(가격정보) = 조달청 관급 단가 — 「관급」 열 · 기간은 비고 「조달 26.7」.
|
|
그 밖의 줄에 남은 Cost1 은 출처가 조달인지 물가지인지 원문이 안 가름 — 값 열에 넣지 않고 비고 「가격정보 ○○」 로만.
|
|
0 · 미저장 = 값 없음(null). 이름·규격은 원문 그대로(띄어쓰기·오자 포함). 코드 없는 빈 줄은 뺌.
|
|
ESTX 에 대·중분류 칸이 없어 구분·상세구분은 build_자재품목_분류.py 가 품명 글자 규칙으로 채움(빌더 끝에서 부름).
|
|
|
|
쓰기: ./venv/Scripts/python.exe resources/master_data/scripts/build_재료_자재품목.py
|
|
쓴 뒤 원본과 대조.
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
import math
|
|
import random
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
import master_keys as mk # noqa: E402
|
|
from _migrate_base import dump_master # noqa: E402
|
|
from compare_estx import ESTX, ROOT, SLOTS, Estx # noqa: E402
|
|
|
|
OUT = ROOT / "resources/master_data/재료_자재품목.json"
|
|
BASE = SLOTS[0] # Cost1 「가격정보」 — 관급이면 관급 열 · 아니면 비고
|
|
MARKET = SLOTS[1:5] # 물가지 넷 · 견적가(Cost6)는 마스터 아님
|
|
# 유류·전력 — 유가·전기요금은 재료_유가전력(유류는 값이 비고 글뿐 · 중유는 받지 않음 · 대장 「폐기」)
|
|
DROP = {
|
|
"M0003039885": "휘발유",
|
|
"M0003039886": "경유",
|
|
"M0004805582": "선박용경유",
|
|
"M0003039887": "중유",
|
|
"M0003039888": "등유",
|
|
"M0005250064": "전력",
|
|
"M0005250065": "전력(주택용저압)",
|
|
}
|
|
|
|
|
|
def num(v):
|
|
if not v:
|
|
return None
|
|
return int(v) if float(v).is_integer() else v
|
|
|
|
|
|
def note(base, page, guard: str | None) -> str | None:
|
|
"""비고 — 관급이면 「조달 <기간>」 · 남은 가격정보는 「가격정보 <값> · <면수>」."""
|
|
if guard is not None:
|
|
return f"조달 {str(guard).removeprefix('조달 ')}".strip()
|
|
if base is None:
|
|
return None
|
|
return " · ".join(x for x in (f"가격정보 {base:,}", page) if x)
|
|
|
|
|
|
def build(rows: list[dict]) -> list[dict]:
|
|
out = []
|
|
for r in sorted(rows, key=lambda r: r["Index"]):
|
|
code = r.get("Code")
|
|
if not code or code in DROP:
|
|
continue
|
|
base, page = num(r.get("Cost1")), r.get("Page1") or None
|
|
public = "관급" in r.get("Name", "") and base is not None
|
|
면수 = {slot: r.get(f"Page{i}") or None for i, slot in enumerate(MARKET, 2)}
|
|
out.append(
|
|
{
|
|
"열쇠": code,
|
|
"구분": None,
|
|
"상세구분": None,
|
|
"이름": r.get("Name", ""),
|
|
"규격": r.get("Size", ""),
|
|
"단위": r.get("Unite", ""),
|
|
**{slot: num(r.get(f"Cost{i}")) for i, slot in enumerate(MARKET, 2)},
|
|
"관급": base if public else None,
|
|
"출처": f"자재품목 {code}",
|
|
"비고": note(base, page, page if public else None),
|
|
"면수": 면수 if any(면수.values()) else None,
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def check(rows: list[dict]) -> None:
|
|
"""원본 대조 — 줄 수 · 값 열마다 합 · 관급·가격정보 가름 · 표본 쉰 줄."""
|
|
data = json.loads(OUT.read_text(encoding="utf-8"))
|
|
got = {r["열쇠"]: r for r in data["줄"]}
|
|
raw = {r["Code"]: r for r in rows if r.get("Code") and r["Code"] not in DROP}
|
|
assert len(got) == len(data["줄"]) == len(raw), (len(got), len(raw))
|
|
print(
|
|
f"줄 {len(got)} = 원본 코드 있는 줄 {len(raw)}"
|
|
f" (코드 없는 빈 줄 {sum(1 for r in rows if not r.get('Code'))} · 유류 {len(DROP)} 뺌)"
|
|
)
|
|
for i, slot in enumerate((*MARKET, "관급"), 2):
|
|
a = math.fsum(r.get(f"Cost{i}") or 0 for r in raw.values())
|
|
if slot == "관급": # Cost1 가운데 관급 줄만
|
|
a = math.fsum(r.get("Cost1") or 0 for r in raw.values() if "관급" in r.get("Name", ""))
|
|
b = math.fsum(r[slot] or 0 for r in got.values())
|
|
n = sum(1 for r in got.values() if r[slot] is not None)
|
|
assert abs(a - b) < 1e-3, (slot, a, b)
|
|
print(f" {slot}: 값 {n}줄 · 합 {b:,.1f} = 원본 {a:,.1f}")
|
|
left = [r for r in got.values() if str(r["비고"] or "").startswith("가격정보")]
|
|
print(
|
|
f" 비고에만 남긴 가격정보 {len(left)}줄 · 관급 기간 비고 {sum(1 for r in got.values() if str(r['비고'] or '').startswith('조달'))}줄"
|
|
)
|
|
random.seed(20260719)
|
|
for code in random.sample(sorted(raw), 50):
|
|
r, g = raw[code], got[code]
|
|
assert (g["이름"], g["규격"], g["단위"]) == (
|
|
r.get("Name", ""),
|
|
r.get("Size", ""),
|
|
r.get("Unite", ""),
|
|
)
|
|
public = "관급" in r.get("Name", "")
|
|
assert (g["관급"] or 0) == ((r.get("Cost1") or 0) if public else 0), code
|
|
for i, slot in enumerate(MARKET, 2):
|
|
assert (g[slot] or 0) == (r.get(f"Cost{i}") or 0), (code, slot)
|
|
assert (g["면수"] or {}).get(slot) == (r.get(f"Page{i}") or None), (code, slot)
|
|
print(" 표본 50줄 이름·규격·단위·값 5·면수 4 같음")
|
|
|
|
|
|
def main():
|
|
rows = Estx().table("StuffcDat")
|
|
head = {
|
|
"그룹": "재료",
|
|
"원문": "자재품목",
|
|
"판": "2026-07",
|
|
"원천": [
|
|
{
|
|
"경로": ESTX.relative_to(ROOT).as_posix(),
|
|
"해시": hashlib.sha256(ESTX.read_bytes()).hexdigest(),
|
|
"역할": "primary",
|
|
}
|
|
],
|
|
"비고": [
|
|
"다산소프트 EST 2026-07 자재 · 개발용 · 배포 전 판권 구매",
|
|
"값 열 다섯 = 물가지 넷(물가자료 · 유통물가 · 물가정보 · 거래가격) · 관급(조달) — 채택값·기본가 없음 · 쓰는 곳이 값을 고르고 출처를 밝힘",
|
|
"유류·전력은 뺌 — 유가·전기요금은 재료_유가전력(유류는 값이 비고 글뿐 · 중유는 받지 않음)",
|
|
"원문번호(다산소프트 품번) 칸 없음 — 대조는 _키대장.json 에만",
|
|
"관급 = 원문 「가격정보」 칸의 조달청 관급 단가(철근 · 레미콘 · 아스콘 · H형강 등) · 기간은 비고 「조달 26.7」",
|
|
"조달청에만 있는 품목 380 줄은 관급 열만 채움 — 출처 「자재품목 조달청 …」 · 원문번호는 _키대장.json 에만",
|
|
"원문 「가격정보」 칸에 시중 값이 남은 줄은 값 열에 넣지 않고 비고 「가격정보 ○○」 로만 둠 — 출처가 조달인지 물가지인지 원문이 안 가름",
|
|
"구분 · 상세구분 = 품명 글자 규칙으로 채움(scripts/build_자재품목_분류.py · 묶음 목록 ref/_묶음_자재품목.md) · 조달청 줄은 조달청 품명을 따름 · 못 가린 줄은 「미분류」",
|
|
"면수 = 물가지 값의 원문 면수 · 화면에는 접어 둠",
|
|
],
|
|
}
|
|
dump_master(OUT, head, build(rows))
|
|
check(rows)
|
|
import build_자재품목_분류 as cls # noqa: E402
|
|
|
|
print("구분·상세구분 채움", len(cls.apply()), "줄")
|
|
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|