feat: explain RTC blockers when no R3 candidates are eligible
This commit is contained in:
@@ -340,6 +340,19 @@ def blocker_filter_summary(session, *, tenant_id=None, agent_id=None, product_co
|
||||
}
|
||||
|
||||
|
||||
def rtc_blocker_summary(session, agent_id: int) -> dict:
|
||||
"""Read-only, installation-scoped RTC blocker aggregates for the cClassTrib page."""
|
||||
query = _blockers_query(session, agent_id=agent_id, field="RTC", status="OPEN", scope="all")
|
||||
blockers = int(session.scalar(query.with_only_columns(func.count(BlockedProduct.id)).order_by(None)) or 0)
|
||||
products = int(session.scalar(query.with_only_columns(func.count(func.distinct(BlockedProduct.product_code))).order_by(None)) or 0)
|
||||
reason = func.coalesce(BlockedProduct.evidence_summary["reason"].as_string(), BlockedProduct.reason_code)
|
||||
reasons = session.execute(
|
||||
query.with_only_columns(reason, func.count(func.distinct(BlockedProduct.product_code)))
|
||||
.group_by(reason).order_by(None)
|
||||
).all()
|
||||
return {"open_blockers": blockers, "products_affected": products, "reason_counts": {str(code or ""): int(count) for code, count in reasons}}
|
||||
|
||||
|
||||
def _blocker_breakdown_query(query):
|
||||
"""Group on the source column itself for PostgreSQL-safe SQL semantics."""
|
||||
return query.with_only_columns(BlockedProduct.field, func.count(BlockedProduct.id)).group_by(BlockedProduct.field).order_by(None)
|
||||
|
||||
@@ -190,9 +190,13 @@ def _rtc_products_view(resolution, enabled):
|
||||
ready = int(resolution.get("ready_to_apply", 0))
|
||||
conflicts = int(resolution.get("conflicts", 0)) + int(resolution.get("official_mismatch", 0))
|
||||
catalog = int(resolution.get("catalog_incompatible", 0))
|
||||
reasons = resolution.get("by_reason") if isinstance(resolution.get("by_reason"), dict) else {}
|
||||
simple_count = sum(int(count or 0) for reason, count in reasons.items() if str(reason).endswith("SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT"))
|
||||
other_reason_max = max((int(count or 0) for reason, count in reasons.items() if not str(reason).endswith("SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT")), default=0)
|
||||
blocked = total - valid - ready
|
||||
pending = total - valid
|
||||
return {"total": total, "valid": valid, "ready": ready, "blocked": blocked, "pending": pending, "conflicts": conflicts, "catalog_incompatible": catalog, "suggestion_only": int(resolution.get("suggestion_only", 0)),
|
||||
"simple_orientation_count": simple_count, "simple_orientation_dominant": simple_count > 0 and simple_count > other_reason_max,
|
||||
"headline": f"Reforma Tributária ativa — cClassTrib pendente em {pending} produto{'s' if pending != 1 else ''}" if pending else "Reforma Tributária ativa — todos os produtos com cClassTrib válido",
|
||||
"catalog": (resolution.get("catalog_compatibility") or {}).get("compatibility")}
|
||||
|
||||
|
||||
+15
-4
@@ -16,7 +16,7 @@ from .blocker_resolution import BlockerResolutionUnavailable, _company_key, reso
|
||||
from .context_keys import ALLOWED_KEYS, ENUMS
|
||||
from .installation_identity import reference_agent_version
|
||||
from .panel_presentation import RTC_MODE_EXPLANATION, RTC_MODE_LABELS, abort_reason_label, audit_outcome_label, candidate_status_label, catalog_state_label, job_kind_label, job_status_label, rtc_rule_label, rtc_status_view
|
||||
from .control_plane import blocker_detail, blocker_filter_summary, dashboard_summary, installation_view, jobs_status_summary, list_blockers, list_installations, list_jobs, list_panel_events
|
||||
from .control_plane import blocker_detail, blocker_filter_summary, dashboard_summary, installation_view, jobs_status_summary, list_blockers, list_installations, list_jobs, list_panel_events, rtc_blocker_summary
|
||||
from .human_auth import create_session, current_session, require_csrf, verify_password
|
||||
from .panel_presentation import format_ui_datetime
|
||||
from .models import AdminUser, Agent, AgentEnrollment, AgentRelease, AgentSnapshot, BlockedProduct, FiscalContextEntry, Installation, Job, RtcCandidate, RtcWriteAudit, Tenant
|
||||
@@ -346,15 +346,26 @@ def router(session_factory):
|
||||
status = q.get("status") or "PENDING"
|
||||
page = max(1, int(q.get("page", "1") or 1)) if str(q.get("page", "1")).isdigit() else 1
|
||||
size = 50
|
||||
base = select(RtcCandidate).where(RtcCandidate.installation_id == installation.id)
|
||||
counts = dict(session.execute(select(RtcCandidate.status, func.count()).where(RtcCandidate.installation_id == installation.id).group_by(RtcCandidate.status)).all())
|
||||
base = select(RtcCandidate).where(RtcCandidate.installation_id == installation.id, RtcCandidate.rule_id == "RTC-R3")
|
||||
counts = dict(session.execute(select(RtcCandidate.status, func.count()).where(RtcCandidate.installation_id == installation.id, RtcCandidate.rule_id == "RTC-R3").group_by(RtcCandidate.status)).all())
|
||||
shown = base.where(RtcCandidate.status == status) if status in ("PENDING", "APPROVED", "REJECTED", "APPLIED", "STALE") else base
|
||||
total = int(session.scalar(select(func.count()).select_from(shown.subquery())) or 0)
|
||||
rows = list(session.scalars(shown.order_by(RtcCandidate.product_code).offset((page - 1) * size).limit(size)))
|
||||
audits = list(session.scalars(select(RtcWriteAudit).where(RtcWriteAudit.installation_id == installation.id, RtcWriteAudit.outcome.in_(("APPLIED", "ROLLED_BACK"))).order_by(RtcWriteAudit.id.desc()).limit(25)))
|
||||
ops = _ops_view(session, installation, agent)
|
||||
rtc_blockers = rtc_blocker_summary(session, agent_id)
|
||||
rtc_products = ((ops.get("rtc") or {}).get("products") or {})
|
||||
r3_candidates = int(rtc_products.get("ready", counts.get("PENDING", 0) + counts.get("APPROVED", 0)))
|
||||
blocked_products = max(int(rtc_products.get("blocked", 0)), rtc_blockers["products_affected"])
|
||||
reasons = rtc_blockers["reason_counts"]
|
||||
simple_reason_count = sum(count for reason, count in reasons.items() if reason.endswith("SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT"))
|
||||
simple_reason_count = max(simple_reason_count, int(rtc_products.get("simple_orientation_count", 0)))
|
||||
other_reason_max = max((count for reason, count in reasons.items() if not reason.endswith("SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT")), default=0)
|
||||
simple_orientation_dominant = bool(rtc_products.get("simple_orientation_dominant")) or simple_reason_count > other_reason_max
|
||||
rtc_empty_state = {"show": r3_candidates == 0 and blocked_products > 0, "r3_candidates": r3_candidates,
|
||||
"blocked_products": blocked_products, "simple_orientation_dominant": simple_orientation_dominant}
|
||||
return render(request, "rtc_control.html", {"agent_id": agent_id, "installation": installation, "mode": installation.rtc_write_mode, "modes": RTC_MODE_LABELS, "explanation": RTC_MODE_EXPLANATION, "ops": ops,
|
||||
"privileged": _is_privileged(auth_data[1]), "rows": rows, "counts": counts, "status": status, "page": page, "pages": max(1, -(-total // size)), "total": total, "audits": audits,
|
||||
"privileged": _is_privileged(auth_data[1]), "rows": rows, "counts": counts, "status": status, "page": page, "pages": max(1, -(-total // size)), "total": total, "audits": audits, "rtc_empty_state": rtc_empty_state,
|
||||
"L": {"status": candidate_status_label, "outcome": audit_outcome_label, "reason": abort_reason_label, "rule": rtc_rule_label, "catalog": catalog_state_label}})
|
||||
finally: session.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user