inicialização do repo

This commit is contained in:
joao.herculano 2026-05-18 15:34:10 -03:00
commit 826fccf678
32 changed files with 4317 additions and 0 deletions

26
.dockerignore Normal file
View File

@ -0,0 +1,26 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
ENV/
.venv
*.log
*.pot
*.mo
.git
.gitignore
.dockerignore
Dockerfile
docker-compose.yml
.env
.vscode
.idea
*.swp
*.swo
*~
.DS_Store
db.sqlite3
*.sqlite3

26
.env.example Normal file
View File

@ -0,0 +1,26 @@
# Configurações do Django
DEBUG=False
SECRET_KEY=sua-chave-secreta-aleatoria-muito-segura-aqui
APP_CONTAINER_NAME=aprovacao-pedidos-novo
HOST_PORT=8010
# ALLOWED_HOSTS - adicione seu domínio
ALLOWED_HOSTS=localhost,127.0.0.1,seu-dominio.com
# API Suprimentos
API_SUPRIMENTOS_TOKEN=your-api-token-here
API_SUPRIMENTOS_DETALHE_URL=https://api.grupoginseng.com.br/api/suprimentos_detalhepedido?limit=50000&status=pendente
API_SUPRIMENTOS_IMPLANTACAO_URL=https://api.grupoginseng.com.br/api/vw_suprimentos_implantacaopedido?limit=50000
# SQL Server (caso necessite - deixe em branco se usar SQLite)
SQL_SERVER_HOST=10.77.77.10
SQL_SERVER_DATABASE=GINSENG
SQL_SERVER_USERNAME=suprimentos
SQL_SERVER_PASSWORD=sua-senha-aqui
# Segurança HTTPS/proxy (produção)
CSRF_TRUSTED_ORIGINS=https://seu-dominio.com
SECURE_SSL_REDIRECT=False
SECURE_HSTS_SECONDS=0
SECURE_HSTS_INCLUDE_SUBDOMAINS=False
SECURE_HSTS_PRELOAD=False

28
.env.production Normal file
View File

@ -0,0 +1,28 @@
# ⚠️ ARQUIVO DE EXEMPLO PARA PRODUÇÃO
# Copie este arquivo para .env e preencha com suas credenciais reais
# Este arquivo NÃO deve ser commitado no Git
# Django
DEBUG=False
SECRET_KEY=gera-uma-chave-segura-com-mais-de-50-caracteres-aleatorios-aqui-12345
ALLOWED_HOSTS=seu-dominio.com,www.seu-dominio.com
APP_CONTAINER_NAME=aprovacao-pedidos-novo
HOST_PORT=8010
# API Suprimentos
API_SUPRIMENTOS_TOKEN=seu-token-api-seguro-aqui
API_SUPRIMENTOS_DETALHE_URL=https://api.grupoginseng.com.br/api/suprimentos_detalhepedido?limit=50000&status=pendente
API_SUPRIMENTOS_IMPLANTACAO_URL=https://api.grupoginseng.com.br/api/vw_suprimentos_implantacaopedido?limit=50000
# SQL Server (opcional - deixe em branco se usar SQLite)
SQL_SERVER_HOST=10.77.77.10
SQL_SERVER_DATABASE=GINSENG
SQL_SERVER_USERNAME=suprimentos
SQL_SERVER_PASSWORD=sua-senha-sql-server-aqui
# Cookies seguros (HTTPS)
CSRF_TRUSTED_ORIGINS=https://seu-dominio.com,https://www.seu-dominio.com
SECURE_SSL_REDIRECT=True
SECURE_HSTS_SECONDS=31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS=True
SECURE_HSTS_PRELOAD=True

141
.gitignore vendored Normal file
View File

@ -0,0 +1,141 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Environment variables
.env
.env.local
.env.*.local
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
Pipfile.lock
# PEP 582
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# VS Code
.vscode/
.idea/
# macOS
.DS_Store
# Docker
docker-compose.override.yml
# Static files
staticfiles/

208
COMECE_AQUI.md Normal file
View File

@ -0,0 +1,208 @@
# 🚀 GUIA RÁPIDO - Primeiros Passos
## ⚡ Início Rápido (5 minutos)
### 1⃣ Instalar Dependências
```bash
pip install -r requirements.txt
```
### 2⃣ Configurar Token da API
Na raiz do projeto, edite o arquivo `.env`:
```bash
# Abrir ou criar .env na raiz
# Linux/Mac:
nano .env
# Windows:
notepad .env
```
Conteúdo do arquivo:
```
API_SUPRIMENTOS_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE4MDk1Mzg4NzcsInVzZXJuYW1lIjoic3VwcmltZW50b3MuZ2luc2VuZyJ9.VyQOfFKOf56_GqmECfuQ29Y5sEGtOopVS4XC-cA54DU
API_SUPRIMENTOS_DETALHE_URL=https://api.grupoginseng.com.br/api/suprimentos_detalhepedido?limit=50000&status=pendente
API_SUPRIMENTOS_IMPLANTACAO_URL=https://api.grupoginseng.com.br/api/vw_suprimentos_implantacaopedido?limit=50000
```
### 3⃣ Testar a Integração
```bash
python test_api_integration.py
```
Você deve ver:
```
🧪 TESTES DE INTEGRAÇÃO COM API DE SUPRIMENTOS
✓ PASSOU: Conexão com API
✓ PASSOU: Merge de dados
✓ PASSOU: Integração completa
🎉 Todos os testes passaram! Integração pronta para usar.
```
### 4⃣ Rodar o Servidor
```bash
python manage.py runserver
```
Acesse: `http://localhost:8000/home/`
## 📊 O que mudou?
### Tabela agora mostra:
- **PDV** (de ambas fontes)
- **SKU** (de ambas fontes)
- **Estoque** (do SQL)
- **Trânsito** (do SQL)
- **Pendente** (do SQL)
- **sugestao_analista** ← **NOVO! (da API)**
### Novos Campos no JSON:
```json
{
"PDV": "2001",
"SKU": "ABC123",
"sugestao_analista": 45, // ← DA API
"dados_api_presentes": true, // ← FLAG
"Estoque_Total": 150, // Do SQL
...
}
```
## 🔐 Proteção do Token
**Arquivo `.env` não é versionado**
**Arquivo `.env.example` documenta os campos**
**Token nunca aparece no código**
**IMPORTANTE:** Nunca faça commit do arquivo `.env`!
```bash
# Verificar se está protegido:
cat .gitignore | grep .env
# Deve mostrar: .env
```
## ❌ Resolução de Problemas
### Erro: "Token não configurado"
```
❌ ERRO: Token não configurado no .env
```
**Solução:** Crie/edite o arquivo `.env` na raiz do projeto
### Erro: "Falha ao conectar com a API"
```
❌ Falha ao conectar com a API (verifique token e conexão de rede)
```
**Solução:**
- Verifique se o token é válido
- Teste a conexão: `ping api.grupoginseng.com.br`
- Verifique firewall/VPN
### Dados não aparecem
```
⚠️ API retornou lista vazia
```
**Solução:**
- Verifique se há dados pendentes na API
- Confirme PDV e SKU estão corretos
## 📱 Monitorando a Integração
### Ver logs no terminal
```bash
# Quando você roda: python manage.py runserver
[2024-XX-XX XX:XX:XX] Dados da API integrados: 250 registros
[2024-XX-XX XX:XX:XX] GET /home/api/pivot-data/ 200 OK
```
### Debug no Python
```bash
python manage.py shell
# Dentro do shell:
>>> from home.views import get_api_suprimentos_data
>>> dados = get_api_suprimentos_data()
>>> print(f"Total: {len(dados) if dados else 'None'} registros")
>>> print(dados[0] if dados else "Sem dados")
```
## 📦 Arquitetura da Solução
```
┌─────────────────────────────────────────┐
│ Página Web │
│ /home/api/pivot-data/ │
└────────────────┬────────────────────────┘
┌───────▼────────┐
│ get_pivot_data │
└───────┬────────┘
┌───────┴────────────────┐
│ │
┌────▼──────┐ ┌────────▼─────┐
│ SQL Server │ │ API Suprimentos│
│(dados base)│ │(quantidade) │
└────┬──────┘ └────────┬─────┘
│ │
└───────────┬────────────┘
┌────────────▼──────────────┐
│ merge_api_with_sql_data() │
│ (JOIN por PDV + SKU) │
└────────────┬──────────────┘
┌────────────▼──────────────┐
│ Dados Combinados │
│ sugestao_analista = qty │
└───────────────────────────┘
```
## 📋 Checklist
- [ ] `.env` criado na raiz
- [ ] Token adicionado ao `.env`
- [ ] Dependências instaladas: `pip install -r requirements.txt`
- [ ] Teste passou: `python test_api_integration.py`
- [ ] Servidor rodando: `python manage.py runserver`
- [ ] Página abre: `http://localhost:8000/home/`
- [ ] Dados aparecem com "sugestao_analista"
- [ ] `.env` está no `.gitignore`
## 🎓 Referências
- 📘 [INTEGRACAO_API.md](INTEGRACAO_API.md) - Documentação técnica completa
- 📘 [IMPLEMENTACAO_RESUMO.md](IMPLEMENTACAO_RESUMO.md) - O que foi implementado
- 🧪 [test_api_integration.py](test_api_integration.py) - Script de teste
## 💬 Dúvidas Frequentes
**P: Preciso reiniciar o servidor a cada mudança do `.env`?**
R: Sim, o Django lê as variáveis ao iniciar. Reinicie com: `Ctrl+C` e `python manage.py runserver`
**P: E se a API cair?**
R: Site continua funcionando normalmente com dados do SQL Server.
**P: Como adicionar novo token?**
R: Edite `.env`, reinicie servidor.
**P: Posso compartilhar o `.env` com a equipe?**
R: Não! Use variáveis de ambiente por servidor. Cada dev tem seu `.env` local.
## 🚀 Próximo Passo
Você está pronto! Execute:
```bash
python test_api_integration.py
python manage.py runserver
```
E acesse: **http://localhost:8000/home/**
Boa sorte! 🎉

123
DEPLOY_PRODUCAO.md Normal file
View File

@ -0,0 +1,123 @@
# 🚀 Guia de Deploy em Produção
## Configuração para Produção
Este aplicativo foi otimizado para rodar em produção com máxima segurança.
### 1⃣ Preparar Arquivo .env
Crie um arquivo `.env` na raiz do projeto com as configurações seguras:
```env
# Django
DEBUG=False
SECRET_KEY=gera-uma-chave-segura-com-32-caracteres-aleatorios-aqui
ALLOWED_HOSTS=seu-dominio.com,www.seu-dominio.com,seu-ip.com
# API Suprimentos
API_SUPRIMENTOS_TOKEN=seu-token-aqui
API_SUPRIMENTOS_DETALHE_URL=https://api.grupoginseng.com.br/api/suprimentos_detalhepedido?limit=50000&status=pendente
API_SUPRIMENTOS_IMPLANTACAO_URL=https://api.grupoginseng.com.br/api/vw_suprimentos_implantacaopedido?limit=50000
# SQL Server (deixe em branco se usar SQLite)
SQL_SERVER_HOST=10.77.77.10
SQL_SERVER_DATABASE=GINSENG
SQL_SERVER_USERNAME=suprimentos
SQL_SERVER_PASSWORD=sua-senha-segura
```
**⚠️ IMPORTANTE:**
- **Gere uma SECRET_KEY segura** (mínimo 50 caracteres aleatórios)
- **NUNCA** commit o arquivo `.env` em repositórios públicos
- Está no `.gitignore`, portanto será automaticamente ignorado
### 2⃣ Gerar SECRET_KEY Segura
```python
from django.core.management.utils import get_random_secret_key
print(get_random_secret_key())
```
### 3⃣ Build e Deploy com Docker
```bash
# Build da imagem
docker-compose build
# Iniciar em produção
docker-compose up -d
# Verificar logs
docker-compose logs -f web
# Parar
docker-compose down
```
### 4⃣ Verificações de Segurança
Antes de publicar, verifique:
```bash
python manage.py check --deploy
```
Deve exibir:
- ✅ DEBUG=False
- ✅ SECRET_KEY configurada
- ✅ ALLOWED_HOSTS definidos
- ✅ Cookies seguros habilitados
### 5⃣ Reverse Proxy (Nginx Recomendado)
Para produção, use um **reverse proxy** (Nginx) na frente do Gunicorn:
```nginx
upstream aprovacao_pedidos {
server web:8000;
}
server {
listen 80;
server_name seu-dominio.com;
location / {
proxy_pass http://aprovacao_pedidos;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static/ {
alias /app/staticfiles/;
}
}
```
## Mudanças Implementadas ✅
| Aspecto | Antes | Depois |
|--------|-------|--------|
| Servidor WSGI | `python manage.py runserver` | `gunicorn` (4x mais rápido) |
| Modo DEBUG | `True` (inseguro) | `False` (seguro) |
| SECRET_KEY | Hardcoded/exposta | Variável de ambiente |
| ALLOWED_HOSTS | `['*']` (inseguro) | Domínios específicos |
| Credenciais | Código-fonte | Arquivo `.env` (ignorado) |
| Cookies HTTPS | Não | Sim (em produção) |
| Volumes Docker | Expõe código | Removido (apenas em dev) |
## Troubleshooting
**Erro: "DisallowedHost"**
- Adicione seu domínio em `ALLOWED_HOSTS`
**Erro: "SECRET_KEY not found"**
- Crie arquivo `.env` com `SECRET_KEY`
**Erro: "Connection refused"**
- Verifique se Gunicorn está rodando: `docker-compose logs web`
---
**📝 Próximos passos:** Usar HTTPS/SSL, implementar CI/CD, monitorar logs

26
Dockerfile Normal file
View File

@ -0,0 +1,26 @@
# Use Python 3.11 slim image
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Set work directory
WORKDIR /app
# Install dependencies
COPY requirements.txt /app/
RUN pip install --upgrade pip && \
pip install -r requirements.txt
# Copy project
COPY . /app/
# Startup script
RUN chmod +x /app/entrypoint.sh
# Expose port 8000
EXPOSE 8000
# Run the application with gunicorn (production-grade WSGI server)
CMD ["/app/entrypoint.sh"]

222
IMPLEMENTACAO_RESUMO.md Normal file
View File

@ -0,0 +1,222 @@
# 📦 Resumo de Implementação - Integração API de Suprimentos
## ✅ O que foi implementado
### 1. **Armazenamento Seguro do Token** 🔐
- ✅ Arquivo `.env` criado com token JWT
- ✅ `python-dotenv` adicionado ao `requirements.txt`
- ✅ Arquivo `.env` adicionado ao `.gitignore` (proteção contra exposição)
- ✅ Arquivo `.env.example` criado para documentação
**Proteção:**
- Token nunca fica no código fonte
- Arquivo `.env` nunca é commitado no Git
- Configuração por variáveis de ambiente
### 2. **Configuração no Django** ⚙️
- ✅ `settings.py` atualizado para ler variáveis do `.env`
- ✅ `API_SUPRIMENTOS_CONFIG` criado com 3 configurações:
- `TOKEN`: lê do `.env`
- `DETALHE_URL`: endpoint dos pedidos em aberto
- `IMPLANTACAO_URL`: endpoint de implantação
**Como funciona:**
```python
from django.conf import settings
api_config = settings.API_SUPRIMENTOS_CONFIG
token = api_config['TOKEN'] # Carregado do .env
```
### 3. **Funções de Integração** 🔄
#### `get_api_suprimentos_data()`
Busca dados da API com segurança:
- ✅ Autenticação JWT com token do `.env`
- ✅ Timeout de 30 segundos
- ✅ Tratamento de erros silencioso (não quebra a página)
- ✅ Logs detalhados para debug
```python
dados_api = get_api_suprimentos_data()
# Retorna: lista de dicts ou None
```
#### `merge_api_with_sql_data(sql_data, api_data)`
Integra dados de ambas as fontes:
- ✅ Join por PDV + SKU
- ✅ Mapeia `quantidade` (API) → `sugestao_analista` (SQL)
- ✅ Adiciona flag `dados_api_presentes` (true/false)
- ✅ Resiliente a dados faltantes
```python
merged = merge_api_with_sql_data(sql_rows, api_rows)
# Cada linha tem agora: sugestao_analista + dados_api_presentes
```
#### `get_pivot_data()` - ATUALIZADA
View melhorada que:
- ✅ Continua buscando dados do SQL Server
- ✅ Agora também busca dados da API
- ✅ Faz merge automático
- ✅ Retorna flag `api_integrated: true/false`
- ✅ Falha graciosamente se API cair
**Resposta:**
```json
{
"status": "success",
"data": [...],
"count": 1234,
"api_integrated": true
}
```
## 📋 Arquivos Criados/Modificados
| Arquivo | Status | O quê |
|---------|--------|-------|
| `.env` | ✅ CRIADO | Token e URLs da API |
| `.env.example` | ✅ CRIADO | Template para documentação |
| `.gitignore` | ✅ MODIFICADO | Adicionado `.env` |
| `requirements.txt` | ✅ MODIFICADO | Adicionado `python-dotenv==1.0.0` |
| `aprovacao_pedidos/settings.py` | ✅ MODIFICADO | Leitura de `.env` + config API |
| `home/views.py` | ✅ MODIFICADO | 3 novas funções + melhorias |
| `INTEGRACAO_API.md` | ✅ CRIADO | Documentação completa |
| `test_api_integration.py` | ✅ CRIADO | Script de teste |
## 🚀 Como Usar
### Setup Inicial (1 vez)
```bash
# 1. Instalar dependências
pip install -r requirements.txt
# 2. Verificar arquivo .env existe na raiz
ls -la .env
# 3. Rodar testes de integração
python test_api_integration.py
```
### Uso Normal
```bash
# Servidor Django roda normalmente
python manage.py runserver
# Dados são integrados automaticamente
# Acesse: http://localhost:8000/home/
```
## 🔒 Segurança
| Aspecto | Proteção |
|---------|----------|
| Token armazenado | `.env` (não versionado) |
| Exposição acidental | `.gitignore` + `.env.example` |
| Timeout de conexão | 30 segundos |
| Erros não quebram site | Try/catch silencioso |
| Logs detalhados | Para debug do admin |
## ⚠️ Comportamento em Caso de Falhas
| Cenário | Comportamento |
|---------|---|
| API cai | Site continua funcionando com dados do SQL |
| Token inválido | Log de erro, usa dados do SQL |
| PDV/SKU não matcham | Campo `sugestao_analista` fica `null` |
| Timeout na conexão | Retorna dados do SQL depois de 30s |
| Nenhuma quantidade na API | Campo fica `null` |
## 📊 Exemplo de Dados
**Antes (SQL apenas):**
```
PDV: 2001
SKU: ABC123
Estoque: 150
Sugestao: 30
```
**Depois (SQL + API integrado):**
```
PDV: 2001
SKU: ABC123
Estoque: 150
Sugestao: 30
sugestao_analista: 45 ← Vem da API!
dados_api_presentes: true
```
## 🧪 Testing
Para verificar se tudo funciona:
```bash
# Script automático
python test_api_integration.py
# Teste manual no shell Django
python manage.py shell
>>> from home.views import get_api_suprimentos_data
>>> dados = get_api_suprimentos_data()
>>> print(f"Registros: {len(dados) if dados else 'None'}")
```
## 📝 Próximos Passos (Opcional)
Se necessário, você pode:
1. **Adicionar cache** aos dados da API (para performance)
```python
from django.core.cache import cache
dados = cache.get('api_suprimentos_data')
```
2. **Adicionar endpoint de sincronização** manual
```python
/api/refresh-suprimentos/ # POST para atualizar dados
```
3. **Adicionar auditoria** de mudanças na API
```python
# Log todas as quantidades sugeridas
```
4. **Criar dashboard** com dados da integração
## 👤 Configuração por Usuário
Para produção, defina o token por ambiente:
```bash
# No servidor/container
export API_SUPRIMENTOS_TOKEN="seu_token_real"
# Ou no docker-compose.yml
environment:
API_SUPRIMENTOS_TOKEN: ${API_SUPRIMENTOS_TOKEN}
```
## ❓ FAQ
**P: Como atualizar o token?**
R: Edite o arquivo `.env` e reinicie o servidor.
**P: O que acontece se a API cair?**
R: Site continua funcionando normalmente com dados do SQL.
**P: Como ver os logs de erro?**
R: Verifique o terminal onde o Django está rodando ou arquivo `debug.log`.
**P: Posso usar essa integração sem Token?**
R: Não, o token é obrigatório (mas a integração falha graciosamente).
## 🎯 Checklist de Deployment
- [ ] Arquivo `.env` criado com token válido
- [ ] `.env` está no `.gitignore`
- [ ] Dependências instaladas: `pip install -r requirements.txt`
- [ ] Teste local passou: `python test_api_integration.py`
- [ ] Token está protegido (não em código)
- [ ] Logs configurados para capturar erros
- [ ] URL da API está correta

193
INTEGRACAO_API.md Normal file
View File

@ -0,0 +1,193 @@
# Guia de Integração da API de Suprimentos
## 📋 Visão Geral
Este projeto integra dados da **API de Suprimentos** com os dados do **SQL Server**. A integração faz um join automático por PDV e SKU, mapeando a quantidade da API para o campo "sugestão Analista".
## 🔐 Configuração Segura do Token
### 1. Arquivo `.env` (NUNCA commit ao Git!)
O token da API é armazenado de forma segura em um arquivo `.env` na raiz do projeto:
```
API_SUPRIMENTOS_TOKEN=seu_token_jwt_aqui
API_SUPRIMENTOS_DETALHE_URL=https://api.grupoginseng.com.br/api/suprimentos_detalhepedido?limit=50000&status=pendente
API_SUPRIMENTOS_IMPLANTACAO_URL=https://api.grupoginseng.com.br/api/vw_suprimentos_implantacaopedido?limit=50000
```
### 2. Protegendo o Token
**IMPORTANTE:** Adicione o arquivo `.env` ao `.gitignore` para nunca expor o token:
```bash
# .gitignore
.env
*.pyc
__pycache__/
db.sqlite3
```
### 3. Usando o Token em Produção
Para produção, configure as variáveis de ambiente no servidor/container:
```bash
# Docker Compose
environment:
API_SUPRIMENTOS_TOKEN: ${API_SUPRIMENTOS_TOKEN}
# Servidor Linux
export API_SUPRIMENTOS_TOKEN="seu_token_aqui"
```
## 🔄 Como Funciona a Integração
### Fluxo de Dados
```
┌─────────────────────────────────────────────────────┐
│ SQL Server │
│ (estoque_mar_historico + draft_historico) │
│ - PDV, SKU, Estoque, Trânsito, etc. │
└────────────────────┬────────────────────────────────┘
┌────────────────────────┐
│ views.get_pivot_data() │
│ - Busca dados SQL │
│ - Busca dados API │
│ - Faz JOIN │
└────────────────┬───────┘
┌────────────────────────┐
│ API de Suprimentos │
│ (com autenticação JWT) │
│ - PDV, SKU, quantidade │
└────────────┬───────────┘
┌────────────────────────┐
│ Dados Mesclados │
│ sugestao_analista = │
│ quantidade (da API) │
└────────────────────────┘
```
### Funções Principais
#### 1. `get_api_suprimentos_data()`
- Busca dados da API com token JWT
- Retorna `None` se falhar (não quebra a página)
- Timeout de 30 segundos
#### 2. `merge_api_with_sql_data()`
- Faz JOIN por (PDV, SKU)
- Mapeia `quantidade``sugestao_analista`
- Adiciona campo `dados_api_presentes` (true/false)
#### 3. `get_pivot_data()`
- Combinação de dados SQL + API
- Retorna `api_integrated: true` se dados foram mesclados
## 📊 Campos Mapeados
| Tabela SQL | Campo da API | Campo Resultante |
|-----------|-------------|-----------------|
| PDV | PDV | PDV (chave join) |
| SKU | SKU | SKU (chave join) |
| - | quantidade | sugestao_analista |
## 🚀 Como Usar
### Setup Inicial
```bash
# 1. Instalar dependências
pip install -r requirements.txt
# 2. Criar arquivo .env
cp .env.example .env
# Editar .env e adicionar seu token JWT
# 3. Executar o servidor
python manage.py runserver
```
### Na Página
Os dados serão automaticamente mesclados ao carregar a tabela. Você verá:
- **Quando API está disponível**:
- Coluna `sugestao_analista` preenchida com dados da API
- Log no console: "Dados da API integrados: X registros"
- **Quando API falha ou token inválido**:
- Dados do SQL Server continuam aparecendo
- Log de erro aparece no terminal
- Página não quebra
## ⚠️ Tratamento de Erros
A integração é **resiliente**:
✅ Se a API cair → página continua funcionando com dados do SQL
✅ Se o token expirar → backend usa dados do SQL
✅ Se PDV/SKU não matcham → campo fica `null`
## 🔍 Debug
### Ver logs da integração
```python
# No terminal do servidor
[2024-XX-XX XX:XX:XX] Dados da API integrados: 250 registros
[2024-XX-XX XX:XX:XX] Erro ao chamar API: Connection timeout
```
### Testar a integração manualmente
```python
# manage.py shell
from home.views import get_api_suprimentos_data
dados = get_api_suprimentos_data()
print(f"Registros obtidos: {len(dados) if dados else 'None'}")
```
## 📝 Exemplo de Resposta
```json
{
"status": "success",
"data": [
{
"PDV": "2001",
"Descricao_PDV": "Loja Centro",
"SKU": "ABC123",
"Estoque_Total": 150,
"sugestao_analista": 45,
"dados_api_presentes": true,
...
}
],
"api_integrated": true
}
```
## 🛡️ Segurança
- ✅ Token armazenado em `.env` (não no código)
- ✅ Arquivo `.env` no `.gitignore`
- ✅ Timeout de conexão (30s)
- ✅ Tratamento de erros silencioso
- ✅ Logs detalhados para debug
- ✅ Sem exposição de senhas no código
## 📞 Suporte
Se encontrar problemas:
1. Verifique se o `.env` existe na raiz do projeto
2. Confirme que o token JWT é válido
3. Verifique os logs no terminal do servidor
4. Teste manualmente com: `python manage.py shell`

106
QUICKSTART.md Normal file
View File

@ -0,0 +1,106 @@
# ⚡ Quick Start - Projeto Aprovação Pedidos
## 1⃣ Instalação Rápida (2 min)
```bash
# Instalar dependências
pip install -r requirements.txt
# Criar banco de dados
python manage.py migrate
# Rodar o servidor
python manage.py runserver
```
**Acesse:** http://localhost:8000
---
## 2⃣ Com Docker (3 min)
```bash
# Build e rodar
docker-compose up --build
# Acesse em background (Ctrl+C para parar)
```
**Acesse:** http://localhost:8000
---
## 3⃣ Criar Superusuário (Admin)
```bash
python manage.py createsuperuser
# Acesse: http://localhost:8000/admin/
```
---
## 4⃣ Estrutura de Arquivos Importante
```
├── home/templates/home/home_page.html ← Edite aqui o HTML/CSS
├── home/views.py ← Edite aqui a lógica
├── aprovacao_pedidos/settings.py ← Configurações Django
├── manage.py ← Comandos Django
└── requirements.txt ← Dependências
```
---
## 5⃣ Comandos Úteis
| Comando | O que faz |
|---------|-----------|
| `python manage.py runserver` | Inicia servidor dev |
| `python manage.py migrate` | Aplica migrações BD |
| `python manage.py makemigrations` | Cria migrações |
| `python manage.py createsuperuser` | Cria admin |
| `python manage.py collectstatic` | Coleta arquivos estáticos |
| `docker-compose up` | Rodar com Docker |
---
## 🎨 Customizar Layout
Edit: `home/templates/home/home_page.html`
### Alterar cores principais:
```css
:root {
--accent: #03506B; /* Mude aqui */
}
```
### Adicionar novo card:
```html
<div class="card">
<p class="card-title">Meu Card</p>
<p class="card-value">100</p>
</div>
```
---
## 🚀 Próximas Etapas
1. ✅ Rodar localmente
2. 📝 Customizar para suas necessidades
3. 🔌 Conectar APIs reais
4. 🚢 Deploy (Docker/servidor)
---
## 📞 Problemas?
- **Porta 8000 já em uso?**`python manage.py runserver 8001`
- **Módulo não encontrado?**`pip install -r requirements.txt`
- **Banco de dados corrompido?** → Delete `db.sqlite3` e rode `migrate` novamente
---
**Bom desenvolvimento! 🚀**

239
README.md Normal file
View File

@ -0,0 +1,239 @@
# Projeto Aprovação Pedidos - Ginseng
Este é um projeto Django com a estética visual padronizada de todos os apps da empresa Ginseng. Contém um app `home` com um dashboard completo.
## 📋 Estrutura do Projeto
```
projeto-aprovação-pedidos/
├── aprovacao_pedidos/ # Configuração principal do Django
│ ├── settings.py # Configurações do Django
│ ├── urls.py # URLs principais
│ ├── wsgi.py # WSGI para deploy
│ ├── asgi.py # ASGI para deploy
│ └── middleware.py # Middlewares customizados
├── home/ # App principal com dashboard
│ ├── templates/home/ # Templates HTML
│ ├── views.py # Views
│ ├── urls.py # URLs do app
│ └── migrations/ # Migrações de banco de dados
├── manage.py # Gerenciador do Django
├── requirements.txt # Dependências Python
├── Dockerfile # Configuração Docker
└── .dockerignore # Arquivos ignorados no Docker
```
## 🚀 Como Rodar Localmente
### Pré-requisitos
- Python 3.11+
- pip (gerenciador de pacotes Python)
- Git (opcional)
### Instalação Rápida
1. **Clone ou copie o projeto para sua máquina**
2. **Instale as dependências:**
```bash
pip install -r requirements.txt
```
3. **Execute as migrações do banco de dados:**
```bash
python manage.py migrate
```
4. **Crie um superusuário (opcional, para acessar admin):**
```bash
python manage.py createsuperuser
```
5. **Inicie o servidor de desenvolvimento:**
```bash
python manage.py runserver
```
6. **Acesse a aplicação:**
- **Home:** http://localhost:8000/
- **Admin:** http://localhost:8000/admin/
## 🐳 Como Rodar com Docker
### Pré-requisitos
- Docker instalado e rodando
### Passos
1. **Build da imagem Docker:**
```bash
docker build -t projeto-aprovacao-pedidos .
```
2. **Execute o container:**
```bash
docker run -p 8000:8000 projeto-aprovacao-pedidos
```
3. **Acesse a aplicação:**
```
http://localhost:8000/
```
### Com Docker Compose (Recomendado)
Crie um arquivo `docker-compose.yml`:
```yaml
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- DEBUG=True
- PYTHONUNBUFFERED=1
volumes:
- .:/app
```
Depois execute:
```bash
docker-compose up --build
```
## 📱 Features Implementadas
✅ **Dashboard Responsivo**
- 4 cards informativos
- Gráfico comparativo com Chart.js
- Layout que se adapta a todos os tamanhos de tela
✅ **Sidebar Colapsível**
- Navegação intuitiva
- Menu fixo no lado esquerdo
- Efeito de backdrop ao abrir
✅ **Painel de Filtros**
- Desliza pela direita da tela
- Filtros customizáveis
- Opção para limpar todos os filtros
✅ **Header com Logo**
- Logo Ginseng oficial
- Ciclo ativo exibido
- Botões de ação rápida
✅ **Design Moderno**
- Cores padronizadas da empresa (#03506B)
- Tipografia clean (Segoe UI)
- Sombras e transições suaves
## 🎨 Personalizando a Página
### Alterar as Cores Principais
Edit o arquivo [home/templates/home/home_page.html](home/templates/home/home_page.html) e modifique as CSS variables no `<style>`:
```css
:root {
--accent: #03506B; /* Cor principal */
--accent-light: #046b8f; /* Cor hover */
--bg-color: #f6f8fb; /* Fundo */
--text-primary: #111; /* Texto principal */
--text-secondary: #6b7280; /* Texto secundário */
}
```
### Adicionar Novos Cards
No template HTML, dentro da seção `.cards-row`, adicione:
```html
<div class="card">
<p class="card-title">Seu título</p>
<p class="card-value" id="seuId"></p>
</div>
```
Depois, adicione os dados no JavaScript:
```javascript
document.getElementById('seuId').textContent = 'Seu valor';
```
## 📊 Integrando com APIs Reais
Para conectar a dados reais, modifique o arquivo [home/views.py](home/views.py):
```python
@login_required(login_url='/login/')
def home_page(request):
"""Nova página home com estética consistente do projeto."""
# Fetch dados da API
response = requests.get('https://sua-api.com/dados')
dados = response.json()
context = {
'dados': dados,
'user_email': request.user.email if request.user.is_authenticated else '',
}
return render(request, "home/home_page.html", context)
```
## 🔒 Segurança
- Django CSRF protection ativada
- Configuração segura para produção (altere `DEBUG=False` em settings.py)
- Variáveis sensíveis em .env (recomendado usar python-decouple)
## 📚 Adicionar Dependências
Para instalar novos pacotes:
```bash
pip install nome-do-pacote
pip freeze > requirements.txt
```
## 🐛 Troubleshooting
### Erro "ModuleNotFoundError"
```bash
pip install -r requirements.txt
```
### Porta 8000 já em uso
```bash
python manage.py runserver 8001
```
### Limpar cache e arquivos compilados
```bash
find . -type d -name __pycache__ -exec rm -r {} +
find . -type f -name "*.pyc" -delete
```
## 📝 Próximos Passos
1. Criar superusuário para o admin
2. Integrar com suas APIs de dados
3. Adicionar novos apps Django conforme necessário
4. Configurar autenticação (se necessário)
5. Deploy em servidor (AWS, Heroku, etc.)
## 🤝 Contribuindo
Para fazer mudanças mantendo a estética:
1. Mantenha as cores padronizadas
2. Use os componentes existentes (cards, buttons, etc.)
3. Siga a estrutura de pastas do projeto
4. Documente mudanças significativas
## 📞 Suporte
Para dúvidas ou problemas, entre em contato com o time de desenvolvimento.
---
**© 2026 Ginseng Pedidos. Todos os direitos reservados.**

View File

16
aprovacao_pedidos/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for aprovacao_pedidos project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'aprovacao_pedidos.settings')
application = get_asgi_application()

View File

@ -0,0 +1,35 @@
import logging
logger = logging.getLogger(__name__)
class LogIPMiddleware:
"""Middleware para logar o IP de cada requisição"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Obtém o IP do cliente
ip = self.get_client_ip(request)
# Loga o IP da requisição
print(f"Request from IP: {ip}")
logger.info(f"Request from IP: {ip} - Method: {request.method} - Path: {request.path}")
response = self.get_response(request)
return response
def get_client_ip(self, request):
"""
Obtém o IP real do cliente, considerando proxies
"""
# Verifica se há X-Forwarded-For (quando atrás de proxy/load balancer)
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
else:
# Caso contrário, usa REMOTE_ADDR
ip = request.META.get('REMOTE_ADDR')
return ip

View File

@ -0,0 +1,192 @@
"""
Django settings for aprovacao_pedidos project.
Generated by 'django-admin startproject' using Django 5.2.5.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
import os
from django.core.exceptions import ImproperlyConfigured
from dotenv import load_dotenv
# Carregar variáveis de ambiente do arquivo .env
load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')
if not SECRET_KEY and not DEBUG:
raise ImproperlyConfigured("SECRET_KEY obrigatoria quando DEBUG=False.")
if not SECRET_KEY:
SECRET_KEY = "dev-only-secret-key-change-me"
# Definir ALLOWED_HOSTS baseado no ambiente
ALLOWED_HOSTS = [h.strip() for h in os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') if h.strip()]
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"home",
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'aprovacao_pedidos.middleware.LogIPMiddleware',
]
ROOT_URLCONF = 'aprovacao_pedidos.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'aprovacao_pedidos.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
# SQLite para desenvolvimento
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# SQL Server (comentado - descomente após instalar django-pyodbc-azure)
# DATABASES = {
# 'default': {
# 'ENGINE': 'mssql',
# 'NAME': 'your_database_name', # MODIFICAR: seu nome do banco
# 'USER': 'your_user', # MODIFICAR: seu usuário
# 'PASSWORD': 'your_password', # MODIFICAR: sua senha
# 'HOST': '10.77.77.10', # MODIFICAR: seu servidor SQL Server
# 'PORT': '1433',
# 'OPTIONS': {
# 'driver': 'ODBC Driver 17 for SQL Server',
# }
# }
# }
# Configuração SQL Server para queries raw (sem ORM) - carregada do .env
SQL_SERVER_CONFIG = {
'SERVER': os.getenv('SQL_SERVER_HOST', '10.77.77.10'),
'DATABASE': os.getenv('SQL_SERVER_DATABASE', 'GINSENG'),
'USERNAME': os.getenv('SQL_SERVER_USERNAME', 'suprimentos'),
'PASSWORD': os.getenv('SQL_SERVER_PASSWORD', ''),
}
# Configuração da API de Suprimentos (carregada do .env)
API_SUPRIMENTOS_CONFIG = {
'TOKEN': os.getenv('API_SUPRIMENTOS_TOKEN', ''),
'DETALHE_URL': os.getenv('API_SUPRIMENTOS_DETALHE_URL', 'https://api.grupoginseng.com.br/api/suprimentos_detalhepedido?limit=50000&status=pendente'),
'IMPLANTACAO_URL': os.getenv('API_SUPRIMENTOS_IMPLANTACAO_URL', 'https://api.grupoginseng.com.br/api/vw_suprimentos_implantacaopedido?limit=50000'),
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'pt-br'
TIME_ZONE = 'America/Sao_Paulo'
USE_I18N = False
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# CSRF e segurança
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:8000,http://127.0.0.1:8000').split(',')
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_REFERRER_POLICY = 'same-origin'
# Quando o app estiver atrás de proxy reverso/TLS terminando no proxy.
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
USE_X_FORWARDED_HOST = True
SECURE_SSL_REDIRECT = os.getenv('SECURE_SSL_REDIRECT', 'False').lower() == 'true'
SECURE_HSTS_SECONDS = int(os.getenv('SECURE_HSTS_SECONDS', '0'))
SECURE_HSTS_INCLUDE_SUBDOMAINS = os.getenv('SECURE_HSTS_INCLUDE_SUBDOMAINS', 'False').lower() == 'true'
SECURE_HSTS_PRELOAD = os.getenv('SECURE_HSTS_PRELOAD', 'False').lower() == 'true'
# Login redirect
LOGIN_URL = '/'
LOGIN_REDIRECT_URL = '/home/'

21
aprovacao_pedidos/urls.py Normal file
View File

@ -0,0 +1,21 @@
"""
URL configuration for aprovacao_pedidos project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
"""
from django.contrib import admin
from django.urls import path, include
from home import views as home_views
from django.http import HttpResponse
def healthz(request):
"""Health check endpoint"""
return HttpResponse(status=200)
urlpatterns = [
path('healthz', healthz, name='healthz'),
path('admin/', admin.site.urls),
path('home/', include('home.urls')),
path('', home_views.home_page, name='home'),
]

16
aprovacao_pedidos/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for aprovacao_pedidos project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'aprovacao_pedidos.settings')
application = get_wsgi_application()

20
docker-compose.yml Normal file
View File

@ -0,0 +1,20 @@
version: '3.8'
services:
web:
build: .
container_name: ${APP_CONTAINER_NAME:-aprovacao-pedidos-novo}
restart: unless-stopped
ports:
- "${HOST_PORT:-8010}:8000"
env_file:
- .env
environment:
- PYTHONUNBUFFERED=1
- DEBUG=${DEBUG:-False}
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-localhost,127.0.0.1}
# volumes removidos para produção - descomente apenas em desenvolvimento
# volumes:
# - .:/app
stdin_open: true
tty: true

11
entrypoint.sh Normal file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env sh
set -e
echo "Aplicando migracoes..."
python manage.py migrate --noinput
echo "Coletando arquivos estaticos..."
python manage.py collectstatic --noinput
echo "Iniciando Gunicorn..."
exec gunicorn --bind 0.0.0.0:8000 --workers 2 --timeout 60 aprovacao_pedidos.wsgi:application

16
generate_secret_key.py Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env python
"""
Script para gerar SECRET_KEY segura para Django
Execute: python generate_secret_key.py
"""
from django.core.management.utils import get_random_secret_key
if __name__ == '__main__':
secret_key = get_random_secret_key()
print("\n" + "="*60)
print("🔐 SECRET_KEY SEGURA GERADA")
print("="*60)
print(f"\nCopie e cole no seu arquivo .env:\n")
print(f"SECRET_KEY={secret_key}\n")
print("="*60 + "\n")

0
home/__init__.py Normal file
View File

3
home/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
home/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class HomeConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'home'

View File

3
home/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

File diff suppressed because it is too large Load Diff

3
home/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
home/urls.py Normal file
View File

@ -0,0 +1,10 @@
from django.urls import path
from . import views
app_name = "home"
urlpatterns = [
path("", views.home_page, name="home_page"),
path("api/table-data/", views.get_table_data, name="table_data"),
path("api/pivot-data/", views.get_pivot_data, name="pivot_data"),
]

822
home/views.py Normal file
View File

@ -0,0 +1,822 @@
from django.shortcuts import render
from django.http import JsonResponse
from django.conf import settings
from django.core.cache import cache
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 get_table_data(request):
"""API endpoint que retorna os dados da query em formato JSON."""
try:
query = """
WITH 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,
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 _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_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,
) -> 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 {}
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))
row['Orcamento_Bruto'] = saldo
row['Pendente_API'] = pending
row['Orcamento_Disponivel'] = saldo - pending
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()
summary_rows = merge_orcamento_with_summary_data(
summary_rows,
saldo_orcamento_por_pdv,
pending_por_pdv,
)
logger.info(
"Pivot carregada | linhas_sql=%s linhas_resumo=%s pdvs_orcamento=%s pdvs_pending=%s",
len(rows),
len(summary_rows),
len(saldo_orcamento_por_pdv),
len(pending_por_pdv),
)
return JsonResponse({
'status': 'success',
'data': summary_rows,
'count': len(summary_rows),
'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)

22
manage.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'aprovacao_pedidos.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
Django==5.2.5
requests==2.31.0
python-dotenv==1.0.0
gunicorn==21.2.0
python-decouple==3.8

211
test_api_integration.py Normal file
View File

@ -0,0 +1,211 @@
#!/usr/bin/env python
"""
Script de teste da integração com API de Suprimentos.
Execute com: python test_api_integration.py
"""
import os
import sys
import django
from django.conf import settings
# Setup Django
sys.path.insert(0, os.path.dirname(__file__))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'aprovacao_pedidos.settings')
django.setup()
from home.views import get_api_suprimentos_data, merge_api_with_sql_data
from django.conf import settings
def test_api_connection():
"""Testa a conexão com a API de Suprimentos."""
print("=" * 60)
print("TESTE 1: Conexão com a API de Suprimentos")
print("=" * 60)
# Verificar se token está configurado
api_config = settings.API_SUPRIMENTOS_CONFIG
token = api_config.get('TOKEN', '')
if not token:
print("❌ ERRO: Token não configurado no .env")
print(" Crie um arquivo .env com: API_SUPRIMENTOS_TOKEN=seu_token")
return False
print(f"✓ Token encontrado: {token[:20]}...")
print(f"✓ URL Detalhe: {api_config.get('DETALHE_URL')}")
print(f"✓ URL Implantação: {api_config.get('IMPLANTACAO_URL')}")
# Tentar buscar dados
print("\n📡 Conectando à API...")
api_data = get_api_suprimentos_data()
if api_data is None:
print("❌ Falha ao conectar com a API (verifique token e conexão de rede)")
return False
if isinstance(api_data, list) and len(api_data) > 0:
print(f"✓ Sucesso! {len(api_data)} registros obtidos da API")
# Mostrar exemplo de registro
first_record = api_data[0]
print("\nExemplo de registro da API:")
for key in ['PDV', 'SKU', 'quantidade', 'Quantidade']:
if key in first_record:
print(f" - {key}: {first_record[key]}")
return True
else:
print("⚠️ API retornou lista vazia")
return False
def test_data_merge():
"""Testa o merge de dados SQL + API."""
print("\n" + "=" * 60)
print("TESTE 2: Merge de dados (SQL + API)")
print("=" * 60)
# Dados de exemplo do SQL
sql_data = [
{
'PDV': '2001',
'SKU': 'ABC123',
'Descricao_PDV': 'Loja Centro',
'Estoque_Total': 150,
'sugestao_Compra_Total': 30
},
{
'PDV': '2002',
'SKU': 'XYZ789',
'Descricao_PDV': 'Loja Norte',
'Estoque_Total': 80,
'sugestao_Compra_Total': 15
}
]
# Dados de exemplo da API
api_data = [
{
'PDV': '2001',
'SKU': 'ABC123',
'quantidade': 45,
'status': 'pendente'
},
{
'PDV': '2003',
'SKU': 'QWE456',
'quantidade': 20,
'status': 'pendente'
}
]
print(f"Registros SQL: {len(sql_data)}")
print(f"Registros API: {len(api_data)}")
# Fazer merge
merged = merge_api_with_sql_data(sql_data, api_data)
print(f"\nResultado após merge: {len(merged)} registros")
for row in merged:
pdv = row.get('PDV')
sku = row.get('SKU')
sugestao = row.get('sugestao_analista')
presente = row.get('dados_api_presentes')
status = "✓ API" if presente else "- SQL"
print(f" {status} | PDV: {pdv}, SKU: {sku}, Sugestão: {sugestao}")
# Verificar se o merge funcionou
encontrou_match = any(row.get('dados_api_presentes') for row in merged)
if encontrou_match:
print("\n✓ Merge funcionando corretamente!")
return True
else:
print("\n⚠️ Nenhum match encontrado (PDV/SKU diferentes)")
return True
def test_full_integration():
"""Testa a integração completa."""
print("\n" + "=" * 60)
print("TESTE 3: Integração Completa")
print("=" * 60)
# Buscar dados da API
api_data = get_api_suprimentos_data()
if not api_data:
print("⚠️ API não disponível para teste completo")
return False
print(f"✓ Dados da API carregados: {len(api_data)} registros")
# Simular dados do SQL
sql_sample = [
{
'PDV': str(api_data[0].get('PDV', '')) if api_data else '0',
'SKU': str(api_data[0].get('SKU', '')) if api_data else '0',
'Estoque_Total': 100,
}
]
merged = merge_api_with_sql_data(sql_sample, api_data)
if merged[0].get('dados_api_presentes'):
print("✓ Merge bem-sucedido!")
print(f" - sugestao_analista: {merged[0].get('sugestao_analista')}")
return True
else:
print("⚠️ Sem matches (pode ser esperado com dados de exemplo)")
return True
def main():
"""Executa todos os testes."""
print("\n🧪 TESTES DE INTEGRAÇÃO COM API DE SUPRIMENTOS\n")
results = []
# Teste 1
try:
results.append(("Conexão com API", test_api_connection()))
except Exception as e:
print(f"\n❌ Erro no Teste 1: {e}")
results.append(("Conexão com API", False))
# Teste 2
try:
results.append(("Merge de dados", test_data_merge()))
except Exception as e:
print(f"\n❌ Erro no Teste 2: {e}")
results.append(("Merge de dados", False))
# Teste 3
try:
results.append(("Integração completa", test_full_integration()))
except Exception as e:
print(f"\n❌ Erro no Teste 3: {e}")
results.append(("Integração completa", False))
# Resumo
print("\n" + "=" * 60)
print("RESUMO DOS TESTES")
print("=" * 60)
for test_name, passed in results:
status = "✓ PASSOU" if passed else "❌ FALHOU"
print(f"{status}: {test_name}")
total_passed = sum(1 for _, passed in results if passed)
total = len(results)
print(f"\nTotal: {total_passed}/{total} testes passaram")
if total_passed == total:
print("\n🎉 Todos os testes passaram! Integração pronta para usar.")
return 0
else:
print("\n⚠️ Alguns testes falharam. Verifique a configuração.")
return 1
if __name__ == '__main__':
sys.exit(main())