Merge remote-tracking branch 'origin/dev' into sub_laptop_1
This commit is contained in:
@@ -269,34 +269,75 @@ def match_spec_column_table(
|
|||||||
if len(specs) < 2 or not all(_SPEC_HEAD.match(spec) for spec in specs):
|
if len(specs) < 2 or not all(_SPEC_HEAD.match(spec) for spec in specs):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
from B09_Estimation.B09_Estimation_ResourceAxis_Join import resolve_family, unmatched_reason
|
||||||
|
|
||||||
unit = table.get("basis_unit") or ""
|
unit = table.get("basis_unit") or ""
|
||||||
work_item_code = str(node.get("work_item_code", ""))
|
work_item_code = str(node.get("work_item_code", ""))
|
||||||
table_id = str(table.get("pum_table_id", ""))
|
table_id = str(table.get("pum_table_id", ""))
|
||||||
form = str(table.get("pum_form", ""))
|
form = str(table.get("pum_form", ""))
|
||||||
|
|
||||||
made = 0
|
made = 0
|
||||||
|
previous_note = ""
|
||||||
|
|
||||||
|
def unmatched(cell: str, reason: str) -> None:
|
||||||
|
result.unmatched.append(
|
||||||
|
UnmatchedRow(
|
||||||
|
work_item_code=work_item_code, pum_table_id=table_id, cell=cell, reason=reason
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for index, row in enumerate(rows[1:], start=1):
|
for index, row in enumerate(rows[1:], start=1):
|
||||||
cells = [_clean(cell) for cell in row]
|
cells = [_clean(cell) for cell in row]
|
||||||
if not cells:
|
if not cells or not cells[0]:
|
||||||
continue
|
continue
|
||||||
name, spec_text = split_name_and_spec(cells[0])
|
# 줄 모양 — | 구분 | 규격 | 단위 | 관경별 값 … | 비고 | (2026-09-14 · 원문 12-11 표 머리)
|
||||||
entry = catalog.resolve(name, spec_text) or catalog.resolve(name, "")
|
values = [
|
||||||
if entry is None:
|
value for value in (_cell_amount(cell) for cell in cells[1:]) if value is not None
|
||||||
result.unmatched.append(
|
]
|
||||||
UnmatchedRow(
|
note = cells[-1] if len(cells) > 1 and _cell_amount(cells[-1]) is None else ""
|
||||||
work_item_code=work_item_code,
|
note = previous_note if note == "〃" else note
|
||||||
pum_table_id=table_id,
|
spec_cell = cells[1] if len(cells) > 1 and _cell_amount(cells[1]) is None else ""
|
||||||
cell=cells[0],
|
if not values and not note and previous_note.replace(" ", "") == "설계수량":
|
||||||
reason="카탈로그에 없는 이름 (규격이 열로 선 표) — 0 으로 때우지 않습니다.",
|
note = previous_note # 원문 병합 칸 — 바로 위 줄 비고를 따름(흄관 거푸집)
|
||||||
)
|
previous_note = note
|
||||||
|
tight_note = note.replace(" ", "")
|
||||||
|
# ⚠ 원문 비고가 「이 일위대가 밖」인 줄 — 못 찾은 게 아니라 안 넣는 것 · 까닭을 남김.
|
||||||
|
if tight_note in ("별산", "별도계산"):
|
||||||
|
unmatched(
|
||||||
|
cells[0], f"「{note}」 — 원문 비고대로 이 일위대가 밖에서 따로 셈 · 여기 안 넣음"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
values = [_cell_amount(cell) for cell in cells[1:]]
|
if tight_note == "설계수량":
|
||||||
values = [value for value in values if value is not None]
|
unmatched(cells[0], "「설계수량」 — 원문 비고대로 설계 물량으로 따로 셈 · 여기 안 넣음")
|
||||||
if len(values) < len(specs):
|
|
||||||
# 값이 빈 규격이 있다 — 「별산」 줄이거나 표가 덜 찼다. 짐작해 채우지 않는다.
|
|
||||||
continue
|
continue
|
||||||
for spec_name, amount in zip(specs, values[-len(specs) :]):
|
if len(values) < len(specs):
|
||||||
|
why = f"「{note}」 — " if note else ""
|
||||||
|
unmatched(cells[0], f"{why}관경별 값이 비어 짐작해 채우지 않음")
|
||||||
|
continue
|
||||||
|
name, inline_spec = split_name_and_spec(cells[0])
|
||||||
|
row_specs = [text for text in (spec_cell, inline_spec) if text]
|
||||||
|
entries: list[CatalogEntry] = []
|
||||||
|
for spec_name in specs:
|
||||||
|
entry = (
|
||||||
|
next(
|
||||||
|
(
|
||||||
|
found
|
||||||
|
for text in (*row_specs, spec_name)
|
||||||
|
if (found := catalog.resolve(name, text)) is not None
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
or catalog.resolve(name, "")
|
||||||
|
or resolve_family(catalog, cells[0], cells)
|
||||||
|
)
|
||||||
|
if entry is None:
|
||||||
|
break
|
||||||
|
entries.append(entry)
|
||||||
|
if len(entries) < len(specs):
|
||||||
|
origin = f" · 원문 규격 {spec_cell}" if spec_cell else ""
|
||||||
|
unmatched(cells[0], f"{unmatched_reason(catalog, name)}{origin} (규격이 열로 선 표)")
|
||||||
|
continue
|
||||||
|
for spec_name, amount, entry in zip(specs, values[-len(specs) :], entries):
|
||||||
result.rows.append(
|
result.rows.append(
|
||||||
ResourceRow(
|
ResourceRow(
|
||||||
work_item_code=work_item_code,
|
work_item_code=work_item_code,
|
||||||
|
|||||||
@@ -168,7 +168,8 @@ def spec_candidates(cells: list[str]) -> list[str]:
|
|||||||
if spec:
|
if spec:
|
||||||
found.append(spec)
|
found.append(spec)
|
||||||
continue
|
continue
|
||||||
token = _RE_SIZE_TOKEN.fullmatch(text.rstrip("㎥㎡톤tonm³"))
|
# 「40.64㎝」(절단기 12-11-2) — 카탈로그는 수만 적음(7620-0003 절단기 40.64).
|
||||||
|
token = _RE_SIZE_TOKEN.fullmatch(text.rstrip("㎥㎡톤tonm³㎝"))
|
||||||
if token:
|
if token:
|
||||||
found.append(token.group(0))
|
found.append(token.group(0))
|
||||||
return found
|
return found
|
||||||
|
|||||||
@@ -259,6 +259,58 @@
|
|||||||
"F0243"
|
"F0243"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "AR-M-423cfb79",
|
||||||
|
"kind": "material",
|
||||||
|
"name": "고무링",
|
||||||
|
"spec": "∅800mm",
|
||||||
|
"unit": "개",
|
||||||
|
"source": {
|
||||||
|
"pum_edition": "2026-01-01",
|
||||||
|
"pum_table_ids": [
|
||||||
|
"F0346"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "AR-M-18c0ce85",
|
||||||
|
"kind": "material",
|
||||||
|
"name": "고무링",
|
||||||
|
"spec": "∅1000mm",
|
||||||
|
"unit": "개",
|
||||||
|
"source": {
|
||||||
|
"pum_edition": "2026-01-01",
|
||||||
|
"pum_table_ids": [
|
||||||
|
"F0346"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "AR-M-d9d5aa16",
|
||||||
|
"kind": "material",
|
||||||
|
"name": "고무링",
|
||||||
|
"spec": "∅1200mm",
|
||||||
|
"unit": "개",
|
||||||
|
"source": {
|
||||||
|
"pum_edition": "2026-01-01",
|
||||||
|
"pum_table_ids": [
|
||||||
|
"F0346"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "AR-M-0cc44237",
|
||||||
|
"kind": "material",
|
||||||
|
"name": "지수활제",
|
||||||
|
"spec": "",
|
||||||
|
"unit": "g",
|
||||||
|
"source": {
|
||||||
|
"pum_edition": "2026-01-01",
|
||||||
|
"pum_table_ids": [
|
||||||
|
"F0346"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""규격이 열로 선 표(관부설 12-11) — 규격 칸·비고를 읽어 줄은 서고 못 넣는 줄은 까닭으로 드러남.
|
||||||
|
|
||||||
|
2026-09-14 축 C 1장 ② (브레인 차례 · 판정 불필요). 원문 = 산림사업 표준품셈 12-11-1·2·3.
|
||||||
|
① 원문 비고 「별산」·「별도계산」·「설계수량」(〃 · 병합 칸 포함)은 **안 넣는 줄**로 까닭을 남김
|
||||||
|
② 규격 칸(「1:2」)과 관경 열(「∅800mm」)이 조인 키 — 접합몰탈·고무링이 자재 줄로 섬
|
||||||
|
③ 기종이 여럿인 크레인은 **고르지 않고** 「규격 미정 · 원문 규격 10ton」
|
||||||
|
④ 옆 칸 규격 「40.64㎝」 = 카탈로그 절단기 40.64
|
||||||
|
"""
|
||||||
|
|
||||||
|
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_ResourceAxis import ( # noqa: E402
|
||||||
|
build_resource_axis,
|
||||||
|
load_combined_catalog,
|
||||||
|
load_work_item_master,
|
||||||
|
)
|
||||||
|
|
||||||
|
AXIS = build_resource_axis(load_work_item_master(), load_combined_catalog())
|
||||||
|
|
||||||
|
|
||||||
|
def _unmatched(code: str) -> dict[str, str]:
|
||||||
|
return {u.cell: u.reason for u in AXIS.unmatched if u.work_item_code == code}
|
||||||
|
|
||||||
|
|
||||||
|
def _rows(code: str) -> list:
|
||||||
|
return [r for r in AXIS.rows if r.work_item_code == code]
|
||||||
|
|
||||||
|
|
||||||
|
def test_원문_비고대로_안_넣는_줄은_까닭을_남긴다() -> None:
|
||||||
|
vr = _unmatched("FP-12-11-01")
|
||||||
|
assert "별도계산" in vr["VR관"] and "설계수량" in vr["기초콘크리트"]
|
||||||
|
assert "설계수량" in vr["거 푸 집"] # 「〃」
|
||||||
|
hume = _unmatched("FP-12-11-02")
|
||||||
|
assert "별산" in hume["흄 관"] and "설계수량" in hume["거 푸 집"] # 병합 칸
|
||||||
|
corrugated = _unmatched("FP-12-11-03")
|
||||||
|
assert "필요시적용" in corrugated["커플링밴드"] and "설계수량" in corrugated["모래부설"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_규격_칸과_관경_열이_조인_키() -> None:
|
||||||
|
rings = {
|
||||||
|
r.variant: r.resource_spec for r in _rows("FP-12-11-01") if r.resource_name == "고무링"
|
||||||
|
}
|
||||||
|
assert rings == {"∅800mm": "∅800mm", "∅1000mm": "∅1000mm", "∅1200mm": "∅1200mm"}
|
||||||
|
mortar = [r for r in _rows("FP-12-11-02") if r.resource_name == "접합몰탈"]
|
||||||
|
assert {r.resource_spec for r in mortar} == {"1:2"} and len(mortar) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_크레인은_고르지_않고_원문_규격을_보인다() -> None:
|
||||||
|
reason = _unmatched("FP-12-11-03")["크레인"]
|
||||||
|
assert "규격 미정" in reason and "원문 규격 5ton" in reason
|
||||||
|
assert not any(r.resource_name.startswith("크레인") for r in _rows("FP-12-11-03"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_절단기_40_64cm_는_카탈로그_40_64() -> None:
|
||||||
|
cutters = [r for r in _rows("FP-12-11-02") if r.resource_name == "절단기"]
|
||||||
|
assert [(r.resource_code, str(r.amount)) for r in cutters] == [("7620-0003", "0.93")]
|
||||||
Reference in New Issue
Block a user