agroplan-ai-cli 1.0.24 → 1.0.26

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.24",
3
- "backend_template_version": "1.0.24",
2
+ "cli_version": "1.0.26",
3
+ "backend_template_version": "1.0.26",
4
4
  "zarc_index_version": "2025-2026-fast-index-v2",
5
5
  "price_index_version": "2025-05-reference-v1",
6
6
  "features": [
@@ -11,7 +11,10 @@
11
11
  "hybrid_mode",
12
12
  "report_generator_zarc_support",
13
13
  "price_provider_index_fallback",
14
- "price_display_only"
14
+ "price_display_only",
15
+ "price_unit_normalization",
16
+ "market_profit_estimate",
17
+ "market_profit_validation"
15
18
  ],
16
- "generated_at": "2026-05-09T20:00:00Z"
19
+ "generated_at": "2026-05-09T22:00:00Z"
17
20
  }
@@ -490,6 +490,96 @@ def get_precos_lote(
490
490
  except Exception as e:
491
491
  raise HTTPException(status_code=500, detail=str(e))
492
492
 
493
+ @app.get("/dados/precos/comparar")
494
+ def get_precos_comparar(
495
+ uf: Optional[str] = Query(None, description="Unidade Federativa (ex: SP, PR)")
496
+ ):
497
+ """Compara preços originais com preços normalizados para tonelada"""
498
+ try:
499
+ from providers.price_provider import buscar_precos_lote
500
+ from core.price_normalizer import normalizar_precos_lote, obter_estatisticas_normalizacao
501
+
502
+ # Culturas do AgroPlan
503
+ culturas = ["soja", "milho", "feijao", "trigo", "algodao", "cafe", "cana", "arroz", "sorgo", "mandioca"]
504
+
505
+ # Buscar preços em lote
506
+ precos_data = buscar_precos_lote(culturas=culturas, uf=uf)
507
+
508
+ # Converter para dict por cultura
509
+ precos_dict = {p["cultura"]: p for p in precos_data}
510
+
511
+ # Normalizar preços
512
+ precos_normalizados = normalizar_precos_lote(precos_dict)
513
+
514
+ # Obter estatísticas
515
+ stats = obter_estatisticas_normalizacao(precos_normalizados)
516
+
517
+ return {
518
+ "uf": uf,
519
+ "precos_normalizados": precos_normalizados,
520
+ "estatisticas": stats
521
+ }
522
+
523
+ except Exception as e:
524
+ raise HTTPException(status_code=500, detail=str(e))
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
+
493
583
  @app.get("/dashboard")
494
584
  def get_dashboard(
495
585
  lat: Optional[float] = None,
@@ -0,0 +1,266 @@
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
+ Args:
51
+ item: Item do plano com dados de lucro
52
+
53
+ Returns:
54
+ Dict com confiabilidade (alta/media/baixa) e motivos
55
+ """
56
+ motivos = []
57
+
58
+ # Verificar se tem preço normalizado
59
+ preco_norm = item.get("preco_normalizado", {})
60
+ if not preco_norm.get("normalizado"):
61
+ return {
62
+ "confiabilidade": "baixa",
63
+ "motivos": ["Preço não normalizado ou não disponível"]
64
+ }
65
+
66
+ # Verificar se tem produtividade e custo
67
+ produtividade = item.get("produtividade", 0)
68
+ custo = item.get("custo", 0)
69
+
70
+ if produtividade <= 0:
71
+ motivos.append("Produtividade não disponível ou inválida")
72
+
73
+ if custo <= 0:
74
+ motivos.append("Custo não disponível ou inválido")
75
+
76
+ if motivos:
77
+ return {
78
+ "confiabilidade": "baixa",
79
+ "motivos": motivos
80
+ }
81
+
82
+ # Verificar se tem lucro de mercado calculado
83
+ lucro_mercado = item.get("lucro_mercado_estimado")
84
+ if lucro_mercado is None:
85
+ return {
86
+ "confiabilidade": "baixa",
87
+ "motivos": ["Lucro de mercado não calculado"]
88
+ }
89
+
90
+ # Calcular diferença com lucro do sistema
91
+ lucro_sistema = item.get("lucro_estimado", 0)
92
+ diferenca = calcular_diferenca_lucro(lucro_sistema, lucro_mercado)
93
+ diferenca_percentual = abs(diferenca["diferenca_percentual"])
94
+
95
+ # Classificar baseado na diferença percentual
96
+ if diferenca_percentual > 100:
97
+ confiabilidade = "baixa"
98
+ motivos.append(f"Diferença muito alta ({diferenca_percentual:.1f}%) entre lucro sistema e mercado")
99
+ elif diferenca_percentual > 50:
100
+ confiabilidade = "media"
101
+ motivos.append(f"Diferença moderada ({diferenca_percentual:.1f}%) entre lucro sistema e mercado")
102
+ else:
103
+ confiabilidade = "alta"
104
+ motivos.append(f"Diferença aceitável ({diferenca_percentual:.1f}%) entre lucro sistema e mercado")
105
+
106
+ # Verificar se lucro de mercado é negativo (prejuízo)
107
+ if lucro_mercado < 0:
108
+ if confiabilidade == "alta":
109
+ confiabilidade = "media"
110
+ motivos.append("Lucro de mercado indica prejuízo - requer validação de preço/produtividade")
111
+
112
+ # Verificar se é fallback
113
+ preco_real = item.get("preco_real", {})
114
+ if preco_real.get("fallback"):
115
+ if confiabilidade == "alta":
116
+ confiabilidade = "media"
117
+ motivos.append("Preço usando fallback (referência) - pode não refletir mercado local")
118
+
119
+ return {
120
+ "confiabilidade": confiabilidade,
121
+ "motivos": motivos,
122
+ "diferenca": diferenca
123
+ }
124
+
125
+
126
+ def validar_plano_lucro_mercado(resultado: Dict) -> Dict:
127
+ """
128
+ Valida lucro de mercado para todo o plano e adiciona classificação de confiabilidade.
129
+
130
+ Args:
131
+ resultado: Resultado do AG ou cenário com plano
132
+
133
+ Returns:
134
+ Resultado enriquecido com validação de lucro de mercado
135
+ """
136
+ if "plano" not in resultado:
137
+ return resultado
138
+
139
+ # Contadores
140
+ alta_confiabilidade = 0
141
+ media_confiabilidade = 0
142
+ baixa_confiabilidade = 0
143
+ alertas = []
144
+
145
+ # Validar cada item do plano
146
+ for item in resultado["plano"]:
147
+ # Classificar confiabilidade
148
+ validacao = classificar_confiabilidade_lucro(item)
149
+ item["validacao_lucro_mercado"] = validacao
150
+
151
+ # Contabilizar
152
+ conf = validacao["confiabilidade"]
153
+ if conf == "alta":
154
+ alta_confiabilidade += 1
155
+ elif conf == "media":
156
+ media_confiabilidade += 1
157
+ else:
158
+ baixa_confiabilidade += 1
159
+ # Adicionar alerta para baixa confiabilidade
160
+ cultura = item.get("cultura", "desconhecida")
161
+ talhao = item.get("talhao", "?")
162
+ alertas.append(f"Talhão {talhao} ({cultura}): {', '.join(validacao['motivos'])}")
163
+
164
+ # Adicionar resumo de validação
165
+ total = len(resultado["plano"])
166
+ percentual_alta = (alta_confiabilidade / total * 100) if total > 0 else 0
167
+ percentual_baixa = (baixa_confiabilidade / total * 100) if total > 0 else 0
168
+
169
+ resultado["validacao_lucro_mercado"] = {
170
+ "ativo": True,
171
+ "total_itens": total,
172
+ "itens_alta_confiabilidade": alta_confiabilidade,
173
+ "itens_media_confiabilidade": media_confiabilidade,
174
+ "itens_baixa_confiabilidade": baixa_confiabilidade,
175
+ "percentual_alta_confiabilidade": round(percentual_alta, 1),
176
+ "percentual_baixa_confiabilidade": round(percentual_baixa, 1),
177
+ "alertas": alertas[:5], # Limitar a 5 alertas principais
178
+ "total_alertas": len(alertas),
179
+ "recomendacao": _gerar_recomendacao(percentual_alta, percentual_baixa)
180
+ }
181
+
182
+ return resultado
183
+
184
+
185
+ def _gerar_recomendacao(percentual_alta: float, percentual_baixa: float) -> str:
186
+ """
187
+ Gera recomendação baseada nos percentuais de confiabilidade.
188
+
189
+ Args:
190
+ percentual_alta: Percentual de itens com alta confiabilidade
191
+ percentual_baixa: Percentual de itens com baixa confiabilidade
192
+
193
+ Returns:
194
+ String com recomendação
195
+ """
196
+ if percentual_alta >= 70:
197
+ return "Lucro de mercado apresenta boa confiabilidade. Considere validação detalhada antes de ativar PRICE_APPLY_TO_PROFIT."
198
+ elif percentual_baixa >= 50:
199
+ return "Muitos itens com baixa confiabilidade. Valide preços, produtividades e custos antes de usar lucro de mercado."
200
+ else:
201
+ return "Confiabilidade mista. Revise itens com baixa confiabilidade e valide dados de mercado."
202
+
203
+
204
+ def gerar_diagnostico_lucro_mercado(plano: List[Dict], uf: Optional[str] = None) -> Dict:
205
+ """
206
+ Gera diagnóstico detalhado do lucro de mercado para análise.
207
+
208
+ Args:
209
+ plano: Lista de itens do plano
210
+ uf: Unidade Federativa (opcional)
211
+
212
+ Returns:
213
+ Dict com diagnóstico por cultura
214
+ """
215
+ diagnostico = {
216
+ "uf": uf,
217
+ "total_culturas": 0,
218
+ "culturas": {}
219
+ }
220
+
221
+ # Agrupar por cultura
222
+ culturas_map = {}
223
+ for item in plano:
224
+ cultura = item.get("cultura", "desconhecida")
225
+
226
+ if cultura not in culturas_map:
227
+ culturas_map[cultura] = []
228
+
229
+ culturas_map[cultura].append(item)
230
+
231
+ # Gerar diagnóstico por cultura
232
+ for cultura, itens in culturas_map.items():
233
+ # Calcular médias
234
+ lucro_sistema_total = sum(i.get("lucro_estimado", 0) for i in itens)
235
+ lucro_mercado_total = sum(i.get("lucro_mercado_estimado", 0) for i in itens if i.get("lucro_mercado_estimado") is not None)
236
+
237
+ lucro_sistema_medio = lucro_sistema_total / len(itens) if itens else 0
238
+ lucro_mercado_medio = lucro_mercado_total / len(itens) if itens else 0
239
+
240
+ # Calcular diferença
241
+ diferenca = calcular_diferenca_lucro(lucro_sistema_medio, lucro_mercado_medio)
242
+
243
+ # Obter confiabilidade do primeiro item (representativo)
244
+ validacao = classificar_confiabilidade_lucro(itens[0]) if itens else {"confiabilidade": "baixa", "motivos": []}
245
+
246
+ # Obter preço normalizado
247
+ preco_norm = itens[0].get("preco_normalizado", {}) if itens else {}
248
+ preco_real = itens[0].get("preco_real", {}) if itens else {}
249
+
250
+ diagnostico["culturas"][cultura] = {
251
+ "total_talhoes": len(itens),
252
+ "lucro_sistema_medio": round(lucro_sistema_medio, 2),
253
+ "lucro_mercado_medio": round(lucro_mercado_medio, 2),
254
+ "diferenca": diferenca,
255
+ "confiabilidade": validacao["confiabilidade"],
256
+ "motivos": validacao["motivos"],
257
+ "preco_original": preco_real.get("preco"),
258
+ "unidade_original": preco_real.get("unidade"),
259
+ "preco_por_tonelada": preco_norm.get("preco_por_tonelada"),
260
+ "normalizado": preco_norm.get("normalizado", False),
261
+ "fallback": preco_real.get("fallback", False)
262
+ }
263
+
264
+ diagnostico["total_culturas"] = len(culturas_map)
265
+
266
+ return diagnostico
@@ -1,30 +1,56 @@
1
1
  """
2
2
  Adaptador de Preços Agrícolas
3
- Integra preços reais no planejamento
3
+ Integra preços reais no planejamento com normalização de unidades e validação
4
4
  """
5
5
 
6
6
  import os
7
+ import pandas as pd
7
8
  from typing import Dict, List, Optional
8
9
  from providers.price_provider import buscar_preco, buscar_precos_lote
10
+ from core.price_normalizer import (
11
+ normalizar_preco_para_tonelada,
12
+ calcular_lucro_com_preco_normalizado,
13
+ obter_estatisticas_normalizacao
14
+ )
15
+ from core.market_profit_validator import validar_plano_lucro_mercado
9
16
 
10
17
  # Configuração
11
18
  PRICE_APPLY_TO_PROFIT = os.getenv("PRICE_APPLY_TO_PROFIT", "false").lower() == "true"
12
19
 
20
+ # Cache de dados de culturas
21
+ _culturas_cache = None
13
22
 
14
- def aplicar_precos_no_plano(resultado: Dict, uf: Optional[str] = None) -> Dict:
23
+ def _carregar_culturas():
24
+ """Carrega dados de culturas do CSV (com cache)"""
25
+ global _culturas_cache
26
+ if _culturas_cache is None:
27
+ _culturas_cache = pd.read_csv("data/culturas.csv")
28
+ return _culturas_cache
29
+
30
+
31
+ def aplicar_precos_no_plano(
32
+ resultado: Dict,
33
+ uf: Optional[str] = None,
34
+ aplicar_no_lucro: bool = None
35
+ ) -> Dict:
15
36
  """
16
- Aplica informações de preços no plano
37
+ Aplica informações de preços no plano com normalização de unidades
17
38
 
18
39
  Args:
19
40
  resultado: Resultado do AG ou cenário
20
41
  uf: Unidade Federativa (opcional)
42
+ aplicar_no_lucro: Se True, recalcula lucro com preços. Se None, usa PRICE_APPLY_TO_PROFIT
21
43
 
22
44
  Returns:
23
- Resultado enriquecido com informações de preços
45
+ Resultado enriquecido com informações de preços e normalização
24
46
  """
25
47
  if "plano" not in resultado:
26
48
  return resultado
27
49
 
50
+ # Determinar se deve aplicar no lucro
51
+ if aplicar_no_lucro is None:
52
+ aplicar_no_lucro = PRICE_APPLY_TO_PROFIT
53
+
28
54
  # Coletar culturas únicas
29
55
  culturas = list(set([item.get("cultura") for item in resultado["plano"]]))
30
56
 
@@ -38,6 +64,7 @@ def aplicar_precos_no_plano(resultado: Dict, uf: Optional[str] = None) -> Dict:
38
64
  culturas_com_preco = 0
39
65
  culturas_fallback = 0
40
66
  culturas_sem_preco = 0
67
+ culturas_normalizadas = 0
41
68
 
42
69
  # Aplicar preços no plano
43
70
  for item in resultado["plano"]:
@@ -47,6 +74,66 @@ def aplicar_precos_no_plano(resultado: Dict, uf: Optional[str] = None) -> Dict:
47
74
  # Adicionar informações de preço
48
75
  item["preco_real"] = preco_info
49
76
 
77
+ # Normalizar preço se disponível
78
+ if preco_info.get("ativo") and preco_info.get("preco") and preco_info.get("unidade"):
79
+ normalizacao = normalizar_preco_para_tonelada(
80
+ preco_info["preco"],
81
+ preco_info["unidade"]
82
+ )
83
+
84
+ item["preco_normalizado"] = normalizacao
85
+
86
+ if normalizacao.get("normalizado"):
87
+ culturas_normalizadas += 1
88
+
89
+ # Buscar produtividade e custo da cultura
90
+ culturas_df = _carregar_culturas()
91
+ cultura_data = culturas_df[culturas_df['nome'] == cultura]
92
+
93
+ if not cultura_data.empty:
94
+ produtividade = float(cultura_data.iloc[0]['produtividade']) # toneladas/ha
95
+ custo = float(cultura_data.iloc[0]['custo']) # R$/ha
96
+ area = item.get("area", 0) # ha
97
+
98
+ # Adicionar campos ao item se não existirem
99
+ if "produtividade" not in item:
100
+ item["produtividade"] = produtividade
101
+ if "custo" not in item:
102
+ item["custo"] = custo
103
+
104
+ # Calcular lucro de mercado estimado (sempre, independente de aplicar_no_lucro)
105
+ if produtividade > 0 and area > 0:
106
+ calculo_mercado = calcular_lucro_com_preco_normalizado(
107
+ preco_por_tonelada=normalizacao["preco_por_tonelada"],
108
+ produtividade=produtividade,
109
+ custo_por_hectare=custo,
110
+ area=area
111
+ )
112
+
113
+ item["lucro_mercado_estimado"] = calculo_mercado["lucro_mercado"]
114
+ item["lucro_mercado_detalhes"] = calculo_mercado
115
+
116
+ # Se aplicar_no_lucro, substituir lucro_estimado
117
+ if aplicar_no_lucro:
118
+ item["lucro_original"] = item.get("lucro_estimado", 0)
119
+ item["lucro_estimado"] = calculo_mercado["lucro_mercado"]
120
+ item["lucro_mercado_aplicado"] = True
121
+ else:
122
+ item["lucro_mercado_aplicado"] = False
123
+ else:
124
+ item["lucro_mercado_estimado"] = None
125
+ item["lucro_mercado_aplicado"] = False
126
+ else:
127
+ item["lucro_mercado_estimado"] = None
128
+ item["lucro_mercado_aplicado"] = False
129
+ else:
130
+ item["lucro_mercado_estimado"] = None
131
+ item["lucro_mercado_aplicado"] = False
132
+ else:
133
+ item["preco_normalizado"] = {"normalizado": False}
134
+ item["lucro_mercado_estimado"] = None
135
+ item["lucro_mercado_aplicado"] = False
136
+
50
137
  # Contabilizar estatísticas
51
138
  if preco_info.get("ativo"):
52
139
  if preco_info.get("fallback"):
@@ -55,29 +142,45 @@ def aplicar_precos_no_plano(resultado: Dict, uf: Optional[str] = None) -> Dict:
55
142
  culturas_com_preco += 1
56
143
  else:
57
144
  culturas_sem_preco += 1
58
-
59
- # Se PRICE_APPLY_TO_PROFIT estiver ativo, recalcular lucro
60
- # NOTA: Por enquanto, apenas adiciona informação sem recalcular
61
- # Recálculo será implementado após validação de unidades
145
+
146
+ # Recalcular lucro_total se aplicar_no_lucro
147
+ if aplicar_no_lucro:
148
+ resultado["lucro_total_original"] = resultado.get("lucro_total", 0)
149
+ resultado["lucro_total"] = sum(
150
+ item.get("lucro_estimado", 0) for item in resultado["plano"]
151
+ )
62
152
 
63
153
  # Adicionar resumo de preços ao resultado
64
154
  resultado["precos"] = {
65
155
  "ativo": True,
66
- "source": "price-local-index",
156
+ "source": "price-local-index" if culturas_com_preco > 0 else "price-fallback",
67
157
  "fallback_count": culturas_fallback,
68
158
  "culturas_com_preco": culturas_com_preco,
69
159
  "culturas_sem_preco": culturas_sem_preco,
70
160
  "total_culturas": len(culturas),
71
- "aplicado_no_lucro": PRICE_APPLY_TO_PROFIT,
72
- "uf": uf
161
+ "aplicado_no_lucro": aplicar_no_lucro,
162
+ "lucro_recalculado_disponivel": any(
163
+ item.get("lucro_mercado_estimado") is not None
164
+ for item in resultado["plano"]
165
+ ),
166
+ "uf": uf,
167
+ "normalizacao": {
168
+ "ativa": True,
169
+ "unidade_base": "tonelada",
170
+ "culturas_normalizadas": culturas_normalizadas,
171
+ "culturas_nao_normalizadas": len(culturas) - culturas_normalizadas
172
+ }
73
173
  }
74
174
 
175
+ # Validar lucro de mercado e adicionar classificação de confiabilidade
176
+ resultado = validar_plano_lucro_mercado(resultado)
177
+
75
178
  return resultado
76
179
 
77
180
 
78
181
  def gerar_secao_precos_relatorio(plano: List[Dict], uf: Optional[str], formato: str = "md") -> str:
79
182
  """
80
- Gera seção de preços para o relatório
183
+ Gera seção de preços para o relatório com informações de normalização
81
184
 
82
185
  Args:
83
186
  plano: Lista de itens do plano com informações de preços
@@ -94,8 +197,8 @@ def gerar_secao_precos_relatorio(plano: List[Dict], uf: Optional[str], formato:
94
197
  secao += f"**Região:** {uf}\n\n"
95
198
 
96
199
  secao += "### Preços por Cultura\n\n"
97
- secao += "| Cultura | Preço | Unidade | Fonte | Observação |\n"
98
- secao += "|---------|-------|---------|-------|------------|\n"
200
+ secao += "| Cultura | Preço Original | Unidade | Preço/Tonelada | Normalizado | Fonte |\n"
201
+ secao += "|---------|----------------|---------|----------------|-------------|-------|\n"
99
202
 
100
203
  # Coletar culturas únicas
101
204
  culturas_vistas = set()
@@ -109,27 +212,66 @@ def gerar_secao_precos_relatorio(plano: List[Dict], uf: Optional[str], formato:
109
212
  culturas_vistas.add(cultura)
110
213
 
111
214
  preco_info = item.get("preco_real", {})
215
+ preco_norm = item.get("preco_normalizado", {})
112
216
 
113
217
  if preco_info.get("ativo"):
114
218
  preco = preco_info.get("preco")
115
219
  unidade = preco_info.get("unidade", "N/A")
116
220
  fonte = preco_info.get("source", "N/A")
117
221
  fallback_icon = "⚠️ " if preco_info.get("fallback") else ""
118
- observacao = preco_info.get("observacao", "")
119
222
 
120
223
  preco_fmt = f"R$ {preco:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") if preco else "N/A"
121
224
 
122
- secao += f"| {cultura} | {preco_fmt} | {unidade} | {fallback_icon}{fonte} | {observacao} |\n"
225
+ # Informações de normalização
226
+ if preco_norm.get("normalizado"):
227
+ preco_ton = preco_norm.get("preco_por_tonelada", 0)
228
+ preco_ton_fmt = f"R$ {preco_ton:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
229
+ norm_status = "✅ Sim"
230
+ else:
231
+ preco_ton_fmt = "N/A"
232
+ norm_status = "❌ Não"
233
+
234
+ secao += f"| {cultura} | {preco_fmt} | {unidade} | {preco_ton_fmt} | {norm_status} | {fallback_icon}{fonte} |\n"
123
235
  else:
124
- secao += f"| {cultura} | N/A | N/A | price-unavailable | Preço não disponível |\n"
236
+ secao += f"| {cultura} | N/A | N/A | N/A | Não | price-unavailable |\n"
125
237
 
126
238
  secao += "\n"
239
+
240
+ # Adicionar informações de lucro de mercado se disponível
241
+ tem_lucro_mercado = any(item.get("lucro_mercado_estimado") is not None for item in plano)
242
+
243
+ if tem_lucro_mercado:
244
+ secao += "### Comparação de Lucro\n\n"
245
+ secao += "| Talhão | Cultura | Lucro Sistema | Lucro Mercado | Diferença |\n"
246
+ secao += "|--------|---------|---------------|---------------|----------|\n"
247
+
248
+ for item in plano:
249
+ if item.get("lucro_mercado_estimado") is not None:
250
+ talhao = item.get("talhao", "N/A")
251
+ cultura = item.get("cultura", "").upper()
252
+ lucro_sistema = item.get("lucro_original", item.get("lucro_estimado", 0))
253
+ lucro_mercado = item.get("lucro_mercado_estimado", 0)
254
+ diferenca = lucro_mercado - lucro_sistema
255
+
256
+ lucro_sistema_fmt = f"R$ {lucro_sistema:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
257
+ lucro_mercado_fmt = f"R$ {lucro_mercado:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
258
+ diferenca_fmt = f"R$ {diferenca:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
259
+ diferenca_icon = "📈" if diferenca > 0 else "📉" if diferenca < 0 else "➡️"
260
+
261
+ secao += f"| {talhao} | {cultura} | {lucro_sistema_fmt} | {lucro_mercado_fmt} | {diferenca_icon} {diferenca_fmt} |\n"
262
+
263
+ secao += "\n"
264
+
127
265
  secao += "### Observações\n\n"
128
266
 
129
267
  if PRICE_APPLY_TO_PROFIT:
130
- secao += "✅ **Os preços foram aplicados ao cálculo de lucro estimado.**\n\n"
268
+ secao += "✅ **Os preços de mercado normalizados foram aplicados ao cálculo de lucro.**\n\n"
131
269
  else:
132
- secao += "ℹ️ **Os preços são exibidos como referência, mas o cálculo de lucro ainda utiliza a base interna simulada.**\n\n"
270
+ secao += "ℹ️ **Os preços foram normalizados para R$/tonelada. Nesta versão, o lucro principal ainda usa a base interna do sistema, mas o lucro de mercado estimado é exibido para comparação.**\n\n"
271
+
272
+ secao += "**Normalização de Unidades:**\n"
273
+ secao += "- Todos os preços foram convertidos para R$/tonelada para permitir comparação consistente\n"
274
+ secao += "- Fatores de conversão: saca_60kg (×16.67), saca_50kg (×20), arroba_15kg (×66.67)\n\n"
133
275
 
134
276
  secao += "**Fontes:**\n"
135
277
  secao += "- `price-local-index`: Índice local de preços\n"
@@ -156,32 +298,68 @@ def gerar_secao_precos_relatorio(plano: List[Dict], uf: Optional[str], formato:
156
298
  culturas_vistas.add(cultura)
157
299
 
158
300
  preco_info = item.get("preco_real", {})
301
+ preco_norm = item.get("preco_normalizado", {})
159
302
 
160
303
  if preco_info.get("ativo"):
161
304
  preco = preco_info.get("preco")
162
305
  unidade = preco_info.get("unidade", "N/A")
163
306
  fonte = preco_info.get("source", "N/A")
164
307
  fallback_text = " (fallback)" if preco_info.get("fallback") else ""
165
- observacao = preco_info.get("observacao", "")
166
308
 
167
309
  preco_fmt = f"R$ {preco:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") if preco else "N/A"
168
310
 
169
311
  secao += f" {cultura}:\n"
170
- secao += f" Preço: {preco_fmt}\n"
312
+ secao += f" Preço Original: {preco_fmt}\n"
171
313
  secao += f" Unidade: {unidade}\n"
172
- secao += f" Fonte: {fonte}{fallback_text}\n"
173
- secao += f" Observação: {observacao}\n\n"
314
+
315
+ if preco_norm.get("normalizado"):
316
+ preco_ton = preco_norm.get("preco_por_tonelada", 0)
317
+ preco_ton_fmt = f"R$ {preco_ton:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
318
+ secao += f" Preço/Tonelada: {preco_ton_fmt}\n"
319
+ secao += f" Normalizado: Sim\n"
320
+ else:
321
+ secao += f" Normalizado: Não\n"
322
+
323
+ secao += f" Fonte: {fonte}{fallback_text}\n\n"
174
324
  else:
175
325
  secao += f" {cultura}:\n"
176
326
  secao += f" Preço: N/A\n"
327
+ secao += f" Normalizado: Não\n"
177
328
  secao += f" Observação: Preço não disponível\n\n"
178
329
 
330
+ # Adicionar comparação de lucro se disponível
331
+ tem_lucro_mercado = any(item.get("lucro_mercado_estimado") is not None for item in plano)
332
+
333
+ if tem_lucro_mercado:
334
+ secao += "Comparação de Lucro:\n\n"
335
+
336
+ for item in plano:
337
+ if item.get("lucro_mercado_estimado") is not None:
338
+ talhao = item.get("talhao", "N/A")
339
+ cultura = item.get("cultura", "").upper()
340
+ lucro_sistema = item.get("lucro_original", item.get("lucro_estimado", 0))
341
+ lucro_mercado = item.get("lucro_mercado_estimado", 0)
342
+ diferenca = lucro_mercado - lucro_sistema
343
+
344
+ lucro_sistema_fmt = f"R$ {lucro_sistema:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
345
+ lucro_mercado_fmt = f"R$ {lucro_mercado:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
346
+ diferenca_fmt = f"R$ {diferenca:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
347
+
348
+ secao += f" Talhão {talhao} ({cultura}):\n"
349
+ secao += f" Lucro Sistema: {lucro_sistema_fmt}\n"
350
+ secao += f" Lucro Mercado: {lucro_mercado_fmt}\n"
351
+ secao += f" Diferença: {diferenca_fmt}\n\n"
352
+
179
353
  secao += "Observações:\n\n"
180
354
 
181
355
  if PRICE_APPLY_TO_PROFIT:
182
- secao += "Os preços foram aplicados ao cálculo de lucro estimado.\n\n"
356
+ secao += "Os preços de mercado normalizados foram aplicados ao cálculo de lucro.\n\n"
183
357
  else:
184
- secao += "Os preços são exibidos como referência, mas o cálculo de lucro ainda utiliza a base interna simulada.\n\n"
358
+ secao += "Os preços foram normalizados para R$/tonelada. Nesta versão, o lucro principal ainda usa a base interna do sistema, mas o lucro de mercado estimado é exibido para comparação.\n\n"
359
+
360
+ secao += "Normalização de Unidades:\n"
361
+ secao += "- Todos os preços foram convertidos para R$/tonelada\n"
362
+ secao += "- Fatores: saca_60kg (x16.67), saca_50kg (x20), arroba_15kg (x66.67)\n\n"
185
363
 
186
364
  secao += "Fontes:\n"
187
365
  secao += "- price-local-index: Índice local de preços\n"
@@ -0,0 +1,182 @@
1
+ """
2
+ Normalizador de Unidades de Preços Agrícolas
3
+
4
+ Converte preços de diferentes unidades para uma base comum (R$/tonelada)
5
+ para permitir comparação e cálculo consistente de lucro.
6
+ """
7
+
8
+ from typing import Dict, Optional
9
+
10
+
11
+ # Fatores de conversão para tonelada
12
+ CONVERSAO_UNIDADES = {
13
+ "tonelada": 1.0,
14
+ "saca_60kg": 1000.0 / 60.0, # 16.6667 sacas por tonelada
15
+ "saca_50kg": 1000.0 / 50.0, # 20 sacas por tonelada
16
+ "arroba_15kg": 1000.0 / 15.0, # 66.6667 arrobas por tonelada
17
+ }
18
+
19
+
20
+ def normalizar_preco_para_tonelada(preco: float, unidade: str) -> Dict:
21
+ """
22
+ Converte preço de qualquer unidade para R$/tonelada.
23
+
24
+ Args:
25
+ preco: Preço na unidade original
26
+ unidade: Unidade do preço (tonelada, saca_60kg, saca_50kg, arroba_15kg)
27
+
28
+ Returns:
29
+ Dict com informações de normalização:
30
+ - preco_original: Preço original
31
+ - unidade_original: Unidade original
32
+ - preco_por_tonelada: Preço convertido para R$/tonelada
33
+ - unidade_normalizada: "tonelada"
34
+ - fator_conversao: Fator usado na conversão
35
+ - normalizado: True se conversão foi bem-sucedida
36
+ - error: Mensagem de erro se conversão falhou
37
+ """
38
+
39
+ # Normalizar nome da unidade (remover espaços, lowercase)
40
+ unidade_normalizada = unidade.lower().strip().replace(" ", "_")
41
+
42
+ # Verificar se unidade é suportada
43
+ if unidade_normalizada not in CONVERSAO_UNIDADES:
44
+ return {
45
+ "preco_original": preco,
46
+ "unidade_original": unidade,
47
+ "normalizado": False,
48
+ "error": f"Unidade '{unidade}' não suportada. Unidades válidas: {', '.join(CONVERSAO_UNIDADES.keys())}"
49
+ }
50
+
51
+ # Obter fator de conversão
52
+ fator = CONVERSAO_UNIDADES[unidade_normalizada]
53
+
54
+ # Calcular preço por tonelada
55
+ preco_por_tonelada = preco * fator
56
+
57
+ return {
58
+ "preco_original": preco,
59
+ "unidade_original": unidade,
60
+ "preco_por_tonelada": round(preco_por_tonelada, 2),
61
+ "unidade_normalizada": "tonelada",
62
+ "fator_conversao": round(fator, 4),
63
+ "normalizado": True
64
+ }
65
+
66
+
67
+ def normalizar_precos_lote(precos: Dict[str, Dict]) -> Dict[str, Dict]:
68
+ """
69
+ Normaliza preços de múltiplas culturas em lote.
70
+
71
+ Args:
72
+ precos: Dict com culturas como chave e dados de preço como valor
73
+
74
+ Returns:
75
+ Dict com culturas como chave e dados normalizados como valor
76
+ """
77
+ resultado = {}
78
+
79
+ for cultura, dados_preco in precos.items():
80
+ if not dados_preco.get("ativo", False):
81
+ resultado[cultura] = {
82
+ **dados_preco,
83
+ "normalizado": False,
84
+ "error": "Preço não disponível"
85
+ }
86
+ continue
87
+
88
+ preco = dados_preco.get("preco")
89
+ unidade = dados_preco.get("unidade")
90
+
91
+ if preco is None or unidade is None:
92
+ resultado[cultura] = {
93
+ **dados_preco,
94
+ "normalizado": False,
95
+ "error": "Dados de preço incompletos"
96
+ }
97
+ continue
98
+
99
+ # Normalizar preço
100
+ normalizacao = normalizar_preco_para_tonelada(preco, unidade)
101
+
102
+ # Combinar dados originais com normalização
103
+ resultado[cultura] = {
104
+ **dados_preco,
105
+ **normalizacao
106
+ }
107
+
108
+ return resultado
109
+
110
+
111
+ def calcular_lucro_com_preco_normalizado(
112
+ preco_por_tonelada: float,
113
+ produtividade: float, # toneladas por hectare
114
+ custo_por_hectare: float,
115
+ area: float
116
+ ) -> Dict:
117
+ """
118
+ Calcula lucro usando preço normalizado de mercado.
119
+
120
+ Args:
121
+ preco_por_tonelada: Preço normalizado em R$/tonelada
122
+ produtividade: Produtividade em toneladas por hectare
123
+ custo_por_hectare: Custo de produção por hectare
124
+ area: Área em hectares
125
+
126
+ Returns:
127
+ Dict com cálculos detalhados:
128
+ - receita_total: Receita bruta
129
+ - custo_total: Custo total de produção
130
+ - lucro_mercado: Lucro líquido com preço de mercado
131
+ - lucro_por_hectare: Lucro por hectare
132
+ """
133
+
134
+ # Calcular receita
135
+ producao_total = produtividade * area # toneladas
136
+ receita_total = preco_por_tonelada * producao_total
137
+
138
+ # Calcular custo
139
+ custo_total = custo_por_hectare * area
140
+
141
+ # Calcular lucro
142
+ lucro_mercado = receita_total - custo_total
143
+ lucro_por_hectare = lucro_mercado / area if area > 0 else 0
144
+
145
+ return {
146
+ "receita_total": round(receita_total, 2),
147
+ "custo_total": round(custo_total, 2),
148
+ "lucro_mercado": round(lucro_mercado, 2),
149
+ "lucro_por_hectare": round(lucro_por_hectare, 2),
150
+ "producao_total_toneladas": round(producao_total, 2)
151
+ }
152
+
153
+
154
+ def obter_estatisticas_normalizacao(precos_normalizados: Dict[str, Dict]) -> Dict:
155
+ """
156
+ Gera estatísticas sobre normalização de preços.
157
+
158
+ Args:
159
+ precos_normalizados: Dict com preços normalizados
160
+
161
+ Returns:
162
+ Dict com estatísticas de normalização
163
+ """
164
+ total = len(precos_normalizados)
165
+ normalizados = sum(1 for p in precos_normalizados.values() if p.get("normalizado", False))
166
+ nao_normalizados = total - normalizados
167
+
168
+ # Agrupar por unidade original
169
+ unidades = {}
170
+ for cultura, dados in precos_normalizados.items():
171
+ if dados.get("normalizado", False):
172
+ unidade = dados.get("unidade_original", "desconhecida")
173
+ unidades[unidade] = unidades.get(unidade, 0) + 1
174
+
175
+ return {
176
+ "ativa": True,
177
+ "unidade_base": "tonelada",
178
+ "total_culturas": total,
179
+ "culturas_normalizadas": normalizados,
180
+ "culturas_nao_normalizadas": nao_normalizados,
181
+ "unidades_originais": unidades
182
+ }
@@ -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,193 @@ 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
+
283
+ perc_alta = validacao.get("percentual_alta_confiabilidade", 0)
284
+ perc_baixa = validacao.get("percentual_baixa_confiabilidade", 0)
285
+
286
+ secao += f"- **Total de itens analisados**: {total}\n"
287
+ secao += f"- **Alta confiabilidade**: {alta} ({perc_alta:.1f}%) 🟢\n"
288
+ secao += f"- **Média confiabilidade**: {media} ({100 - perc_alta - perc_baixa:.1f}%) 🟡\n"
289
+ secao += f"- **Baixa confiabilidade**: {baixa} ({perc_baixa:.1f}%) 🔴\n\n"
290
+
291
+ # Recomendação
292
+ recomendacao = validacao.get("recomendacao", "")
293
+ if recomendacao:
294
+ secao += f"**Recomendação**: {recomendacao}\n\n"
295
+
296
+ # Alertas
297
+ alertas = validacao.get("alertas", [])
298
+ if alertas:
299
+ secao += "### ⚠️ Alertas\n\n"
300
+ for alerta in alertas:
301
+ secao += f"- {alerta}\n"
302
+ secao += "\n"
303
+
304
+ total_alertas = validacao.get("total_alertas", 0)
305
+ if total_alertas > len(alertas):
306
+ secao += f"*Mostrando {len(alertas)} de {total_alertas} alertas*\n\n"
307
+
308
+ # Detalhes por item
309
+ secao += "### Detalhes por Talhão\n\n"
310
+ secao += "| Talhão | Cultura | Lucro Sistema | Lucro Mercado | Diferença % | Confiabilidade |\n"
311
+ secao += "|--------|---------|---------------|---------------|-------------|----------------|\n"
312
+
313
+ for item in resultado["plano"]:
314
+ validacao_item = item.get("validacao_lucro_mercado", {})
315
+ if not validacao_item:
316
+ continue
317
+
318
+ talhao = item.get("talhao", "N/A")
319
+ cultura = item.get("cultura", "").upper()
320
+ lucro_sistema = item.get("lucro_estimado", 0)
321
+ lucro_mercado = item.get("lucro_mercado_estimado", 0)
322
+
323
+ lucro_sistema_fmt = f"R$ {lucro_sistema:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
324
+
325
+ if lucro_mercado is not None:
326
+ lucro_mercado_fmt = f"R$ {lucro_mercado:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
327
+ diferenca = validacao_item.get("diferenca", {})
328
+ diferenca_perc = diferenca.get("diferenca_percentual", 0)
329
+ diferenca_fmt = f"{diferenca_perc:.1f}%"
330
+ else:
331
+ lucro_mercado_fmt = "N/A"
332
+ diferenca_fmt = "N/A"
333
+
334
+ confiabilidade = validacao_item.get("confiabilidade", "baixa")
335
+ conf_emoji = "🟢" if confiabilidade == "alta" else "🟡" if confiabilidade == "media" else "🔴"
336
+ conf_label = confiabilidade.title()
337
+
338
+ secao += f"| {talhao} | {cultura} | {lucro_sistema_fmt} | {lucro_mercado_fmt} | {diferenca_fmt} | {conf_emoji} {conf_label} |\n"
339
+
340
+ secao += "\n"
341
+
342
+ # Explicação
343
+ secao += "### 📊 Sobre a Classificação de Confiabilidade\n\n"
344
+ secao += "A confiabilidade do lucro de mercado é classificada com base em:\n\n"
345
+ secao += "- **Alta (🟢)**: Diferença < 50% entre lucro sistema e mercado, preço normalizado disponível\n"
346
+ secao += "- **Média (🟡)**: Diferença 50-100%, uso de fallback, ou lucro negativo\n"
347
+ secao += "- **Baixa (🔴)**: Diferença > 100%, dados incompletos, ou preço não disponível\n\n"
348
+
349
+ secao += "### ⚠️ Aviso Importante\n\n"
350
+ secao += "**O lucro de mercado ainda é experimental e não substitui o lucro principal do sistema.**\n\n"
351
+ secao += "Os valores são exibidos apenas como comparação para validação. Diferenças altas indicam "
352
+ secao += "necessidade de validação de produtividade, custos ou unidade comercial.\n\n"
353
+ secao += "**Status atual**: `PRICE_APPLY_TO_PROFIT=false` (lucro de mercado não afeta otimização)\n"
354
+
355
+ else: # txt
356
+ secao = "VALIDAÇÃO DO LUCRO DE MERCADO\n\n"
357
+
358
+ secao += "Resumo de Confiabilidade:\n\n"
359
+
360
+ total = validacao.get("total_itens", 0)
361
+ alta = validacao.get("itens_alta_confiabilidade", 0)
362
+ media = validacao.get("itens_media_confiabilidade", 0)
363
+ baixa = validacao.get("itens_baixa_confiabilidade", 0)
364
+
365
+ perc_alta = validacao.get("percentual_alta_confiabilidade", 0)
366
+ perc_baixa = validacao.get("percentual_baixa_confiabilidade", 0)
367
+
368
+ secao += f" Total de itens analisados: {total}\n"
369
+ secao += f" Alta confiabilidade: {alta} ({perc_alta:.1f}%)\n"
370
+ secao += f" Média confiabilidade: {media} ({100 - perc_alta - perc_baixa:.1f}%)\n"
371
+ secao += f" Baixa confiabilidade: {baixa} ({perc_baixa:.1f}%)\n\n"
372
+
373
+ # Recomendação
374
+ recomendacao = validacao.get("recomendacao", "")
375
+ if recomendacao:
376
+ secao += f"Recomendação: {recomendacao}\n\n"
377
+
378
+ # Alertas
379
+ alertas = validacao.get("alertas", [])
380
+ if alertas:
381
+ secao += "Alertas:\n\n"
382
+ for alerta in alertas:
383
+ secao += f" - {alerta}\n"
384
+ secao += "\n"
385
+
386
+ total_alertas = validacao.get("total_alertas", 0)
387
+ if total_alertas > len(alertas):
388
+ secao += f" (Mostrando {len(alertas)} de {total_alertas} alertas)\n\n"
389
+
390
+ # Detalhes por item
391
+ secao += "Detalhes por Talhão:\n\n"
392
+
393
+ for item in resultado["plano"]:
394
+ validacao_item = item.get("validacao_lucro_mercado", {})
395
+ if not validacao_item:
396
+ continue
397
+
398
+ talhao = item.get("talhao", "N/A")
399
+ cultura = item.get("cultura", "").upper()
400
+ lucro_sistema = item.get("lucro_estimado", 0)
401
+ lucro_mercado = item.get("lucro_mercado_estimado", 0)
402
+
403
+ lucro_sistema_fmt = f"R$ {lucro_sistema:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
404
+
405
+ secao += f" Talhão {talhao} ({cultura}):\n"
406
+ secao += f" Lucro Sistema: {lucro_sistema_fmt}\n"
407
+
408
+ if lucro_mercado is not None:
409
+ lucro_mercado_fmt = f"R$ {lucro_mercado:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
410
+ diferenca = validacao_item.get("diferenca", {})
411
+ diferenca_perc = diferenca.get("diferenca_percentual", 0)
412
+ secao += f" Lucro Mercado: {lucro_mercado_fmt}\n"
413
+ secao += f" Diferença: {diferenca_perc:.1f}%\n"
414
+ else:
415
+ secao += f" Lucro Mercado: N/A\n"
416
+
417
+ confiabilidade = validacao_item.get("confiabilidade", "baixa")
418
+ secao += f" Confiabilidade: {confiabilidade.title()}\n"
419
+
420
+ motivos = validacao_item.get("motivos", [])
421
+ if motivos:
422
+ secao += f" Motivos: {', '.join(motivos)}\n"
423
+
424
+ secao += "\n"
425
+
426
+ # Explicação
427
+ secao += "Sobre a Classificação de Confiabilidade:\n\n"
428
+ secao += "A confiabilidade do lucro de mercado é classificada com base em:\n\n"
429
+ secao += " - Alta: Diferença < 50% entre lucro sistema e mercado\n"
430
+ secao += " - Média: Diferença 50-100%, uso de fallback, ou lucro negativo\n"
431
+ secao += " - Baixa: Diferença > 100%, dados incompletos, ou preço não disponível\n\n"
432
+
433
+ secao += "Aviso Importante:\n\n"
434
+ secao += "O lucro de mercado ainda é experimental e não substitui o lucro principal\n"
435
+ secao += "do sistema. Os valores são exibidos apenas como comparação para validação.\n"
436
+ secao += "Diferenças altas indicam necessidade de validação de produtividade, custos\n"
437
+ secao += "ou unidade comercial.\n\n"
438
+ secao += "Status atual: PRICE_APPLY_TO_PROFIT=false (lucro de mercado não afeta\n"
439
+ secao += "otimização)\n"
440
+
441
+ return secao
442
+
443
+
252
444
  def gerar_relatorio_markdown(culturas, talhoes, regras, objetivo, cenarios, resultado_ag, validacao, estabilidade):
253
445
  """Gera relatório em formato Markdown"""
254
446
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroplan-ai-cli",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "CLI global para AgroPlan AI - modo local acelerado",
5
5
  "type": "module",
6
6
  "bin": {