From 8ff58cc51e78b671e28bcb03ceabcf1f143cbafc Mon Sep 17 00:00:00 2001 From: LS Date: Wed, 16 Apr 2025 13:54:31 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20implementa=20sistema=20de=20comprovante?= =?UTF-8?q?s=20com=20centraliza=C3=A7=C3=B5es=20e=20PIX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 18 ++ app.py | 323 +++++++---------------------- create_admin.py | 123 +++-------- create_test_users.py | 65 ++++++ functions/database.py | 116 ++++++++--- functions/rbac.py | 29 ++- requirements.txt | 5 +- seed_data.py | 39 ++-- static/js/main.js | 12 ++ templates/base.html | 8 +- templates/home.html | 2 +- templates/listar_comprovantes.html | 228 ++++++++++++++------ templates/login.html | 2 +- templates/nova_assinatura.html | 2 +- tests/test_routes.py | 89 ++++++++ 15 files changed, 581 insertions(+), 480 deletions(-) create mode 100644 create_test_users.py create mode 100644 tests/test_routes.py diff --git a/Makefile b/Makefile index a9a5333..889a60a 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/app.py b/app.py index 44e556a..588b007 100644 --- a/app.py +++ b/app.py @@ -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/', 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/", 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 ) diff --git a/create_admin.py b/create_admin.py index 9d70e6d..0c6a05d 100644 --- a/create_admin.py +++ b/create_admin.py @@ -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() diff --git a/create_test_users.py b/create_test_users.py new file mode 100644 index 0000000..ce757e5 --- /dev/null +++ b/create_test_users.py @@ -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() \ No newline at end of file diff --git a/functions/database.py b/functions/database.py index 885461b..76a9d76 100644 --- a/functions/database.py +++ b/functions/database.py @@ -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 diff --git a/functions/rbac.py b/functions/rbac.py index 21baf57..9537ed7 100644 --- a/functions/rbac.py +++ b/functions/rbac.py @@ -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() diff --git a/requirements.txt b/requirements.txt index 6f6c92e..2e89b4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/seed_data.py b/seed_data.py index 947ade1..a234ec8 100644 --- a/seed_data.py +++ b/seed_data.py @@ -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() diff --git a/static/js/main.js b/static/js/main.js index fa31277..f8a532a 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -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 diff --git a/templates/base.html b/templates/base.html index 9327f54..ea0b2a9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -541,8 +541,8 @@
  • - - Pagamentos + + Comprovantes
  • @@ -563,8 +563,8 @@
  • - - Assinaturas + + Assinaturas de Jornal
  • diff --git a/templates/home.html b/templates/home.html index cf4212b..b47197e 100644 --- a/templates/home.html +++ b/templates/home.html @@ -57,7 +57,7 @@
    Assinaturas Ativas
    {{ total_assinaturas }}
    - + Ver detalhes
    diff --git a/templates/listar_comprovantes.html b/templates/listar_comprovantes.html index 3781c48..75c6049 100644 --- a/templates/listar_comprovantes.html +++ b/templates/listar_comprovantes.html @@ -7,7 +7,7 @@

    Comprovantes

    -