10 Commits

Author SHA1 Message Date
LS
911ead7835 fix: Corrige relacionamentos entre modelos na estrutura MVC
- Adiciona os modelos que faltavam na arquitetura MVC
- Corrige relacionamentos de referência cruzada entre modelos
- Atualiza script de preparação para criar arquivos __init__.py adequados
- Torna o script de preparação executável

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-04-23 08:16:53 -03:00
LS
91d9bef6c6 rebase usando pagamentos 2025-04-22 18:14:32 -03:00
andersonid
4742a888b7 tela de administração em ajustes 2025-04-22 18:13:33 -03:00
LS
6a3675b735 rebase usando pagamentos 2025-04-22 18:13:24 -03:00
LS
bb6e5c887b rebase usando pagamentos 2025-04-22 18:11:32 -03:00
LS
63ebf09fb6 refactor: renomeia pagamentos para comprovantes e adiciona novos tipos 2025-04-22 18:10:25 -03:00
andersonid
f87e03640d Correções nas mensagens de notificação 2025-04-22 18:10:25 -03:00
andersonid
debcbe6663 Corrige erro de sintaxe no template de login 2025-04-22 18:10:25 -03:00
andersonid
d45fefd72c mensagem de notificação movida para fora do card de formulario de login 2025-04-22 18:10:25 -03:00
LS
62aaec3fbe refactor: Implementa arquitetura MVC limpa
- Separa modelos em entidades individuais
- Cria camada de serviços para acesso a dados
- Implementa controladores para lógica de negócio
- Organiza rotas em blueprints por funcionalidade
- Adiciona documentação de arquitetura no README
- Cria script para preparação da estrutura

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-04-22 16:35:08 -03:00
52 changed files with 3537 additions and 1146 deletions

184
README.md
View File

@@ -1,134 +1,116 @@
# Sistema de Controle de Militantes # Sistema de Controles
Sistema para gerenciamento de militantes, células, setores e comitês regionais. Sistema de gestão para controle de militantes, pagamentos, cotas e relatórios.
## Estrutura de Permissões (RBAC) ## Arquitetura MVC
O sistema utiliza um sistema de controle de acesso baseado em papéis (RBAC) com a seguinte hierarquia: O projeto segue a arquitetura Model-View-Controller (MVC) para separação de responsabilidades:
### Níveis de Papéis ### Models
1. **Militante Básico** (Nível 1) Os modelos representam as entidades do sistema e estão organizados em:
- Visualizar próprios dados
- Editar próprios dados
- Visualizar dados da célula
2. **Secretário de Célula** (Nível 2) - **models/entities/**: Classes de entidades do banco de dados (SQLAlchemy)
- Todas as permissões do Militante Básico - `base.py`: Configuração do SQLAlchemy e classe Base
- Gerenciar membros da célula - `usuario.py`: Modelo de usuário
- Criar membros na célula - `militante.py`: Modelo de militante
- Visualizar relatórios da célula - `cota_mensal.py`: Modelo de cota mensal
- etc.
3. **Membro de Setor** (Nível 3) ### Controllers
- Todas as permissões do Secretário de Célula
- Visualizar relatórios do setor
4. **Secretário de Setor** (Nível 4) Os controladores contêm a lógica de negócio e manipulam os dados dos modelos:
- Todas as permissões do Membro de Setor
- Gerenciar células do setor
- Criar células no setor
5. **Membro de CR** (Nível 5) - **controllers/**: Implementação dos controladores
- Todas as permissões do Secretário de Setor - `auth_controller.py`: Controle de autenticação
- Visualizar relatórios do CR - `usuario_controller.py`: Operações com usuários
- `militante_controller.py`: Operações com militantes
- `home_controller.py`: Controlador da página inicial
- etc.
6. **Secretário de CR** (Nível 6) ### Views
- Todas as permissões do Membro de CR
- Gerenciar setores do CR
- Criar setores no CR
7. **Membro do CC** (Nível 7) As views são os templates que exibem os dados para o usuário:
- Todas as permissões do Secretário de CR
- Visualizar relatórios nacionais
8. **Secretário Geral** (Nível 8) - **templates/**: Templates Jinja2
- Todas as permissões do Membro do CC - Organizados por funcionalidade (admin, militantes, cotas, etc.)
- Gerenciar CRs
- Criar CRs ### Services
- Configurar sistema
Camada adicional para encapsular a lógica de acesso a dados:
- **services/**: Serviços para acesso a dados
- `database_service.py`: Gerenciamento de conexões com o banco
- `usuario_service.py`: Acesso a dados de usuários
- `militante_service.py`: Acesso a dados de militantes
- etc.
### Routes
Rotas da aplicação organizadas em blueprints:
- **routes/**: Módulos de rotas (Flask Blueprints)
- `main.py`: Rotas principais
- `auth.py`: Rotas de autenticação
- `admin.py`: Rotas administrativas
- `militante.py`: Rotas para gerenciamento de militantes
- etc.
## Instalação ## Instalação
1. Clone o repositório 1. Clone o repositório:
2. Crie um ambiente virtual:
```bash
python -m venv venv
source venv/bin/activate # Linux/Mac
# ou
venv\Scripts\activate # Windows
``` ```
git clone [URL_DO_REPOSITORIO]
```
2. Crie e ative um ambiente virtual:
```
python -m venv myenv
source myenv/bin/activate # Linux/Mac
myenv\Scripts\activate # Windows
```
3. Instale as dependências: 3. Instale as dependências:
```bash ```
pip install -r requirements.txt pip install -r requirements.txt
``` ```
4. Execute as migrações do banco de dados:
```bash 4. Inicialize o banco de dados:
python sql/migrate_db.py
``` ```
5. Configure as variáveis de ambiente no arquivo `.env`: python app.py --init
```
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=sua_chave_secreta
MAIL_SERVER=seu_servidor_smtp
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=seu_email
MAIL_PASSWORD=sua_senha
```
6. Execute o aplicativo:
```bash
flask run
``` ```
## Uso 5. Execute a aplicação:
```
python app.py
```
### Decoradores de Permissão ## Credenciais padrão
O sistema fornece três decoradores para controle de acesso: - **Administrador**:
- Usuário: admin
- Senha: admin123
1. `@require_permission(permission_name)` ## Desenvolvimento
- Verifica se o usuário tem uma permissão específica
- Exemplo: `@require_permission('create_cell_member')`
2. `@require_role(role_name)` Para adicionar novos recursos, siga a arquitetura MVC:
- Verifica se o usuário tem um papel específico
- Exemplo: `@require_role('Secretário de Célula')`
3. `@require_minimum_role(min_level)` 1. Crie modelos necessários em `models/entities/`
- Verifica se o usuário tem um papel com nível mínimo 2. Implemente serviços para acesso a dados em `services/`
- Exemplo: `@require_minimum_role(Role.SECRETARIO_CR)` 3. Crie controladores com lógica de negócio em `controllers/`
4. Adicione rotas em módulos existentes ou crie novos em `routes/`
5. Desenvolva templates em `templates/`
### Verificando Permissões no Código ## Testes
```python Execute os testes usando pytest:
# Verificar se um usuário tem uma permissão
if user.has_permission('create_cell_member'):
# Faça algo
# Verificar se um usuário tem um papel ```
if user.has_role('Secretário de Célula'): python -m pytest
# Faça algo
# Obter o papel mais alto do usuário
highest_role = user.get_highest_role()
if highest_role and highest_role.nivel >= Role.SECRETARIO_CR:
# Faça algo
``` ```
## Estrutura do Banco de Dados Ou use o script de teste:
O sistema utiliza as seguintes tabelas para o RBAC: ```
./run_tests.sh
- `roles`: Armazena os papéis disponíveis ```
- `permissions`: Armazena as permissões disponíveis
- `role_permissions`: Mapeia papéis para permissões
- `user_roles`: Mapeia usuários para papéis
## Segurança
- Todas as senhas são armazenadas com hash bcrypt
- Sessões expiram após período de inatividade
- Controle de acesso granular baseado em papéis
- Proteção contra CSRF
- Validação de entrada de dados

954
app.py

File diff suppressed because it is too large Load Diff

123
app.py.new Normal file
View File

@@ -0,0 +1,123 @@
from flask import Flask
from flask_bootstrap import Bootstrap5
from flask_mail import Mail
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect
from dotenv import load_dotenv
import os
import secrets
import logging
# Importações de configurações
from models.entities.base import Base, engine
from routes.main import main_bp
from routes.admin import admin_bp
from routes.auth import auth_bp
from routes.militante import militante_bp
from routes.pagamento import pagamento_bp
from routes.relatorio import relatorio_bp
from routes.cota import cota_bp
# Configuração do logger
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Carregar variáveis de ambiente
load_dotenv()
def create_app():
"""Factory para criação da aplicação Flask"""
app = Flask(__name__)
# Configuração secreta
app.secret_key = os.getenv('SECRET_KEY', secrets.token_hex(16))
# Configuração de Bootstrap
bootstrap = Bootstrap5(app)
# Registrar blueprints
app.register_blueprint(main_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(auth_bp)
app.register_blueprint(militante_bp)
app.register_blueprint(pagamento_bp)
app.register_blueprint(relatorio_bp)
app.register_blueprint(cota_bp)
# Configurar proteção CSRF
csrf = CSRFProtect()
csrf.init_app(app)
app.config['WTF_CSRF_CHECK_DEFAULT'] = False
app.config['WTF_CSRF_HEADERS'] = ['X-CSRFToken']
# Configurar Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
# Função para carregar usuário no login_manager
from models.entities.usuario import Usuario
from services.database_service import DatabaseService
from sqlalchemy.orm import joinedload
@login_manager.user_loader
def load_user(user_id):
"""Carrega o usuário pelo ID"""
db = DatabaseService.get_db_connection()
try:
user = db.query(Usuario).options(
joinedload(Usuario.roles)
).get(user_id)
return user
finally:
db.close()
# Adicionar filtros Jinja2
@app.template_filter('bitwise_and')
def bitwise_and(value1, value2):
"""Filtro para operação bit a bit AND"""
return value1 & value2
# Configurar Flask-Mail
app.config['MAIL_SERVER'] = os.getenv('MAIL_SERVER', 'smtp.gmail.com')
app.config['MAIL_PORT'] = int(os.getenv('MAIL_PORT', 587))
app.config['MAIL_USE_TLS'] = os.getenv('MAIL_USE_TLS', 'True').lower() == 'true'
app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD')
app.config['MAIL_DEFAULT_SENDER'] = os.getenv('MAIL_DEFAULT_SENDER')
# Inicializar Mail
mail = Mail(app)
return app
def init_system():
"""Inicializa o sistema com banco de dados e usuários padrão"""
from functions.database import init_database
# Inicializar banco de dados
logger.info("Inicializando banco de dados...")
init_database()
# Outros procedimentos de inicialização podem ser adicionados aqui
def main():
"""Inicializa e retorna a aplicação Flask"""
return create_app()
# Criar a aplicação
app = main()
if __name__ == '__main__':
import sys
# Verificar se é para inicializar o sistema
if '--init' in sys.argv:
init_system()
else:
# Executar a aplicação
app.run(
host='0.0.0.0',
port=5000,
debug=os.getenv('FLASK_ENV') == 'development'
)

View File

@@ -0,0 +1,124 @@
from flask import session, flash, redirect, url_for, request
from flask_login import login_user, logout_user, current_user
from datetime import datetime
import pyotp
import qrcode
from io import BytesIO
import base64
from models.entities.usuario import Usuario
from services.database_service import DatabaseService
class AuthController:
"""Controlador para funções de autenticação"""
@staticmethod
def login():
"""Processa o login de usuário"""
if request.method != "POST":
return False
email_or_username = request.form.get("email")
password = request.form.get("password")
otp = request.form.get("otp")
if not all([email_or_username, password]):
flash("Email/usuário e senha são obrigatórios.", "danger")
return False
db = DatabaseService.get_db_connection()
try:
# Tenta encontrar o usuário por email ou username
user = db.query(Usuario).filter(
(Usuario.email == email_or_username) |
(Usuario.username == email_or_username)
).first()
if not user or not user.check_password(password):
flash("Email/usuário ou senha incorretos.", "danger")
return False
# Verificar OTP se o usuário tiver configurado
if user.otp_secret and not otp:
flash("Código OTP é obrigatório para sua conta.", "danger")
return False
if user.otp_secret and not user.verify_otp(otp):
flash("Código OTP inválido.", "danger")
return False
# Atualizar último login
user.ultimo_login = datetime.utcnow()
db.commit()
# Fazer login e setar sessão
login_user(user)
session['user_id'] = user.id
session['username'] = user.username
session['is_admin'] = user.is_admin
return True
finally:
db.close()
@staticmethod
def logout():
"""Processa o logout de usuário"""
db = DatabaseService.get_db_connection()
try:
user = current_user
if user.is_authenticated:
user.logout()
db.commit()
logout_user()
flash('Logout realizado com sucesso!', 'success')
return True
finally:
db.close()
@staticmethod
def alterar_senha(user_id, senha_atual, nova_senha, confirmar_senha):
"""Altera a senha do usuário"""
if not all([senha_atual, nova_senha, confirmar_senha]):
flash("Todos os campos são obrigatórios.", "error")
return False
if nova_senha != confirmar_senha:
flash("As senhas não coincidem.", "error")
return False
db = DatabaseService.get_db_connection()
try:
user = db.query(Usuario).get(user_id)
if not user:
flash("Usuário não encontrado.", "error")
return False
if not user.check_password(senha_atual):
flash("Senha atual incorreta.", "error")
return False
user.set_password(nova_senha)
db.commit()
flash("Senha alterada com sucesso!", "success")
return True
finally:
db.close()
@staticmethod
def generate_qr_code(user):
"""Gera um QR code para o usuário"""
if not user.otp_secret:
user.otp_secret = pyotp.random_base32()
totp = pyotp.TOTP(user.otp_secret)
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles"))
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buffer = BytesIO()
img.save(buffer, format="PNG")
qr_code = base64.b64encode(buffer.getvalue()).decode('utf-8')
return qr_code

View File

@@ -0,0 +1,80 @@
from flask import session, render_template
from datetime import datetime
from sqlalchemy.sql import func
from models.entities.militante import Militante
from models.entities.cota_mensal import CotaMensal
from models.entities.material_vendido import MaterialVendido
from models.entities.assinatura_anual import AssinaturaAnual
from models.entities.pagamento import Pagamento
from models.entities.tipo_pagamento import TipoPagamento
from models.entities.usuario import Usuario
from services.database_service import DatabaseService
class HomeController:
"""Controlador para página inicial e dashboard"""
@staticmethod
def dashboard():
"""Gera dados para o dashboard principal"""
db = DatabaseService.get_db_connection()
try:
# Buscar nome do usuário
usuario = db.query(Usuario).get(session.get('user_id'))
nome_usuario = usuario.username if usuario else "Usuário"
# Formatar data atual em português
data_atual = datetime.now().strftime("%d de %B de %Y")
# Buscar dados para o dashboard
total_militantes = db.query(Militante).count()
total_cotas = db.query(func.sum(CotaMensal.valor_novo)).scalar() or 0
total_materiais = db.query(MaterialVendido).count()
total_assinaturas = db.query(AssinaturaAnual).count()
# Buscar últimos militantes cadastrados
ultimos_militantes = db.query(Militante)\
.order_by(Militante.id.desc())\
.limit(5)\
.all()
# Buscar últimos pagamentos
ultimos_pagamentos = db.query(Pagamento)\
.join(Militante)\
.order_by(Pagamento.data_pagamento.desc())\
.limit(5)\
.all()
# Buscar tipos de pagamento
tipos_pagamento = db.query(TipoPagamento).all()
return {
'nome_usuario': nome_usuario,
'data_atual': data_atual,
'total_militantes': total_militantes,
'total_cotas': "{:.2f}".format(total_cotas),
'total_materiais': total_materiais,
'total_assinaturas': total_assinaturas,
'ultimos_militantes': ultimos_militantes,
'ultimos_pagamentos': ultimos_pagamentos,
'tipos_pagamento': tipos_pagamento
}
except Exception as e:
print(f"Erro ao carregar dashboard: {e}")
import traceback
traceback.print_exc()
return {
'nome_usuario': "Usuário",
'data_atual': datetime.now().strftime("%d/%m/%Y"),
'total_militantes': 0,
'total_cotas': "0.00",
'total_materiais': 0,
'total_assinaturas': 0,
'ultimos_militantes': [],
'ultimos_pagamentos': [],
'tipos_pagamento': []
}
finally:
db.close()

View File

@@ -0,0 +1,270 @@
from flask import request, jsonify, flash
from datetime import datetime
from werkzeug.exceptions import NotFound
from services.militante_service import MilitanteService
from models.entities.militante import Militante, EstadoMilitante
from models.entities.endereco import Endereco
from models.entities.email_militante import EmailMilitante
from utils.date_utils import validar_data, converter_data, validar_sequencia_datas, calcular_idade
class MilitanteController:
"""Controlador para operações com militantes"""
@staticmethod
def listar_militantes():
"""Lista todos os militantes"""
return MilitanteService.listar_militantes()
@staticmethod
def buscar_militante(militante_id):
"""Busca um militante pelo ID"""
militante = MilitanteService.buscar_militante(militante_id)
if not militante:
raise NotFound(f"Militante com ID {militante_id} não encontrado")
return militante
@staticmethod
def criar_militante(form_data):
"""Cria um novo militante"""
# Validar CPF
from functions.validations import validar_cpf
cpf = form_data.get('cpf')
if not validar_cpf(cpf):
return jsonify({
'status': 'error',
'message': 'CPF inválido'
}), 400
# Verificar se já existe militante com este CPF
if MilitanteService.buscar_por_cpf(cpf):
return jsonify({
'status': 'error',
'message': 'CPF já cadastrado'
}), 400
try:
# Criar endereço
endereco = Endereco(
cep=form_data.get('cep'),
estado=form_data.get('estado'),
cidade=form_data.get('cidade'),
bairro=form_data.get('bairro'),
rua=form_data.get('logradouro'),
numero=form_data.get('numero'),
complemento=form_data.get('complemento')
)
# Salvar endereço para obter ID
endereco_id = MilitanteService.salvar_endereco(endereco)
# Processar datas
data_nascimento = datetime.strptime(form_data.get('data_nascimento'), '%Y-%m-%d') if form_data.get('data_nascimento') else None
data_entrada_oci = datetime.strptime(form_data.get('data_entrada_oci'), '%Y-%m-%d') if form_data.get('data_entrada_oci') else None
data_efetivacao_oci = datetime.strptime(form_data.get('data_efetivacao_oci'), '%Y-%m-%d') if form_data.get('data_efetivacao_oci') else None
# Criar militante
militante = Militante(
# Dados Básicos
nome=form_data.get('nome'),
cpf=cpf,
titulo_eleitoral=form_data.get('titulo_eleitoral'),
data_nascimento=data_nascimento,
data_entrada_oci=data_entrada_oci,
data_efetivacao_oci=data_efetivacao_oci,
# Contato
telefone1=form_data.get('telefone1'),
telefone2=form_data.get('telefone2'),
endereco_id=endereco_id,
# Profissional
profissao=form_data.get('profissao'),
regime_trabalho=form_data.get('regime_trabalho'),
empresa=form_data.get('empresa'),
contratante=form_data.get('contratante'),
# Acadêmico
instituicao_ensino=form_data.get('instituicao_ensino'),
tipo_instituicao=form_data.get('tipo_instituicao'),
# Sindical
sindicato=form_data.get('sindicato'),
cargo_sindical=form_data.get('cargo_sindical'),
central_sindical=form_data.get('central_sindical'),
dirigente_sindical=form_data.get('dirigente_sindical') == 'on',
# Organização
estado=EstadoMilitante(form_data.get('estado', 'ATIVO')),
celula_id=form_data.get('celula_id', type=int),
responsabilidades=form_data.get('responsabilidades', type=int, default=0),
# Por padrão, todo novo militante é aspirante
aspirante=True,
data_inicio_aspirante=datetime.now()
)
# Salvar militante para obter ID
militante_id = MilitanteService.salvar_militante(militante)
# Adicionar email principal se fornecido
email = form_data.get('email')
if email:
email_militante = EmailMilitante(
endereco_email=email,
militante_id=militante_id
)
MilitanteService.salvar_email_militante(email_militante)
return jsonify({
'status': 'success',
'message': 'Militante criado com sucesso!',
'id': militante_id
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Erro ao criar militante: {str(e)}'
}), 500
@staticmethod
def atualizar_militante(militante_id, form_data):
"""Atualiza um militante existente"""
try:
militante = MilitanteService.buscar_militante(militante_id)
if not militante:
return jsonify({
'status': 'error',
'message': 'Militante não encontrado'
}), 404
# Obter dados do formulário
nome = form_data.get('nome')
cpf = form_data.get('cpf')
titulo_eleitoral = form_data.get('titulo_eleitoral')
data_nascimento = form_data.get('data_nascimento')
data_entrada_oci = form_data.get('data_entrada_oci')
data_efetivacao_oci = form_data.get('data_efetivacao_oci')
telefone1 = form_data.get('telefone1')
telefone2 = form_data.get('telefone2')
email = form_data.get('email')
# Validar e converter datas
try:
data_nascimento = converter_data(data_nascimento) if data_nascimento else None
data_entrada_oci = converter_data(data_entrada_oci) if data_entrada_oci else None
data_efetivacao_oci = converter_data(data_efetivacao_oci) if data_efetivacao_oci else None
# Validar sequência lógica das datas
validar_sequencia_datas(
data_nascimento=data_nascimento,
data_entrada=data_entrada_oci,
data_efetivacao=data_efetivacao_oci
)
except ValueError as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 400
# Atualizar dados básicos
if nome: militante.nome = nome
if cpf: militante.cpf = cpf
if titulo_eleitoral: militante.titulo_eleitoral = titulo_eleitoral
militante.data_nascimento = data_nascimento
militante.data_entrada_oci = data_entrada_oci
militante.data_efetivacao_oci = data_efetivacao_oci
militante.telefone1 = telefone1
militante.telefone2 = telefone2
# Calcular idade
if data_nascimento:
militante.idade = calcular_idade(data_nascimento)
# Atualizar ou criar email
if email:
MilitanteService.atualizar_email_militante(militante_id, email)
# Salvar alterações
MilitanteService.salvar_militante(militante)
return jsonify({
'status': 'success',
'message': 'Militante atualizado com sucesso',
'data': {
'nome': militante.nome,
'cpf': militante.cpf,
'idade': militante.idade if hasattr(militante, 'idade') else None,
'emails': [e.endereco_email for e in militante.emails],
'telefone1': militante.telefone1,
'celula_id': str(militante.celula_id) if militante.celula_id else None,
'responsabilidades_valor': militante.responsabilidades
}
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Erro ao atualizar militante: {str(e)}'
}), 500
@staticmethod
def excluir_militante(militante_id):
"""Exclui um militante"""
try:
if MilitanteService.excluir_militante(militante_id):
flash('Militante excluído com sucesso!', 'success')
return True
else:
flash('Militante não encontrado', 'danger')
return False
except Exception as e:
flash(f'Erro ao excluir militante: {str(e)}', 'danger')
return False
@staticmethod
def buscar_dados_militante(militante_id):
"""Busca os dados de um militante específico"""
militante = MilitanteService.buscar_militante(militante_id)
if not militante:
return jsonify({
'status': 'error',
'message': 'Militante não encontrado'
}), 404
# Função auxiliar para formatar data com validação
def formatar_data_segura(data):
try:
if not data:
return None
return data.strftime('%Y-%m-%d')
except Exception as e:
print(f"Erro ao formatar data: {str(e)}, valor: {data}")
return None
# Formatar datas com validação
data_nascimento = formatar_data_segura(militante.data_nascimento)
data_entrada_oci = formatar_data_segura(militante.data_entrada_oci)
data_efetivacao_oci = formatar_data_segura(militante.data_efetivacao_oci)
return jsonify({
'status': 'success',
'id': militante.id,
'nome': militante.nome,
'cpf': militante.cpf,
'titulo_eleitoral': militante.titulo_eleitoral,
'data_nascimento': data_nascimento,
'data_entrada_oci': data_entrada_oci,
'data_efetivacao_oci': data_efetivacao_oci,
'emails': [email.endereco_email for email in militante.emails] if militante.emails else [],
'telefone1': militante.telefone1,
'telefone2': militante.telefone2,
'celula_id': militante.celula_id,
'responsabilidades_valor': militante.responsabilidades,
'sindicato': militante.sindicato,
'cargo_sindical': militante.cargo_sindical,
'central_sindical': militante.central_sindical,
'dirigente_sindical': militante.dirigente_sindical
})

View File

@@ -0,0 +1,202 @@
from flask import request, jsonify, flash, session
from flask_login import current_user
from datetime import datetime
import secrets
import pyotp
from models.entities.usuario import Usuario
from services.usuario_service import UsuarioService
from services.database_service import DatabaseService
class UsuarioController:
"""Controlador para operações com usuários"""
@staticmethod
def listar_usuarios():
"""Lista todos os usuários do sistema"""
return UsuarioService.listar_usuarios()
@staticmethod
def buscar_usuario(user_id):
"""Busca um usuário pelo ID"""
return UsuarioService.buscar_usuario(user_id)
@staticmethod
def criar_usuario(data):
"""Cria um novo usuário"""
# Verificar campos obrigatórios
required_fields = ['username', 'password', 'email']
for field in required_fields:
if field not in data:
flash(f'Campo {field} é obrigatório.', 'danger')
return False
# Verificar se usuário já existe
if UsuarioService.buscar_por_username(data['username']):
flash('Nome de usuário já existe.', 'danger')
return False
try:
# Criar usuário
usuario = Usuario(
username=data['username'],
email=data['email'],
nome=data.get('nome'),
is_admin=data.get('is_admin', False)
)
# Definir senha
usuario.set_password(data['password'])
# Gerar OTP secret
usuario.otp_secret = pyotp.random_base32()
# Definir outros campos
if 'role_id' in data:
usuario.role_id = data['role_id']
if 'setor_id' in data:
usuario.setor_id = data['setor_id']
# Salvar no banco
result = UsuarioService.salvar_usuario(usuario)
if result:
flash('Usuário cadastrado com sucesso!', 'success')
return True
else:
flash('Erro ao cadastrar usuário.', 'danger')
return False
except Exception as e:
flash(f'Erro ao cadastrar usuário: {str(e)}', 'danger')
return False
@staticmethod
def atualizar_usuario(user_id, data):
"""Atualiza um usuário existente"""
usuario = UsuarioService.buscar_usuario(user_id)
if not usuario:
flash('Usuário não encontrado.', 'danger')
return False
# Atualizar campos
if 'email' in data:
usuario.email = data['email']
if 'nome' in data:
usuario.nome = data['nome']
if 'role_id' in data:
usuario.role_id = data['role_id']
if 'setor_id' in data:
usuario.setor_id = data['setor_id']
if 'is_admin' in data:
usuario.is_admin = data['is_admin']
if 'password' in data and data['password']:
usuario.set_password(data['password'])
# Salvar alterações
result = UsuarioService.salvar_usuario(usuario)
if result:
flash('Usuário atualizado com sucesso!', 'success')
return True
else:
flash('Erro ao atualizar usuário.', 'danger')
return False
@staticmethod
def toggle_status(user_id):
"""Ativa/desativa um usuário"""
if not current_user.is_admin:
return jsonify({
'success': False,
'error': 'Você não tem permissão para alterar o status de usuários.'
}), 403
usuario = UsuarioService.buscar_usuario(user_id)
if not usuario:
return jsonify({
'success': False,
'error': 'Usuário não encontrado.'
}), 404
usuario.ativo = not usuario.ativo
if UsuarioService.salvar_usuario(usuario):
return jsonify({
'success': True,
'message': f'Usuário {\'ativado\' if usuario.ativo else \'desativado\'}'
})
else:
return jsonify({
'success': False,
'error': 'Erro ao salvar alterações.'
}), 500
@staticmethod
def reset_password(user_id):
"""Reseta a senha de um usuário"""
usuario = UsuarioService.buscar_usuario(user_id)
if not usuario:
flash('Usuário não encontrado.', 'danger')
return False, None
# Gerar nova senha
new_password = secrets.token_urlsafe(8)
usuario.set_password(new_password)
# Salvar alterações
if UsuarioService.salvar_usuario(usuario):
return True, new_password
else:
flash('Erro ao resetar senha.', 'danger')
return False, None
@staticmethod
def reset_otp(user_id):
"""Reseta o OTP de um usuário"""
usuario = UsuarioService.buscar_usuario(user_id)
if not usuario:
flash('Usuário não encontrado.', 'danger')
return False
# Gerar novo OTP secret
usuario.otp_secret = pyotp.random_base32()
# Salvar alterações
if UsuarioService.salvar_usuario(usuario):
flash(f'OTP resetado com sucesso para {usuario.email}.', 'success')
return True
else:
flash('Erro ao resetar OTP.', 'danger')
return False
@staticmethod
def check_session():
"""Verifica status da sessão"""
if 'user_id' not in session:
return {'status': 'expired'}
if 'last_activity' in session:
last_activity = datetime.fromtimestamp(session['last_activity'])
now = datetime.now()
if now - last_activity > timedelta(hours=2):
# Registrar o logout por timeout
try:
user = UsuarioService.buscar_usuario(session.get('user_id'))
if user:
user.ultimo_logout = datetime.now()
user.motivo_logout = "Timeout de sessão"
UsuarioService.salvar_usuario(user)
except Exception as e:
print(f"Erro ao registrar logout por timeout: {e}")
session.clear()
return {'status': 'expired'}
return {'status': 'active'}

View File

@@ -1,428 +0,0 @@
# Status de Implementação do Sistema de Controles
> Gerado em: 2026-03-05
> Baseado na análise do código-fonte e na demanda original.
---
## Legenda de Status
| Ícone | Significado |
|-------|-------------|
| ✅ | Implementado e funcional |
| ⚠️ | Parcialmente implementado |
| ❌ | Não implementado |
---
## Resumo Geral
| Requisito | Status | Progresso |
|-----------|--------|-----------|
| 1. Gerenciamento de Militantes | ⚠️ Parcial | 80% |
| 2. Gestão de Células e Setores | ⚠️ Parcial | 75% |
| 3. Registros de Depósitos | ⚠️ Parcial | 50% |
| 4. Cadastro de Objetivos | ❌ Não iniciado | 0% |
| 5. Relatórios Financeiros | ⚠️ Parcial | 40% |
| 6. Controle Hierárquico (RBAC) | ✅ Implementado | 90% |
| 7. Alterações de Responsáveis | ⚠️ Parcial | 60% |
| 8. Auditoria e Rastreabilidade | ❌ Não iniciado | 5% |
| 9. Segurança | ⚠️ Parcial | 70% |
| 10. Compatibilidade com Planilhas | ❌ Não iniciado | 0% |
| 11. Usabilidade / Responsividade | ⚠️ Parcial | 65% |
| 12. Carteirinha do Militante | ❌ Não iniciado | 0% |
| 13. Desacoplamento | ✅ Implementado | 90% |
---
## Detalhamento por Requisito
---
### 1. Gerenciamento de Militantes
**Status: ⚠️ Parcial (80%)**
| Funcionalidade | Status | Observações |
|----------------|--------|-------------|
| Cadastro de militantes | ✅ | Formulário completo com dados pessoais, profissionais, acadêmicos e sindicais |
| Edição de militantes | ✅ | Rota e modal de edição funcionais |
| Listagem de militantes | ✅ | Tabela com filtros e ordenação |
| Exclusão de militantes | ✅ | Com confirmação via modal |
| Níveis de permissão (8 níveis) | ✅ | RBAC hierárquico implementado com 8 níveis |
| Hierarquias de usuários | ✅ | Estrutura Célula → Setor → CR → CC |
| Status do militante (ativo, suspenso, etc.) | ✅ | 4 estados: ATIVO, DESLIGADO, SUSPENSO, AFASTADO |
| Status de Aspirante | ✅ | Período mínimo de 3 meses com avaliação obrigatória |
| Quadro-Orientador | ✅ | Responsabilidade especial implementada |
| Alterar nível do usuário | ✅ | Rota `/usuarios/<id>/alterar_nivel` |
| Avaliação do aspirante | ⚠️ | Modelo prevê, mas interface de avaliação pode estar incompleta |
| Histórico de avaliações | ⚠️ | Registro existe no modelo, verificar completude da UI |
**O que falta:**
- Verificar se a interface de avaliação do aspirante está completa com todos os campos (participação, desenvolvimento político, pontos fortes, recomendações)
---
### 2. Gestão de Células e Setores
**Status: ⚠️ Parcial (75%)**
| Funcionalidade | Status | Observações |
|----------------|--------|-------------|
| Cadastro de células | ✅ | Formulário e rotas funcionais |
| Cadastro de setores | ✅ | Formulário e rotas funcionais |
| Cadastro de CRs | ✅ | Comitês Regionais cadastráveis |
| Cadastro de CC | ✅ | Comitê Central cadastrável |
| Organização hierárquica | ✅ | Células → Setores → CRs → CC |
| Responsável geral da célula | ✅ | Campo `responsavel` no modelo |
| Responsável de finanças | ✅ | Adicionado via migration recente |
| Responsável de imprensa | ✅ | Adicionado via migration recente |
| Edição de células | ✅ | Template `editar_celula.html` existe |
| Edição de setores | ⚠️ | Template de edição de setor não foi confirmado |
| Listagem hierárquica visual | ⚠️ | Listagens existem, mas visualização em árvore pode estar incompleta |
| Atribuição de responsáveis via UI | ⚠️ | Funcionalidade adicionada no banco, verificar interface |
**O que falta:**
- Confirmar que a UI de atribuição dos responsáveis de finanças e imprensa está completa
- Verificar se a edição de setores está implementada
- Adicionar visualização hierárquica em árvore (Célula → Setor → CR → CC)
---
### 3. Registros de Depósitos
**Status: ⚠️ Parcial (50%)**
| Funcionalidade | Status | Observações |
|----------------|--------|-------------|
| Registro de cotas mensais | ✅ | Modelo `CotaMensal` com cálculo automático |
| Registro de pagamentos gerais | ✅ | Modelo `Pagamento` multi-tipo |
| Pagamento por PIX | ✅ | Modelo `TransacaoPIX` com QR code |
| Depósitos por célula | ✅ | Modelo `PagamentoCelula` |
| Depósitos por setor | ⚠️ | Rota existe, verificar completude do modelo |
| Depósitos por CR | ⚠️ | Rota existe, verificar completude do modelo |
| Valor, data e fonte do depósito | ✅ | Campos presentes nos modelos |
| Depósito vinculado a objetivo/material | ⚠️ | Parcialmente via `TipoMaterial`, mas sem `Objetivo` |
| Depósitos em eventos específicos | ❌ | Não há modelo de `Evento` com depósitos vinculados |
| Registro de coletas em plenárias | ❌ | Não implementado |
| Registro de coletas em formações | ❌ | Não implementado |
| Contribuintes permanentes (CF) | ⚠️ | Existe como tipo de pagamento, mas sem funcionalidade específica |
**O que falta:**
- Criar modelo `Evento` (plenária, formação, célula) para vincular depósitos/coletas a eventos específicos
- Implementar registro de coletas por evento com informação de "para qual objetivo"
- Separar claramente o registro de contribuintes permanentes (CF)
---
### 4. Cadastro de Objetivos
**Status: ❌ Não implementado (0%)**
| Funcionalidade | Status | Observações |
|----------------|--------|-------------|
| Modelo de Objetivos | ❌ | Não existe no banco de dados |
| Objetivos por militante | ❌ | Não implementado |
| Objetivos gerais por evento | ❌ | Não implementado |
| Objetivos de venda de materiais | ❌ | Não implementado |
| Objetivos de coleta em eventos | ❌ | Não implementado |
| Objetivos por nível (Nacional, Regional, Setor, Célula) | ❌ | Não implementado |
| Acompanhamento de progresso | ❌ | Não implementado |
| Interface de cadastro de objetivos | ❌ | Nenhum template existe |
| Relatório de atingimento de objetivos | ❌ | Não implementado |
**O que falta (tudo):**
- Criar modelo `Objetivo` com campos: tipo_evento, instancia (Célula/Setor/CR/CC/Nacional), militante (opcional), valor_meta, data_evento
- Criar rotas CRUD para objetivos
- Criar interface para cadastro e acompanhamento
- Vincular depósitos/coletas a objetivos
- Criar relatório de progresso por objetivo
---
### 5. Relatórios Financeiros
**Status: ⚠️ Parcial (40%)**
| Funcionalidade | Status | Observações |
|----------------|--------|-------------|
| Modelo de relatório de cotas | ✅ | `RelatorioCotasMensais` existe |
| Modelo de relatório de vendas | ✅ | `RelatorioVendasMateriais` existe |
| Template de criação de relatório | ✅ | Templates `novo_relatorio_*.html` existem |
| Template de listagem de relatórios | ✅ | Templates `listar_relatorios_*.html` existem |
| Lógica de geração de relatórios | ❌ | `functions/relatorio.py` está **vazio** |
| Relatório por célula | ❌ | Não implementado na lógica |
| Relatório por setor | ❌ | Não implementado na lógica |
| Relatório por CR | ❌ | Não implementado na lógica |
| Relatório por CC (nacional) | ❌ | Não implementado na lógica |
| Filtro por período | ⚠️ | Parcialmente nos templates |
| Cotas detalhadas | ⚠️ | Modelo existe, lógica de aggregação ausente |
| Contribuintes financeiros (CF) | ⚠️ | Tipo de pagamento existe, sem relatório específico |
| Contribuintes permanentes | ⚠️ | Sem segmentação clara nos relatórios |
| Coletas para eventos | ❌ | Depende do módulo de Eventos/Objetivos |
| Exportação (PDF/CSV/Excel) | ❌ | Não implementado |
**O que falta:**
- **Prioridade máxima:** Implementar `functions/relatorio.py` com toda a lógica de aggregação de dados
- Criar queries de aggregação por nível hierárquico (célula, setor, CR, CC)
- Adicionar exportação em PDF e/ou CSV
- Criar relatórios consolidados com breakdown por categoria (cotas, CF, materiais, coletas)
---
### 6. Controle Hierárquico (RBAC)
**Status: ✅ Implementado (90%)**
| Funcionalidade | Status | Observações |
|----------------|--------|-------------|
| 8 níveis de permissão | ✅ | De Militante Básico a Secretário Geral |
| Tabelas RBAC no banco | ✅ | `roles`, `permissions`, `role_permissions`, `user_roles` |
| Decorators de acesso | ✅ | `@require_login`, `@require_permission`, `@require_role`, etc. |
| Permissões por instância | ✅ | `@require_instance_permission`, `@require_instance_access` |
| Herança de permissões | ✅ | Nível superior herda permissões dos níveis abaixo |
| Filtragem de dados por hierarquia | ✅ | Queries filtradas por nível de acesso |
| Verificação de hierarquia entre usuários | ✅ | `user.is_higher_or_equal_than()` |
| Permissões de finanças e imprensa | ✅ | Roles específicos por instância |
| Testes de permissões | ✅ | `tests/test_permissions.py` existe |
**O que falta:**
- Garantir que o RBAC cobre os novos módulos a serem criados (Objetivos, Eventos, Auditoria)
---
### 7. Alterações de Responsáveis
**Status: ⚠️ Parcial (60%)**
| Funcionalidade | Status | Observações |
|----------------|--------|-------------|
| Alterar responsável de célula | ✅ | Campo `responsavel_id` editável |
| Alterar responsável financeiro | ✅ | Adicionado via migration |
| Alterar responsável de imprensa | ✅ | Adicionado via migration |
| Rota para alterar nível do usuário | ✅ | `/usuarios/<id>/alterar_nivel` |
| Registro histórico de mudanças | ❌ | Não há tabela de histórico de responsáveis |
| Interface de histórico | ❌ | Sem template para visualizar mudanças |
| Notificação ao alterar responsável | ❌ | Não implementado |
**O que falta:**
- Criar modelo `HistoricoResponsavel` com: instancia_tipo, instancia_id, responsavel_anterior_id, responsavel_novo_id, tipo_responsabilidade, data_mudanca, alterado_por_id
- Criar interface para visualizar histórico de responsáveis por instância
---
### 8. Auditoria e Rastreabilidade
**Status: ❌ Não iniciado (5%)**
| Funcionalidade | Status | Observações |
|----------------|--------|-------------|
| Log de sessões | ⚠️ | `last_activity` e `logout_reason` no modelo `Usuario` |
| Log de alterações | ❌ | Sem modelo de `AuditLog` |
| Rastrear quem fez o quê e quando | ❌ | Não implementado |
| Log de criação/edição/exclusão | ❌ | Não implementado |
| Log de alterações financeiras | ❌ | Não implementado |
| Interface de auditoria para admins | ❌ | Não implementado |
| Filtro por usuário, data, ação | ❌ | Não implementado |
**O que falta (tudo):**
- Criar modelo `AuditLog` com: user_id, acao, modelo_afetado, registro_id, dados_anteriores (JSON), dados_novos (JSON), ip_address, timestamp
- Implementar hooks automáticos nos modelos SQLAlchemy (usando `event.listen`) para capturar INSERT, UPDATE, DELETE
- Criar interface de auditoria no painel admin
- Adicionar filtros por usuário, data, entidade
---
### 9. Segurança
**Status: ⚠️ Parcial (70%)**
| Funcionalidade | Status | Observações |
|----------------|--------|-------------|
| Autenticação com usuário e senha | ✅ | bcrypt para hash de senha |
| OTP / 2FA (autenticação em dois fatores) | ✅ | pyotp + QR code implementados |
| Proteção CSRF | ✅ | Flask-WTF com tokens CSRF |
| Timeout de sessão | ✅ | 30 minutos configurável |
| Reset de OTP pelo admin | ✅ | Rota `/admin/users/<id>/reset-otp` |
| Reset de senha pelo admin | ✅ | Rota `/admin/users/<id>/reset-password` |
| Ativar/desativar usuários | ✅ | Toggle de status |
| Criptografia de dados sensíveis | ⚠️ | `cryptography` está nas dependências, uso não confirmado |
| HTTPS / TLS | ⚠️ | Dependente de configuração de produção (Nginx/gunicorn) |
| Validação de CPF | ✅ | `functions/validations.py` |
| Rate limiting (anti-brute force) | ❌ | Não implementado |
| Logs de tentativas de login | ❌ | Não implementado |
**O que falta:**
- Implementar rate limiting nas rotas de login
- Garantir que dados financeiros sensíveis (valores, CPF) estejam criptografados no banco
- Documentar configuração de HTTPS para produção
---
### 10. Compatibilidade com Planilhas dos CRs
**Status: ❌ Não iniciado (0%)**
| Funcionalidade | Status | Observações |
|----------------|--------|-------------|
| Importação de planilhas Excel | ❌ | Não implementado |
| Importação de planilhas CSV | ❌ | Não implementado |
| Exportação para Excel | ❌ | Não implementado |
| Exportação para CSV | ❌ | Não implementado |
| Mapeamento de colunas da planilha | ❌ | Não implementado |
| Validação de dados importados | ❌ | Não implementado |
| Interface de importação/exportação | ❌ | Nenhum template existe |
**O que falta (tudo):**
- Adicionar biblioteca `openpyxl` ou `pandas` para leitura/escrita de Excel
- Criar rota de importação com preview e validação antes de confirmar
- Criar rota de exportação com filtros (por período, instância, tipo)
- Documentar o formato esperado das planilhas dos CRs
---
### 11. Usabilidade e Responsividade
**Status: ⚠️ Parcial (65%)**
| Funcionalidade | Status | Observações |
|----------------|--------|-------------|
| Framework CSS responsivo | ✅ | Bootstrap via `Bootstrap-Flask` |
| Templates com layout responsivo | ✅ | `base.html` usa Bootstrap grid |
| Ordenação de tabelas | ✅ | `table_sort.js` implementado |
| Formulários com validação | ✅ | `forms.js` implementado |
| Modais para ações rápidas | ✅ | Modais para criar/editar/excluir militantes |
| Navegação por breadcrumb | ⚠️ | Verificar se está completo em todas as páginas |
| Otimização para mobile | ⚠️ | Bootstrap ajuda, mas tabelas complexas podem quebrar no mobile |
| Feedback visual de ações (toast/alert) | ⚠️ | Parcialmente implementado |
| Loading states | ⚠️ | Não confirmado |
| Suporte a múltiplos idiomas | ❌ | Sistema apenas em português (OK para o caso de uso) |
**O que falta:**
- Revisar tabelas grandes para mobile (considerar scroll horizontal ou layout card)
- Garantir feedback visual (mensagens de sucesso/erro) em todas as ações
- Testar em dispositivos móveis reais
---
### 12. Carteirinha do Militante
**Status: ❌ Não iniciado (0%)**
| Funcionalidade | Status | Observações |
|----------------|--------|-------------|
| Geração de imagem PNG | ❌ | `Pillow` está nas dependências, mas sem uso para carteirinha |
| Logo da organização na carteirinha | ❌ | Não implementado |
| Número de cadastro do militante | ❌ | Verificar se existe campo de número único |
| QR Code com chave de recuperação | ❌ | `qrcode` está nas dependências, mas sem uso para carteirinha |
| Download da carteirinha | ❌ | Não implementado |
| Geração automática no cadastro | ❌ | Não implementado |
| Template visual da carteirinha | ❌ | Não implementado |
**O que falta (tudo):**
- Verificar/criar campo de número único de cadastro no modelo `Militante`
- Criar função em `functions/` para gerar o PNG usando Pillow:
- Fundo com template da organização
- Nome do militante
- Número de cadastro
- QR code com token de recuperação de senha
- Criar rota `/militantes/<id>/carteirinha` que gera e faz download do PNG
- Adicionar botão "Baixar Carteirinha" na tela de visualização do militante
---
### 13. Desacoplamento
**Status: ✅ Implementado (90%)**
| Funcionalidade | Status | Observações |
|----------------|--------|-------------|
| Mínimo de frameworks | ✅ | Flask + SQLAlchemy + bibliotecas padrão |
| Sem dependência de cloud | ✅ | Roda localmente com SQLite |
| Containerização com Docker | ✅ | Dockerfile e docker-compose.yml presentes |
| Build automatizado | ✅ | Makefile para comandos comuns |
| Configuração via .env | ✅ | python-dotenv utilizado |
| Banco de dados portável | ✅ | SQLite sem necessidade de servidor externo |
**O que falta:**
- Avaliar se `openpyxl/pandas` (para planilhas) adicionaria dependência pesada demais
- Documentar como compilar e rodar do zero no README
---
## Backlog Priorizado
### 🔴 Alta Prioridade (bloqueadores de uso em produção)
| # | Tarefa | Requisito | Esforço |
|---|--------|-----------|---------|
| 1 | Implementar `functions/relatorio.py` com lógica de geração | Req. 5 | Alto |
| 2 | Criar módulo de Objetivos (modelo, rotas, templates) | Req. 4 | Alto |
| 3 | Criar módulo de Eventos com registro de coletas/depósitos | Req. 3 | Médio |
| 4 | Implementar Auditoria (AuditLog automático) | Req. 8 | Médio |
### 🟡 Média Prioridade (funcionalidades importantes)
| # | Tarefa | Requisito | Esforço |
|---|--------|-----------|---------|
| 5 | Histórico de mudança de responsáveis | Req. 7 | Baixo |
| 6 | Carteirinha do militante em PNG | Req. 12 | Médio |
| 7 | Exportação de relatórios para PDF/CSV | Req. 5 | Médio |
| 8 | Rate limiting no login | Req. 9 | Baixo |
### 🟢 Baixa Prioridade (melhorias e integrações)
| # | Tarefa | Requisito | Esforço |
|---|--------|-----------|---------|
| 9 | Importação/exportação de planilhas Excel | Req. 10 | Alto |
| 10 | Revisão de responsividade mobile | Req. 11 | Médio |
| 11 | Completar interface de avaliação do aspirante | Req. 1 | Baixo |
| 12 | Verificar e completar edição de setores | Req. 2 | Baixo |
---
## Arquitetura de Arquivos — O que existe
```
controles/
├── app.py ✅ 44 rotas implementadas
├── functions/
│ ├── database.py ✅ 25+ modelos SQLAlchemy
│ ├── rbac.py ✅ Sistema RBAC completo
│ ├── permissions.py ✅ Funções de checagem de permissão
│ ├── decorators.py ✅ Decorators de autorização
│ ├── auth.py ✅ Utilitários de autenticação
│ ├── cota_calculator.py ✅ Cálculo automático de cotas
│ ├── validations.py ✅ Validação de CPF
│ └── relatorio.py ❌ ARQUIVO VAZIO — precisa ser implementado
├── routes/
│ └── admin.py ✅ Rotas do painel admin
├── templates/ ✅ ~64 templates HTML
├── static/
│ └── js/ ✅ 9 módulos JavaScript
├── tests/ ✅ Testes de permissão e rotas admin
└── docs/ ✅ Documentação RBAC e hierarquia
```
---
## Novos Arquivos a Criar
| Arquivo | Propósito |
|---------|-----------|
| `functions/relatorio.py` | Lógica de geração de relatórios (já existe vazio) |
| `functions/audit.py` | Sistema de auditoria e log |
| `functions/carteirinha.py` | Geração de carteirinha em PNG |
| `routes/objetivos.py` | Blueprint para CRUD de objetivos |
| `routes/eventos.py` | Blueprint para CRUD de eventos/coletas |
| `routes/relatorios.py` | Blueprint para geração/download de relatórios |
| `templates/novo_objetivo.html` | Interface de cadastro de objetivo |
| `templates/listar_objetivos.html` | Listagem de objetivos |
| `templates/novo_evento.html` | Interface de cadastro de evento |
| `templates/listar_eventos.html` | Listagem de eventos |
| `templates/relatorio_consolidado.html` | Relatório financeiro consolidado |
| `templates/auditoria.html` | Interface de auditoria para admin |
---
*Documento gerado automaticamente com base na análise do código-fonte em 2026-03-05.*

View File

@@ -109,22 +109,26 @@ CREATE TABLE user_roles (
- `manage_cell_members`: Gerenciar membros da célula - `manage_cell_members`: Gerenciar membros da célula
- `create_cell_member`: Criar novos membros na célula - `create_cell_member`: Criar novos membros na célula
- `view_cell_reports`: Visualizar relatórios da célula - `view_cell_reports`: Visualizar relatórios da célula
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
### Permissões de Setor ### Permissões de Setor
- `manage_sector_cells`: Gerenciar células do setor - `manage_sector_cells`: Gerenciar células do setor
- `create_sector_cell`: Criar novas células no setor - `create_sector_cell`: Criar novas células no setor
- `view_sector_reports`: Visualizar relatórios do setor - `view_sector_reports`: Visualizar relatórios do setor
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
### Permissões de CR ### Permissões de CR
- `manage_cr_sectors`: Gerenciar setores do CR - `manage_cr_sectors`: Gerenciar setores do CR
- `create_cr_sector`: Criar novos setores no CR - `create_cr_sector`: Criar novos setores no CR
- `view_cr_reports`: Visualizar relatórios do CR - `view_cr_reports`: Visualizar relatórios do CR
- `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
### Permissões de CC ### Permissões de CC
- `manage_cc_crs`: Gerenciar CRs - `manage_cc_crs`: Gerenciar CRs
- `create_cc_cr`: Criar novos CRs - `create_cc_cr`: Criar novos CRs
- `view_cc_reports`: Visualizar relatórios nacionais - `view_cc_reports`: Visualizar relatórios nacionais
- `system_config`: Configurar o sistema - `system_config`: Configurar o sistema
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
## Uso no Código ## Uso no Código
@@ -166,12 +170,12 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
- `MANAGE_CELL_MEMBERS`: Gerenciar membros da célula - `MANAGE_CELL_MEMBERS`: Gerenciar membros da célula
- `VIEW_CELL_DATA`: Visualizar dados da célula - `VIEW_CELL_DATA`: Visualizar dados da célula
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula - `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
- `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula - `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
- **Tesoureiro(a)**: - **Tesoureiro(a)**:
- `VIEW_CELL_DATA`: Visualizar dados da célula - `VIEW_CELL_DATA`: Visualizar dados da célula
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula - `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
- `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula - `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
- **Militante**: - **Militante**:
- `VIEW_OWN_DATA`: Visualizar apenas seus próprios dados - `VIEW_OWN_DATA`: Visualizar apenas seus próprios dados
@@ -180,32 +184,32 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
- **Secretário(a)**: - **Secretário(a)**:
- `MANAGE_SECTOR_CELLS`: Gerenciar células do setor - `MANAGE_SECTOR_CELLS`: Gerenciar células do setor
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor - `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor - `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
- **Tesoureiro(a)**: - **Tesoureiro(a)**:
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor - `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor - `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
### CR ### CR
- **Secretário(a)**: - **Secretário(a)**:
- `MANAGE_CR_SECTORS`: Gerenciar setores do CR - `MANAGE_CR_SECTORS`: Gerenciar setores do CR
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR - `VIEW_CR_REPORTS`: Visualizar relatórios do CR
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR - `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
- **Tesoureiro(a)**: - **Tesoureiro(a)**:
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR - `VIEW_CR_REPORTS`: Visualizar relatórios do CR
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR - `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
### CC ### CC
- **Secretário(a)**: - **Secretário(a)**:
- `MANAGE_CC_CRS`: Gerenciar CRs - `MANAGE_CC_CRS`: Gerenciar CRs
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC - `VIEW_CC_REPORTS`: Visualizar relatórios do CC
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC - `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
- `SYSTEM_CONFIG`: Configurar o sistema - `SYSTEM_CONFIG`: Configurar o sistema
- **Tesoureiro(a)**: - **Tesoureiro(a)**:
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC - `VIEW_CC_REPORTS`: Visualizar relatórios do CC
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC - `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
## Regras de Acesso a Dados ## Regras de Acesso a Dados
@@ -214,10 +218,10 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
- Secretários e tesoureiros podem ver dados de sua instância - Secretários e tesoureiros podem ver dados de sua instância
- O CC tem acesso a todos os dados - O CC tem acesso a todos os dados
2. **Registro de Pagamentos**: 2. **Registro de Comprovantes**:
- Apenas tesoureiros e secretários podem registrar pagamentos - Apenas tesoureiros e secretários podem registrar comprovantes
- O registro é restrito à instância do usuário - O registro é restrito à instância do usuário
- O CC pode registrar pagamentos em qualquer nível - O CC pode registrar comprovantes em qualquer nível
## Implementação Técnica ## Implementação Técnica

View File

@@ -1,6 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text, Float
from sqlalchemy.orm import sessionmaker, relationship, backref from sqlalchemy.orm import sessionmaker, relationship, backref
import os import os
import pyotp import pyotp
@@ -187,9 +187,11 @@ class Militante(Base):
cotas_mensais = relationship("CotaMensal", back_populates="militante") cotas_mensais = relationship("CotaMensal", back_populates="militante")
pagamentos = relationship("Pagamento", back_populates="militante") pagamentos = relationship("Pagamento", back_populates="militante")
materiais_vendidos = relationship("MaterialVendido", back_populates="militante") materiais_vendidos = relationship("MaterialVendido", back_populates="militante")
vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante") vendas_jornais = relationship("VendaJornal", back_populates="militante")
assinaturas = relationship("AssinaturaAnual", back_populates="militante") assinaturas = relationship("AssinaturaJornal", back_populates="militante")
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id]) celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
comprovantes = relationship("Comprovante", back_populates="militante")
vendas_jornais_avulsos = relationship("VendaJornalAvulso", back_populates="militante")
# Constantes para responsabilidades # Constantes para responsabilidades
SECRETARIO = 1 SECRETARIO = 1
@@ -361,7 +363,7 @@ class VendaJornalAvulso(Base):
valor_total = Column(Numeric(10, 2), nullable=False) valor_total = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False) data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="vendas_jornais") militante = relationship("Militante", back_populates="vendas_jornais_avulsos")
class AssinaturaAnual(Base): class AssinaturaAnual(Base):
__tablename__ = 'assinaturas_anuais' __tablename__ = 'assinaturas_anuais'
@@ -623,8 +625,51 @@ class TransacaoPIX(Base):
status = Column(String(20)) # Pendente, Pago, Expirado status = Column(String(20)) # Pendente, Pago, Expirado
qr_code = Column(Text) qr_code = Column(Text)
pagamento_id = Column(Integer, ForeignKey('pagamentos.id')) pagamento_id = Column(Integer, ForeignKey('pagamentos.id'))
comprovante_id = Column(Integer, ForeignKey('comprovantes.id'))
pagamento = relationship("Pagamento", back_populates="transacoes_pix") pagamento = relationship("Pagamento", back_populates="transacoes_pix")
comprovante = relationship("Comprovante", back_populates="transacoes_pix")
class TipoComprovante(Base):
__tablename__ = 'tipos_comprovante'
id = Column(Integer, primary_key=True)
descricao = Column(String(50), nullable=False)
valor = Column(Float, nullable=False)
class Comprovante(Base):
__tablename__ = 'comprovantes'
id = Column(Integer, primary_key=True)
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
tipo_comprovante = Column(String(50)) # Cota, Jornal, Assinatura, etc.
data_comprovante = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="comprovantes")
transacoes_pix = relationship("TransacaoPIX", back_populates="comprovante")
class VendaJornal(Base):
__tablename__ = 'vendas_jornais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="vendas_jornais")
class AssinaturaJornal(Base):
__tablename__ = 'assinaturas_jornais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_inicio = Column(Date, nullable=False)
data_fim = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="assinaturas")
tipo_material = relationship("TipoMaterial", back_populates="assinaturas")
def init_database(): def init_database():
"""Inicializa o banco de dados com dados básicos""" """Inicializa o banco de dados com dados básicos"""

23
functions/usuario.py Normal file
View File

@@ -0,0 +1,23 @@
def get_permissoes_por_cargo(cargo_id):
permissoes = {
1: [ # Secretário Geral
'gerenciar_relatorios_celula',
'visualizar_relatorios_celula',
'gerenciar_militantes',
'gerenciar_tipos_comprovante'
],
2: [ # Admin
'gerenciar_relatorios_celula',
'visualizar_relatorios_celula',
'gerenciar_militantes',
'gerenciar_tipos_comprovante'
],
3: [ # Secretário Financeiro do Comitê Central
'gerenciar_relatorios_celula',
'visualizar_relatorios_celula',
'gerenciar_militantes',
'gerenciar_tipos_comprovante'
],
# ... existing code ...
}
return permissoes.get(cargo_id, [])

View File

@@ -0,0 +1,18 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class AssinaturaJornal(Base):
__tablename__ = 'assinaturas_jornais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_inicio = Column(Date, nullable=False)
data_fim = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="assinaturas", foreign_keys=[militante_id])
tipo_material = relationship("TipoMaterial", back_populates="assinaturas")

17
models/entities/base.py Normal file
View File

@@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship, backref
from pathlib import Path
import os
# Configurar caminho do banco de dados
db_dir = Path.home() / '.local' / 'share' / 'controles'
db_dir.mkdir(parents=True, exist_ok=True)
db_path = db_dir / 'database.db'
DATABASE_URL = f"sqlite:///{db_path}"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Base SQLAlchemy
Base = declarative_base()

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class Comprovante(Base):
__tablename__ = 'comprovantes'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
tipo_comprovante = Column(String(50)) # Cota, Jornal, Assinatura, etc.
data_comprovante = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="comprovantes")
transacoes_pix = relationship("TransacaoPIX", back_populates="comprovante")

View File

@@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date, Boolean
from sqlalchemy.orm import relationship
from models.entities.base import Base
class CotaMensal(Base):
__tablename__ = 'cotas_mensais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
valor_antigo = Column(Numeric(10, 2), nullable=False)
valor_novo = Column(Numeric(10, 2), nullable=False)
data_alteracao = Column(Date, nullable=False)
data_vencimento = Column(Date, nullable=False)
pago = Column(Boolean, default=False)
militante = relationship("Militante", back_populates="cotas_mensais")

View File

@@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from models.entities.base import Base
class EmailMilitante(Base):
__tablename__ = 'emails_militantes'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
endereco_email = Column(String(100))
# Relacionamentos
militante = relationship("Militante", back_populates="emails")

View File

@@ -0,0 +1,19 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from models.entities.base import Base
class Endereco(Base):
__tablename__ = 'enderecos'
id = Column(Integer, primary_key=True, autoincrement=True)
estado = Column(String(2))
cidade = Column(String(50))
bairro = Column(String(50))
rua = Column(String(100))
numero = Column(String(10))
complemento = Column(String(50))
cep = Column(String(9))
# Relacionamentos
militantes = relationship("Militante", back_populates="endereco")

View File

@@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class MaterialVendido(Base):
__tablename__ = 'materiais_vendidos'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
descricao = Column(String(255), nullable=False)
valor = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="materiais_vendidos")
tipo_material = relationship("TipoMaterial", back_populates="materiais_vendidos")

View File

@@ -0,0 +1,155 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Date, Text, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
import secrets
from models.entities.base import Base
class EstadoMilitante(enum.Enum):
ATIVO = 'ativo'
DESLIGADO = 'desligado'
SUSPENSO = 'suspenso'
AFASTADO = 'afastado'
class Militante(Base):
__tablename__ = 'militantes'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(100), nullable=False)
cpf = Column(String(14), unique=True)
# Novos campos básicos
titulo_eleitoral = Column(String(20))
data_nascimento = Column(Date)
data_entrada_oci = Column(Date)
data_efetivacao_oci = Column(Date)
# Campos de contato
telefone1 = Column(String(15))
telefone2 = Column(String(15))
# Relacionamento para múltiplos emails
emails = relationship("EmailMilitante", back_populates="militante")
# Endereço
endereco_id = Column(Integer, ForeignKey('enderecos.id', use_alter=True, name='fk_militante_endereco'))
endereco = relationship("Endereco", back_populates="militantes")
# Redes sociais
redes_sociais = relationship("RedeSocial", back_populates="militante")
# Campos profissionais
profissao = Column(String(100))
regime_trabalho = Column(String(50)) # CLT, Estatutário, etc.
empresa = Column(String(100))
contratante = Column(String(100)) # Para terceirizados
# Campos acadêmicos
instituicao_ensino = Column(String(100))
tipo_instituicao = Column(String(20)) # Federal, Estadual, etc.
# Campos sindicais
sindicato = Column(String(100))
cargo_sindical = Column(String(50))
dirigente_sindical = Column(Boolean)
central_sindical = Column(String(100))
# Responsável pelo cadastro
registrado_por = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_militante_registrado_por'))
# Campos existentes
celula_id = Column(Integer, ForeignKey('celulas.id', use_alter=True, name='fk_militante_celula'))
responsabilidades = Column(Integer, default=0)
otp_secret = Column(String(32))
temp_token = Column(String(64))
temp_token_expiry = Column(DateTime)
# Novo campo para Quadro-Orientador
quadro_orientador = Column(Boolean, default=False)
# Campos para Aspirante
aspirante = Column(Boolean, default=True) # Por padrão, todo novo militante é aspirante
data_inicio_aspirante = Column(DateTime, default=datetime.utcnow)
avaliacao_aspirante = Column(Text)
data_avaliacao_aspirante = Column(DateTime)
# Campos para estado do militante
estado = Column(Enum(EstadoMilitante), default=EstadoMilitante.ATIVO)
data_desligamento = Column(DateTime)
motivo_desligamento = Column(Text)
# Relacionamentos existentes
cotas_mensais = relationship("CotaMensal", back_populates="militante")
pagamentos = relationship("Pagamento", back_populates="militante")
materiais_vendidos = relationship("MaterialVendido", back_populates="militante")
vendas_jornais_avulsos = relationship("VendaJornalAvulso", back_populates="militante")
vendas_jornais = relationship("VendaJornal", back_populates="militante", foreign_keys="[VendaJornal.militante_id]")
assinaturas = relationship("AssinaturaJornal", back_populates="militante", foreign_keys="[AssinaturaJornal.militante_id]")
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
comprovantes = relationship("Comprovante", back_populates="militante")
# Constantes para responsabilidades
SECRETARIO = 1
TESOUREIRO = 2
IMPRENSA = 4
MNS = 8
MPS = 16
JUVENTUDE = 32
QUADRO_ORIENTADOR = 64
ASPIRANTE = 128
RESPONSAVEL_FINANCAS = 256
RESPONSAVEL_IMPRENSA = 512
@staticmethod
def get_responsabilidades_list():
return [
(Militante.SECRETARIO, "Secretário"),
(Militante.TESOUREIRO, "Tesoureiro"),
(Militante.IMPRENSA, "Imprensa"),
(Militante.MNS, "MNS"),
(Militante.MPS, "MPS"),
(Militante.JUVENTUDE, "Juventude"),
(Militante.QUADRO_ORIENTADOR, "Quadro-Orientador"),
(Militante.ASPIRANTE, "Aspirante"),
(Militante.RESPONSAVEL_FINANCAS, "Responsável de Finanças"),
(Militante.RESPONSAVEL_IMPRENSA, "Responsável de Imprensa")
]
def set_responsabilidades(self, resp_list):
"""
Define as responsabilidades do militante
resp_list: lista de inteiros representando as responsabilidades
"""
self.responsabilidades = sum(resp_list)
def get_responsabilidades(self):
"""
Retorna lista de responsabilidades ativas
"""
resp = []
for valor, nome in self.get_responsabilidades_list():
if self.responsabilidades & valor:
resp.append(nome)
return resp
def generate_temp_token(self):
"""
Gera um token temporário para acesso ao QR code
"""
self.temp_token = secrets.token_urlsafe(32)
self.temp_token_expiry = datetime.now() + timedelta(hours=48)
return self.temp_token
def generate_username(self):
"""Gera um nome de usuário único baseado no primeiro nome e um código"""
from sqlalchemy import func
from services.database_service import DatabaseService
db = DatabaseService.get_db_connection()
try:
# Pega o primeiro nome
primeiro_nome = self.nome.split()[0].lower()
# Importação local para evitar dependência circular
from models.entities.usuario import Usuario
# Conta quantos usuários já existem com esse prefixo
count = db.query(func.count(Usuario.id)).filter(
Usuario.username.like(f"{primeiro_nome}%")
).scalar()
# Gera o código (número sequencial)
codigo = str(count + 1).zfill(3)
return f"{primeiro_nome}{codigo}"
finally:
db.close()

View File

@@ -0,0 +1,21 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class Pagamento(Base):
__tablename__ = 'pagamentos'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo_pagamento = Column(String(50)) # Cota, Jornal, Assinatura, etc.
mes_referencia = Column(Date)
numero_jornal = Column(String(20))
numero_inicial_assinatura = Column(String(20))
numero_final_assinatura = Column(String(20))
campanha_financeira = Column(String(50))
valor = Column(Numeric(10, 2), nullable=False)
data_pagamento = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="pagamentos")
transacoes_pix = relationship("TransacaoPIX", back_populates="pagamento")

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from models.entities.base import Base
class RedeSocial(Base):
__tablename__ = 'redes_sociais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo = Column(String(20)) # Instagram, TikTok, Discord, etc.
identificador = Column(String(100))
# Relacionamentos
militante = relationship("Militante", back_populates="redes_sociais")

View File

@@ -0,0 +1,13 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from models.entities.base import Base
class TipoMaterial(Base):
__tablename__ = 'tipos_materiais'
id = Column(Integer, primary_key=True, autoincrement=True)
descricao = Column(String(100), nullable=False)
materiais_vendidos = relationship("MaterialVendido", back_populates="tipo_material")
assinaturas = relationship("AssinaturaJornal", back_populates="tipo_material")

126
models/entities/usuario.py Normal file
View File

@@ -0,0 +1,126 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship, backref
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime, timedelta
from flask_login import UserMixin
import pyotp
from models.entities.base import Base
from models.entities.militante import Militante
class TipoUsuario(enum.Enum):
ADMIN = "admin"
CR_RESPONSAVEL = "cr_responsavel"
SETOR_RESPONSAVEL = "setor_responsavel"
USUARIO = "usuario"
class Usuario(Base, UserMixin):
__tablename__ = 'usuarios'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
email = Column(String(100), unique=True, nullable=False)
nome = Column(String(100)) # Nome completo do usuário
otp_secret = Column(String(32))
role_id = Column(Integer, ForeignKey('roles.id'))
setor_id = Column(Integer, ForeignKey('setores.id'))
ativo = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False)
ultimo_login = Column(DateTime)
ultimo_logout = Column(DateTime)
motivo_logout = Column(String(100))
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
celula_id = Column(Integer, ForeignKey('celulas.id'))
session_timeout = Column(Integer, default=30)
tipo = Column(String(17), nullable=False)
ultima_atividade = Column(DateTime, default=datetime.utcnow)
# Relacionamento com militante
militante_id = Column(Integer, ForeignKey('militantes.id'))
# Relacionamentos
roles = relationship("Role", secondary="user_roles", back_populates="users")
setor = relationship('Setor', back_populates='usuarios')
cr = relationship('ComiteRegional', back_populates='usuarios')
celula = relationship('Celula', back_populates='usuarios')
militante = relationship("Militante", backref=backref("usuario", uselist=False))
def __init__(self, username, email=None, is_admin=False, nome=None):
self.username = username
self.email = email
self.is_admin = is_admin
self.nome = nome
self.ativo = True
self.session_timeout = 30
self.tipo = "USUARIO"
self.ultima_atividade = datetime.utcnow()
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def update_last_activity(self):
self.ultima_atividade = datetime.utcnow()
def is_session_expired(self):
if not self.ultima_atividade:
return True
time_diff = datetime.utcnow() - self.ultima_atividade
return time_diff.total_seconds() > (self.session_timeout * 60)
def check_session_timeout(self):
"""Verifica se a sessão do usuário expirou"""
if not self.ultima_atividade:
return True
time_diff = datetime.utcnow() - self.ultima_atividade
return time_diff.total_seconds() > (self.session_timeout * 60)
def has_permission(self, permission_name):
"""Verifica se o usuário tem uma permissão específica"""
if self.is_admin: # Se for admin, tem todas as permissões
return True
# Verifica se o usuário tem a permissão através de suas roles
for role in self.roles:
for permission in role.permissions:
if permission.nome == permission_name:
return True
return False
def has_role(self, role_nivel):
"""Verifica se o usuário tem um determinado nível de role"""
for role in self.roles:
if role.nivel == role_nivel:
return True
return False
def get_otp_uri(self):
"""Gera a URI para autenticação em duas etapas"""
if not self.otp_secret:
self.otp_secret = pyotp.random_base32()
return pyotp.totp.TOTP(self.otp_secret).provisioning_uri(
self.username,
issuer_name="Sistema de Controles"
)
def verify_otp(self, code):
"""Verifica se um código OTP é válido"""
if not self.otp_secret:
print(f"Erro: OTP secret não configurado para o usuário {self.username}")
return False
totp = pyotp.totp.TOTP(self.otp_secret)
is_valid = totp.verify(code)
return is_valid
def logout(self):
"""Registra o logout do usuário"""
self.ultimo_logout = datetime.utcnow()
self.motivo_logout = "Logout manual"
self.ultima_atividade = None
def is_admin_user(self):
"""Verifica se o usuário é admin"""
return self.is_admin or any(role.nome == "admin" for role in self.roles)

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class VendaJornal(Base):
__tablename__ = 'vendas_jornais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="vendas_jornais", foreign_keys=[militante_id])

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class VendaJornalAvulso(Base):
__tablename__ = 'vendas_jornais_avulsos'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="vendas_jornais_avulsos")

View File

@@ -8,7 +8,7 @@ Werkzeug==3.0.1
python-dotenv==1.0.1 python-dotenv==1.0.1
pyotp==2.9.0 pyotp==2.9.0
qrcode==7.4.2 qrcode==7.4.2
Pillow==10.2.0 Pillow==9.5.0
email-validator==2.1.0.post1 email-validator==2.1.0.post1
cryptography==42.0.2 cryptography==42.0.2
bcrypt==4.1.2 bcrypt==4.1.2

View File

@@ -9,7 +9,10 @@ import secrets
from functools import wraps from functools import wraps
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
import logging import logging
<<<<<<< HEAD
from datetime import datetime from datetime import datetime
=======
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -49,8 +52,12 @@ def dashboard():
total_users=total_users, total_users=total_users,
active_users=active_users, active_users=active_users,
inactive_users=inactive_users, inactive_users=inactive_users,
<<<<<<< HEAD
users=users, users=users,
now=now now=now
=======
users=users
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
) )
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"Erro ao buscar dados do dashboard: {str(e)}") logger.error(f"Erro ao buscar dados do dashboard: {str(e)}")

61
routes/auth.py Normal file
View File

@@ -0,0 +1,61 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify
from flask_login import login_required, current_user
from controllers.auth_controller import AuthController
from services.database_service import DatabaseService
from models.entities.usuario import Usuario
auth_bp = Blueprint('auth', __name__)
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
"""Rota de login"""
if request.method == "POST":
# Processar o login através do controlador
if AuthController.login():
# Redirecionar para home em caso de sucesso
return redirect(url_for("home"))
# GET ou falha no login, renderizar template
return render_template("login.html")
@auth_bp.route("/logout")
@login_required
def logout():
"""Rota de logout"""
AuthController.logout()
return redirect(url_for('auth.login'))
@auth_bp.route("/alterar_senha", methods=["GET", "POST"])
@login_required
def alterar_senha():
"""Rota para alterar a senha do usuário"""
if request.method == "POST":
senha_atual = request.form.get("senha_atual")
nova_senha = request.form.get("nova_senha")
confirmar_senha = request.form.get("confirmar_senha")
if AuthController.alterar_senha(current_user.id, senha_atual, nova_senha, confirmar_senha):
return redirect(url_for("home"))
return render_template("alterar_senha.html")
@auth_bp.route("/qr/<token>")
def get_qr_code(token):
"""Rota para exibir QR code para configuração 2FA"""
db = DatabaseService.get_db_connection()
try:
user = db.query(Usuario).filter_by(username='admin').first()
if not user:
flash('Usuário não encontrado', 'error')
return redirect(url_for('auth.login'))
qr_uri = user.get_otp_uri()
return render_template('mostrar_qr_code.html', qr_uri=qr_uri)
finally:
db.close()
@auth_bp.route('/check_session')
def check_session():
"""Rota para verificar status da sessão via AJAX"""
return jsonify(AuthController.check_session())

123
routes/cota.py Normal file
View File

@@ -0,0 +1,123 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, flash
from flask_login import login_required
from models.entities.cota_mensal import CotaMensal
from services.cota_service import CotaService
from services.militante_service import MilitanteService
from functions.decorators import require_permission
from utils.date_utils import validar_data, converter_data
cota_bp = Blueprint('cota', __name__, url_prefix='/cotas')
@cota_bp.route("/")
@login_required
@require_permission('gerenciar_cotas')
def listar():
"""Lista todas as cotas mensais"""
cotas = CotaService.listar_cotas()
# Calcular status de cada cota
for cota in cotas:
if cota.pago:
cota.status = "paga"
elif cota.data_vencimento < datetime.now().date():
cota.status = "atrasada"
else:
cota.status = "pendente"
militantes = MilitanteService.listar_militantes()
return render_template("listar_cotas.html", cotas=cotas, militantes=militantes)
@cota_bp.route("/novo", methods=["GET", "POST"])
@login_required
@require_permission('gerenciar_cotas')
def nova():
"""Cria uma nova cota"""
if request.method == "POST":
militante_id = request.form.get("militante_id")
valor_antigo = float(request.form.get("valor_antigo"))
valor_novo = float(request.form.get("valor_novo"))
data_alteracao = converter_data(request.form.get("data_alteracao"))
data_vencimento = converter_data(request.form.get("data_vencimento"))
# Validar datas
if not validar_data(data_alteracao) or not validar_data(data_vencimento):
flash('Datas inválidas', 'danger')
return redirect(url_for('cota.nova'))
result = CotaService.criar_cota(militante_id, valor_antigo, valor_novo,
data_alteracao, data_vencimento)
if result:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'success',
'message': 'Cota cadastrada com sucesso!'
})
flash('Cota cadastrada com sucesso!', 'success')
return redirect(url_for('cota.listar'))
else:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'error',
'message': 'Erro ao cadastrar cota. Verifique os dados e tente novamente.'
}), 400
flash('Erro ao cadastrar cota', 'danger')
return redirect(url_for('cota.nova'))
# GET
militantes = MilitanteService.listar_militantes()
return render_template("nova_cota.html", militantes=militantes)
@cota_bp.route('/editar/<int:id>', methods=['GET', 'POST'])
@login_required
@require_permission('gerenciar_cotas')
def editar(id):
"""Edita uma cota existente"""
cota = CotaService.buscar_cota(id)
if not cota:
flash('Cota não encontrada', 'danger')
return redirect(url_for('cota.listar'))
if request.method == 'POST':
militante_id = int(request.form['militante_id'])
valor_antigo = float(request.form['valor_antigo'])
valor_novo = float(request.form['valor_novo'])
data_alteracao = converter_data(request.form['data_alteracao'])
data_vencimento = converter_data(request.form['data_vencimento'])
pago = request.form.get('pago', '').lower() == 'true'
if CotaService.atualizar_cota(id, militante_id, valor_antigo, valor_novo,
data_alteracao, data_vencimento, pago):
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'success',
'message': 'Cota atualizada com sucesso!'
})
flash('Cota atualizada com sucesso!', 'success')
return redirect(url_for('cota.listar'))
else:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'error',
'message': 'Erro ao atualizar cota'
}), 400
flash('Erro ao atualizar cota', 'danger')
return redirect(url_for('cota.editar', id=id))
return render_template('editar_cota.html', cota=cota)
@cota_bp.route('/excluir/<int:id>', methods=['POST'])
@login_required
@require_permission('gerenciar_cotas')
def excluir(id):
"""Exclui uma cota existente"""
if CotaService.excluir_cota(id):
flash('Cota excluída com sucesso!', 'success')
else:
flash('Erro ao excluir cota', 'danger')
return redirect(url_for('cota.listar'))

41
routes/main.py Normal file
View File

@@ -0,0 +1,41 @@
from flask import Blueprint, render_template, redirect, url_for
from flask_login import login_required
from functions.decorators import require_login
from controllers.home_controller import HomeController
main_bp = Blueprint('main', __name__)
@main_bp.route("/")
@require_login
def index():
"""Rota principal - redireciona para home se autenticado"""
return redirect(url_for('main.home'))
@main_bp.route("/home")
@require_login
def home():
"""Página inicial do sistema com dashboard"""
dashboard_data = HomeController.dashboard()
return render_template('home.html',
nome_usuario=dashboard_data['nome_usuario'],
data_atual=dashboard_data['data_atual'],
total_militantes=dashboard_data['total_militantes'],
total_cotas=dashboard_data['total_cotas'],
total_materiais=dashboard_data['total_materiais'],
total_assinaturas=dashboard_data['total_assinaturas'],
ultimos_militantes=dashboard_data['ultimos_militantes'],
ultimos_pagamentos=dashboard_data['ultimos_pagamentos'],
tipos_pagamento=dashboard_data['tipos_pagamento'],
Militante=None) # Militante class for constants
@main_bp.route("/api/setores/<int:cr_id>")
@require_login
def get_setores(cr_id):
"""API para listar setores por comitê regional"""
from services.setor_service import SetorService
setores = SetorService.listar_setores_por_cr(cr_id)
return jsonify({
'setores': [{'id': s.id, 'nome': s.nome} for s in setores]
})

50
routes/militante.py Normal file
View File

@@ -0,0 +1,50 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify
from flask_login import login_required
from controllers.militante_controller import MilitanteController
from services.celula_service import CelulaService
from functions.decorators import require_permission, require_role
militante_bp = Blueprint('militante', __name__, url_prefix='/militantes')
@militante_bp.route("/")
@login_required
@require_permission('gerenciar_militantes')
def listar():
"""Lista todos os militantes"""
militantes = MilitanteController.listar_militantes()
celulas = CelulaService.listar_celulas()
return render_template('listar_militantes.html',
militantes=militantes,
celulas=celulas,
Militante=None) # Militante class for constants
@militante_bp.route("/criar", methods=["POST"])
@login_required
@require_permission('gerenciar_militantes')
def criar():
"""Cria um novo militante"""
return MilitanteController.criar_militante(request.form)
@militante_bp.route("/editar/<int:militante_id>", methods=["POST"])
@login_required
@require_permission('gerenciar_militantes')
def editar(militante_id):
"""Edita um militante existente"""
return MilitanteController.atualizar_militante(militante_id, request.form)
@militante_bp.route("/excluir/<int:militante_id>", methods=["POST"])
@login_required
@require_permission('gerenciar_militantes')
def excluir(militante_id):
"""Exclui um militante"""
if MilitanteController.excluir_militante(militante_id):
return redirect(url_for('militante.listar'))
return redirect(url_for('militante.listar'))
@militante_bp.route("/dados/<int:militante_id>")
@login_required
@require_permission('gerenciar_militantes')
def dados(militante_id):
"""Busca os dados de um militante específico"""
return MilitanteController.buscar_dados_militante(militante_id)

116
routes/pagamento.py Normal file
View File

@@ -0,0 +1,116 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, flash
from flask_login import login_required
from services.pagamento_service import PagamentoService
from services.militante_service import MilitanteService
from services.tipo_pagamento_service import TipoPagamentoService
from functions.decorators import require_permission
from utils.date_utils import validar_data, converter_data
pagamento_bp = Blueprint('pagamento', __name__, url_prefix='/pagamentos')
@pagamento_bp.route("/")
@login_required
@require_permission('gerenciar_pagamentos')
def listar():
"""Lista todos os pagamentos"""
pagamentos = PagamentoService.listar_pagamentos()
militantes = MilitanteService.listar_militantes()
tipos_pagamento = TipoPagamentoService.listar_tipos_pagamento()
return render_template("listar_pagamentos.html",
pagamentos=pagamentos,
militantes=militantes,
tipos_pagamento=tipos_pagamento)
@pagamento_bp.route("/novo", methods=["GET", "POST"])
@login_required
@require_permission('gerenciar_pagamentos')
def novo():
"""Cria um novo pagamento"""
if request.method == "POST":
militante_id = request.form.get("militante_id")
tipo_pagamento_id = request.form.get("tipo_pagamento_id")
valor = float(request.form.get("valor"))
data_pagamento = converter_data(request.form.get("data_pagamento"))
if not validar_data(data_pagamento):
flash('Data de pagamento inválida ou futura', 'danger')
return redirect(url_for('pagamento.novo'))
if PagamentoService.criar_pagamento(militante_id, tipo_pagamento_id, valor, data_pagamento):
flash('Pagamento cadastrado com sucesso!', 'success')
return redirect(url_for('pagamento.listar'))
else:
flash('Erro ao cadastrar pagamento', 'danger')
return redirect(url_for('pagamento.novo'))
# GET
militantes = MilitanteService.listar_militantes()
tipos_pagamento = TipoPagamentoService.listar_tipos_pagamento()
return render_template("novo_pagamento.html",
militantes=militantes,
tipos_pagamento=tipos_pagamento)
@pagamento_bp.route("/adicionar", methods=["POST"])
@login_required
@require_permission('gerenciar_pagamentos')
def adicionar():
"""Adiciona um novo pagamento (via AJAX)"""
militante_id = request.form.get("militante_id")
tipo_pagamento = request.form.get("tipo_pagamento")
valor = float(request.form.get("valor"))
data_pagamento = converter_data(request.form.get("data_pagamento"))
if PagamentoService.criar_pagamento_simples(militante_id, tipo_pagamento, valor, data_pagamento):
return jsonify({
'status': 'success',
'message': 'Pagamento adicionado com sucesso!'
})
else:
return jsonify({
'status': 'error',
'message': 'Erro ao adicionar pagamento'
}), 400
@pagamento_bp.route('/editar/<int:id>', methods=['GET', 'POST'])
@login_required
@require_permission('gerenciar_pagamentos')
def editar(id):
"""Edita um pagamento existente"""
pagamento = PagamentoService.buscar_pagamento(id)
if not pagamento:
flash('Pagamento não encontrado', 'danger')
return redirect(url_for('pagamento.listar'))
if request.method == 'POST':
militante_id = int(request.form['militante_id'])
tipo_pagamento_id = int(request.form['tipo_pagamento_id'])
valor = float(request.form['valor'])
data_pagamento = converter_data(request.form['data_pagamento'])
if PagamentoService.atualizar_pagamento(id, militante_id, tipo_pagamento_id, valor, data_pagamento):
flash('Pagamento atualizado com sucesso!', 'success')
return redirect(url_for('pagamento.listar'))
else:
flash('Erro ao atualizar pagamento', 'danger')
return redirect(url_for('pagamento.editar', id=id))
militantes = MilitanteService.listar_militantes()
tipos_pagamento = TipoPagamentoService.listar_tipos_pagamento()
return render_template('editar_pagamento.html',
pagamento=pagamento,
militantes=militantes,
tipos_pagamento=tipos_pagamento)
@pagamento_bp.route('/excluir/<int:id>', methods=['POST'])
@login_required
@require_permission('gerenciar_pagamentos')
def excluir(id):
"""Exclui um pagamento existente"""
if PagamentoService.excluir_pagamento(id):
flash('Pagamento excluído com sucesso!', 'success')
else:
flash('Erro ao excluir pagamento', 'danger')
return redirect(url_for('pagamento.listar'))

149
routes/relatorio.py Normal file
View File

@@ -0,0 +1,149 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, flash
from flask_login import login_required
from datetime import date
from services.relatorio_service import RelatorioService
from services.setor_service import SetorService
from services.comite_service import ComiteService
from functions.decorators import require_permission
from utils.date_utils import validar_data, converter_data
relatorio_bp = Blueprint('relatorio', __name__, url_prefix='/relatorios')
# Rotas para relatórios de cotas
@relatorio_bp.route("/cotas")
@login_required
@require_permission('visualizar_relatorios')
def listar_cotas():
"""Lista todos os relatórios de cotas"""
relatorios = RelatorioService.listar_relatorios_cotas()
return render_template("listar_relatorios_cotas.html", relatorios=relatorios)
@relatorio_bp.route("/cotas/novo", methods=["GET", "POST"])
@login_required
@require_permission('gerar_relatorios')
def novo_relatorio_cotas():
"""Cria um novo relatório de cotas"""
if request.method == "POST":
setor_id = request.form.get("setor_id")
comite_id = request.form.get("comite_id")
total_cotas = float(request.form.get("total_cotas"))
data_relatorio = request.form.get("data_relatorio")
# Validar data
if not validar_data(data_relatorio):
flash('Data do relatório inválida', 'danger')
return render_template("novo_relatorio_cotas.html")
# Converter data
data_relatorio = converter_data(data_relatorio)
# Validar data futura
if data_relatorio > date.today():
flash('A data do relatório não pode ser futura', 'danger')
return render_template("novo_relatorio_cotas.html")
if RelatorioService.criar_relatorio_cotas(setor_id, comite_id, total_cotas, data_relatorio):
flash('Relatório de cotas cadastrado com sucesso!', 'success')
return redirect(url_for('relatorio.listar_cotas'))
else:
flash('Erro ao cadastrar relatório de cotas', 'danger')
return render_template("novo_relatorio_cotas.html")
# GET
setores = SetorService.listar_setores()
comites = ComiteService.listar_comites()
return render_template("novo_relatorio_cotas.html",
setores=setores,
comites=comites,
hoje=date.today().strftime('%Y-%m-%d'))
@relatorio_bp.route('/cotas/editar/<int:id>', methods=['GET', 'POST'])
@login_required
@require_permission('gerar_relatorios')
def editar_relatorio_cotas(id):
"""Edita um relatório de cotas existente"""
relatorio = RelatorioService.buscar_relatorio_cotas(id)
if not relatorio:
flash('Relatório não encontrado', 'danger')
return redirect(url_for('relatorio.listar_cotas'))
if request.method == 'POST':
setor_id = int(request.form['setor_id']) if request.form['setor_id'] else None
comite_id = int(request.form['comite_id']) if request.form['comite_id'] else None
total_cotas = float(request.form['total_cotas'])
data_relatorio = converter_data(request.form['data_relatorio'])
if RelatorioService.atualizar_relatorio_cotas(id, setor_id, comite_id, total_cotas, data_relatorio):
flash('Relatório atualizado com sucesso!', 'success')
return redirect(url_for('relatorio.listar_cotas'))
else:
flash('Erro ao atualizar relatório', 'danger')
return redirect(url_for('relatorio.editar_relatorio_cotas', id=id))
setores = SetorService.listar_setores()
comites = ComiteService.listar_comites()
return render_template('editar_relatorio_cotas.html',
relatorio=relatorio,
setores=setores,
comites=comites)
@relatorio_bp.route('/cotas/excluir/<int:id>', methods=['POST'])
@login_required
@require_permission('gerar_relatorios')
def excluir_relatorio_cotas(id):
"""Exclui um relatório de cotas existente"""
if RelatorioService.excluir_relatorio_cotas(id):
flash('Relatório excluído com sucesso!', 'success')
else:
flash('Erro ao excluir relatório', 'danger')
return redirect(url_for('relatorio.listar_cotas'))
# Rotas para relatórios de vendas
@relatorio_bp.route("/vendas")
@login_required
@require_permission('visualizar_relatorios')
def listar_vendas():
"""Lista todos os relatórios de vendas"""
relatorios = RelatorioService.listar_relatorios_vendas()
return render_template("listar_relatorios_vendas.html", relatorios=relatorios)
@relatorio_bp.route("/vendas/novo", methods=["GET", "POST"])
@login_required
@require_permission('gerar_relatorios')
def novo_relatorio_vendas():
"""Cria um novo relatório de vendas"""
if request.method == "POST":
setor_id = request.form.get("setor_id")
comite_id = request.form.get("comite_id")
total_vendas = float(request.form.get("total_vendas"))
data_relatorio = request.form.get("data_relatorio")
# Validar data
if not validar_data(data_relatorio):
flash('Data do relatório inválida', 'danger')
return render_template("novo_relatorio_vendas.html")
# Converter data
data_relatorio = converter_data(data_relatorio)
# Validar data futura
if data_relatorio > date.today():
flash('A data do relatório não pode ser futura', 'danger')
return render_template("novo_relatorio_vendas.html")
if RelatorioService.criar_relatorio_vendas(setor_id, comite_id, total_vendas, data_relatorio):
flash('Relatório de vendas cadastrado com sucesso!', 'success')
return redirect(url_for('relatorio.listar_vendas'))
else:
flash('Erro ao cadastrar relatório de vendas', 'danger')
return render_template("novo_relatorio_vendas.html")
# GET
setores = SetorService.listar_setores()
comites = ComiteService.listar_comites()
return render_template("novo_relatorio_vendas.html",
setores=setores,
comites=comites,
hoje=date.today().strftime('%Y-%m-%d'))

44
scripts/prepare_mvc.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/bin/bash
# Script para preparar a estrutura MVC
echo "Preparando a estrutura MVC para o Sistema de Controles..."
# Criar estrutura de diretórios
echo "Criando estrutura de diretórios..."
mkdir -p models/entities controllers services
# Mover arquivos refatorados
echo "Movendo arquivos refatorados..."
cp app.py.new app.py
# Criar arquivo __init__.py nos diretórios Python
echo "Criando arquivos de inicialização..."
touch models/__init__.py
touch models/entities/__init__.py
touch controllers/__init__.py
touch services/__init__.py
# Criar arquivo __init__.py com importações para models/entities
cat > models/entities/__init__.py << EOF
from models.entities.base import Base
from models.entities.usuario import Usuario, TipoUsuario
from models.entities.militante import Militante, EstadoMilitante
from models.entities.endereco import Endereco
from models.entities.email_militante import EmailMilitante
from models.entities.rede_social import RedeSocial
from models.entities.cota_mensal import CotaMensal
from models.entities.pagamento import Pagamento
from models.entities.tipo_material import TipoMaterial
from models.entities.material_vendido import MaterialVendido
from models.entities.venda_jornal import VendaJornal
from models.entities.venda_jornal_avulso import VendaJornalAvulso
from models.entities.assinatura_jornal import AssinaturaJornal
from models.entities.comprovante import Comprovante
EOF
echo "Todos os arquivos criados com sucesso!"
echo "Para usar a nova estrutura MVC, execute:"
echo "1. chmod +x scripts/prepare_mvc.sh"
echo "2. ./scripts/prepare_mvc.sh"
echo "3. python app.py"

View File

@@ -1,10 +1,11 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functions.database import ( from functions.database import (
Base, Militante, CotaMensal, TipoPagamento, Pagamento, Base, Militante, CotaMensal, TipoComprovante, Comprovante,
MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual, MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual,
RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal, RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal,
Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco, Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco,
ComiteRegional, Celula, EstadoMilitante ComiteRegional, Celula, EstadoMilitante, get_db_connection,
init_database
) )
import random import random
from faker import Faker from faker import Faker
@@ -54,20 +55,28 @@ def criar_estrutura_organizacional(session):
session.commit() session.commit()
return crs, setores return crs, setores
def criar_tipos_pagamento(session): def criar_tipos_comprovante(session):
"""Cria tipos de pagamento padrão""" """Cria tipos de comprovante padrão"""
print("\nCriando tipos de pagamento...") print("\nCriando tipos de comprovante...")
tipos = [ tipos = [
"Dinheiro", "Comprovante Padrão",
"PIX", "Comprovante Especial",
"Cartão de Crédito", "Comprovante Extraordinário",
"Cartão de Débito", "Jornal Avulso",
"Transferência Bancária" "Assinatura de Jornal",
"Campanha Financeira"
] ]
for tipo in tipos: for tipo in tipos:
if not session.query(TipoPagamento).filter_by(descricao=tipo).first(): if not session.query(TipoComprovante).filter_by(descricao=tipo).first():
session.add(TipoPagamento(descricao=tipo)) session.add(TipoComprovante(descricao=tipo))
session.commit()
try:
session.commit()
print("Tipos de comprovante criados com sucesso!")
except Exception as e:
session.rollback()
print(f"Erro ao criar tipos de comprovante: {e}")
def criar_tipos_material(session): def criar_tipos_material(session):
"""Cria tipos de material padrão""" """Cria tipos de material padrão"""
@@ -211,27 +220,27 @@ def criar_cotas(session, militantes):
print(f"Erro ao criar cotas para militante {militante.nome}: {e}") print(f"Erro ao criar cotas para militante {militante.nome}: {e}")
session.rollback() session.rollback()
def criar_pagamentos(session, militantes): def criar_comprovantes(session, militantes):
"""Cria pagamentos para os militantes""" """Cria comprovantes para os militantes"""
print("\nCriando pagamentos...") print("\nCriando comprovantes...")
tipos_pagamento = session.query(TipoPagamento).all() tipos_comprovante = session.query(TipoComprovante).all()
for militante in militantes: for militante in militantes:
try: try:
# Criar entre 3 e 8 pagamentos por militante # Criar entre 3 e 8 comprovantes por militante
for _ in range(random.randint(3, 8)): for _ in range(random.randint(3, 8)):
tipo = random.choice(tipos_pagamento) tipo = random.choice(tipos_comprovante)
pagamento = Pagamento( comprovante = Comprovante(
militante_id=militante.id, militante_id=militante.id,
tipo_pagamento=tipo.descricao, # Usando a descrição do tipo tipo_comprovante=tipo.descricao, # Usando a descrição do tipo
valor=random.uniform(50, 500), valor=random.uniform(10, 1000),
data_pagamento=fake.date_between(start_date='-1y', end_date='today') data_comprovante=fake.date_between(start_date='-1y', end_date='today')
) )
session.add(pagamento) session.add(comprovante)
session.commit() session.commit()
except Exception as e: except Exception as e:
print(f"Erro ao criar pagamentos para militante {militante.nome}: {e}")
session.rollback() session.rollback()
print(f"Erro ao criar comprovantes para militante {militante.nome}: {e}")
def criar_materiais_vendidos(session, militantes): def criar_materiais_vendidos(session, militantes):
"""Cria registros de materiais vendidos""" """Cria registros de materiais vendidos"""
@@ -302,7 +311,7 @@ def criar_assinaturas(session, militantes):
def seed_database(): def seed_database():
"""Função principal para popular o banco de dados""" """Função principal para popular o banco de dados"""
session = SessionLocal() session = get_db_connection()
try: try:
print("Iniciando população do banco de dados...") print("Iniciando população do banco de dados...")
@@ -310,7 +319,7 @@ def seed_database():
crs, setores = criar_estrutura_organizacional(session) crs, setores = criar_estrutura_organizacional(session)
# Criar tipos básicos # Criar tipos básicos
criar_tipos_pagamento(session) criar_tipos_comprovante(session)
criar_tipos_material(session) criar_tipos_material(session)
# Criar militantes (30 militantes para teste) # Criar militantes (30 militantes para teste)
@@ -318,7 +327,7 @@ def seed_database():
# Criar dados financeiros e materiais # Criar dados financeiros e materiais
criar_cotas(session, militantes) criar_cotas(session, militantes)
criar_pagamentos(session, militantes) criar_comprovantes(session, militantes)
criar_materiais_vendidos(session, militantes) criar_materiais_vendidos(session, militantes)
criar_vendas_jornal(session, militantes) criar_vendas_jornal(session, militantes)
criar_assinaturas(session, militantes) criar_assinaturas(session, militantes)

View File

@@ -0,0 +1,35 @@
from sqlalchemy import text
from models.entities.base import engine, SessionLocal
class DatabaseService:
"""Serviço para gerenciar conexões com o banco de dados"""
@staticmethod
def get_db_connection():
"""Retorna uma nova sessão do banco de dados"""
db = SessionLocal()
try:
# Configurar SQLite para melhor tratamento de concorrência
db.execute(text("PRAGMA journal_mode=WAL"))
db.execute(text("PRAGMA busy_timeout=5000"))
return db
except:
db.close()
raise
@staticmethod
def execute_query(query, params=None):
"""
Executa uma query usando SQLAlchemy
"""
session = DatabaseService.get_db_connection()
try:
result = session.execute(query, params)
session.commit()
return result
except Exception as e:
session.rollback()
raise e
finally:
session.close()

View File

@@ -0,0 +1,161 @@
from sqlalchemy.orm import joinedload
from datetime import datetime
from models.entities.militante import Militante
from models.entities.email_militante import EmailMilitante
from models.entities.endereco import Endereco
from services.database_service import DatabaseService
class MilitanteService:
"""Serviço para operações com militantes"""
@staticmethod
def listar_militantes():
"""Lista todos os militantes"""
db = DatabaseService.get_db_connection()
try:
militantes = db.query(Militante)\
.options(
joinedload(Militante.celula),
joinedload(Militante.emails)
)\
.order_by(Militante.nome)\
.all()
return militantes
finally:
db.close()
@staticmethod
def buscar_militante(militante_id):
"""Busca um militante pelo ID"""
db = DatabaseService.get_db_connection()
try:
militante = db.query(Militante)\
.options(
joinedload(Militante.celula),
joinedload(Militante.emails),
joinedload(Militante.endereco),
joinedload(Militante.redes_sociais)
)\
.get(militante_id)
return militante
finally:
db.close()
@staticmethod
def buscar_por_cpf(cpf):
"""Busca um militante pelo CPF"""
db = DatabaseService.get_db_connection()
try:
militante = db.query(Militante).filter(Militante.cpf == cpf).first()
return militante
finally:
db.close()
@staticmethod
def salvar_militante(militante):
"""Salva um militante no banco de dados"""
db = DatabaseService.get_db_connection()
try:
if militante.id is None: # Novo militante
db.add(militante)
db.flush() # Para obter o ID gerado
militante_id = militante.id
else: # Militante existente
db.merge(militante)
militante_id = militante.id
db.commit()
return militante_id
except Exception as e:
db.rollback()
print(f"Erro ao salvar militante: {e}")
raise
finally:
db.close()
@staticmethod
def salvar_endereco(endereco):
"""Salva um endereço no banco de dados"""
db = DatabaseService.get_db_connection()
try:
db.add(endereco)
db.flush() # Para obter o ID gerado
endereco_id = endereco.id
db.commit()
return endereco_id
except Exception as e:
db.rollback()
print(f"Erro ao salvar endereço: {e}")
raise
finally:
db.close()
@staticmethod
def salvar_email_militante(email_militante):
"""Salva um email de militante no banco de dados"""
db = DatabaseService.get_db_connection()
try:
db.add(email_militante)
db.commit()
return email_militante.id
except Exception as e:
db.rollback()
print(f"Erro ao salvar email: {e}")
raise
finally:
db.close()
@staticmethod
def atualizar_email_militante(militante_id, email):
"""Atualiza ou cria o email principal de um militante"""
db = DatabaseService.get_db_connection()
try:
# Verificar se já existe email
email_existente = db.query(EmailMilitante)\
.filter_by(militante_id=militante_id)\
.first()
if email_existente:
email_existente.endereco_email = email
db.commit()
else:
novo_email = EmailMilitante(
endereco_email=email,
militante_id=militante_id
)
db.add(novo_email)
db.commit()
return True
except Exception as e:
db.rollback()
print(f"Erro ao atualizar email: {e}")
raise
finally:
db.close()
@staticmethod
def excluir_militante(militante_id):
"""Exclui um militante pelo ID"""
db = DatabaseService.get_db_connection()
try:
militante = db.query(Militante).get(militante_id)
if not militante:
return False
# Excluir emails associados
db.query(EmailMilitante)\
.filter_by(militante_id=militante_id)\
.delete()
# Excluir o militante
db.delete(militante)
db.commit()
return True
except Exception as e:
db.rollback()
print(f"Erro ao excluir militante: {e}")
raise
finally:
db.close()

View File

@@ -0,0 +1,95 @@
from sqlalchemy.orm import joinedload
from models.entities.usuario import Usuario
from services.database_service import DatabaseService
class UsuarioService:
"""Serviço para operações com usuários"""
@staticmethod
def listar_usuarios():
"""Lista todos os usuários"""
db = DatabaseService.get_db_connection()
try:
usuarios = db.query(Usuario).options(
joinedload(Usuario.roles),
joinedload(Usuario.militante),
joinedload(Usuario.setor),
joinedload(Usuario.cr),
joinedload(Usuario.celula)
).all()
return usuarios
finally:
db.close()
@staticmethod
def buscar_usuario(user_id):
"""Busca um usuário pelo ID"""
db = DatabaseService.get_db_connection()
try:
usuario = db.query(Usuario).options(
joinedload(Usuario.roles),
joinedload(Usuario.militante),
joinedload(Usuario.setor),
joinedload(Usuario.cr),
joinedload(Usuario.celula)
).get(user_id)
return usuario
finally:
db.close()
@staticmethod
def buscar_por_username(username):
"""Busca um usuário pelo nome de usuário"""
db = DatabaseService.get_db_connection()
try:
usuario = db.query(Usuario).filter(Usuario.username == username).first()
return usuario
finally:
db.close()
@staticmethod
def buscar_por_email(email):
"""Busca um usuário pelo email"""
db = DatabaseService.get_db_connection()
try:
usuario = db.query(Usuario).filter(Usuario.email == email).first()
return usuario
finally:
db.close()
@staticmethod
def salvar_usuario(usuario):
"""Salva um usuário no banco de dados"""
db = DatabaseService.get_db_connection()
try:
if usuario.id is None: # Novo usuário
db.add(usuario)
else: # Usuário existente
db.merge(usuario)
db.commit()
return True
except Exception as e:
db.rollback()
print(f"Erro ao salvar usuário: {e}")
return False
finally:
db.close()
@staticmethod
def excluir_usuario(user_id):
"""Exclui um usuário pelo ID"""
db = DatabaseService.get_db_connection()
try:
usuario = db.query(Usuario).get(user_id)
if usuario:
db.delete(usuario)
db.commit()
return True
return False
except Exception as e:
db.rollback()
print(f"Erro ao excluir usuário: {e}")
return False
finally:
db.close()

51
static/js/comprovantes.js Normal file
View File

@@ -0,0 +1,51 @@
$(document).ready(function() {
// Inicialização da tabela
$('#tabelaComprovantes').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
}
});
// Modal de edição
$('#modalEditarComprovante').on('show.bs.modal', function(event) {
var button = $(event.relatedTarget);
var comprovanteId = button.data('comprovante-id');
var militanteId = button.data('militante-id');
var militanteNome = button.data('militante-nome');
var tipoComprovante = button.data('tipo-comprovante');
var valor = button.data('valor');
var dataComprovante = button.data('data-comprovante');
var modal = $(this);
modal.find('#editMilitante').val(militanteId);
modal.find('#editMilitanteNome').val(militanteNome);
modal.find('#editTipoComprovante').val(tipoComprovante);
modal.find('#editValor').val(valor);
modal.find('#editDataComprovante').val(dataComprovante);
modal.find('form').attr('action', '/comprovantes/editar/' + comprovanteId);
});
// Modal de exclusão
$('#modalExcluirComprovante').on('show.bs.modal', function(event) {
var button = $(event.relatedTarget);
var comprovanteId = button.data('comprovante-id');
var comprovanteInfo = button.data('comprovante-info');
var modal = $(this);
modal.find('#comprovanteInfo').text(comprovanteInfo);
modal.find('form').attr('action', '/comprovantes/excluir/' + comprovanteId);
});
// Formatação de valores monetários
$('.money').mask('000.000.000.000.000,00', {reverse: true});
// Validação de formulários
$('form').on('submit', function(e) {
if (!this.checkValidity()) {
e.preventDefault();
e.stopPropagation();
}
$(this).addClass('was-validated');
});
});

View File

@@ -1,10 +1,10 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Configurar clique nos itens da lista de pagamentos // Configurar clique nos itens da lista de comprovantes
document.querySelectorAll('.list-group-item[onclick*="carregarDadosPagamento"]').forEach(item => { document.querySelectorAll('.list-group-item[onclick*="carregarDadosComprovante"]').forEach(item => {
item.addEventListener('click', function(e) { item.addEventListener('click', function(e) {
const pagamentoId = this.getAttribute('data-pagamento-id'); const comprovanteId = this.getAttribute('data-comprovante-id');
if (pagamentoId) { if (comprovanteId) {
carregarDadosPagamento(pagamentoId); carregarDadosComprovante(comprovanteId);
} }
}); });
}); });

View File

@@ -1,316 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
console.log('Carregando script pagamentos.js...');
// Inicializar DataTable
const table = $('#tabelaPagamentos').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
},
columnDefs: [
{
targets: 3, // Coluna de data
type: 'date-br',
render: function(data, type, row) {
if (type === 'sort') {
return data.split('/').reverse().join('');
}
return data;
}
},
{
targets: 2, // Coluna de valor
type: 'numeric',
render: function(data, type, row) {
if (type === 'sort') {
return parseFloat(data.replace('R$ ', '').replace(',', '.'));
}
return data;
}
},
{ targets: -1, orderable: false } // Coluna de ações
],
order: [[3, 'desc']] // Ordenar por data decrescente por padrão
});
// Configuração do modal de edição
const modalEditarPagamento = document.getElementById('modalEditarPagamento');
if (modalEditarPagamento) {
modalEditarPagamento.addEventListener('show.bs.modal', function(event) {
console.log('Modal de edição sendo exibido');
const button = event.relatedTarget;
if (!button) {
console.error('Botão não encontrado!');
return;
}
const pagamentoId = button.getAttribute('data-pagamento-id');
console.log('ID do pagamento:', pagamentoId);
// Dados do pagamento
const dados = {
militanteId: button.getAttribute('data-militante-id'),
militanteNome: button.closest('tr').querySelector('td').textContent.trim(),
tipoPagamento: button.getAttribute('data-tipo-pagamento'),
valor: button.getAttribute('data-valor'),
dataPagamento: button.getAttribute('data-data-pagamento')
};
console.log('Dados do pagamento:', dados);
// Preencher campos
document.getElementById('editMilitante').value = dados.militanteId;
document.getElementById('editMilitanteNome').value = dados.militanteNome;
document.getElementById('editTipoPagamento').value = dados.tipoPagamento;
document.getElementById('editValor').value = dados.valor;
document.getElementById('editDataPagamento').value = dados.dataPagamento;
// Configurar formulário
const form = document.getElementById('formEditarPagamento');
if (form) {
form.action = `/pagamentos/editar/${pagamentoId}`;
console.log('Action do formulário:', form.action);
// Remover listeners antigos para evitar duplicação
const newForm = form.cloneNode(true);
form.parentNode.replaceChild(newForm, form);
// Adicionar listener para o submit do formulário
newForm.addEventListener('submit', function(e) {
e.preventDefault();
console.log('Formulário submetido');
// Criar FormData com os dados do formulário
const formData = new FormData(this);
// Log dos dados sendo enviados
console.log('Dados do formulário:');
for (let [key, value] of formData.entries()) {
console.log(key + ': ' + value);
}
// Enviar requisição
fetch(this.action, {
method: 'POST',
body: formData
})
.then(response => {
console.log('Status da resposta:', response.status);
return response.json();
})
.then(data => {
console.log('Resposta:', data);
if (data.status === 'success') {
// Fechar modal
const modal = bootstrap.Modal.getInstance(modalEditarPagamento);
modal.hide();
// Recarregar página
window.location.reload();
} else {
alert('Erro ao atualizar pagamento: ' + data.message);
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao atualizar pagamento. Por favor, tente novamente.');
});
});
}
});
}
// Configuração do modal de exclusão
const modalExcluirPagamento = document.getElementById('modalExcluirPagamento');
if (modalExcluirPagamento) {
modalExcluirPagamento.addEventListener('show.bs.modal', function(event) {
console.log('Modal de exclusão sendo exibido');
const button = event.relatedTarget;
if (!button) {
console.error('Botão não encontrado!');
return;
}
const pagamentoId = button.getAttribute('data-pagamento-id');
const pagamentoInfo = button.getAttribute('data-pagamento-info');
console.log('ID do pagamento:', pagamentoId);
// Atualizar informações no modal
document.getElementById('pagamentoInfo').textContent = pagamentoInfo;
// Configurar formulário
const form = document.getElementById('formExcluirPagamento');
if (form) {
form.action = `/pagamentos/excluir/${pagamentoId}`;
console.log('Action do formulário:', form.action);
// Remover listeners antigos para evitar duplicação
const newForm = form.cloneNode(true);
form.parentNode.replaceChild(newForm, form);
// Adicionar listener para o submit do formulário
newForm.addEventListener('submit', function(e) {
e.preventDefault();
console.log('Formulário submetido');
// Enviar requisição
fetch(this.action, {
method: 'POST'
})
.then(response => {
console.log('Status da resposta:', response.status);
return response.json();
})
.then(data => {
console.log('Resposta:', data);
if (data.status === 'success') {
// Fechar modal
const modal = bootstrap.Modal.getInstance(modalExcluirPagamento);
modal.hide();
// Recarregar página
window.location.reload();
} else {
alert('Erro ao excluir pagamento: ' + data.message);
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao excluir pagamento. Por favor, tente novamente.');
});
});
}
});
}
// Configuração do formulário de novo pagamento
const formNovoPagamento = document.getElementById('formNovoPagamento');
if (formNovoPagamento) {
formNovoPagamento.addEventListener('submit', function(e) {
e.preventDefault();
console.log('Formulário de novo pagamento submetido');
// Criar FormData com os dados do formulário
const formData = new FormData(this);
// Log dos dados sendo enviados
console.log('Dados do formulário:');
for (let [key, value] of formData.entries()) {
console.log(key + ': ' + value);
}
// Enviar requisição
fetch(this.action, {
method: 'POST',
body: formData
})
.then(response => {
console.log('Status da resposta:', response.status);
return response.json();
})
.then(data => {
console.log('Resposta:', data);
if (data.status === 'success') {
// Fechar modal
const modal = bootstrap.Modal.getInstance(document.getElementById('modalNovoPagamento'));
modal.hide();
// Recarregar página
window.location.reload();
} else {
alert('Erro ao adicionar pagamento: ' + data.message);
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao adicionar pagamento. Por favor, tente novamente.');
});
});
}
// Configuração do botão de exportar
const btnExportar = document.getElementById('btnExportar');
if (btnExportar) {
btnExportar.addEventListener('click', function() {
console.log('Exportando dados...');
// Coletar dados da tabela
const dados = [];
table.rows().every(function() {
const row = this.data();
dados.push({
militante: row[0],
tipo_pagamento: row[1],
valor: row[2].replace('R$ ', ''),
data_pagamento: row[3]
});
});
// Converter para CSV
const csv = [
['Militante', 'Tipo de Pagamento', 'Valor', 'Data do Pagamento'],
...dados.map(row => [
row.militante,
row.tipo_pagamento,
row.valor,
row.data_pagamento
])
]
.map(row => row.join(','))
.join('\n');
// Criar blob e fazer download
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'pagamentos.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
});
}
// Funções de validação e formatação de datas
function validarData(data) {
if (!data) return false;
const dataObj = new Date(data);
if (isNaN(dataObj.getTime())) return false;
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
return dataObj <= hoje;
}
function formatarData(data) {
if (!data) return '';
const dataObj = new Date(data);
if (isNaN(dataObj.getTime())) return '';
return dataObj.toLocaleDateString('pt-BR');
}
// Configurar campos de data
const camposData = document.querySelectorAll('input[type="date"]');
camposData.forEach(campo => {
// Definir data máxima como hoje
const hoje = new Date().toISOString().split('T')[0];
campo.setAttribute('max', hoje);
campo.addEventListener('change', function() {
if (!validarData(this.value)) {
this.setCustomValidity('Data inválida ou futura');
this.classList.add('is-invalid');
} else {
this.setCustomValidity('');
this.classList.remove('is-invalid');
}
});
});
});

View File

@@ -56,7 +56,7 @@ function configurarOrdenacaoTabela(tabelaId) {
if (column === 'data' || if (column === 'data' ||
column === 'data_vencimento' || column === 'data_vencimento' ||
column === 'data_alteracao' || column === 'data_alteracao' ||
column === 'data_pagamento' || column === 'data_comprovante' ||
column === 'data_venda' || column === 'data_venda' ||
column === 'data_relatorio') { column === 'data_relatorio') {
const aDate = converterDataParaComparacao(aValue); const aDate = converterDataParaComparacao(aValue);
@@ -112,7 +112,7 @@ document.addEventListener('DOMContentLoaded', function() {
'materiaisTable', 'materiaisTable',
'vendasTable', 'vendasTable',
'cotasTable', 'cotasTable',
'pagamentosTable' 'comprovantesTable'
]; ];
tabelas.forEach(tabelaId => { tabelas.forEach(tabelaId => {
@@ -197,4 +197,12 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
}); });
}); });
function sortTable(table, column, type = 'text') {
// ... existing code ...
if (column === 'data_comprovante') {
// ... existing code ...
}
// ... existing code ...
}

View File

@@ -2,6 +2,7 @@
{% block title %}Dashboard Administrativo{% endblock %} {% block title %}Dashboard Administrativo{% endblock %}
<<<<<<< HEAD
{% block extra_css %} {% block extra_css %}
<style> <style>
.card { .card {
@@ -126,10 +127,21 @@
<h2 class="display-4 mb-0">{{ total_users }}</h2> <h2 class="display-4 mb-0">{{ total_users }}</h2>
<i class="fas fa-users fa-3x opacity-50"></i> <i class="fas fa-users fa-3x opacity-50"></i>
</div> </div>
=======
{% block content %}
<div class="row mb-4">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Total de Usuários</h5>
<p class="card-text display-4">{{ total_users }}</p>
<i class="fas fa-users fa-2x text-primary"></i>
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<<<<<<< HEAD
<div class="card bg-success text-white"> <div class="card bg-success text-white">
<div class="card-body"> <div class="card-body">
<h5 class="card-title text-uppercase">Usuários Ativos</h5> <h5 class="card-title text-uppercase">Usuários Ativos</h5>
@@ -137,10 +149,18 @@
<h2 class="display-4 mb-0">{{ active_users }}</h2> <h2 class="display-4 mb-0">{{ active_users }}</h2>
<i class="fas fa-user-check fa-3x opacity-50"></i> <i class="fas fa-user-check fa-3x opacity-50"></i>
</div> </div>
=======
<div class="card">
<div class="card-body">
<h5 class="card-title">Usuários Ativos</h5>
<p class="card-text display-4">{{ active_users }}</p>
<i class="fas fa-user-check fa-2x text-success"></i>
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<<<<<<< HEAD
<div class="card bg-danger text-white"> <div class="card bg-danger text-white">
<div class="card-body"> <div class="card-body">
<h5 class="card-title text-uppercase">Usuários Inativos</h5> <h5 class="card-title text-uppercase">Usuários Inativos</h5>
@@ -148,11 +168,19 @@
<h2 class="display-4 mb-0">{{ inactive_users }}</h2> <h2 class="display-4 mb-0">{{ inactive_users }}</h2>
<i class="fas fa-user-times fa-3x opacity-50"></i> <i class="fas fa-user-times fa-3x opacity-50"></i>
</div> </div>
=======
<div class="card">
<div class="card-body">
<h5 class="card-title">Usuários Inativos</h5>
<p class="card-text display-4">{{ inactive_users }}</p>
<i class="fas fa-user-times fa-2x text-danger"></i>
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<<<<<<< HEAD
<div class="card lista-usuarios"> <div class="card lista-usuarios">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"> <h5 class="mb-0">
@@ -188,10 +216,45 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-warning btn-sm" title="Reset OTP" onclick="return confirm('Confirma o reset do OTP deste usuário?')"> <button type="submit" class="btn btn-warning btn-sm" title="Reset OTP" onclick="return confirm('Confirma o reset do OTP deste usuário?')">
<i class="fas fa-key"></i> <i class="fas fa-key"></i>
=======
<div class="card">
<div class="card-header">
<h5 class="mb-0">Gerenciamento de Usuários</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="users-table" class="table table-striped">
<thead>
<tr>
<th>Email</th>
<th>Nome</th>
<th>Status</th>
<th>Último Login</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.email }}</td>
<td>{{ user.name }}</td>
<td>
<span class="badge {% if user.is_active %}bg-success{% else %}bg-danger{% endif %}">
{{ "Ativo" if user.is_active else "Inativo" }}
</span>
</td>
<td>{{ user.last_login.strftime('%d/%m/%Y %H:%M') if user.last_login else 'Nunca' }}</td>
<td>
<form action="{{ url_for('admin.reset_user_otp', user_id=user.id) }}" method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-warning btn-sm" onclick="return confirm('Confirma o reset do OTP deste usuário?')">
<i class="fas fa-key"></i> Reset OTP
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</button> </button>
</form> </form>
<form action="{{ url_for('admin.reset_user_password', user_id=user.id) }}" method="post" class="d-inline"> <form action="{{ url_for('admin.reset_user_password', user_id=user.id) }}" method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<<<<<<< HEAD
<button type="submit" class="btn btn-info btn-sm" title="Reset Senha" onclick="return confirm('Confirma o reset da senha deste usuário?')"> <button type="submit" class="btn btn-info btn-sm" title="Reset Senha" onclick="return confirm('Confirma o reset da senha deste usuário?')">
<i class="fas fa-lock"></i> <i class="fas fa-lock"></i>
</button> </button>
@@ -208,19 +271,58 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
=======
<button type="submit" class="btn btn-info btn-sm" onclick="return confirm('Confirma o reset da senha deste usuário?')">
<i class="fas fa-lock"></i> Reset Senha
</button>
</form>
<button onclick="toggleUserStatus({{ user.id }})" class="btn btn-{% if user.is_active %}danger{% else %}success{% endif %} btn-sm">
<i class="fas fa-{% if user.is_active %}user-times{% else %}user-check{% endif %}"></i>
{{ "Desativar" if user.is_active else "Ativar" }}
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<script> <script>
<<<<<<< HEAD
=======
function toggleUserStatus(userId) {
if (confirm('Deseja alterar o status deste usuário?')) {
fetch(`/admin/users/${userId}/toggle-status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
}).then(response => {
if (response.ok) {
window.location.reload();
}
});
}
}
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
$(document).ready(function() { $(document).ready(function() {
$('#users-table').DataTable({ $('#users-table').DataTable({
language: { language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json' url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
}, },
<<<<<<< HEAD
order: [[0, 'asc']], order: [[0, 'asc']],
pageLength: 25 pageLength: 25
=======
order: [[1, 'asc']]
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
}); });
}); });
</script> </script>

View File

@@ -630,4 +630,4 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -6,7 +6,14 @@
<div class="container mt-4"> <div class="container mt-4">
<h2 class="mb-4"><i class="fas fa-users-cog"></i> Administração de Usuários</h2> <h2 class="mb-4"><i class="fas fa-users-cog"></i> Administração de Usuários</h2>
<div class="card"> <div class="card">
<div class="card-header bg-dark text-white">
<h3 class="mb-0"><i class="fas fa-users-cog"></i> Administração de Usuários</h3>
</div>
<div class="card-body"> <div class="card-body">
<div class="alert alert-info" role="alert">
<i class="fas fa-info-circle"></i> Aqui você pode gerenciar todos os usuários do sistema. Use os controles abaixo para ativar/desativar contas ou alterar níveis de acesso.
</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover">
<thead class="thead-light"> <thead class="thead-light">
@@ -29,6 +36,7 @@
<td>{{ usuario.last_login }}</td> <td>{{ usuario.last_login }}</td>
<td> <td>
<span class="badge {% if usuario.ativo %}bg-success{% else %}bg-danger{% endif %}"> <span class="badge {% if usuario.ativo %}bg-success{% else %}bg-danger{% endif %}">
<span class="badge {% if usuario.ativo %}badge-success{% else %}badge-danger{% endif %}">
{{ "Ativo" if usuario.ativo else "Inativo" }} {{ "Ativo" if usuario.ativo else "Inativo" }}
</span> </span>
</td> </td>
@@ -195,4 +203,4 @@ $(function () {
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends 'base.html' %}
{% block title %}Editar Comprovante{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h4 class="card-title mb-0">
<i class="fas fa-money-bill-wave me-2"></i>Editar Comprovante
</h4>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="tipo_comprovante_id">Tipo de Comprovante</label>
<select class="form-control" id="tipo_comprovante_id" name="tipo_comprovante_id" required>
<option value="1" {% if comprovante.tipo_comprovante_id == 1 %}selected{% endif %}>1 - Comprovante Padrão</option>
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
<option value="2" {% if comprovante.tipo_comprovante_id == 2 %}selected{% endif %}>2 - Comprovante Especial</option>
<option value="3" {% if comprovante.tipo_comprovante_id == 3 %}selected{% endif %}>3 - Comprovante Extraordinário</option>
<option value="4" {% if comprovante.tipo_comprovante_id == 4 %}selected{% endif %}>4 - Jornal Avulso</option>
<option value="5" {% if comprovante.tipo_comprovante_id == 5 %}selected{% endif %}>5 - Assinatura de Jornal</option>
<option value="6" {% if comprovante.tipo_comprovante_id == 6 %}selected{% endif %}>6 - Campanha Financeira</option>
{% endif %}
</select>
</div>
<div class="mb-3">
<label for="data_comprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="data_comprovante" name="data_comprovante"
required max="{{ hoje }}">
</div>
<button type="submit" class="btn btn-primary">Salvar</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,12 +1,12 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Editar Relatório de Pagamentos{% endblock %} {% block title %}Editar Relatório de Comprovantes{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h1 class="mb-4">Editar Relatório de Pagamentos</h1> <h1 class="mb-4">Editar Relatório de Comprovantes</h1>
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
@@ -44,10 +44,10 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="total_pagamentos" class="form-label">Total de Pagamentos</label> <label for="total_comprovantes" class="form-label">Total de Comprovantes</label>
<input type="number" class="form-control" id="total_pagamentos" name="total_pagamentos" step="0.01" value="{{ relatorio.total_pagamentos }}" required> <input type="number" class="form-control" id="total_comprovantes" name="total_comprovantes" step="0.01" value="{{ relatorio.total_comprovantes }}" required>
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, insira o total de pagamentos. Por favor, insira o total de comprovantes.
</div> </div>
</div> </div>
@@ -61,7 +61,7 @@
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Salvar</button> <button type="submit" class="btn btn-success">Salvar</button>
<a href="{{ url_for('listar_relatorios_pagamentos') }}" class="btn btn-outline-secondary">Voltar</a> <a href="{{ url_for('listar_relatorios_comprovantes') }}" class="btn btn-outline-secondary">Voltar</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -0,0 +1,49 @@
{% extends 'base.html' %}
{% block title %}Lista de Comprovantes{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h4 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Lista de Comprovantes
</h4>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Data</th>
<th>Valor</th>
<th>Tipo</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for comprovante in comprovantes %}
<tr>
<td>{{ comprovante.id }}</td>
<td>{{ comprovante.data.strftime('%d/%m/%Y') }}</td>
<td>R$ {{ "%.2f"|format(comprovante.valor) }}</td>
<td>
{% if comprovante.tipo_comprovante_id == 1 %}
1 - Comprovante Padrão
{% elif current_user.has_permission('gerenciar_tipos_comprovante') %}
{% if comprovante.tipo_comprovante_id == 2 %}
2 - Comprovante Especial
<td>{{ comprovante.tipo }}</td>
<td>{{ comprovante.data }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,14 +1,14 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Pagamentos{% endblock %} {% block title %}Comprovantes{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid mt-3"> <div class="container-fluid mt-3">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="fas fa-money-bill-wave"></i> Pagamentos</h2> <h2><i class="fas fa-money-bill-wave"></i> Comprovantes</h2>
<div> <div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoPagamento"> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoComprovante">
<i class="fas fa-plus"></i> Novo Pagamento <i class="fas fa-plus"></i> Novo Comprovante
</button> </button>
<button type="button" class="btn btn-outline-primary" id="btnExportar"> <button type="button" class="btn btn-outline-primary" id="btnExportar">
<i class="fas fa-file-export"></i> Exportar <i class="fas fa-file-export"></i> Exportar
@@ -19,56 +19,62 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover" id="tabelaPagamentos"> <table class="table table-striped table-hover" id="tabelaComprovantes">
<thead> <thead>
<tr> <tr>
<th>Militante</th> <th>Militante</th>
<th>Tipo de Pagamento</th> <th>Tipo de Comprovante</th>
<th>Valor</th> <th>Valor</th>
<th>Data do Pagamento</th> <th>Data do Comprovante</th>
<th>Ações</th> <th>Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for pagamento in pagamentos %} {% for comprovante in comprovantes %}
<tr> <tr>
<td data-militante="{{ pagamento.militante_id }}">{{ pagamento.militante.nome if pagamento.militante else 'N/A' }}</td> <td data-militante="{{ comprovante.militante_id }}">{{ comprovante.militante.nome if comprovante.militante else 'N/A' }}</td>
<td data-tipo="{{ pagamento.tipo_pagamento }}"> <td data-tipo="{{ comprovante.tipo_comprovante }}">
{% if pagamento.tipo_pagamento == 1 %} {% if comprovante.tipo_comprovante == 1 %}
Mensalidade Cota
{% elif pagamento.tipo_pagamento == 2 %} {% elif comprovante.tipo_comprovante == 2 %}
Contribuição Extra Contribuição Extra
{% elif pagamento.tipo_pagamento == 3 %} {% elif comprovante.tipo_comprovante == 3 %}
Doação Doação
{% elif pagamento.tipo_pagamento == 4 %} {% elif comprovante.tipo_comprovante == 4 %}
Taxa de Evento Taxa de Evento
{% elif pagamento.tipo_pagamento == 5 %} {% elif comprovante.tipo_comprovante == 5 %}
Jornal Avulso
{% elif comprovante.tipo_comprovante == 6 %}
Assinatura de Jornal
{% elif comprovante.tipo_comprovante == 7 %}
Campanha Financeira
{% elif comprovante.tipo_comprovante == 8 %}
Outros Outros
{% else %} {% else %}
Não Definido Não Definido
{% endif %} {% endif %}
</td> </td>
<td data-valor="{{ pagamento.valor }}">R$ {{ "%.2f"|format(pagamento.valor) }}</td> <td data-valor="{{ comprovante.valor }}">R$ {{ "%.2f"|format(comprovante.valor) }}</td>
<td data-data="{{ pagamento.data_pagamento }}">{{ pagamento.data_pagamento.strftime('%d/%m/%Y') }}</td> <td data-data="{{ comprovante.data_comprovante }}">{{ comprovante.data_comprovante.strftime('%d/%m/%Y') }}</td>
<td> <td>
<button type="button" <button type="button"
class="btn btn-sm btn-outline-primary" class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#modalEditarPagamento" data-bs-target="#modalEditarComprovante"
data-pagamento-id="{{ pagamento.id }}" data-comprovante-id="{{ comprovante.id }}"
data-militante-id="{{ pagamento.militante_id }}" data-militante-id="{{ comprovante.militante_id }}"
data-tipo-pagamento="{{ pagamento.tipo_pagamento }}" data-tipo-comprovante="{{ comprovante.tipo_comprovante }}"
data-valor="{{ pagamento.valor }}" data-valor="{{ comprovante.valor }}"
data-data-pagamento="{{ pagamento.data_pagamento.strftime('%Y-%m-%d') }}" data-data-comprovante="{{ comprovante.data_comprovante.strftime('%Y-%m-%d') }}"
title="Editar"> title="Editar">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</button> </button>
<button type="button" <button type="button"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#modalExcluirPagamento" data-bs-target="#modalExcluirComprovante"
data-pagamento-id="{{ pagamento.id }}" data-comprovante-id="{{ comprovante.id }}"
data-pagamento-info="Pagamento de {{ pagamento.militante.nome if pagamento.militante else 'N/A' }} - R$ {{ "%.2f"|format(pagamento.valor) }}" data-comprovante-info="Comprovante de {{ comprovante.militante.nome if comprovante.militante else 'N/A' }} - R$ {{ "%.2f"|format(comprovante.valor) }}"
title="Excluir"> title="Excluir">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
@@ -82,16 +88,16 @@
</div> </div>
</div> </div>
<!-- Modal Novo Pagamento --> <!-- Modal Novo Comprovante -->
<div class="modal fade" id="modalNovoPagamento" tabindex="-1"> <div class="modal fade" id="modalNovoComprovante" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><i class="fas fa-plus"></i> Novo Pagamento</h5> <h5 class="modal-title"><i class="fas fa-plus"></i> Novo Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="formNovoPagamento" method="post" action="{{ url_for('adicionar_pagamento') }}"> <form id="formNovoComprovante" method="post" action="{{ url_for('adicionar_comprovante') }}">
<div class="mb-3"> <div class="mb-3">
<label for="militante" class="form-label">Militante:</label> <label for="militante" class="form-label">Militante:</label>
<select class="form-select" id="militante" name="militante_id" required> <select class="form-select" id="militante" name="militante_id" required>
@@ -102,14 +108,19 @@
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="tipoPagamento" class="form-label">Tipo de Pagamento:</label> <label for="tipoComprovante" class="form-label">Tipo de Comprovante:</label>
<select class="form-select" id="tipoPagamento" name="tipo_pagamento" required> <select class="form-select" id="tipoComprovante" name="tipo_comprovante" required>
<option value="">Selecione o tipo</option> <option value="">Selecione o tipo</option>
<option value="1">Mensalidade</option> <option value="1">Cota</option>
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
<option value="2">Contribuição Extra</option> <option value="2">Contribuição Extra</option>
<option value="3">Doação</option> <option value="3">Doação</option>
<option value="4">Taxa de Evento</option> <option value="4">Taxa de Evento</option>
<option value="5">Outros</option> <option value="5">Jornal Avulso</option>
<option value="6">Assinatura de Jornal</option>
<option value="7">Campanha Financeira</option>
<option value="8">Outros</option>
{% endif %}
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -117,8 +128,8 @@
<input type="number" step="0.01" class="form-control" id="valor" name="valor" required> <input type="number" step="0.01" class="form-control" id="valor" name="valor" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="dataPagamento" class="form-label">Data do Pagamento:</label> <label for="dataComprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="dataPagamento" name="data_pagamento" required> <input type="date" class="form-control" id="dataComprovante" name="data_comprovante" required>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
@@ -130,30 +141,35 @@
</div> </div>
</div> </div>
<!-- Modal Editar Pagamento --> <!-- Modal Editar Comprovante -->
<div class="modal fade" id="modalEditarPagamento" tabindex="-1"> <div class="modal fade" id="modalEditarComprovante" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><i class="fas fa-edit"></i> Editar Pagamento</h5> <h5 class="modal-title"><i class="fas fa-edit"></i> Editar Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="formEditarPagamento" method="post"> <form id="formEditarComprovante" method="post">
<div class="mb-3"> <div class="mb-3">
<label for="editMilitante" class="form-label">Militante:</label> <label for="editMilitante" class="form-label">Militante:</label>
<input type="text" class="form-control bg-light" id="editMilitanteNome" readonly> <input type="text" class="form-control bg-light" id="editMilitanteNome" readonly>
<input type="hidden" id="editMilitante" name="militante_id"> <input type="hidden" id="editMilitante" name="militante_id">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="editTipoPagamento" class="form-label">Tipo de Pagamento:</label> <label for="editTipoComprovante" class="form-label">Tipo de Comprovante:</label>
<select class="form-select" id="editTipoPagamento" name="tipo_pagamento" required> <select class="form-select" id="editTipoComprovante" name="tipo_comprovante" required>
<option value="">Selecione o tipo</option> <option value="">Selecione o tipo</option>
<option value="1">Mensalidade</option> <option value="1">Cota</option>
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
<option value="2">Contribuição Extra</option> <option value="2">Contribuição Extra</option>
<option value="3">Doação</option> <option value="3">Doação</option>
<option value="4">Taxa de Evento</option> <option value="4">Taxa de Evento</option>
<option value="5">Outros</option> <option value="5">Jornal Avulso</option>
<option value="6">Assinatura de Jornal</option>
<option value="7">Campanha Financeira</option>
<option value="8">Outros</option>
{% endif %}
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -161,8 +177,8 @@
<input type="number" step="0.01" class="form-control" id="editValor" name="valor" required> <input type="number" step="0.01" class="form-control" id="editValor" name="valor" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="editDataPagamento" class="form-label">Data do Pagamento:</label> <label for="editDataComprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="editDataPagamento" name="data_pagamento" required> <input type="date" class="form-control" id="editDataComprovante" name="data_comprovante" required>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
@@ -174,20 +190,20 @@
</div> </div>
</div> </div>
<!-- Modal Excluir Pagamento --> <!-- Modal Excluir Comprovante -->
<div class="modal fade" id="modalExcluirPagamento" tabindex="-1"> <div class="modal fade" id="modalExcluirComprovante" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><i class="fas fa-trash"></i> Excluir Pagamento</h5> <h5 class="modal-title"><i class="fas fa-trash"></i> Excluir Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>Tem certeza que deseja excluir este pagamento?</p> <p>Tem certeza que deseja excluir este comprovante?</p>
<p id="pagamentoInfo" class="text-muted"></p> <p id="comprovanteInfo" class="text-muted"></p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<form id="formExcluirPagamento" method="post"> <form id="formExcluirComprovante" method="post">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-danger">Excluir</button> <button type="submit" class="btn btn-danger">Excluir</button>
</form> </form>
@@ -198,6 +214,6 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ url_for('static', filename='js/pagamentos.js') }}"></script> <script src="{{ url_for('static', filename='js/comprovantes.js') }}"></script>
{% endblock %} {% endblock %}

View File

@@ -1,12 +1,12 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Listar Relatórios de Pagamentos{% endblock %} {% block title %}Listar Relatórios de Comprovantes{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h1 class="mb-4">Lista de Relatórios de Pagamentos</h1> <h1 class="mb-4">Lista de Relatórios de Comprovantes</h1>
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
@@ -17,7 +17,7 @@
{% endwith %} {% endwith %}
<div class="d-flex justify-content-between mb-4"> <div class="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_relatorio_pagamentos') }}" class="btn btn-success">Novo Relatório</a> <a href="{{ url_for('novo_relatorio_comprovantes') }}" class="btn btn-success">Novo Relatório</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a> <a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div> </div>
@@ -28,7 +28,7 @@
<th>ID</th> <th>ID</th>
<th>Setor</th> <th>Setor</th>
<th>Comitê Central</th> <th>Comitê Central</th>
<th>Total de Pagamentos</th> <th>Total de Comprovantes</th>
<th>Data do Relatório</th> <th>Data do Relatório</th>
<th>Ações</th> <th>Ações</th>
</tr> </tr>
@@ -39,11 +39,11 @@
<td>{{ relatorio.id }}</td> <td>{{ relatorio.id }}</td>
<td>{{ relatorio.setor.nome }}</td> <td>{{ relatorio.setor.nome }}</td>
<td>{{ relatorio.comite.nome }}</td> <td>{{ relatorio.comite.nome }}</td>
<td>R$ {{ "%.2f"|format(relatorio.total_pagamentos) }}</td> <td>R$ {{ "%.2f"|format(relatorio.total_comprovantes) }}</td>
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td> <td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
<td> <td>
<a href="{{ url_for('editar_relatorio_pagamentos', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a> <a href="{{ url_for('editar_relatorio_comprovantes', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_relatorio_pagamentos', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a> <a href="{{ url_for('deletar_relatorio_comprovantes', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -1,6 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Novo Pagamento{% endblock %} {% block title %}Novo Comprovante{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
@@ -9,7 +9,7 @@
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h4 class="card-title mb-0"> <h4 class="card-title mb-0">
<i class="fas fa-money-bill-wave me-2"></i>Registrar Novo Pagamento <i class="fas fa-money-bill-wave me-2"></i>Registrar Novo Comprovante
</h4> </h4>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -27,17 +27,18 @@
</div> </div>
</div> </div>
<div class="mb-3"> <div class="form-group">
<label for="tipo_pagamento_id" class="form-label">Tipo de Pagamento:</label> <label for="tipo_comprovante_id">Tipo de Comprovante</label>
<select class="form-select" id="tipo_pagamento_id" name="tipo_pagamento_id" required> <select class="form-control" id="tipo_comprovante_id" name="tipo_comprovante_id" required>
<option value="">Selecione o tipo de pagamento</option> <option value="1">1 - Comprovante Padrão</option>
{% for tipo in tipos_pagamento %} {% if current_user.has_permission('gerenciar_tipos_comprovante') %}
<option value="{{ tipo.id }}">{{ tipo.descricao }}</option> <option value="2">2 - Comprovante Especial</option>
{% endfor %} <option value="3">3 - Comprovante Extraordinário</option>
<option value="4">4 - Jornal Avulso</option>
<option value="5">5 - Assinatura de Jornal</option>
<option value="6">6 - Campanha Financeira</option>
{% endif %}
</select> </select>
<div class="invalid-feedback">
Por favor, selecione o tipo de pagamento.
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -52,8 +53,8 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="data_pagamento" class="form-label">Data do Pagamento:</label> <label for="data_comprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="data_pagamento" name="data_pagamento" <input type="date" class="form-control" id="data_comprovante" name="data_comprovante"
required max="{{ hoje }}"> required max="{{ hoje }}">
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, informe uma data válida. Por favor, informe uma data válida.
@@ -64,7 +65,7 @@
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Registrar <i class="fas fa-save me-1"></i>Registrar
</button> </button>
<a href="{{ url_for('listar_pagamentos') }}" class="btn btn-secondary"> <a href="{{ url_for('listar_comprovantes') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i>Voltar <i class="fas fa-arrow-left me-1"></i>Voltar
</a> </a>
</div> </div>

View File

@@ -1,12 +1,12 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Novo Relatório de Pagamentos{% endblock %} {% block title %}Novo Relatório de Comprovantes{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h1 class="mb-4">Novo Relatório de Pagamentos</h1> <h1 class="mb-4">Novo Relatório de Comprovantes</h1>
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
@@ -44,10 +44,10 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="total_pagamentos" class="form-label">Total de Pagamentos</label> <label for="total_comprovantes" class="form-label">Total de Comprovantes</label>
<input type="number" class="form-control" id="total_pagamentos" name="total_pagamentos" step="0.01" required> <input type="number" class="form-control" id="total_comprovantes" name="total_comprovantes" step="0.01" required>
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, insira o total de pagamentos. Por favor, insira o total de comprovantes.
</div> </div>
</div> </div>
@@ -61,7 +61,7 @@
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Registrar</button> <button type="submit" class="btn btn-success">Registrar</button>
<a href="{{ url_for('listar_relatorios_pagamentos') }}" class="btn btn-outline-secondary">Voltar</a> <a href="{{ url_for('listar_relatorios_comprovantes') }}" class="btn btn-outline-secondary">Voltar</a>
</div> </div>
</form> </form>
</div> </div>