agroplan-ai-cli 1.0.24 → 1.0.25

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.25",
3
+ "backend_template_version": "1.0.25",
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,9 @@
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"
15
17
  ],
16
- "generated_at": "2026-05-09T20:00:00Z"
18
+ "generated_at": "2026-05-09T21:00:00Z"
17
19
  }
@@ -490,6 +490,39 @@ 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
+
493
526
  @app.get("/dashboard")
494
527
  def get_dashboard(
495
528
  lat: Optional[float] = None,
@@ -1,30 +1,55 @@
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
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
+ )
9
15
 
10
16
  # Configuração
11
17
  PRICE_APPLY_TO_PROFIT = os.getenv("PRICE_APPLY_TO_PROFIT", "false").lower() == "true"
12
18
 
19
+ # Cache de dados de culturas
20
+ _culturas_cache = None
13
21
 
14
- def aplicar_precos_no_plano(resultado: Dict, uf: Optional[str] = None) -> Dict:
22
+ def _carregar_culturas():
23
+ """Carrega dados de culturas do CSV (com cache)"""
24
+ global _culturas_cache
25
+ if _culturas_cache is None:
26
+ _culturas_cache = pd.read_csv("data/culturas.csv")
27
+ return _culturas_cache
28
+
29
+
30
+ def aplicar_precos_no_plano(
31
+ resultado: Dict,
32
+ uf: Optional[str] = None,
33
+ aplicar_no_lucro: bool = None
34
+ ) -> Dict:
15
35
  """
16
- Aplica informações de preços no plano
36
+ Aplica informações de preços no plano com normalização de unidades
17
37
 
18
38
  Args:
19
39
  resultado: Resultado do AG ou cenário
20
40
  uf: Unidade Federativa (opcional)
41
+ aplicar_no_lucro: Se True, recalcula lucro com preços. Se None, usa PRICE_APPLY_TO_PROFIT
21
42
 
22
43
  Returns:
23
- Resultado enriquecido com informações de preços
44
+ Resultado enriquecido com informações de preços e normalização
24
45
  """
25
46
  if "plano" not in resultado:
26
47
  return resultado
27
48
 
49
+ # Determinar se deve aplicar no lucro
50
+ if aplicar_no_lucro is None:
51
+ aplicar_no_lucro = PRICE_APPLY_TO_PROFIT
52
+
28
53
  # Coletar culturas únicas
29
54
  culturas = list(set([item.get("cultura") for item in resultado["plano"]]))
30
55
 
@@ -38,6 +63,7 @@ def aplicar_precos_no_plano(resultado: Dict, uf: Optional[str] = None) -> Dict:
38
63
  culturas_com_preco = 0
39
64
  culturas_fallback = 0
40
65
  culturas_sem_preco = 0
66
+ culturas_normalizadas = 0
41
67
 
42
68
  # Aplicar preços no plano
43
69
  for item in resultado["plano"]:
@@ -47,6 +73,66 @@ def aplicar_precos_no_plano(resultado: Dict, uf: Optional[str] = None) -> Dict:
47
73
  # Adicionar informações de preço
48
74
  item["preco_real"] = preco_info
49
75
 
76
+ # Normalizar preço se disponível
77
+ if preco_info.get("ativo") and preco_info.get("preco") and preco_info.get("unidade"):
78
+ normalizacao = normalizar_preco_para_tonelada(
79
+ preco_info["preco"],
80
+ preco_info["unidade"]
81
+ )
82
+
83
+ item["preco_normalizado"] = normalizacao
84
+
85
+ if normalizacao.get("normalizado"):
86
+ culturas_normalizadas += 1
87
+
88
+ # Buscar produtividade e custo da cultura
89
+ culturas_df = _carregar_culturas()
90
+ cultura_data = culturas_df[culturas_df['nome'] == cultura]
91
+
92
+ if not cultura_data.empty:
93
+ produtividade = float(cultura_data.iloc[0]['produtividade']) # toneladas/ha
94
+ custo = float(cultura_data.iloc[0]['custo']) # R$/ha
95
+ area = item.get("area", 0) # ha
96
+
97
+ # Adicionar campos ao item se não existirem
98
+ if "produtividade" not in item:
99
+ item["produtividade"] = produtividade
100
+ if "custo" not in item:
101
+ item["custo"] = custo
102
+
103
+ # Calcular lucro de mercado estimado (sempre, independente de aplicar_no_lucro)
104
+ if produtividade > 0 and area > 0:
105
+ calculo_mercado = calcular_lucro_com_preco_normalizado(
106
+ preco_por_tonelada=normalizacao["preco_por_tonelada"],
107
+ produtividade=produtividade,
108
+ custo_por_hectare=custo,
109
+ area=area
110
+ )
111
+
112
+ item["lucro_mercado_estimado"] = calculo_mercado["lucro_mercado"]
113
+ item["lucro_mercado_detalhes"] = calculo_mercado
114
+
115
+ # Se aplicar_no_lucro, substituir lucro_estimado
116
+ if aplicar_no_lucro:
117
+ item["lucro_original"] = item.get("lucro_estimado", 0)
118
+ item["lucro_estimado"] = calculo_mercado["lucro_mercado"]
119
+ item["lucro_mercado_aplicado"] = True
120
+ else:
121
+ item["lucro_mercado_aplicado"] = False
122
+ else:
123
+ item["lucro_mercado_estimado"] = None
124
+ item["lucro_mercado_aplicado"] = False
125
+ else:
126
+ item["lucro_mercado_estimado"] = None
127
+ item["lucro_mercado_aplicado"] = False
128
+ else:
129
+ item["lucro_mercado_estimado"] = None
130
+ item["lucro_mercado_aplicado"] = False
131
+ else:
132
+ item["preco_normalizado"] = {"normalizado": False}
133
+ item["lucro_mercado_estimado"] = None
134
+ item["lucro_mercado_aplicado"] = False
135
+
50
136
  # Contabilizar estatísticas
51
137
  if preco_info.get("ativo"):
52
138
  if preco_info.get("fallback"):
@@ -55,21 +141,34 @@ def aplicar_precos_no_plano(resultado: Dict, uf: Optional[str] = None) -> Dict:
55
141
  culturas_com_preco += 1
56
142
  else:
57
143
  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
144
+
145
+ # Recalcular lucro_total se aplicar_no_lucro
146
+ if aplicar_no_lucro:
147
+ resultado["lucro_total_original"] = resultado.get("lucro_total", 0)
148
+ resultado["lucro_total"] = sum(
149
+ item.get("lucro_estimado", 0) for item in resultado["plano"]
150
+ )
62
151
 
63
152
  # Adicionar resumo de preços ao resultado
64
153
  resultado["precos"] = {
65
154
  "ativo": True,
66
- "source": "price-local-index",
155
+ "source": "price-local-index" if culturas_com_preco > 0 else "price-fallback",
67
156
  "fallback_count": culturas_fallback,
68
157
  "culturas_com_preco": culturas_com_preco,
69
158
  "culturas_sem_preco": culturas_sem_preco,
70
159
  "total_culturas": len(culturas),
71
- "aplicado_no_lucro": PRICE_APPLY_TO_PROFIT,
72
- "uf": uf
160
+ "aplicado_no_lucro": aplicar_no_lucro,
161
+ "lucro_recalculado_disponivel": any(
162
+ item.get("lucro_mercado_estimado") is not None
163
+ for item in resultado["plano"]
164
+ ),
165
+ "uf": uf,
166
+ "normalizacao": {
167
+ "ativa": True,
168
+ "unidade_base": "tonelada",
169
+ "culturas_normalizadas": culturas_normalizadas,
170
+ "culturas_nao_normalizadas": len(culturas) - culturas_normalizadas
171
+ }
73
172
  }
74
173
 
75
174
  return resultado
@@ -77,7 +176,7 @@ def aplicar_precos_no_plano(resultado: Dict, uf: Optional[str] = None) -> Dict:
77
176
 
78
177
  def gerar_secao_precos_relatorio(plano: List[Dict], uf: Optional[str], formato: str = "md") -> str:
79
178
  """
80
- Gera seção de preços para o relatório
179
+ Gera seção de preços para o relatório com informações de normalização
81
180
 
82
181
  Args:
83
182
  plano: Lista de itens do plano com informações de preços
@@ -94,8 +193,8 @@ def gerar_secao_precos_relatorio(plano: List[Dict], uf: Optional[str], formato:
94
193
  secao += f"**Região:** {uf}\n\n"
95
194
 
96
195
  secao += "### Preços por Cultura\n\n"
97
- secao += "| Cultura | Preço | Unidade | Fonte | Observação |\n"
98
- secao += "|---------|-------|---------|-------|------------|\n"
196
+ secao += "| Cultura | Preço Original | Unidade | Preço/Tonelada | Normalizado | Fonte |\n"
197
+ secao += "|---------|----------------|---------|----------------|-------------|-------|\n"
99
198
 
100
199
  # Coletar culturas únicas
101
200
  culturas_vistas = set()
@@ -109,27 +208,66 @@ def gerar_secao_precos_relatorio(plano: List[Dict], uf: Optional[str], formato:
109
208
  culturas_vistas.add(cultura)
110
209
 
111
210
  preco_info = item.get("preco_real", {})
211
+ preco_norm = item.get("preco_normalizado", {})
112
212
 
113
213
  if preco_info.get("ativo"):
114
214
  preco = preco_info.get("preco")
115
215
  unidade = preco_info.get("unidade", "N/A")
116
216
  fonte = preco_info.get("source", "N/A")
117
217
  fallback_icon = "⚠️ " if preco_info.get("fallback") else ""
118
- observacao = preco_info.get("observacao", "")
119
218
 
120
219
  preco_fmt = f"R$ {preco:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") if preco else "N/A"
121
220
 
122
- secao += f"| {cultura} | {preco_fmt} | {unidade} | {fallback_icon}{fonte} | {observacao} |\n"
221
+ # Informações de normalização
222
+ if preco_norm.get("normalizado"):
223
+ preco_ton = preco_norm.get("preco_por_tonelada", 0)
224
+ preco_ton_fmt = f"R$ {preco_ton:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
225
+ norm_status = "✅ Sim"
226
+ else:
227
+ preco_ton_fmt = "N/A"
228
+ norm_status = "❌ Não"
229
+
230
+ secao += f"| {cultura} | {preco_fmt} | {unidade} | {preco_ton_fmt} | {norm_status} | {fallback_icon}{fonte} |\n"
123
231
  else:
124
- secao += f"| {cultura} | N/A | N/A | price-unavailable | Preço não disponível |\n"
232
+ secao += f"| {cultura} | N/A | N/A | N/A | Não | price-unavailable |\n"
125
233
 
126
234
  secao += "\n"
235
+
236
+ # Adicionar informações de lucro de mercado se disponível
237
+ tem_lucro_mercado = any(item.get("lucro_mercado_estimado") is not None for item in plano)
238
+
239
+ if tem_lucro_mercado:
240
+ secao += "### Comparação de Lucro\n\n"
241
+ secao += "| Talhão | Cultura | Lucro Sistema | Lucro Mercado | Diferença |\n"
242
+ secao += "|--------|---------|---------------|---------------|----------|\n"
243
+
244
+ for item in plano:
245
+ if item.get("lucro_mercado_estimado") is not None:
246
+ talhao = item.get("talhao", "N/A")
247
+ cultura = item.get("cultura", "").upper()
248
+ lucro_sistema = item.get("lucro_original", item.get("lucro_estimado", 0))
249
+ lucro_mercado = item.get("lucro_mercado_estimado", 0)
250
+ diferenca = lucro_mercado - lucro_sistema
251
+
252
+ lucro_sistema_fmt = f"R$ {lucro_sistema:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
253
+ lucro_mercado_fmt = f"R$ {lucro_mercado:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
254
+ diferenca_fmt = f"R$ {diferenca:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
255
+ diferenca_icon = "📈" if diferenca > 0 else "📉" if diferenca < 0 else "➡️"
256
+
257
+ secao += f"| {talhao} | {cultura} | {lucro_sistema_fmt} | {lucro_mercado_fmt} | {diferenca_icon} {diferenca_fmt} |\n"
258
+
259
+ secao += "\n"
260
+
127
261
  secao += "### Observações\n\n"
128
262
 
129
263
  if PRICE_APPLY_TO_PROFIT:
130
- secao += "✅ **Os preços foram aplicados ao cálculo de lucro estimado.**\n\n"
264
+ secao += "✅ **Os preços de mercado normalizados foram aplicados ao cálculo de lucro.**\n\n"
131
265
  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"
266
+ 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"
267
+
268
+ secao += "**Normalização de Unidades:**\n"
269
+ secao += "- Todos os preços foram convertidos para R$/tonelada para permitir comparação consistente\n"
270
+ secao += "- Fatores de conversão: saca_60kg (×16.67), saca_50kg (×20), arroba_15kg (×66.67)\n\n"
133
271
 
134
272
  secao += "**Fontes:**\n"
135
273
  secao += "- `price-local-index`: Índice local de preços\n"
@@ -156,32 +294,68 @@ def gerar_secao_precos_relatorio(plano: List[Dict], uf: Optional[str], formato:
156
294
  culturas_vistas.add(cultura)
157
295
 
158
296
  preco_info = item.get("preco_real", {})
297
+ preco_norm = item.get("preco_normalizado", {})
159
298
 
160
299
  if preco_info.get("ativo"):
161
300
  preco = preco_info.get("preco")
162
301
  unidade = preco_info.get("unidade", "N/A")
163
302
  fonte = preco_info.get("source", "N/A")
164
303
  fallback_text = " (fallback)" if preco_info.get("fallback") else ""
165
- observacao = preco_info.get("observacao", "")
166
304
 
167
305
  preco_fmt = f"R$ {preco:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") if preco else "N/A"
168
306
 
169
307
  secao += f" {cultura}:\n"
170
- secao += f" Preço: {preco_fmt}\n"
308
+ secao += f" Preço Original: {preco_fmt}\n"
171
309
  secao += f" Unidade: {unidade}\n"
172
- secao += f" Fonte: {fonte}{fallback_text}\n"
173
- secao += f" Observação: {observacao}\n\n"
310
+
311
+ if preco_norm.get("normalizado"):
312
+ preco_ton = preco_norm.get("preco_por_tonelada", 0)
313
+ preco_ton_fmt = f"R$ {preco_ton:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
314
+ secao += f" Preço/Tonelada: {preco_ton_fmt}\n"
315
+ secao += f" Normalizado: Sim\n"
316
+ else:
317
+ secao += f" Normalizado: Não\n"
318
+
319
+ secao += f" Fonte: {fonte}{fallback_text}\n\n"
174
320
  else:
175
321
  secao += f" {cultura}:\n"
176
322
  secao += f" Preço: N/A\n"
323
+ secao += f" Normalizado: Não\n"
177
324
  secao += f" Observação: Preço não disponível\n\n"
178
325
 
326
+ # Adicionar comparação de lucro se disponível
327
+ tem_lucro_mercado = any(item.get("lucro_mercado_estimado") is not None for item in plano)
328
+
329
+ if tem_lucro_mercado:
330
+ secao += "Comparação de Lucro:\n\n"
331
+
332
+ for item in plano:
333
+ if item.get("lucro_mercado_estimado") is not None:
334
+ talhao = item.get("talhao", "N/A")
335
+ cultura = item.get("cultura", "").upper()
336
+ lucro_sistema = item.get("lucro_original", item.get("lucro_estimado", 0))
337
+ lucro_mercado = item.get("lucro_mercado_estimado", 0)
338
+ diferenca = lucro_mercado - lucro_sistema
339
+
340
+ lucro_sistema_fmt = f"R$ {lucro_sistema:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
341
+ lucro_mercado_fmt = f"R$ {lucro_mercado:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
342
+ diferenca_fmt = f"R$ {diferenca:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
343
+
344
+ secao += f" Talhão {talhao} ({cultura}):\n"
345
+ secao += f" Lucro Sistema: {lucro_sistema_fmt}\n"
346
+ secao += f" Lucro Mercado: {lucro_mercado_fmt}\n"
347
+ secao += f" Diferença: {diferenca_fmt}\n\n"
348
+
179
349
  secao += "Observações:\n\n"
180
350
 
181
351
  if PRICE_APPLY_TO_PROFIT:
182
- secao += "Os preços foram aplicados ao cálculo de lucro estimado.\n\n"
352
+ secao += "Os preços de mercado normalizados foram aplicados ao cálculo de lucro.\n\n"
183
353
  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"
354
+ 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"
355
+
356
+ secao += "Normalização de Unidades:\n"
357
+ secao += "- Todos os preços foram convertidos para R$/tonelada\n"
358
+ secao += "- Fatores: saca_60kg (x16.67), saca_50kg (x20), arroba_15kg (x66.67)\n\n"
185
359
 
186
360
  secao += "Fontes:\n"
187
361
  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
+ }
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.25",
4
4
  "description": "CLI global para AgroPlan AI - modo local acelerado",
5
5
  "type": "module",
6
6
  "bin": {