fix(B09): 「값 못 읽음」 오탐 좁히고 「값 × 비율%」 표기 읽기
- ⚠ 앞 커밋의 「값 못 읽은 줄」 판정이 **머리 줄까지 잡고 있었음** — 「특별인부 | 벌목부 | 보통인부」처럼 자원 이름만 나열된 줄. 28 공종 중 대부분이 오탐이었음. **숫자가 있는데** 못 읽은 줄만 성분 빠짐으로 봄 (28 → 12) - 「0.2 × 30%」꼴을 0.06 으로 읽음. ⚠ 딱지의 30 % 를 **또 곱하지 않음** — 두 번 곱하면 0.018. 그 밖의 식(「5인/km」)은 기준이 달라 안 읽고 막은 채 둠 기초잡석 12-25 성립 (121,661.1원/㎥) 소할 할석공 0.06 + 부설다짐 보통인부 0.6 + 적사 굴착기 0.04535hr (Q = 3600 ÷ 22 × 0.7 × 0.55 × 1.0 × 0.35 = 22.05㎥/hr) 운반(덤프)은 품셈이 10장에서 따로 내므로 여기 안 붙음 ⇒ 옹벽 묶음 4조각이 모두 섬 — 타설 65,826.4 · 거푸집 81,321.9 · 철근 1,032,497.4 · 기초잡석 121,661.1 검증: pytest 174 통과(신규 4 — 오탐 짝 시험 포함: 머리 줄은 안 걸리고, 숫자 있는 미해독 줄은 걸림) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -333,6 +333,23 @@ def parse_amount(cell: str) -> Decimal | None:
|
|||||||
return 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:
|
def convert_amount(raw: Decimal, *, pum_form: str, basis_quantity: Decimal | None) -> Decimal:
|
||||||
"""표 형태에 맞춰 소요량으로 환산한다.
|
"""표 형태에 맞춰 소요량으로 환산한다.
|
||||||
|
|
||||||
@@ -502,10 +519,31 @@ def match_table(
|
|||||||
amount_cell = next(
|
amount_cell = next(
|
||||||
(parse_amount(c) for c in value_cells if parse_amount(c) is not None), None
|
(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 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", "")] = (
|
result.partial_items[node.get("work_item_code", "")] = (
|
||||||
f"{name_cell} 줄의 값을 못 읽었습니다"
|
f"{name_cell} 줄의 값을 못 읽었습니다"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2597,6 +2597,20 @@
|
|||||||
"variant": "",
|
"variant": "",
|
||||||
"work_item_code": "FP-12-24-02"
|
"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": "0.6",
|
||||||
"amount_unit": "㎥",
|
"amount_unit": "㎥",
|
||||||
@@ -3422,12 +3436,12 @@
|
|||||||
"sha256": "fe454c56c9dc01ad7dae04a8f90d776d08c5ce33badeadb2606b35234bfce7eb"
|
"sha256": "fe454c56c9dc01ad7dae04a8f90d776d08c5ce33badeadb2606b35234bfce7eb"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"rows": 243,
|
"rows": 244,
|
||||||
"skipped_forms": {
|
"skipped_forms": {
|
||||||
"coefficient": 19,
|
"coefficient": 19,
|
||||||
"reference": 98,
|
"reference": 98,
|
||||||
"undetermined": 76
|
"undetermined": 76
|
||||||
},
|
},
|
||||||
"unmatched": 331
|
"unmatched": 312
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,12 +80,6 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-02-02"
|
"work_item_code": "FP-02-02"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "0.2 | 보통인부",
|
|
||||||
"pum_table_id": "F0075",
|
|
||||||
"reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.",
|
|
||||||
"work_item_code": "FP-03-01"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "경비(기계경비)",
|
"cell": "경비(기계경비)",
|
||||||
"pum_table_id": "F0472",
|
"pum_table_id": "F0472",
|
||||||
@@ -248,36 +242,12 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-04-02-02"
|
"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": "비 고",
|
"cell": "비 고",
|
||||||
"pum_table_id": "F0093",
|
"pum_table_id": "F0093",
|
||||||
"reason": "숫자 칸 0 개가 자원 열 2 개와 안 맞아 버렸습니다(자리 밀림 방지).",
|
"reason": "숫자 칸 0 개가 자원 열 2 개와 안 맞아 버렸습니다(자리 밀림 방지).",
|
||||||
"work_item_code": "FP-05-01-02"
|
"work_item_code": "FP-05-01-02"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "특별인부(인) | 보통인부(인) | 굴착기(시간)",
|
|
||||||
"pum_table_id": "F0094",
|
|
||||||
"reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.",
|
|
||||||
"work_item_code": "FP-05-01-03"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "지게",
|
"cell": "지게",
|
||||||
"pum_table_id": "F0097",
|
"pum_table_id": "F0097",
|
||||||
@@ -296,12 +266,6 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-05-01-05"
|
"work_item_code": "FP-05-01-05"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "특별인부 | 보통인부 | 특별인부",
|
|
||||||
"pum_table_id": "F0098",
|
|
||||||
"reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.",
|
|
||||||
"work_item_code": "FP-05-02"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "소묘식재",
|
"cell": "소묘식재",
|
||||||
"pum_table_id": "F0099",
|
"pum_table_id": "F0099",
|
||||||
@@ -320,18 +284,6 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-05-03-01"
|
"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": "파종상 만들기",
|
"cell": "파종상 만들기",
|
||||||
"pum_table_id": "F0109",
|
"pum_table_id": "F0109",
|
||||||
@@ -800,18 +752,6 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-07-13"
|
"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": "경비(기계경비)",
|
"cell": "경비(기계경비)",
|
||||||
"pum_table_id": "F0454",
|
"pum_table_id": "F0454",
|
||||||
@@ -1304,12 +1244,6 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-09-14-01"
|
"work_item_code": "FP-09-14-01"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "보통인부 (인) | 공기압축기 (시간) | 소형브레이커 (시간)",
|
|
||||||
"pum_table_id": "F0288",
|
|
||||||
"reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.",
|
|
||||||
"work_item_code": "FP-09-19-01"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "모래ㆍ사질토ㆍ점토ㆍ점질토",
|
"cell": "모래ㆍ사질토ㆍ점토ㆍ점질토",
|
||||||
"pum_table_id": "F0288",
|
"pum_table_id": "F0288",
|
||||||
@@ -1544,24 +1478,12 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-12-16"
|
"work_item_code": "FP-12-16"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "보통인부 | 인 | ",
|
|
||||||
"pum_table_id": "F0354",
|
|
||||||
"reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.",
|
|
||||||
"work_item_code": "FP-12-16"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "벽체 | 특별인부 | 인",
|
"cell": "벽체 | 특별인부 | 인",
|
||||||
"pum_table_id": "F0354",
|
"pum_table_id": "F0354",
|
||||||
"reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.",
|
"reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.",
|
||||||
"work_item_code": "FP-12-16"
|
"work_item_code": "FP-12-16"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "보통인부 | 인 | ",
|
|
||||||
"pum_table_id": "F0354",
|
|
||||||
"reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.",
|
|
||||||
"work_item_code": "FP-12-16"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "설치비",
|
"cell": "설치비",
|
||||||
"pum_table_id": "F0354",
|
"pum_table_id": "F0354",
|
||||||
@@ -1640,30 +1562,12 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-12-24-02"
|
"work_item_code": "FP-12-24-02"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "소할(30%) | 할석공(인) | 0.2 × 30%",
|
|
||||||
"pum_table_id": "F0373",
|
|
||||||
"reason": "자원은 알아봤으나 값을 못 읽었습니다 — 단가가 일부만 섭니다.",
|
|
||||||
"work_item_code": "FP-12-25"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "적사",
|
"cell": "적사",
|
||||||
"pum_table_id": "F0373",
|
"pum_table_id": "F0373",
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-12-25"
|
"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": "운반 및 설치 (목도운반)",
|
"cell": "운반 및 설치 (목도운반)",
|
||||||
"pum_table_id": "F0374",
|
"pum_table_id": "F0374",
|
||||||
@@ -1760,24 +1664,6 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-13-03-02"
|
"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": "보통인부 | | 〃",
|
"cell": "보통인부 | | 〃",
|
||||||
"pum_table_id": "F0419",
|
"pum_table_id": "F0419",
|
||||||
|
|||||||
Reference in New Issue
Block a user