diff --git a/B09_Estimation/B09_Estimation_BasisSheet.py b/B09_Estimation/B09_Estimation_BasisSheet.py index d12d1a96..fe047296 100644 --- a/B09_Estimation/B09_Estimation_BasisSheet.py +++ b/B09_Estimation/B09_Estimation_BasisSheet.py @@ -22,6 +22,7 @@ import os from typing import Any from B09_Estimation.B09_Estimation_PriceBook import PriceKind +from B09_Estimation.B09_Estimation_ResourceAxis_UnitBasis import similar_occupation_note _MANIFEST = ("resources", "data_cost_input_value", "_manifest.json") _MASTER_DIR = ("resources", "data_work_item_master") @@ -118,6 +119,9 @@ def work_item_basis(build: Any) -> list[dict[str, Any]]: source = build.factor_sources.get(work_item_code) if source and source not in notes: notes.append(str(source)) + similar = similar_occupation_note(work_item_code) + if similar and similar not in notes: + notes.append(similar) if not notes: continue rows.append( diff --git a/B09_Estimation/B09_Estimation_ResourceAxis_UnitBasis.py b/B09_Estimation/B09_Estimation_ResourceAxis_UnitBasis.py index f594434b..4a948397 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis_UnitBasis.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis_UnitBasis.py @@ -43,6 +43,21 @@ _RE_BASIS = re.compile(r"^(㎡|㎥|㏊|m|㎝|개|본|주|ton|톤|kg|㎏)\s*당$" #: 밑수 표기 → 그 갈래가 갖는 단위. _BASIS_UNIT = {"㏊": "ha", "톤": "ton", "㎏": "kg"} +#: 품셈이 **노임표에 없는 직종명**을 쓴 자리 → 유사 직종 +#: (2026-09-09 사용자 확정 8-1 「바꿔쓰기 허용」). +#: 근거 — 「미조사 직종은 유사 직종 단가에 준하여 적용」(재경원 회계 45101-45). +#: ⚠ **공종 단위로만 건다** — 「인부」를 전역으로 읽으면 확정 밖 공종까지 조용히 붙는다. +SIMILAR_OCCUPATION = {("FP-13-02-04", "인부"): "보통인부"} + + +def similar_occupation_note(work_item_code: str) -> str: + """그 공종이 유사 직종으로 바꿔 쓴 자리면 산출근거 문구, 아니면 빈 문자열.""" + return " · ".join( + f"품셈 「{written}」 → {used} 준용(유사 직종 · 재경원 회계 45101-45 · 사용자 확정 8-1)" + for (code, written), used in SIMILAR_OCCUPATION.items() + if code == work_item_code + ) + def _tight(text: str) -> str: return "".join(str(text or "").split()) @@ -93,7 +108,8 @@ def match_unit_basis_table( continue if basis_at > 0: # 이름이 앞에 붙은 줄 — 「인 부 | ㎡당 | 0.11 | …」. 이름을 여기서 갱신한다. - found = _resolve(catalog, cells[0]) + name_cell = SIMILAR_OCCUPATION.get((work_item_code, _tight(cells[0])), cells[0]) + found = _resolve(catalog, name_cell) if found is None: result.unmatched.append( UnmatchedRow( diff --git a/resources/tester/test_b09_similar_occupation.py b/resources/tester/test_b09_similar_occupation.py new file mode 100644 index 00000000..42a645ee --- /dev/null +++ b/resources/tester/test_b09_similar_occupation.py @@ -0,0 +1,48 @@ +"""유사 직종 바꿔쓰기 — 야면석 채집 「인 부」 → 보통인부 (2026-09-13 · 사용자 확정 8-1). + +⚠ 겨누는 것 셋 + ① 야면석 채집(13-2-4)의 「인 부」가 **못 붙은 줄에서 빠지고** 보통인부 노임으로 붙는다 + ② 바꿔쓰기는 **그 공종에만** 걸린다 — 전역 별칭이 아니다 + ③ 산출기초에 **준용 근거**가 함께 뜬다(조용히 바꾸지 않음) +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from B09_Estimation.B09_Estimation_BasisSheet import work_item_basis # noqa: E402 +from B09_Estimation.B09_Estimation_ResourceAxis_UnitBasis import ( # noqa: E402 + SIMILAR_OCCUPATION, + similar_occupation_note, +) +from B09_Estimation.B09_Estimation_UnitPrice import cached_build # noqa: E402 + +CODE = "FP-13-02-04" + + +def test_인부가_보통인부로_붙는다() -> None: + build = cached_build() + assert "인 부" not in build.unattached.get(CODE, []) + titles = [code for code in build.book.titles if code.startswith(f"B-{CODE}#")] + # 뒷길이 다섯 × 밑수 둘(㎡당·㎥당) + assert len(titles) == 10, titles + for code in titles: + names = {build.book.title(d.ref_code).name for d in build.book.details.get(code, [])} + assert "보통인부" in names, (code, names) + assert build.book.resolve(code).labor > 0 + + +def test_바꿔쓰기는_그_공종에만_걸린다() -> None: + assert {code for code, _ in SIMILAR_OCCUPATION} == {CODE} + assert similar_occupation_note("FP-12-10") == "" + + +def test_산출기초에_준용_근거가_뜬다() -> None: + rows = [row for row in work_item_basis(cached_build()) if row["code"].startswith(f"B-{CODE}#")] + assert rows + for row in rows: + assert any("보통인부 준용" in note and "45101-45" in note for note in row["notes"]), row