diff --git a/B09_Estimation/B09_Estimation_ResourceAxis.py b/B09_Estimation/B09_Estimation_ResourceAxis.py index 54a8b041..2c9a4a06 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis.py @@ -333,6 +333,23 @@ def parse_amount(cell: str) -> Decimal | None: return None +#: 「0.2 × 30%」 꼴 — 값과 배분율이 **한 칸에** 적힌 표기(기초잡석 12-25). +#: ⚠ 이 값을 읽었으면 **딱지의 배분율을 또 곱하면 안 된다** — 같은 30 % 가 두 번 곱해진다. +_RE_RATIO_EXPRESSION = re.compile(r"^(\d+(?:\.\d+)?)\s*[×xX*]\s*(\d+(?:\.\d+)?)\s*%$") + + +def parse_amount_expression(cell: str) -> Decimal | None: + """「0.2 × 30%」를 0.06 으로 읽는다. 그 밖의 식은 **읽지 않는다**. + + 식을 넓게 읽으려 들면 「5인/km」처럼 **기준이 다른 값**까지 삼킨다. 여기서 보는 것은 + 「값 × 비율%」 한 모양뿐이다. + """ + found = _RE_RATIO_EXPRESSION.match(_normalize(cell)) + if found is None: + return None + return Decimal(found.group(1)) * Decimal(found.group(2)) / Decimal(100) + + def convert_amount(raw: Decimal, *, pum_form: str, basis_quantity: Decimal | None) -> Decimal: """표 형태에 맞춰 소요량으로 환산한다. @@ -502,10 +519,31 @@ def match_table( amount_cell = next( (parse_amount(c) for c in value_cells if parse_amount(c) is not None), None ) + if amount_cell is None: + # 「0.2 × 30%」 꼴은 값과 배분율이 한 칸에 있다 — 읽었으면 딱지 배분율은 버린다. + expression = next( + ( + parse_amount_expression(c) + for c in value_cells + if parse_amount_expression(c) is not None + ), + None, + ) + if expression is not None: + amount_cell = expression + group_ratio = None # ⚠ 이미 값 안에 들어 있다 — 또 곱하면 두 번이다 if amount_cell is None: # ⚠ **이름은 자원인데 값을 못 읽은 줄**은 다르다 — 그 공종 단가는 성분이 # 빠진 채 서게 된다. 조용히 넘기지 않고 「일부만 섬」으로 표시한다. - if _resolve_cell(catalog, name_cell, [name_cell, *value_cells]) is not None: + # ⚠ **숫자가 아예 없는 줄은 머리 줄**이다 — 자원 이름만 나열된 줄 + # (「특별인부 | 벌목부 | 보통인부」). 그것까지 「못 읽은 값」으로 세면 + # 정상 공종이 무더기로 막힌다(2026-09-08: 28건 중 대부분이 이 오탐이었다). + # 숫자가 **있는데** 못 읽은 줄만 성분 빠짐으로 본다. + has_digit = any(ch.isdigit() for cell in value_cells for ch in cell) + if ( + has_digit + and _resolve_cell(catalog, name_cell, [name_cell, *value_cells]) is not None + ): result.partial_items[node.get("work_item_code", "")] = ( f"{name_cell} 줄의 값을 못 읽었습니다" ) diff --git a/resources/data_cost_resource_axis/resource_axis_2026-01-01.json b/resources/data_cost_resource_axis/resource_axis_2026-01-01.json index 80a17298..d6b0bfbe 100644 --- a/resources/data_cost_resource_axis/resource_axis_2026-01-01.json +++ b/resources/data_cost_resource_axis/resource_axis_2026-01-01.json @@ -2597,6 +2597,20 @@ "variant": "", "work_item_code": "FP-12-24-02" }, + { + "amount": "0.06", + "amount_unit": "㎥", + "group_ratio_pct": null, + "pum_form": "requirement", + "pum_table_id": "F0373", + "raw_row_index": 0, + "resource_code": "1017", + "resource_kind": "labor", + "resource_name": "할석공", + "resource_spec": "", + "variant": "", + "work_item_code": "FP-12-25" + }, { "amount": "0.6", "amount_unit": "㎥", @@ -3422,12 +3436,12 @@ "sha256": "fe454c56c9dc01ad7dae04a8f90d776d08c5ce33badeadb2606b35234bfce7eb" }, "stats": { - "rows": 243, + "rows": 244, "skipped_forms": { "coefficient": 19, "reference": 98, "undetermined": 76 }, - "unmatched": 331 + "unmatched": 312 } } \ No newline at end of file diff --git a/resources/data_cost_resource_axis/unmatched_2026-01-01.json b/resources/data_cost_resource_axis/unmatched_2026-01-01.json index 72431775..2926fae4 100644 --- a/resources/data_cost_resource_axis/unmatched_2026-01-01.json +++ b/resources/data_cost_resource_axis/unmatched_2026-01-01.json @@ -80,12 +80,6 @@ "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-02-02" }, - { - "cell": "0.2 | 보통인부", - "pum_table_id": "F0075", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-03-01" - }, { "cell": "경비(기계경비)", "pum_table_id": "F0472", @@ -248,36 +242,12 @@ "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-04-02-02" }, - { - "cell": "특별인부 | 벌목부 | 보통인부", - "pum_table_id": "F0088", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-04-03" - }, - { - "cell": "0.13인 | 벌목부", - "pum_table_id": "F0090", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-04-05" - }, - { - "cell": "1인 | 보통인부", - "pum_table_id": "F0091", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-04-06" - }, { "cell": "비 고", "pum_table_id": "F0093", "reason": "숫자 칸 0 개가 자원 열 2 개와 안 맞아 버렸습니다(자리 밀림 방지).", "work_item_code": "FP-05-01-02" }, - { - "cell": "특별인부(인) | 보통인부(인) | 굴착기(시간)", - "pum_table_id": "F0094", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-05-01-03" - }, { "cell": "지게", "pum_table_id": "F0097", @@ -296,12 +266,6 @@ "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-05-01-05" }, - { - "cell": "특별인부 | 보통인부 | 특별인부", - "pum_table_id": "F0098", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-05-02" - }, { "cell": "소묘식재", "pum_table_id": "F0099", @@ -320,18 +284,6 @@ "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-05-03-01" }, - { - "cell": "특별인부(인) | 보통인부(인) | 특별인부(인)", - "pum_table_id": "F0104", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-05-03-04" - }, - { - "cell": "특별인부(인) | 보통인부(인) | 굴착기(시간)", - "pum_table_id": "F0106", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-05-03-05" - }, { "cell": "파종상 만들기", "pum_table_id": "F0109", @@ -800,18 +752,6 @@ "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-07-13" }, - { - "cell": "특별인부 | 보통인부 | 특별인부", - "pum_table_id": "F0201", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-08-01-01" - }, - { - "cell": "특별인부 | 보통인부 | 특별인부", - "pum_table_id": "F0202", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-08-01-02" - }, { "cell": "경비(기계경비)", "pum_table_id": "F0454", @@ -1304,12 +1244,6 @@ "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-14-01" }, - { - "cell": "보통인부 (인) | 공기압축기 (시간) | 소형브레이커 (시간)", - "pum_table_id": "F0288", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-09-19-01" - }, { "cell": "모래ㆍ사질토ㆍ점토ㆍ점질토", "pum_table_id": "F0288", @@ -1544,24 +1478,12 @@ "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-12-16" }, - { - "cell": "보통인부 | 인 | ", - "pum_table_id": "F0354", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-12-16" - }, { "cell": "벽체 | 특별인부 | 인", "pum_table_id": "F0354", "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", "work_item_code": "FP-12-16" }, - { - "cell": "보통인부 | 인 | ", - "pum_table_id": "F0354", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-12-16" - }, { "cell": "설치비", "pum_table_id": "F0354", @@ -1640,30 +1562,12 @@ "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-12-24-02" }, - { - "cell": "소할(30%) | 할석공(인) | 0.2 × 30%", - "pum_table_id": "F0373", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-12-25" - }, { "cell": "적사", "pum_table_id": "F0373", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-12-25" }, - { - "cell": "운반 | 덤프트럭(15ton) | ", - "pum_table_id": "F0373", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-12-25" - }, - { - "cell": "기계 | 양수기(150m/m) | ", - "pum_table_id": "F0374", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-12-26" - }, { "cell": "운반 및 설치 (목도운반)", "pum_table_id": "F0374", @@ -1760,24 +1664,6 @@ "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-13-03-02" }, - { - "cell": "석공 (인) | 보통인부 (인) | 석공 (인)", - "pum_table_id": "F0404", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-13-04-01" - }, - { - "cell": "석 공 (인) | 보통인부 (인) | 석공 (인)", - "pum_table_id": "F0409", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-13-04-04" - }, - { - "cell": "뒷길이 (㎝) | 석공 (인) | 보통 인부 (인)", - "pum_table_id": "F0416", - "reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.", - "work_item_code": "FP-13-05-01" - }, { "cell": "보통인부 | | 〃", "pum_table_id": "F0419",