8 Commits

42 changed files with 1398 additions and 2196 deletions

View File

@@ -1,7 +1,19 @@
.PHONY: install run test clean refresh
install: install:
pip install -r requirements.txt pip install -r requirements.txt
pip install pytest pytest-cov
clean: clean:
find . -type d -name "__pycache__" -exec rm -r {} +
find . -type f -name "*.pyc" -delete
find . -type f -name "*.pyo" -delete
find . -type f -name "*.pyd" -delete
find . -type f -name ".coverage" -delete
find . -type d -name "*.egg-info" -exec rm -r {} +
find . -type d -name "*.egg" -exec rm -r {} +
find . -type d -name ".pytest_cache" -exec rm -r {} +
find . -type d -name "htmlcov" -exec rm -r {} +
rm -rf ~/.local/share/controles/database.db* rm -rf ~/.local/share/controles/database.db*
rm -f admin_qr.png rm -f admin_qr.png
@@ -11,13 +23,16 @@ init-db: clean
seed: init-db seed: init-db
python seed.py python seed.py
init:
python app.py --init
run: run:
python app.py python app.py
run-with-seed: seed init run run-with-seed: seed run
reset-admin: clean reset-admin: clean
python create_admin.py python create_admin.py
test:
pytest tests/ --cov=app --cov=functions --cov-report=term-missing
refresh: clean install test
python app.py

857
app.py

File diff suppressed because it is too large Load Diff

View File

@@ -41,111 +41,37 @@ def generate_qr_code(user):
return qr_path, otp_uri return qr_path, otp_uri
def create_admin_user(): def create_admin_user():
"""Cria ou atualiza o usuário admin""" """Cria o usuário admin do sistema"""
session = get_db_connection()
try: try:
# Inicializar banco de dados # Buscar role de administrador
init_database() admin_role = session.query(Role).filter_by(nome="Administrador").first()
if not admin_role:
print("Role de administrador não encontrada!")
return
# Criar sessão # Verificar se o usuário admin já existe
db = get_db_connection() if not session.query(Usuario).filter_by(username="admin").first():
admin = Usuario(
try: username="admin",
# Verificar se já existe um usuário admin email="admin@example.com",
admin = db.query(Usuario).filter_by(username="admin").first() is_admin=True
)
if admin: admin.set_password("admin123")
print("\n=== Usuário Admin Encontrado ===") admin.tipo = "ADMIN"
if not admin.otp_secret: admin.roles.append(admin_role)
print("Gerando novo segredo OTP...") session.add(admin)
admin.generate_otp_secret() session.commit()
db.commit() print("Usuário admin criado com sucesso!")
else: else:
print("\n=== Criando Novo Usuário Admin ===") print("Usuário admin já existe!")
# Criar novo usuário admin
admin = Usuario(
username="admin",
email="admin@example.com",
is_admin=True
)
admin.set_password("admin123")
admin.generate_otp_secret()
# Buscar ou criar role de admin
admin_role = db.query(Role).filter_by(nome="admin").first()
if not admin_role:
admin_role = Role(nome="admin", nivel=0) # Nível 0 é o mais alto
db.add(admin_role)
# Adicionar role ao usuário
admin.roles.append(admin_role)
# Adicionar e fazer commit
db.add(admin)
db.commit()
# Gerar QR code apenas se solicitado ou se for novo usuário
if not os.path.exists('admin_qr.png'):
qr_path, otp_uri = generate_qr_code(admin)
print("\n=== QR Code Gerado ===")
print(f"QR Code salvo em: {qr_path}")
print(f"URI do OTP: {otp_uri}")
else:
print("\n=== QR Code Existente ===")
print("Usando QR Code existente em: admin_qr.png")
qr_path = 'admin_qr.png'
# Mostrar informações
print("\n=== Informações do Admin ===")
print(f"Username: {admin.username}")
print(f"Email: {admin.email}")
print(f"Senha: admin123")
print(f"Segredo OTP: {admin.otp_secret}")
# Gerar código atual para verificação
totp = pyotp.TOTP(admin.otp_secret)
current_code = totp.now()
print("\n=== Verificação do OTP ===")
print(f"Código OTP atual: {current_code}")
print(f"Verificação do código: {totp.verify(current_code)}")
print("\n=== Instruções para Configuração ===")
print("1. Instale um aplicativo autenticador no seu celular")
print(" (Google Authenticator, Microsoft Authenticator, etc)")
print("2. Abra o aplicativo")
print("3. Selecione a opção para adicionar uma nova conta")
print("4. Escaneie o QR Code salvo em:", qr_path)
print("\nOU configure manualmente:")
print(f"- Nome da conta: {admin.username}")
print(f"- Segredo: {admin.otp_secret}")
print("- Tipo: Baseado em tempo (TOTP)")
print("- Algoritmo: SHA1")
print("- Dígitos: 6")
print("- Intervalo: 30 segundos")
# Verificação final
print("\n=== Teste de Verificação ===")
test_code = totp.now()
print(f"Código de teste: {test_code}")
is_valid = admin.verify_otp(test_code)
print(f"Verificação do código: {'Sucesso' if is_valid else 'Falha'}")
if not is_valid:
print("\nALERTA: Verificação do OTP falhou!")
print("Por favor, verifique se o segredo OTP está correto.")
# Fazer commit final para garantir que tudo foi salvo
db.commit()
except Exception as e:
db.rollback()
raise e
finally:
db.close()
except Exception as e: except Exception as e:
print(f"\nErro durante a execução: {e}") print(f"Erro ao criar usuário admin: {e}")
import traceback session.rollback()
traceback.print_exc() raise
finally:
session.close()
if __name__ == "__main__": if __name__ == "__main__":
create_admin_user() create_admin_user()

View File

@@ -1,56 +1,65 @@
from functions.database import get_db_connection, Usuario, Role from functions.database import Usuario, Role, get_db_connection
from werkzeug.security import generate_password_hash
def create_test_users(): def create_test_users():
"""Cria usuários de teste""" """Cria usuários de teste para o sistema"""
db = get_db_connection() session = get_db_connection()
try: try:
# Lista de usuários de teste # Buscar roles
test_users = [ secretario_celula = session.query(Role).filter_by(nivel=Role.SECRETARIO_CELULA).first()
secretario_setor = session.query(Role).filter_by(nivel=Role.SECRETARIO_SETOR).first()
secretario_cr = session.query(Role).filter_by(nivel=Role.SECRETARIO_CR).first()
secretario_geral = session.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
# Criar usuários de teste
usuarios = [
{ {
'username': 'aligner', 'username': 'celula',
'email': 'aligner@test.com', 'email': 'celula@example.com',
'password': 'Test123!@#', 'password': 'celula123',
'is_admin': False 'role': secretario_celula,
'tipo': 'SECRETARIO_CELULA'
}, },
{ {
'username': 'tester', 'username': 'setor',
'email': 'tester@test.com', 'email': 'setor@example.com',
'password': 'Test123!@#', 'password': 'setor123',
'is_admin': False 'role': secretario_setor,
'tipo': 'SECRETARIO_SETOR'
}, },
{ {
'username': 'deployer', 'username': 'cr',
'email': 'deployer@test.com', 'email': 'cr@example.com',
'password': 'Test123!@#', 'password': 'cr123',
'is_admin': False 'role': secretario_cr,
'tipo': 'SECRETARIO_CR'
},
{
'username': 'geral',
'email': 'geral@example.com',
'password': 'geral123',
'role': secretario_geral,
'tipo': 'SECRETARIO_GERAL'
} }
] ]
# Criar cada usuário for user_data in usuarios:
for user_data in test_users: # Verificar se o usuário já existe
user = db.query(Usuario).filter_by(username=user_data['username']).first() if not session.query(Usuario).filter_by(username=user_data['username']).first():
if not user:
user = Usuario( user = Usuario(
username=user_data['username'], username=user_data['username'],
email=user_data['email'], email=user_data['email']
is_admin=user_data['is_admin']
) )
user.set_password(user_data['password']) user.set_password(user_data['password'])
db.add(user) user.tipo = user_data['tipo']
print(f"Usuário {user_data['username']} criado") user.roles.append(user_data['role'])
else: session.add(user)
print(f"Usuário {user_data['username']} já existe")
db.commit() session.commit()
print("Usuários de teste criados com sucesso") print("Usuários de teste criados com sucesso!")
except Exception as e: except Exception as e:
print(f"Erro ao criar usuários de teste: {str(e)}") print(f"Erro ao criar usuários de teste: {e}")
db.rollback() session.rollback()
raise
finally: finally:
db.close() session.close()
if __name__ == "__main__":
create_test_users()

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

@@ -0,0 +1,94 @@
# Regras de Negócio - Comprovantes
## 1. Estrutura do Comprovante
### 1.1 Dados Básicos
- Todo comprovante deve ter:
- Militante associado (obrigatório)
- Data do comprovante (obrigatório)
- Forma de pagamento (obrigatório)
- Campanha financeira (opcional)
### 1.2 Formas de Pagamento
- As formas de pagamento aceitas são:
- PIX
- Transferência/DOC
- Depósito
- Maquininha
## 2. Centralizações
### 2.1 Tipos de Centralização
- Cada comprovante pode ter uma ou mais centralizações
- Os tipos de centralização são:
- Cota
- Jornal
- Assinatura
### 2.2 Valores
- Cada centralização deve ter:
- Tipo (obrigatório)
- Valor (obrigatório, maior que zero)
## 3. Transações PIX
### 3.1 Dados da Transação
- Para pagamentos via PIX, o comprovante deve incluir:
- Chave PIX
- Valor
- Data de geração
- Data de pagamento
- Status (Pendente, Pago, Expirado)
- QR Code (quando aplicável)
## 4. Validações
### 4.1 Obrigatoriedades
- Um comprovante deve ter pelo menos uma centralização
- O valor total do comprovante deve ser igual à soma das centralizações
- A data do comprovante não pode ser futura
### 4.2 Restrições
- Não é permitido excluir comprovantes com centralizações já registradas
- Não é permitido alterar valores de centralizações após confirmação
- O militante associado deve estar ativo no sistema
## 5. Permissões
### 5.1 Acesso
- Apenas usuários com permissão `MANAGE_MATERIALS` podem:
- Criar comprovantes
- Editar comprovantes
- Excluir comprovantes
- Visualizar lista de comprovantes
### 5.2 Restrições
- Usuários só podem editar comprovantes de sua própria célula/setor/CR
- Apenas administradores podem editar comprovantes de qualquer nível
## 6. Relacionamentos
### 6.1 Militante
- Todo comprovante deve estar associado a um militante
- O militante deve estar ativo no sistema
- O militante deve pertencer a uma célula/setor/CR válido
### 6.2 Campanha Financeira
- O comprovante pode estar associado a uma campanha financeira
- A campanha deve estar ativa no período do comprovante
- O valor do comprovante é contabilizado no total da campanha
## 7. Histórico
### 7.1 Registro
- Todas as alterações em comprovantes devem ser registradas
- O sistema mantém histórico de:
- Data de criação
- Usuário que criou
- Data de alteração
- Usuário que alterou
### 7.2 Auditoria
- Os comprovantes são auditáveis
- O sistema mantém logs de todas as operações
- As alterações podem ser rastreadas por usuário e data

View File

@@ -14,6 +14,9 @@ from flask_login import UserMixin
from .rbac import Role, Permission, role_permissions, user_roles from .rbac import Role, Permission, role_permissions, user_roles
from .base import Base, engine, Session from .base import Base, engine, Session
import logging import logging
import qrcode
from PIL import Image
import re
# Configurar caminho do banco de dados # Configurar caminho do banco de dados
db_dir = Path.home() / '.local' / 'share' / 'controles' db_dir = Path.home() / '.local' / 'share' / 'controles'
@@ -190,6 +193,7 @@ class Militante(Base):
vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante") vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante")
assinaturas = relationship("AssinaturaAnual", back_populates="militante") assinaturas = relationship("AssinaturaAnual", 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")
# Constantes para responsabilidades # Constantes para responsabilidades
SECRETARIO = 1 SECRETARIO = 1
@@ -328,7 +332,6 @@ class Pagamento(Base):
data_pagamento = Column(Date, nullable=False) data_pagamento = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="pagamentos") militante = relationship("Militante", back_populates="pagamentos")
transacoes_pix = relationship("TransacaoPIX", back_populates="pagamento")
class TipoMaterial(Base): class TipoMaterial(Base):
__tablename__ = 'tipos_materiais' __tablename__ = 'tipos_materiais'
@@ -441,7 +444,6 @@ class Usuario(Base, UserMixin):
username = Column(String(50), unique=True, nullable=False) username = Column(String(50), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False) password_hash = Column(String(255), nullable=False)
email = Column(String(100), unique=True, nullable=False) email = Column(String(100), unique=True, nullable=False)
nome = Column(String(100)) # Nome completo do usuário
otp_secret = Column(String(32)) otp_secret = Column(String(32))
role_id = Column(Integer, ForeignKey('roles.id')) role_id = Column(Integer, ForeignKey('roles.id'))
setor_id = Column(Integer, ForeignKey('setores.id')) setor_id = Column(Integer, ForeignKey('setores.id'))
@@ -465,11 +467,11 @@ class Usuario(Base, UserMixin):
cr = relationship('ComiteRegional', back_populates='usuarios') cr = relationship('ComiteRegional', back_populates='usuarios')
celula = relationship('Celula', back_populates='usuarios') celula = relationship('Celula', back_populates='usuarios')
def __init__(self, username, email=None, is_admin=False, nome=None): def __init__(self, username, email=None, is_admin=False):
self.username = username self.username = username
self.email = email self.email = email
self.is_admin = is_admin self.is_admin = is_admin
self.nome = nome self.email = email
self.ativo = True self.ativo = True
self.session_timeout = 30 self.session_timeout = 30
self.tipo = "USUARIO" self.tipo = "USUARIO"
@@ -550,10 +552,6 @@ class Usuario(Base, UserMixin):
self.motivo_logout = "Logout manual" self.motivo_logout = "Logout manual"
self.ultima_atividade = None 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)
class PagamentoCelula(Base): class PagamentoCelula(Base):
__tablename__ = 'pagamentos_celula' __tablename__ = 'pagamentos_celula'
@@ -612,6 +610,49 @@ class Relatorio(Base):
setor = relationship("Setor", foreign_keys=[setor_id]) setor = relationship("Setor", foreign_keys=[setor_id])
cr = relationship("ComiteRegional", foreign_keys=[cr_id]) cr = relationship("ComiteRegional", foreign_keys=[cr_id])
class CampanhaFinanceira(Base):
__tablename__ = 'campanhas_financeiras'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(100), nullable=False)
descricao = Column(Text)
data_inicio = Column(Date, nullable=False)
data_fim = Column(Date, nullable=False)
meta = Column(Numeric(10, 2), nullable=False)
valor_arrecadado = Column(Numeric(10, 2), default=0)
status = Column(String(20), default='Em andamento') # Em andamento, Concluída, Cancelada
comprovantes = relationship("Comprovante", back_populates="campanha")
class TipoComprovante(Base):
__tablename__ = 'tipos_comprovante'
id = Column(Integer, primary_key=True)
descricao = Column(String(50), nullable=False)
valor = Column(Numeric(10, 2), nullable=False)
class CentralizacaoComprovante(Base):
__tablename__ = 'centralizacoes_comprovante'
id = Column(Integer, primary_key=True, autoincrement=True)
comprovante_id = Column(Integer, ForeignKey('comprovantes.id'), nullable=False)
tipo_comprovante = Column(String(50), nullable=False) # Cota, Jornal, Assinatura, etc.
valor = Column(Numeric(10, 2), nullable=False)
comprovante = relationship("Comprovante", back_populates="centralizacoes")
class Comprovante(Base):
__tablename__ = 'comprovantes'
id = Column(Integer, primary_key=True)
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
data_comprovante = Column(Date, nullable=False)
forma_pagamento = Column(String(20), nullable=False) # PIX, transferência/DOC, depósito, maquininha
campanha_id = Column(Integer, ForeignKey('campanhas_financeiras.id'))
militante = relationship("Militante", back_populates="comprovantes")
transacoes_pix = relationship("TransacaoPIX", back_populates="comprovante")
campanha = relationship("CampanhaFinanceira", back_populates="comprovantes")
centralizacoes = relationship("CentralizacaoComprovante", back_populates="comprovante", cascade="all, delete-orphan")
class TransacaoPIX(Base): class TransacaoPIX(Base):
__tablename__ = 'transacoes_pix' __tablename__ = 'transacoes_pix'
@@ -622,9 +663,9 @@ class TransacaoPIX(Base):
data_pagamento = Column(DateTime) data_pagamento = Column(DateTime)
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')) comprovante_id = Column(Integer, ForeignKey('comprovantes.id'))
pagamento = relationship("Pagamento", back_populates="transacoes_pix") comprovante = relationship("Comprovante", back_populates="transacoes_pix")
def init_database(): def init_database():
"""Inicializa o banco de dados com dados básicos""" """Inicializa o banco de dados com dados básicos"""
@@ -665,9 +706,30 @@ def init_database():
session.add(comite) session.add(comite)
session.commit() session.commit()
# Gerar OTP para admin # Verificar se existe QR code do admin
admin_otp_secret = pyotp.random_base32() admin_otp_secret = None
print(f"Novo OTP gerado: {admin_otp_secret}") qr_path = 'admin_qr.png'
if os.path.exists(qr_path):
try:
# Tentar ler o QR code existente
from pyzbar.pyzbar import decode
qr_data = decode(Image.open(qr_path))
if qr_data:
# O URI do OTP está no formato: otpauth://totp/Sistema%20de%20Controles:admin?secret=XXXXX&issuer=Sistema%20de%20Controles
uri = qr_data[0].data.decode('utf-8')
# Extrair o secret do URI
match = re.search(r'secret=([A-Z0-9]+)', uri)
if match:
admin_otp_secret = match.group(1)
print("OTP existente encontrado no QR code")
except Exception as e:
print(f"Erro ao ler QR code existente: {e}")
if not admin_otp_secret:
# Se não conseguiu ler o QR code ou ele não existe, gera um novo
admin_otp_secret = pyotp.random_base32()
print(f"Novo OTP gerado: {admin_otp_secret}")
# Criar usuário admin # Criar usuário admin
admin_role = session.query(Role).filter_by(nome="Administrador").first() admin_role = session.query(Role).filter_by(nome="Administrador").first()
@@ -686,23 +748,23 @@ def init_database():
session.add(admin) session.add(admin)
session.commit() session.commit()
# Gerar QR code # Gerar QR code apenas se não existir
totp = pyotp.totp.TOTP(admin_otp_secret) if not os.path.exists(qr_path):
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles") totp = pyotp.totp.TOTP(admin_otp_secret)
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
import qrcode qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr = qrcode.QRCode(version=1, box_size=10, border=5) qr.add_data(provisioning_uri)
qr.add_data(provisioning_uri) qr.make(fit=True)
qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white")
img = qr.make_image(fill_color="black", back_color="white") img.save(qr_path)
img.save('admin_qr.png')
print("=== Usuário Admin Criado ===") print("=== Usuário Admin Criado ===")
print(f"Username: admin") print(f"Username: admin")
print(f"Senha: admin123") print(f"Senha: admin123")
print(f"Email: {admin.email}") print(f"Email: {admin.email}")
print(f"OTP Secret: {admin_otp_secret}") print(f"OTP Secret: {admin_otp_secret}")
print(f"QR Code: admin_qr.png") print(f"QR Code: {qr_path}")
# Importar e executar o seed após criar todas as dependências # Importar e executar o seed após criar todas as dependências
from seed_data import seed_database from seed_data import seed_database

View File

@@ -2,7 +2,7 @@ from functools import wraps
from flask import session, redirect, url_for, flash from flask import session, redirect, url_for, flash
from flask_login import current_user, login_required from flask_login import current_user, login_required
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from .database import get_db_connection, Usuario, Role from .database import get_db_connection, Usuario
from .rbac import Permission from .rbac import Permission
def require_login(f): def require_login(f):
@@ -15,13 +15,9 @@ def require_login(f):
db = get_db_connection() db = get_db_connection()
try: try:
# Carregar o usuário com suas roles e permissões # Carregar o usuário com suas roles
user = db.query(Usuario).options( user = db.query(Usuario).options(
joinedload(Usuario.roles).joinedload(Role.permissions), joinedload(Usuario.roles)
joinedload(Usuario.militante),
joinedload(Usuario.cr),
joinedload(Usuario.setor),
joinedload(Usuario.celula)
).get(current_user.id) ).get(current_user.id)
if not user: if not user:
@@ -32,15 +28,7 @@ def require_login(f):
user.update_last_activity() user.update_last_activity()
db.commit() db.commit()
# Substituir o current_user pelo usuário carregado
setattr(current_user, '_get_current_object', lambda: user)
# Executar a função com o usuário carregado
return f(*args, **kwargs) return f(*args, **kwargs)
except Exception as e:
db.rollback()
flash('Erro ao carregar dados do usuário.', 'danger')
return redirect(url_for('login'))
finally: finally:
db.close() db.close()
return decorated_function return decorated_function
@@ -51,38 +39,14 @@ def require_permission(permission_name):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not current_user.is_authenticated: if not current_user.is_authenticated:
flash('Você precisa estar logado para acessar esta página.', 'error') flash('Por favor, faça login para acessar esta página.', 'danger')
return redirect(url_for('login')) return redirect(url_for('login'))
db = get_db_connection() if not current_user.has_permission(permission_name):
try: flash('Você não tem permissão para acessar esta página.', 'danger')
# Carregar o usuário com suas roles e permissões return redirect(url_for('home'))
user = db.query(Usuario).options(
joinedload(Usuario.roles).joinedload(Role.permissions),
joinedload(Usuario.militante),
joinedload(Usuario.cr),
joinedload(Usuario.setor),
joinedload(Usuario.celula)
).get(current_user.id)
if not user: return f(*args, **kwargs)
flash('Usuário não encontrado.', 'error')
return redirect(url_for('login'))
if not user.has_permission(permission_name):
flash('Você não tem permissão para acessar esta página.', 'error')
return redirect(url_for('index'))
# Atualiza timestamp da última atividade
user.update_last_activity()
db.commit()
# Substituir o current_user pelo usuário carregado
setattr(current_user, '_get_current_object', lambda: user)
return f(*args, **kwargs)
finally:
db.close()
return decorated_function return decorated_function
return decorator return decorator

View File

@@ -68,6 +68,8 @@ class Permission(Base):
EDIT_OWN_DATA = "edit_own_data" EDIT_OWN_DATA = "edit_own_data"
VIEW_CELL_DATA = "view_cell_data" VIEW_CELL_DATA = "view_cell_data"
CREATE_MILITANT = "create_militant" # Nova permissão para criar militantes CREATE_MILITANT = "create_militant" # Nova permissão para criar militantes
MANAGE_MATERIALS = "manage_materials" # Nova permissão para gerenciar materiais
MANAGE_REPORTS = "manage_reports" # Nova permissão para gerenciar relatórios
# Permissões de célula # Permissões de célula
MANAGE_CELL_MEMBERS = "manage_cell_members" MANAGE_CELL_MEMBERS = "manage_cell_members"
@@ -102,13 +104,15 @@ class Permission(Base):
(Permission.VIEW_OWN_DATA, "Visualizar próprios dados"), (Permission.VIEW_OWN_DATA, "Visualizar próprios dados"),
(Permission.EDIT_OWN_DATA, "Editar próprios dados"), (Permission.EDIT_OWN_DATA, "Editar próprios dados"),
(Permission.VIEW_CELL_DATA, "Visualizar dados da célula"), (Permission.VIEW_CELL_DATA, "Visualizar dados da célula"),
(Permission.CREATE_MILITANT, "Criar novos militantes"), # Nova permissão (Permission.CREATE_MILITANT, "Criar novos militantes"),
(Permission.MANAGE_MATERIALS, "Gerenciar materiais"),
(Permission.MANAGE_REPORTS, "Gerenciar relatórios"),
# Permissões de célula # Permissões de célula
(Permission.MANAGE_CELL_MEMBERS, "Gerenciar membros da célula"), (Permission.MANAGE_CELL_MEMBERS, "Gerenciar membros da célula"),
(Permission.CREATE_CELL_MEMBER, "Criar membros na célula"), (Permission.CREATE_CELL_MEMBER, "Criar membros na célula"),
(Permission.VIEW_CELL_REPORTS, "Visualizar relatórios da célula"), (Permission.VIEW_CELL_REPORTS, "Visualizar relatórios da célula"),
(Permission.MANAGE_CELL_REPORTS, "Gerenciar relatórios da célula"), # Nova permissão (Permission.MANAGE_CELL_REPORTS, "Gerenciar relatórios da célula"),
(Permission.REGISTER_CELL_PAYMENT, "Registrar pagamentos da célula"), (Permission.REGISTER_CELL_PAYMENT, "Registrar pagamentos da célula"),
# Permissões de setor # Permissões de setor
@@ -193,7 +197,8 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(), session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first() session.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
] ]
# Membro de Setor # Membro de Setor
@@ -207,7 +212,8 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(), session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first() session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
] ]
# Secretário de Setor # Secretário de Setor
@@ -223,7 +229,8 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(), session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(), session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first() session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
] ]
# Membro de CR # Membro de CR
@@ -240,7 +247,8 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(), session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(), session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(), session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first() session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
] ]
# Secretário de CR # Secretário de CR
@@ -259,7 +267,8 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(), session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(), session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(), session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first() session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
] ]
# Membro do CC # Membro do CC
@@ -279,7 +288,8 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(), session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(), session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(), session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first() session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
] ]
# Secretário Geral # Secretário Geral
@@ -302,7 +312,8 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.MANAGE_CC_CRS).first(), session.query(Permission).filter_by(nome=Permission.MANAGE_CC_CRS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CC_CR).first(), session.query(Permission).filter_by(nome=Permission.CREATE_CC_CR).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(), session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(),
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first() session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
] ]
session.commit() session.commit()

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

@@ -1,5 +0,0 @@
[pytest]
pythonpath = .
testpaths = tests
python_files = test_*.py
addopts = -v --cov=. --cov-report=term-missing

View File

@@ -17,3 +17,6 @@ flask-bootstrap5==0.1.dev1
PyJWT==2.8.0 PyJWT==2.8.0
gunicorn==21.2.0 gunicorn==21.2.0
Faker==19.13.0 Faker==19.13.0
pytest==8.0.0
pytest-cov==4.1.0
pyzbar==0.1.9

View File

@@ -1,2 +0,0 @@
# Este arquivo está intencionalmente vazio
# Ele é usado para marcar o diretório como um pacote Python

View File

@@ -1,128 +0,0 @@
from flask import Blueprint, render_template, flash, redirect, url_for, request, jsonify
from functions.database import Usuario, get_db_connection
from functions.decorators import require_permission, require_role, require_minimum_role
from flask_login import login_required, current_user
from sqlalchemy.orm import joinedload
import pyotp
from werkzeug.security import generate_password_hash
import secrets
from functools import wraps
from sqlalchemy.exc import SQLAlchemyError
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_admin:
flash('Acesso não autorizado.', 'danger')
return redirect(url_for('main.index'))
return f(*args, **kwargs)
return decorated_function
@admin_bp.route('/')
@login_required
@admin_required
def dashboard():
"""Dashboard principal da área administrativa com lista de usuários"""
db = get_db_connection()
try:
now = datetime.now()
# Carregar estatísticas relevantes
total_users = db.query(Usuario).count()
active_users = db.query(Usuario).filter(Usuario.is_active == True).count()
inactive_users = total_users - active_users
# Carregar lista de usuários
users = db.query(Usuario).options(
joinedload(Usuario.roles),
joinedload(Usuario.militante)
).all()
return render_template(
'admin/dashboard.html',
total_users=total_users,
active_users=active_users,
inactive_users=inactive_users,
users=users,
now=now
)
except SQLAlchemyError as e:
logger.error(f"Erro ao buscar dados do dashboard: {str(e)}")
flash('Erro ao carregar dados. Por favor, tente novamente.', 'danger')
return render_template('admin/dashboard.html',
total_users=0,
active_users=0,
inactive_users=0,
users=[])
finally:
db.close()
@admin_bp.route('/users/<int:user_id>/reset-otp', methods=['POST'])
@login_required
@require_role('ADMIN')
def reset_user_otp(user_id):
"""Reseta o OTP de um usuário"""
db = get_db_connection()
try:
user = db.query(Usuario).get(user_id)
if not user:
flash('Usuário não encontrado.', 'danger')
return redirect(url_for('admin.dashboard'))
# Gerar novo segredo OTP
user.otp_secret = pyotp.random_base32()
db.commit()
flash(f'OTP resetado com sucesso para {user.email}.', 'success')
return redirect(url_for('admin.dashboard'))
finally:
db.close()
@admin_bp.route('/users/<int:user_id>/reset-password', methods=['POST'])
@login_required
@require_role('ADMIN')
def reset_user_password(user_id):
"""Reseta a senha de um usuário"""
db = get_db_connection()
try:
user = db.query(Usuario).get(user_id)
if not user:
flash('Usuário não encontrado.', 'danger')
return redirect(url_for('admin.dashboard'))
# Gerar nova senha aleatória
new_password = secrets.token_urlsafe(8)
user.password = generate_password_hash(new_password)
db.commit()
flash(f'Senha resetada com sucesso. Nova senha: {new_password}', 'success')
return redirect(url_for('admin.dashboard'))
finally:
db.close()
@admin_bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@login_required
@require_role('ADMIN')
def toggle_user_status(user_id):
"""Ativa/desativa um usuário"""
db = get_db_connection()
try:
user = db.query(Usuario).get(user_id)
if not user:
flash('Usuário não encontrado.', 'danger')
return redirect(url_for('admin.dashboard'))
user.is_active = not user.is_active
db.commit()
status = 'ativado' if user.is_active else 'desativado'
flash(f'Usuário {status} com sucesso.', 'success')
return redirect(url_for('admin.dashboard'))
finally:
db.close()

View File

@@ -1,17 +0,0 @@
#!/bin/bash
# Criar e ativar ambiente virtual
python -m venv venv
source venv/bin/activate
# Instalar dependências de teste
pip install -r tests/requirements-test.txt
# Instalar o projeto em modo de desenvolvimento
pip install -e .
# Executar testes
python -m pytest
# Desativar ambiente virtual
deactivate

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, CentralizacaoComprovante
) )
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", 50.00),
"PIX", ("Comprovante Especial", 100.00),
"Cartão de Crédito", ("Comprovante Extraordinário", 200.00),
"Cartão de Débito", ("Jornal Avulso", 5.00),
"Transferência Bancária" ("Assinatura de Jornal", 30.00),
("Campanha Financeira", 0.00) # Valor variável
] ]
for tipo in tipos:
if not session.query(TipoPagamento).filter_by(descricao=tipo).first(): for descricao, valor in tipos:
session.add(TipoPagamento(descricao=tipo)) if not session.query(TipoComprovante).filter_by(descricao=descricao).first():
session.commit() session.add(TipoComprovante(descricao=descricao, valor=valor))
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,38 @@ 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) # Criar o comprovante base
pagamento = Pagamento( comprovante = Comprovante(
militante_id=militante.id, militante_id=militante.id,
tipo_pagamento=tipo.descricao, # Usando a descrição do tipo data_comprovante=fake.date_between(start_date='-1y', end_date='today'),
valor=random.uniform(50, 500), forma_pagamento=random.choice(['PIX', 'transferência/DOC', 'depósito', 'maquininha'])
data_pagamento=fake.date_between(start_date='-1y', end_date='today')
) )
session.add(pagamento) session.add(comprovante)
session.flush() # Para obter o ID do comprovante
# Criar a centralização para o comprovante
tipo = random.choice(tipos_comprovante)
valor = random.uniform(10, 1000)
centralizacao = CentralizacaoComprovante(
comprovante_id=comprovante.id,
tipo_comprovante=tipo.descricao,
valor=valor
)
session.add(centralizacao)
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 +322,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 +330,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 +338,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

@@ -1,18 +0,0 @@
from setuptools import setup, find_packages
setup(
name="controles",
version="0.1",
packages=find_packages(),
include_package_data=True,
install_requires=[
'flask',
'flask-login',
'flask-sqlalchemy',
'flask-wtf',
'flask-mail',
'python-dotenv',
'pyotp',
'qrcode',
],
)

View File

@@ -20,10 +20,6 @@
--bs-success-dark: #157347; --bs-success-dark: #157347;
--bs-secondary: #6c757d; --bs-secondary: #6c757d;
--bs-secondary-dark: #565e64; --bs-secondary-dark: #565e64;
/* Variáveis para status */
--status-active: #28a745;
--status-inactive: #dc3545;
} }
/* Tabelas */ /* Tabelas */
@@ -613,14 +609,3 @@ input.btn-secondary:hover,
background-color: #cff4fc; background-color: #cff4fc;
border-color: #b6effb; border-color: #b6effb;
} }
/* Status styles */
.status-active {
color: var(--status-active);
font-weight: 500;
}
.status-inactive {
color: var(--status-inactive);
font-weight: 500;
}

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,3 +1,15 @@
// Configuração do token CSRF para requisições AJAX
document.addEventListener('DOMContentLoaded', function() {
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrfToken);
}
}
});
});
// Máscaras para campos de formulário // Máscaras para campos de formulário
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Máscara para CPF // Máscara para CPF

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 => {
@@ -198,3 +198,11 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
}); });
function sortTable(table, column, type = 'text') {
// ... existing code ...
if (column === 'data_comprovante') {
// ... existing code ...
}
// ... existing code ...
}

View File

@@ -1,102 +0,0 @@
{% extends "base.html" %}
{% block title %}Área Administrativa{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<nav id="sidebar" class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.dashboard' %}active{% endif %}"
href="{{ url_for('admin.dashboard') }}">
<i class="fas fa-tachometer-alt me-2"></i>
Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.list_users' %}active{% endif %}"
href="{{ url_for('admin.list_users') }}">
<i class="fas fa-users me-2"></i>
Usuários
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('home') }}">
<i class="fas fa-arrow-left me-2"></i>
Voltar ao Sistema
</a>
</li>
</ul>
</div>
</nav>
<!-- Main content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{% block admin_title %}{% endblock %}</h1>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block admin_content %}{% endblock %}
</main>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100;
padding: 48px 0 0;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link.active {
color: #2470dc;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
main {
padding-top: 48px;
}
@media (max-width: 767.98px) {
.sidebar {
position: static;
padding-top: 0;
}
main {
padding-top: 0;
}
}
</style>
{% endblock %}
{% block extra_scripts %}{% endblock %}

View File

@@ -1,227 +0,0 @@
{% extends "admin/base.html" %}
{% block title %}Dashboard Administrativo{% endblock %}
{% block extra_css %}
<style>
.card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
transition: all 0.3s ease;
overflow: hidden;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
}
.bg-primary {
background: linear-gradient(135deg, #0d6efd, #0a58ca) !important;
}
.bg-success {
background: linear-gradient(135deg, #198754, #146c43) !important;
}
.bg-danger {
background: linear-gradient(135deg, #dc3545, #b02a37) !important;
}
.card .opacity-50 {
opacity: 0.2 !important;
transition: all 0.3s ease;
}
.card:hover .opacity-50 {
opacity: 0.3 !important;
transform: scale(1.1);
}
.card-title {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 1rem;
text-transform: uppercase;
color: rgba(255,255,255,0.8);
}
.display-4 {
font-size: 2.5rem;
font-weight: 600;
margin: 0.5rem 0;
}
.btn-group {
gap: 0.25rem;
}
/* Estilo da lista de usuários */
.card.lista-usuarios {
border-radius: 0;
box-shadow: none;
transition: none;
border: 1px solid #dee2e6;
}
.card.lista-usuarios:hover {
transform: none;
box-shadow: none;
}
.card.lista-usuarios .card-header {
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color));
color: white;
border: none;
padding: 1rem 1.5rem;
}
.card.lista-usuarios .card-header h5 {
margin: 0;
font-size: 1.1rem;
font-weight: 500;
}
.card.lista-usuarios .table {
margin-bottom: 0;
}
.card.lista-usuarios .table th {
border-top: none;
font-weight: 600;
padding: 1rem;
background-color: #f8f9fa;
}
.card.lista-usuarios .table td {
padding: 1rem;
vertical-align: middle;
}
.card.lista-usuarios .badge {
padding: 0.5em 0.8em;
font-weight: 500;
}
.btn-group .btn {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
</style>
{% endblock %}
{% block content %}
<h2 class="mb-4">
<i class="fas fa-users-cog"></i>
Administração de Usuários
</h2>
<div class="row mb-4">
<div class="col-md-4">
<div class="card bg-primary text-white">
<div class="card-body">
<h5 class="card-title text-uppercase">Total de Usuários</h5>
<div class="d-flex justify-content-between align-items-center">
<h2 class="display-4 mb-0">{{ total_users }}</h2>
<i class="fas fa-users fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-success text-white">
<div class="card-body">
<h5 class="card-title text-uppercase">Usuários Ativos</h5>
<div class="d-flex justify-content-between align-items-center">
<h2 class="display-4 mb-0">{{ active_users }}</h2>
<i class="fas fa-user-check fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-danger text-white">
<div class="card-body">
<h5 class="card-title text-uppercase">Usuários Inativos</h5>
<div class="d-flex justify-content-between align-items-center">
<h2 class="display-4 mb-0">{{ inactive_users }}</h2>
<i class="fas fa-user-times fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
</div>
<div class="card lista-usuarios">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-users me-2"></i>
Lista de Usuários
</h5>
</div>
<div class="card-body p-0">
<table id="users-table" class="table table-striped table-hover">
<thead>
<tr>
<th>Nome</th>
<th>Email</th>
<th>Status</th>
<th>Último Login</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.name }}</td>
<td>{{ user.email }}</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>
<div class="btn-group">
<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" title="Reset OTP" onclick="return confirm('Confirma o reset do OTP deste usuário?')">
<i class="fas fa-key"></i>
</button>
</form>
<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() }}">
<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>
</button>
</form>
<form action="{{ url_for('admin.toggle_user_status', user_id=user.id) }}" method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-{{ 'danger' if user.is_active else 'success' }} btn-sm" title="{{ 'Desativar' if user.is_active else 'Ativar' }} Usuário">
<i class="fas fa-{{ 'user-times' if user.is_active else 'user-check' }}"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
$('#users-table').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
},
order: [[0, 'asc']],
pageLength: 25
});
});
</script>
{% endblock %}

View File

@@ -10,9 +10,9 @@
<!-- Bootstrap 5 CSS --> <!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css?v=1" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css?v=1" rel="stylesheet">
<!-- Font Awesome 6 --> <!-- Font Awesome 6 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css?v=1">
<!-- Componentes CSS --> <!-- Componentes CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}?v={{ range(1, 10000) | random }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}">
<style> <style>
:root { :root {
@@ -541,8 +541,8 @@
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="{{ url_for('listar_pagamentos') }}"> <a class="dropdown-item" href="{{ url_for('listar_comprovantes') }}">
<i class="fas fa-receipt"></i>Pagamentos <i class="fas fa-receipt"></i>Comprovantes
</a> </a>
</li> </li>
</ul> </ul>
@@ -563,8 +563,8 @@
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="{{ url_for('listar_assinaturas') }}"> <a class="dropdown-item" href="{{ url_for('listar_vendas_jornal') }}">
<i class="fas fa-file-signature"></i>Assinaturas <i class="fas fa-file-signature"></i>Assinaturas de Jornal
</a> </a>
</li> </li>
</ul> </ul>
@@ -599,11 +599,6 @@
<i class="fas fa-user-plus"></i>Novo Usuário <i class="fas fa-user-plus"></i>Novo Usuário
</a> </a>
</li> </li>
<li>
<a class="dropdown-item" href="{{ url_for('admin.dashboard') }}">
<i class="fas fa-cog fa fa-cog fa-solid fa-cog" style="display: inline-block !important; visibility: visible !important;"></i>Administração
</a>
</li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% endif %} {% endif %}
<li> <li>

View File

@@ -1,69 +1,63 @@
{% extends "base.html" %} {% extends 'base.html' %}
{% block title %}Dashboard Administrativo{% endblock %} {% block title %}Dashboard Administrativo{% endblock %}
{% block content %} {% block content %}
<div class="container mt-4"> <div class="container">
<h2 class="mb-4"><i class="fas fa-users-cog"></i> Administração de Usuários</h2> <h1 class="mb-4">Dashboard Administrativo</h1>
<div class="card">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Gerenciamento de Usuários</h5>
</div>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-striped">
<thead class="thead-light"> <thead>
<tr> <tr>
<th>ID</th>
<th>Usuário</th> <th>Usuário</th>
<th>Email</th> <th>Email</th>
<th>Nome</th> <th>Admin</th>
<th>Último Acesso</th> <th>OTP Configurado</th>
<th>Status</th>
<th>Nível</th>
<th>Ações</th> <th>Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for usuario in usuarios %} {% for usuario in usuarios %}
<tr> <tr>
<td>{{ usuario.id }}</td>
<td>{{ usuario.username }}</td> <td>{{ usuario.username }}</td>
<td>{{ usuario.email }}</td> <td>{{ usuario.email }}</td>
<td>{{ usuario.nome }}</td>
<td>{{ usuario.last_login }}</td>
<td>
<span class="badge {% if usuario.ativo %}bg-success{% else %}bg-danger{% endif %}">
{{ "Ativo" if usuario.ativo else "Inativo" }}
</span>
</td>
<td> <td>
{% if usuario.is_admin %} {% if usuario.is_admin %}
Administrador <span class="badge bg-success">Sim</span>
{% else %} {% else %}
{{ usuario.nivel }} <span class="badge bg-secondary">Não</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
<div class="btn-group" role="group"> {% if usuario.otp_secret %}
<button class="btn btn-sm btn-outline-primary" <span class="badge bg-success">Sim</span>
onclick="toggleStatus('{{ usuario.id }}')" {% else %}
data-toggle="tooltip" <span class="badge bg-danger">Não</span>
title="{{ 'Desativar' if usuario.ativo else 'Ativar' }} usuário"> {% endif %}
<i class="fas {% if usuario.ativo %}fa-user-times{% else %}fa-user-check{% endif %}"></i> </td>
<td>
<form action="{{ url_for('reset_otp', user_id=usuario.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-warning btn-sm"
onclick="return confirm('Tem certeza que deseja resetar o OTP deste usuário?')">
Resetar OTP
</button> </button>
</form>
<button class="btn btn-sm btn-outline-warning"
onclick="resetarSenha('{{ usuario.id }}')"
data-toggle="tooltip"
title="Resetar senha">
<i class="fas fa-key"></i>
</button>
{% if not usuario.is_admin %}
<button class="btn btn-sm btn-outline-info"
onclick="alterarNivel('{{ usuario.id }}')"
data-toggle="tooltip"
title="Alterar nível">
<i class="fas fa-level-up-alt"></i>
</button>
{% endif %}
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -72,127 +66,18 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Modal de Feedback --> <div class="card">
<div class="modal fade" id="feedbackModal" tabindex="-1" role="dialog"> <div class="card-header">
<div class="modal-dialog" role="document"> <h5 class="card-title mb-0">Ações Rápidas</h5>
<div class="modal-content"> </div>
<div class="modal-header"> <div class="card-body">
<h5 class="modal-title">Aviso</h5> <div class="d-grid gap-2">
<button type="button" class="close" data-dismiss="modal"> <a href="{{ url_for('novo_usuario') }}" class="btn btn-primary">
<span>&times;</span> Criar Novo Usuário
</button> </a>
</div>
<div class="modal-body">
<p id="feedbackMessage"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Fechar</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
{% block scripts %}
<script>
function showFeedback(message, type = 'info') {
const modal = document.getElementById('feedbackModal');
const messageElement = document.getElementById('feedbackMessage');
messageElement.textContent = message;
messageElement.className = `alert alert-${type}`;
$(modal).modal('show');
}
function handleResponse(response) {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
function toggleStatus(userId) {
if (!confirm('Tem certeza que deseja alterar o status deste usuário?')) {
return;
}
fetch(`/usuarios/${userId}/toggle_status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
}
})
.then(handleResponse)
.then(data => {
showFeedback(data.message || 'Status alterado com sucesso!', data.success ? 'success' : 'danger');
if (data.success) {
setTimeout(() => location.reload(), 1500);
}
})
.catch(error => {
console.error('Error:', error);
showFeedback('Erro ao alterar status do usuário. Por favor, tente novamente.', 'danger');
});
}
function resetarSenha(userId) {
if (!confirm('Tem certeza que deseja resetar a senha deste usuário?')) {
return;
}
fetch(`/reset_password/${userId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
}
})
.then(handleResponse)
.then(data => {
showFeedback(data.message || 'Senha resetada com sucesso!', data.success ? 'success' : 'danger');
})
.catch(error => {
console.error('Error:', error);
showFeedback('Erro ao resetar senha. Por favor, tente novamente.', 'danger');
});
}
function alterarNivel(userId) {
const novoNivel = prompt('Digite o novo nível do usuário (1-5):');
if (!novoNivel) return;
if (!/^[1-5]$/.test(novoNivel)) {
showFeedback('Por favor, insira um nível válido entre 1 e 5.', 'warning');
return;
}
fetch(`/usuarios/${userId}/alterar_nivel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ nivel: parseInt(novoNivel) })
})
.then(handleResponse)
.then(data => {
showFeedback(data.message || 'Nível alterado com sucesso!', data.success ? 'success' : 'danger');
if (data.success) {
setTimeout(() => location.reload(), 1500);
}
})
.catch(error => {
console.error('Error:', error);
showFeedback('Erro ao alterar nível. Por favor, tente novamente.', 'danger');
});
}
// Inicializa os tooltips do Bootstrap
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</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

@@ -57,7 +57,7 @@
<div class="stats-card yellow"> <div class="stats-card yellow">
<div class="title">Assinaturas Ativas</div> <div class="title">Assinaturas Ativas</div>
<div class="value">{{ total_assinaturas }}</div> <div class="value">{{ total_assinaturas }}</div>
<a href="{{ url_for('listar_assinaturas') }}" class="link"> <a href="{{ url_for('listar_vendas_jornal') }}" class="link">
Ver detalhes <i class="fas fa-arrow-right"></i> Ver detalhes <i class="fas fa-arrow-right"></i>
</a> </a>
<div class="icon"> <div class="icon">

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

@@ -0,0 +1,309 @@
{% extends "base.html" %}
{% block title %}Comprovantes{% endblock %}
{% block content %}
<div class="container-fluid mt-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="fas fa-money-bill-wave"></i> Comprovantes</h2>
<div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#novoComprovanteModal">
<i class="fas fa-plus"></i> Novo Comprovante
</button>
<button type="button" class="btn btn-outline-primary" id="btnExportar">
<i class="fas fa-file-export"></i> Exportar
</button>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="tabelaComprovantes">
<thead>
<tr>
<th>ID</th>
<th>Militante</th>
<th>Data</th>
<th>Forma de Pagamento</th>
<th>Campanha</th>
<th>Centralizações</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for comprovante in comprovantes %}
<tr>
<td>{{ comprovante.id }}</td>
<td>{{ comprovante.militante.nome }}</td>
<td>{{ comprovante.data_comprovante.strftime('%d/%m/%Y') }}</td>
<td>{{ comprovante.forma_pagamento }}</td>
<td>{{ comprovante.campanha.nome if comprovante.campanha else '-' }}</td>
<td>
<ul class="list-unstyled">
{% for centralizacao in comprovante.centralizacoes %}
<li>{{ centralizacao.tipo_comprovante }}: R$ {{ "%.2f"|format(centralizacao.valor) }}</li>
{% endfor %}
</ul>
</td>
<td>
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarComprovante"
data-comprovante-id="{{ comprovante.id }}"
data-militante-id="{{ comprovante.militante_id }}"
data-militante-nome="{{ comprovante.militante.nome }}"
data-data-comprovante="{{ comprovante.data_comprovante.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#modalExcluirComprovante"
data-comprovante-id="{{ comprovante.id }}"
data-comprovante-info="Comprovante de {{ comprovante.militante.nome }} - Total: R$ {{ "%.2f"|format(comprovante.centralizacoes|sum(attribute='valor')) }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modal Novo Comprovante -->
<div class="modal fade" id="novoComprovanteModal" tabindex="-1" aria-labelledby="novoComprovanteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="novoComprovanteModalLabel">Novo Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="novoComprovanteForm">
<!-- Dados únicos do comprovante -->
<div class="row mb-3">
<div class="col-md-6">
<label for="militante_id" class="form-label">Militante</label>
<select class="form-select" id="militante_id" name="militante_id" required>
<option value="">Selecione o militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label for="data_comprovante" class="form-label">Data do Comprovante</label>
<input type="date" class="form-control" id="data_comprovante" name="data_comprovante" required>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="forma_pagamento" class="form-label">Forma de Pagamento</label>
<select class="form-select" id="forma_pagamento" name="forma_pagamento" required>
<option value="">Selecione a forma de pagamento</option>
<option value="PIX">PIX</option>
<option value="TRANSFERENCIA">Transferência/DOC</option>
<option value="DEPOSITO">Depósito</option>
<option value="MAQUININHA">Maquininha</option>
</select>
</div>
<div class="col-md-6">
<label for="campanha_id" class="form-label">Campanha</label>
<select class="form-select" id="campanha_id" name="campanha_id">
<option value="">Selecione a campanha</option>
{% for campanha in campanhas %}
<option value="{{ campanha.id }}">{{ campanha.nome }}</option>
{% endfor %}
</select>
</div>
</div>
<!-- Centralizações -->
<div class="centralizacoes-container">
<h6 class="mb-3">Centralizações</h6>
<div class="centralizacao-item mb-3">
<div class="row">
<div class="col-md-6">
<label class="form-label">Tipo de Comprovante</label>
<select class="form-select tipo-comprovante" name="tipo_comprovante[]" required>
<option value="">Selecione o tipo</option>
<option value="COTA">Cota</option>
<option value="JORNAL">Jornal</option>
<option value="ASSINATURA">Assinatura</option>
</select>
</div>
<div class="col-md-5">
<label class="form-label">Valor</label>
<input type="number" class="form-control valor" name="valor[]" step="0.01" required>
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="button" class="btn btn-danger btn-sm remover-centralizacao">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-secondary btn-sm mb-3" id="adicionar-centralizacao">
<i class="bi bi-plus"></i> Adicionar Centralização
</button>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary" id="salvarComprovante">Salvar</button>
</div>
</div>
</div>
</div>
<style>
.centralizacao-item {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
border: 1px solid #dee2e6;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Adicionar nova centralização
document.getElementById('adicionar-centralizacao').addEventListener('click', function() {
const container = document.querySelector('.centralizacoes-container');
const newItem = document.querySelector('.centralizacao-item').cloneNode(true);
newItem.querySelector('.valor').value = '';
newItem.querySelector('.tipo-comprovante').value = '';
container.appendChild(newItem);
});
// Remover centralização
document.addEventListener('click', function(e) {
if (e.target.closest('.remover-centralizacao')) {
const centralizacoes = document.querySelectorAll('.centralizacao-item');
if (centralizacoes.length > 1) {
e.target.closest('.centralizacao-item').remove();
}
}
});
// Salvar comprovante
document.getElementById('salvarComprovante').addEventListener('click', function() {
const form = document.getElementById('novoComprovanteForm');
const formData = new FormData(form);
// Coletar dados das centralizações
const centralizacoes = [];
document.querySelectorAll('.centralizacao-item').forEach(item => {
centralizacoes.push({
tipo_comprovante: item.querySelector('.tipo-comprovante').value,
valor: item.querySelector('.valor').value
});
});
// Adicionar centralizações ao formData
formData.append('centralizacoes', JSON.stringify(centralizacoes));
fetch('/comprovantes/novo', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.message || 'Erro ao salvar comprovante');
}
})
.catch(error => {
console.error('Error:', error);
alert('Erro ao salvar comprovante');
});
});
});
</script>
<!-- Modal Editar Comprovante -->
<div class="modal fade" id="modalEditarComprovante" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-edit"></i> Editar Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarComprovante" method="post">
<div class="mb-3">
<label for="editMilitante" class="form-label">Militante:</label>
<input type="text" class="form-control bg-light" id="editMilitanteNome" readonly>
<input type="hidden" id="editMilitante" name="militante_id">
</div>
<div class="mb-3">
<label for="editTipoComprovante" class="form-label">Tipo de Comprovante:</label>
<select class="form-select" id="editTipoComprovante" name="tipo_comprovante" required>
<option value="">Selecione o tipo</option>
<option value="1">Cota</option>
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
<option value="2">Contribuição Extra</option>
<option value="3">Doação</option>
<option value="4">Taxa de Evento</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>
</div>
<div class="mb-3">
<label for="editValor" class="form-label">Valor:</label>
<input type="number" step="0.01" class="form-control" id="editValor" name="valor" required>
</div>
<div class="mb-3">
<label for="editDataComprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="editDataComprovante" name="data_comprovante" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Salvar</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Modal Excluir Comprovante -->
<div class="modal fade" id="modalExcluirComprovante" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-trash"></i> Excluir Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir este comprovante?</p>
<p id="comprovanteInfo" class="text-muted"></p>
</div>
<div class="modal-footer">
<form id="formExcluirComprovante" method="post">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-danger">Excluir</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/comprovantes.js') }}"></script>
{% endblock %}

View File

@@ -1,203 +0,0 @@
{% extends "base.html" %}
{% block title %}Pagamentos{% endblock %}
{% block content %}
<div class="container-fluid mt-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="fas fa-money-bill-wave"></i> Pagamentos</h2>
<div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoPagamento">
<i class="fas fa-plus"></i> Novo Pagamento
</button>
<button type="button" class="btn btn-outline-primary" id="btnExportar">
<i class="fas fa-file-export"></i> Exportar
</button>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="tabelaPagamentos">
<thead>
<tr>
<th>Militante</th>
<th>Tipo de Pagamento</th>
<th>Valor</th>
<th>Data do Pagamento</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for pagamento in pagamentos %}
<tr>
<td data-militante="{{ pagamento.militante_id }}">{{ pagamento.militante.nome if pagamento.militante else 'N/A' }}</td>
<td data-tipo="{{ pagamento.tipo_pagamento }}">
{% if pagamento.tipo_pagamento == 1 %}
Mensalidade
{% elif pagamento.tipo_pagamento == 2 %}
Contribuição Extra
{% elif pagamento.tipo_pagamento == 3 %}
Doação
{% elif pagamento.tipo_pagamento == 4 %}
Taxa de Evento
{% elif pagamento.tipo_pagamento == 5 %}
Outros
{% else %}
Não Definido
{% endif %}
</td>
<td data-valor="{{ pagamento.valor }}">R$ {{ "%.2f"|format(pagamento.valor) }}</td>
<td data-data="{{ pagamento.data_pagamento }}">{{ pagamento.data_pagamento.strftime('%d/%m/%Y') }}</td>
<td>
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarPagamento"
data-pagamento-id="{{ pagamento.id }}"
data-militante-id="{{ pagamento.militante_id }}"
data-tipo-pagamento="{{ pagamento.tipo_pagamento }}"
data-valor="{{ pagamento.valor }}"
data-data-pagamento="{{ pagamento.data_pagamento.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#modalExcluirPagamento"
data-pagamento-id="{{ pagamento.id }}"
data-pagamento-info="Pagamento de {{ pagamento.militante.nome if pagamento.militante else 'N/A' }} - R$ {{ "%.2f"|format(pagamento.valor) }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modal Novo Pagamento -->
<div class="modal fade" id="modalNovoPagamento" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-plus"></i> Novo Pagamento</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovoPagamento" method="post" action="{{ url_for('adicionar_pagamento') }}">
<div class="mb-3">
<label for="militante" class="form-label">Militante:</label>
<select class="form-select" id="militante" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="tipoPagamento" class="form-label">Tipo de Pagamento:</label>
<select class="form-select" id="tipoPagamento" name="tipo_pagamento" required>
<option value="">Selecione o tipo</option>
<option value="1">Mensalidade</option>
<option value="2">Contribuição Extra</option>
<option value="3">Doação</option>
<option value="4">Taxa de Evento</option>
<option value="5">Outros</option>
</select>
</div>
<div class="mb-3">
<label for="valor" class="form-label">Valor:</label>
<input type="number" step="0.01" class="form-control" id="valor" name="valor" required>
</div>
<div class="mb-3">
<label for="dataPagamento" class="form-label">Data do Pagamento:</label>
<input type="date" class="form-control" id="dataPagamento" name="data_pagamento" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Salvar</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Modal Editar Pagamento -->
<div class="modal fade" id="modalEditarPagamento" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-edit"></i> Editar Pagamento</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarPagamento" method="post">
<div class="mb-3">
<label for="editMilitante" class="form-label">Militante:</label>
<input type="text" class="form-control bg-light" id="editMilitanteNome" readonly>
<input type="hidden" id="editMilitante" name="militante_id">
</div>
<div class="mb-3">
<label for="editTipoPagamento" class="form-label">Tipo de Pagamento:</label>
<select class="form-select" id="editTipoPagamento" name="tipo_pagamento" required>
<option value="">Selecione o tipo</option>
<option value="1">Mensalidade</option>
<option value="2">Contribuição Extra</option>
<option value="3">Doação</option>
<option value="4">Taxa de Evento</option>
<option value="5">Outros</option>
</select>
</div>
<div class="mb-3">
<label for="editValor" class="form-label">Valor:</label>
<input type="number" step="0.01" class="form-control" id="editValor" name="valor" required>
</div>
<div class="mb-3">
<label for="editDataPagamento" class="form-label">Data do Pagamento:</label>
<input type="date" class="form-control" id="editDataPagamento" name="data_pagamento" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Salvar</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Modal Excluir Pagamento -->
<div class="modal fade" id="modalExcluirPagamento" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-trash"></i> Excluir Pagamento</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir este pagamento?</p>
<p id="pagamentoInfo" class="text-muted"></p>
</div>
<div class="modal-footer">
<form id="formExcluirPagamento" method="post">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-danger">Excluir</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/pagamentos.js') }}"></script>
{% 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

@@ -5,27 +5,28 @@
{% block navbar %}{% endblock %} {% block navbar %}{% endblock %}
{% block content %} {% block content %}
<div class="alert-container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<div class="login-container"> <div class="login-container">
<div class="login-content"> <div class="login-content">
<div class="login-header"> <div class="login-header">
<img src="{{ url_for('static', filename='img/logo001-alpha.png') }}" alt="Logo OCI" class="login-logo"> <img src="{{ url_for('static', filename='img/logo001-alpha.png') }}" alt="Logo OCI" class="login-logo">
<h4 class="login-title">Controles OCI</h4> <h4 class="login-title">Controles OCI</h4>
</div> </div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('login') }}" class="needs-validation" novalidate> <form method="POST" action="{{ url_for('login') }}" class="needs-validation" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input type="text" class="form-control" id="email" name="email" placeholder="Email ou Usuário" required> <input type="text" class="form-control" id="email" name="email" placeholder="Email ou Usuário" required>
<label for="email">Email ou Usuário</label> <label for="email">Email ou Usuário</label>
@@ -46,7 +47,7 @@
</div> </div>
<div class="form-floating mb-4"> <div class="form-floating mb-4">
<input type="text" class="form-control" id="otp" name="otp" placeholder="Código OTP" required> <input type="text" class="form-control" id="otp" name="otp" placeholder="Código OTP">
<label for="otp">Código OTP</label> <label for="otp">Código OTP</label>
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, informe o código OTP. Por favor, informe o código OTP.
@@ -208,4 +209,7 @@ form {
} }
} }
</style> </style>
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
{% endblock %} {% endblock %}

View File

@@ -31,7 +31,7 @@
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Registrar</button> <button type="submit" class="btn btn-primary">Registrar</button>
<a href="{{ url_for('listar_assinaturas') }}" class="btn btn-secondary">Voltar</a> <a href="{{ url_for('listar_vendas_jornal') }}" class="btn btn-secondary">Voltar</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>
</form> </form>

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>

View File

@@ -1,33 +0,0 @@
import pytest
from app import create_app
from functions.database import init_database, get_db_connection
@pytest.fixture
def app():
"""Cria uma instância do app para testes"""
app = create_app()
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
# Inicializar banco de dados de teste
init_database()
yield app
# Limpar banco após os testes
db = get_db_connection()
try:
db.execute('DROP TABLE IF EXISTS usuarios CASCADE')
db.commit()
finally:
db.close()
@pytest.fixture
def client(app):
"""Cria um cliente de teste"""
return app.test_client()
@pytest.fixture
def runner(app):
"""Cria um runner de CLI para testes"""
return app.test_cli_runner()

View File

@@ -1,4 +0,0 @@
pytest==7.4.3
pytest-cov==4.1.0
pytest-flask==1.3.0
coverage==7.3.2

View File

@@ -1,100 +0,0 @@
import pytest
from flask import url_for
from functions.database import Usuario, get_db_connection
from werkzeug.security import generate_password_hash
import json
@pytest.fixture
def admin_user(client):
"""Fixture que cria um usuário admin para testes"""
db = get_db_connection()
try:
admin = Usuario(
username='admin_test',
email='admin@test.com',
password_hash=generate_password_hash('admin123'),
is_admin=True,
is_active=True
)
db.add(admin)
db.commit()
return admin
finally:
db.close()
@pytest.fixture
def auth_admin_client(client, admin_user):
"""Fixture que retorna um cliente autenticado como admin"""
client.post('/login', data={
'email': 'admin@test.com',
'password': 'admin123'
})
return client
def test_dashboard_access_sem_login(client):
"""Testa acesso ao dashboard sem login"""
response = client.get('/admin/')
assert response.status_code == 302
assert '/login' in response.headers['Location']
def test_dashboard_access_com_login(auth_admin_client):
"""Testa acesso ao dashboard com login de admin"""
response = auth_admin_client.get('/admin/')
assert response.status_code == 200
assert b'Dashboard Administrativo' in response.data
def test_lista_usuarios(auth_admin_client):
"""Testa listagem de usuários"""
response = auth_admin_client.get('/admin/users')
assert response.status_code == 200
assert b'Lista de' in response.data
assert b'admin_test' in response.data
def test_reset_otp(auth_admin_client, admin_user):
"""Testa reset de OTP"""
response = auth_admin_client.post(f'/admin/users/{admin_user.id}/reset-otp')
assert response.status_code == 302
assert 'success' in response.headers['Location']
def test_reset_password(auth_admin_client, admin_user):
"""Testa reset de senha"""
response = auth_admin_client.post(f'/admin/users/{admin_user.id}/reset-password')
assert response.status_code == 302
assert 'success' in response.headers['Location']
def test_toggle_status(auth_admin_client, admin_user):
"""Testa alteração de status do usuário"""
response = auth_admin_client.post(
f'/admin/users/{admin_user.id}/toggle-status',
headers={'Content-Type': 'application/json'}
)
data = json.loads(response.data)
assert response.status_code == 200
assert data['success'] is True
def test_acesso_nao_admin(client):
"""Testa acesso de usuário não admin"""
db = get_db_connection()
try:
# Criar usuário normal
user = Usuario(
username='normal_user',
email='user@test.com',
password_hash=generate_password_hash('user123'),
is_admin=False,
is_active=True
)
db.add(user)
db.commit()
# Login
client.post('/login', data={
'email': 'user@test.com',
'password': 'user123'
})
# Tentar acessar área admin
response = client.get('/admin/')
assert response.status_code == 403
finally:
db.close()

89
tests/test_routes.py Normal file
View File

@@ -0,0 +1,89 @@
import pytest
from flask import url_for
from app import create_app
from functions.database import get_db_connection, init_database
import os
@pytest.fixture
def app():
app = create_app()
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
# Criar banco de dados temporário para testes
with app.app_context():
init_database()
yield app
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def runner(app):
return app.test_cli_runner()
def test_home_page(client):
response = client.get('/')
assert response.status_code == 302 # Redireciona para login
def test_login_page(client):
response = client.get('/login')
assert response.status_code == 200
assert b'Login' in response.data
def test_listar_assinaturas_jornal(client):
# Primeiro fazer login
client.post('/login', data={
'username': 'admin',
'password': 'admin123'
})
response = client.get('/assinaturas/jornal')
assert response.status_code == 200
assert b'Assinaturas de Jornal' in response.data
def test_nova_assinatura_jornal(client):
# Primeiro fazer login
client.post('/login', data={
'username': 'admin',
'password': 'admin123'
})
response = client.get('/assinaturas/jornal/novo')
assert response.status_code == 200
assert b'Registrar Nova Assinatura Anual' in response.data
def test_listar_militantes(client):
# Primeiro fazer login
client.post('/login', data={
'username': 'admin',
'password': 'admin123'
})
response = client.get('/militantes')
assert response.status_code == 200
assert b'Militantes' in response.data
def test_listar_cotas(client):
# Primeiro fazer login
client.post('/login', data={
'username': 'admin',
'password': 'admin123'
})
response = client.get('/cotas')
assert response.status_code == 200
assert b'Cotas' in response.data
def test_listar_materiais(client):
# Primeiro fazer login
client.post('/login', data={
'username': 'admin',
'password': 'admin123'
})
response = client.get('/materiais')
assert response.status_code == 200
assert b'Materiais' in response.data