Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f9d42ac35 |
@@ -226,6 +226,28 @@ def _tidy_resource_name(cell: str) -> str:
|
||||
_MACHINE_UNITS = ("시간", "hr", "h", "대", "시 간")
|
||||
|
||||
|
||||
#: 조용히 빠지던 기계·연료 줄의 사유(2026-09-15 ㉠ — 인력만으로 싸게 서던 7공종).
|
||||
SILENT_MACHINE = "기계·연료 줄이 안 붙음 — 이 공종 단가는 일부만 섬(인력만으로 싸게 서지 않게 막음)"
|
||||
#: 규격이 이름 앞에 온 장비 칸 — 「0.8㎥ 굴착기」.
|
||||
_RE_SPEC_FIRST = re.compile(r"^\d+(?:\.\d+)?(?:㎥|m3|ton|톤|㎾|kW)\s*[가-힣]{2,}")
|
||||
#: 연료·기관 줄 이름 — 값이 비었거나 식이라도 기계 몫(연료비 · 휘발유 · 경유 · 엔진 · 기관).
|
||||
_RE_FUEL_OR_ENGINE = re.compile(r"연료|휘발유|경유|엔진|기관|양수기|발전기|원동기")
|
||||
_POWER_UNITS = ("kW", "㎾", "HP", "PS", "마력")
|
||||
|
||||
|
||||
def _silent_machine_row(name_cell: str, value_cells: list[str], has_digit: bool) -> bool:
|
||||
"""값을 못 읽고 이름도 안 풀린 줄이 **기계·연료 몫**인가 — 머리 줄·설명 줄은 아님."""
|
||||
name = _normalize(name_cell)
|
||||
if not name or is_non_resource_label(name_cell) and not _RE_FUEL_OR_ENGINE.search(name):
|
||||
return False
|
||||
if _RE_FUEL_OR_ENGINE.search(name):
|
||||
return True
|
||||
joined = " ".join(value_cells)
|
||||
return has_digit and (
|
||||
_is_machine_like_row(value_cells) or any(unit in joined for unit in _POWER_UNITS)
|
||||
)
|
||||
|
||||
|
||||
def _is_machine_like_row(cells: list[str]) -> bool:
|
||||
"""그 줄이 **장비 몫**인가 — 단위 칸이 시간·대수인지로 본다."""
|
||||
for cell in cells:
|
||||
@@ -480,6 +502,17 @@ def match_table(
|
||||
reason="자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.",
|
||||
)
|
||||
)
|
||||
elif _silent_machine_row(name_cell, value_cells, has_digit):
|
||||
# 이름도 값도 못 읽은 기계·연료 줄 — 조용히 넘기면 인력만으로 싸게 섬(2026-09-15 ㉠).
|
||||
code = node.get("work_item_code", "")
|
||||
result.partial_items[code] = (
|
||||
f"{_normalize(name_cell)[:20]} (기계·연료 줄)이 안 붙음"
|
||||
)
|
||||
result.unmatched.append(
|
||||
UnmatchedRow(
|
||||
code, str(table.get("pum_table_id", "")), name_cell, SILENT_MACHINE
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# ⚠ **카탈로그 조회를 먼저 한다.** 이름 필터를 앞에 두면 필터가 넓을 때
|
||||
@@ -491,6 +524,17 @@ def match_table(
|
||||
name_cell = apply_scoped_alias(catalog, name_cell, node["work_item_code"], value_cells)
|
||||
entry = entry or _resolve_cell(catalog, name_cell, [name_cell, *value_cells])
|
||||
if entry is None:
|
||||
if _RE_SPEC_FIRST.match(_normalize(name_cell)) and _is_machine_like_row(value_cells):
|
||||
# 「0.8㎥ 굴착기 | h | 0.25」 — 규격이 이름 앞이라 머리글로 걸러지던 장비 줄(14-2 · ㉠).
|
||||
result.partial_items[node["work_item_code"]] = (
|
||||
f"{_normalize(name_cell)[:20]} (기계·연료 줄)이 안 붙음"
|
||||
)
|
||||
result.unmatched.append(
|
||||
UnmatchedRow(
|
||||
node["work_item_code"], table["pum_table_id"], name_cell, SILENT_MACHINE
|
||||
)
|
||||
)
|
||||
continue
|
||||
if is_non_resource_label(name_cell):
|
||||
continue # 머리글·소계 — 못 맞춘 목록에도 안 올린다
|
||||
# 「규격 미정 — 후보 N」 · 「같은 이름 여럿」 · 「카탈로그에 없는 이름」을 가른다.
|
||||
|
||||
@@ -99,7 +99,15 @@ def attach_wood_chipping(build: Any, nodes_by_code: dict[str, dict[str, Any]]) -
|
||||
note=f"[주]② 1일 보통인부 {LABORERS_PER_DAY}인 ÷ [주]① {HOURS_PER_DAY}시간 ÷ Q {q}",
|
||||
)
|
||||
)
|
||||
labels = [label for label in build.unattached.get(CODE, []) if "파쇄기날" not in label]
|
||||
# 표의 파쇄기 줄은 이 모듈이 씀 — 자원 축이 「기계·연료 줄이 안 붙음」 으로 올린 것을 걷음.
|
||||
labels = [
|
||||
label
|
||||
for label in build.unattached.get(CODE, [])
|
||||
if "파쇄기날" not in label and "".join(label.split()) != "이동식임목파쇄기"
|
||||
]
|
||||
if "파쇄기" in (build.component_gaps.get(CODE) or ""):
|
||||
build.component_gaps.pop(CODE)
|
||||
build.partial_ratio.pop(CODE, None)
|
||||
if BLADE in book.titles:
|
||||
book.add_detail(
|
||||
PriceDetail(
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""말없이 버려지던 기계·연료 줄 — 2026-09-15 브레인 ㉠(인력만으로 싸게 서던 공종 막기).
|
||||
|
||||
자원 축 줄 읽기가 세 자리에서 기계·연료 줄을 「못 붙은 줄」 목록에도 안 올리고 버렸음 →
|
||||
그 공종 단가가 인력만으로 **막히지도 사유도 없이** 싸게 섬(사용자는 값이 맞는 줄 앎).
|
||||
① 규격이 이름 앞 「0.8㎥ 굴착기 | h | 0.25」(14-2 · 14-1) — 숫자로 시작해 머리글로 걸러짐
|
||||
② 값·이름 둘 다 못 읽음 「디젤엔진(15HP) | 11.19kW」(12-26) · 「연료비 | 소요량 적용 | ps × 0.253ℓ × 6h」(10-7-4)
|
||||
③ 수량 빈 연료 줄 「연료비 | 경유 | L | 」(10-8-3) — 숫자 없는 줄은 머리 줄로 봄
|
||||
⇒ 기계·연료로 보이는 줄은 「못 맞춤 + 일부만」 — 틀린 금액보다 안 선 금액(사유 또렷이).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
|
||||
|
||||
|
||||
def test_기계_연료_줄이_안_붙은_다섯_공종은_일부만으로_막히고_사유() -> None:
|
||||
build = cached_build()
|
||||
for code in ("FP-12-26", "FP-14-02", "FP-14-01", "FP-10-08-03", "FP-10-07-04"):
|
||||
assert code in build.partial_ratio, code
|
||||
assert "기계·연료 줄" in (build.component_gaps.get(code) or ""), (
|
||||
code,
|
||||
build.component_gaps.get(code),
|
||||
)
|
||||
left = " ".join(build.unattached.get("FP-12-26", []))
|
||||
assert "디젤엔진" in left and "양수기" in left, left
|
||||
assert "0.8㎥ 굴착기" in " ".join(build.unattached.get("FP-14-02", []))
|
||||
|
||||
|
||||
def test_머리_줄과_인력만인_정상_공종은_안_막힘() -> None:
|
||||
build = cached_build()
|
||||
for code in ("FP-09-15-02", "FP-12-12", "FP-04-02-02", "FP-09-03-02"):
|
||||
assert code not in build.partial_ratio, code
|
||||
|
||||
|
||||
def test_파쇄_모듈이_쓰는_파쇄기_줄은_막힘으로_안_남음() -> None:
|
||||
build = cached_build()
|
||||
assert "FP-08-11" not in build.partial_ratio
|
||||
assert "기계·연료 줄" not in (build.component_gaps.get("FP-08-11") or "")
|
||||
assert not any(
|
||||
"파쇄기 " in label or label == "이동식 임목 파쇄기"
|
||||
for label in build.unattached.get("FP-08-11", [])
|
||||
)
|
||||
Reference in New Issue
Block a user