From 47943f2990e0da9760b3346fe8b9108d00a3ac1e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 12:48:34 +0900 Subject: [PATCH] auto: 2026-09-08 12:48 (EOMSANGDON-HOME) --- B08_Quantity/B08_Quantity_Engine_Handoff.py | 72 +++++++++++++++++-- .../work_item_mapping_2026-01-01.json | 10 +++ 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff.py b/B08_Quantity/B08_Quantity_Engine_Handoff.py index f26afd6d..72d65abb 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff.py @@ -45,6 +45,10 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Any, Iterable +from B08_Quantity.B08_Quantity_Engine_BasisUnit import ( + unit_for_code, + verify_unit_matches_basis, +) from B08_Quantity.B08_Quantity_Engine_Preparation import ( STATUS_COUNTED_ELSEWHERE as PREP_COUNTED_ELSEWHERE, ) @@ -515,6 +519,7 @@ def _earthwork_rows( # 토공·운반 줄에는 갈래 축이 없다 — 그래도 **칸은 둔다**(계약이 한 모양). "variant_axis": None, "variant_value": None, + "secondary_axes": None, "spec_class": None, "spec_class_basis": "", # 토공 줄은 막힐 자리가 없다 — 그래도 **칸은 둔다**(계약이 한 모양이어야 한다). @@ -570,6 +575,7 @@ def _haul_rows( # 토공·운반 줄에는 갈래 축이 없다 — 그래도 **칸은 둔다**(계약이 한 모양). "variant_axis": None, "variant_value": None, + "secondary_axes": None, "spec_class": None, "spec_class_basis": "", "blocked_kind": None, @@ -617,6 +623,29 @@ def blocked_of( return None, "" +def _component_billing( + structure: dict[str, Any], entry: dict[str, Any] +) -> tuple[str, float] | None: + """(내역 단위, 그 단위로 센 수량) — **전개 성분 하나**에서 가져온다. 없으면 `None`. + + ⚠ **왜 성분에서 가져오나** — 돌쌓기 면적은 이미 전개가 냈다(비탈면적 = 정면적 × + √(1+n²)). 여기서 다시 재면 **같은 식이 두 벌**이 되고 한쪽만 고쳐지는 자리가 된다. + ⚠ **어느 성분인지는 매핑이 말한다** — 단위만 보고 고르면 거푸집 같은 다른 ㎡ 성분을 + 집는다. + """ + name = str(entry.get("billing_component") or "") + if not name: + return None + for component in structure.get("components") or []: + if str(component.get("name") or "") != name: + continue + unit = str(component.get("unit") or "") + amount = float(component.get("amount") or 0.0) + if unit and amount > 0: + return unit, amount + return None + + def _structure_rows( unit_quantity_table: dict[str, Any], mapping: WorkItemMapping ) -> tuple[list[dict[str, Any]], list[str]]: @@ -651,16 +680,31 @@ def _structure_rows( if code and entry.get("class_from") == "back_length": class_key, class_basis = masonry_class(structure.get("options") or {}) + # ⚠ 품셈 밑수가 「㎡당」인 공종은 **연장으로 세면 안 된다** — 받는 쪽이 ㎡ 단가를 + # m 수량에 곱해 **2.6배** 금액이 섰다(2026-09-08 실증). 어느 성분으로 세는지는 + # 매핑이 말한다(`billing_component`) — 코드가 짐작하지 않는다. + billing = _component_billing(structure, entry) composite = mapping.composite_for(type_id) if code is None else None kind = structure_kind(structure) if composite else None parts: list[dict[str, Any]] | None = None parts_missing: list[dict[str, Any]] = [] if composite: parts, parts_missing = composite_quantities(structure, composite, mapping) - blocked_kind, blocked_reason = blocked_of(structure, class_basis, has_code=bool(code)) + # ⚠ 매핑이 「이 성분으로 센다」고 했는데 그 성분이 없으면 **연장으로 세지 않는다** — + # 세면 다시 틀린 축으로 금액이 선다. 코드가 없는 것과 같이 보아 사유를 찾는다. + counts_by_component = bool(entry.get("billing_component")) + blocked_kind, blocked_reason = blocked_of( + structure, + class_basis, + has_code=bool(code) and (billing is not None or not counts_by_component), + ) # 갈래 축과 **저장 제원 원본값**. 가공하지 않는다. variant_axis = str(entry.get("variant_axis") or "") or None variant_value = (structure.get("options") or {}).get(variant_axis) if variant_axis else None + secondary_axes = [ + {"axis": axis, "value": (structure.get("options") or {}).get(axis)} + for axis in entry.get("secondary_axes") or [] + ] if code is None and composite is None: unmatched.append(f"{wording_type_label(type_id)} — 품셈 공종을 아직 못 이었습니다") elif entry.get("class_from") in ("back_length", "bond") and class_key is None: @@ -669,12 +713,17 @@ def _structure_rows( # ⚠ 관측 원단위가 「개소당」·「㎡당」인 종류는 **연장으로 세면 축이 어긋난다** — # 집수정 한 개소가 연장 2m 면 값이 두 배로 실린다(2026-09-08 ㉕ 실증). # 성분은 개소 기준으로 맞게 서는데 **줄의 축만** 틀렸던 자리다. - bill_unit = str(structure.get("billing_unit") or "") or "m" - bill_quantity = ( - float(structure.get("billing_quantity") or 0.0) - if structure.get("billing_unit") - else length - ) + if billing is not None: + bill_unit, bill_quantity = billing + elif counts_by_component: + # ⚠ 물량이 못 섰다 — **연장으로 대신 세지 않는다.** 단위는 품셈 밑수를 그대로 + # 실어 둔다(「0 m」로 내면 받는 쪽이 길이로 읽고 축이 어긋난 채 채워진다). + bill_unit, bill_quantity = unit_for_code(str(code or "")), 0.0 + elif structure.get("billing_unit"): + bill_unit = str(structure["billing_unit"]) + bill_quantity = float(structure.get("billing_quantity") or 0.0) + else: + bill_unit, bill_quantity = "m", length rows.append( { "work_item_code": code, @@ -707,6 +756,9 @@ def _structure_rows( # 저장 원본값**만 보내고, 원문을 읽는 쪽이 그 표기를 흡수한다. "variant_axis": variant_axis, "variant_value": variant_value, + # ⚠ 갈래 축이 **둘 이상**인 자리 — 돌쌓기는 뒷길이와 **돌 종류**로 갈린다. + # 여기서도 **저장 원본값만** 싣는다(키 조립은 원문 읽는 쪽 몫). + "secondary_axes": secondary_axes or None, "spec_class": class_key, "spec_class_basis": class_basis, # ⚠ 물량을 못 채운 조각 — 0 으로 적지 않고 사유와 함께 드러낸다. @@ -788,6 +840,7 @@ def _placing_rows( "blocked_reason": "", "variant_axis": "structure_kind", "variant_value": kind, + "secondary_axes": None, "spec_class": kind, "spec_class_basis": ( "철근이 있으면 철근구조물, 없으면 무근구조물 — 원단위로 자동 판정" @@ -849,6 +902,7 @@ def _preparation_rows(preparation_table: dict[str, Any]) -> list[dict[str, Any]] "blocked_reason": "" if ready else str(row.get("reason") or status), "variant_axis": None, "variant_value": None, + "secondary_axes": None, "spec_class": None, "spec_class_basis": str(row.get("reason") or ""), "composite_not_ready": None, @@ -907,6 +961,7 @@ def _pipe_rows(pipe_table: dict[str, Any]) -> list[dict[str, Any]]: # 갈래는 **저장 원본값**만 — 「…㎜ 이하」 구간 나누기는 원문을 읽는 쪽 몫. "variant_axis": row.get("variant_axis"), "variant_value": row.get("variant_value"), + "secondary_axes": None, "spec_class": None, "spec_class_basis": str(row.get("blocked_reason") or ""), "composite_not_ready": None, @@ -986,6 +1041,7 @@ def _length_rows( "blocked_reason": reason, "variant_axis": None, "variant_value": None, + "secondary_axes": None, "spec_class": None, "spec_class_basis": note, "composite_not_ready": None, @@ -1107,6 +1163,8 @@ def build_handoff( result["ratio_math_warnings"] = verify_ratio_math(result) result["material_code_warnings"] = verify_no_code_on_materials(result) result["bill_flag_warnings"] = verify_bill_flags(result) + # ⚠ 보내는 단위가 **품셈 밑수**와 같은가 — 받는 쪽이 그대로 곱하는 자리다(2026-09-08). + result["basis_unit_warnings"] = verify_unit_matches_basis(work_items) result["placing_notes"] = placing_notes return result diff --git a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json index c041c79a..f1a2da69 100644 --- a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json +++ b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json @@ -116,6 +116,10 @@ "structure": [ { "type_id": "masonry_wet", + "secondary_axes": ["stone_kind"], + "secondary_axes_note": "⚠ 돌 종류는 **갈래 축이 하나 더**인 자리다. 품셈 13-4-2·13-4-5 [주]② 가 「본 품은 **깬돌 및 깬 잡석**의 돌쌓기 기준」이라 못 박고, 돌 종류로 갈리는 표는 **13-5 돌붙임**에 따로 있다(뒷길이 7 × 돌종류 6). 어느 쪽 공종으로 볼지는 **사용자 확정 대기** — 여기서 공종을 바꾸지 않고 **저장 원본값만 실어 보낸다**(2026-09-08 계약과 같은 방식).", + "billing_component": "돌쌓기", + "billing_note": "⚠ 품셈 밑수가 「㎡당」이라 **연장(m)으로 세면 안 된다** — 받는 쪽이 ㎡ 단가를 m 수량에 곱해 금액이 2.6배로 섰다(2026-09-08 실증: 10m × 52,938.9 = 529,389원 / 26.101㎡ × 52,938.9 = 1,381,753원). 내역 줄 수량은 이 성분(비탈면적)으로 센다. 고임돌·야면석·막자갈은 **자재 축**으로 따로 가므로 여기서 빠지지 않는다.", "work_item_code": "FP-13-04-05", "master_name": "돌쌓기 > 찰쌓기(장비)", "note": "인력 시공이면 FP-13-04-04", @@ -124,6 +128,10 @@ }, { "type_id": "masonry_dry", + "secondary_axes": ["stone_kind"], + "secondary_axes_note": "⚠ 돌 종류는 **갈래 축이 하나 더**인 자리다. 품셈 13-4-2·13-4-5 [주]② 가 「본 품은 **깬돌 및 깬 잡석**의 돌쌓기 기준」이라 못 박고, 돌 종류로 갈리는 표는 **13-5 돌붙임**에 따로 있다(뒷길이 7 × 돌종류 6). 어느 쪽 공종으로 볼지는 **사용자 확정 대기** — 여기서 공종을 바꾸지 않고 **저장 원본값만 실어 보낸다**(2026-09-08 계약과 같은 방식).", + "billing_component": "돌쌓기", + "billing_note": "⚠ 품셈 밑수가 「㎡당」이라 **연장(m)으로 세면 안 된다** — 받는 쪽이 ㎡ 단가를 m 수량에 곱해 금액이 2.6배로 섰다(2026-09-08 실증: 10m × 52,938.9 = 529,389원 / 26.101㎡ × 52,938.9 = 1,381,753원). 내역 줄 수량은 이 성분(비탈면적)으로 센다. 고임돌·야면석·막자갈은 **자재 축**으로 따로 가므로 여기서 빠지지 않는다.", "work_item_code": "FP-13-04-02", "master_name": "돌쌓기 > 메쌓기(장비)", "note": "인력 시공이면 FP-13-04-01", @@ -142,6 +150,8 @@ }, { "type_id": "boulder_masonry", + "billing_component": "큰돌쌓기", + "billing_note": "⚠ 품셈 밑수가 「㎡당」이라 **연장(m)으로 세면 안 된다** — 받는 쪽이 ㎡ 단가를 m 수량에 곱해 금액이 2.6배로 섰다(2026-09-08 실증: 10m × 52,938.9 = 529,389원 / 26.101㎡ × 52,938.9 = 1,381,753원). 내역 줄 수량은 이 성분(비탈면적)으로 센다. 고임돌·야면석·막자갈은 **자재 축**으로 따로 가므로 여기서 빠지지 않는다. ⚠ 13-6 은 밑수가 **10㎡** 다 — 받는 쪽이 마스터의 `basis_quantity` 를 함께 봐야 한다.", "work_item_code": null, "class_from": "bond", "bond_codes": {