auto: 2026-09-08 12:48 (EOMSANGDON-HOME)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user