Merge remote-tracking branch 'origin/main_desktop_1' into sub_desktop_1
This commit is contained in:
@@ -207,6 +207,18 @@ MACHINE_CHOICES: dict[str, dict[str, Any]] = {
|
||||
"암절취 깎기를 고려하여 0.7㎥ 적용한다」.",
|
||||
],
|
||||
},
|
||||
"FP-09-16-01": {
|
||||
"work_item_name": "노체포설",
|
||||
"default_code": "0201-0070",
|
||||
"source": "note",
|
||||
"basis": [
|
||||
"산림사업 표준품셈 9-16-1 [주]① 「장비는 무한궤도 굴착기(0.7㎥)를 적용한다」 — "
|
||||
"흙깎기(9-3-2)와 같이 표가 아니라 [주] 에 있어 공종 마스터가 못 싣는 값입니다.",
|
||||
"⚠ 이 칸이 비어 있던 동안 **노체포설 단가가 아예 안 서고 있었습니다** — "
|
||||
"계수표(K·f·E·㎝)는 있는데 기종을 못 골라 시공능력 공식이 안 돌았습니다 "
|
||||
"(2026-09-09 밤에 드러남).",
|
||||
],
|
||||
},
|
||||
"FP-09-18": {
|
||||
"work_item_name": "층따기",
|
||||
"default_code": "0201-0070",
|
||||
|
||||
@@ -255,10 +255,44 @@ def _machine_by_name(text: str, preferred_spec: str) -> tuple[str, str] | None:
|
||||
return (hits[0][0], hits[0][1].name) if len(hits) == 1 else None
|
||||
|
||||
|
||||
#: ⚠ **표가 아니라 절 제목·[주]에 작업량이 적힌 자리.**
|
||||
#: 9-17-1 은 표가 **아예 없고** 제목 줄이 값을 든다 — 「9-17-1. 비탈면 다짐 : **A = 77.7 ㎡/시간**」.
|
||||
#: 공종 마스터는 표만 절에 붙이므로 이런 자리는 **표가 0장인 공종**으로 남아 단가가 안 섰다
|
||||
#: (2026-09-09 밤 실측: 성토면다짐 12,350.3㎡ 가 금액 없이 내역에 서 있었다).
|
||||
#: ⚠ **값을 지어내지 않는다** — 아래 수·기종은 전부 원문 문구 그대로이고 그 문구를 함께 싣는다.
|
||||
#: ⓘ 마스터가 제목의 작업량까지 싣게 되는 날 이 표는 지운다(그때는 표 쪽이 정본).
|
||||
SOURCE_TEXT_CAPACITY: dict[str, tuple[dict[str, Any], ...]] = {
|
||||
"FP-09-17-01": (
|
||||
{
|
||||
"machine_code": "0201-0070",
|
||||
"machine_name": "굴착기(무한궤도)",
|
||||
"capacity_per_hour": Decimal("77.7"),
|
||||
"cell": "비탈면 다짐 A(㎡/hr)",
|
||||
"source_text": (
|
||||
"산림사업 표준품셈 9-17-1 「비탈면 다짐 : A = 77.7 ㎡/시간」 · "
|
||||
"[주]② 「굴착기 부착용 유압식 진동 콤펙터와 굴착기(0.7㎥)를 적용한다」"
|
||||
),
|
||||
},
|
||||
{
|
||||
"machine_code": "0240-0007",
|
||||
"machine_name": "유압식 진동콤팩터(굴착기 부착용, 0.7)",
|
||||
"capacity_per_hour": Decimal("77.7"),
|
||||
"cell": "비탈면 다짐 A(㎡/hr) — 부착 콤팩터",
|
||||
"source_text": (
|
||||
"산림사업 표준품셈 9-17-1 [주]② — 굴착기와 **함께** 도는 부착 장비라 "
|
||||
"같은 작업량으로 붙는다(품셈 제8장 [주]⑤ 조합 사용)."
|
||||
),
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def direct_capacity_rows(node: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""그 공종에서 **시간당 작업량을 직접 준 기계 줄**들.
|
||||
|
||||
돌려주는 것 — 기계 이름 칸 · 기종 코드/이름 · 시간당 작업량 · 묶음 배분율(%) · 원문 문구.
|
||||
|
||||
⚠ 표에서 읽은 것에 더해 **절 제목·[주]에만 적힌 자리**(`SOURCE_TEXT_CAPACITY`)도 함께 낸다.
|
||||
"""
|
||||
from B09_Estimation.B09_Estimation_MachineProductivity import parse_measure
|
||||
|
||||
@@ -299,4 +333,22 @@ def direct_capacity_rows(node: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"row_cells": [c for c in cells if c],
|
||||
}
|
||||
)
|
||||
|
||||
# 절 제목·[주]에만 있는 자리 — 표에서 이미 잡혔으면 **덧붙이지 않는다**(두 번 세지 않게).
|
||||
already = {row["machine_code"] for row in found}
|
||||
for entry in SOURCE_TEXT_CAPACITY.get(str(node.get("work_item_code") or ""), ()):
|
||||
if entry["machine_code"] in already:
|
||||
continue
|
||||
found.append(
|
||||
{
|
||||
"cell": entry["cell"],
|
||||
"machine_code": entry["machine_code"],
|
||||
"machine_name": entry["machine_name"],
|
||||
"capacity_per_hour": entry["capacity_per_hour"],
|
||||
"ratio_pct": None,
|
||||
"table_id": "",
|
||||
"row_cells": [],
|
||||
"source_text": entry["source_text"],
|
||||
}
|
||||
)
|
||||
return found
|
||||
|
||||
@@ -276,8 +276,14 @@ def _add_machine_layers(
|
||||
# ⇒ 그 셋은 **손료만으로** 층을 세운다. 운전경비 줄이 없다고 통째로 버리면
|
||||
# 깨기 몫이 영영 안 붙는다(구조물터파기 암 갈래가 그 자리였다).
|
||||
# ⚠ **다른 기계에는 이 길을 열지 않는다** — 연료가 빠진 채 조용히 싼 값이 선다.
|
||||
attachment = machine is not None and any(
|
||||
word in re.sub(r"\s", "", machine.name) for word in _ATTACHMENT_WORDS
|
||||
# ⚠ 이름으로만 가리면 **이름이 빈 줄을 놓친다** — 카탈로그의 `0240-0007`
|
||||
# (유압식 진동콤팩터, 굴착기 부착용)이 원천에서 **이름을 잃은 채** 들어와 있어
|
||||
# 비탈면 다짐의 콤팩터 손료가 통째로 안 붙고 있었다(2026-09-09 밤 실측).
|
||||
# 그래서 **분류번호**로도 가린다 — 아래 번호는 이미 조합 사용으로 못 박아 둔
|
||||
# 그 넷(`_ATTACHMENT_PREFIXES`)이고, **목록을 넓히는 것이 아니다.**
|
||||
attachment = machine is not None and (
|
||||
any(word in re.sub(r"\s", "", machine.name) for word in _ATTACHMENT_WORDS)
|
||||
or f"X-{code}".startswith(_ATTACHMENT_PREFIXES)
|
||||
)
|
||||
if machine is None or machine.loss_coefficient_per_hour is None:
|
||||
incomplete.append(code)
|
||||
@@ -675,6 +681,12 @@ def build_unit_prices(
|
||||
for label in labels:
|
||||
by_item.setdefault((code, label), [])
|
||||
continue
|
||||
# ⚠ **표가 한 장도 없어도 작업량을 원문이 준 공종은 세운다** — 9-17-1 비탈면 다짐이
|
||||
# 그 자리다(제목 줄이 「A = 77.7 ㎡/시간」을 든다). 표만 보면 영영 안 서고,
|
||||
# 실제로 성토면다짐 12,350.3㎡ 가 금액 없이 내역에 서 있었다(2026-09-09 밤).
|
||||
if capacity_rows.get(code):
|
||||
by_item[(code, "")] = []
|
||||
continue
|
||||
for table in node.get("tables", []):
|
||||
if isinstance(
|
||||
extract_cycle_factors(code, table, factor_choices, machine_picks), CycleFactors
|
||||
@@ -698,10 +710,12 @@ def build_unit_prices(
|
||||
for row in rows
|
||||
]
|
||||
attachable = [(row, ref) for row, ref in attachable if ref in build.book.titles]
|
||||
if not attachable and not _has_full_formula(
|
||||
master, work_item_code, factor_choices, machine_picks
|
||||
if (
|
||||
not attachable
|
||||
and not capacity_rows.get(work_item_code)
|
||||
and not _has_full_formula(master, work_item_code, factor_choices, machine_picks)
|
||||
):
|
||||
# 붙을 상세도 없고 공식도 없으면 **제목도 안 세운다**(0 원 일위대가 금지).
|
||||
# 붙을 상세도 없고 공식도 작업량도 없으면 **제목도 안 세운다**(0 원 일위대가 금지).
|
||||
build.skipped.append(work_item_code)
|
||||
continue
|
||||
|
||||
@@ -805,6 +819,12 @@ def build_unit_prices(
|
||||
note=(
|
||||
f"작업량을 표가 직접 줌 — {capacity['cell']} {capacity['capacity_per_hour']}"
|
||||
f" (품셈 원문 표기 그대로)"
|
||||
if not capacity.get("source_text")
|
||||
# 표가 아니라 절 제목·[주]에서 온 값은 **어디서 왔는지 그대로** 싣는다.
|
||||
else (
|
||||
f"작업량을 원문이 직접 줌 — {capacity['cell']}"
|
||||
f" {capacity['capacity_per_hour']} · {capacity['source_text']}"
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""성토·성토면다짐에 단가가 서는가 (2026-09-09 밤).
|
||||
|
||||
**왜 안 서고 있었나** — 둘 다 원문에는 값이 다 있는데 서로 다른 까닭으로 막혀 있었다.
|
||||
|
||||
9-16-1 노체포설 계수표(K·f·E·㎝)는 붙었는데 **기종이 [주]에만 있어** 못 골랐음
|
||||
→ 시공능력 공식이 안 돌아 단가가 통째로 안 섬
|
||||
9-17-1 비탈면 다짐 **표가 한 장도 없음** — 값이 절 제목에 있음(「A = 77.7 ㎡/시간」)
|
||||
→ 표만 보는 조립이 그 공종을 아예 안 봄
|
||||
|
||||
⚠ 그동안 인계에는 물량이 그대로 실려 **성토 13,150.2㎥ · 성토면다짐 12,350.3㎡ 가 금액 없이**
|
||||
내역에 서 있었다. 그래서 아래 셋을 못으로 박는다.
|
||||
|
||||
① 두 공종에 단가가 선다 · ② 단위가 원문과 같다(㎥·㎡) · ③ 근거가 줄에 남는다
|
||||
④ 부착 콤팩터 손료가 함께 붙는다 — 이름이 빈 카탈로그 줄이라 놓치던 자리
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B09_Estimation.B09_Estimation_FactorChoices import MACHINE_CHOICES # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_MachineProductivity_Reference import ( # noqa: E402
|
||||
SOURCE_TEXT_CAPACITY,
|
||||
direct_capacity_rows,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import ( # noqa: E402
|
||||
cached_build,
|
||||
load_work_item_master,
|
||||
)
|
||||
|
||||
|
||||
def _build():
|
||||
return cached_build()
|
||||
|
||||
|
||||
def test_노체포설에_단가가_선다() -> None:
|
||||
build = _build()
|
||||
title = build.book.titles.get("B-FP-09-16-01")
|
||||
assert title is not None, "노체포설 일위대가가 안 섰습니다"
|
||||
assert title.unit == "㎥"
|
||||
assert build.book.resolve("B-FP-09-16-01").total > 0
|
||||
|
||||
|
||||
def test_노체포설_기종은_주에서_왔음을_적는다() -> None:
|
||||
"""⚠ 기본값을 어디서 가져왔는지 안 적으면 임의 값과 구별이 안 된다."""
|
||||
entry = MACHINE_CHOICES["FP-09-16-01"]
|
||||
assert entry["default_code"] == "0201-0070" # 무한궤도 굴착기 0.7㎥
|
||||
assert entry["source"] == "note"
|
||||
assert any("9-16-1" in line for line in entry["basis"])
|
||||
|
||||
|
||||
def test_비탈면다짐에_단가가_선다() -> None:
|
||||
build = _build()
|
||||
title = build.book.titles.get("B-FP-09-17-01")
|
||||
assert title is not None, "비탈면 다짐 일위대가가 안 섰습니다"
|
||||
assert title.unit == "㎡"
|
||||
assert build.book.resolve("B-FP-09-17-01").total > 0
|
||||
|
||||
|
||||
def test_비탈면다짐은_원문_작업량을_그대로_쓴다() -> None:
|
||||
"""② 77.7 은 원문 값이다 — 지어낸 수가 아님을 줄에 남긴다."""
|
||||
build = _build()
|
||||
rows = build.book.details["B-FP-09-17-01"]
|
||||
assert rows, "상세 줄이 없습니다"
|
||||
for row in rows:
|
||||
# 시간 = 1 ÷ 77.7 ㎡/시간
|
||||
assert row.quantity == Decimal(1) / Decimal("77.7")
|
||||
assert "9-17-1" in row.note and "77.7" in row.note
|
||||
|
||||
|
||||
def test_부착_콤팩터_손료가_함께_붙는다() -> None:
|
||||
"""④ 카탈로그에 **이름이 빈 줄**(0240-0007)이라 이름으로만 가리면 놓친다."""
|
||||
build = _build()
|
||||
refs = {row.ref_code for row in build.book.details["B-FP-09-17-01"]}
|
||||
assert "X-0240-0007" in refs, refs
|
||||
# 본체 굴착기는 조합 사용 층(잡재료 16%)으로 바뀌어야 한다 — 품셈 제8장 [주]⑤.
|
||||
assert any(ref.startswith("X-0201-0070#") for ref in refs), refs
|
||||
assert "0240-0007" not in build.incomplete_machines
|
||||
|
||||
|
||||
def test_제목에만_있는_작업량은_표에서_온_것과_안_겹친다() -> None:
|
||||
"""⚠ 같은 기종이 표에도 있으면 두 번 세면 안 된다."""
|
||||
master = load_work_item_master()
|
||||
node = next(w for w in master["work_items"] if w["work_item_code"] == "FP-09-17-01")
|
||||
rows = direct_capacity_rows(node)
|
||||
codes = [row["machine_code"] for row in rows]
|
||||
assert len(codes) == len(set(codes))
|
||||
assert set(codes) == {entry["machine_code"] for entry in SOURCE_TEXT_CAPACITY["FP-09-17-01"]}
|
||||
Reference in New Issue
Block a user