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: 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
@@ -18,3 +30,9 @@ 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

297
app.py
View File

@@ -24,14 +24,14 @@ from functions.database import (
Endereco, Endereco,
TipoComprovante, TipoComprovante,
Comprovante, Comprovante,
VendaJornal,
AssinaturaJornal,
CampanhaFinanceira, CampanhaFinanceira,
TransacaoPIX, TransacaoPIX,
Permission, Permission,
Role, Role,
RolePermission, Atividade,
UserRole MaterialAtividade,
Relatorio,
CentralizacaoComprovante
) )
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, joinedload from sqlalchemy.orm import sessionmaker, joinedload
@@ -73,10 +73,6 @@ def create_app():
csrf = CSRFProtect() csrf = CSRFProtect()
csrf.init_app(app) 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 # Configurar Flask-Login
login_manager = LoginManager() login_manager = LoginManager()
login_manager.init_app(app) login_manager.init_app(app)
@@ -88,6 +84,13 @@ def create_app():
"""Filtro para operação bit a bit AND""" """Filtro para operação bit a bit AND"""
return value1 & value2 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 @login_manager.user_loader
def load_user(user_id): def load_user(user_id):
"""Carrega o usuário pelo ID""" """Carrega o usuário pelo ID"""
@@ -214,12 +217,13 @@ def create_app():
flash("Email/usuário ou senha incorretos.", "danger") flash("Email/usuário ou senha incorretos.", "danger")
return redirect(url_for("login")) return redirect(url_for("login"))
# Verificar OTP se o usuário tiver configurado # Verificar OTP apenas se o usuário tiver configurado
if user.otp_secret and not otp: if user.otp_secret:
if not otp:
flash("Código OTP é obrigatório para sua conta.", "danger") flash("Código OTP é obrigatório para sua conta.", "danger")
return redirect(url_for("login")) return redirect(url_for("login"))
if user.otp_secret and not user.verify_otp(otp): if not user.verify_otp(otp):
flash("Código OTP inválido.", "danger") flash("Código OTP inválido.", "danger")
return redirect(url_for("login")) return redirect(url_for("login"))
@@ -282,27 +286,22 @@ def create_app():
.limit(5)\ .limit(5)\
.all() .all()
# Buscar últimos comprovantes # Buscar últimos pagamentos
ultimos_comprovantes = db.query(Comprovante)\ ultimos_pagamentos = db.query(Pagamento)\
.join(Militante)\ .order_by(Pagamento.data_pagamento.desc())\
.order_by(Comprovante.data_comprovante.desc())\
.limit(5)\ .limit(5)\
.all() .all()
# Buscar tipos de comprovante
tipos_comprovante = db.query(TipoComprovante).all()
return render_template('home.html', return render_template('home.html',
nome_usuario=nome_usuario, nome_usuario=nome_usuario,
data_atual=data_atual, data_atual=data_atual,
total_militantes=total_militantes, total_militantes=total_militantes,
total_cotas="{:.2f}".format(total_cotas), total_cotas=total_cotas,
total_materiais=total_materiais, total_materiais=total_materiais,
total_assinaturas=total_assinaturas, total_assinaturas=total_assinaturas,
ultimos_militantes=ultimos_militantes, ultimos_militantes=ultimos_militantes,
ultimos_comprovantes=ultimos_comprovantes, ultimos_pagamentos=ultimos_pagamentos,
tipos_comprovante=tipos_comprovante, Militante=Militante)
user=current_user)
except Exception as e: except Exception as e:
print(f"Erro na página inicial: {e}") print(f"Erro na página inicial: {e}")
import traceback import traceback
@@ -316,7 +315,7 @@ def create_app():
total_materiais=0, total_materiais=0,
total_assinaturas=0, total_assinaturas=0,
ultimos_militantes=[], ultimos_militantes=[],
ultimos_comprovantes=[], ultimos_pagamentos=[],
Militante=Militante) Militante=Militante)
finally: finally:
db.close() db.close()
@@ -819,7 +818,7 @@ def create_app():
return redirect(url_for("nova_venda_jornal")) return redirect(url_for("nova_venda_jornal"))
db = get_db_connection() db = get_db_connection()
venda_jornal = VendaJornal( venda_jornal = VendaJornalAvulso(
militante_id=militante_id, militante_id=militante_id,
quantidade=quantidade, quantidade=quantidade,
data_venda=data_venda data_venda=data_venda
@@ -842,7 +841,7 @@ def create_app():
@require_permission(Permission.MANAGE_MATERIALS) @require_permission(Permission.MANAGE_MATERIALS)
def listar_vendas_jornal(): def listar_vendas_jornal():
db = get_db_connection() 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) return render_template("listar_vendas_jornal.html", vendas_jornal=vendas_jornal)
# Rota para criar um novo relatório de cotas # Rota para criar um novo relatório de cotas
@@ -888,8 +887,8 @@ def create_app():
return redirect(url_for("novo_relatorio_jornais")) return redirect(url_for("novo_relatorio_jornais"))
db = get_db_connection() db = get_db_connection()
jornais = db.query(VendaJornal).filter( jornais = db.query(VendaJornalAvulso).filter(
VendaJornal.data_venda.between(data_inicio, data_fim) VendaJornalAvulso.data_venda.between(data_inicio, data_fim)
).all() ).all()
return render_template("relatorio_jornais.html", return render_template("relatorio_jornais.html",
@@ -916,8 +915,8 @@ def create_app():
return redirect(url_for("novo_relatorio_assinaturas")) return redirect(url_for("novo_relatorio_assinaturas"))
db = get_db_connection() db = get_db_connection()
assinaturas = db.query(AssinaturaJornal).filter( assinaturas = db.query(AssinaturaAnual).filter(
AssinaturaJornal.data_assinatura.between(data_inicio, data_fim) AssinaturaAnual.data_assinatura.between(data_inicio, data_fim)
).all() ).all()
return render_template("relatorio_assinaturas.html", return render_template("relatorio_assinaturas.html",
@@ -1406,227 +1405,41 @@ def create_app():
finally: finally:
session.close() session.close()
@app.route('/novo_comprovante', methods=['GET', 'POST']) @app.route('/comprovantes')
@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')
@login_required @login_required
@require_permission(Permission.MANAGE_MATERIALS)
def listar_comprovantes(): def listar_comprovantes():
"""Rota para listar todos os comprovantes"""
try: try:
session = get_db_connection() db = get_db_connection()
comprovantes = session.query(Comprovante).all() comprovantes = db.query(Comprovante)\
return render_template('listar_comprovantes.html', comprovantes=comprovantes) .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: except Exception as e:
flash(f'Erro ao listar comprovantes: {str(e)}', 'error') flash(f'Erro ao listar comprovantes: {str(e)}', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
finally: finally:
session.close() db.close()
@app.route('/adicionar_comprovante', methods=['POST']) @app.route('/comprovantes/<int:id>', methods=['DELETE'])
@login_required @login_required
def adicionar_comprovante():
"""Rota para adicionar um novo comprovante via AJAX"""
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!'
})
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) @require_permission(Permission.MANAGE_MATERIALS)
def nova_assinatura_jornal(): def excluir_comprovante(id):
if request.method == "POST":
try: try:
militante_id = request.form.get("militante_id") comprovante = Comprovante.query.get_or_404(id)
data_inicio = request.form.get("data_inicio") db.session.delete(comprovante)
data_fim = request.form.get("data_fim") db.session.commit()
return jsonify({'success': True})
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: except Exception as e:
flash(f"Erro ao registrar assinatura de jornal: {str(e)}", "danger") db.session.rollback()
return redirect(url_for("nova_assinatura_jornal")) return jsonify({'success': False, 'message': str(e)})
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)
return app return app
@@ -1696,6 +1509,10 @@ def main():
# Inicializar o sistema # Inicializar o sistema
init_system() init_system()
# Configurar modo debug
app.debug = True
app.config['DEBUG'] = True
return app return app
# Criar a aplicação usando a função main # Criar a aplicação usando a função main
@@ -1704,6 +1521,6 @@ app = main()
if __name__ == '__main__': if __name__ == '__main__':
app.run( app.run(
host='0.0.0.0', host='0.0.0.0',
port=5000, port=int(os.getenv('FLASK_PORT', 5000)),
debug=os.getenv('FLASK_ENV') == 'development' debug=True
) )

View File

@@ -41,102 +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():
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( admin = Usuario(
username="admin", username="admin",
email="admin@example.com", email="admin@example.com",
is_admin=True is_admin=True
) )
admin.set_password("admin123") admin.set_password("admin123")
admin.generate_otp_secret() admin.tipo = "ADMIN"
admin.roles.append(admin_role)
# Adicionar e fazer commit session.add(admin)
db.add(admin) session.commit()
db.commit() print("Usuário admin criado com sucesso!")
# 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: else:
print("\n=== QR Code Existente ===") print("Usuário admin já existe!")
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: except Exception as e:
db.rollback() print(f"Erro ao criar usuário admin: {e}")
raise e session.rollback()
raise
finally: finally:
db.close() session.close()
except Exception as e:
print(f"\nErro durante a execução: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__": if __name__ == "__main__":
create_admin_user() 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 .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'
@@ -329,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'
@@ -608,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'
@@ -618,25 +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")
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")
def init_database(): def init_database():
"""Inicializa o banco de dados com dados básicos""" """Inicializa o banco de dados com dados básicos"""
@@ -677,7 +706,28 @@ 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 = 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() admin_otp_secret = pyotp.random_base32()
print(f"Novo OTP gerado: {admin_otp_secret}") print(f"Novo OTP gerado: {admin_otp_secret}")
@@ -698,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
if not os.path.exists(qr_path):
totp = pyotp.totp.TOTP(admin_otp_secret) totp = pyotp.totp.TOTP(admin_otp_secret)
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles") 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('admin_qr.png') img.save(qr_path)
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

@@ -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()

View File

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

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

@@ -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>

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

@@ -7,7 +7,7 @@
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="fas fa-money-bill-wave"></i> Comprovantes</h2> <h2><i class="fas fa-money-bill-wave"></i> Comprovantes</h2>
<div> <div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoComprovante"> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#novoComprovanteModal">
<i class="fas fa-plus"></i> Novo Comprovante <i class="fas fa-plus"></i> Novo Comprovante
</button> </button>
<button type="button" class="btn btn-outline-primary" id="btnExportar"> <button type="button" class="btn btn-outline-primary" id="btnExportar">
@@ -22,40 +22,30 @@
<table class="table table-striped table-hover" id="tabelaComprovantes"> <table class="table table-striped table-hover" id="tabelaComprovantes">
<thead> <thead>
<tr> <tr>
<th>ID</th>
<th>Militante</th> <th>Militante</th>
<th>Tipo de Comprovante</th> <th>Data</th>
<th>Valor</th> <th>Forma de Pagamento</th>
<th>Data do Comprovante</th> <th>Campanha</th>
<th>Centralizações</th>
<th>Ações</th> <th>Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for comprovante in comprovantes %} {% for comprovante in comprovantes %}
<tr> <tr>
<td data-militante="{{ comprovante.militante_id }}">{{ comprovante.militante.nome if comprovante.militante else 'N/A' }}</td> <td>{{ comprovante.id }}</td>
<td data-tipo="{{ comprovante.tipo_comprovante }}"> <td>{{ comprovante.militante.nome }}</td>
{% if comprovante.tipo_comprovante == 1 %} <td>{{ comprovante.data_comprovante.strftime('%d/%m/%Y') }}</td>
Cota <td>{{ comprovante.forma_pagamento }}</td>
{% elif comprovante.tipo_comprovante == 2 %} <td>{{ comprovante.campanha.nome if comprovante.campanha else '-' }}</td>
Contribuição Extra <td>
{% elif comprovante.tipo_comprovante == 3 %} <ul class="list-unstyled">
Doação {% for centralizacao in comprovante.centralizacoes %}
{% elif comprovante.tipo_comprovante == 4 %} <li>{{ centralizacao.tipo_comprovante }}: R$ {{ "%.2f"|format(centralizacao.valor) }}</li>
Taxa de Evento {% endfor %}
{% elif comprovante.tipo_comprovante == 5 %} </ul>
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> </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> <td>
<button type="button" <button type="button"
class="btn btn-sm btn-outline-primary" class="btn btn-sm btn-outline-primary"
@@ -63,8 +53,7 @@
data-bs-target="#modalEditarComprovante" data-bs-target="#modalEditarComprovante"
data-comprovante-id="{{ comprovante.id }}" data-comprovante-id="{{ comprovante.id }}"
data-militante-id="{{ comprovante.militante_id }}" data-militante-id="{{ comprovante.militante_id }}"
data-tipo-comprovante="{{ comprovante.tipo_comprovante }}" data-militante-nome="{{ comprovante.militante.nome }}"
data-valor="{{ comprovante.valor }}"
data-data-comprovante="{{ comprovante.data_comprovante.strftime('%Y-%m-%d') }}" data-data-comprovante="{{ comprovante.data_comprovante.strftime('%Y-%m-%d') }}"
title="Editar"> title="Editar">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
@@ -74,7 +63,7 @@
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#modalExcluirComprovante" data-bs-target="#modalExcluirComprovante"
data-comprovante-id="{{ comprovante.id }}" 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"> title="Excluir">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
@@ -89,58 +78,159 @@
</div> </div>
<!-- Modal Novo Comprovante --> <!-- Modal Novo Comprovante -->
<div class="modal fade" id="modalNovoComprovante" tabindex="-1"> <div class="modal fade" id="novoComprovanteModal" tabindex="-1" aria-labelledby="novoComprovanteModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><i class="fas fa-plus"></i> Novo Comprovante</h5> <h5 class="modal-title" id="novoComprovanteModalLabel">Novo Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="formNovoComprovante" method="post" action="{{ url_for('adicionar_comprovante') }}"> <form id="novoComprovanteForm">
<div class="mb-3"> <!-- Dados únicos do comprovante -->
<label for="militante" class="form-label">Militante:</label> <div class="row mb-3">
<select class="form-select" id="militante" name="militante_id" required> <div class="col-md-6">
<option value="">Selecione um militante</option> <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 %} {% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option> <option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="mb-3"> <div class="col-md-6">
<label for="tipoComprovante" class="form-label">Tipo de Comprovante:</label> <label for="data_comprovante" class="form-label">Data do Comprovante</label>
<select class="form-select" id="tipoComprovante" name="tipo_comprovante" required> <input type="date" class="form-control" id="data_comprovante" name="data_comprovante" required>
<option value="">Selecione o tipo</option> </div>
<option value="1">Cota</option> </div>
{% if current_user.has_permission('gerenciar_tipos_comprovante') %} <div class="row mb-3">
<option value="2">Contribuição Extra</option> <div class="col-md-6">
<option value="3">Doação</option> <label for="forma_pagamento" class="form-label">Forma de Pagamento</label>
<option value="4">Taxa de Evento</option> <select class="form-select" id="forma_pagamento" name="forma_pagamento" required>
<option value="5">Jornal Avulso</option> <option value="">Selecione a forma de pagamento</option>
<option value="6">Assinatura de Jornal</option> <option value="PIX">PIX</option>
<option value="7">Campanha Financeira</option> <option value="TRANSFERENCIA">Transferência/DOC</option>
<option value="8">Outros</option> <option value="DEPOSITO">Depósito</option>
{% endif %} <option value="MAQUININHA">Maquininha</option>
</select> </select>
</div> </div>
<div class="mb-3"> <div class="col-md-6">
<label for="valor" class="form-label">Valor:</label> <label for="campanha_id" class="form-label">Campanha</label>
<input type="number" step="0.01" class="form-control" id="valor" name="valor" required> <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"> </div>
<label for="dataComprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="dataComprovante" name="data_comprovante" required> <!-- 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>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Salvar</button> <button type="button" class="btn btn-primary" id="salvarComprovante">Salvar</button>
</div>
</form>
</div> </div>
</div> </div>
</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 --> <!-- Modal Editar Comprovante -->
<div class="modal fade" id="modalEditarComprovante" tabindex="-1"> <div class="modal fade" id="modalEditarComprovante" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">

View File

@@ -47,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.

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>

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