- 자재 0 해소 첫 걸음: 사급 자원 19건(AR-M 17·AR-X 2, 이름·규격·단위, 단가 칸 없음) resources/data_resource_catalog/ 신설 — 임도 60 + 사방 11 코드 범위의 진짜 자원만 - 규격이 조인 키: AR 항목은 후보가 하나여도 규격이 같아야 고름 · 품셈 칸에 규격이 없으면 「규격 미정 — 후보 N」(성공으로 안 셈) - 형식이 하나뿐인 계열만 규격으로 고름(공기압축기(이동식) 3.5·10.3) · 형식 둘이면 규격 미정 - 범위 별칭 한 벌: 화약공→화약취급공(1016) scope FP-09-05 · pum_edition 다르면 안 씀 · scope 없음·겹친 두 코드는 읽을 때 오류 - 일위대가: 맞췄으나 단가 층 없는 줄은 조용히 안 빠지고 드러냄 · 기계 몫이면 막음 - ResourceAxis.py 700줄 초과분(조사용 덤프)을 _Dump.py 로 뗌 - 돌망태 품셈 절 표기 13-8 → 13-11 바로잡음 - 결과: 자원 줄 575→592(자재 0→11) · 못 맞춤 508→491 · 내역 금액 변화 0(막힌 공종 그대로) - 시험 test_b09_resource_axis_join.py 8건 · 전체 1377 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
95 lines
4.1 KiB
Python
95 lines
4.1 KiB
Python
"""B09 원가계산 — 자원 축 **조사용 덤프** (`B09_Estimation_ResourceAxis` 에서 갈라냄).
|
|
|
|
⚠ **정본이 아니다.** 자원 축의 정본은 `build_unit_prices` 가 부를 때마다
|
|
**메모리에서 새로 돈 값**이고, 이 파일이 쓰는 JSON 은 시험·조사용 자취일 뿐이다
|
|
(명세 1장 2026-09-13 정정 — 옛 덤프 418/497 을 정본으로 읽어 추산이 틀렸던 자리).
|
|
부르는 곳이 없어도 지우지 않는다.
|
|
⚠ **왜 갈랐나** — 본 파일이 700줄 제한을 넘어(732줄) 조인 키 규칙을 더하기 전에 뗐다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from typing import Any
|
|
|
|
from B09_Estimation.B09_Estimation_ResourceAxis import (
|
|
_MASTER_SUBPATH,
|
|
AxisResult,
|
|
_project_root,
|
|
)
|
|
|
|
#: 자원 축 산출물이 나가는 자리 — **메인의 `data_work_item_master/` 안에 넣지 않는다.**
|
|
#: 메인이 품셈을 다시 돌리면 그 폴더가 덮이므로 섞으면 사라진다.
|
|
OUTPUT_SUBPATH = ("resources", "data_cost_resource_axis")
|
|
|
|
|
|
def _master_file_fingerprint(master: dict[str, Any]) -> dict[str, str]:
|
|
"""공종 마스터 **파일 자체**의 지문. 낡은 파생물을 드러내는 유일한 근거다.
|
|
|
|
`dataset_version`(품셈 원판 지문)은 마스터가 다시 생성돼도 그대로라, 그것만
|
|
적어 두면 「내 자원 축이 옛 마스터에서 나왔다」는 사실이 안 보인다.
|
|
"""
|
|
effective_date = master.get("effective_date", "")
|
|
file_name = f"work_item_master_{effective_date}.json"
|
|
path = os.path.join(_project_root(), *_MASTER_SUBPATH, file_name)
|
|
try:
|
|
with open(path, "rb") as handle:
|
|
digest = hashlib.sha256(handle.read()).hexdigest()
|
|
except OSError:
|
|
return {"file": file_name, "sha256": ""}
|
|
return {"file": file_name, "sha256": digest}
|
|
|
|
|
|
def write_resource_axis(
|
|
result: AxisResult,
|
|
master: dict[str, Any],
|
|
*,
|
|
output_dir: str | None = None,
|
|
) -> dict[str, str]:
|
|
"""자원 축과 못 맞춘 목록을 파일로 낸다. 만든 파일 경로를 돌려준다."""
|
|
directory = output_dir or os.path.join(_project_root(), *OUTPUT_SUBPATH)
|
|
os.makedirs(directory, exist_ok=True)
|
|
effective_date = master.get("effective_date", "")
|
|
|
|
axis_path = os.path.join(directory, f"resource_axis_{effective_date}.json")
|
|
unmatched_path = os.path.join(directory, f"unmatched_{effective_date}.json")
|
|
|
|
axis_payload = {
|
|
"schema_version": "1.0",
|
|
"dataset_id": "resource_axis_forest",
|
|
"effective_date": effective_date,
|
|
# 어느 공종 축 판에 붙인 것인지 — 세 쪽을 그대로 옮겨 적는다(PLAN 9-2).
|
|
"source_dataset_version": master.get("dataset_version", {}),
|
|
# ⚠ 위 지문은 **품셈 원판**의 것이라 B08 이 마스터를 다시 생성해도 안 움직인다.
|
|
# 낡음을 실제로 드러내려면 **마스터 파일 자체의 지문**이 있어야 한다.
|
|
"source_master_file": _master_file_fingerprint(master),
|
|
"policy": {
|
|
"axis": "resource_only",
|
|
"work_item_axis_owner": "B08",
|
|
"material_amounts_are_before_surcharge": True,
|
|
},
|
|
"stats": {
|
|
"rows": len(result.rows),
|
|
"unmatched": len(result.unmatched),
|
|
"skipped_forms": result.skipped_forms,
|
|
},
|
|
"rows": [r.as_dict() for r in result.rows],
|
|
}
|
|
unmatched_payload = {
|
|
"schema_version": "1.0",
|
|
"effective_date": effective_date,
|
|
"note": (
|
|
"못 맞춘 자원 이름. 빈칸으로 두지 않고 여기 모은다. "
|
|
"기계·자재 카탈로그가 아직 없어 그 계열은 전부 여기로 온다."
|
|
),
|
|
"rows": [u.as_dict() for u in result.unmatched],
|
|
}
|
|
|
|
for path, payload in ((axis_path, axis_payload), (unmatched_path, unmatched_payload)):
|
|
with open(path, "w", encoding="utf-8", newline="\n") as handle:
|
|
json.dump(payload, handle, ensure_ascii=False, indent=2, sort_keys=True)
|
|
|
|
return {"resource_axis": axis_path, "unmatched": unmatched_path}
|