Server : Apache System : Linux dedi-14684855.grupobig.com 5.14.0-611.49.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Apr 21 16:39:08 EDT 2026 x86_64 User : grupo692 ( 1004) PHP Version : 8.2.31 Disable Function : NONE Directory : /opt/dash_backend_new/app/ |
from datetime import date
from typing import List, Optional, Any, Dict, Tuple
import math
import pandas as pd
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from .db import get_db_conn
from .auth import get_current_user, UserTokenData
from .tenant_oracle_db import get_tenant_context, TenantContext
from .sandbox_data import is_sandbox_empresa, vendas_por_unidade_fake, vendas_por_grupo_fake
router = APIRouter(prefix="/vendas", tags=["vendas"])
# -----------------------------------------------------------------------------
# HELPERS
# -----------------------------------------------------------------------------
def _sanitize_for_json(value: Any) -> Any:
"""
Garante que não existam NaN/Infinity no payload final.
JSON não aceita NaN/Inf.
"""
if value is None:
return None
if isinstance(value, (float, int)):
if isinstance(value, float) and (math.isnan(value) or math.isinf(value)):
return None
return value
try:
if hasattr(value, "item"):
value = value.item()
if isinstance(value, float) and (math.isnan(value) or math.isinf(value)):
return None
return value
except Exception:
pass
return value
def _sanitize_records(records: List[Dict]) -> List[Dict]:
out: List[Dict] = []
for row in records:
out.append({k: _sanitize_for_json(v) for k, v in row.items()})
return out
def _bind_in_params(prefix: str, values: List[int]) -> Tuple[str, Dict[str, int]]:
"""
Oracle não aceita bind de lista direto no IN.
Então criamos :u0,:u1,:u2... e devolvemos também o dict dos params.
"""
params: Dict[str, int] = {}
keys: List[str] = []
for i, v in enumerate(values):
k = f"{prefix}{i}"
keys.append(f":{k}")
params[k] = int(v)
return ", ".join(keys), params
def _unidades_cte(prefix: str, unidades: List[int]) -> Tuple[str, Dict[str, int]]:
"""
Pequena CTE para garantir que todas as unidades apareçam, mesmo com 0 vendas.
"""
params: Dict[str, int] = {}
parts: List[str] = []
for i, u in enumerate(unidades):
k = f"{prefix}{i}"
params[k] = int(u)
if i == 0:
parts.append(f"SELECT :{k} AS cod_und FROM dual")
else:
parts.append(f"UNION ALL SELECT :{k} FROM dual")
return "\n ".join(parts), params
# -----------------------------------------------------------------------------
# META MODE (compatibilidade com app publicado)
# -----------------------------------------------------------------------------
def _is_month_to_date_current_month(start_date: date, end_date: date) -> bool:
today = date.today()
return (
start_date.year == today.year
and start_date.month == today.month
and start_date.day == 1
and end_date == today
)
def _resolve_meta_mode(meta_mode: Optional[str], start_date: date, end_date: date) -> str:
"""
Retorna "full_month" ou "period".
- Se meta_mode vier explícito, respeita.
- Se NÃO vier, preserva comportamento do app publicado:
* quando o range for (1º do mês atual -> hoje), usa meta do mês inteiro.
- Caso contrário, usa meta do período (correto para Dia/Semana/Quinzena/Livre).
"""
if meta_mode:
mm = meta_mode.strip().lower()
if mm in ("full_month", "month", "mensal", "mes", "mês"):
return "full_month"
if mm in ("period", "range", "periodo", "período", "periodo"):
return "period"
return "full_month" if _is_month_to_date_current_month(start_date, end_date) else "period"
# -----------------------------------------------------------------------------
# MODELS
# -----------------------------------------------------------------------------
class VendasUnidadeItem(BaseModel):
cod_und: str
unidade: str
qntd_pedidos: int
valor_total: float
mta_meta: float
mta_big_meta: float
tkt_medio: float
mta_meta_tkt_medio: float
valor_total_ano_anterior: Optional[float] = None
crescimento_pct: Optional[float] = None
class VendasGrupoItem(BaseModel):
grupo: str
valor: float
# -----------------------------------------------------------------------------
# ENDPOINTS
# -----------------------------------------------------------------------------
@router.get("/unidades", response_model=List[VendasUnidadeItem])
def vendas_por_unidade(
start_date: date,
end_date: date,
meta_mode: Optional[str] = None,
current_user: UserTokenData = Depends(get_current_user),
ctx: TenantContext = Depends(get_tenant_context),
conn=Depends(get_db_conn),
):
if is_sandbox_empresa(ctx.empresa) or getattr(ctx, "sandbox", False):
return vendas_por_unidade_fake(start_date, end_date, ctx.unidades)
unidades = ctx.unidades or []
if not unidades:
return []
in_clause, in_params = _bind_in_params("u", unidades)
unidades_cte_sql, unidades_cte_params = _unidades_cte("c", unidades)
mode = _resolve_meta_mode(meta_mode, start_date, end_date)
# Observação importante:
# - full_month: meta mensal inteira (BIG_METAS)
# - period: meta do período selecionado (BIG_METAS_PESO * BIG_METAS)
#
# Isso permite corrigir Quinzena/Livre sem quebrar o comportamento legado do "Mês"
# para o app publicado (quando meta_mode não é enviado).
if mode == "full_month":
meta_cte = f"""
meta_calc AS (
SELECT
m.MTA_UNIDADE_FK_PK AS cod_und,
NVL(m.MTA_META, 0) AS MTA_META,
NVL(m.MTA_BIG_META, 0) AS MTA_BIG_META,
NVL(m.MTA_META_TKT_MEDIO, 0) AS MTA_META_TKT_MEDIO
FROM DBAUSER.BIG_METAS m
WHERE TRUNC(m.MTA_MES_ANO, 'MM') = TRUNC(:start_date, 'MM')
AND m.MTA_UNIDADE_FK_PK IN ({in_clause})
),
"""
else:
meta_cte = f"""
meta_calc AS (
SELECT
p.MTA_UNIDADE_FK_PK AS cod_und,
NVL(SUM(p.MTA_PESO * m.MTA_META), 0) AS MTA_META,
NVL(SUM(p.MTA_PESO * m.MTA_BIG_META), 0) AS MTA_BIG_META,
NVL(SUM(p.MTA_PESO * m.MTA_META_TKT_MEDIO), 0) AS MTA_META_TKT_MEDIO
FROM DBAUSER.BIG_METAS_PESO p
JOIN DBAUSER.BIG_METAS m
ON m.MTA_UNIDADE_FK_PK = p.MTA_UNIDADE_FK_PK
AND TRUNC(m.MTA_MES_ANO, 'MM') = TRUNC(p.MTA_DATA, 'MM')
WHERE p.MTA_DATA >= :start_date
AND p.MTA_DATA < (:end_date + 1)
AND p.MTA_UNIDADE_FK_PK IN ({in_clause})
GROUP BY p.MTA_UNIDADE_FK_PK
),
"""
query = f"""
WITH
unidades AS (
{unidades_cte_sql}
),
{meta_cte}
pedidos_atual AS (
SELECT
TPED_UNIDADE_FK_PK AS cod_und,
COUNT(*) AS qntd_pedidos,
SUM(TPED_VALOR_TOTAL_PEDIDO) AS valor_pedidos
FROM TPED_PEDIDO_VENDA
WHERE TPED_UNIDADE_FK_PK IN ({in_clause})
AND TPED_DATA_EMISSAO >= :start_date
AND TPED_DATA_EMISSAO < (:end_date + 1)
AND TPED_STATUS_PEDIDO = 'MA'
AND TPED_NATUREZA_MOVIMENTACAO = 'VM'
GROUP BY TPED_UNIDADE_FK_PK
UNION ALL
SELECT
TPED_UNIDADE_FK_PK AS cod_und,
COUNT(*) AS qntd_pedidos,
SUM(TPED_VALOR_TOTAL_PEDIDO) AS valor_pedidos
FROM TPED_HISTORICO_VENDA
WHERE TPED_UNIDADE_FK_PK IN ({in_clause})
AND TPED_DATA_EMISSAO >= :start_date
AND TPED_DATA_EMISSAO < (:end_date + 1)
AND TPED_STATUS_PEDIDO = 'MA'
AND TPED_NATUREZA_MOVIMENTACAO = 'VM'
GROUP BY TPED_UNIDADE_FK_PK
),
atual_agg AS (
SELECT
cod_und,
SUM(qntd_pedidos) AS qntd_pedidos,
SUM(valor_pedidos) AS valor_pedidos
FROM pedidos_atual
GROUP BY cod_und
),
descontos_atual AS (
SELECT
TMOV_UNIDADE_FK AS cod_und,
SUM(TMOV_VALOR_TOTAL) AS valor_descontos
FROM TMOV_EXTRA
WHERE TMOV_UNIDADE_FK IN ({in_clause})
AND TMOV_NATUREZA_MOVIMENTACAO = 'DC'
AND TMOV_ESTADO_MOVIMENTACAO = 'CL'
AND TMOV_APENAS_IMPRESSAO = 'N'
AND TMOV_DATA_DOCUMENTO >= :start_date
AND TMOV_DATA_DOCUMENTO < (:end_date + 1)
GROUP BY TMOV_UNIDADE_FK
),
vendas_atual AS (
SELECT
u.cod_und,
NVL(a.qntd_pedidos, 0) AS qntd_pedidos,
NVL(a.valor_pedidos, 0) - NVL(d.valor_descontos, 0) AS valor_total
FROM unidades u
LEFT JOIN atual_agg a ON a.cod_und = u.cod_und
LEFT JOIN descontos_atual d ON d.cod_und = u.cod_und
),
pedidos_aa AS (
SELECT
TPED_UNIDADE_FK_PK AS cod_und,
SUM(TPED_VALOR_TOTAL_PEDIDO) AS valor_pedidos
FROM TPED_PEDIDO_VENDA
WHERE TPED_UNIDADE_FK_PK IN ({in_clause})
AND TPED_DATA_EMISSAO >= ADD_MONTHS(:start_date, -12)
AND TPED_DATA_EMISSAO < (ADD_MONTHS(:end_date, -12) + 1)
AND TPED_STATUS_PEDIDO = 'MA'
AND TPED_NATUREZA_MOVIMENTACAO = 'VM'
GROUP BY TPED_UNIDADE_FK_PK
UNION ALL
SELECT
TPED_UNIDADE_FK_PK AS cod_und,
SUM(TPED_VALOR_TOTAL_PEDIDO) AS valor_pedidos
FROM TPED_HISTORICO_VENDA
WHERE TPED_UNIDADE_FK_PK IN ({in_clause})
AND TPED_DATA_EMISSAO >= ADD_MONTHS(:start_date, -12)
AND TPED_DATA_EMISSAO < (ADD_MONTHS(:end_date, -12) + 1)
AND TPED_STATUS_PEDIDO = 'MA'
AND TPED_NATUREZA_MOVIMENTACAO = 'VM'
GROUP BY TPED_UNIDADE_FK_PK
),
aa_agg AS (
SELECT
cod_und,
SUM(valor_pedidos) AS valor_pedidos
FROM pedidos_aa
GROUP BY cod_und
),
descontos_aa AS (
SELECT
TMOV_UNIDADE_FK AS cod_und,
SUM(TMOV_VALOR_TOTAL) AS valor_descontos
FROM TMOV_EXTRA
WHERE TMOV_UNIDADE_FK IN ({in_clause})
AND TMOV_NATUREZA_MOVIMENTACAO = 'DC'
AND TMOV_ESTADO_MOVIMENTACAO = 'CL'
AND TMOV_APENAS_IMPRESSAO = 'N'
AND TMOV_DATA_DOCUMENTO >= ADD_MONTHS(:start_date, -12)
AND TMOV_DATA_DOCUMENTO < (ADD_MONTHS(:end_date, -12) + 1)
GROUP BY TMOV_UNIDADE_FK
),
vendas_aa AS (
SELECT
u.cod_und,
NVL(a.valor_pedidos, 0) - NVL(d.valor_descontos, 0) AS valor_total_ano_anterior
FROM unidades u
LEFT JOIN aa_agg a ON a.cod_und = u.cod_und
LEFT JOIN descontos_aa d ON d.cod_und = u.cod_und
)
SELECT
u.cod_und AS COD_UND,
tu.TUND_FANTASIA AS UNIDADE,
va.qntd_pedidos AS QNTD_PEDIDOS,
va.valor_total AS VALOR_TOTAL,
NVL(mc.MTA_META, 0) AS MTA_META,
NVL(mc.MTA_BIG_META, 0) AS MTA_BIG_META,
NVL(mc.MTA_META_TKT_MEDIO, 0) AS MTA_META_TKT_MEDIO,
NVL(ROUND(va.valor_total / NULLIF(va.qntd_pedidos, 0), 2), 0) AS TKT_MEDIO,
vaa.valor_total_ano_anterior AS VALOR_TOTAL_ANO_ANTERIOR,
CASE
WHEN vaa.valor_total_ano_anterior IS NULL OR vaa.valor_total_ano_anterior = 0 THEN NULL
ELSE ROUND(((va.valor_total - vaa.valor_total_ano_anterior) / vaa.valor_total_ano_anterior) * 100, 2)
END AS CRESCIMENTO_PCT
FROM unidades u
LEFT JOIN vendas_atual va ON va.cod_und = u.cod_und
LEFT JOIN vendas_aa vaa ON vaa.cod_und = u.cod_und
LEFT JOIN TUND_UNIDADE tu ON tu.TUND_UNIDADE_PK = u.cod_und
LEFT JOIN meta_calc mc ON mc.cod_und = u.cod_und
ORDER BY VALOR_TOTAL DESC
"""
params: Dict[str, Any] = {}
params.update(in_params)
params.update(unidades_cte_params)
params.update({"start_date": start_date, "end_date": end_date})
try:
df = pd.read_sql(query, con=conn, params=params)
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Erro ao executar vendas/unidades: {exc}")
if "COD_UND" in df.columns:
df["COD_UND"] = df["COD_UND"].astype(str)
df = df.rename(
columns={
"COD_UND": "cod_und",
"UNIDADE": "unidade",
"QNTD_PEDIDOS": "qntd_pedidos",
"VALOR_TOTAL": "valor_total",
"MTA_META": "mta_meta",
"MTA_BIG_META": "mta_big_meta",
"TKT_MEDIO": "tkt_medio",
"MTA_META_TKT_MEDIO": "mta_meta_tkt_medio",
"VALOR_TOTAL_ANO_ANTERIOR": "valor_total_ano_anterior",
"CRESCIMENTO_PCT": "crescimento_pct",
}
)
return _sanitize_records(df.to_dict(orient="records"))
@router.get("/grupos", response_model=List[VendasGrupoItem])
def vendas_por_grupo(
start_date: date,
end_date: date,
current_user: UserTokenData = Depends(get_current_user),
ctx: TenantContext = Depends(get_tenant_context),
conn=Depends(get_db_conn),
):
if is_sandbox_empresa(ctx.empresa) or getattr(ctx, "sandbox", False):
return vendas_por_grupo_fake(start_date, end_date)
unidades = ctx.unidades or []
if not unidades:
return []
in_clause, in_params = _bind_in_params("u", unidades)
query = f"""
SELECT *
FROM (
SELECT
COD || ' - ' || GRUPO AS GRUPO,
SUM(VALOR) AS VALOR
FROM (
SELECT
SUM(TPED_PEDIDO_VENDA_ITEM.TPED_VALOR_LIQUIDO_ITEM) AS VALOR,
TMER_GRUPO_MERCADORIA.TMER_GRUPO_MERCADORIA_PK AS COD,
TMER_GRUPO_MERCADORIA.TMER_DESCRICAO AS GRUPO
FROM
TPED_PEDIDO_VENDA_ITEM
LEFT JOIN TPED_PEDIDO_VENDA
ON TPED_PEDIDO_VENDA.TPED_NUMERO_PEDIDO_PK = TPED_PEDIDO_VENDA_ITEM.TPED_NUMERO_PEDIDO_FK_PK
LEFT JOIN TMER_ESTOQUE
ON TMER_ESTOQUE.TMER_CODIGO_PRI_FK_PK = TPED_PEDIDO_VENDA_ITEM.TPED_CODIGO_PRI_FK_PK
LEFT JOIN TMER_MERCADORIA
ON TMER_MERCADORIA.TMER_CODIGO_PRI_PK = TMER_ESTOQUE.TMER_CODIGO_PRI_FK_PK
LEFT JOIN TMER_GRUPO_MERCADORIA
ON TMER_GRUPO_MERCADORIA.TMER_GRUPO_MERCADORIA_PK = TMER_MERCADORIA.TMER_GRUPO_MERCADORIA_FK
WHERE
TPED_PEDIDO_VENDA_ITEM.TPED_UNIDADE_FK_PK IN ({in_clause})
AND TMER_ESTOQUE.TMER_UNIDADE_FK_PK IN ({in_clause})
AND TPED_PEDIDO_VENDA.TPED_UNIDADE_FK_PK IN ({in_clause})
AND TPED_PEDIDO_VENDA.TPED_DATA_EMISSAO >= :start_date
AND TPED_PEDIDO_VENDA.TPED_DATA_EMISSAO < (:end_date + 1)
AND TPED_STATUS_PEDIDO = 'MA'
AND TPED_PEDIDO_VENDA.TPED_NATUREZA_MOVIMENTACAO = 'VM'
AND TPED_PEDIDO_VENDA_ITEM.TPED_UNIDADE_FK_PK = TMER_ESTOQUE.TMER_UNIDADE_FK_PK
AND TPED_PEDIDO_VENDA_ITEM.TPED_UNIDADE_FK_PK = TPED_PEDIDO_VENDA.TPED_UNIDADE_FK_PK
AND TMER_ESTOQUE.TMER_UNIDADE_FK_PK = TPED_PEDIDO_VENDA.TPED_UNIDADE_FK_PK
GROUP BY
TMER_GRUPO_MERCADORIA.TMER_DESCRICAO,
TMER_GRUPO_MERCADORIA.TMER_GRUPO_MERCADORIA_PK
UNION ALL
SELECT
SUM(TPED_HISTORICO_VENDA_ITEM.TPED_VALOR_LIQUIDO_ITEM) AS VALOR,
TMER_GRUPO_MERCADORIA.TMER_GRUPO_MERCADORIA_PK AS COD,
TMER_GRUPO_MERCADORIA.TMER_DESCRICAO AS GRUPO
FROM
TPED_HISTORICO_VENDA_ITEM
LEFT JOIN TPED_HISTORICO_VENDA
ON TPED_HISTORICO_VENDA.TPED_NUMERO_PEDIDO_PK = TPED_HISTORICO_VENDA_ITEM.TPED_NUMERO_PEDIDO_FK_PK
LEFT JOIN TMER_ESTOQUE
ON TMER_ESTOQUE.TMER_CODIGO_PRI_FK_PK = TPED_HISTORICO_VENDA_ITEM.TPED_CODIGO_PRI_FK_PK
LEFT JOIN TMER_MERCADORIA
ON TMER_MERCADORIA.TMER_CODIGO_PRI_PK = TMER_ESTOQUE.TMER_CODIGO_PRI_FK_PK
LEFT JOIN TMER_GRUPO_MERCADORIA
ON TMER_GRUPO_MERCADORIA.TMER_GRUPO_MERCADORIA_PK = TMER_MERCADORIA.TMER_GRUPO_MERCADORIA_FK
WHERE
TPED_HISTORICO_VENDA_ITEM.TPED_UNIDADE_FK_PK IN ({in_clause})
AND TMER_ESTOQUE.TMER_UNIDADE_FK_PK IN ({in_clause})
AND TPED_HISTORICO_VENDA.TPED_UNIDADE_FK_PK IN ({in_clause})
AND TPED_HISTORICO_VENDA.TPED_DATA_EMISSAO >= :start_date
AND TPED_HISTORICO_VENDA.TPED_DATA_EMISSAO < (:end_date + 1)
AND TPED_STATUS_PEDIDO = 'MA'
AND TPED_HISTORICO_VENDA.TPED_NATUREZA_MOVIMENTACAO = 'VM'
AND TPED_HISTORICO_VENDA_ITEM.TPED_UNIDADE_FK_PK = TMER_ESTOQUE.TMER_UNIDADE_FK_PK
AND TPED_HISTORICO_VENDA_ITEM.TPED_UNIDADE_FK_PK = TPED_HISTORICO_VENDA.TPED_UNIDADE_FK_PK
AND TMER_ESTOQUE.TMER_UNIDADE_FK_PK = TPED_HISTORICO_VENDA.TPED_UNIDADE_FK_PK
GROUP BY
TMER_GRUPO_MERCADORIA.TMER_DESCRICAO,
TMER_GRUPO_MERCADORIA.TMER_GRUPO_MERCADORIA_PK
)
GROUP BY COD || ' - ' || GRUPO
ORDER BY 2 DESC
)
WHERE ROWNUM <= 10
"""
params: Dict[str, Any] = {"start_date": start_date, "end_date": end_date}
params.update(in_params)
try:
df = pd.read_sql(query, con=conn, params=params)
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Erro ao executar vendas/grupos: {exc}")
df = df.rename(columns={"GRUPO": "grupo", "VALOR": "valor"})
return _sanitize_records(df.to_dict(orient="records"))