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.
|
|
3
|
-
"backend_template_version": "1.0.
|
|
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-
|
|
18
|
+
"generated_at": "2026-05-09T21:00:00Z"
|
|
17
19
|
}
|
package/backend-template/api.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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":
|
|
72
|
-
"
|
|
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 |
|
|
98
|
-
secao += "
|
|
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
|
-
|
|
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 |
|
|
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
|
|
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
|
|
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
|
-
|
|
173
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|