2026-05-28 09:24:43 -03:00

1370 lines
52 KiB
Python

from django.shortcuts import render
from django.http import JsonResponse
from django.conf import settings
from django.core.cache import cache
from django.utils import timezone
import logging
import pyodbc
import requests
from collections import defaultdict
from typing import Dict, List, Optional
from urllib.parse import urlencode, urlparse, parse_qsl, urlunparse
from datetime import datetime
logger = logging.getLogger(__name__)
def get_connection():
sql_config = settings.SQL_SERVER_CONFIG
connection_string = (
f"Driver={{SQL Server}};"
f"Server={sql_config['SERVER']},1433;"
f"Database={sql_config['DATABASE']};"
f"UID={sql_config['USERNAME']};"
f"PWD={sql_config['PASSWORD']}"
)
return pyodbc.connect(connection_string)
def home_page(request):
"""Nova página home com estética consistente do projeto."""
context = {
'user_email': request.user.email if request.user.is_authenticated else '',
'is_admin': request.user.is_staff if request.user.is_authenticated else False,
}
return render(request, "home/home_page.html", context)
def controle_saldo_page(request):
"""Página simulador de transferência de saldo entre PDVs."""
context = {
'user_email': request.user.email if request.user.is_authenticated else '',
'is_admin': request.user.is_staff if request.user.is_authenticated else False,
}
return render(request, "home/controle_saldo.html", context)
def get_controle_saldo_data(request):
"""Retorna saldo disponível por PDV para simulação de transferência."""
try:
saldo_orcamento_por_pdv_marca = get_api_orcamento_saldo_por_pdv_marca()
pending_por_pdv_marca = get_api_pendingorder_por_pdv_marca()
pending_ignorado_por_pdv_marca = get_api_pendingorder_ignorados_por_pdv_marca()
cobertura_por_pdv_marca = get_home_cobertura_por_pdv_marca()
base_pdvs_data = get_api_base_pdvs_data() or []
descricao_por_pdv: Dict[str, str] = {}
for row in base_pdvs_data:
pdv = _normalize_pdv(
row.get('pdv', row.get('PDV', row.get('loja_id', row.get('LOJA_ID', ''))))
)
if not pdv:
continue
descricao = str(
row.get(
'descricao_pdv',
row.get(
'DESCRICAO_PDV',
row.get('descricao', row.get('DESCRICAO', row.get('nome', row.get('NOME', ''))))
)
)
or ''
).strip()
if descricao:
descricao_por_pdv[pdv] = descricao
chaves_unicas = (
set(saldo_orcamento_por_pdv_marca.keys())
| set(pending_por_pdv_marca.keys())
| set(pending_ignorado_por_pdv_marca.keys())
)
pdvs = []
for key in chaves_unicas:
pdv, marca = key.split('|', 1)
saldo = float(saldo_orcamento_por_pdv_marca.get(key, 0.0))
pending = float(pending_por_pdv_marca.get(key, 0.0))
pending_ignorado = float(pending_ignorado_por_pdv_marca.get(key, 0.0))
orcamento_disponivel = saldo - pending + pending_ignorado
pdvs.append({
'key': key,
'pdv': pdv,
'marca': marca,
'descricao_pdv': descricao_por_pdv.get(pdv, ''),
'orcamento_disponivel': orcamento_disponivel,
'cobertura_dias': float(cobertura_por_pdv_marca.get(key, 0.0)),
'orcamento_bruto': saldo,
'pendente_api': pending,
'pendente_ignorado_api': pending_ignorado,
})
pdvs.sort(key=lambda item: (
(item.get('descricao_pdv') or '').lower(),
(item.get('pdv') or ''),
(item.get('marca') or ''),
))
return JsonResponse({
'status': 'success',
'data': pdvs,
'count': len(pdvs),
})
except Exception as e:
logger.exception("Erro ao carregar dados de controle de saldo")
return JsonResponse({
'status': 'error',
'message': str(e),
'data': []
}, status=200)
def get_home_cobertura_por_pdv() -> Dict[str, float]:
"""Calcula cobertura (dias) agregada por PDV usando a mesma base SQL do /home."""
try:
cache_key = "home_cobertura_pdv"
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data
query = """
WITH draft as (
SELECT
dh.businessunit AS MARCA,
dh.loja_id AS PDV,
dh.code AS SKU,
dh.stock_actual AS ESTOQUE,
dh.pendingorder AS PENDENTE,
dh.stock_intransit AS TRANSITO
FROM draft_historico dh with(nolock)
WHERE dh.data = (SELECT MAX(data) FROM draft_historico)
)
SELECT
emh.PDV,
emh.SKU,
emh.[DDV PREVISTO] AS DDV_PREVISTO,
COALESCE(d.ESTOQUE, emh.[ESTOQUE ATUAL], 0) AS ESTOQUE,
COALESCE(d.TRANSITO, emh.[ESTOQUE EM TRANSITO], 0) AS TRANSITO,
COALESCE(d.PENDENTE, emh.[PEDIDO PENDENTE], 0) AS PENDENTE
FROM estoque_mar_historico emh with(nolock)
LEFT JOIN draft d on d.PDV = emh.PDV and d.SKU = emh.SKU
WHERE emh.data_estoque = (select max(data_estoque) from estoque_mar_historico with(nolock))
and emh.CATEGORIA not in ('SUPORTE À VENDA','SUPORTE A VENDA', 'EMBALAGENS')
and emh.pdv not in ('23703')
"""
with get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(query)
columns = [col[0] for col in cursor.description]
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
agg: Dict[str, Dict[str, float]] = {}
for row in rows:
pdv = _normalize_pdv(row.get('PDV', row.get('pdv', '')))
if not pdv:
continue
if pdv not in agg:
agg[pdv] = {'estoque': 0.0, 'ddv': 0.0}
estoque = _parse_float(row.get('ESTOQUE', row.get('estoque', 0)))
ddv = _parse_float(row.get('DDV_PREVISTO', row.get('ddv_previsto', 0)))
agg[pdv]['estoque'] += estoque
agg[pdv]['ddv'] += ddv
cobertura_por_pdv: Dict[str, float] = {}
for pdv, vals in agg.items():
ddv = vals['ddv']
cobertura = (vals['estoque'] / ddv) if ddv > 0 else 0.0
cobertura_por_pdv[pdv] = round(cobertura, 0)
cache.set(cache_key, cobertura_por_pdv, timeout=300)
return cobertura_por_pdv
except Exception as e:
logger.error("Erro ao processar cobertura SQL do /home (PDV): %s", e)
return {}
def get_home_cobertura_por_pdv_marca() -> Dict[str, float]:
"""Calcula cobertura (dias) por PDV+MARCA usando a mesma base SQL do /home."""
try:
cache_key = "home_cobertura_pdv_marca"
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data
query = """
WITH draft as (
SELECT
dh.businessunit AS MARCA,
dh.loja_id AS PDV,
dh.code AS SKU,
dh.stock_actual AS ESTOQUE,
dh.pendingorder AS PENDENTE,
dh.stock_intransit AS TRANSITO
FROM draft_historico dh with(nolock)
WHERE dh.data = (SELECT MAX(data) FROM draft_historico)
)
SELECT
CASE
WHEN emh.DESCRICAO LIKE '%OUI%' THEN 'OUI'
ELSE emh.ORIGEM
END AS MARCA,
emh.PDV,
emh.SKU,
emh.[DDV PREVISTO] AS DDV_PREVISTO,
COALESCE(d.ESTOQUE, emh.[ESTOQUE ATUAL], 0) AS ESTOQUE,
COALESCE(d.TRANSITO, emh.[ESTOQUE EM TRANSITO], 0) AS TRANSITO,
COALESCE(d.PENDENTE, emh.[PEDIDO PENDENTE], 0) AS PENDENTE
FROM estoque_mar_historico emh with(nolock)
LEFT JOIN draft d on d.PDV = emh.PDV and d.SKU = emh.SKU
WHERE emh.data_estoque = (select max(data_estoque) from estoque_mar_historico with(nolock))
and emh.CATEGORIA not in ('SUPORTE À VENDA','SUPORTE A VENDA', 'EMBALAGENS')
and emh.pdv not in ('23703')
"""
with get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(query)
columns = [col[0] for col in cursor.description]
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
agg: Dict[str, Dict[str, float]] = {}
for row in rows:
pdv = _normalize_pdv(row.get('PDV', row.get('pdv', '')))
marca = _normalize_marca(row.get('MARCA', row.get('marca', '')))
if not pdv or not marca:
continue
key = f"{pdv}|{marca}"
if key not in agg:
agg[key] = {'estoque': 0.0, 'transito': 0.0, 'pendente': 0.0, 'ddv': 0.0}
estoque = _parse_float(row.get('ESTOQUE', row.get('estoque', 0)))
transito = _parse_float(row.get('TRANSITO', row.get('transito', 0)))
pendente = _parse_float(row.get('PENDENTE', row.get('pendente', 0)))
ddv = _parse_float(row.get('DDV_PREVISTO', row.get('ddv_previsto', 0)))
agg[key]['estoque'] += estoque
agg[key]['transito'] += transito
agg[key]['pendente'] += pendente
agg[key]['ddv'] += ddv
cobertura_por_pdv_marca: Dict[str, float] = {}
for key, vals in agg.items():
ddv = vals['ddv']
# No controle de saldo exibimos cobertura atual (estoque / ddv),
# alinhada com a validação manual solicitada.
cobertura = (vals['estoque'] / ddv) if ddv > 0 else 0.0
cobertura_por_pdv_marca[key] = round(cobertura, 0)
cache.set(cache_key, cobertura_por_pdv_marca, timeout=300)
return cobertura_por_pdv_marca
except Exception as e:
logger.error("Erro ao processar cobertura SQL do /home (PDV+MARCA): %s", e)
return {}
def get_api_orcamento_saldo_por_pdv_marca(mes: Optional[int] = None) -> Dict[str, float]:
"""Busca saldo_pdv por PDV+MARCA na API de orçamento, filtrando mês atual (ou informado)."""
try:
mes_alvo = int(mes or datetime.now().month)
cache_key = f"api_orcamento_saldo_pdv_marca:mes={mes_alvo}"
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data
api_config = getattr(settings, 'API_SUPRIMENTOS_CONFIG', {})
token = api_config.get('TOKEN')
url = 'https://api.grupoginseng.com.br/api/vw_orcamento_vs_notas_2026?limit=50000'
headers = {'Content-Type': 'application/json'}
if token:
headers['Authorization'] = f'Bearer {token}'
response = requests.get(url, headers=headers, timeout=30)
if response.status_code != 200:
logger.error("Erro ao chamar API orçamento (PDV+MARCA): %s", response.status_code)
return {}
payload = response.json()
rows = payload.get('data') if isinstance(payload, dict) else payload
if not isinstance(rows, list):
return {}
saldo_por_pdv_marca: Dict[str, float] = {}
for row in rows:
try:
mes_row = int(_parse_float(row.get('mes')))
except Exception:
continue
if mes_row != mes_alvo:
continue
pdv = _normalize_pdv(row.get('pdv', row.get('PDV', row.get('loja_id', row.get('LOJA_ID', '')))))
marca = _normalize_marca(row.get('brand', row.get('BRAND', row.get('marca', row.get('MARCA', '')))))
if not pdv or not marca:
continue
saldo = _parse_float(row.get('saldo_pdv', row.get('SALDO_PDV', 0)))
key = f"{pdv}|{marca}"
saldo_por_pdv_marca[key] = saldo_por_pdv_marca.get(key, 0.0) + saldo
cache.set(cache_key, saldo_por_pdv_marca, timeout=300)
return saldo_por_pdv_marca
except requests.exceptions.RequestException as e:
logger.error("Erro de conexão com API orçamento (PDV+MARCA): %s", e)
return {}
except Exception as e:
logger.error("Erro ao processar API orçamento (PDV+MARCA): %s", e)
return {}
def get_api_pendingorder_por_pdv_marca() -> Dict[str, float]:
"""Busca pendingorder por PDV+MARCA na API e agrega por loja/marca."""
try:
cache_key = "api_pendingorder_pdv_marca"
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data
api_config = getattr(settings, 'API_SUPRIMENTOS_CONFIG', {})
token = api_config.get('TOKEN')
url = 'https://api.grupoginseng.com.br/api/vw_pendingorder_draft?limit=50000'
headers = {'Content-Type': 'application/json'}
if token:
headers['Authorization'] = f'Bearer {token}'
response = requests.get(url, headers=headers, timeout=30)
if response.status_code != 200:
logger.error("Erro ao chamar API pendingorder (PDV+MARCA): %s", response.status_code)
return {}
payload = response.json()
rows = payload.get('data') if isinstance(payload, dict) else payload
if not isinstance(rows, list):
return {}
pending_por_pdv_marca: Dict[str, float] = {}
for row in rows:
pdv = _normalize_pdv(row.get('loja_id', row.get('LOJA_ID', row.get('pdv', row.get('PDV', '')))))
marca = _normalize_marca(row.get('businessunit', row.get('BUSINESSUNIT', row.get('marca', row.get('MARCA', '')))))
if not pdv or not marca:
continue
pending = _parse_float(
row.get(
'total_pricesellin',
row.get('TOTAL_PRICESELLIN', row.get('pendingorder', row.get('PENDINGORDER', row.get('pendente', 0))))
)
)
key = f"{pdv}|{marca}"
pending_por_pdv_marca[key] = pending_por_pdv_marca.get(key, 0.0) + pending
cache.set(cache_key, pending_por_pdv_marca, timeout=300)
return pending_por_pdv_marca
except requests.exceptions.RequestException as e:
logger.error("Erro de conexão com API pendingorder (PDV+MARCA): %s", e)
return {}
except Exception as e:
logger.error("Erro ao processar API pendingorder (PDV+MARCA): %s", e)
return {}
def get_api_pendingorder_ignorados_por_pdv_marca() -> Dict[str, float]:
"""Busca pendingorder ignorado por PDV+MARCA na API e agrega por loja/marca."""
try:
cache_key = "api_pendingorder_ignorados_pdv_marca"
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data
api_config = getattr(settings, 'API_SUPRIMENTOS_CONFIG', {})
token = api_config.get('TOKEN')
url = 'https://api.grupoginseng.com.br/api/vw_pendingorder_ignorados?limit=50000'
headers = {'Content-Type': 'application/json'}
if token:
headers['Authorization'] = f'Bearer {token}'
response = requests.get(url, headers=headers, timeout=30)
if response.status_code != 200:
logger.error("Erro ao chamar API pendingorder ignorados (PDV+MARCA): %s", response.status_code)
return {}
payload = response.json()
rows = payload.get('data') if isinstance(payload, dict) else payload
if not isinstance(rows, list):
return {}
pending_ignorado_por_pdv_marca: Dict[str, float] = {}
for row in rows:
pdv = _normalize_pdv(
row.get('loja_id', row.get('LOJA_ID', row.get('pdv', row.get('PDV', ''))))
)
marca = _normalize_marca(row.get('businessunit', row.get('BUSINESSUNIT', row.get('marca', row.get('MARCA', '')))))
if not pdv or not marca:
continue
pending_ignorado = _parse_float(
row.get(
'total_ignorado',
row.get(
'TOTAL_IGNORADO',
row.get(
'total_pricesellin',
row.get(
'TOTAL_PRICESELLIN',
row.get(
'pendingorder',
row.get('PENDINGORDER', row.get('pendente', 0))
)
)
)
)
)
)
key = f"{pdv}|{marca}"
pending_ignorado_por_pdv_marca[key] = pending_ignorado_por_pdv_marca.get(key, 0.0) + pending_ignorado
cache.set(cache_key, pending_ignorado_por_pdv_marca, timeout=300)
return pending_ignorado_por_pdv_marca
except requests.exceptions.RequestException as e:
logger.error("Erro de conexão com API pendingorder ignorados (PDV+MARCA): %s", e)
return {}
except Exception as e:
logger.error("Erro ao processar API pendingorder ignorados (PDV+MARCA): %s", e)
return {}
def get_table_data(request):
"""API endpoint que retorna os dados da query em formato JSON."""
try:
query = """
WITH draft as (
SELECT
dh.data,
CASE WHEN dh.description like '%OUI%' then 'OUI' else businessunit end as MARCA,
dh.loja_id AS PDV,
dh.code AS SKU,
dh.description AS DESCRICAO_PRODUTO,
dh.codcategory AS CATEGORIA,
dh.stock_actual AS ESTOQUE,
dh.pendingorder AS PENDENTE,
dh.stock_intransit AS Transito,
dh.dayswithoutsales AS Dias_Sem_Vendas,
dh.nextcycleprojection AS Projecao_Ciclo,
dh.secondtonextcycleprojection AS Projecao_ProxCiclo,
dh.thirdtolastcyclesales AS Vendas_C2,
dh.secondtolastcyclesales AS Vendas_C1,
dh.currentcyclesales AS Vendas_Atual,
dh.pricesellin AS Preco_compra,
dh.salescurve AS CLASSE,
dh.purchasesuggestion AS sugestao_Compra,
NULL AS [DDV PREVISTO],
CASE
WHEN dh.promotions_description = '' THEN 'REGULAR'
ELSE 'PROMOCAO'
END AS CAMPANHA,
CASE
WHEN dh.isproductdeactivated = 0 THEN 'ATIVO'
ELSE 'DESATIVADO'
END AS STATUS_PRODUTO
FROM draft_historico dh with(nolock)
WHERE dh.data = (SELECT MAX(data) FROM draft_historico))
SELECT
CASE
WHEN emh.DESCRICAO LIKE '%OUI%'
THEN 'OUI'
ELSE emh.ORIGEM
END AS MARCA,
pa.MESORREGIAO,
pa.[DESCRIÇÃO] as Descricao_PDV,
pa.UF,
pa.CANAL,
emh.data_estoque,
emh.PDV,
emh.SKU,
emh.SKU_PARA,
emh.DESCRICAO,
emh.CATEGORIA,
emh.CLASSE,
emh.[FASES PRODUTO],
emh.[ESTOQUE ATUAL],
emh.[DDV PREVISTO],
emh.[ESTOQUE EM TRANSITO],
emh.[PEDIDO PENDENTE],
D.VENDAS_ATUAL,
D.Projecao_Ciclo,
D.Projecao_Ciclo - D.VENDAS_ATUAL AS DIF_PROJE_VS_VENDA,
D.sugestao_Compra,
D.Preco_compra
FROM estoque_mar_historico emh with(nolock)
LEFT JOIN pdvs_att pa
ON pa.PDV_PARA = emh.PDV
LEFT JOIN draft d on d.PDV= emh.PDV and d.SKU = emh.SKU
WHERE emh.data_estoque = (select max(data_estoque) from estoque_mar_historico with(nolock)) and emh.CATEGORIA not in ('SUPORTE À VENDA','SUPORTE A VENDA', 'EMBALAGENS')
AND emh.pdv not in ('23703')
ORDER BY pa.[DESCRIÇÃO], emh.CLASSE
"""
with get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(query)
columns = [col[0] for col in cursor.description]
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
return JsonResponse({
'status': 'success',
'data': rows,
'count': len(rows)
})
except Exception as e:
# Retornar erro mas não quebrar a página
logger.exception("Erro ao carregar dados")
return JsonResponse({
'status': 'error',
'message': str(e),
'data': [] # Retornar array vazio para não quebrar frontend
}, status=200) # Retornar 200 para não gerar erro no console
# ===== INTEGRAÇÃO COM API DE SUPRIMENTOS =====
def get_api_suprimentos_data(ciclo: str = '', idpedido: str = '', tipo: str = '') -> Optional[List[Dict]]:
"""
Busca dados das APIs de Suprimentos e faz INNER JOIN por idpedido.
Retorna apenas registros presentes simultaneamente em:
- API_SUPRIMENTOS_IMPLANTACAO_URL
- API_SUPRIMENTOS_DETALHE_URL (em aberto/pendente)
"""
try:
ciclo_norm = str(ciclo or '').strip()
tipo_norm = str(tipo or '').strip().lower()
idpedido_norm = ','.join(
sorted({p.strip() for p in str(idpedido or '').replace(';', ',').split(',') if p.strip()})
)
cache_key = f"api_suprimentos_join:v2:ciclo={ciclo_norm}:tipo={tipo_norm}:idpedido={idpedido_norm}"
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data
api_config = settings.API_SUPRIMENTOS_CONFIG
token = api_config['TOKEN']
if not token:
logger.warning("Token da API de Suprimentos não configurado no .env")
return None
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
def _append_query_params(url: str, params: Dict[str, str]) -> str:
parsed = urlparse(url)
query = dict(parse_qsl(parsed.query, keep_blank_values=True))
for key, value in params.items():
if value:
query[key] = value
return urlunparse(parsed._replace(query=urlencode(query)))
def _fetch_rows(url: str) -> Optional[List[Dict]]:
response = requests.get(url, headers=headers, timeout=30)
if response.status_code != 200:
logger.error(f"Erro ao chamar API {url}: {response.status_code}")
return None
payload = response.json()
if isinstance(payload, dict) and 'data' in payload:
return payload['data']
if isinstance(payload, list):
return payload
return None
detalhe_url = _append_query_params(
api_config['DETALHE_URL'],
{'ciclo': ciclo}
)
implantacao_rows = _fetch_rows(api_config['IMPLANTACAO_URL'])
detalhe_rows = _fetch_rows(detalhe_url)
if implantacao_rows is None or detalhe_rows is None:
return None
def _get_idpedido(row: Dict) -> str:
return str(
row.get('idpedido', row.get('idPedido', row.get('IDPEDIDO', '')))
).strip()
idpedidos_filtro = set(idpedido_norm.split(',')) if idpedido_norm else set()
tipo_filtro = tipo_norm
# Índice da API de detalhe (abertos) por idpedido
detalhe_by_id = {}
for row in detalhe_rows:
idpedido = _get_idpedido(row)
tipo_row = _get_api_tipo(row).lower()
if tipo_filtro and tipo_row != tipo_filtro:
continue
if idpedidos_filtro and idpedido not in idpedidos_filtro:
continue
if idpedido:
detalhe_by_id[idpedido] = row
# INNER JOIN por idpedido: mantém somente implantação que existe na detalhe
joined_rows = []
for row in implantacao_rows:
idpedido = _get_idpedido(row)
if idpedido and idpedido in detalhe_by_id:
detalhe_row = detalhe_by_id[idpedido]
merged_row = dict(row)
ciclo_val = _get_api_ciclo(detalhe_row)
if ciclo_val and not _get_api_ciclo(merged_row):
merged_row['ciclo'] = ciclo_val
joined_rows.append(merged_row)
logger.info(
"API join por idpedido: implantacao=%s detalhe=%s inner_join=%s ciclo=%s tipo=%s idpedidos=%s",
len(implantacao_rows), len(detalhe_rows), len(joined_rows), ciclo or "(todos)", tipo or "(todos)", len(idpedidos_filtro)
)
cache.set(cache_key, joined_rows, timeout=300)
return joined_rows
except requests.exceptions.RequestException as e:
logger.error(f"Erro de conexão com API de Suprimentos: {e}")
return None
except Exception as e:
logger.error(f"Erro ao processar dados da API: {e}")
return None
def get_api_base_pdvs_data() -> Optional[List[Dict]]:
"""Busca base de PDVs para enriquecer dados com CANAL e ANALISTA."""
try:
api_config = getattr(settings, 'API_SUPRIMENTOS_CONFIG', {})
token = api_config.get('TOKEN')
url = 'https://api.grupoginseng.com.br/api/base_pdvs'
headers = {'Content-Type': 'application/json'}
if token:
headers['Authorization'] = f'Bearer {token}'
response = requests.get(url, headers=headers, timeout=30)
if response.status_code != 200:
logger.error("Erro ao chamar API base_pdvs: %s", response.status_code)
return None
payload = response.json()
if isinstance(payload, dict) and isinstance(payload.get('data'), list):
return payload['data']
if isinstance(payload, list):
return payload
return None
except requests.exceptions.RequestException as e:
logger.error("Erro de conexão com API base_pdvs: %s", e)
return None
except Exception as e:
logger.error("Erro ao processar API base_pdvs: %s", e)
return None
def get_api_orcamento_saldo_por_pdv(mes: Optional[int] = None) -> Dict[str, float]:
"""Busca saldo_pdv por PDV na API de orçamento, filtrando mês atual (ou informado)."""
try:
mes_alvo = int(mes or datetime.now().month)
cache_key = f"api_orcamento_saldo_pdv:mes={mes_alvo}"
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data
api_config = getattr(settings, 'API_SUPRIMENTOS_CONFIG', {})
token = api_config.get('TOKEN')
url = 'https://api.grupoginseng.com.br/api/vw_orcamento_vs_notas_2026?limit=50000'
headers = {'Content-Type': 'application/json'}
if token:
headers['Authorization'] = f'Bearer {token}'
response = requests.get(url, headers=headers, timeout=30)
if response.status_code != 200:
logger.error("Erro ao chamar API orçamento: %s", response.status_code)
return {}
payload = response.json()
rows = payload.get('data') if isinstance(payload, dict) else payload
if not isinstance(rows, list):
return {}
saldo_por_pdv: Dict[str, float] = {}
for row in rows:
try:
mes_row = int(_parse_float(row.get('mes')))
except Exception:
continue
if mes_row != mes_alvo:
continue
pdv = _normalize_pdv(row.get('pdv', row.get('PDV', row.get('loja_id', row.get('LOJA_ID', '')))))
if not pdv:
continue
saldo = _parse_float(row.get('saldo_pdv', row.get('SALDO_PDV', 0)))
saldo_por_pdv[pdv] = saldo_por_pdv.get(pdv, 0.0) + saldo
cache.set(cache_key, saldo_por_pdv, timeout=300)
return saldo_por_pdv
except requests.exceptions.RequestException as e:
logger.error("Erro de conexão com API orçamento: %s", e)
return {}
except Exception as e:
logger.error("Erro ao processar API orçamento: %s", e)
return {}
def get_api_pendingorder_por_pdv() -> Dict[str, float]:
"""Busca pendingorder por PDV (loja_id) na API e agrega por loja."""
try:
cache_key = "api_pendingorder_pdv"
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data
api_config = getattr(settings, 'API_SUPRIMENTOS_CONFIG', {})
token = api_config.get('TOKEN')
url = 'https://api.grupoginseng.com.br/api/vw_pendingorder_draft?limit=50000'
headers = {'Content-Type': 'application/json'}
if token:
headers['Authorization'] = f'Bearer {token}'
response = requests.get(url, headers=headers, timeout=30)
if response.status_code != 200:
logger.error("Erro ao chamar API pendingorder: %s", response.status_code)
return {}
payload = response.json()
rows = payload.get('data') if isinstance(payload, dict) else payload
if not isinstance(rows, list):
return {}
pending_por_pdv: Dict[str, float] = {}
for row in rows:
pdv = _normalize_pdv(row.get('loja_id', row.get('LOJA_ID', row.get('pdv', row.get('PDV', '')))))
if not pdv:
continue
# Nesta API o valor financeiro pendente vem em total_pricesellin.
pending = _parse_float(
row.get(
'total_pricesellin',
row.get('TOTAL_PRICESELLIN', row.get('pendingorder', row.get('PENDINGORDER', row.get('pendente', 0))))
)
)
pending_por_pdv[pdv] = pending_por_pdv.get(pdv, 0.0) + pending
cache.set(cache_key, pending_por_pdv, timeout=300)
return pending_por_pdv
except requests.exceptions.RequestException as e:
logger.error("Erro de conexão com API pendingorder: %s", e)
return {}
except Exception as e:
logger.error("Erro ao processar API pendingorder: %s", e)
return {}
def get_api_pendingorder_ignorados_por_pdv() -> Dict[str, float]:
"""Busca pendingorder ignorado por PDV na API e agrega por loja."""
try:
cache_key = "api_pendingorder_ignorados_pdv"
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data
api_config = getattr(settings, 'API_SUPRIMENTOS_CONFIG', {})
token = api_config.get('TOKEN')
url = 'https://api.grupoginseng.com.br/api/vw_pendingorder_ignorados?limit=50000'
headers = {'Content-Type': 'application/json'}
if token:
headers['Authorization'] = f'Bearer {token}'
response = requests.get(url, headers=headers, timeout=30)
if response.status_code != 200:
logger.error("Erro ao chamar API pendingorder ignorados: %s", response.status_code)
return {}
payload = response.json()
rows = payload.get('data') if isinstance(payload, dict) else payload
if not isinstance(rows, list):
return {}
pending_ignorado_por_pdv: Dict[str, float] = {}
for row in rows:
pdv = _normalize_pdv(
row.get('loja_id', row.get('LOJA_ID', row.get('pdv', row.get('PDV', ''))))
)
if not pdv:
continue
pending_ignorado = _parse_float(
row.get(
'total_ignorado',
row.get(
'TOTAL_IGNORADO',
row.get(
'total_pricesellin',
row.get(
'TOTAL_PRICESELLIN',
row.get(
'pendingorder',
row.get('PENDINGORDER', row.get('pendente', 0))
)
)
)
)
)
)
pending_ignorado_por_pdv[pdv] = pending_ignorado_por_pdv.get(pdv, 0.0) + pending_ignorado
cache.set(cache_key, pending_ignorado_por_pdv, timeout=300)
return pending_ignorado_por_pdv
except requests.exceptions.RequestException as e:
logger.error("Erro de conexão com API pendingorder ignorados: %s", e)
return {}
except Exception as e:
logger.error("Erro ao processar API pendingorder ignorados: %s", e)
return {}
def get_api_aprovado_hoje_total() -> float:
"""Soma o valor aprovado no dia atual (dtimplantacao == hoje) na API de detalhepedido."""
try:
hoje = timezone.localdate()
cache_key = f"api_aprovado_hoje_total:{hoje.isoformat()}"
cached_data = cache.get(cache_key)
if cached_data is not None:
return float(cached_data)
api_config = getattr(settings, 'API_SUPRIMENTOS_CONFIG', {})
token = api_config.get('TOKEN')
url = 'https://api.grupoginseng.com.br/api/suprimentos_detalhepedido?limit=50000&status=aprovado'
headers = {'Content-Type': 'application/json'}
if token:
headers['Authorization'] = f'Bearer {token}'
response = requests.get(url, headers=headers, timeout=30)
if response.status_code != 200:
logger.error("Erro ao chamar API aprovados de hoje: %s", response.status_code)
return 0.0
payload = response.json()
rows = payload.get('data') if isinstance(payload, dict) else payload
if not isinstance(rows, list):
return 0.0
def _is_row_hoje(dt_value) -> bool:
if dt_value in (None, ''):
return False
text = str(dt_value).strip()
if not text:
return False
iso_candidate = text.replace('Z', '+00:00')
try:
dt = datetime.fromisoformat(iso_candidate)
return dt.date() == hoje
except Exception:
pass
for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%Y-%m-%d %H:%M:%S", "%d/%m/%Y %H:%M:%S"):
try:
return datetime.strptime(text, fmt).date() == hoje
except Exception:
continue
return False
total_aprovado_hoje = 0.0
for row in rows:
if not _is_row_hoje(row.get('dtimplantacao', row.get('DTIMPLANTACAO'))):
continue
valor = _parse_float(
row.get(
'total_pricesellin',
row.get(
'TOTAL_PRICESELLIN',
row.get(
'total_ignorado',
row.get(
'TOTAL_IGNORADO',
row.get('valor_total', row.get('VALOR_TOTAL', 0))
)
)
)
)
)
total_aprovado_hoje += valor
cache.set(cache_key, total_aprovado_hoje, timeout=300)
return total_aprovado_hoje
except requests.exceptions.RequestException as e:
logger.error("Erro de conexão com API aprovados de hoje: %s", e)
return 0.0
except Exception as e:
logger.error("Erro ao processar API aprovados de hoje: %s", e)
return 0.0
def _parse_float(value) -> float:
"""Converte valores numéricos aceitando formatos BR/EN (ex.: 1.234,56 / 1,234.56)."""
if value in (None, ''):
return 0.0
if isinstance(value, (int, float)):
return float(value)
try:
text = str(value).strip()
if not text:
return 0.0
# Mantém apenas dígitos, separadores e sinal.
clean = ''.join(ch for ch in text if ch.isdigit() or ch in ',.-')
if not clean or clean in ('-', '.', ',', '-.', '-,'):
return 0.0
# Caso com ambos separadores: assume o último como decimal.
if ',' in clean and '.' in clean:
if clean.rfind(',') > clean.rfind('.'):
# BR: 1.234,56 -> 1234.56
clean = clean.replace('.', '').replace(',', '.')
else:
# EN: 1,234.56 -> 1234.56
clean = clean.replace(',', '')
elif ',' in clean:
# Apenas vírgula: trata como decimal.
clean = clean.replace('.', '').replace(',', '.')
else:
# Apenas ponto: remove vírgulas residuais.
clean = clean.replace(',', '')
return float(clean)
except (TypeError, ValueError):
return 0.0
def _get_api_quantidade(row: Dict) -> float:
"""Obtém quantidade da API aceitando variações de chave."""
return _parse_float(
row.get('quantidade', row.get('Quantidade', row.get('QUANTIDADE', 0)))
)
def _get_api_ciclo(row: Dict) -> str:
"""Obtém ciclo da API aceitando variações de chave."""
return str(
row.get('ciclo', row.get('CICLO', row.get('Ciclo', '')))
).strip()
def _get_api_tipo(row: Dict) -> str:
"""Obtém tipo da API aceitando variações de chave."""
return str(
row.get('tipo', row.get('TIPO', row.get('Tipo', row.get('tipo_pedido', row.get('TIPO_PEDIDO', '')))))
).strip()
def _normalize_pdv(value) -> str:
"""Normaliza PDV para comparação entre fontes diferentes."""
text = str(value or '').strip()
if not text:
return ''
# Remove .0 comum quando vem de conversão numérica.
if text.endswith('.0'):
text = text[:-2]
# Remove zeros à esquerda para evitar mismatch 02001 vs 2001.
text = text.lstrip('0')
return text or '0'
def _normalize_marca(value) -> str:
"""Normaliza marca para comparação entre fontes diferentes."""
text = str(value or '').strip()
if not text:
return ''
return text.upper()
def _normalize_sku(value) -> str:
"""Normaliza SKU para comparação entre fontes diferentes."""
text = str(value or '').strip()
if not text:
return ''
if text.endswith('.0'):
text = text[:-2]
return text.upper()
def merge_api_with_sql_data(sql_data: List[Dict], api_data: Optional[List[Dict]]) -> List[Dict]:
"""
Faz o join entre dados do SQL Server e da API por PDV + SKU.
Agrega quantidade da API por (PDV, SKU) e mapeia para sugestao_analista.
"""
# Agregar API por (PDV, SKU) (somatório de quantidade)
api_por_chave = {}
for row in api_data or []:
pdv = _normalize_pdv(row.get('pdv', row.get('PDV', '')))
sku = _normalize_sku(row.get('sku', row.get('SKU', row.get('codigo', row.get('CODIGO', '')))))
if not pdv or not sku:
continue
key = (pdv, sku)
api_por_chave[key] = api_por_chave.get(key, 0.0) + _get_api_quantidade(row)
# Fazer o merge da agregação por (PDV, SKU) no resultado da pivot
merged_data = []
for sql_row in sql_data:
pdv = _normalize_pdv(sql_row.get('PDV', sql_row.get('pdv', '')))
sku = _normalize_sku(sql_row.get('SKU', sql_row.get('sku', sql_row.get('SKU_PARA', ''))))
key = (pdv, sku)
sugestao_analista = api_por_chave.get(key, 0.0)
sql_row['sugestao_analista'] = sugestao_analista
sql_row['Sugestao_Compra_Analista'] = sugestao_analista
# Mantém Total_Compra sem incluir quantidade da API:
# a API de implantacao alimenta apenas Sugestao_Compra_Analista.
sql_row['Total_Compra'] = None
sql_row['dados_api_presentes'] = key in api_por_chave
merged_data.append(sql_row)
return merged_data
def merge_base_pdvs_with_sql_data(sql_data: List[Dict], base_pdvs_data: Optional[List[Dict]]) -> List[Dict]:
"""
Enriquecer linhas SQL com CANAL e ANALISTA vindos da API base_pdvs via join por PDV.
"""
if not base_pdvs_data:
return sql_data
base_por_pdv: Dict[str, Dict] = {}
for row in base_pdvs_data:
pdv = _normalize_pdv(
row.get('PDV', row.get('pdv', row.get('loja_id', row.get('LOJA_ID', ''))))
)
if not pdv:
continue
base_por_pdv[pdv] = row
for sql_row in sql_data:
pdv = _normalize_pdv(sql_row.get('PDV', sql_row.get('pdv', '')))
base_row = base_por_pdv.get(pdv, {})
canal_api = (
base_row.get('CANAL')
or base_row.get('canal')
or base_row.get('Canal')
)
analista_api = (
base_row.get('ANALISTA')
or base_row.get('analista')
or base_row.get('Analista')
)
if canal_api:
sql_row['CANAL'] = canal_api
if analista_api:
sql_row['ANALISTA'] = analista_api
sql_row['Analista'] = analista_api
return sql_data
def aggregate_rows_for_dashboard(rows: List[Dict]) -> List[Dict]:
"""Agrega linhas por PDV + CATEGORIA + CLASSE para reduzir volume no frontend."""
grouped = {}
numeric_fields = [
'Estoque_Total',
'Transito_Total',
'Pendente_Total',
'DDV_Previsto_Total',
'Projecao_Total',
'Vendas_Atual',
'Sugestao_Compra_Total',
'Sugestao_Compra_Analista',
'Diff Proj x Venda',
'Total_SKUs',
]
sku_sets = defaultdict(set)
for row in rows or []:
key = (
str(row.get('PDV') or '').strip(),
str(row.get('MESORREGIAO') or '').strip(),
str(row.get('Descricao_PDV') or '').strip(),
str(row.get('CATEGORIA') or '').strip(),
str(row.get('CLASSE') or '').strip(),
)
if key not in grouped:
grouped[key] = {
'PDV': row.get('PDV'),
'MESORREGIAO': row.get('MESORREGIAO'),
'Descricao_PDV': row.get('Descricao_PDV'),
'UF': row.get('UF'),
'CANAL': row.get('CANAL'),
'CATEGORIA': row.get('CATEGORIA'),
'CLASSE': row.get('CLASSE'),
'data_estoque': row.get('data_estoque'),
'Analista': row.get('Analista', row.get('ANALISTA')),
'ANALISTA': row.get('ANALISTA', row.get('Analista')),
'dados_api_presentes': False,
'Total_Compra': 0.0,
}
for field in numeric_fields:
grouped[key][field] = 0.0
target = grouped[key]
for field in numeric_fields:
target[field] += _parse_float(row.get(field))
target['Total_Compra'] += (
_parse_float(row.get('Sugestao_Compra_Analista')) * _parse_float(row.get('Preco_compra'))
)
sku = _normalize_sku(row.get('SKU', row.get('sku', row.get('SKU_PARA'))))
if sku:
sku_sets[key].add(sku)
target['dados_api_presentes'] = target['dados_api_presentes'] or bool(row.get('dados_api_presentes'))
result = []
for key, row in grouped.items():
ddv = _parse_float(row.get('DDV_Previsto_Total'))
estoque = _parse_float(row.get('Estoque_Total'))
transito = _parse_float(row.get('Transito_Total'))
pendente = _parse_float(row.get('Pendente_Total'))
row['Total_SKUs'] = len(sku_sets[key])
row['Cobertura_Atual'] = round(estoque / ddv, 0) if ddv > 0 else 0
row['Cobertura_Est_Trans'] = round((estoque + transito) / ddv, 0) if ddv > 0 else 0
row['Cobertura_Est_Trans_Pend'] = round((estoque + transito + pendente) / ddv, 0) if ddv > 0 else 0
result.append(row)
result.sort(key=lambda r: (
str(r.get('Descricao_PDV') or '').lower(),
str(r.get('CLASSE') or '').lower(),
str(r.get('CATEGORIA') or '').lower(),
))
return result
def merge_orcamento_with_summary_data(
summary_rows: List[Dict],
saldo_por_pdv: Dict[str, float],
pending_por_pdv: Optional[Dict[str, float]] = None,
pending_ignorado_por_pdv: Optional[Dict[str, float]] = None,
) -> List[Dict]:
"""Anexa orçamento disponível por PDV às linhas resumidas, descontando pendingorder."""
if not summary_rows:
return summary_rows
pending_por_pdv = pending_por_pdv or {}
pending_ignorado_por_pdv = pending_ignorado_por_pdv or {}
for row in summary_rows:
pdv = _normalize_pdv(row.get('PDV', row.get('pdv', '')))
saldo = float(saldo_por_pdv.get(pdv, 0.0))
pending = float(pending_por_pdv.get(pdv, 0.0))
pending_ignorado = float(pending_ignorado_por_pdv.get(pdv, 0.0))
row['Orcamento_Bruto'] = saldo
row['Pendente_API'] = pending
row['Pendente_Ignorado_API'] = pending_ignorado
row['Orcamento_Disponivel'] = saldo - pending + pending_ignorado
return summary_rows
def get_pivot_data(request):
"""API endpoint que retorna dados em granularidade PDV+SKU para pivot table.
Inclui integração com dados da API de Suprimentos."""
try:
ciclo_filtro = str(request.GET.get('ciclo', '') or '').strip()
idpedido_filtro = str(request.GET.get('idpedido', '') or '').strip()
tipo_filtro = str(request.GET.get('tipo', '') or '').strip()
# Query com GROUP BY PDV + SKU + CATEGORIA + CLASSE para permitir
# merge por PDV+SKU antes do agrupamento/resumo no frontend.
query = """
WITH max_data AS (
SELECT MAX(data_estoque) AS data_estoque
FROM estoque_mar_historico
),
draft AS (
SELECT
dh.data,
dh.businessunit AS MARCA,
dh.loja_id AS PDV,
dh.code AS SKU,
dh.description AS DESCRICAO_PRODUTO,
dh.codcategory AS CATEGORIA,
dh.stock_actual AS ESTOQUE,
dh.pendingorder AS PENDENTE,
dh.stock_intransit AS Transito,
dh.dayswithoutsales AS Dias_Sem_Vendas,
dh.nextcycleprojection AS Projecao_Ciclo,
dh.secondtonextcycleprojection AS Projecao_ProxCiclo,
dh.thirdtolastcyclesales AS Vendas_C2,
dh.secondtolastcyclesales AS Vendas_C1,
dh.currentcyclesales AS Vendas_Atual,
dh.pricesellin AS Preco_compra,
dh.salescurve AS CLASSE,
dh.purchasesuggestion AS sugestao_Compra,
CASE
WHEN dh.promotions_description = '' THEN 'REGULAR'
ELSE 'PROMOCAO'
END AS CAMPANHA,
CASE
WHEN dh.isproductdeactivated = 0 THEN 'ATIVO'
ELSE 'DESATIVADO'
END AS STATUS_PRODUTO
FROM draft_historico dh
WHERE dh.data = (SELECT MAX(data) FROM draft_historico)
),
base AS (
SELECT
emh.*,
-- Conversões seguras
TRY_CAST(REPLACE(emh.[ESTOQUE ATUAL],',', '.') as float) AS ESTOQUE_ATUAL_F,
TRY_CAST(REPLACE(emh.[ESTOQUE EM TRANSITO],',', '.') as float) AS TRANSITO_F,
TRY_CAST(REPLACE(emh.[PEDIDO PENDENTE],',', '.') as float) AS PENDENTE_F,
TRY_CAST(REPLACE(emh.[DDV PREVISTO],',', '.') as float) AS DDV_F
FROM estoque_mar_historico emh
WHERE emh.data_estoque = (SELECT MAX(data_estoque) FROM estoque_mar_historico WITH (NOLOCK))
AND emh.CATEGORIA NOT IN ('SUPORTE À VENDA','SUPORTE A VENDA', 'EMBALAGENS')
AND emh.PDV NOT IN ('23703')
)
SELECT
pa.PDV_PARA as PDV,
pa.[DESCRIÇÃO] as Descricao_PDV,
pa.MESORREGIAO,
pa.UF,
pa.CANAL,
b.SKU,
b.CATEGORIA,
b.CLASSE,
SUM(b.ESTOQUE_ATUAL_F) as Estoque_Total,
SUM(b.TRANSITO_F) as Transito_Total,
SUM(b.PENDENTE_F) as Pendente_Total,
SUM(b.DDV_F) as DDV_Previsto_Total,
SUM(TRY_CAST(d.Projecao_Ciclo AS FLOAT)) as Projecao_Total,
SUM(TRY_CAST(REPLACE(d.Vendas_Atual, ',', '.') AS FLOAT)) as Vendas_Atual,
MAX(TRY_CAST(REPLACE(d.Preco_compra, ',', '.') AS FLOAT)) as Preco_compra,
SUM(TRY_CAST(d.sugestao_Compra AS FLOAT)) as Sugestao_Compra_Total,
COUNT(DISTINCT b.SKU) as Total_SKUs,
SUM(TRY_CAST(d.Projecao_Ciclo AS FLOAT)) - SUM(TRY_CAST(REPLACE(d.Vendas_Atual, ',', '.') AS FLOAT)) as 'Diff Proj x Venda',
-- Coberturas
CASE
WHEN SUM(DDV_F) > 0
THEN round(SUM(b.ESTOQUE_ATUAL_F) /
SUM(b.DDV_F),0)
ELSE 0
END as Cobertura_Atual,
CASE
WHEN SUM(DDV_F) > 0
THEN round((SUM(b.ESTOQUE_ATUAL_F) + SUM(b.TRANSITO_F)) / SUM(DDV_F),0)
ELSE 0
END as Cobertura_Est_Trans,
CASE
WHEN SUM(DDV_F) > 0
THEN round((SUM(b.ESTOQUE_ATUAL_F) + SUM(b.TRANSITO_F)+ SUM(b.PENDENTE_F)) /
SUM(DDV_F),0)
ELSE 0
END as Cobertura_Est_Trans_Pend,
NULL as Sugestao_Compra_Analista,
NULL as Total_Compra,
b.data_estoque
FROM base b
JOIN max_data md
ON b.data_estoque = md.data_estoque
LEFT JOIN pdvs_att pa
ON pa.PDV_PARA = b.PDV
LEFT JOIN draft d
ON d.PDV = b.PDV
AND d.SKU = b.SKU
GROUP BY
pa.PDV_PARA,
pa.[DESCRIÇÃO],
pa.MESORREGIAO,
pa.UF,
pa.CANAL,
b.SKU,
b.CATEGORIA,
b.CLASSE,
b.data_estoque
ORDER BY pa.[DESCRIÇÃO];
"""
categorias_query = """
SELECT DISTINCT emh.CATEGORIA
FROM estoque_mar_historico emh
WHERE emh.data_estoque = (SELECT MAX(data_estoque) FROM estoque_mar_historico WITH (NOLOCK))
AND emh.CATEGORIA NOT IN ('SUPORTE À VENDA','SUPORTE A VENDA', 'EMBALAGENS')
AND emh.PDV NOT IN ('23703')
AND emh.CATEGORIA IS NOT NULL
ORDER BY emh.CATEGORIA
"""
classes_query = """
SELECT DISTINCT emh.CLASSE
FROM estoque_mar_historico emh
WHERE emh.data_estoque = (SELECT MAX(data_estoque) FROM estoque_mar_historico WITH (NOLOCK))
AND emh.CATEGORIA NOT IN ('SUPORTE À VENDA','SUPORTE A VENDA', 'EMBALAGENS')
AND emh.PDV NOT IN ('23703')
AND emh.CLASSE IS NOT NULL
ORDER BY emh.CLASSE
"""
# Buscar dados do SQL Server
with get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(query)
columns = [col[0] for col in cursor.description]
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
cursor.execute(categorias_query)
categorias = [str(row[0]).strip() for row in cursor.fetchall() if row and row[0]]
cursor.execute(classes_query)
classes = [str(row[0]).strip() for row in cursor.fetchall() if row and row[0]]
# Enriquecimento com base_pdvs (CANAL/ANALISTA)
base_pdvs_data = get_api_base_pdvs_data()
rows = merge_base_pdvs_with_sql_data(rows, base_pdvs_data)
# Tentar buscar dados da API de suprimentos (não falha se indisponível)
api_data = get_api_suprimentos_data(ciclo=ciclo_filtro, idpedido=idpedido_filtro, tipo=tipo_filtro)
ciclos = sorted({
_get_api_ciclo(row) for row in (api_data or []) if _get_api_ciclo(row)
})
tipos = sorted({
_get_api_tipo(row) for row in (api_data or []) if _get_api_tipo(row)
})
# Fazer merge dos dados se API estiver disponível
if api_data:
rows = merge_api_with_sql_data(rows, api_data)
logger.info(f"Dados da API integrados: {len(api_data)} registros")
else:
logger.info("Dados da API não disponíveis, retornando apenas dados do SQL Server")
summary_rows = aggregate_rows_for_dashboard(rows)
saldo_orcamento_por_pdv = get_api_orcamento_saldo_por_pdv()
pending_por_pdv = get_api_pendingorder_por_pdv()
pending_ignorado_por_pdv = get_api_pendingorder_ignorados_por_pdv()
summary_rows = merge_orcamento_with_summary_data(
summary_rows,
saldo_orcamento_por_pdv,
pending_por_pdv,
pending_ignorado_por_pdv,
)
aprovado_hoje_total = get_api_aprovado_hoje_total()
logger.info(
"Pivot carregada | linhas_sql=%s linhas_resumo=%s pdvs_orcamento=%s pdvs_pending=%s pdvs_pending_ignorados=%s aprovado_hoje_total=%s",
len(rows),
len(summary_rows),
len(saldo_orcamento_por_pdv),
len(pending_por_pdv),
len(pending_ignorado_por_pdv),
round(aprovado_hoje_total, 2),
)
return JsonResponse({
'status': 'success',
'data': summary_rows,
'count': len(summary_rows),
'aprovado_hoje_total': aprovado_hoje_total,
'api_integrated': bool(api_data),
'base_pdvs_integrated': bool(base_pdvs_data),
'orcamento_integrated': bool(saldo_orcamento_por_pdv),
'categorias': categorias,
'classes': classes,
'ciclos': ciclos,
'tipos': tipos,
'ciclo_aplicado': ciclo_filtro,
'tipo_aplicado': tipo_filtro,
'idpedido_aplicado': idpedido_filtro,
})
except Exception as e:
# Retornar erro mas não quebrar a página
logger.exception("Erro ao carregar dados da pivot")
return JsonResponse({
'status': 'error',
'message': str(e),
'data': []
}, status=200)