fix(B08): 코드 없는 줄에 막힘 표시를 붙임 — 금액이 조용히 빠지던 자리

랩탑 메인이 화면에서 잡음: 「측구터파기 · 굴삭기+브레카」가 work_item_code null
인데 blocked_kind 도 null 이라 아무 말 없이 금액만 빠졌음.

인계본의 bill_flag_warnings·unmatched_work_items 에는 이미 실려 있었음 —
목록은 있는데 줄에 표시가 없었던 것. 줄 단위로 보는 쪽(B09·화면)은 목록을 안 봄.
같은 모양이 여덟 줄이었음(흙깎기·측구터파기·도자운반 둘·덤프운반 둘·지장목제거·큰돌쌓기).

- 토공 줄: 코드가 없으면 시공법 미지정이면 input_missing, 매핑을 못 이었으면
  unit_data_missing 으로 갈라 답. 다음에 할 일이 다름(사용자 입력 vs 매핑 작업).
- 운반 줄: 같은 규칙. 무대(free_haul)는 품에 포함이라 안 세우는 것이지 막힌 게
  아니므로 표시를 안 답.
- 불변식 시험을 세움 — 코드도 막힘 표시도 없이 내역에 서는 줄이 하나라도 있으면 깨짐.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 13:10:22 +09:00
co-authored by Claude Opus 5
parent 072f560a59
commit e71147c1b2
@@ -131,9 +131,22 @@ def _earthwork_rows(
elif mismatch is not None:
spec_detail = f"집계 {amount:,.2f} {unit} (품셈 밑수 {mismatch[0]})"
unit, amount = mismatch[0], 0.0
# ⚠⚠ **코드가 없으면 반드시 막힘 표시를 단다**(2026-09-09 랩탑 메인 제보).
# 종전에는 `unmatched_work_items` 목록과 `bill_flag_warnings` 에만 실려,
# **줄 단위로 보는 쪽**(B09·화면)이 「코드도 없고 막힘 표시도 없는 멀쩡한 줄」로
# 읽었다 — 금액이 조용히 빠졌다(측구터파기·흙깎기 「굴삭기+브레카」).
# ⇒ 오늘 세운 규칙 「막혔다고 말하기 전에 `blocked_kind` 를 볼 것」의 **뒤집힌 얼굴**:
# 보는 쪽을 고쳤으면 **다는 쪽도** 빠짐없이 달아야 한다.
blocked_kind = mismatch[1] if mismatch else None
blocked_reason = mismatch[2] if mismatch else ""
if code is None and not is_subtotal:
label = f"{group}({ground})" if ground else group
unmatched.append(f"{label}{method_note}" if method_note else label)
if blocked_kind is None:
# 시공법을 고르면 풀리는 자리와, 품셈을 아직 못 이은 자리를 **갈라 적는다** —
# 다음에 할 일이 다르다(하나는 사용자 입력, 하나는 매핑 작업).
blocked_kind = BLOCKED_INPUT_MISSING if method_note else BLOCKED_UNIT_DATA_MISSING
blocked_reason = method_note or f"{label} 의 품셈 공종을 아직 못 이었습니다"
rows.append(
{
"work_item_code": code,
@@ -162,9 +175,9 @@ def _earthwork_rows(
"secondary_axes": None,
"spec_class": None,
"spec_class_basis": "",
# 토공 줄도 막힐 수 있다 — 품셈 밑수와 단위가 다르면 그 사유가 실린다.
"blocked_kind": mismatch[1] if mismatch else None,
"blocked_reason": mismatch[2] if mismatch else "",
# 토공 줄도 막힐 수 있다 — 밑수 어긋남과 **코드 없음** 둘 다 여기 실린다.
"blocked_kind": blocked_kind,
"blocked_reason": blocked_reason,
"composite_not_ready": None,
# 합계 줄과 무대 줄은 값은 내되 내역에 안 선다.
"in_bill": bool(row.get("in_bill", True)) and not is_subtotal and mismatch is None,
@@ -191,6 +204,13 @@ def _haul_rows(
in_bill = bool(row.get("in_bill", True)) and entry.get("in_bill", True)
if code is None and in_bill:
unmatched.append(f"운반({equipment})")
# ⚠ 코드가 없으면 **줄에 막힘 표시를 단다**(2026-09-09) — 목록에만 실으면 줄 단위로
# 보는 쪽이 「멀쩡한 줄」로 읽어 금액이 조용히 빠진다(도자운반·덤프운반이 그랬다).
# ⚠ `in_bill` 이 False 인 무대 줄은 **막힌 것이 아니다** — 품에 포함이라 안 세우는 것.
haul_blocked = BLOCKED_UNIT_DATA_MISSING if (code is None and in_bill) else None
haul_blocked_reason = (
f"운반({equipment})의 품셈 공종을 아직 못 이었습니다" if haul_blocked else ""
)
# ⚠⚠ **내역서 수량은 자연상태다** — 유토곡선은 다짐상태로 쌓고(운반거리를 그 기준으로
# 재야 맞는다) 내역에 오르는 수량은 되돌린 값이다(`config_system_design` 5-4-3
# 「운반거리 산정 시 모든 수량은 다짐상태로 환산해 계산하고, **내역서에 적용하는
@@ -242,8 +262,8 @@ def _haul_rows(
"secondary_axes": None,
"spec_class": None,
"spec_class_basis": "",
"blocked_kind": None,
"blocked_reason": "",
"blocked_kind": haul_blocked,
"blocked_reason": haul_blocked_reason,
"composite_not_ready": None,
"in_bill": in_bill,
"in_bill_reason": str(entry.get("reason") or ""),