feat(B08): 인계 줄마다 막힌 까닭을 실음 + 계약 칸 대조 시험
서브 화면에 「왜 안 서는지」가 안 뜨던 줄 셋(돌쌓기·흙막이·큰돌쌓기)을 고침. - `blocked_reason` · `blocked_kind` 를 줄마다 실음. 갈래는 셋 — `input_missing`(입력하면 풀림) / `unit_data_missing`(자료 없음) / `formula_missing`(산출식 없음). 받는 쪽이 「사용자가 입력하면 풀리는 것」과 「우리가 만들어야 하는 것」을 화면에서 갈라야 함(8-27 표와 같은 축). - 문구는 `B08_Quantity_Wording` 것을 그대로 씀 — 두 벌로 짜면 갈림. - 토공·운반 줄은 막힐 자리가 없어도 **칸은 둠** — 계약이 한 모양이어야 함. ⚠ 계약 칸 대조 시험 추가(`test_b08_handoff_contract.py`). 오늘 이 병이 계약 양쪽에서 하나씩 났음 — 우리 쪽은 레지스트리 키와 엔진이 읽는 키가 달라 저장값이 안 닿았고, 받는 쪽은 우리가 보낸 `composite_parts`· `quantity_gross` 를 안 읽고 버렸음. **「보내는 쪽은 보냈는데 받는 쪽이 안 읽는」 자리는 양쪽 다 조용함.** 키를 늘릴 때 계약에 안 적고 늘리면 받는 쪽이 영영 모름. 자재 줄에 공종코드가 없는 것도 계약으로 못 박음. 검증 — 전체 638 passed. 실물 프로젝트(`5601e828`)에서 네 줄 다 사유가 실림. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,12 @@ NOTE_METHOD_MISSING = "시공법 미지정으로 공종을 못 고름"
|
|||||||
#: 아니라 공종 이름이라 성분 목록에는 안 온다.
|
#: 아니라 공종 이름이라 성분 목록에는 안 온다.
|
||||||
REBAR_PREFIXES = ("이형철근", "원형철근", "철근")
|
REBAR_PREFIXES = ("이형철근", "원형철근", "철근")
|
||||||
|
|
||||||
|
#: 줄이 왜 막혔나 — **받는 쪽이 「사용자가 입력하면 풀리는 것」과 「우리가 만들어야 하는 것」을
|
||||||
|
#: 화면에서 갈라야** 한다(2026-09-07 3자 확정). 8-27 표에서 이미 가른 그 축이다.
|
||||||
|
BLOCKED_INPUT_MISSING = "input_missing" # 저장 제원 칸이 비어 있음 — 입력하면 풀림
|
||||||
|
BLOCKED_UNIT_DATA_MISSING = "unit_data_missing" # 원단위·표준 물량 자료가 없음
|
||||||
|
BLOCKED_FORMULA_MISSING = "formula_missing" # 수량 산출식 자체가 없음
|
||||||
|
|
||||||
#: 사면 계열 — 토공집계에 함께 실리지만 출처가 사면적이다.
|
#: 사면 계열 — 토공집계에 함께 실리지만 출처가 사면적이다.
|
||||||
SLOPE_GROUPS = frozenset({"성토면다짐", "초류종자살포", "지장목제거", "층따기", "면고르기"})
|
SLOPE_GROUPS = frozenset({"성토면다짐", "초류종자살포", "지장목제거", "층따기", "면고르기"})
|
||||||
|
|
||||||
@@ -459,6 +465,14 @@ def _earthwork_rows(
|
|||||||
"station_from": None,
|
"station_from": None,
|
||||||
"station_to": None,
|
"station_to": None,
|
||||||
"spec_detail": "",
|
"spec_detail": "",
|
||||||
|
"composite_parts": None,
|
||||||
|
"structure_kind": None,
|
||||||
|
"spec_class": None,
|
||||||
|
"spec_class_basis": "",
|
||||||
|
# 토공 줄은 막힐 자리가 없다 — 그래도 **칸은 둔다**(계약이 한 모양이어야 한다).
|
||||||
|
"blocked_kind": None,
|
||||||
|
"blocked_reason": "",
|
||||||
|
"composite_not_ready": None,
|
||||||
# 합계 줄과 무대 줄은 값은 내되 내역에 안 선다.
|
# 합계 줄과 무대 줄은 값은 내되 내역에 안 선다.
|
||||||
"in_bill": bool(row.get("in_bill", True)) and not is_subtotal,
|
"in_bill": bool(row.get("in_bill", True)) and not is_subtotal,
|
||||||
"excavation_method": methods.get(ground) if ground else None,
|
"excavation_method": methods.get(ground) if ground else None,
|
||||||
@@ -499,9 +513,17 @@ def _haul_rows(
|
|||||||
"ground_class": row.get("ground") or None,
|
"ground_class": row.get("ground") or None,
|
||||||
"haul_distance_m": float(row.get("average_distance_m") or 0.0),
|
"haul_distance_m": float(row.get("average_distance_m") or 0.0),
|
||||||
"haul_equipment": equipment,
|
"haul_equipment": equipment,
|
||||||
|
"excavation_method": None,
|
||||||
"station_from": None,
|
"station_from": None,
|
||||||
"station_to": None,
|
"station_to": None,
|
||||||
"spec_detail": "",
|
"spec_detail": "",
|
||||||
|
"composite_parts": None,
|
||||||
|
"structure_kind": None,
|
||||||
|
"spec_class": None,
|
||||||
|
"spec_class_basis": "",
|
||||||
|
"blocked_kind": None,
|
||||||
|
"blocked_reason": "",
|
||||||
|
"composite_not_ready": None,
|
||||||
"in_bill": in_bill,
|
"in_bill": in_bill,
|
||||||
"in_bill_reason": str(entry.get("reason") or ""),
|
"in_bill_reason": str(entry.get("reason") or ""),
|
||||||
"origin": ORIGIN_HAUL,
|
"origin": ORIGIN_HAUL,
|
||||||
@@ -510,6 +532,30 @@ def _haul_rows(
|
|||||||
return rows, unmatched
|
return rows, unmatched
|
||||||
|
|
||||||
|
|
||||||
|
def blocked_of(structure: dict[str, Any], class_basis: str = "") -> tuple[str | None, str]:
|
||||||
|
"""(막힌 갈래, 사유). 안 막혔으면 `(None, "")`.
|
||||||
|
|
||||||
|
⚠ 사유 문구는 **`B08_Quantity_Wording` 것을 그대로** 쓴다 — 두 벌로 짜면 갈린다.
|
||||||
|
전개 알림(`notes`)에 이미 사람 말로 적혀 있으므로 그것을 그대로 옮긴다.
|
||||||
|
"""
|
||||||
|
notes = [str(note) for note in structure.get("notes") or []]
|
||||||
|
if structure.get("components"):
|
||||||
|
# 물량은 섰는데 **단가 갈래**를 못 고른 자리(돌쌓기 뒷길이 등).
|
||||||
|
if class_basis and "입력되지 않았습니다" in class_basis:
|
||||||
|
return BLOCKED_INPUT_MISSING, class_basis
|
||||||
|
return None, ""
|
||||||
|
for note in notes:
|
||||||
|
if "입력되지 않았습니다" in note:
|
||||||
|
return BLOCKED_INPUT_MISSING, note
|
||||||
|
if "자료에 없습니다" in note or "표준 물량 자료" in note:
|
||||||
|
return BLOCKED_UNIT_DATA_MISSING, note
|
||||||
|
if "산출식이 아직 없습니다" in note:
|
||||||
|
return BLOCKED_FORMULA_MISSING, note
|
||||||
|
if notes:
|
||||||
|
return BLOCKED_UNIT_DATA_MISSING, notes[0]
|
||||||
|
return None, ""
|
||||||
|
|
||||||
|
|
||||||
def _structure_rows(
|
def _structure_rows(
|
||||||
unit_quantity_table: dict[str, Any], mapping: WorkItemMapping
|
unit_quantity_table: dict[str, Any], mapping: WorkItemMapping
|
||||||
) -> tuple[list[dict[str, Any]], list[str]]:
|
) -> tuple[list[dict[str, Any]], list[str]]:
|
||||||
@@ -534,9 +580,10 @@ def _structure_rows(
|
|||||||
composite = mapping.composite_for(type_id) if code is None else None
|
composite = mapping.composite_for(type_id) if code is None else None
|
||||||
kind = structure_kind(structure) if composite else None
|
kind = structure_kind(structure) if composite else None
|
||||||
parts: list[dict[str, Any]] | None = None
|
parts: list[dict[str, Any]] | None = None
|
||||||
parts_missing: list[str] = []
|
parts_missing: list[dict[str, Any]] = []
|
||||||
if composite:
|
if composite:
|
||||||
parts, parts_missing = composite_quantities(structure, composite, mapping)
|
parts, parts_missing = composite_quantities(structure, composite, mapping)
|
||||||
|
blocked_kind, blocked_reason = blocked_of(structure, class_basis)
|
||||||
if code is None and composite is None:
|
if code is None and composite is None:
|
||||||
unmatched.append(f"{wording_type_label(type_id)} — 품셈 공종을 아직 못 이었습니다")
|
unmatched.append(f"{wording_type_label(type_id)} — 품셈 공종을 아직 못 이었습니다")
|
||||||
elif entry.get("class_from") == "back_length" and class_key is None:
|
elif entry.get("class_from") == "back_length" and class_key is None:
|
||||||
@@ -558,11 +605,15 @@ def _structure_rows(
|
|||||||
"haul_equipment": None,
|
"haul_equipment": None,
|
||||||
"station_from": structure.get("start_m"),
|
"station_from": structure.get("start_m"),
|
||||||
"station_to": structure.get("end_m"),
|
"station_to": structure.get("end_m"),
|
||||||
|
"excavation_method": None,
|
||||||
"spec_detail": _spec_detail(structure),
|
"spec_detail": _spec_detail(structure),
|
||||||
# 품셈에 그 이름의 공종이 없어 여러 공종을 묶는 자리 — 빈 코드와 구별한다.
|
# 품셈에 그 이름의 공종이 없어 여러 공종을 묶는 자리 — 빈 코드와 구별한다.
|
||||||
"composite_parts": parts,
|
"composite_parts": parts,
|
||||||
# 철근이 있나 없나로 자동 판정 — 사람이 고르는 값이 아니다.
|
# 철근이 있나 없나로 자동 판정 — 사람이 고르는 값이 아니다.
|
||||||
"structure_kind": kind,
|
"structure_kind": kind,
|
||||||
|
# ⚠ 줄마다 **왜 막혔는지**를 싣는다 — 안 실으면 받는 쪽 화면이 빈다.
|
||||||
|
"blocked_kind": blocked_kind,
|
||||||
|
"blocked_reason": blocked_reason,
|
||||||
# 규격 갈래(뒷길이 …㎝ 이하) — 못 고르면 사유가 남는다.
|
# 규격 갈래(뒷길이 …㎝ 이하) — 못 고르면 사유가 남는다.
|
||||||
"spec_class": class_key,
|
"spec_class": class_key,
|
||||||
"spec_class_basis": class_basis,
|
"spec_class_basis": class_basis,
|
||||||
|
|||||||
Reference in New Issue
Block a user