feat: implementa sistema de comprovantes com centralizações e PIX

This commit is contained in:
LS
2025-04-16 13:54:31 -03:00
parent 813c968efd
commit 8ff58cc51e
15 changed files with 581 additions and 480 deletions

View File

@@ -1,7 +1,19 @@
.PHONY: install run test clean refresh
install:
pip install -r requirements.txt
pip install pytest pytest-cov
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 -f admin_qr.png
@@ -18,3 +30,9 @@ run-with-seed: seed run
reset-admin: clean
python create_admin.py
test:
pytest tests/ --cov=app --cov=functions --cov-report=term-missing
refresh: clean install test
python app.py

323
app.py
View File

@@ -24,14 +24,14 @@ from functions.database import (
Endereco,
TipoComprovante,
Comprovante,
VendaJornal,
AssinaturaJornal,
CampanhaFinanceira,
TransacaoPIX,
Permission,
Role,
RolePermission,
UserRole
Atividade,
MaterialAtividade,
Relatorio,
CentralizacaoComprovante
)
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, joinedload
@@ -73,10 +73,6 @@ def create_app():
csrf = CSRFProtect()
csrf.init_app(app)
# Configurar cabeçalhos CSRF personalizados
app.config['WTF_CSRF_CHECK_DEFAULT'] = False
app.config['WTF_CSRF_HEADERS'] = ['X-CSRFToken']
# Configurar Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)
@@ -88,6 +84,13 @@ def create_app():
"""Filtro para operação bit a bit AND"""
return value1 & value2
@app.before_request
def before_request():
"""Configurações antes de cada requisição"""
session.permanent = True
app.permanent_session_lifetime = timedelta(minutes=30)
session.modified = True
@login_manager.user_loader
def load_user(user_id):
"""Carrega o usuário pelo ID"""
@@ -214,14 +217,15 @@ def create_app():
flash("Email/usuário ou senha incorretos.", "danger")
return redirect(url_for("login"))
# Verificar OTP se o usuário tiver configurado
if user.otp_secret and not otp:
flash("Código OTP é obrigatório para sua conta.", "danger")
return redirect(url_for("login"))
if user.otp_secret and not user.verify_otp(otp):
flash("Código OTP inválido.", "danger")
return redirect(url_for("login"))
# Verificar OTP apenas se o usuário tiver configurado
if user.otp_secret:
if not otp:
flash("Código OTP é obrigatório para sua conta.", "danger")
return redirect(url_for("login"))
if not user.verify_otp(otp):
flash("Código OTP inválido.", "danger")
return redirect(url_for("login"))
# Atualizar último login
user.ultimo_login = datetime.utcnow()
@@ -281,28 +285,23 @@ def create_app():
.order_by(Militante.id.desc())\
.limit(5)\
.all()
# Buscar últimos comprovantes
ultimos_comprovantes = db.query(Comprovante)\
.join(Militante)\
.order_by(Comprovante.data_comprovante.desc())\
# Buscar últimos pagamentos
ultimos_pagamentos = db.query(Pagamento)\
.order_by(Pagamento.data_pagamento.desc())\
.limit(5)\
.all()
# Buscar tipos de comprovante
tipos_comprovante = db.query(TipoComprovante).all()
return render_template('home.html',
nome_usuario=nome_usuario,
data_atual=data_atual,
total_militantes=total_militantes,
total_cotas="{:.2f}".format(total_cotas),
total_materiais=total_materiais,
total_assinaturas=total_assinaturas,
ultimos_militantes=ultimos_militantes,
ultimos_comprovantes=ultimos_comprovantes,
tipos_comprovante=tipos_comprovante,
user=current_user)
nome_usuario=nome_usuario,
data_atual=data_atual,
total_militantes=total_militantes,
total_cotas=total_cotas,
total_materiais=total_materiais,
total_assinaturas=total_assinaturas,
ultimos_militantes=ultimos_militantes,
ultimos_pagamentos=ultimos_pagamentos,
Militante=Militante)
except Exception as e:
print(f"Erro na página inicial: {e}")
import traceback
@@ -316,7 +315,7 @@ def create_app():
total_materiais=0,
total_assinaturas=0,
ultimos_militantes=[],
ultimos_comprovantes=[],
ultimos_pagamentos=[],
Militante=Militante)
finally:
db.close()
@@ -819,7 +818,7 @@ def create_app():
return redirect(url_for("nova_venda_jornal"))
db = get_db_connection()
venda_jornal = VendaJornal(
venda_jornal = VendaJornalAvulso(
militante_id=militante_id,
quantidade=quantidade,
data_venda=data_venda
@@ -842,7 +841,7 @@ def create_app():
@require_permission(Permission.MANAGE_MATERIALS)
def listar_vendas_jornal():
db = get_db_connection()
vendas_jornal = db.query(VendaJornal).all()
vendas_jornal = db.query(VendaJornalAvulso).all()
return render_template("listar_vendas_jornal.html", vendas_jornal=vendas_jornal)
# Rota para criar um novo relatório de cotas
@@ -888,8 +887,8 @@ def create_app():
return redirect(url_for("novo_relatorio_jornais"))
db = get_db_connection()
jornais = db.query(VendaJornal).filter(
VendaJornal.data_venda.between(data_inicio, data_fim)
jornais = db.query(VendaJornalAvulso).filter(
VendaJornalAvulso.data_venda.between(data_inicio, data_fim)
).all()
return render_template("relatorio_jornais.html",
@@ -916,8 +915,8 @@ def create_app():
return redirect(url_for("novo_relatorio_assinaturas"))
db = get_db_connection()
assinaturas = db.query(AssinaturaJornal).filter(
AssinaturaJornal.data_assinatura.between(data_inicio, data_fim)
assinaturas = db.query(AssinaturaAnual).filter(
AssinaturaAnual.data_assinatura.between(data_inicio, data_fim)
).all()
return render_template("relatorio_assinaturas.html",
@@ -1406,227 +1405,41 @@ def create_app():
finally:
session.close()
@app.route('/novo_comprovante', methods=['GET', 'POST'])
@login_required
def novo_comprovante():
"""Rota para criar um novo comprovante"""
try:
session = get_db_connection()
if request.method == 'POST':
militante_id = request.form.get('militante_id')
tipo_comprovante = request.form.get('tipo_comprovante')
valor = request.form.get('valor')
data_comprovante = request.form.get('data_comprovante')
if not all([militante_id, tipo_comprovante, valor, data_comprovante]):
flash('Todos os campos são obrigatórios', 'error')
return redirect(url_for('novo_comprovante'))
comprovante = Comprovante(
militante_id=militante_id,
tipo_comprovante=tipo_comprovante,
valor=float(valor.replace('R$', '').replace('.', '').replace(',', '.')),
data_comprovante=datetime.strptime(data_comprovante, '%Y-%m-%d')
)
session.add(comprovante)
session.commit()
flash('Comprovante criado com sucesso!', 'success')
return redirect(url_for('listar_comprovantes'))
militantes = session.query(Militante).all()
tipos_comprovante = session.query(TipoComprovante).all()
return render_template(
'novo_comprovante.html',
militantes=militantes,
tipos_comprovante=tipos_comprovante
)
except Exception as e:
flash(f'Erro ao criar comprovante: {str(e)}', 'error')
return redirect(url_for('listar_comprovantes'))
finally:
session.close()
@app.route('/listar_comprovantes')
@app.route('/comprovantes')
@login_required
@require_permission(Permission.MANAGE_MATERIALS)
def listar_comprovantes():
"""Rota para listar todos os comprovantes"""
try:
session = get_db_connection()
comprovantes = session.query(Comprovante).all()
return render_template('listar_comprovantes.html', comprovantes=comprovantes)
db = get_db_connection()
comprovantes = db.query(Comprovante)\
.options(joinedload(Comprovante.centralizacoes))\
.options(joinedload(Comprovante.militante))\
.options(joinedload(Comprovante.campanha))\
.all()
militantes = db.query(Militante).order_by(Militante.nome).all()
campanhas = db.query(CampanhaFinanceira).all()
return render_template('listar_comprovantes.html',
comprovantes=comprovantes,
militantes=militantes,
campanhas=campanhas)
except Exception as e:
flash(f'Erro ao listar comprovantes: {str(e)}', 'error')
return redirect(url_for('dashboard'))
finally:
session.close()
db.close()
@app.route('/adicionar_comprovante', methods=['POST'])
@app.route('/comprovantes/<int:id>', methods=['DELETE'])
@login_required
def adicionar_comprovante():
"""Rota para adicionar um novo comprovante via AJAX"""
@require_permission(Permission.MANAGE_MATERIALS)
def excluir_comprovante(id):
try:
session = get_db_connection()
data = request.get_json()
comprovante = Comprovante(
militante_id=data['militante_id'],
tipo_comprovante=data['tipo_comprovante'],
valor=float(data['valor'].replace('R$', '').replace('.', '').replace(',', '.')),
data_comprovante=datetime.strptime(data['data_comprovante'], '%Y-%m-%d')
)
session.add(comprovante)
session.commit()
return jsonify({
'status': 'success',
'message': 'Comprovante adicionado com sucesso!'
})
comprovante = Comprovante.query.get_or_404(id)
db.session.delete(comprovante)
db.session.commit()
return jsonify({'success': True})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Erro ao adicionar comprovante: {str(e)}'
})
finally:
session.close()
@app.route("/relatorios/comprovantes/novo", methods=["GET", "POST"])
@require_login
@require_permission(Permission.MANAGE_CELL_REPORTS)
def novo_relatorio_comprovantes():
if request.method == "POST":
try:
data_inicio = request.form.get("data_inicio")
data_fim = request.form.get("data_fim")
if not all([data_inicio, data_fim]):
flash("Todos os campos são obrigatórios", "danger")
return redirect(url_for("novo_relatorio_comprovantes"))
db = get_db_connection()
comprovantes = db.query(Comprovante).filter(
Comprovante.data_comprovante.between(data_inicio, data_fim)
).all()
return render_template("relatorio_comprovantes.html",
comprovantes=comprovantes,
data_inicio=data_inicio,
data_fim=data_fim)
except Exception as e:
flash(f"Erro ao gerar relatório: {str(e)}", "danger")
return redirect(url_for("novo_relatorio_comprovantes"))
return render_template("novo_relatorio_comprovantes.html")
@app.route("/relatorios/comprovantes/celula/<int:celula_id>", methods=["GET", "POST"])
@require_login
@require_permission(Permission.MANAGE_CELL_REPORTS)
def relatorio_comprovantes_celula(celula_id):
if request.method == "POST":
try:
data_inicio = request.form.get("data_inicio")
data_fim = request.form.get("data_fim")
if not all([data_inicio, data_fim]):
flash("Todos os campos são obrigatórios", "danger")
return redirect(url_for("relatorio_comprovantes_celula", celula_id=celula_id))
db = get_db_connection()
comprovantes = db.query(Comprovante).join(Militante).filter(
Militante.celula_id == celula_id,
Comprovante.data_comprovante.between(data_inicio, data_fim)
).all()
return render_template("relatorio_comprovantes.html",
comprovantes=comprovantes,
data_inicio=data_inicio,
data_fim=data_fim)
except Exception as e:
flash(f"Erro ao gerar relatório: {str(e)}", "danger")
return redirect(url_for("relatorio_comprovantes_celula", celula_id=celula_id))
return render_template("novo_relatorio_comprovantes.html", celula_id=celula_id)
@app.route("/assinaturas/jornal/novo", methods=["GET", "POST"])
@require_login
@require_permission(Permission.MANAGE_MATERIALS)
def nova_assinatura_jornal():
if request.method == "POST":
try:
militante_id = request.form.get("militante_id")
data_inicio = request.form.get("data_inicio")
data_fim = request.form.get("data_fim")
if not all([militante_id, data_inicio, data_fim]):
flash("Todos os campos são obrigatórios", "danger")
return redirect(url_for("nova_assinatura_jornal"))
db = get_db_connection()
assinatura = AssinaturaJornal(
militante_id=militante_id,
data_inicio=data_inicio,
data_fim=data_fim
)
db.add(assinatura)
db.commit()
flash("Assinatura de jornal registrada com sucesso", "success")
return redirect(url_for("listar_assinaturas_jornal"))
except Exception as e:
flash(f"Erro ao registrar assinatura de jornal: {str(e)}", "danger")
return redirect(url_for("nova_assinatura_jornal"))
db = get_db_connection()
militantes = db.query(Militante).all()
return render_template("nova_assinatura_jornal.html", militantes=militantes)
@app.route("/assinaturas/jornal")
@require_login
@require_permission(Permission.MANAGE_MATERIALS)
def listar_assinaturas_jornal():
db = get_db_connection()
assinaturas = db.query(AssinaturaJornal).all()
return render_template("listar_assinaturas_jornal.html", assinaturas=assinaturas)
@app.route("/campanhas/financeira/novo", methods=["GET", "POST"])
@require_login
@require_permission(Permission.MANAGE_MATERIALS)
def nova_campanha_financeira():
if request.method == "POST":
try:
militante_id = request.form.get("militante_id")
valor = request.form.get("valor")
data_campanha = request.form.get("data_campanha")
if not all([militante_id, valor, data_campanha]):
flash("Todos os campos são obrigatórios", "danger")
return redirect(url_for("nova_campanha_financeira"))
db = get_db_connection()
campanha = CampanhaFinanceira(
militante_id=militante_id,
valor=valor,
data_campanha=data_campanha
)
db.add(campanha)
db.commit()
flash("Campanha financeira registrada com sucesso", "success")
return redirect(url_for("listar_campanhas_financeira"))
except Exception as e:
flash(f"Erro ao registrar campanha financeira: {str(e)}", "danger")
return redirect(url_for("nova_campanha_financeira"))
db = get_db_connection()
militantes = db.query(Militante).all()
return render_template("nova_campanha_financeira.html", militantes=militantes)
@app.route("/campanhas/financeira")
@require_login
@require_permission(Permission.MANAGE_MATERIALS)
def listar_campanhas_financeira():
db = get_db_connection()
campanhas = db.query(CampanhaFinanceira).all()
return render_template("listar_campanhas_financeira.html", campanhas=campanhas)
db.session.rollback()
return jsonify({'success': False, 'message': str(e)})
return app
@@ -1696,6 +1509,10 @@ def main():
# Inicializar o sistema
init_system()
# Configurar modo debug
app.debug = True
app.config['DEBUG'] = True
return app
# Criar a aplicação usando a função main
@@ -1704,6 +1521,6 @@ app = main()
if __name__ == '__main__':
app.run(
host='0.0.0.0',
port=5000,
debug=os.getenv('FLASK_ENV') == 'development'
port=int(os.getenv('FLASK_PORT', 5000)),
debug=True
)

View File

@@ -41,102 +41,37 @@ def generate_qr_code(user):
return qr_path, otp_uri
def create_admin_user():
"""Cria ou atualiza o usuário admin"""
"""Cria o usuário admin do sistema"""
session = get_db_connection()
try:
# Inicializar banco de dados
init_database()
# Criar sessão
db = get_db_connection()
try:
# Verificar se já existe um usuário admin
admin = db.query(Usuario).filter_by(username="admin").first()
if admin:
print("\n=== Usuário Admin Encontrado ===")
if not admin.otp_secret:
print("Gerando novo segredo OTP...")
admin.generate_otp_secret()
db.commit()
else:
print("\n=== Criando Novo Usuário Admin ===")
# Criar novo usuário admin
admin = Usuario(
username="admin",
email="admin@example.com",
is_admin=True
)
admin.set_password("admin123")
admin.generate_otp_secret()
# 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()
# Buscar role de administrador
admin_role = session.query(Role).filter_by(nome="Administrador").first()
if not admin_role:
print("Role de administrador não encontrada!")
return
# Verificar se o usuário admin já existe
if not session.query(Usuario).filter_by(username="admin").first():
admin = Usuario(
username="admin",
email="admin@example.com",
is_admin=True
)
admin.set_password("admin123")
admin.tipo = "ADMIN"
admin.roles.append(admin_role)
session.add(admin)
session.commit()
print("Usuário admin criado com sucesso!")
else:
print("Usuário admin já existe!")
except Exception as e:
print(f"\nErro durante a execução: {e}")
import traceback
traceback.print_exc()
print(f"Erro ao criar usuário admin: {e}")
session.rollback()
raise
finally:
session.close()
if __name__ == "__main__":
create_admin_user()

65
create_test_users.py Normal file
View File

@@ -0,0 +1,65 @@
from functions.database import Usuario, Role, get_db_connection
def create_test_users():
"""Cria usuários de teste para o sistema"""
session = get_db_connection()
try:
# Buscar roles
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': 'celula',
'email': 'celula@example.com',
'password': 'celula123',
'role': secretario_celula,
'tipo': 'SECRETARIO_CELULA'
},
{
'username': 'setor',
'email': 'setor@example.com',
'password': 'setor123',
'role': secretario_setor,
'tipo': 'SECRETARIO_SETOR'
},
{
'username': 'cr',
'email': 'cr@example.com',
'password': 'cr123',
'role': secretario_cr,
'tipo': 'SECRETARIO_CR'
},
{
'username': 'geral',
'email': 'geral@example.com',
'password': 'geral123',
'role': secretario_geral,
'tipo': 'SECRETARIO_GERAL'
}
]
for user_data in usuarios:
# Verificar se o usuário já existe
if not session.query(Usuario).filter_by(username=user_data['username']).first():
user = Usuario(
username=user_data['username'],
email=user_data['email']
)
user.set_password(user_data['password'])
user.tipo = user_data['tipo']
user.roles.append(user_data['role'])
session.add(user)
session.commit()
print("Usuários de teste criados com sucesso!")
except Exception as e:
print(f"Erro ao criar usuários de teste: {e}")
session.rollback()
raise
finally:
session.close()

View File

@@ -14,6 +14,9 @@ from flask_login import UserMixin
from .rbac import Role, Permission, role_permissions, user_roles
from .base import Base, engine, Session
import logging
import qrcode
from PIL import Image
import re
# Configurar caminho do banco de dados
db_dir = Path.home() / '.local' / 'share' / 'controles'
@@ -329,7 +332,6 @@ class Pagamento(Base):
data_pagamento = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="pagamentos")
transacoes_pix = relationship("TransacaoPIX", back_populates="pagamento")
class TipoMaterial(Base):
__tablename__ = 'tipos_materiais'
@@ -608,6 +610,49 @@ class Relatorio(Base):
setor = relationship("Setor", foreign_keys=[setor_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):
__tablename__ = 'transacoes_pix'
@@ -618,25 +663,9 @@ class TransacaoPIX(Base):
data_pagamento = Column(DateTime)
status = Column(String(20)) # Pendente, Pago, Expirado
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")
class TipoComprovante(Base):
__tablename__ = 'tipos_comprovante'
id = Column(Integer, primary_key=True)
descricao = Column(String(50), nullable=False)
valor = Column(Float, nullable=False)
class Comprovante(Base):
__tablename__ = 'comprovantes'
id = Column(Integer, primary_key=True)
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
tipo_comprovante = Column(String(50)) # Cota, Jornal, Assinatura, etc.
data_comprovante = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="comprovantes")
transacoes_pix = relationship("TransacaoPIX", back_populates="comprovante")
comprovante = relationship("Comprovante", back_populates="transacoes_pix")
def init_database():
"""Inicializa o banco de dados com dados básicos"""
@@ -677,9 +706,30 @@ def init_database():
session.add(comite)
session.commit()
# Gerar OTP para admin
admin_otp_secret = pyotp.random_base32()
print(f"Novo OTP gerado: {admin_otp_secret}")
# Verificar se existe QR code do admin
admin_otp_secret = None
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
admin_role = session.query(Role).filter_by(nome="Administrador").first()
@@ -698,23 +748,23 @@ def init_database():
session.add(admin)
session.commit()
# Gerar QR code
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.add_data(provisioning_uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save('admin_qr.png')
# Gerar QR code apenas se não existir
if not os.path.exists(qr_path):
totp = pyotp.totp.TOTP(admin_otp_secret)
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(provisioning_uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(qr_path)
print("=== Usuário Admin Criado ===")
print(f"Username: admin")
print(f"Senha: admin123")
print(f"Email: {admin.email}")
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
from seed_data import seed_database

View File

@@ -68,6 +68,8 @@ class Permission(Base):
EDIT_OWN_DATA = "edit_own_data"
VIEW_CELL_DATA = "view_cell_data"
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
MANAGE_CELL_MEMBERS = "manage_cell_members"
@@ -102,13 +104,15 @@ class Permission(Base):
(Permission.VIEW_OWN_DATA, "Visualizar próprios dados"),
(Permission.EDIT_OWN_DATA, "Editar próprios dados"),
(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
(Permission.MANAGE_CELL_MEMBERS, "Gerenciar membros da célula"),
(Permission.CREATE_CELL_MEMBER, "Criar membros na 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"),
# 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.VIEW_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
@@ -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.MANAGE_CELL_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
@@ -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.MANAGE_SECTOR_CELLS).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
@@ -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.CREATE_SECTOR_CELL).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
@@ -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.MANAGE_CR_SECTORS).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
@@ -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.CREATE_CR_SECTOR).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
@@ -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.CREATE_CC_CR).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()

View File

@@ -8,7 +8,7 @@ Werkzeug==3.0.1
python-dotenv==1.0.1
pyotp==2.9.0
qrcode==7.4.2
Pillow==9.5.0
Pillow==10.2.0
email-validator==2.1.0.post1
cryptography==42.0.2
bcrypt==4.1.2
@@ -17,3 +17,6 @@ flask-bootstrap5==0.1.dev1
PyJWT==2.8.0
gunicorn==21.2.0
Faker==19.13.0
pytest==8.0.0
pytest-cov==4.1.0
pyzbar==0.1.9

View File

@@ -5,7 +5,7 @@ from functions.database import (
RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal,
Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco,
ComiteRegional, Celula, EstadoMilitante, get_db_connection,
init_database
init_database, CentralizacaoComprovante
)
import random
from faker import Faker
@@ -59,17 +59,17 @@ def criar_tipos_comprovante(session):
"""Cria tipos de comprovante padrão"""
print("\nCriando tipos de comprovante...")
tipos = [
"Comprovante Padrão",
"Comprovante Especial",
"Comprovante Extraordinário",
"Jornal Avulso",
"Assinatura de Jornal",
"Campanha Financeira"
("Comprovante Padrão", 50.00),
("Comprovante Especial", 100.00),
("Comprovante Extraordinário", 200.00),
("Jornal Avulso", 5.00),
("Assinatura de Jornal", 30.00),
("Campanha Financeira", 0.00) # Valor variável
]
for tipo in tipos:
if not session.query(TipoComprovante).filter_by(descricao=tipo).first():
session.add(TipoComprovante(descricao=tipo))
for descricao, valor in tipos:
if not session.query(TipoComprovante).filter_by(descricao=descricao).first():
session.add(TipoComprovante(descricao=descricao, valor=valor))
try:
session.commit()
@@ -229,14 +229,25 @@ def criar_comprovantes(session, militantes):
try:
# Criar entre 3 e 8 comprovantes por militante
for _ in range(random.randint(3, 8)):
tipo = random.choice(tipos_comprovante)
# Criar o comprovante base
comprovante = Comprovante(
militante_id=militante.id,
tipo_comprovante=tipo.descricao, # Usando a descrição do tipo
valor=random.uniform(10, 1000),
data_comprovante=fake.date_between(start_date='-1y', end_date='today')
data_comprovante=fake.date_between(start_date='-1y', end_date='today'),
forma_pagamento=random.choice(['PIX', 'transferência/DOC', 'depósito', 'maquininha'])
)
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()
except Exception as e:
session.rollback()

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
document.addEventListener('DOMContentLoaded', function() {
// Máscara para CPF

View File

@@ -541,8 +541,8 @@
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('listar_pagamentos') }}">
<i class="fas fa-receipt"></i>Pagamentos
<a class="dropdown-item" href="{{ url_for('listar_comprovantes') }}">
<i class="fas fa-receipt"></i>Comprovantes
</a>
</li>
</ul>
@@ -563,8 +563,8 @@
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('listar_assinaturas') }}">
<i class="fas fa-file-signature"></i>Assinaturas
<a class="dropdown-item" href="{{ url_for('listar_vendas_jornal') }}">
<i class="fas fa-file-signature"></i>Assinaturas de Jornal
</a>
</li>
</ul>

View File

@@ -57,7 +57,7 @@
<div class="stats-card yellow">
<div class="title">Assinaturas Ativas</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>
</a>
<div class="icon">

View File

@@ -7,7 +7,7 @@
<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="#modalNovoComprovante">
<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">
@@ -22,40 +22,30 @@
<table class="table table-striped table-hover" id="tabelaComprovantes">
<thead>
<tr>
<th>ID</th>
<th>Militante</th>
<th>Tipo de Comprovante</th>
<th>Valor</th>
<th>Data do Comprovante</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 data-militante="{{ comprovante.militante_id }}">{{ comprovante.militante.nome if comprovante.militante else 'N/A' }}</td>
<td data-tipo="{{ comprovante.tipo_comprovante }}">
{% if comprovante.tipo_comprovante == 1 %}
Cota
{% elif comprovante.tipo_comprovante == 2 %}
Contribuição Extra
{% elif comprovante.tipo_comprovante == 3 %}
Doação
{% elif comprovante.tipo_comprovante == 4 %}
Taxa de Evento
{% elif comprovante.tipo_comprovante == 5 %}
Jornal Avulso
{% elif comprovante.tipo_comprovante == 6 %}
Assinatura de Jornal
{% elif comprovante.tipo_comprovante == 7 %}
Campanha Financeira
{% elif comprovante.tipo_comprovante == 8 %}
Outros
{% else %}
Não Definido
{% endif %}
<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 data-valor="{{ comprovante.valor }}">R$ {{ "%.2f"|format(comprovante.valor) }}</td>
<td data-data="{{ comprovante.data_comprovante }}">{{ comprovante.data_comprovante.strftime('%d/%m/%Y') }}</td>
<td>
<button type="button"
class="btn btn-sm btn-outline-primary"
@@ -63,8 +53,7 @@
data-bs-target="#modalEditarComprovante"
data-comprovante-id="{{ comprovante.id }}"
data-militante-id="{{ comprovante.militante_id }}"
data-tipo-comprovante="{{ comprovante.tipo_comprovante }}"
data-valor="{{ comprovante.valor }}"
data-militante-nome="{{ comprovante.militante.nome }}"
data-data-comprovante="{{ comprovante.data_comprovante.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i>
@@ -74,7 +63,7 @@
data-bs-toggle="modal"
data-bs-target="#modalExcluirComprovante"
data-comprovante-id="{{ comprovante.id }}"
data-comprovante-info="Comprovante de {{ comprovante.militante.nome if comprovante.militante else 'N/A' }} - R$ {{ "%.2f"|format(comprovante.valor) }}"
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>
@@ -89,58 +78,159 @@
</div>
<!-- Modal Novo Comprovante -->
<div class="modal fade" id="modalNovoComprovante" tabindex="-1">
<div class="modal-dialog">
<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"><i class="fas fa-plus"></i> Novo Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<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="formNovoComprovante" method="post" action="{{ url_for('adicionar_comprovante') }}">
<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>
<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="mb-3">
<label for="tipoComprovante" class="form-label">Tipo de Comprovante:</label>
<select class="form-select" id="tipoComprovante" 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 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>
<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="dataComprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="dataComprovante" 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>
<!-- 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">

View File

@@ -47,7 +47,7 @@
</div>
<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>
<div class="invalid-feedback">
Por favor, informe o código OTP.

View File

@@ -31,7 +31,7 @@
</div>
<div class="d-flex gap-2">
<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>
</div>
</form>

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