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.
- package/backend-template/VERSION.json +7 -4
- package/backend-template/api.py +90 -0
- package/backend-template/core/market_profit_validator.py +266 -0
- package/backend-template/core/price_adapter.py +203 -25
- package/backend-template/core/price_normalizer.py +182 -0
- package/backend-template/core/report_generator.py +192 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"cli_version": "1.0.
|
|
3
|
-
"backend_template_version": "1.0.
|
|
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-
|
|
19
|
+
"generated_at": "2026-05-09T22:00:00Z"
|
|
17
20
|
}
|
package/backend-template/api.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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":
|
|
72
|
-
"
|
|
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 |
|
|
98
|
-
secao += "
|
|
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
|
-
|
|
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 |
|
|
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
|
|
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
|
|
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
|
-
|
|
173
|
-
|
|
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
|
|
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
|
|
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
|
|