agroplan-ai-cli 1.0.25 → 1.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"cli_version": "1.0.
|
|
3
|
-
"backend_template_version": "1.0.
|
|
2
|
+
"cli_version": "1.0.27",
|
|
3
|
+
"backend_template_version": "1.0.27",
|
|
4
4
|
"zarc_index_version": "2025-2026-fast-index-v2",
|
|
5
5
|
"price_index_version": "2025-05-reference-v1",
|
|
6
6
|
"features": [
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"price_provider_index_fallback",
|
|
14
14
|
"price_display_only",
|
|
15
15
|
"price_unit_normalization",
|
|
16
|
-
"market_profit_estimate"
|
|
16
|
+
"market_profit_estimate",
|
|
17
|
+
"market_profit_validation",
|
|
18
|
+
"market_profit_confidence_refinement"
|
|
17
19
|
],
|
|
18
|
-
"generated_at": "2026-05-
|
|
20
|
+
"generated_at": "2026-05-09T23:00:00Z"
|
|
19
21
|
}
|
package/backend-template/api.py
CHANGED
|
@@ -523,6 +523,63 @@ def get_precos_comparar(
|
|
|
523
523
|
except Exception as e:
|
|
524
524
|
raise HTTPException(status_code=500, detail=str(e))
|
|
525
525
|
|
|
526
|
+
@app.get("/debug/lucro-mercado")
|
|
527
|
+
def get_debug_lucro_mercado(
|
|
528
|
+
uf: Optional[str] = Query(None, description="Unidade Federativa (ex: SP, PR)"),
|
|
529
|
+
municipio: Optional[str] = Query(None, description="Município"),
|
|
530
|
+
safra: str = Query("2025/2026", description="Safra ZARC")
|
|
531
|
+
):
|
|
532
|
+
"""Diagnóstico detalhado do lucro de mercado para validação"""
|
|
533
|
+
try:
|
|
534
|
+
from core.loader import carregar_dados
|
|
535
|
+
from core.planner import gerar_plano_inteligente
|
|
536
|
+
from core.price_adapter import aplicar_precos_no_plano
|
|
537
|
+
from core.market_profit_validator import gerar_diagnostico_lucro_mercado
|
|
538
|
+
|
|
539
|
+
# Carregar dados
|
|
540
|
+
culturas, talhoes, regras = carregar_dados()
|
|
541
|
+
|
|
542
|
+
# Gerar plano inteligente
|
|
543
|
+
plano_inteligente = gerar_plano_inteligente(culturas, talhoes, regras)
|
|
544
|
+
|
|
545
|
+
# Mapear para formato esperado pelo price_adapter
|
|
546
|
+
plano = []
|
|
547
|
+
for item in plano_inteligente:
|
|
548
|
+
plano.append({
|
|
549
|
+
'talhao': item['talhao'],
|
|
550
|
+
'area': item['area'],
|
|
551
|
+
'solo': item['solo'],
|
|
552
|
+
'clima': item['clima'],
|
|
553
|
+
'relevo': item['relevo'],
|
|
554
|
+
'agua': item['agua'],
|
|
555
|
+
'cultura': item['cultura_recomendada'], # Mapear cultura_recomendada -> cultura
|
|
556
|
+
'lucro_estimado': item['lucro_estimado'],
|
|
557
|
+
'risco': item['risco'],
|
|
558
|
+
'nota': item['nota'],
|
|
559
|
+
'tempo': item['tempo']
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
resultado = {"plano": plano}
|
|
563
|
+
|
|
564
|
+
# Aplicar preços e normalização
|
|
565
|
+
resultado = aplicar_precos_no_plano(resultado, uf=uf)
|
|
566
|
+
|
|
567
|
+
# Gerar diagnóstico
|
|
568
|
+
diagnostico = gerar_diagnostico_lucro_mercado(resultado["plano"], uf=uf)
|
|
569
|
+
|
|
570
|
+
# Adicionar resumo de validação
|
|
571
|
+
validacao = resultado.get("validacao_lucro_mercado", {})
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
"diagnostico": diagnostico,
|
|
575
|
+
"validacao_resumo": validacao,
|
|
576
|
+
"municipio": municipio,
|
|
577
|
+
"safra": safra
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
except Exception as e:
|
|
581
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
582
|
+
|
|
526
583
|
@app.get("/dashboard")
|
|
527
584
|
def get_dashboard(
|
|
528
585
|
lat: Optional[float] = None,
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validador de Lucro de Mercado
|
|
3
|
+
|
|
4
|
+
Classifica confiabilidade dos valores de lucro de mercado comparando com lucro do sistema.
|
|
5
|
+
Detecta distorções e fornece diagnóstico para validação antes de ativar recálculo automático.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def calcular_diferenca_lucro(lucro_sistema: float, lucro_mercado: float) -> Dict:
|
|
12
|
+
"""
|
|
13
|
+
Calcula diferença entre lucro do sistema e lucro de mercado.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
lucro_sistema: Lucro calculado com base interna
|
|
17
|
+
lucro_mercado: Lucro calculado com preços de mercado normalizados
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Dict com diferença absoluta, percentual e direção
|
|
21
|
+
"""
|
|
22
|
+
diferenca_absoluta = lucro_mercado - lucro_sistema
|
|
23
|
+
|
|
24
|
+
# Calcular percentual baseado no valor absoluto do lucro sistema
|
|
25
|
+
# para evitar divisão por zero e lidar com valores negativos
|
|
26
|
+
if lucro_sistema != 0:
|
|
27
|
+
diferenca_percentual = (diferenca_absoluta / abs(lucro_sistema)) * 100
|
|
28
|
+
else:
|
|
29
|
+
diferenca_percentual = 0 if lucro_mercado == 0 else float('inf')
|
|
30
|
+
|
|
31
|
+
# Determinar direção
|
|
32
|
+
if abs(diferenca_percentual) < 5:
|
|
33
|
+
direcao = "igual"
|
|
34
|
+
elif diferenca_absoluta > 0:
|
|
35
|
+
direcao = "maior"
|
|
36
|
+
else:
|
|
37
|
+
direcao = "menor"
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
"diferenca_absoluta": round(diferenca_absoluta, 2),
|
|
41
|
+
"diferenca_percentual": round(diferenca_percentual, 2),
|
|
42
|
+
"direcao": direcao
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def classificar_confiabilidade_lucro(item: Dict) -> Dict:
|
|
47
|
+
"""
|
|
48
|
+
Classifica confiabilidade do lucro de mercado para um item do plano.
|
|
49
|
+
|
|
50
|
+
Critérios refinados (Fase 9.5):
|
|
51
|
+
- Baixa: diferença >= 150%, dados incompletos, lucro invertido, fallback com diferença >= 100%
|
|
52
|
+
- Média: diferença 50-150%, fallback, lucro negativo
|
|
53
|
+
- Alta: diferença < 50%, sem fallback, dados completos
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
item: Item do plano com dados de lucro
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Dict com confiabilidade (alta/media/baixa), motivos e flag crítico
|
|
60
|
+
"""
|
|
61
|
+
motivos = []
|
|
62
|
+
critico = False
|
|
63
|
+
|
|
64
|
+
# Verificar se tem preço normalizado
|
|
65
|
+
preco_norm = item.get("preco_normalizado", {})
|
|
66
|
+
if not preco_norm.get("normalizado"):
|
|
67
|
+
return {
|
|
68
|
+
"confiabilidade": "baixa",
|
|
69
|
+
"motivos": ["Preço não normalizado ou não disponível"],
|
|
70
|
+
"critico": True
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Verificar se tem produtividade e custo
|
|
74
|
+
produtividade = item.get("produtividade", 0)
|
|
75
|
+
custo = item.get("custo", 0)
|
|
76
|
+
|
|
77
|
+
if produtividade <= 0:
|
|
78
|
+
motivos.append("Produtividade não disponível ou inválida")
|
|
79
|
+
|
|
80
|
+
if custo <= 0:
|
|
81
|
+
motivos.append("Custo não disponível ou inválido")
|
|
82
|
+
|
|
83
|
+
if motivos:
|
|
84
|
+
return {
|
|
85
|
+
"confiabilidade": "baixa",
|
|
86
|
+
"motivos": motivos,
|
|
87
|
+
"critico": True
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Verificar se tem lucro de mercado calculado
|
|
91
|
+
lucro_mercado = item.get("lucro_mercado_estimado")
|
|
92
|
+
if lucro_mercado is None:
|
|
93
|
+
return {
|
|
94
|
+
"confiabilidade": "baixa",
|
|
95
|
+
"motivos": ["Lucro de mercado não calculado"],
|
|
96
|
+
"critico": True
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Calcular diferença com lucro do sistema
|
|
100
|
+
lucro_sistema = item.get("lucro_estimado", 0)
|
|
101
|
+
diferenca = calcular_diferenca_lucro(lucro_sistema, lucro_mercado)
|
|
102
|
+
diferenca_percentual = abs(diferenca["diferenca_percentual"])
|
|
103
|
+
|
|
104
|
+
# Verificar se é fallback
|
|
105
|
+
preco_real = item.get("preco_real", {})
|
|
106
|
+
is_fallback = preco_real.get("fallback", False)
|
|
107
|
+
|
|
108
|
+
# Verificar inversão de sinal (lucro vira prejuízo ou vice-versa)
|
|
109
|
+
lucro_invertido = (lucro_sistema > 0 and lucro_mercado < 0) or (lucro_sistema < 0 and lucro_mercado > 0)
|
|
110
|
+
|
|
111
|
+
# CRITÉRIOS REFINADOS (Fase 9.5)
|
|
112
|
+
|
|
113
|
+
# Baixa confiabilidade (crítico)
|
|
114
|
+
if diferenca_percentual >= 150:
|
|
115
|
+
confiabilidade = "baixa"
|
|
116
|
+
motivos.append(f"Diferença extrema ({diferenca_percentual:.1f}%) entre lucro sistema e mercado")
|
|
117
|
+
critico = True
|
|
118
|
+
elif lucro_invertido and lucro_sistema > 0:
|
|
119
|
+
confiabilidade = "baixa"
|
|
120
|
+
motivos.append("Lucro do sistema é positivo mas lucro de mercado indica prejuízo")
|
|
121
|
+
critico = True
|
|
122
|
+
elif is_fallback and diferenca_percentual >= 100:
|
|
123
|
+
confiabilidade = "baixa"
|
|
124
|
+
motivos.append(f"Preço de fallback com diferença muito alta ({diferenca_percentual:.1f}%)")
|
|
125
|
+
critico = True
|
|
126
|
+
elif diferenca_percentual >= 100:
|
|
127
|
+
# Diferença > 100% mesmo sem fallback é baixa confiabilidade
|
|
128
|
+
confiabilidade = "baixa"
|
|
129
|
+
motivos.append(f"Diferença muito alta ({diferenca_percentual:.1f}%) entre lucro sistema e mercado")
|
|
130
|
+
critico = False # Não crítico se não for fallback
|
|
131
|
+
|
|
132
|
+
# Média confiabilidade
|
|
133
|
+
elif diferenca_percentual >= 50:
|
|
134
|
+
confiabilidade = "media"
|
|
135
|
+
motivos.append(f"Diferença moderada ({diferenca_percentual:.1f}%) entre lucro sistema e mercado")
|
|
136
|
+
elif is_fallback:
|
|
137
|
+
confiabilidade = "media"
|
|
138
|
+
motivos.append("Preço de referência (fallback) - pode não refletir mercado local")
|
|
139
|
+
elif lucro_mercado < 0:
|
|
140
|
+
confiabilidade = "media"
|
|
141
|
+
motivos.append("Lucro de mercado indica prejuízo - requer validação")
|
|
142
|
+
|
|
143
|
+
# Alta confiabilidade
|
|
144
|
+
else:
|
|
145
|
+
confiabilidade = "alta"
|
|
146
|
+
motivos.append(f"Diferença aceitável ({diferenca_percentual:.1f}%) entre lucro sistema e mercado")
|
|
147
|
+
if not is_fallback:
|
|
148
|
+
motivos.append("Preço real disponível (não fallback)")
|
|
149
|
+
|
|
150
|
+
# Adicionar informação sobre normalização bem-sucedida
|
|
151
|
+
if preco_norm.get("normalizado") and confiabilidade != "baixa":
|
|
152
|
+
motivos.append("Unidade comercial normalizada com sucesso")
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
"confiabilidade": confiabilidade,
|
|
156
|
+
"motivos": motivos,
|
|
157
|
+
"diferenca": diferenca,
|
|
158
|
+
"critico": critico
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def validar_plano_lucro_mercado(resultado: Dict) -> Dict:
|
|
163
|
+
"""
|
|
164
|
+
Valida lucro de mercado para todo o plano e adiciona classificação de confiabilidade.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
resultado: Resultado do AG ou cenário com plano
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Resultado enriquecido com validação de lucro de mercado
|
|
171
|
+
"""
|
|
172
|
+
if "plano" not in resultado:
|
|
173
|
+
return resultado
|
|
174
|
+
|
|
175
|
+
# Contadores
|
|
176
|
+
alta_confiabilidade = 0
|
|
177
|
+
media_confiabilidade = 0
|
|
178
|
+
baixa_confiabilidade = 0
|
|
179
|
+
itens_criticos = 0
|
|
180
|
+
alertas = []
|
|
181
|
+
|
|
182
|
+
# Validar cada item do plano
|
|
183
|
+
for item in resultado["plano"]:
|
|
184
|
+
# Classificar confiabilidade
|
|
185
|
+
validacao = classificar_confiabilidade_lucro(item)
|
|
186
|
+
item["validacao_lucro_mercado"] = validacao
|
|
187
|
+
|
|
188
|
+
# Contabilizar
|
|
189
|
+
conf = validacao["confiabilidade"]
|
|
190
|
+
if conf == "alta":
|
|
191
|
+
alta_confiabilidade += 1
|
|
192
|
+
elif conf == "media":
|
|
193
|
+
media_confiabilidade += 1
|
|
194
|
+
else:
|
|
195
|
+
baixa_confiabilidade += 1
|
|
196
|
+
# Adicionar alerta para baixa confiabilidade
|
|
197
|
+
cultura = item.get("cultura", "desconhecida")
|
|
198
|
+
talhao = item.get("talhao", "?")
|
|
199
|
+
alertas.append(f"Talhão {talhao} ({cultura}): {', '.join(validacao['motivos'])}")
|
|
200
|
+
|
|
201
|
+
# Contabilizar itens críticos
|
|
202
|
+
if validacao.get("critico", False):
|
|
203
|
+
itens_criticos += 1
|
|
204
|
+
|
|
205
|
+
# Adicionar resumo de validação
|
|
206
|
+
total = len(resultado["plano"])
|
|
207
|
+
percentual_alta = (alta_confiabilidade / total * 100) if total > 0 else 0
|
|
208
|
+
percentual_baixa = (baixa_confiabilidade / total * 100) if total > 0 else 0
|
|
209
|
+
percentual_critico = (itens_criticos / total * 100) if total > 0 else 0
|
|
210
|
+
|
|
211
|
+
resultado["validacao_lucro_mercado"] = {
|
|
212
|
+
"ativo": True,
|
|
213
|
+
"total_itens": total,
|
|
214
|
+
"itens_alta_confiabilidade": alta_confiabilidade,
|
|
215
|
+
"itens_media_confiabilidade": media_confiabilidade,
|
|
216
|
+
"itens_baixa_confiabilidade": baixa_confiabilidade,
|
|
217
|
+
"itens_criticos": itens_criticos,
|
|
218
|
+
"percentual_alta_confiabilidade": round(percentual_alta, 1),
|
|
219
|
+
"percentual_baixa_confiabilidade": round(percentual_baixa, 1),
|
|
220
|
+
"percentual_critico": round(percentual_critico, 1),
|
|
221
|
+
"alertas": alertas[:5], # Limitar a 5 alertas principais
|
|
222
|
+
"total_alertas": len(alertas),
|
|
223
|
+
"recomendacao": _gerar_recomendacao(percentual_alta, percentual_baixa, percentual_critico)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return resultado
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _gerar_recomendacao(percentual_alta: float, percentual_baixa: float, percentual_critico: float = 0) -> str:
|
|
230
|
+
"""
|
|
231
|
+
Gera recomendação baseada nos percentuais de confiabilidade.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
percentual_alta: Percentual de itens com alta confiabilidade
|
|
235
|
+
percentual_baixa: Percentual de itens com baixa confiabilidade
|
|
236
|
+
percentual_critico: Percentual de itens críticos
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
String com recomendação
|
|
240
|
+
"""
|
|
241
|
+
if percentual_critico >= 30:
|
|
242
|
+
return "Há valores críticos no lucro de mercado. Eles não devem ser usados para otimização sem validação manual."
|
|
243
|
+
elif percentual_alta >= 70:
|
|
244
|
+
return "Lucro de mercado apresenta boa confiabilidade. Considere validação detalhada antes de ativar PRICE_APPLY_TO_PROFIT."
|
|
245
|
+
elif percentual_baixa >= 50:
|
|
246
|
+
return "Muitos itens com baixa confiabilidade. Valide preços, produtividades e custos antes de usar lucro de mercado."
|
|
247
|
+
elif percentual_critico > 0:
|
|
248
|
+
return "Alguns valores críticos detectados. Revise itens com baixa confiabilidade antes de usar lucro de mercado."
|
|
249
|
+
else:
|
|
250
|
+
return "Confiabilidade mista. Revise itens com baixa confiabilidade e valide dados de mercado."
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def gerar_diagnostico_lucro_mercado(plano: List[Dict], uf: Optional[str] = None) -> Dict:
|
|
254
|
+
"""
|
|
255
|
+
Gera diagnóstico detalhado do lucro de mercado para análise.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
plano: Lista de itens do plano
|
|
259
|
+
uf: Unidade Federativa (opcional)
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Dict com diagnóstico por cultura
|
|
263
|
+
"""
|
|
264
|
+
diagnostico = {
|
|
265
|
+
"uf": uf,
|
|
266
|
+
"total_culturas": 0,
|
|
267
|
+
"culturas": {}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Agrupar por cultura
|
|
271
|
+
culturas_map = {}
|
|
272
|
+
for item in plano:
|
|
273
|
+
cultura = item.get("cultura", "desconhecida")
|
|
274
|
+
|
|
275
|
+
if cultura not in culturas_map:
|
|
276
|
+
culturas_map[cultura] = []
|
|
277
|
+
|
|
278
|
+
culturas_map[cultura].append(item)
|
|
279
|
+
|
|
280
|
+
# Gerar diagnóstico por cultura
|
|
281
|
+
for cultura, itens in culturas_map.items():
|
|
282
|
+
# Calcular médias
|
|
283
|
+
lucro_sistema_total = sum(i.get("lucro_estimado", 0) for i in itens)
|
|
284
|
+
lucro_mercado_total = sum(i.get("lucro_mercado_estimado", 0) for i in itens if i.get("lucro_mercado_estimado") is not None)
|
|
285
|
+
|
|
286
|
+
lucro_sistema_medio = lucro_sistema_total / len(itens) if itens else 0
|
|
287
|
+
lucro_mercado_medio = lucro_mercado_total / len(itens) if itens else 0
|
|
288
|
+
|
|
289
|
+
# Calcular diferença
|
|
290
|
+
diferenca = calcular_diferenca_lucro(lucro_sistema_medio, lucro_mercado_medio)
|
|
291
|
+
|
|
292
|
+
# Obter confiabilidade do primeiro item (representativo)
|
|
293
|
+
validacao = classificar_confiabilidade_lucro(itens[0]) if itens else {"confiabilidade": "baixa", "motivos": []}
|
|
294
|
+
|
|
295
|
+
# Obter preço normalizado
|
|
296
|
+
preco_norm = itens[0].get("preco_normalizado", {}) if itens else {}
|
|
297
|
+
preco_real = itens[0].get("preco_real", {}) if itens else {}
|
|
298
|
+
|
|
299
|
+
diagnostico["culturas"][cultura] = {
|
|
300
|
+
"total_talhoes": len(itens),
|
|
301
|
+
"lucro_sistema_medio": round(lucro_sistema_medio, 2),
|
|
302
|
+
"lucro_mercado_medio": round(lucro_mercado_medio, 2),
|
|
303
|
+
"diferenca": diferenca,
|
|
304
|
+
"confiabilidade": validacao["confiabilidade"],
|
|
305
|
+
"motivos": validacao["motivos"],
|
|
306
|
+
"preco_original": preco_real.get("preco"),
|
|
307
|
+
"unidade_original": preco_real.get("unidade"),
|
|
308
|
+
"preco_por_tonelada": preco_norm.get("preco_por_tonelada"),
|
|
309
|
+
"normalizado": preco_norm.get("normalizado", False),
|
|
310
|
+
"fallback": preco_real.get("fallback", False)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
diagnostico["total_culturas"] = len(culturas_map)
|
|
314
|
+
|
|
315
|
+
return diagnostico
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Adaptador de Preços Agrícolas
|
|
3
|
-
Integra preços reais no planejamento com normalização de unidades
|
|
3
|
+
Integra preços reais no planejamento com normalização de unidades e validação
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import os
|
|
@@ -12,6 +12,7 @@ from core.price_normalizer import (
|
|
|
12
12
|
calcular_lucro_com_preco_normalizado,
|
|
13
13
|
obter_estatisticas_normalizacao
|
|
14
14
|
)
|
|
15
|
+
from core.market_profit_validator import validar_plano_lucro_mercado
|
|
15
16
|
|
|
16
17
|
# Configuração
|
|
17
18
|
PRICE_APPLY_TO_PROFIT = os.getenv("PRICE_APPLY_TO_PROFIT", "false").lower() == "true"
|
|
@@ -171,6 +172,9 @@ def aplicar_precos_no_plano(
|
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
# Validar lucro de mercado e adicionar classificação de confiabilidade
|
|
176
|
+
resultado = validar_plano_lucro_mercado(resultado)
|
|
177
|
+
|
|
174
178
|
return resultado
|
|
175
179
|
|
|
176
180
|
|
|
@@ -169,6 +169,11 @@ def gerar_relatorio_completo(culturas, talhoes, regras, objetivo='equilibrado',
|
|
|
169
169
|
secao_precos = gerar_secao_precos_relatorio(resultado_temp["plano"], uf, formato)
|
|
170
170
|
conteudo += "\n\n" + secao_precos
|
|
171
171
|
|
|
172
|
+
# Adicionar seção de validação de lucro de mercado
|
|
173
|
+
if resultado_temp.get("validacao_lucro_mercado", {}).get("ativo"):
|
|
174
|
+
secao_validacao = gerar_secao_validacao_lucro_mercado(resultado_temp, formato)
|
|
175
|
+
conteudo += "\n\n" + secao_validacao
|
|
176
|
+
|
|
172
177
|
# Salva arquivo
|
|
173
178
|
os.makedirs('reports', exist_ok=True)
|
|
174
179
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
@@ -249,6 +254,217 @@ def gerar_secao_climatica(contexto_climatico, formato='md'):
|
|
|
249
254
|
return secao
|
|
250
255
|
|
|
251
256
|
|
|
257
|
+
def gerar_secao_validacao_lucro_mercado(resultado, formato='md'):
|
|
258
|
+
"""
|
|
259
|
+
Gera seção de validação de lucro de mercado para o relatório
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
resultado: Resultado com validacao_lucro_mercado
|
|
263
|
+
formato: 'md' ou 'txt'
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
String com seção formatada
|
|
267
|
+
"""
|
|
268
|
+
validacao = resultado.get("validacao_lucro_mercado", {})
|
|
269
|
+
|
|
270
|
+
if not validacao.get("ativo"):
|
|
271
|
+
return ""
|
|
272
|
+
|
|
273
|
+
if formato == "md":
|
|
274
|
+
secao = "## 🔍 Validação do Lucro de Mercado\n\n"
|
|
275
|
+
|
|
276
|
+
secao += "### Resumo de Confiabilidade\n\n"
|
|
277
|
+
|
|
278
|
+
total = validacao.get("total_itens", 0)
|
|
279
|
+
alta = validacao.get("itens_alta_confiabilidade", 0)
|
|
280
|
+
media = validacao.get("itens_media_confiabilidade", 0)
|
|
281
|
+
baixa = validacao.get("itens_baixa_confiabilidade", 0)
|
|
282
|
+
criticos = validacao.get("itens_criticos", 0)
|
|
283
|
+
|
|
284
|
+
perc_alta = validacao.get("percentual_alta_confiabilidade", 0)
|
|
285
|
+
perc_baixa = validacao.get("percentual_baixa_confiabilidade", 0)
|
|
286
|
+
perc_critico = validacao.get("percentual_critico", 0)
|
|
287
|
+
|
|
288
|
+
secao += f"- **Total de itens analisados**: {total}\n"
|
|
289
|
+
secao += f"- **Alta confiabilidade**: {alta} ({perc_alta:.1f}%) 🟢\n"
|
|
290
|
+
secao += f"- **Média confiabilidade**: {media} ({100 - perc_alta - perc_baixa:.1f}%) 🟡\n"
|
|
291
|
+
secao += f"- **Baixa confiabilidade**: {baixa} ({perc_baixa:.1f}%) 🔴\n"
|
|
292
|
+
|
|
293
|
+
if criticos > 0:
|
|
294
|
+
secao += f"- **⚠️ Itens críticos**: {criticos} ({perc_critico:.1f}%)\n"
|
|
295
|
+
|
|
296
|
+
secao += "\n"
|
|
297
|
+
|
|
298
|
+
# Aviso de itens críticos
|
|
299
|
+
if criticos > 0:
|
|
300
|
+
secao += "### ⚠️ ATENÇÃO: Valores Críticos Detectados\n\n"
|
|
301
|
+
secao += f"**{criticos} item(ns) apresenta(m) valores críticos** que indicam possível desalinhamento entre:\n"
|
|
302
|
+
secao += "- Preço de mercado vs preço interno\n"
|
|
303
|
+
secao += "- Produtividade estimada vs real\n"
|
|
304
|
+
secao += "- Custos operacionais\n"
|
|
305
|
+
secao += "- Unidade comercial\n\n"
|
|
306
|
+
secao += "**Estes valores não devem ser usados para otimização sem validação manual.**\n\n"
|
|
307
|
+
|
|
308
|
+
# Recomendação
|
|
309
|
+
recomendacao = validacao.get("recomendacao", "")
|
|
310
|
+
if recomendacao:
|
|
311
|
+
secao += f"**Recomendação**: {recomendacao}\n\n"
|
|
312
|
+
|
|
313
|
+
# Alertas
|
|
314
|
+
alertas = validacao.get("alertas", [])
|
|
315
|
+
if alertas:
|
|
316
|
+
secao += "### ⚠️ Alertas\n\n"
|
|
317
|
+
for alerta in alertas:
|
|
318
|
+
secao += f"- {alerta}\n"
|
|
319
|
+
secao += "\n"
|
|
320
|
+
|
|
321
|
+
total_alertas = validacao.get("total_alertas", 0)
|
|
322
|
+
if total_alertas > len(alertas):
|
|
323
|
+
secao += f"*Mostrando {len(alertas)} de {total_alertas} alertas*\n\n"
|
|
324
|
+
|
|
325
|
+
# Detalhes por item
|
|
326
|
+
secao += "### Detalhes por Talhão\n\n"
|
|
327
|
+
secao += "| Talhão | Cultura | Lucro Sistema | Lucro Mercado | Diferença % | Confiabilidade |\n"
|
|
328
|
+
secao += "|--------|---------|---------------|---------------|-------------|----------------|\n"
|
|
329
|
+
|
|
330
|
+
for item in resultado["plano"]:
|
|
331
|
+
validacao_item = item.get("validacao_lucro_mercado", {})
|
|
332
|
+
if not validacao_item:
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
talhao = item.get("talhao", "N/A")
|
|
336
|
+
cultura = item.get("cultura", "").upper()
|
|
337
|
+
lucro_sistema = item.get("lucro_estimado", 0)
|
|
338
|
+
lucro_mercado = item.get("lucro_mercado_estimado", 0)
|
|
339
|
+
|
|
340
|
+
lucro_sistema_fmt = f"R$ {lucro_sistema:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
|
341
|
+
|
|
342
|
+
if lucro_mercado is not None:
|
|
343
|
+
lucro_mercado_fmt = f"R$ {lucro_mercado:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
|
344
|
+
diferenca = validacao_item.get("diferenca", {})
|
|
345
|
+
diferenca_perc = diferenca.get("diferenca_percentual", 0)
|
|
346
|
+
diferenca_fmt = f"{diferenca_perc:.1f}%"
|
|
347
|
+
else:
|
|
348
|
+
lucro_mercado_fmt = "N/A"
|
|
349
|
+
diferenca_fmt = "N/A"
|
|
350
|
+
|
|
351
|
+
confiabilidade = validacao_item.get("confiabilidade", "baixa")
|
|
352
|
+
critico = validacao_item.get("critico", False)
|
|
353
|
+
|
|
354
|
+
if critico:
|
|
355
|
+
conf_emoji = "⚠️🔴"
|
|
356
|
+
conf_label = "**CRÍTICO**"
|
|
357
|
+
else:
|
|
358
|
+
conf_emoji = "🟢" if confiabilidade == "alta" else "🟡" if confiabilidade == "media" else "🔴"
|
|
359
|
+
conf_label = confiabilidade.title()
|
|
360
|
+
|
|
361
|
+
secao += f"| {talhao} | {cultura} | {lucro_sistema_fmt} | {lucro_mercado_fmt} | {diferenca_fmt} | {conf_emoji} {conf_label} |\n"
|
|
362
|
+
|
|
363
|
+
secao += "\n"
|
|
364
|
+
|
|
365
|
+
# Explicação
|
|
366
|
+
secao += "### 📊 Sobre a Classificação de Confiabilidade\n\n"
|
|
367
|
+
secao += "A confiabilidade do lucro de mercado é classificada com base em:\n\n"
|
|
368
|
+
secao += "- **Alta (🟢)**: Diferença < 50% entre lucro sistema e mercado, preço normalizado disponível\n"
|
|
369
|
+
secao += "- **Média (🟡)**: Diferença 50-150%, uso de fallback, ou lucro negativo\n"
|
|
370
|
+
secao += "- **Baixa (🔴)**: Diferença > 150%, dados incompletos, ou preço não disponível\n"
|
|
371
|
+
secao += "- **⚠️ CRÍTICO**: Diferença extrema (>150%), lucro invertido, ou fallback com diferença >100%\n\n"
|
|
372
|
+
|
|
373
|
+
secao += "### ⚠️ Aviso Importante\n\n"
|
|
374
|
+
secao += "**O lucro de mercado ainda é experimental e não substitui o lucro principal do sistema.**\n\n"
|
|
375
|
+
secao += "Os valores são exibidos apenas como comparação para validação. Diferenças altas indicam "
|
|
376
|
+
secao += "necessidade de validação de produtividade, custos ou unidade comercial.\n\n"
|
|
377
|
+
secao += "**Status atual**: `PRICE_APPLY_TO_PROFIT=false` (lucro de mercado não afeta otimização)\n"
|
|
378
|
+
|
|
379
|
+
else: # txt
|
|
380
|
+
secao = "VALIDAÇÃO DO LUCRO DE MERCADO\n\n"
|
|
381
|
+
|
|
382
|
+
secao += "Resumo de Confiabilidade:\n\n"
|
|
383
|
+
|
|
384
|
+
total = validacao.get("total_itens", 0)
|
|
385
|
+
alta = validacao.get("itens_alta_confiabilidade", 0)
|
|
386
|
+
media = validacao.get("itens_media_confiabilidade", 0)
|
|
387
|
+
baixa = validacao.get("itens_baixa_confiabilidade", 0)
|
|
388
|
+
|
|
389
|
+
perc_alta = validacao.get("percentual_alta_confiabilidade", 0)
|
|
390
|
+
perc_baixa = validacao.get("percentual_baixa_confiabilidade", 0)
|
|
391
|
+
|
|
392
|
+
secao += f" Total de itens analisados: {total}\n"
|
|
393
|
+
secao += f" Alta confiabilidade: {alta} ({perc_alta:.1f}%)\n"
|
|
394
|
+
secao += f" Média confiabilidade: {media} ({100 - perc_alta - perc_baixa:.1f}%)\n"
|
|
395
|
+
secao += f" Baixa confiabilidade: {baixa} ({perc_baixa:.1f}%)\n\n"
|
|
396
|
+
|
|
397
|
+
# Recomendação
|
|
398
|
+
recomendacao = validacao.get("recomendacao", "")
|
|
399
|
+
if recomendacao:
|
|
400
|
+
secao += f"Recomendação: {recomendacao}\n\n"
|
|
401
|
+
|
|
402
|
+
# Alertas
|
|
403
|
+
alertas = validacao.get("alertas", [])
|
|
404
|
+
if alertas:
|
|
405
|
+
secao += "Alertas:\n\n"
|
|
406
|
+
for alerta in alertas:
|
|
407
|
+
secao += f" - {alerta}\n"
|
|
408
|
+
secao += "\n"
|
|
409
|
+
|
|
410
|
+
total_alertas = validacao.get("total_alertas", 0)
|
|
411
|
+
if total_alertas > len(alertas):
|
|
412
|
+
secao += f" (Mostrando {len(alertas)} de {total_alertas} alertas)\n\n"
|
|
413
|
+
|
|
414
|
+
# Detalhes por item
|
|
415
|
+
secao += "Detalhes por Talhão:\n\n"
|
|
416
|
+
|
|
417
|
+
for item in resultado["plano"]:
|
|
418
|
+
validacao_item = item.get("validacao_lucro_mercado", {})
|
|
419
|
+
if not validacao_item:
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
talhao = item.get("talhao", "N/A")
|
|
423
|
+
cultura = item.get("cultura", "").upper()
|
|
424
|
+
lucro_sistema = item.get("lucro_estimado", 0)
|
|
425
|
+
lucro_mercado = item.get("lucro_mercado_estimado", 0)
|
|
426
|
+
|
|
427
|
+
lucro_sistema_fmt = f"R$ {lucro_sistema:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
|
428
|
+
|
|
429
|
+
secao += f" Talhão {talhao} ({cultura}):\n"
|
|
430
|
+
secao += f" Lucro Sistema: {lucro_sistema_fmt}\n"
|
|
431
|
+
|
|
432
|
+
if lucro_mercado is not None:
|
|
433
|
+
lucro_mercado_fmt = f"R$ {lucro_mercado:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
|
434
|
+
diferenca = validacao_item.get("diferenca", {})
|
|
435
|
+
diferenca_perc = diferenca.get("diferenca_percentual", 0)
|
|
436
|
+
secao += f" Lucro Mercado: {lucro_mercado_fmt}\n"
|
|
437
|
+
secao += f" Diferença: {diferenca_perc:.1f}%\n"
|
|
438
|
+
else:
|
|
439
|
+
secao += f" Lucro Mercado: N/A\n"
|
|
440
|
+
|
|
441
|
+
confiabilidade = validacao_item.get("confiabilidade", "baixa")
|
|
442
|
+
secao += f" Confiabilidade: {confiabilidade.title()}\n"
|
|
443
|
+
|
|
444
|
+
motivos = validacao_item.get("motivos", [])
|
|
445
|
+
if motivos:
|
|
446
|
+
secao += f" Motivos: {', '.join(motivos)}\n"
|
|
447
|
+
|
|
448
|
+
secao += "\n"
|
|
449
|
+
|
|
450
|
+
# Explicação
|
|
451
|
+
secao += "Sobre a Classificação de Confiabilidade:\n\n"
|
|
452
|
+
secao += "A confiabilidade do lucro de mercado é classificada com base em:\n\n"
|
|
453
|
+
secao += " - Alta: Diferença < 50% entre lucro sistema e mercado\n"
|
|
454
|
+
secao += " - Média: Diferença 50-100%, uso de fallback, ou lucro negativo\n"
|
|
455
|
+
secao += " - Baixa: Diferença > 100%, dados incompletos, ou preço não disponível\n\n"
|
|
456
|
+
|
|
457
|
+
secao += "Aviso Importante:\n\n"
|
|
458
|
+
secao += "O lucro de mercado ainda é experimental e não substitui o lucro principal\n"
|
|
459
|
+
secao += "do sistema. Os valores são exibidos apenas como comparação para validação.\n"
|
|
460
|
+
secao += "Diferenças altas indicam necessidade de validação de produtividade, custos\n"
|
|
461
|
+
secao += "ou unidade comercial.\n\n"
|
|
462
|
+
secao += "Status atual: PRICE_APPLY_TO_PROFIT=false (lucro de mercado não afeta\n"
|
|
463
|
+
secao += "otimização)\n"
|
|
464
|
+
|
|
465
|
+
return secao
|
|
466
|
+
|
|
467
|
+
|
|
252
468
|
def gerar_relatorio_markdown(culturas, talhoes, regras, objetivo, cenarios, resultado_ag, validacao, estabilidade):
|
|
253
469
|
"""Gera relatório em formato Markdown"""
|
|
254
470
|
|