From 826fccf6785d10558f3a0fdf78b3478b8d4d7145 Mon Sep 17 00:00:00 2001 From: "joao.herculano" Date: Mon, 18 May 2026 15:34:10 -0300 Subject: [PATCH] =?UTF-8?q?inicializa=C3=A7=C3=A3o=20do=20repo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 26 + .env.example | 26 + .env.production | 28 + .gitignore | 141 +++ COMECE_AQUI.md | 208 ++++ DEPLOY_PRODUCAO.md | 123 +++ Dockerfile | 26 + IMPLEMENTACAO_RESUMO.md | 222 ++++ INTEGRACAO_API.md | 193 ++++ QUICKSTART.md | 106 ++ README.md | 239 +++++ aprovacao_pedidos/__init__.py | 0 aprovacao_pedidos/asgi.py | 16 + aprovacao_pedidos/middleware.py | 35 + aprovacao_pedidos/settings.py | 192 ++++ aprovacao_pedidos/urls.py | 21 + aprovacao_pedidos/wsgi.py | 16 + docker-compose.yml | 20 + entrypoint.sh | 11 + generate_secret_key.py | 16 + home/__init__.py | 0 home/admin.py | 3 + home/apps.py | 6 + home/migrations/__init__.py | 0 home/models.py | 3 + home/templates/home/home_page.html | 1567 ++++++++++++++++++++++++++++ home/tests.py | 3 + home/urls.py | 10 + home/views.py | 822 +++++++++++++++ manage.py | 22 + requirements.txt | 5 + test_api_integration.py | 211 ++++ 32 files changed, 4317 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .env.production create mode 100644 .gitignore create mode 100644 COMECE_AQUI.md create mode 100644 DEPLOY_PRODUCAO.md create mode 100644 Dockerfile create mode 100644 IMPLEMENTACAO_RESUMO.md create mode 100644 INTEGRACAO_API.md create mode 100644 QUICKSTART.md create mode 100644 README.md create mode 100644 aprovacao_pedidos/__init__.py create mode 100644 aprovacao_pedidos/asgi.py create mode 100644 aprovacao_pedidos/middleware.py create mode 100644 aprovacao_pedidos/settings.py create mode 100644 aprovacao_pedidos/urls.py create mode 100644 aprovacao_pedidos/wsgi.py create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 generate_secret_key.py create mode 100644 home/__init__.py create mode 100644 home/admin.py create mode 100644 home/apps.py create mode 100644 home/migrations/__init__.py create mode 100644 home/models.py create mode 100644 home/templates/home/home_page.html create mode 100644 home/tests.py create mode 100644 home/urls.py create mode 100644 home/views.py create mode 100644 manage.py create mode 100644 requirements.txt create mode 100644 test_api_integration.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cec8b9d --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..784e0ff --- /dev/null +++ b/.env.example @@ -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 diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..b880c86 --- /dev/null +++ b/.env.production @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a47c97 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/COMECE_AQUI.md b/COMECE_AQUI.md new file mode 100644 index 0000000..f8ca258 --- /dev/null +++ b/COMECE_AQUI.md @@ -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! 🎉 diff --git a/DEPLOY_PRODUCAO.md b/DEPLOY_PRODUCAO.md new file mode 100644 index 0000000..7faaded --- /dev/null +++ b/DEPLOY_PRODUCAO.md @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d63c7c1 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/IMPLEMENTACAO_RESUMO.md b/IMPLEMENTACAO_RESUMO.md new file mode 100644 index 0000000..6934370 --- /dev/null +++ b/IMPLEMENTACAO_RESUMO.md @@ -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 diff --git a/INTEGRACAO_API.md b/INTEGRACAO_API.md new file mode 100644 index 0000000..4c0eb85 --- /dev/null +++ b/INTEGRACAO_API.md @@ -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` diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..ca6373c --- /dev/null +++ b/QUICKSTART.md @@ -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 +
+

Meu Card

+

100

+
+``` + +--- + +## 🚀 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! 🚀** diff --git a/README.md b/README.md new file mode 100644 index 0000000..33508ff --- /dev/null +++ b/README.md @@ -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 ` + + + +
+ +
+
+ + Logo Ginseng +
+ +
+ +
+
+ + + + + +
+ + +
+

Filtros

+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ Mostrar apenas PDVs com sugestão + +
+ +
+ +
+
+ + +
+
+ +
+
+

Quantidade em Estoque

+

-

+
+ +
+

Quantidade em Trânsito

+

-

+
+ +
+

Quantidade Pendente

+

-

+
+ +
+

Sugestão Analista

+

-

+
+
+

Total Compra

+

-

+
+
+

Orçamento disponível Mês

+

-

+
+
+
+
+

Cobertura atual

+

-

+
+
+

Cobertura Est+Trans

+

-

+
+
+

Cobertura Est+Trans+Pend

+

-

+
+
+

Cobertura projetada com compra

+

-

+
+
+ + +
+

Dados de Estoque e Sugestões

+ +
+
+ +

Carregando dados agregados...

+
+
+
+
+
+ + +
+

© 2026 Ginseng Pedidos. Todos os direitos reservados.

+
+
+ + + + diff --git a/home/tests.py b/home/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/home/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/home/urls.py b/home/urls.py new file mode 100644 index 0000000..fe2f92d --- /dev/null +++ b/home/urls.py @@ -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"), +] diff --git a/home/views.py b/home/views.py new file mode 100644 index 0000000..1803ffa --- /dev/null +++ b/home/views.py @@ -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) diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..bdc3c0e --- /dev/null +++ b/manage.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..afcf461 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/test_api_integration.py b/test_api_integration.py new file mode 100644 index 0000000..23d836e --- /dev/null +++ b/test_api_integration.py @@ -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())