"""Cálculo de métricas de Contratos (``Contrato_oPT__c``). Calcula, a partir dos DataFrames já extraídos do Salesforce: - quantos contratos foram modificados no dia e quem modificou cada um (usa apenas campos padrão: ``LastModifiedDate``/``LastModifiedBy``); - o total de reajuste aplicado no MÊS corrente, quando o módulo configurável de reajuste estiver ativo (``value_field``/``previous_value_field``); - um alerta de qualidade de dados quando o reajuste informado no contrato diverge do delta calculado (valor atual - valor anterior), que costuma indicar erro de preenchimento (ex.: valor anterior errado). Segue o princípio do projeto: Python calcula, a IA apenas interpreta o JSON resultante. Este módulo NUNCA deve ser chamado pela camada de IA. """ from __future__ import annotations from typing import Any import pandas as pd AVISO_REAJUSTE = ( "Os valores de reajuste são estimados pela diferença entre o valor atual " "e o valor anterior do contrato. Podem não refletir com 100% de precisão " "o reajuste real caso haja erro de preenchimento no contrato (ex.: valor " "anterior lançado incorretamente)." ) _TOLERANCIA_CENTAVOS = 0.01 _TOLERANCIA_PONTOS_PERCENTUAIS = 0.1 def _valor_relacionamento(linha: Any, campo: str) -> Any: atual: Any = linha for parte in campo.split("."): if atual is None: return None if isinstance(atual, dict): atual = atual.get(parte) elif hasattr(atual, "get"): atual = atual.get(parte) else: return None return atual def _serializar_data(valor: Any) -> str | None: if valor is None: return None try: if pd.isna(valor): return None except (TypeError, ValueError): pass try: return valor.isoformat() except AttributeError: texto = str(valor).strip() return texto or None def _listar_contratos_modificados(df: pd.DataFrame, account_name_field: str) -> list[dict[str, Any]]: if df is None or df.empty: return [] itens: list[dict[str, Any]] = [] for _, linha in df.iterrows(): item: dict[str, Any] = { "id": linha.get("Id"), "name": linha.get("Name") or linha.get("Id"), "modified_by": _valor_relacionamento(linha, "LastModifiedBy.Name"), "modified_at": _serializar_data(linha.get("LastModifiedDate")), } if account_name_field: item["account_name"] = _valor_relacionamento(linha, account_name_field) itens.append(item) return itens def calculate_contract_metrics( df_modificados: pd.DataFrame | None, df_reajustados: pd.DataFrame | None, source: dict[str, Any] | None, ) -> dict[str, Any]: source = source or {} account_name_field = source.get("account_name_field") or "" value_field = source.get("value_field") or "" previous_field = source.get("previous_value_field") or "" readjustment_field = source.get("readjustment_field") or "" readjustment_field_unit = (source.get("readjustment_field_unit") or "amount").strip().lower() if readjustment_field_unit not in ("amount", "percent"): readjustment_field_unit = "amount" readjustment_configured = bool( value_field and previous_field and source.get("readjustment_date_field") ) modificados = _listar_contratos_modificados(df_modificados, account_name_field) resultado: dict[str, Any] = { "configured": bool(source.get("object")), "modified_today_count": len(modificados), "modified_today": modificados, "readjustment_configured": readjustment_configured, "readjustment_month_count": 0, "readjustment_month_total": 0.0, "readjustment_month_avg_percent": None, "readjustment_inconsistent_count": 0, "readjustment_contracts": [], "readjustment_disclaimer": AVISO_REAJUSTE, } if not readjustment_configured or df_reajustados is None or df_reajustados.empty: return resultado df = df_reajustados.copy() df["_valor_atual"] = pd.to_numeric(df.get(value_field), errors="coerce") df["_valor_anterior"] = pd.to_numeric(df.get(previous_field), errors="coerce") validos = df.dropna(subset=["_valor_atual", "_valor_anterior"]) if validos.empty: return resultado validos = validos.copy() validos["_delta"] = validos["_valor_atual"] - validos["_valor_anterior"] tem_informado = bool(readjustment_field) and readjustment_field in validos.columns if tem_informado: validos["_informado"] = pd.to_numeric(validos[readjustment_field], errors="coerce") contratos: list[dict[str, Any]] = [] percentuais: list[float] = [] inconsistentes = 0 for _, linha in validos.iterrows(): anterior = float(linha["_valor_anterior"]) atual = float(linha["_valor_atual"]) delta = float(linha["_delta"]) pct = (delta / anterior * 100.0) if anterior else None if pct is not None: percentuais.append(pct) informado_pct: float | None = None informado_amount: float | None = None inconsistente = False if tem_informado: bruto = linha.get("_informado") if bruto is not None and not pd.isna(bruto): bruto = float(bruto) if readjustment_field_unit == "percent": informado_pct = bruto informado_amount = anterior * (bruto / 100.0) if anterior else None if pct is not None: inconsistente = abs(informado_pct - pct) > _TOLERANCIA_PONTOS_PERCENTUAIS else: informado_amount = bruto informado_pct = (bruto / anterior * 100.0) if anterior else None inconsistente = abs(informado_amount - delta) > _TOLERANCIA_CENTAVOS if inconsistente: inconsistentes += 1 item: dict[str, Any] = { "id": linha.get("Id"), "name": linha.get("Name") or linha.get("Id"), "previous_value": anterior, "current_value": atual, "delta": delta, "reported_readjustment_percent": informado_pct, "reported_readjustment_amount": informado_amount, "percent": pct, "inconsistent": inconsistente, } if account_name_field: item["account_name"] = _valor_relacionamento(linha, account_name_field) contratos.append(item) resultado["readjustment_month_count"] = len(contratos) resultado["readjustment_month_total"] = float(validos["_delta"].sum()) resultado["readjustment_month_avg_percent"] = ( sum(percentuais) / len(percentuais) if percentuais else None ) resultado["readjustment_inconsistent_count"] = inconsistentes resultado["readjustment_contracts"] = sorted( contratos, key=lambda c: abs(c["delta"]), reverse=True ) return resultado